package config

import (
	"os"
	"strings"
	"time"

	mmodel "github.com/tech/sendico/fx/ingestor/internal/model"
	"github.com/tech/sendico/pkg/db"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/messaging"
	"gopkg.in/yaml.v3"
)

const defaultPollInterval = 30 * time.Second

type Config struct {
	PollIntervalSeconds int               `yaml:"poll_interval_seconds"` //nolint:tagliatelle // matches config file format
	Market              MarketConfig      `yaml:"market"`
	Database            *db.Config        `yaml:"database"`
	Metrics             *MetricsConfig    `yaml:"metrics"`
	Messaging           *messaging.Config `yaml:"messaging"`

	pairs         []Pair
	pairsBySource map[mmodel.Driver][]PairConfig
}

//nolint:cyclop,funlen
func Load(path string) (*Config, error) {
	if path == "" {
		return nil, merrors.InvalidArgument("config: path is empty")
	}

	//nolint:gosec // config path is provided by process startup arguments/config.
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, merrors.InternalWrap(err, "config: failed to read file")
	}

	cfg := &Config{}

	err = yaml.Unmarshal(data, cfg)
	if err != nil {
		return nil, merrors.InternalWrap(err, "config: failed to parse yaml")
	}

	if len(cfg.Market.Sources) == 0 {
		return nil, merrors.InvalidArgument("config: no market sources configured")
	}

	sourceSet := make(map[mmodel.Driver]struct{}, len(cfg.Market.Sources))
	for idx := range cfg.Market.Sources {
		src := &cfg.Market.Sources[idx]
		if src.Driver.IsEmpty() {
			return nil, merrors.InvalidArgument("config: market source driver is empty")
		}

		sourceSet[src.Driver] = struct{}{}
	}

	if len(cfg.Market.Pairs) == 0 {
		return nil, merrors.InvalidArgument("config: no pairs configured")
	}

	normalizedPairs := make(map[string][]PairConfig, len(cfg.Market.Pairs))
	pairsBySource := make(map[mmodel.Driver][]PairConfig, len(cfg.Market.Pairs))

	var flattened []Pair

	for rawSource, pairList := range cfg.Market.Pairs {
		driver := mmodel.Driver(rawSource)
		if driver.IsEmpty() {
			return nil, merrors.InvalidArgument("config: pair source is empty")
		}

		if _, ok := sourceSet[driver]; !ok {
			return nil, merrors.InvalidArgument(
				"config: pair references unknown source: "+driver.String(),
				"pairs."+driver.String(),
			)
		}

		processed := make([]PairConfig, len(pairList))
		for idx := range pairList {
			pair := pairList[idx]
			pair.Base = strings.ToUpper(strings.TrimSpace(pair.Base))
			pair.Quote = strings.ToUpper(strings.TrimSpace(pair.Quote))

			pair.Symbol = strings.TrimSpace(pair.Symbol)

			if pair.Base == "" || pair.Quote == "" || pair.Symbol == "" {
				return nil, merrors.InvalidArgument(
					"config: pair entries must define base, quote, and symbol",
					"pairs."+driver.String(),
				)
			}

			if strings.TrimSpace(pair.Provider) == "" {
				pair.Provider = strings.ToLower(driver.String())
			}

			processed[idx] = pair
			flattened = append(flattened, Pair{
				PairConfig: pair,
				Source:     driver,
			})
		}

		pairsBySource[driver] = processed
		normalizedPairs[driver.String()] = processed
	}

	cfg.Market.Pairs = normalizedPairs
	cfg.pairsBySource = pairsBySource
	cfg.pairs = flattened

	if cfg.Database == nil {
		return nil, merrors.InvalidArgument("config: database configuration is required")
	}

	if cfg.Metrics != nil && cfg.Metrics.Enabled {
		cfg.Metrics.Address = strings.TrimSpace(cfg.Metrics.Address)
		if cfg.Metrics.Address == "" {
			cfg.Metrics.Address = ":9102"
		}
	}

	return cfg, nil
}

func (c *Config) PollInterval() time.Duration {
	if c == nil {
		return defaultPollInterval
	}

	if c.PollIntervalSeconds <= 0 {
		return defaultPollInterval
	}

	return time.Duration(c.PollIntervalSeconds) * time.Second
}

func (c *Config) Pairs() []Pair {
	if c == nil {
		return nil
	}

	out := make([]Pair, len(c.pairs))
	copy(out, c.pairs)

	return out
}

func (c *Config) PairsBySource() map[mmodel.Driver][]PairConfig {
	if c == nil {
		return nil
	}

	out := make(map[mmodel.Driver][]PairConfig, len(c.pairsBySource))
	for driver, pairs := range c.pairsBySource {
		cp := make([]PairConfig, len(pairs))
		copy(cp, pairs)
		out[driver] = cp
	}

	return out
}

func (c *Config) MetricsConfig() *MetricsConfig {
	if c == nil || c.Metrics == nil {
		return nil
	}

	cp := *c.Metrics

	return &cp
}
