package schema

import (
	"fmt"
	"log"
	"time"

	"github.com/mitchellh/copystructure"

	"github.com/hashicorp/terraform-plugin-sdk/v2/internal/configs/hcl2shim"
	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)

const TimeoutKey = "e2bfb730-ecaa-11e6-8f88-34363bc7c4c0"
const TimeoutsConfigKey = "timeouts"

const (
	TimeoutCreate  = "create"
	TimeoutRead    = "read"
	TimeoutUpdate  = "update"
	TimeoutDelete  = "delete"
	TimeoutDefault = "default"
)

func timeoutKeys() []string {
	return []string{
		TimeoutCreate,
		TimeoutRead,
		TimeoutUpdate,
		TimeoutDelete,
		TimeoutDefault,
	}
}

// could be time.Duration, int64 or float64
func DefaultTimeout(tx interface{}) *time.Duration {
	var td time.Duration
	switch raw := tx.(type) {
	case time.Duration:
		return &raw
	case int64:
		td = time.Duration(raw)
	case float64:
		td = time.Duration(int64(raw))
	default:
		log.Printf("[WARN] Unknown type in DefaultTimeout: %#v", tx)
	}
	return &td
}

type ResourceTimeout struct {
	Create, Read, Update, Delete, Default *time.Duration
}

// ConfigDecode takes a schema and the configuration (available in Diff) and
// validates, parses the timeouts into `t`
func (t *ResourceTimeout) ConfigDecode(s *Resource, c *terraform.ResourceConfig) error {
	if s.Timeouts != nil {
		raw, err := copystructure.Copy(s.Timeouts)
		if err != nil {
			log.Printf("[DEBUG] Error with deep copy: %s", err)
		}
		*t = *raw.(*ResourceTimeout)
	}

	if raw, ok := c.Config[TimeoutsConfigKey]; ok {
		var rawTimeouts []map[string]interface{}
		switch raw := raw.(type) {
		case map[string]interface{}:
			rawTimeouts = append(rawTimeouts, raw)
		case []map[string]interface{}:
			rawTimeouts = raw
		case string:
			if raw == hcl2shim.UnknownVariableValue {
				// Timeout is not defined in the config
				// Defaults will be used instead
				return nil
			} else {
				log.Printf("[ERROR] Invalid timeout value: %q", raw)
				return fmt.Errorf("Invalid Timeout value found")
			}
		case []interface{}:
			for _, r := range raw {
				if rMap, ok := r.(map[string]interface{}); ok {
					rawTimeouts = append(rawTimeouts, rMap)
				} else {
					// Go will not allow a fallthrough
					log.Printf("[ERROR] Invalid timeout structure: %#v", raw)
					return fmt.Errorf("Invalid Timeout structure found")
				}
			}
		default:
			log.Printf("[ERROR] Invalid timeout structure: %#v", raw)
			return fmt.Errorf("Invalid Timeout structure found")
		}

		for _, timeoutValues := range rawTimeouts {
			for timeKey, timeValue := range timeoutValues {
				// validate that we're dealing with the normal CRUD actions
				var found bool
				for _, key := range timeoutKeys() {
					if timeKey == key {
						found = true
						break
					}
				}

				if !found {
					return fmt.Errorf("Unsupported Timeout configuration key found (%s)", timeKey)
				}

				// Get timeout
				rt, err := time.ParseDuration(timeValue.(string))
				if err != nil {
					return fmt.Errorf("Error parsing %q timeout: %s", timeKey, err)
				}

				var timeout *time.Duration
				switch timeKey {
				case TimeoutCreate:
					timeout = t.Create
				case TimeoutUpdate:
					timeout = t.Update
				case TimeoutRead:
					timeout = t.Read
				case TimeoutDelete:
					timeout = t.Delete
				case TimeoutDefault:
					timeout = t.Default
				}

				// If the resource has not delcared this in the definition, then error
				// with an unsupported message
				if timeout == nil {
					return unsupportedTimeoutKeyError(timeKey)
				}

				*timeout = rt
			}

			// This early return, which makes this function handle a single
			// timeout configuration block, should likely not be here but the
			// SDK has never raised an error for multiple blocks nor made any
			// precedence decisions for them in the past.
			// It is left here for compatibility reasons.
			return nil //nolint:staticcheck
		}
	}

	return nil
}

func unsupportedTimeoutKeyError(key string) error {
	return fmt.Errorf("Timeout Key (%s) is not supported", key)
}

// DiffEncode, StateEncode, and MetaDecode are analogous to the Go stdlib JSONEncoder
// interface: they encode/decode a timeouts struct from an instance diff, which is
// where the timeout data is stored after a diff to pass into Apply.
//
// StateEncode encodes the timeout into the ResourceData's InstanceState for
// saving to state
func (t *ResourceTimeout) DiffEncode(id *terraform.InstanceDiff) error {
	return t.metaEncode(id)
}

func (t *ResourceTimeout) StateEncode(is *terraform.InstanceState) error {
	return t.metaEncode(is)
}

// metaEncode encodes the ResourceTimeout into a map[string]interface{} format
// and stores it in the Meta field of the interface it's given.
// Assumes the interface is either *terraform.InstanceState or
// *terraform.InstanceDiff, returns an error otherwise
func (t *ResourceTimeout) metaEncode(ids interface{}) error {
	m := make(map[string]interface{})

	if t.Create != nil {
		m[TimeoutCreate] = t.Create.Nanoseconds()
	}
	if t.Read != nil {
		m[TimeoutRead] = t.Read.Nanoseconds()
	}
	if t.Update != nil {
		m[TimeoutUpdate] = t.Update.Nanoseconds()
	}
	if t.Delete != nil {
		m[TimeoutDelete] = t.Delete.Nanoseconds()
	}
	if t.Default != nil {
		m[TimeoutDefault] = t.Default.Nanoseconds()
		// for any key above that is nil, if default is specified, we need to
		// populate it with the default
		for _, k := range timeoutKeys() {
			if _, ok := m[k]; !ok {
				m[k] = t.Default.Nanoseconds()
			}
		}
	}

	// only add the Timeout to the Meta if we have values
	if len(m) > 0 {
		switch instance := ids.(type) {
		case *terraform.InstanceDiff:
			if instance.Meta == nil {
				instance.Meta = make(map[string]interface{})
			}
			instance.Meta[TimeoutKey] = m
		case *terraform.InstanceState:
			if instance.Meta == nil {
				instance.Meta = make(map[string]interface{})
			}
			instance.Meta[TimeoutKey] = m
		default:
			return fmt.Errorf("Error matching type for Diff Encode")
		}
	}

	return nil
}

func (t *ResourceTimeout) StateDecode(id *terraform.InstanceState) error {
	return t.metaDecode(id)
}
func (t *ResourceTimeout) DiffDecode(is *terraform.InstanceDiff) error {
	return t.metaDecode(is)
}

func (t *ResourceTimeout) metaDecode(ids interface{}) error {
	var rawMeta interface{}
	var ok bool
	switch rawInstance := ids.(type) {
	case *terraform.InstanceDiff:
		rawMeta, ok = rawInstance.Meta[TimeoutKey]
		if !ok {
			return nil
		}
	case *terraform.InstanceState:
		rawMeta, ok = rawInstance.Meta[TimeoutKey]
		if !ok {
			return nil
		}
	default:
		return fmt.Errorf("Unknown or unsupported type in metaDecode: %#v", ids)
	}

	times := rawMeta.(map[string]interface{})
	if len(times) == 0 {
		return nil
	}

	if v, ok := times[TimeoutCreate]; ok {
		t.Create = DefaultTimeout(v)
	}
	if v, ok := times[TimeoutRead]; ok {
		t.Read = DefaultTimeout(v)
	}
	if v, ok := times[TimeoutUpdate]; ok {
		t.Update = DefaultTimeout(v)
	}
	if v, ok := times[TimeoutDelete]; ok {
		t.Delete = DefaultTimeout(v)
	}
	if v, ok := times[TimeoutDefault]; ok {
		t.Default = DefaultTimeout(v)
	}

	return nil
}
