package states

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"sort"
	"strings"

	ctyjson "github.com/zclconf/go-cty/cty/json"

	"github.com/camptocamp/terraboard/internal/terraform/addrs"
	"github.com/camptocamp/terraboard/internal/terraform/configs/hcl2shim"
)

// String returns a rather-odd string representation of the entire state.
//
// This is intended to match the behavior of the older terraform.State.String
// method that is used in lots of existing tests. It should not be used in
// new tests: instead, use "cmp" to directly compare the state data structures
// and print out a diff if they do not match.
//
// This method should never be used in non-test code, whether directly by call
// or indirectly via a %s or %q verb in package fmt.
func (s *State) String() string {
	if s == nil {
		return "<nil>"
	}

	// sort the modules by name for consistent output
	modules := make([]string, 0, len(s.Modules))
	for m := range s.Modules {
		modules = append(modules, m)
	}
	sort.Strings(modules)

	var buf bytes.Buffer
	for _, name := range modules {
		m := s.Modules[name]
		mStr := m.testString()

		// If we're the root module, we just write the output directly.
		if m.Addr.IsRoot() {
			buf.WriteString(mStr + "\n")
			continue
		}

		// We need to build out a string that resembles the not-quite-standard
		// format that terraform.State.String used to use, where there's a
		// "module." prefix but then just a chain of all of the module names
		// without any further "module." portions.
		buf.WriteString("module")
		for _, step := range m.Addr {
			buf.WriteByte('.')
			buf.WriteString(step.Name)
			if step.InstanceKey != addrs.NoKey {
				buf.WriteString(step.InstanceKey.String())
			}
		}
		buf.WriteString(":\n")

		s := bufio.NewScanner(strings.NewReader(mStr))
		for s.Scan() {
			text := s.Text()
			if text != "" {
				text = "  " + text
			}

			buf.WriteString(fmt.Sprintf("%s\n", text))
		}
	}

	return strings.TrimSpace(buf.String())
}

// testString is used to produce part of the output of State.String. It should
// never be used directly.
func (ms *Module) testString() string {
	var buf bytes.Buffer

	if len(ms.Resources) == 0 {
		buf.WriteString("<no state>")
	}

	// We use AbsResourceInstance here, even though everything belongs to
	// the same module, just because we have a sorting behavior defined
	// for those but not for just ResourceInstance.
	addrsOrder := make([]addrs.AbsResourceInstance, 0, len(ms.Resources))
	for _, rs := range ms.Resources {
		for ik := range rs.Instances {
			addrsOrder = append(addrsOrder, rs.Addr.Instance(ik))
		}
	}

	sort.Slice(addrsOrder, func(i, j int) bool {
		return addrsOrder[i].Less(addrsOrder[j])
	})

	for _, fakeAbsAddr := range addrsOrder {
		addr := fakeAbsAddr.Resource
		rs := ms.Resource(addr.ContainingResource())
		is := ms.ResourceInstance(addr)

		// Here we need to fake up a legacy-style address as the old state
		// types would've used, since that's what our tests against those
		// old types expect. The significant difference is that instancekey
		// is dot-separated rather than using index brackets.
		k := addr.ContainingResource().String()
		if addr.Key != addrs.NoKey {
			switch tk := addr.Key.(type) {
			case addrs.IntKey:
				k = fmt.Sprintf("%s.%d", k, tk)
			default:
				// No other key types existed for the legacy types, so we
				// can do whatever we want here. We'll just use our standard
				// syntax for these.
				k = k + tk.String()
			}
		}

		id := LegacyInstanceObjectID(is.Current)

		taintStr := ""
		if is.Current != nil && is.Current.Status == ObjectTainted {
			taintStr = " (tainted)"
		}

		deposedStr := ""
		if len(is.Deposed) > 0 {
			deposedStr = fmt.Sprintf(" (%d deposed)", len(is.Deposed))
		}

		buf.WriteString(fmt.Sprintf("%s:%s%s\n", k, taintStr, deposedStr))
		buf.WriteString(fmt.Sprintf("  ID = %s\n", id))
		buf.WriteString(fmt.Sprintf("  provider = %s\n", rs.ProviderConfig.String()))

		// Attributes were a flatmap before, but are not anymore. To preserve
		// our old output as closely as possible we need to do a conversion
		// to flatmap. Normally we'd want to do this with schema for
		// accuracy, but for our purposes here it only needs to be approximate.
		// This should produce an identical result for most cases, though
		// in particular will differ in a few cases:
		//  - The keys used for elements in a set will be different
		//  - Values for attributes of type cty.DynamicPseudoType will be
		//    misinterpreted (but these weren't possible in old world anyway)
		var attributes map[string]string
		if obj := is.Current; obj != nil {
			switch {
			case obj.AttrsFlat != nil:
				// Easy (but increasingly unlikely) case: the state hasn't
				// actually been upgraded to the new form yet.
				attributes = obj.AttrsFlat
			case obj.AttrsJSON != nil:
				ty, err := ctyjson.ImpliedType(obj.AttrsJSON)
				if err == nil {
					val, err := ctyjson.Unmarshal(obj.AttrsJSON, ty)
					if err == nil {
						attributes = hcl2shim.FlatmapValueFromHCL2(val)
					}
				}
			}
		}
		attrKeys := make([]string, 0, len(attributes))
		for ak, val := range attributes {
			if ak == "id" {
				continue
			}

			// don't show empty containers in the output
			if val == "0" && (strings.HasSuffix(ak, ".#") || strings.HasSuffix(ak, ".%")) {
				continue
			}

			attrKeys = append(attrKeys, ak)
		}

		sort.Strings(attrKeys)

		for _, ak := range attrKeys {
			av := attributes[ak]
			buf.WriteString(fmt.Sprintf("  %s = %s\n", ak, av))
		}

		// CAUTION: Since deposed keys are now random strings instead of
		// incrementing integers, this result will not be deterministic
		// if there is more than one deposed object.
		i := 1
		for _, t := range is.Deposed {
			id := LegacyInstanceObjectID(t)
			taintStr := ""
			if t.Status == ObjectTainted {
				taintStr = " (tainted)"
			}
			buf.WriteString(fmt.Sprintf("  Deposed ID %d = %s%s\n", i, id, taintStr))
			i++
		}

		if obj := is.Current; obj != nil && len(obj.Dependencies) > 0 {
			buf.WriteString("\n  Dependencies:\n")
			for _, dep := range obj.Dependencies {
				buf.WriteString(fmt.Sprintf("    %s\n", dep.String()))
			}
		}
	}

	if len(ms.OutputValues) > 0 {
		buf.WriteString("\nOutputs:\n\n")

		ks := make([]string, 0, len(ms.OutputValues))
		for k := range ms.OutputValues {
			ks = append(ks, k)
		}
		sort.Strings(ks)

		for _, k := range ks {
			v := ms.OutputValues[k]
			lv := hcl2shim.ConfigValueFromHCL2(v.Value)
			switch vTyped := lv.(type) {
			case string:
				buf.WriteString(fmt.Sprintf("%s = %s\n", k, vTyped))
			case []interface{}:
				buf.WriteString(fmt.Sprintf("%s = %s\n", k, vTyped))
			case map[string]interface{}:
				var mapKeys []string
				for key := range vTyped {
					mapKeys = append(mapKeys, key)
				}
				sort.Strings(mapKeys)

				var mapBuf bytes.Buffer
				mapBuf.WriteString("{")
				for _, key := range mapKeys {
					mapBuf.WriteString(fmt.Sprintf("%s:%s ", key, vTyped[key]))
				}
				mapBuf.WriteString("}")

				buf.WriteString(fmt.Sprintf("%s = %s\n", k, mapBuf.String()))
			default:
				buf.WriteString(fmt.Sprintf("%s = %#v\n", k, lv))
			}
		}
	}

	return buf.String()
}

// LegacyInstanceObjectID is a helper for extracting an object id value from
// an instance object in a way that approximates how we used to do this
// for the old state types. ID is no longer first-class, so this is preserved
// only for compatibility with old tests that include the id as part of their
// expected value.
func LegacyInstanceObjectID(obj *ResourceInstanceObjectSrc) string {
	if obj == nil {
		return "<not created>"
	}

	if obj.AttrsJSON != nil {
		type WithID struct {
			ID string `json:"id"`
		}
		var withID WithID
		err := json.Unmarshal(obj.AttrsJSON, &withID)
		if err == nil {
			return withID.ID
		}
	} else if obj.AttrsFlat != nil {
		if flatID, exists := obj.AttrsFlat["id"]; exists {
			return flatID
		}
	}

	// For resource types created after we removed id as special there may
	// not actually be one at all. This is okay because older tests won't
	// encounter this, and new tests shouldn't be using ids.
	return "<none>"
}
