package convert

import (
	"context"
	"fmt"
	"reflect"
	"sort"

	"github.com/hashicorp/go-cty/cty"
	"github.com/hashicorp/terraform-plugin-go/tfprotov5"
	"github.com/hashicorp/terraform-plugin-go/tftypes"
	"github.com/hashicorp/terraform-plugin-sdk/v2/internal/configs/configschema"
	"github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging"
)

func tftypeFromCtyType(in cty.Type) (tftypes.Type, error) {
	switch {
	case in.Equals(cty.String):
		return tftypes.String, nil
	case in.Equals(cty.Number):
		return tftypes.Number, nil
	case in.Equals(cty.Bool):
		return tftypes.Bool, nil
	case in.Equals(cty.DynamicPseudoType):
		return tftypes.DynamicPseudoType, nil
	case in.IsSetType():
		elemType, err := tftypeFromCtyType(in.ElementType())
		if err != nil {
			return nil, err
		}
		return tftypes.Set{
			ElementType: elemType,
		}, nil
	case in.IsListType():
		elemType, err := tftypeFromCtyType(in.ElementType())
		if err != nil {
			return nil, err
		}
		return tftypes.List{
			ElementType: elemType,
		}, nil
	case in.IsTupleType():
		elemTypes := make([]tftypes.Type, 0, in.Length())
		for _, typ := range in.TupleElementTypes() {
			elemType, err := tftypeFromCtyType(typ)
			if err != nil {
				return nil, err
			}
			elemTypes = append(elemTypes, elemType)
		}
		return tftypes.Tuple{
			ElementTypes: elemTypes,
		}, nil
	case in.IsMapType():
		elemType, err := tftypeFromCtyType(in.ElementType())
		if err != nil {
			return nil, err
		}
		return tftypes.Map{
			ElementType: elemType,
		}, nil
	case in.IsObjectType():
		attrTypes := make(map[string]tftypes.Type)
		for key, typ := range in.AttributeTypes() {
			attrType, err := tftypeFromCtyType(typ)
			if err != nil {
				return nil, err
			}
			attrTypes[key] = attrType
		}
		return tftypes.Object{
			AttributeTypes: attrTypes,
		}, nil
	}
	return nil, fmt.Errorf("unknown cty type %s", in.GoString())
}

func ctyTypeFromTFType(in tftypes.Type) (cty.Type, error) {
	switch {
	case in.Is(tftypes.String):
		return cty.String, nil
	case in.Is(tftypes.Bool):
		return cty.Bool, nil
	case in.Is(tftypes.Number):
		return cty.Number, nil
	case in.Is(tftypes.DynamicPseudoType):
		return cty.DynamicPseudoType, nil
	case in.Is(tftypes.List{}):
		elemType, err := ctyTypeFromTFType(in.(tftypes.List).ElementType)
		if err != nil {
			return cty.Type{}, err
		}
		return cty.List(elemType), nil
	case in.Is(tftypes.Set{}):
		elemType, err := ctyTypeFromTFType(in.(tftypes.Set).ElementType)
		if err != nil {
			return cty.Type{}, err
		}
		return cty.Set(elemType), nil
	case in.Is(tftypes.Map{}):
		elemType, err := ctyTypeFromTFType(in.(tftypes.Map).ElementType)
		if err != nil {
			return cty.Type{}, err
		}
		return cty.Map(elemType), nil
	case in.Is(tftypes.Tuple{}):
		elemTypes := make([]cty.Type, 0, len(in.(tftypes.Tuple).ElementTypes))
		for _, typ := range in.(tftypes.Tuple).ElementTypes {
			elemType, err := ctyTypeFromTFType(typ)
			if err != nil {
				return cty.Type{}, err
			}
			elemTypes = append(elemTypes, elemType)
		}
		return cty.Tuple(elemTypes), nil
	case in.Is(tftypes.Object{}):
		attrTypes := make(map[string]cty.Type, len(in.(tftypes.Object).AttributeTypes))
		for k, v := range in.(tftypes.Object).AttributeTypes {
			attrType, err := ctyTypeFromTFType(v)
			if err != nil {
				return cty.Type{}, err
			}
			attrTypes[k] = attrType
		}
		return cty.Object(attrTypes), nil
	}
	return cty.Type{}, fmt.Errorf("unknown tftypes.Type %s", in)
}

// ConfigSchemaToProto takes a *configschema.Block and converts it to a
// tfprotov5.SchemaBlock for a grpc response.
func ConfigSchemaToProto(ctx context.Context, b *configschema.Block) *tfprotov5.SchemaBlock {
	block := &tfprotov5.SchemaBlock{
		Description:     b.Description,
		DescriptionKind: protoStringKind(ctx, b.DescriptionKind),
		Deprecated:      b.Deprecated,
	}

	for _, name := range sortedKeys(b.Attributes) {
		a := b.Attributes[name]

		attr := &tfprotov5.SchemaAttribute{
			Name:            name,
			Description:     a.Description,
			DescriptionKind: protoStringKind(ctx, a.DescriptionKind),
			Optional:        a.Optional,
			Computed:        a.Computed,
			Required:        a.Required,
			Sensitive:       a.Sensitive,
			Deprecated:      a.Deprecated,
		}

		var err error
		attr.Type, err = tftypeFromCtyType(a.Type)
		if err != nil {
			panic(err)
		}

		block.Attributes = append(block.Attributes, attr)
	}

	for _, name := range sortedKeys(b.BlockTypes) {
		b := b.BlockTypes[name]
		block.BlockTypes = append(block.BlockTypes, protoSchemaNestedBlock(ctx, name, b))
	}

	return block
}

func protoStringKind(ctx context.Context, k configschema.StringKind) tfprotov5.StringKind {
	switch k {
	default:
		logging.HelperSchemaTrace(ctx, fmt.Sprintf("Unexpected configschema.StringKind: %d", k))
		return tfprotov5.StringKindPlain
	case configschema.StringPlain:
		return tfprotov5.StringKindPlain
	case configschema.StringMarkdown:
		return tfprotov5.StringKindMarkdown
	}
}

func protoSchemaNestedBlock(ctx context.Context, name string, b *configschema.NestedBlock) *tfprotov5.SchemaNestedBlock {
	var nesting tfprotov5.SchemaNestedBlockNestingMode
	switch b.Nesting {
	case configschema.NestingSingle:
		nesting = tfprotov5.SchemaNestedBlockNestingModeSingle
	case configschema.NestingGroup:
		nesting = tfprotov5.SchemaNestedBlockNestingModeGroup
	case configschema.NestingList:
		nesting = tfprotov5.SchemaNestedBlockNestingModeList
	case configschema.NestingSet:
		nesting = tfprotov5.SchemaNestedBlockNestingModeSet
	case configschema.NestingMap:
		nesting = tfprotov5.SchemaNestedBlockNestingModeMap
	default:
		nesting = tfprotov5.SchemaNestedBlockNestingModeInvalid
	}
	return &tfprotov5.SchemaNestedBlock{
		TypeName: name,
		Block:    ConfigSchemaToProto(ctx, &b.Block),
		Nesting:  nesting,
		MinItems: int64(b.MinItems),
		MaxItems: int64(b.MaxItems),
	}
}

// ProtoToConfigSchema takes the GetSchema_Block from a grpc response and converts it
// to a terraform *configschema.Block.
func ProtoToConfigSchema(ctx context.Context, b *tfprotov5.SchemaBlock) *configschema.Block {
	block := &configschema.Block{
		Attributes: make(map[string]*configschema.Attribute),
		BlockTypes: make(map[string]*configschema.NestedBlock),

		Description:     b.Description,
		DescriptionKind: schemaStringKind(ctx, b.DescriptionKind),
		Deprecated:      b.Deprecated,
	}

	for _, a := range b.Attributes {
		attr := &configschema.Attribute{
			Description:     a.Description,
			DescriptionKind: schemaStringKind(ctx, a.DescriptionKind),
			Required:        a.Required,
			Optional:        a.Optional,
			Computed:        a.Computed,
			Sensitive:       a.Sensitive,
			Deprecated:      a.Deprecated,
		}

		var err error
		attr.Type, err = ctyTypeFromTFType(a.Type)
		if err != nil {
			panic(err)
		}

		block.Attributes[a.Name] = attr
	}

	for _, b := range b.BlockTypes {
		block.BlockTypes[b.TypeName] = schemaNestedBlock(ctx, b)
	}

	return block
}

func schemaStringKind(ctx context.Context, k tfprotov5.StringKind) configschema.StringKind {
	switch k {
	default:
		logging.HelperSchemaTrace(ctx, fmt.Sprintf("Unexpected tfprotov5.StringKind: %d", k))
		return configschema.StringPlain
	case tfprotov5.StringKindPlain:
		return configschema.StringPlain
	case tfprotov5.StringKindMarkdown:
		return configschema.StringMarkdown
	}
}

func schemaNestedBlock(ctx context.Context, b *tfprotov5.SchemaNestedBlock) *configschema.NestedBlock {
	var nesting configschema.NestingMode
	switch b.Nesting {
	case tfprotov5.SchemaNestedBlockNestingModeSingle:
		nesting = configschema.NestingSingle
	case tfprotov5.SchemaNestedBlockNestingModeGroup:
		nesting = configschema.NestingGroup
	case tfprotov5.SchemaNestedBlockNestingModeList:
		nesting = configschema.NestingList
	case tfprotov5.SchemaNestedBlockNestingModeMap:
		nesting = configschema.NestingMap
	case tfprotov5.SchemaNestedBlockNestingModeSet:
		nesting = configschema.NestingSet
	default:
		// In all other cases we'll leave it as the zero value (invalid) and
		// let the caller validate it and deal with this.
	}

	nb := &configschema.NestedBlock{
		Nesting:  nesting,
		MinItems: int(b.MinItems),
		MaxItems: int(b.MaxItems),
	}

	nested := ProtoToConfigSchema(ctx, b.Block)
	nb.Block = *nested
	return nb
}

// sortedKeys returns the lexically sorted keys from the given map. This is
// used to make schema conversions are deterministic. This panics if map keys
// are not a string.
func sortedKeys(m interface{}) []string {
	v := reflect.ValueOf(m)
	keys := make([]string, v.Len())

	mapKeys := v.MapKeys()
	for i, k := range mapKeys {
		keys[i] = k.Interface().(string)
	}

	sort.Strings(keys)
	return keys
}
