package openstack

import (
	"fmt"
	"log"
	"strings"
	"time"

	"github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions"
	"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"

	"github.com/hashicorp/terraform/helper/resource"
	"github.com/hashicorp/terraform/helper/schema"
)

func resourceBlockStorageVolumeAttachV2() *schema.Resource {
	return &schema.Resource{
		Create: resourceBlockStorageVolumeAttachV2Create,
		Read:   resourceBlockStorageVolumeAttachV2Read,
		Delete: resourceBlockStorageVolumeAttachV2Delete,

		Timeouts: &schema.ResourceTimeout{
			Create: schema.DefaultTimeout(10 * time.Minute),
			Delete: schema.DefaultTimeout(10 * time.Minute),
		},

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

			"volume_id": &schema.Schema{
				Type:     schema.TypeString,
				Required: true,
				ForceNew: true,
			},

			"instance_id": &schema.Schema{
				Type:     schema.TypeString,
				Optional: true,
				ForceNew: true,
				Removed:  "instance_id is no longer used in this resource",
			},

			"host_name": &schema.Schema{
				Type:     schema.TypeString,
				Required: true,
				ForceNew: true,
			},

			"device": &schema.Schema{
				Type:     schema.TypeString,
				Optional: true,
				ForceNew: true,
			},

			"attach_mode": &schema.Schema{
				Type:     schema.TypeString,
				Optional: true,
				ForceNew: true,
				ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
					value := v.(string)
					if value != "ro" && value != "rw" {
						errors = append(errors, fmt.Errorf(
							"Only 'ro' and 'rw' are supported values for 'attach_mode'"))
					}
					return
				},
			},

			"initiator": &schema.Schema{
				Type:     schema.TypeString,
				Optional: true,
				ForceNew: true,
			},

			"ip_address": &schema.Schema{
				Type:     schema.TypeString,
				Optional: true,
				ForceNew: true,
			},

			"multipath": &schema.Schema{
				Type:     schema.TypeBool,
				Optional: true,
				ForceNew: true,
			},

			"os_type": &schema.Schema{
				Type:     schema.TypeString,
				Optional: true,
				ForceNew: true,
			},

			"platform": &schema.Schema{
				Type:     schema.TypeString,
				Optional: true,
				ForceNew: true,
			},

			"wwpn": &schema.Schema{
				Type:     schema.TypeList,
				Optional: true,
				ForceNew: true,
				Elem:     &schema.Schema{Type: schema.TypeString},
			},

			"wwnn": &schema.Schema{
				Type:     schema.TypeString,
				Optional: true,
				ForceNew: true,
			},

			// Volume attachment information
			"data": &schema.Schema{
				Type:      schema.TypeMap,
				Computed:  true,
				Sensitive: true,
			},

			"driver_volume_type": &schema.Schema{
				Type:     schema.TypeString,
				Computed: true,
			},

			"mount_point_base": &schema.Schema{
				Type:     schema.TypeString,
				Computed: true,
			},
		},
	}
}

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

	// initialize the connection
	volumeId := d.Get("volume_id").(string)
	connOpts := &volumeactions.InitializeConnectionOpts{}
	if v, ok := d.GetOk("host_name"); ok {
		connOpts.Host = v.(string)
	}

	if v, ok := d.GetOk("multipath"); ok {
		multipath := v.(bool)
		connOpts.Multipath = &multipath
	}

	if v, ok := d.GetOk("ip_address"); ok {
		connOpts.IP = v.(string)
	}

	if v, ok := d.GetOk("initiator"); ok {
		connOpts.Initiator = v.(string)
	}

	if v, ok := d.GetOk("os_type"); ok {
		connOpts.OSType = v.(string)
	}

	if v, ok := d.GetOk("platform"); ok {
		connOpts.Platform = v.(string)
	}

	if v, ok := d.GetOk("wwnns"); ok {
		connOpts.Wwnns = v.(string)
	}

	if v, ok := d.GetOk("wwpns"); ok {
		var wwpns []string
		for _, i := range v.([]string) {
			wwpns = append(wwpns, i)
		}

		connOpts.Wwpns = wwpns
	}

	connInfo, err := volumeactions.InitializeConnection(client, volumeId, connOpts).Extract()
	if err != nil {
		return fmt.Errorf("Unable to create connection: %s", err)
	}

	// Only uncomment this when debugging since connInfo contains sensitive information.
	// log.Printf("[DEBUG] Volume Connection for %s: %#v", volumeId, connInfo)

	// Because this information is only returned upon creation,
	// it must be set in Create.
	if v, ok := connInfo["data"]; ok {
		data := make(map[string]string)
		for key, value := range v.(map[string]interface{}) {
			if v, ok := value.(string); ok {
				data[key] = v
			}
		}

		d.Set("data", data)
	}

	if v, ok := connInfo["driver_volume_type"]; ok {
		d.Set("driver_volume_type", v)
	}

	if v, ok := connInfo["mount_point_base"]; ok {
		d.Set("mount_point_base", v)
	}

	// Once the connection has been made, tell Cinder to mark the volume as attached.
	attachMode, err := blockStorageVolumeAttachV2AttachMode(d.Get("attach_mode").(string))
	if err != nil {
		return nil
	}

	attachOpts := &volumeactions.AttachOpts{
		HostName:   d.Get("host_name").(string),
		MountPoint: d.Get("device").(string),
		Mode:       attachMode,
	}

	log.Printf("[DEBUG] Attachment Options: %#v", attachOpts)

	if err := volumeactions.Attach(client, volumeId, attachOpts).ExtractErr(); err != nil {
		return err
	}

	// Wait for the volume to become available.
	log.Printf("[DEBUG] Waiting for volume (%s) to become available", volumeId)

	stateConf := &resource.StateChangeConf{
		Pending:    []string{"available", "attaching"},
		Target:     []string{"in-use"},
		Refresh:    VolumeV2StateRefreshFunc(client, volumeId),
		Timeout:    d.Timeout(schema.TimeoutCreate),
		Delay:      10 * time.Second,
		MinTimeout: 3 * time.Second,
	}

	_, err = stateConf.WaitForState()
	if err != nil {
		return fmt.Errorf("Error waiting for volume (%s) to become ready: %s", volumeId, err)
	}

	// Once the volume has been marked as attached,
	// retrieve a fresh copy of it with all information now available.
	volume, err := volumes.Get(client, volumeId).Extract()
	if err != nil {
		return err
	}

	// Search for the attachmentId
	var attachmentId string
	hostName := d.Get("host_name").(string)
	for _, attachment := range volume.Attachments {
		if hostName != "" && hostName == attachment.HostName {
			attachmentId = attachment.AttachmentID
		}
	}

	if attachmentId == "" {
		return fmt.Errorf("Unable to determine attachment ID.")
	}

	// The ID must be a combination of the volume and attachment ID
	// since a volume ID is required to retrieve an attachment ID.
	id := fmt.Sprintf("%s/%s", volumeId, attachmentId)
	d.SetId(id)

	return resourceBlockStorageVolumeAttachV2Read(d, meta)
}

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

	volumeId, attachmentId, err := blockStorageVolumeAttachV2ParseId(d.Id())
	if err != nil {
		return err
	}

	volume, err := volumes.Get(client, volumeId).Extract()
	if err != nil {
		return err
	}

	log.Printf("[DEBUG] Retrieved volume %s: %#v", d.Id(), volume)

	var attachment volumes.Attachment
	for _, v := range volume.Attachments {
		if attachmentId == v.AttachmentID {
			attachment = v
		}
	}

	log.Printf("[DEBUG] Retrieved volume attachment: %#v", attachment)

	return nil
}

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

	volumeId, attachmentId, err := blockStorageVolumeAttachV2ParseId(d.Id())

	// Terminate the connection
	termOpts := &volumeactions.TerminateConnectionOpts{}
	if v, ok := d.GetOk("host_name"); ok {
		termOpts.Host = v.(string)
	}

	if v, ok := d.GetOk("multipath"); ok {
		multipath := v.(bool)
		termOpts.Multipath = &multipath
	}

	if v, ok := d.GetOk("ip_address"); ok {
		termOpts.IP = v.(string)
	}

	if v, ok := d.GetOk("initiator"); ok {
		termOpts.Initiator = v.(string)
	}

	if v, ok := d.GetOk("os_type"); ok {
		termOpts.OSType = v.(string)
	}

	if v, ok := d.GetOk("platform"); ok {
		termOpts.Platform = v.(string)
	}

	if v, ok := d.GetOk("wwnns"); ok {
		termOpts.Wwnns = v.(string)
	}

	if v, ok := d.GetOk("wwpns"); ok {
		var wwpns []string
		for _, i := range v.([]string) {
			wwpns = append(wwpns, i)
		}

		termOpts.Wwpns = wwpns
	}

	err = volumeactions.TerminateConnection(client, volumeId, termOpts).ExtractErr()
	if err != nil {
		return fmt.Errorf("Error terminating volume connection %s: %s", volumeId, err)
	}

	// Detach the volume
	detachOpts := volumeactions.DetachOpts{
		AttachmentID: attachmentId,
	}

	log.Printf("[DEBUG] Detachment Options: %#v", detachOpts)

	if err := volumeactions.Detach(client, volumeId, detachOpts).ExtractErr(); err != nil {
		return err
	}

	stateConf := &resource.StateChangeConf{
		Pending:    []string{"in-use", "attaching", "detaching"},
		Target:     []string{"available"},
		Refresh:    VolumeV2StateRefreshFunc(client, volumeId),
		Timeout:    d.Timeout(schema.TimeoutDelete),
		Delay:      10 * time.Second,
		MinTimeout: 3 * time.Second,
	}

	_, err = stateConf.WaitForState()
	if err != nil {
		return fmt.Errorf("Error waiting for volume (%s) to become available: %s", volumeId, err)
	}

	return nil
}

func blockStorageVolumeAttachV2AttachMode(v string) (volumeactions.AttachMode, error) {
	var attachMode volumeactions.AttachMode
	var attachError error
	switch v {
	case "":
		attachMode = ""
	case "ro":
		attachMode = volumeactions.ReadOnly
	case "rw":
		attachMode = volumeactions.ReadWrite
	default:
		attachError = fmt.Errorf("Invalid attach_mode specified")
	}

	return attachMode, attachError
}

func blockStorageVolumeAttachV2ParseId(id string) (string, string, error) {
	parts := strings.Split(id, "/")
	if len(parts) < 2 {
		return "", "", fmt.Errorf("Unable to determine attachment ID")
	}

	return parts[0], parts[1], nil
}
