package resource

import (
	"strings"

	"github.com/hashicorp/go-version"
	"github.com/hashicorp/terraform/configs/configschema"
	"github.com/hashicorp/terraform/providers"
	"github.com/sirupsen/logrus"
)

type AttributeSchema struct {
	ConfigSchema configschema.Attribute
	JsonString   bool
}

type Flags uint32

const (
	FlagDeepMode Flags = 1 << iota
)

func (f Flags) HasFlag(flag Flags) bool {
	return f&flag != 0
}

func (f *Flags) AddFlag(flag Flags) {
	*f |= flag
}

type Schema struct {
	ProviderVersion             *version.Version
	Flags                       Flags
	SchemaVersion               int64
	Attributes                  map[string]AttributeSchema
	NormalizeFunc               func(res *Resource)
	HumanReadableAttributesFunc func(res *Resource) map[string]string
	ResolveReadAttributesFunc   func(res *Resource) map[string]string
	DiscriminantFunc            func(*Resource, *Resource) bool
}

func (s *Schema) IsComputedField(path []string) bool {
	metadata, exist := s.Attributes[strings.Join(path, ".")]
	if !exist {
		return false
	}
	return metadata.ConfigSchema.Computed
}

func (s *Schema) IsJsonStringField(path []string) bool {
	metadata, exist := s.Attributes[strings.Join(path, ".")]
	if !exist {
		return false
	}
	return metadata.JsonString
}

type SchemaRepositoryInterface interface {
	GetSchema(resourceType string) (*Schema, bool)
	SetFlags(typ string, flags ...Flags)
	UpdateSchema(typ string, schemasMutators map[string]func(attributeSchema *AttributeSchema))
	SetNormalizeFunc(typ string, normalizeFunc func(res *Resource))
	SetHumanReadableAttributesFunc(typ string, humanReadableAttributesFunc func(res *Resource) map[string]string)
	SetResolveReadAttributesFunc(typ string, resolveReadAttributesFunc func(res *Resource) map[string]string)
	SetDiscriminantFunc(string, func(*Resource, *Resource) bool)
}

type SchemaRepository struct {
	schemas         map[string]*Schema
	ProviderName    string
	ProviderVersion *version.Version
}

func NewSchemaRepository() *SchemaRepository {
	return &SchemaRepository{
		schemas: make(map[string]*Schema),
	}
}

func (r *SchemaRepository) GetSchema(resourceType string) (*Schema, bool) {
	schema, exist := r.schemas[resourceType]
	return schema, exist
}

func (r *SchemaRepository) fetchNestedBlocks(root string, metadata map[string]AttributeSchema, block map[string]*configschema.NestedBlock) {
	for s, nestedBlock := range block {
		path := s
		if root != "" {
			path = strings.Join([]string{root, s}, ".")
		}
		for s2, attr := range nestedBlock.Attributes {
			nestedPath := strings.Join([]string{path, s2}, ".")
			metadata[nestedPath] = AttributeSchema{
				ConfigSchema: *attr,
			}
		}
		r.fetchNestedBlocks(path, metadata, nestedBlock.BlockTypes)
	}
}

func (r *SchemaRepository) Init(providerName, providerVersion string, schema map[string]providers.Schema) error {
	v, err := version.NewVersion(providerVersion)
	if err != nil {
		return err
	}
	r.ProviderVersion = v
	r.ProviderName = providerName
	for typ, sch := range schema {
		attributeMetas := map[string]AttributeSchema{}
		for s, attribute := range sch.Block.Attributes {
			attributeMetas[s] = AttributeSchema{
				ConfigSchema: *attribute,
			}
		}

		r.fetchNestedBlocks("", attributeMetas, sch.Block.BlockTypes)

		r.schemas[typ] = &Schema{
			ProviderVersion: r.ProviderVersion,
			SchemaVersion:   sch.Version,
			Attributes:      attributeMetas,
		}
	}
	return nil
}

func (r SchemaRepository) SetFlags(typ string, flags ...Flags) {
	metadata, exist := r.GetSchema(typ)
	if !exist {
		logrus.WithFields(logrus.Fields{"type": typ}).Warning("Unable to set flags, no schema found")
		return
	}
	for _, flag := range flags {
		metadata.Flags.AddFlag(flag)
	}
}

func (r *SchemaRepository) UpdateSchema(typ string, schemasMutators map[string]func(attributeSchema *AttributeSchema)) {
	for s, f := range schemasMutators {
		metadata, exist := r.GetSchema(typ)
		if !exist {
			logrus.WithFields(logrus.Fields{"type": typ}).Warning("Unable to set metadata, no schema found")
			return
		}
		m := (*metadata).Attributes[s]
		f(&m)
		(*metadata).Attributes[s] = m
	}
}

func (r *SchemaRepository) SetNormalizeFunc(typ string, normalizeFunc func(res *Resource)) {
	metadata, exist := r.GetSchema(typ)
	if !exist {
		logrus.WithFields(logrus.Fields{"type": typ}).Warning("Unable to set normalize func, no schema found")
		return
	}
	(*metadata).NormalizeFunc = normalizeFunc
}

func (r *SchemaRepository) SetHumanReadableAttributesFunc(typ string, humanReadableAttributesFunc func(res *Resource) map[string]string) {
	metadata, exist := r.GetSchema(typ)
	if !exist {
		logrus.WithFields(logrus.Fields{"type": typ}).Warning("Unable to add human readable attributes, no schema found")
		return
	}
	(*metadata).HumanReadableAttributesFunc = humanReadableAttributesFunc
}

func (r *SchemaRepository) SetResolveReadAttributesFunc(typ string, resolveReadAttributesFunc func(res *Resource) map[string]string) {
	metadata, exist := r.GetSchema(typ)
	if !exist {
		logrus.WithFields(logrus.Fields{"type": typ}).Warning("Unable to add read resource attributes, no schema found")
		return
	}
	(*metadata).ResolveReadAttributesFunc = resolveReadAttributesFunc
}

func (r *SchemaRepository) SetDiscriminantFunc(typ string, fn func(self, res *Resource) bool) {
	metadata, exist := r.GetSchema(typ)
	if !exist {
		logrus.WithFields(logrus.Fields{"type": typ}).Warning("Unable to set discriminant function, no schema found")
		return
	}
	(*metadata).DiscriminantFunc = fn
}
