package openstack

import (
	"bytes"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects"
	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
	"github.com/mitchellh/go-homedir"
)

func resourceObjectStorageObjectV1() *schema.Resource {
	return &schema.Resource{
		Create: resourceObjectStorageObjectV1Create,
		Read:   resourceObjectStorageObjectV1Read,
		Update: resourceObjectStorageObjectV1Update,
		Delete: resourceObjectStorageObjectV1Delete,

		Schema: map[string]*schema.Schema{
			"region": {
				Type:     schema.TypeString,
				Optional: true,
				Computed: true,
				ForceNew: true,
			},

			"container_name": {
				Type:     schema.TypeString,
				Required: true,
				ForceNew: true,
			},

			"name": {
				Type:     schema.TypeString,
				Required: true,
				ForceNew: true,
			},

			"content_disposition": {
				Type:          schema.TypeString,
				Optional:      true,
				Computed:      true,
				ConflictsWith: []string{"copy_from"},
			},

			"content_encoding": {
				Type:          schema.TypeString,
				Optional:      true,
				Computed:      true,
				ConflictsWith: []string{"copy_from"},
			},

			"content_type": {
				Type:          schema.TypeString,
				Optional:      true,
				Computed:      true,
				ConflictsWith: []string{"copy_from"},
			},

			"content": {
				Type:          schema.TypeString,
				Optional:      true,
				ConflictsWith: []string{"source", "copy_from", "object_manifest"},
			},

			"copy_from": {
				Type:          schema.TypeString,
				Optional:      true,
				ConflictsWith: []string{"content", "source", "object_manifest"},
			},

			"delete_after": {
				Type:     schema.TypeInt,
				Optional: true,
			},

			"delete_at": {
				Type:             schema.TypeString,
				Computed:         true,
				Optional:         true,
				DiffSuppressFunc: suppressEquivilentTimeDiffs,
			},

			"detect_content_type": {
				Type:     schema.TypeBool,
				Optional: true,
				Default:  false,
			},

			// this attribute is used to trigger resource updates
			// if the file content is changed
			"etag": {
				Type:     schema.TypeString,
				Optional: true,
				Computed: true,
			},

			"metadata": {
				Type:     schema.TypeMap,
				Optional: true,
			},

			"object_manifest": {
				Type:          schema.TypeString,
				Computed:      true,
				Optional:      true,
				ConflictsWith: []string{"copy_from", "source", "content"},
			},

			"source": {
				Type:          schema.TypeString,
				Optional:      true,
				ConflictsWith: []string{"content", "copy_from", "object_manifest"},
			},

			// Read Only
			"content_length": {
				Type:     schema.TypeInt,
				Computed: true,
			},

			"date": {
				Type:     schema.TypeString,
				Computed: true,
			},

			"last_modified": {
				Type:     schema.TypeString,
				Computed: true,
			},

			"trans_id": {
				Type:     schema.TypeString,
				Computed: true,
			},
		},
	}
}

func resourceObjectStorageObjectV1Create(d *schema.ResourceData, meta interface{}) error {
	config := meta.(*Config)
	objectStorageClient, err := config.ObjectStorageV1Client(GetRegion(d, config))
	if err != nil {
		return fmt.Errorf("Error creating OpenStack object storage client: %s", err)
	}

	name := d.Get("name").(string)
	cn := d.Get("container_name").(string)

	createOpts := &objects.CreateOpts{
		Metadata:         resourceObjectMetadataV1(d),
		TransferEncoding: "chunked",
	}

	var isValid bool
	if v, ok := d.GetOk("source"); ok {
		isValid = true
		file, size, err := resourceObjectSourceV1(v.(string))
		if err != nil {
			return err
		}

		createOpts.Content = file
		createOpts.ContentLength = size
		defer file.Close()
	}

	if v, ok := d.GetOk("content"); ok {
		isValid = true
		content := v.(string)
		createOpts.Content = bytes.NewReader([]byte(content))
		createOpts.ContentLength = int64(len(content))
	}

	if v, ok := d.GetOk("copy_from"); ok {
		isValid = true
		createOpts.CopyFrom = v.(string)
		createOpts.Content = bytes.NewReader([]byte(""))
	}

	if v, ok := d.GetOk("object_manifest"); ok {
		isValid = true
		createOpts.Content = bytes.NewReader([]byte(""))
		createOpts.ObjectManifest = v.(string)
	}

	if !isValid {
		return fmt.Errorf("Must specify \"source\", \"content\", \"copy_from\" or \"object_manifest\" field")
	}

	if v, ok := d.GetOk("content_disposition"); ok {
		createOpts.ContentDisposition = v.(string)
	}

	if v, ok := d.GetOk("content_encoding"); ok {
		createOpts.ContentEncoding = v.(string)
	}

	if v, ok := d.GetOk("content_type"); ok {
		createOpts.ContentType = v.(string)
	}

	if v, ok := d.GetOk("delete_after"); ok {
		createOpts.DeleteAfter = v.(int)
	}

	if v, ok := d.GetOk("delete_at"); ok && v != "" {
		t, err := time.Parse(time.RFC3339, fmt.Sprintf("%s", v))
		if err != nil {
			return fmt.Errorf("Error Parsing Swift Object Lifecycle Expiration Date: %s, %s", err.Error(), v)
		}

		createOpts.DeleteAt = int(t.Unix())
	}

	if v, ok := d.GetOk("detect_content_type"); ok && v.(bool) {
		createOpts.DetectContentType = "true"
	}

	if v, ok := d.GetOk("etag"); ok {
		createOpts.ETag = v.(string)
	}

	log.Printf("[DEBUG] Create Options: %#v", createOpts)
	_, err = objects.Create(objectStorageClient, cn, name, createOpts).Extract()
	if err != nil {
		return fmt.Errorf("Error creating OpenStack container object: %s", err)
	}

	// Store the ID now
	d.SetId(fmt.Sprintf("%s/%s", cn, name))

	return resourceObjectStorageObjectV1Read(d, meta)
}

func resourceObjectStorageObjectV1Read(d *schema.ResourceData, meta interface{}) error {
	config := meta.(*Config)
	objectStorageClient, err := config.ObjectStorageV1Client(GetRegion(d, config))
	if err != nil {
		return fmt.Errorf("Error creating OpenStack object storage client: %s", err)
	}

	name := d.Get("name").(string)
	cn := d.Get("container_name").(string)

	getOpts := &objects.GetOpts{}

	if v, ok := d.GetOk("tmp_url_sig"); ok {
		getOpts.Signature = v.(string)
	}
	if v, ok := d.GetOk("tmp_url_expires"); ok {
		getOpts.Expires = v.(string)
	}

	log.Printf("[DEBUG] Get Options: %#v", getOpts)
	result, err := objects.Get(objectStorageClient, cn, name, getOpts).Extract()
	if err != nil {
		return fmt.Errorf("Error getting OpenStack container object: %s", err)
	}

	log.Printf("[DEBUG] Retrieved OpenStack Object Storage Object: %#v", result)

	d.Set("etag", result.ETag)
	d.Set("content_disposition", result.ContentDisposition)
	d.Set("content_encoding", result.ContentEncoding)
	d.Set("content_length", result.ContentLength)
	d.Set("content_type", result.ContentType)
	if result.Date.Unix() > 0 {
		d.Set("date", result.Date.Format(time.RFC3339))
	}
	if result.DeleteAt.Unix() > 0 {
		d.Set("delete_at", result.DeleteAt.Format(time.RFC3339))
	}
	if result.LastModified.Unix() > 0 {
		d.Set("last_modified", result.LastModified.Format(time.RFC3339))
	}
	d.Set("object_manifest", result.ObjectManifest)
	d.Set("trans_id", result.TransID)

	return nil
}

func resourceObjectStorageObjectV1Update(d *schema.ResourceData, meta interface{}) error {
	config := meta.(*Config)
	objectStorageClient, err := config.ObjectStorageV1Client(GetRegion(d, config))
	if err != nil {
		return fmt.Errorf("Error creating OpenStack object storage client: %s", err)
	}

	name := d.Get("name").(string)
	cn := d.Get("container_name").(string)

	// This is not a typo. Reusing CreateOpts for the update.
	createOpts := &objects.CreateOpts{
		NoETag:           true,
		TransferEncoding: "chunked",
	}

	if d.HasChange("metadata") {
		createOpts.Metadata = resourceObjectMetadataV1(d)
	}

	if d.HasChange("source") {
		v := d.Get("source").(string)
		file, size, err := resourceObjectSourceV1(v)
		if err != nil {
			return err
		}

		createOpts.Content = file
		createOpts.ContentLength = size
		defer file.Close()
	}

	if d.HasChange("content") {
		v := d.Get("content").(string)
		createOpts.Content = bytes.NewReader([]byte(v))
		createOpts.ContentLength = int64(len(v))
	}

	if d.HasChange("copy_from") {
		v := d.Get("copy_from").(string)
		createOpts.CopyFrom = v
		createOpts.Content = bytes.NewReader([]byte(""))
	}

	if d.HasChange("object_manifest") {
		v := d.Get("object_manifest").(string)
		createOpts.ObjectManifest = v
		createOpts.Content = bytes.NewReader([]byte(""))
	}

	if v, ok := d.GetOk("content_disposition"); ok {
		createOpts.ContentDisposition = v.(string)
	}

	if v, ok := d.GetOk("content_encoding"); ok {
		createOpts.ContentEncoding = v.(string)
	}

	if v, ok := d.GetOk("content_type"); ok {
		createOpts.ContentType = v.(string)
	}

	if v, ok := d.GetOk("delete_after"); ok {
		createOpts.DeleteAfter = v.(int)
	}

	if v, ok := d.GetOk("delete_at"); ok && v != "" {
		t, err := time.Parse(time.RFC3339, fmt.Sprintf("%s", v))
		if err != nil {
			return fmt.Errorf("Error Parsing Swift Object Lifecycle Expiration Date: %s, %s", err.Error(), v)
		}

		createOpts.DeleteAt = int(t.Unix())
	}

	if v, ok := d.GetOk("detect_content_type"); ok && v.(bool) {
		createOpts.DetectContentType = "true"
	}

	if d.HasChange("etag") {
		createOpts.NoETag = false
		createOpts.ETag = d.Get("etag").(string)
	}

	log.Printf("[DEBUG] Update Options: %#v", createOpts)
	_, err = objects.Create(objectStorageClient, cn, name, createOpts).Extract()
	if err != nil {
		return fmt.Errorf("Error updating OpenStack container object: %s", err)
	}

	return resourceObjectStorageObjectV1Read(d, meta)
}

func resourceObjectStorageObjectV1Delete(d *schema.ResourceData, meta interface{}) error {
	config := meta.(*Config)
	objectStorageClient, err := config.ObjectStorageV1Client(GetRegion(d, config))
	if err != nil {
		return fmt.Errorf("Error creating OpenStack object storage client: %s", err)
	}

	name := d.Get("name").(string)
	cn := d.Get("container_name").(string)
	deleteOpts := &objects.DeleteOpts{}

	_, err = objects.Delete(objectStorageClient, cn, name, deleteOpts).Extract()
	if err != nil {
		return fmt.Errorf("Error getting OpenStack container object: %s", err)
	}
	return nil
}

func resourceObjectMetadataV1(d *schema.ResourceData) map[string]string {
	m := make(map[string]string)
	for key, val := range d.Get("metadata").(map[string]interface{}) {
		m[key] = val.(string)
	}
	return m
}

func resourceObjectSourceV1(source string) (*os.File, int64, error) {
	path, err := homedir.Expand(source)
	if err != nil {
		return nil, 0, fmt.Errorf("Error expanding homedir in source (%s): %s", source, err)
	}

	file, err := os.Open(path)
	if err != nil {
		return nil, 0, fmt.Errorf("Error opening openstack swift object source (%s): %s", source, err)
	}

	fileinfo, err := file.Stat()
	if err != nil {
		return nil, 0, fmt.Errorf("Error opening openstack swift object source (%s): %s", source, err)
	}

	return file, fileinfo.Size(), nil
}
