package types

import (
	"bytes"
	"fmt"
	"reflect"
	"strings"
	"sync"

	"github.com/rancher/norman/types/convert"
	"github.com/rancher/norman/types/definition"
	"github.com/rancher/wrangler/v2/pkg/name"
)

type SchemaCollection struct {
	Data []Schema
}

type SchemasInitFunc func(*Schemas) *Schemas

type SchemaHook func(*Schema)

type MappersFactory func() []Mapper

type BackReference struct {
	FieldName string
	Schema    *Schema
}

type Schemas struct {
	sync.Mutex
	processingTypes    map[reflect.Type]*Schema
	typeNames          map[reflect.Type]string
	schemasByPath      map[string]map[string]*Schema
	mappers            map[string]map[string][]Mapper
	references         map[string][]BackReference
	embedded           map[string]*Schema
	DefaultMappers     MappersFactory
	DefaultPostMappers MappersFactory
	versions           []APIVersion
	schemas            []*Schema
	AddHook            SchemaHook
	errors             []error
}

func NewSchemas() *Schemas {
	return &Schemas{
		processingTypes: map[reflect.Type]*Schema{},
		typeNames:       map[reflect.Type]string{},
		schemasByPath:   map[string]map[string]*Schema{},
		mappers:         map[string]map[string][]Mapper{},
		references:      map[string][]BackReference{},
		embedded:        map[string]*Schema{},
	}
}

func (s *Schemas) Init(initFunc SchemasInitFunc) *Schemas {
	return initFunc(s)
}

func (s *Schemas) Err() error {
	return NewErrors(s.errors...)
}

func (s *Schemas) AddSchemas(schema *Schemas) *Schemas {
	for _, schema := range schema.Schemas() {
		s.AddSchema(*schema)
	}
	return s
}

func (s *Schemas) RemoveSchema(schema Schema) *Schemas {
	s.Lock()
	defer s.Unlock()
	return s.doRemoveSchema(schema)
}

func (s *Schemas) doRemoveSchema(schema Schema) *Schemas {
	delete(s.schemasByPath[schema.Version.Path], schema.ID)

	s.removeReferences(&schema)

	if schema.Embed {
		s.removeEmbed(&schema)
	}

	return s
}

func (s *Schemas) removeReferences(schema *Schema) {
	for name, values := range s.references {
		changed := false
		var modified []BackReference
		for _, value := range values {
			if value.Schema.ID == schema.ID && value.Schema.Version.Path == schema.Version.Path {
				changed = true
				continue
			}
			modified = append(modified, value)
		}

		if changed {
			s.references[name] = modified
		}
	}
}

func (s *Schemas) AddSchema(schema Schema) *Schemas {
	s.Lock()
	defer s.Unlock()
	return s.doAddSchema(schema, false)
}

func (s *Schemas) ForceAddSchema(schema Schema) *Schemas {
	s.Lock()
	defer s.Unlock()
	return s.doAddSchema(schema, true)
}

func (s *Schemas) doAddSchema(schema Schema, replace bool) *Schemas {
	s.setupDefaults(&schema)

	if s.AddHook != nil {
		s.AddHook(&schema)
	}

	schemas, ok := s.schemasByPath[schema.Version.Path]
	if !ok {
		schemas = map[string]*Schema{}
		s.schemasByPath[schema.Version.Path] = schemas
		s.versions = append(s.versions, schema.Version)
	}

	if _, ok := schemas[schema.ID]; !ok ||
		(replace && schema.DynamicSchemaVersion != schemas[schema.ID].DynamicSchemaVersion) {
		schemas[schema.ID] = &schema

		if replace {
			for i, candidate := range s.schemas {
				if candidate.ID == schema.ID {
					s.schemas[i] = &schema
					break
				}
			}
		} else {
			s.schemas = append(s.schemas, &schema)
		}

		if !schema.Embed {
			s.addReferences(&schema)
		}
	}

	if schema.Embed {
		s.embed(&schema)
	}

	return s
}

func (s *Schemas) removeEmbed(schema *Schema) {
	target := s.doSchema(&schema.Version, schema.EmbedType, false)
	if target == nil {
		return
	}

	newSchema := *target
	newSchema.ResourceFields = map[string]Field{}

	for k, v := range target.ResourceFields {
		newSchema.ResourceFields[k] = v
	}

	for k := range schema.ResourceFields {
		delete(newSchema.ResourceFields, k)
	}

	s.doRemoveSchema(*target)
	s.doAddSchema(newSchema, false)
}

func (s *Schemas) embed(schema *Schema) {
	target := s.doSchema(&schema.Version, schema.EmbedType, false)
	if target == nil {
		return
	}

	newSchema := *target
	newSchema.ResourceFields = map[string]Field{}

	for k, v := range target.ResourceFields {
		// We remove the dynamic fields off the existing schema in case
		// they've been removed from the dynamic schema so they won't
		// be accidentally left over
		if !v.DynamicField {
			newSchema.ResourceFields[k] = v
		}
	}
	for k, v := range schema.ResourceFields {
		newSchema.ResourceFields[k] = v
	}

	s.doRemoveSchema(*target)
	s.doAddSchema(newSchema, false)
}

func (s *Schemas) addReferences(schema *Schema) {
	for name, field := range schema.ResourceFields {
		if !definition.IsReferenceType(field.Type) {
			continue
		}

		refType := definition.SubType(field.Type)
		if !strings.HasPrefix(refType, "/") {
			refType = convert.ToFullReference(schema.Version.Path, refType)
		}

		s.references[refType] = append(s.references[refType], BackReference{
			FieldName: name,
			Schema:    schema,
		})
	}
}

func (s *Schemas) setupDefaults(schema *Schema) {
	schema.Type = "/meta/schemas/schema"
	if schema.ID == "" {
		s.errors = append(s.errors, fmt.Errorf("ID is not set on schema: %v", schema))
		return
	}
	if schema.Version.Path == "" || schema.Version.Version == "" {
		s.errors = append(s.errors, fmt.Errorf("version is not set on schema: %s", schema.ID))
		return
	}
	if schema.PluralName == "" {
		schema.PluralName = name.GuessPluralName(schema.ID)
	}
	if schema.CodeName == "" {
		schema.CodeName = convert.Capitalize(schema.ID)
	}
	if schema.CodeNamePlural == "" {
		schema.CodeNamePlural = name.GuessPluralName(schema.CodeName)
	}
	if schema.BaseType == "" {
		schema.BaseType = schema.ID
	}
}

func (s *Schemas) References(schema *Schema) []BackReference {
	refType := convert.ToFullReference(schema.Version.Path, schema.ID)
	s.Lock()
	defer s.Unlock()
	return s.references[refType]
}

func (s *Schemas) AddMapper(version *APIVersion, schemaID string, mapper Mapper) *Schemas {
	mappers, ok := s.mappers[version.Path]
	if !ok {
		mappers = map[string][]Mapper{}
		s.mappers[version.Path] = mappers
	}

	mappers[schemaID] = append(mappers[schemaID], mapper)
	return s
}

func (s *Schemas) SchemasForVersion(version APIVersion) map[string]*Schema {
	s.Lock()
	defer s.Unlock()
	return s.schemasByPath[version.Path]
}

func (s *Schemas) Versions() []APIVersion {
	return s.versions
}

func (s *Schemas) Schemas() []*Schema {
	return s.schemas
}

func (s *Schemas) mapper(version *APIVersion, name string) []Mapper {
	var (
		path string
	)

	if strings.Contains(name, "/") {
		idx := strings.LastIndex(name, "/")
		path = name[0:idx]
		name = name[idx+1:]
	} else if version != nil {
		path = version.Path
	} else {
		path = "core"
	}

	mappers, ok := s.mappers[path]
	if !ok {
		return nil
	}

	mapper := mappers[name]
	if mapper != nil {
		return mapper
	}

	return nil
}

func (s *Schemas) Schema(version *APIVersion, name string) *Schema {
	return s.doSchema(version, name, true)
}

func (s *Schemas) doSchema(version *APIVersion, name string, lock bool) *Schema {
	var (
		path string
	)

	if strings.Contains(name, "/schemas/") {
		parts := strings.SplitN(name, "/schemas/", 2)
		path = parts[0]
		name = parts[1]
	} else if version != nil {
		path = version.Path
	} else {
		path = "core"
	}

	if lock {
		s.Lock()
		defer s.Unlock()
	}
	schemas, ok := s.schemasByPath[path]
	if !ok {
		return nil
	}

	schema := schemas[name]
	if schema != nil {
		return schema
	}

	for _, check := range schemas {
		if strings.EqualFold(check.ID, name) || strings.EqualFold(check.PluralName, name) {
			return check
		}
	}

	return nil
}

func (s *Schemas) SubContextVersionForSchema(schema *Schema) *APIVersion {
	fullName := fmt.Sprintf("%s/schemas/%s", schema.Version.Path, schema.ID)
	for _, version := range s.Versions() {
		if version.SubContextSchema == fullName {
			return &version
		}
	}
	return nil
}

type MultiErrors struct {
	Errors []error
}

type Errors struct {
	errors []error
}

func (e *Errors) Add(err error) {
	if err != nil {
		e.errors = append(e.errors, err)
	}
}

func (e *Errors) Err() error {
	return NewErrors(e.errors...)
}

func NewErrors(inErrors ...error) error {
	var errors []error
	for _, err := range inErrors {
		if err != nil {
			errors = append(errors, err)
		}
	}

	if len(errors) == 0 {
		return nil
	} else if len(errors) == 1 {
		return errors[0]
	}
	return &MultiErrors{
		Errors: errors,
	}
}

func (m *MultiErrors) Error() string {
	buf := bytes.NewBuffer(nil)
	for _, err := range m.Errors {
		if buf.Len() > 0 {
			buf.WriteString(", ")
		}
		buf.WriteString(err.Error())
	}

	return buf.String()
}
