package field

import (
	"context"
	"strings"

	"git.perx.ru/perxis/perxis-go/pkg/data"
	"git.perx.ru/perxis/perxis-go/pkg/errors"
	"git.perx.ru/perxis/perxis-go/pkg/expr"
)

const (
	FieldSeparator = "."
	IncludeLimit   = 10
)

type (
	Preparer interface {
		Prepare(f *Field) error
	}

	Fielder interface {
		GetField(path string) *Field
	}
)

type Translation struct {
	Locale      string `json:"locale,omitempty"`
	Title       string `json:"title,omitempty"`
	Description string `json:"description,omitempty"`
}

type View struct {
	Widget  string                 `json:"widget,omitempty"`  // Виджет для отображения поля в списке
	Options map[string]interface{} `json:"options,omitempty"` // Опции виджета, на усмотрения виджета
}

type UI struct {
	Widget      string                 `json:"widget,omitempty"`      // Имя виджета для отображения поля в пользовательском интерфейсе
	Placeholder string                 `json:"placeholder,omitempty"` // Подсказка для заполнения значения
	Options     map[string]interface{} `json:"options,omitempty"`     // Опции виджета для отображения
	ReadView    *View                  `json:"read_view,omitempty"`   // Настройки для отображения экрана в режиме просмотра элемента
	EditView    *View                  `json:"edit_view,omitempty"`   // Настройки для отображения экрана в режиме редактирования элемента
	ListView    *View                  `json:"list_view,omitempty"`   // Настройки для отображения экрана в режиме списке элементов
}

type Include struct {
	Ref      string `json:"ref,omitempty"`
	Optional bool   `json:"optional,omitempty"`
}

type Field struct {
	Title            string        `json:"title,omitempty"`             // Название поля (Например: name)
	Description      string        `json:"description,omitempty"`       // Описание поле (Например: User name)
	Translations     []Translation `json:"translations,omitempty"`      // Переводы данных на разных языках
	UI               *UI           `json:"ui,omitempty"`                // Опции пользовательского интерфейса
	Includes         []Include     `json:"includes,omitempty"`          // Импорт схем
	SingleLocale     bool          `json:"single_locale,omitempty"`     // Без перевода
	Indexed          bool          `json:"indexed,omitempty"`           // Построить индекс для поля
	Unique           bool          `json:"unique,omitempty"`            // Значение поля должны быть уникальными
	TextSearch       bool          `json:"text_search,omitempty"`       // Значение поля доступны для полнотекстового поиска
	Params           Parameters    `json:"-"`                           // Параметры поля, определяет так же тип поля
	Options          Options       `json:"options,omitempty"`           // Дополнительные опции
	Condition        string        `json:"condition,omitempty"`         // Условие отображения поля
	AdditionalValues bool          `json:"additional_values,omitempty"` // Разрешает дополнительные значения вне ограничений правил

	prepared bool
}

// TODO: Replace with Named field???
type PathField struct {
	Field
	Name string
	Path string
}

type NamedField struct {
	*Field
	Name string
}

func NewField(params Parameters, opts ...interface{}) *Field {
	f := &Field{}
	f.Params = params
	f.Options.Add(opts...)
	return f
}

func (f Field) GetType() Type {
	return f.Params.Type()
}

func (f *Field) AddOptions(t ...interface{}) *Field {
	f.Options.Add(t...)
	return f
}

func (f Field) WithUI(ui *UI) *Field {
	f.UI = ui
	return &f
}

func (f *Field) SetIncludes(includes ...interface{}) {
	f.Includes = make([]Include, 0, len(includes))
	for _, i := range includes {
		switch v := i.(type) {
		case string:
			f.Includes = append(f.Includes, Include{Ref: v})
		case Include:
			f.Includes = append(f.Includes, v)
		default:
			panic("incorrect import type")
		}
	}
}

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

func (f Field) GetIncludes() []string {
	return f.getIncludes()
}

func (f Field) getIncludes() []string {
	res := make([]string, len(f.Includes))
	for i, inc := range f.Includes {
		res[i] = inc.Ref
	}
	nested := f.GetNestedFields()
	for _, fld := range nested {
		res = append(res, fld.getIncludes()...)
	}
	return res
}

func (f Field) IsIncluded(name string) bool {
	return data.GlobMatch(name, f.GetIncludes()...)
}

func (f Field) SetTitle(title string) *Field {
	f.Title = title
	return &f
}

func (f Field) SetDescription(desc string) *Field {
	f.Description = desc
	return &f
}

func (f Field) AddTranslation(locale, title, desc string) *Field {
	for i, t := range f.Translations {
		if t.Locale == locale {
			f.Translations[i] = Translation{Locale: locale, Title: title, Description: desc}
			return &f
		}
	}

	f.Translations = append(f.Translations, Translation{Locale: locale, Title: title, Description: desc})
	return &f
}

func (f Field) SetSingleLocale(r bool) *Field {
	f.SingleLocale = r
	return &f
}

func (f Field) SetIndexed(r bool) *Field {
	f.Indexed = r
	return &f
}

func (f Field) SetAdditionalValues() *Field {
	f.AdditionalValues = true
	return &f
}

func (f Field) SetUnique(r bool) *Field {
	f.Unique = r
	return &f
}

func (f Field) SetTextSearch(r bool) *Field {
	f.TextSearch = r
	return &f
}

func (f Field) SetCondition(c string) *Field {
	f.Condition = c
	return &f
}

func (f *Field) MustEnabled(ctx context.Context) bool {
	if enabled, err := f.IsEnabled(ctx); !enabled || err != nil {
		return false
	}
	return true
}

func (f *Field) IsEnabled(ctx context.Context) (bool, error) {
	if f.Condition != "" {
		out, err := expr.Eval(ctx, f.Condition, nil)
		if err != nil {
			return false, err
		}

		if enabled, ok := out.(bool); ok {
			return enabled, nil
		}

		return false, errors.New("condition returns non-boolean value")
	}

	return true, nil
}

// Walk - выполняет обход данных по схеме и выполняет функцию, которая может модифицировать данные при необходимости
func (f *Field) Walk(ctx context.Context, v interface{}, fn WalkFunc, opt ...WalkOption) (interface{}, bool, error) {
	res, err := fn(ctx, f, v)

	if err != nil {
		return nil, false, err
	}

	if res.Changed || res.Stop {
		return res.Value, res.Changed, err
	}

	if res.Context != nil {
		ctx = res.Context
	}

	if walker, ok := f.GetType().(FieldWalker); ok {
		val, changed, err := walker.Walk(ctx, f, v, fn, NewWalkOptions(opt...))
		if err != nil {
			return nil, false, err
		}
		return val, changed, err
	}

	return v, false, nil
}

// DEPRECATED
func (f *Field) Prepare() error {
	if preparer, ok := f.GetType().(Preparer); ok {
		if err := preparer.Prepare(f); err != nil {
			return err
		}
	}
	for _, o := range f.Options {
		if preparer, ok := o.(Preparer); ok {
			if err := preparer.Prepare(f); err != nil {
				return err
			}
		}
	}
	return nil
}

// GetField возвращает поле по строковому пути
func (f *Field) GetField(path string) *Field {
	if path == "" {
		switch params := f.Params.(type) {
		case *ArrayParameters:
			// Возвращаем поле Item если путь указан как "arr."
			return params.Item
		}
		return nil
	}

	switch params := f.Params.(type) {
	case *ObjectParameters:
		pp := strings.SplitN(path, FieldSeparator, 2)

		for k, v := range params.Fields {

			p, ok := v.Params.(*ObjectParameters)
			if ok && p.Inline {
				f := v.GetField(path)
				if f != nil {
					return f
				}
			}

			if k == pp[0] {
				if len(pp) == 1 {
					return v
				}
				return v.GetField(pp[1])
			}
		}
	case Fielder:
		return params.GetField(path)

	case *ArrayParameters:
		return params.Item.GetField(path)
	}

	return nil
}

// GetFieldsPath возвращает полный путь для массива полей
func GetFieldsPath(flds []PathField) (res []string) {
	for _, f := range flds {
		res = append(res, f.Path)
	}
	return res
}

type FilterFunc func(*Field, string) bool

func GetAll(field *Field, path string) bool { return true }

func (f *Field) GetFields(filterFunc FilterFunc, pathPrefix ...string) (res []PathField) {
	var path string

	if len(pathPrefix) > 0 {
		path = pathPrefix[0]
	}

	// добавление корневого объекта для чего-то нужно?
	if path != "" && filterFunc(f, path) {
		res = append(res, PathField{
			Field: *f,
			Path:  path,
		})
	}

	switch params := f.Params.(type) {
	case *ObjectParameters:
		res = append(res, getFieldsObject(path, params, filterFunc, false)...)
	case *ArrayParameters:
		res = append(res, getFieldsArray(path, params, filterFunc)...)
	}

	// if len(pathPrefix) > 0 {
	//	for _, r := range res {
	//		r.Path = strings.Join([]string{pathPrefix[0], r.Path}, FieldSeparator)
	//	}
	// }

	return res
}

func getFieldsArray(path string, params *ArrayParameters, filterFunc FilterFunc) (res []PathField) {

	switch params := params.Item.Params.(type) {
	case *ObjectParameters:
		res = append(res, getFieldsObject(path, params, filterFunc, params.Inline)...)

	case *ArrayParameters:
		res = append(res, getFieldsArray(path, params, filterFunc)...)
	}

	return res
}

func getFieldsObject(path string, params *ObjectParameters, filterFunc FilterFunc, ignoreInline bool) (res []PathField) {
	for k, v := range params.Fields {
		if v == nil {
			continue
		}

		var newPath string
		lastIdx := strings.LastIndex(path, ".")

		if path == "" || !ignoreInline && params.Inline && lastIdx < 0 {
			newPath = k
		} else {
			if !params.Inline || ignoreInline {
				newPath = strings.Join([]string{path, k}, FieldSeparator)
			} else {
				newPath = strings.Join([]string{path[:lastIdx], k}, FieldSeparator)
			}
		}

		if flds := v.GetFields(filterFunc, newPath); len(flds) > 0 {
			res = append(res, flds...)
		}
	}

	return res
}

func (f *Field) GetNestedFields() []*Field {
	switch params := f.Params.(type) {
	case *ObjectParameters:
		flds := make([]*Field, 0, len(params.Fields))
		for _, v := range params.Fields {
			if v == nil {
				continue
			}
			flds = append(flds, v)
		}
		return flds
	case *ArrayParameters:
		return []*Field{params.Item}
	}

	return nil
}

// Clone создает копию поля
// Параметр reset указывает необходимо ли отвязать параметры поля от вложенных полей
func (f Field) Clone(reset bool) *Field {
	if f.UI != nil {
		ui := *f.UI
		f.UI = &ui
	}

	if len(f.Translations) > 0 {
		f.Translations = append(make([]Translation, 0, len(f.Translations)), f.Translations...)
	}

	if f.Options != nil {
		opts := make(Options)
		for k, v := range f.Options {
			opts[k] = v
		}
		f.Options = opts
	}

	if f.Params != nil {
		f.Params = f.Params.Clone(reset)
	}

	return &f
}

func (f *Field) mergeField(fld *Field) error {
	if f.Title == "" {
		f.Title = fld.Title
	}

	if f.Description == "" {
		f.Description = fld.Description
	}

	if len(f.Translations) == 0 {
		f.Translations = fld.Translations
	}

	if f.UI == nil {
		f.UI = fld.UI
	}

	if len(f.Includes) > 0 {
		f.Includes = fld.Includes
	}

	if f.Params == nil {
		f.Params = fld.Params
	} else if fld.Params != nil {
		type Merger interface {
			Merge(parameters Parameters) error
		}

		if merger, ok := f.Params.(Merger); ok {
			if err := merger.Merge(fld.Params); err != nil {
				return err
			}
		}
	}

	if f.Options == nil {
		f.Options = fld.Options
	}

	if f.Condition == "" {
		f.Condition = fld.Condition
	}

	return nil
}

func (f *Field) Merge(fields ...*Field) error {
	for _, fld := range fields {
		f.mergeField(fld)
	}
	return nil
}

func (f *Field) loadIncludes(ctx context.Context, loader Loader, depth int) error {
	if depth > IncludeLimit {
		return errors.New("limit for included fields exceeded")
	}

	for _, i := range f.Includes {
		if loader == nil {
			panic("schema loader not set")
		}
		importedField, err := loader.Load(ctx, i.Ref)
		if err != nil {
			if i.Optional {
				continue
			}
			return err
		}

		for _, fld := range importedField {
			depth += 1
			if err := fld.loadIncludes(ctx, loader, depth); err != nil {
				return err
			}
		}

		if err = f.Merge(importedField...); err != nil {
			return err
		}
	}
	for _, i := range f.GetNestedFields() {
		if err := i.loadIncludes(ctx, loader, depth); err != nil {
			return err
		}
	}
	return nil
}

func (f *Field) LoadIncludes(ctx context.Context, loader Loader) error {
	return f.loadIncludes(ctx, loader, 0)
}

func (f *Field) LoadRef(ctx context.Context, ref string, loader Loader) error {
	f.SetIncludes(ref)
	return f.LoadIncludes(ctx, loader)
}