package generator

import (
	"fmt"
	"strings"

	"slices"

	tfjson "github.com/hashicorp/terraform-json"
	"github.com/iancoleman/strcase"
	"github.com/zclconf/go-cty/cty"
)

type field struct {
	Name string
	Type string
	Tag  string
}

type structType struct {
	Name   string
	Fields []field
}

// walker represents the set of types to declare to
// represent a [tfjson.SchemaBlock] as Go structs.
// See the [walk] function for usage.
type walker struct {
	StructTypes []structType
}

func processAttributeType(typ cty.Type) string {
	var out string

	switch {
	case typ.IsPrimitiveType():
		switch {
		case typ.Equals(cty.Bool):
			out = "bool"
		case typ.Equals(cty.Number):
			out = "int"
		case typ.Equals(cty.String):
			out = "string"
		default:
			panic("No idea what to do for: " + typ.FriendlyName())
		}
	case typ.IsMapType():
		out = "map[string]" + processAttributeType(*typ.MapElementType())
	case typ.IsSetType():
		out = "[]" + processAttributeType(*typ.SetElementType())
	case typ.IsListType():
		out = "[]" + processAttributeType(*typ.ListElementType())
	case typ.IsObjectType():
		out = "any"
	default:
		panic("No idea what to do for: " + typ.FriendlyName())
	}

	return out
}

func nestedBlockKeys(block *tfjson.SchemaBlock) []string {
	keys := sortKeys(block.NestedBlocks)

	// Remove TF specific "timeouts" block.
	if i := slices.Index(keys, "timeouts"); i != -1 {
		keys = slices.Delete(keys, i, i+1)
	}

	return keys
}

func (w *walker) walk(block *tfjson.SchemaBlock, name []string) error {
	// Produce nested types before this block itself.
	// This ensures types are defined before they are referenced.
	for _, k := range nestedBlockKeys(block) {
		v := block.NestedBlocks[k]
		err := w.walk(v.Block, append(name, strcase.ToCamel(k)))
		if err != nil {
			return err
		}
	}

	// Declare type.
	typ := structType{
		Name: strings.Join(name, ""),
	}

	// Declare attributes.
	for _, k := range sortKeys(block.Attributes) {
		v := block.Attributes[k]

		// Assert the attribute type is always set.
		if v.AttributeType == cty.NilType {
			return fmt.Errorf("unexpected nil type for attribute %s", k)
		}

		// Collect field properties.
		fieldName := strcase.ToCamel(k)
		fieldType := processAttributeType(v.AttributeType)
		fieldTag := k
		if v.Required && v.Optional {
			return fmt.Errorf("both required and optional are set for attribute %s", k)
		}
		if !v.Required {
			fieldTag = fmt.Sprintf("%s,omitempty", fieldTag)
		}

		// Append to list of fields for type.
		typ.Fields = append(typ.Fields, field{
			Name: fieldName,
			Type: fieldType,
			Tag:  fieldTag,
		})
	}

	// Declare nested blocks.
	for _, k := range nestedBlockKeys(block) {
		v := block.NestedBlocks[k]

		// Collect field properties.
		fieldName := strcase.ToCamel(k)
		fieldTypePrefix := ""
		if v.MaxItems == 1 {
			fieldTypePrefix = "*"
		} else {
			fieldTypePrefix = "[]"
		}
		fieldType := fmt.Sprintf("%s%s", fieldTypePrefix, strings.Join(append(name, strcase.ToCamel(k)), ""))
		fieldTag := fmt.Sprintf("%s,omitempty", k)

		// Append to list of fields for type.
		typ.Fields = append(typ.Fields, field{
			Name: fieldName,
			Type: fieldType,
			Tag:  fieldTag,
		})
	}

	// Append type to list of structs.
	w.StructTypes = append(w.StructTypes, typ)
	return nil
}

// walk recursively traverses [tfjson.SchemaBlock] and returns the
// set of types to declare to represents it as Go structs.
func walk(block *tfjson.SchemaBlock, name []string) (*walker, error) {
	w := &walker{}
	err := w.walk(block, name)
	return w, err
}
