package resource

import (
	"encoding/json"
	"fmt"

	"github.com/hashicorp/terraform/addrs"
	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/configs/hcl2shim"
	"github.com/hashicorp/terraform/helper/schema"

	"github.com/hashicorp/terraform/states"
	"github.com/hashicorp/terraform/terraform"
)

// shimState takes a new *states.State and reverts it to a legacy state for the provider ACC tests
func shimNewState(newState *states.State, providers map[string]terraform.ResourceProvider) (*terraform.State, error) {
	state := terraform.NewState()

	// in the odd case of a nil state, let the helper packages handle it
	if newState == nil {
		return nil, nil
	}

	for _, newMod := range newState.Modules {
		mod := state.AddModule(newMod.Addr)

		for name, out := range newMod.OutputValues {
			outputType := ""
			val := hcl2shim.ConfigValueFromHCL2(out.Value)
			ty := out.Value.Type()
			switch {
			case ty == cty.String:
				outputType = "string"
			case ty.IsTupleType() || ty.IsListType():
				outputType = "list"
			case ty.IsMapType():
				outputType = "map"
			}

			mod.Outputs[name] = &terraform.OutputState{
				Type:      outputType,
				Value:     val,
				Sensitive: out.Sensitive,
			}
		}

		for _, res := range newMod.Resources {
			resType := res.Addr.Resource.Type
			providerType := res.ProviderConfig.Provider.Type

			resource := getResource(providers, providerType, res.Addr.Resource)

			for key, i := range res.Instances {
				resState := &terraform.ResourceState{
					Type:     resType,
					Provider: legacyProviderConfigString(res.ProviderConfig),
				}

				// We should always have a Current instance here, but be safe about checking.
				if i.Current != nil {
					flatmap, err := shimmedAttributes(i.Current, resource)
					if err != nil {
						return nil, fmt.Errorf("error decoding state for %q: %s", resType, err)
					}

					var meta map[string]interface{}
					if i.Current.Private != nil {
						err := json.Unmarshal(i.Current.Private, &meta)
						if err != nil {
							return nil, err
						}
					}

					resState.Primary = &terraform.InstanceState{
						ID:         flatmap["id"],
						Attributes: flatmap,
						Tainted:    i.Current.Status == states.ObjectTainted,
						Meta:       meta,
					}

					if i.Current.SchemaVersion != 0 {
						if resState.Primary.Meta == nil {
							resState.Primary.Meta = map[string]interface{}{}
						}
						resState.Primary.Meta["schema_version"] = i.Current.SchemaVersion
					}

					// convert the indexes to the old style flapmap indexes
					idx := ""
					switch key.(type) {
					case addrs.IntKey:
						// don't add numeric index values to resources with a count of 0
						if len(res.Instances) > 1 {
							idx = fmt.Sprintf(".%d", key)
						}
					case addrs.StringKey:
						idx = "." + key.String()
					}

					mod.Resources[res.Addr.Resource.String()+idx] = resState
				}

				// add any deposed instances
				for _, dep := range i.Deposed {
					flatmap, err := shimmedAttributes(dep, resource)
					if err != nil {
						return nil, fmt.Errorf("error decoding deposed state for %q: %s", resType, err)
					}

					var meta map[string]interface{}
					if dep.Private != nil {
						err := json.Unmarshal(dep.Private, &meta)
						if err != nil {
							return nil, err
						}
					}

					deposed := &terraform.InstanceState{
						ID:         flatmap["id"],
						Attributes: flatmap,
						Tainted:    dep.Status == states.ObjectTainted,
						Meta:       meta,
					}
					if dep.SchemaVersion != 0 {
						deposed.Meta = map[string]interface{}{
							"schema_version": dep.SchemaVersion,
						}
					}

					resState.Deposed = append(resState.Deposed, deposed)
				}
			}
		}
	}

	return state, nil
}

func getResource(providers map[string]terraform.ResourceProvider, providerName string, addr addrs.Resource) *schema.Resource {
	p := providers[providerName]
	if p == nil {
		panic(fmt.Sprintf("provider %q not found in test step", providerName))
	}

	// this is only for tests, so should only see schema.Providers
	provider := p.(*schema.Provider)

	switch addr.Mode {
	case addrs.ManagedResourceMode:
		resource := provider.ResourcesMap[addr.Type]
		if resource != nil {
			return resource
		}
	case addrs.DataResourceMode:
		resource := provider.DataSourcesMap[addr.Type]
		if resource != nil {
			return resource
		}
	}

	panic(fmt.Sprintf("resource %s not found in test step", addr.Type))
}

func shimmedAttributes(instance *states.ResourceInstanceObjectSrc, res *schema.Resource) (map[string]string, error) {
	flatmap := instance.AttrsFlat
	if flatmap != nil {
		return flatmap, nil
	}

	// if we have json attrs, they need to be decoded
	rio, err := instance.Decode(res.CoreConfigSchema().ImpliedType())
	if err != nil {
		return nil, err
	}

	instanceState, err := res.ShimInstanceStateFromValue(rio.Value)
	if err != nil {
		return nil, err
	}

	return instanceState.Attributes, nil
}

func shimLegacyState(legacy *terraform.State) (*states.State, error) {
	state, err := terraform.ShimLegacyState(legacy)
	if err != nil {
		return nil, err
	}

	if state.HasResources() {
		for _, module := range state.Modules {
			for name, resource := range module.Resources {
				module.Resources[name].ProviderConfig.Provider = addrs.ImpliedProviderForUnqualifiedType(resource.Addr.Resource.ImpliedProvider())
			}
		}
	}
	return state, err
}

// legacyProviderConfigString was copied from addrs.Provider.LegacyString() to
// create a legacy-style string from a non-legacy provider. This is only
// necessary as this package shims back and forth between legacy and modern
// state, neither of which encode the addrs.Provider for a resource.
func legacyProviderConfigString(pc addrs.AbsProviderConfig) string {
	if pc.Alias != "" {
		if len(pc.Module) == 0 {
			return fmt.Sprintf("%s.%s.%s", "provider", pc.Provider.Type, pc.Alias)
		} else {
			return fmt.Sprintf("%s.%s.%s.%s", pc.Module.String(), "provider", pc.Provider.LegacyString(), pc.Alias)
		}
	}
	if len(pc.Module) == 0 {
		return fmt.Sprintf("%s.%s", "provider", pc.Provider.Type)
	}
	return fmt.Sprintf("%s.%s.%s", pc.Module.String(), "provider", pc.Provider.Type)
}
