// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package convert

import (
	"encoding/json"
	"fmt"
	"reflect"
	"sort"

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

	"github.com/opentofu/opentofu/internal/configs/configschema"
	"github.com/opentofu/opentofu/internal/providers"
	proto "github.com/opentofu/opentofu/internal/tfplugin5"
)

// ConfigSchemaToProto takes a *configschema.Block and converts it to a
// proto.Schema_Block for a grpc response.
func ConfigSchemaToProto(b *configschema.Block) *proto.Schema_Block {
	block := &proto.Schema_Block{
		Description:        b.Description,
		DescriptionKind:    protoStringKind(b.DescriptionKind),
		Deprecated:         b.Deprecated,
		DeprecationMessage: b.DeprecationMessage,
	}

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

		attr := &proto.Schema_Attribute{
			Name:               name,
			Description:        a.Description,
			DescriptionKind:    protoStringKind(a.DescriptionKind),
			Optional:           a.Optional,
			Computed:           a.Computed,
			Required:           a.Required,
			Sensitive:          a.Sensitive,
			Deprecated:         a.Deprecated,
			DeprecationMessage: a.DeprecationMessage,
			WriteOnly:          a.WriteOnly,
		}

		ty, err := json.Marshal(a.Type)
		if err != nil {
			panic(err)
		}

		attr.Type = ty

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

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

	return block
}

func ProtoToResourceIdentitySchema(schema *proto.ResourceIdentitySchema) *providers.ResourceIdentitySchema {
	// This method is taking a similar approach to ProtoToConfigSchema below, basically

	// We can't convert these, and so we should return nil here.
	if schema == nil {
		return nil
	}

	attributes := make(map[string]*configschema.Attribute, len(schema.IdentityAttributes))
	for _, a := range schema.IdentityAttributes {
		attribute := &configschema.Attribute{
			Description: a.Description,
			Required:    a.RequiredForImport,
			Optional:    a.OptionalForImport,
		}

		if a.Type != nil {
			t, err := ctyjson.UnmarshalType(a.Type)
			if err != nil {
				panic(fmt.Errorf("failed to unmarshal attribute type for resource identity: %w", err))
			}
			attribute.Type = t
		}
		attributes[a.Name] = attribute
	}

	return &providers.ResourceIdentitySchema{
		Version: schema.Version,

		Body: &configschema.Object{
			Attributes: attributes,
			Nesting:    configschema.NestingSingle, // We don't allow nested schema here, hence we're using an Object and not a Block
		},
	}
}

// ResourceIdentitySchemaToProto takes a *configschema.Object and converts it to a
// proto.ResourceIdentitySchema
func ResourceIdentitySchemaToProto(schema *providers.ResourceIdentitySchema) *proto.ResourceIdentitySchema {
	if schema == nil {
		return nil
	}

	body := schema.Body

	identityAttributes := make([]*proto.ResourceIdentitySchema_IdentityAttribute, 0, len(body.Attributes))
	for _, name := range sortedKeys(body.Attributes) {
		attribute := body.Attributes[name]

		attr := &proto.ResourceIdentitySchema_IdentityAttribute{
			Name:              name,
			Description:       attribute.Description,
			RequiredForImport: attribute.Required,
			OptionalForImport: attribute.Optional,
		}

		if attribute.Type != cty.NilType {
			ty, err := json.Marshal(attribute.Type)
			if err != nil {
				panic(fmt.Errorf("failed to marshal attribute type for resource identity: %w", err))
			}
			attr.Type = ty
		}

		identityAttributes = append(identityAttributes, attr)
	}

	return &proto.ResourceIdentitySchema{
		Version:            schema.Version,
		IdentityAttributes: identityAttributes,
	}
}

func protoStringKind(k configschema.StringKind) proto.StringKind {
	switch k {
	default:
		return proto.StringKind_PLAIN
	case configschema.StringMarkdown:
		return proto.StringKind_MARKDOWN
	}
}

func protoSchemaNestedBlock(name string, b *configschema.NestedBlock) *proto.Schema_NestedBlock {
	var nesting proto.Schema_NestedBlock_NestingMode
	switch b.Nesting {
	case configschema.NestingSingle:
		nesting = proto.Schema_NestedBlock_SINGLE
	case configschema.NestingGroup:
		nesting = proto.Schema_NestedBlock_GROUP
	case configschema.NestingList:
		nesting = proto.Schema_NestedBlock_LIST
	case configschema.NestingSet:
		nesting = proto.Schema_NestedBlock_SET
	case configschema.NestingMap:
		nesting = proto.Schema_NestedBlock_MAP
	default:
		nesting = proto.Schema_NestedBlock_INVALID
	}
	return &proto.Schema_NestedBlock{
		TypeName: name,
		Block:    ConfigSchemaToProto(&b.Block),
		Nesting:  nesting,
		MinItems: int64(b.MinItems),
		MaxItems: int64(b.MaxItems),
	}
}

// ProtoToProviderSchema takes a proto.Schema and converts it to a providers.Schema.
func ProtoToProviderSchema(s *proto.Schema) providers.Schema {
	return providers.Schema{
		Version: s.Version,
		Block:   ProtoToConfigSchema(s.Block),
	}
}

// ProtoToEphemeralProviderSchema takes a proto.Schema and converts it to a providers.Schema
// marking it as being able to work with ephemeral values.
func ProtoToEphemeralProviderSchema(s *proto.Schema) providers.Schema {
	ret := ProtoToProviderSchema(s)
	ret.Block.Ephemeral = true

	return ret
}

// ProtoToConfigSchema takes the Schema_Block from a grpc response and converts it
// to a tofu *configschema.Block.
func ProtoToConfigSchema(b *proto.Schema_Block) *configschema.Block {
	block := &configschema.Block{
		Attributes: make(map[string]*configschema.Attribute),
		BlockTypes: make(map[string]*configschema.NestedBlock),

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

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

		if err := json.Unmarshal(a.Type, &attr.Type); err != nil {
			panic(err)
		}

		block.Attributes[a.Name] = attr
	}

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

	return block
}

func schemaStringKind(k proto.StringKind) configschema.StringKind {
	switch k {
	default:
		return configschema.StringPlain
	case proto.StringKind_MARKDOWN:
		return configschema.StringMarkdown
	}
}

func schemaNestedBlock(b *proto.Schema_NestedBlock) *configschema.NestedBlock {
	var nesting configschema.NestingMode
	switch b.Nesting {
	case proto.Schema_NestedBlock_SINGLE:
		nesting = configschema.NestingSingle
	case proto.Schema_NestedBlock_GROUP:
		nesting = configschema.NestingGroup
	case proto.Schema_NestedBlock_LIST:
		nesting = configschema.NestingList
	case proto.Schema_NestedBlock_MAP:
		nesting = configschema.NestingMap
	case proto.Schema_NestedBlock_SET:
		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(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
}
