package schema

import (
	"context"

	"git.perx.ru/perxis/perxis-go/pkg/errors"
	"git.perx.ru/perxis/perxis-go/pkg/expr"
	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
	"git.perx.ru/perxis/perxis-go/pkg/schema/modify"
	"git.perx.ru/perxis/perxis-go/pkg/schema/validate"
)

type Schema struct {
	field.Field
	Loaded   bool              `json:"loaded"`
	Metadata map[string]string `json:"metadata"`
}

func New(kv ...interface{}) *Schema {
	return &Schema{Field: *field.Object(kv...)}
}

func NewFromField(f *field.Field) *Schema {
	return &Schema{Field: *f}
}

var (
	Encode   = field.Encode
	Decode   = field.Decode
	Modify   = modify.Modify
	Validate = validate.Validate
	Evaluate = field.Evaluate
)

func (s *Schema) Clone(reset bool) *Schema {
	return &Schema{
		Field:    *s.Field.Clone(reset),
		Loaded:   s.Loaded,
		Metadata: s.Metadata,
	}
}

func (s Schema) WithIncludes(includes ...interface{}) *Schema {
	s.Field.SetIncludes(includes...)
	return &s
}

func (s *Schema) WithMetadata(kv ...string) *Schema {
	if s.Metadata == nil {
		s.Metadata = make(map[string]string)
	}
	for i := 0; i < len(kv); i += 2 {
		s.Metadata[kv[i]] = kv[i+1]
	}
	return s
}

func (s Schema) SetMetadata(md map[string]string) *Schema {
	s.Metadata = md
	return &s
}

func (s *Schema) Load(ctx context.Context) error {
	if s.Loaded {
		return nil
	}
	return s.LoadIncludes(ctx, nil)
}

func (s *Schema) LoadIncludes(ctx context.Context, loader field.Loader) (err error) {
	if loader == nil {
		loader = GetLoader()
	}
	err = s.Field.LoadIncludes(ctx, loader)
	if err == nil {
		s.Loaded = true
	}
	return
}

func (s *Schema) Modify(ctx context.Context, data map[string]interface{}) (res map[string]interface{}, err error) {
	if err = s.Load(ctx); err != nil {
		return nil, err
	}

	v, _, err := Modify(ctx, s, data)
	if err != nil || v == nil {
		return
	}

	res, _ = v.(map[string]interface{})
	return
}

func (s *Schema) Validate(ctx context.Context, data map[string]interface{}) (err error) {
	if err = s.Load(ctx); err != nil {
		return err
	}

	return Validate(ctx, s, data)
}

func (s *Schema) Evaluate(ctx context.Context, data map[string]interface{}) (res map[string]interface{}, err error) {
	if err = s.Load(ctx); err != nil {
		return nil, err
	}

	v, err := Evaluate(ctx, s, data)
	if err != nil || v == nil {
		return
	}
	res, _ = v.(map[string]interface{})
	return
}

func (s *Schema) Decode(ctx context.Context, v interface{}) (res map[string]interface{}, err error) {
	if err = s.Load(ctx); err != nil {
		return nil, err
	}

	if v, err = Decode(ctx, s, v); err != nil {
		return nil, err
	}
	res, _ = v.(map[string]interface{})
	return
}

func (s *Schema) Encode(ctx context.Context, v interface{}) (interface{}, error) {
	if err := s.Load(ctx); err != nil {
		return nil, err
	}

	var res interface{}
	var err error

	if res, err = Encode(ctx, s, v); err != nil {
		return nil, err
	}

	return res, nil
}

func (s *Schema) ToValue(ctx context.Context, data map[string]interface{}) (res map[string]interface{}, err error) {
	if err = s.Load(ctx); err != nil {
		return nil, err
	}

	if data, err = s.Decode(ctx, data); err != nil {
		return nil, err
	}
	if data, err = s.Modify(ctx, data); err != nil {
		return nil, err
	}
	if data, err = s.Evaluate(ctx, data); err != nil {
		return nil, err
	}
	if err = s.Validate(ctx, data); err != nil {
		return nil, err
	}
	return data, err
}

type parentFieldCtxKey struct{}

func (s *Schema) Introspect(ctx context.Context, data map[string]interface{}) (map[string]interface{}, *Schema, error) {
	if err := s.Load(ctx); err != nil {
		return nil, nil, err
	}

	var err error

	chg := true
	val := data
	i := 0

	var mutatedSchema *Schema

	for chg {
		mutatedSchema = nil

		var res interface{}
		res, chg, err = s.Walk(expr.WithEnv(ctx, val), val, func(ctx context.Context, f *field.Field, v interface{}) (res field.WalkFuncResult, err error) {
			parent, _ := ctx.Value(parentFieldCtxKey{}).(*field.Field)
			name, _ := ctx.Value(field.FieldName).(string)
			enabled, err := f.IsEnabled(ctx)
			if err != nil {
				return
			}

			if !enabled {
				res.Stop = true
				if v != nil {
					res.Changed = true
				}
				return
			}

			fld := f.Clone(true)
			if mutatedSchema == nil {
				mutatedSchema = &Schema{Field: *fld}
				fld = &mutatedSchema.Field
			}

			if parent != nil && name != "" {
				field.AddField(parent, name, fld)
			}

			ctx = context.WithValue(ctx, parentFieldCtxKey{}, fld)
			res.Context = ctx

			return
		}, field.WalkSchema())

		if err != nil {
			return nil, nil, errors.Wrap(err, "evaluation error")
		}

		if res != nil {
			val = res.(map[string]interface{})
		} else {
			val = nil
		}

		i += 1

		if i > field.EvaluatePassesLimit {
			return nil, nil, errors.New("fail to evaluate data conditions")
		}
	}

	return val, mutatedSchema, nil
}