package perxis

import (
	"io"
	"io/fs"
	"path/filepath"

	"git.perx.ru/perxis/perxis-go/pkg/errors"
	jsoniter "github.com/json-iterator/go"
	"gopkg.in/yaml.v3"
)

// Assets предоставляет методы для загрузки данных из файловой системы
type Assets[T any] struct {
	Constructor func() T
}

// NewAssets возвращает новый экземпляр загрузчика
func NewAssets[T any]() *Assets[T] {
	return &Assets[T]{
		Constructor: func() (t T) { return t },
	}
}

type FromFSFunc[T any] func(fsys fs.FS) ([]T, error)
type FromFileFunc[T any] func(file fs.File) ([]T, error)

func (a *Assets[T]) Funcs() (FromFSFunc[T], FromFileFunc[T]) {
	return a.FromFS, a.FromFile
}

// WithConstructor устанавливает конструктор для создания новых экземпляров
func (a *Assets[T]) WithConstructor(t func() T) *Assets[T] {
	a.Constructor = t
	return a
}

// MustFrom возвращает все записи в переданной файловой системе
func (a *Assets[T]) MustFrom(fsys fs.FS, path string) []T {
	res, err := a.From(fsys, path)
	if err != nil {
		panic(err)
	}
	return res
}

// MustOneFrom возвращает одну запись из переданного файла
func (a *Assets[T]) MustOneFrom(fsys fs.FS, path string) T {
	res, err := a.From(fsys, path)
	if err != nil {
		panic(err)
	}
	if len(res) == 0 {
		panic(errors.Errorf("no entries found"))
	}
	if len(res) > 1 {
		panic(errors.Errorf("multiple entries found"))
	}
	return res[0]
}

// From возвращает записи из переданного файла
func (a *Assets[T]) From(fsys fs.FS, path string) ([]T, error) {
	f, err := fsys.Open(path)
	if err != nil {
		return nil, err
	}

	defer f.Close()

	stat, err := f.Stat()
	if err != nil {
		return nil, err
	}

	if stat.IsDir() {
		sub, err := fs.Sub(fsys, path)
		if err != nil {
			return nil, err
		}
		return a.FromFS(sub)
	}

	return a.FromFile(f)
}

// FromFile возвращает записи в переданном файле
func (a *Assets[T]) FromFile(file fs.File) ([]T, error) {
	stat, err := file.Stat()
	if err != nil {
		return nil, err
	}

	switch filepath.Ext(stat.Name()) {
	case ".json":
		entry, err := a.FromJSON(file)
		if err != nil {
			return nil, errors.Wrapf(err, "file '%s'", stat.Name())
		}
		return []T{entry}, nil

	case ".yaml", ".yml":
		entries, err := a.FromYAML(file)
		return entries, errors.Wrapf(err, "file '%s'", stat.Name())
	}

	return nil, errors.Errorf("file '%s' must be in JSON or YAML format", stat.Name())
}

// FromFS возвращает все записи в переданной файловой системе
func (a *Assets[T]) FromFS(fsys fs.FS) (result []T, err error) {
	if err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, _ error) error {
		file, err := fsys.Open(path)
		if err != nil {
			return err
		}
		defer file.Close()

		if entries, err := a.FromFile(file); err == nil {
			result = append(result, entries...)
		}
		return nil
	}); err != nil {
		return nil, err
	}

	return result, nil
}

// FromJSON возвращает запись из JSON
func (c *Assets[T]) FromJSON(r io.Reader) (T, error) {
	entry := c.Constructor()
	data, err := io.ReadAll(r)
	if err != nil {
		return entry, err
	}

	err = jsoniter.Unmarshal(data, &entry)
	return entry, err
}

// FromYAML возвращает записи из YAML
func (c *Assets[T]) FromYAML(r io.Reader) (result []T, err error) {
	decoder := yaml.NewDecoder(r)
	for {
		var data interface{}
		err = decoder.Decode(&data)
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			return nil, err
		}

		json, err := jsoniter.Marshal(data)
		if err != nil {
			return nil, err
		}

		entry := c.Constructor()
		if err = jsoniter.Unmarshal(json, &entry); err != nil {
			return nil, err
		}
		result = append(result, entry)
	}

	return result, nil
}