/*
 * Copyright (c) 2025-2026, Антон Аксенов
 * This file is part of iptvc project
 * MIT License: https://git.axenov.dev/IPTV/iptvc/src/branch/master/LICENSE
 */

package config

import (
	"fmt"
	"html/template"
	"log"
	"math/rand"
	"os"
	"strconv"
	"strings"

	"github.com/joho/godotenv"
	"gopkg.in/yaml.v3"
)

// Константы значений по умолчанию для временных параметров.
// Внешний интерфейс (YAML, env, CLI) задаёт их в секундах,
// внутреннее представление — в миллисекундах.
const (
	defaultTimeoutSeconds          = 10
	defaultAllCooldownSeconds      = 1800 // 30 минут
	defaultOneCooldownSeconds      = 2
	defaultChannelsCooldownSeconds = 0
	defaultCacheTtlSeconds         = 3600
)

// Config описывает конфигурацию
type Config struct {
	App    AppConfig    `yaml:"app"`
	Server ServerConfig `yaml:"server"`
	Site   SiteConfig   `yaml:"site"`
	Check  CheckConfig  `yaml:"check"`
	Cache  CacheConfig  `yaml:"cache"`
}

// AppConfig описывает общие настройки приложения
type AppConfig struct {
	Timezone  string `yaml:"timezone"`
	Debug     bool   `yaml:"debug"`
	LogLevel  string `yaml:"logLevel"`
	Playlists string `yaml:"playlists"`
	Tags      string `yaml:"tags"`
}

// ServerConfig описывает конфигурацию веб-сервера
type ServerConfig struct {
	Port uint   `yaml:"port"`
	Host string `yaml:"host"`
}

// Link описывает ссылку в навигации или подвале сайта.
// Если заданы Children, рендерится как выпадающее меню.
type Link struct {
	Title    string `yaml:"title"`
	Url      string `yaml:"url"`
	Icon     string `yaml:"icon"`
	Children []Link `yaml:"children"`
}

// TabConfig описывает настройку одной вкладки на странице плейлиста.
type TabConfig struct {
	Visible bool          `yaml:"visible"`
	Text    template.HTML `yaml:"text"`
}

// TabsConfig описывает группу вкладок на странице плейлиста.
type TabsConfig struct {
	Raw   TabConfig `yaml:"raw"`
	Legal TabConfig `yaml:"legal"`
}

// HeaderConfig описывает настройки шапки сайта
type HeaderConfig struct {
	Menu []Link `yaml:"menu"`
}

// FooterConfig описывает настройки подвала сайта
type FooterConfig struct {
	Links []Link `yaml:"links"`
}

// SiteConfig описывает настройки сайта
type SiteConfig struct {
	Title    string       `yaml:"title"`
	BaseUrl  string       `yaml:"baseUrl"`
	RepoUrl  string       `yaml:"repoUrl"`
	PageSize uint         `yaml:"pageSize"`
	Favicon  string       `yaml:"favicon"`
	Meta     MetaConfig   `yaml:"meta"`
	Tabs     TabsConfig   `yaml:"tabs"`
	Header   HeaderConfig `yaml:"header"`
	Footer   FooterConfig `yaml:"footer"`
}

// MetaConfig описывает SEO-метаданные сайта.
type MetaConfig struct {
	Description string `yaml:"description"`
	Keywords    string `yaml:"keywords"`
}

// CacheConfig описывает конфигурацию подключения к кешу.
// Ttl задаётся в секундах.
type CacheConfig struct {
	Enabled  bool   `yaml:"enabled"`
	Host     string `yaml:"host"`
	Port     uint   `yaml:"port"`
	Username string `yaml:"username"`
	Password string `yaml:"password"`
	Db       uint   `yaml:"db"`
	Ttl      uint   `yaml:"ttl"`
	IsActive bool   `yaml:"-"`
}

// CheckConfig описывает параметры проверки плейлистов и каналов
type CheckConfig struct {
	StartOnServe bool                 `yaml:"startOnServe"`
	Playlists    CheckPlaylistsConfig `yaml:"playlists"`
	Channels     CheckChannelsConfig  `yaml:"channels"`
}

// DurationSeconds — тип для временных интервалов, задаваемых в секундах,
// но хранящихся внутри в миллисекундах.
type DurationSeconds int

// UnmarshalYAML декодирует значение из секунд в миллисекунды.
func (d *DurationSeconds) UnmarshalYAML(value *yaml.Node) error {
	var v float64
	if err := value.Decode(&v); err != nil {
		return err
	}
	*d = DurationSeconds(v * 1000)
	return nil
}

// CheckPlaylistsConfig — параметры проверки плейлистов.
// Значения Timeout, AllCooldown, OneCooldown задаются в секундах
// в YAML/CLI/env, но хранятся в миллисекундах для использования в коде.
type CheckPlaylistsConfig struct {
	UserAgent   UserAgents      `yaml:"userAgent"`
	Timeout     DurationSeconds `yaml:"timeout"`      // ms internally
	AllCooldown IntRange        `yaml:"allCooldown"` // ms internally
	OneCooldown IntRange        `yaml:"oneCooldown"` // ms internally
	MaxRoutines int             `yaml:"maxRoutines"` // count
	PerRoutine  int             `yaml:"perRoutine"`  // count
}

// CheckChannelsConfig — параметры проверки каналов.
// Значения Timeout и Cooldown задаются в секундах
// в YAML/CLI/env, но хранятся в миллисекундах для использования в коде.
type CheckChannelsConfig struct {
	UserAgent   UserAgents      `yaml:"userAgent"`
	Timeout     DurationSeconds `yaml:"timeout"`      // ms internally
	ByteRange   int             `yaml:"byteRange"`   // bytes
	Cooldown    IntRange        `yaml:"cooldown"`     // ms internally
	MaxRoutines int             `yaml:"maxRoutines"` // count
	PerRoutine  int             `yaml:"perRoutine"`  // count
}

// IntRange — значение, которое может быть скаляром или [min, max].
// Если скаляр: Min == Max. Если [min, max]: Min <= Max.
type IntRange struct {
	Min int
	Max int
}

// UnmarshalYAML реализует кастомный разбор IntRange из YAML.
// В YAML значение задаётся в секундах, результат сохраняется в миллисекундах.
// Поддерживает скаляр (int/float) или массив из двух значений [min, max].
func (r *IntRange) UnmarshalYAML(value *yaml.Node) error {
	decodeNode := func(n *yaml.Node) (int, error) {
		var f float64
		if err := n.Decode(&f); err != nil {
			return 0, err
		}
		return int(f * 1000), nil
	}

	if value.Kind == yaml.ScalarNode {
		v, err := decodeNode(value)
		if err != nil {
			return err
		}
		r.Min = v
		r.Max = v
		return nil
	}
	if value.Kind == yaml.SequenceNode {
		if len(value.Content) == 0 {
			r.Min, r.Max = 0, 0
			return nil
		}
		min, err := decodeNode(value.Content[0])
		if err != nil {
			return err
		}
		r.Min = min
		if len(value.Content) >= 2 {
			max, err := decodeNode(value.Content[1])
			if err != nil {
				return err
			}
			r.Max = max
		} else {
			r.Max = min
		}
		return nil
	}
	return nil
}

// Value возвращает значение: если Min == Max — константу, иначе случайное в [Min, Max].
func (r IntRange) Value() int {
	if r.Min == r.Max {
		return r.Min
	}
	return r.Min + rand.Intn(r.Max-r.Min+1)
}

// UserAgents — список User-Agent строк. Может быть задан как скаляр или массив.
type UserAgents []string

// UnmarshalYAML реализует кастомный разбор UserAgents из YAML.
func (u *UserAgents) UnmarshalYAML(value *yaml.Node) error {
	if value.Kind == yaml.ScalarNode {
		var s string
		if err := value.Decode(&s); err != nil {
			return err
		}
		*u = []string{s}
		return nil
	}
	if value.Kind == yaml.SequenceNode {
		var arr []string
		if err := value.Decode(&arr); err != nil {
			return err
		}
		*u = arr
		return nil
	}
	return nil
}

// Pick возвращает случайный User-Agent или пустую строку если список пуст.
func (u UserAgents) Pick() string {
	if len(u) == 0 {
		return ""
	}
	if len(u) == 1 {
		return u[0]
	}
	return u[rand.Intn(len(u))]
}

// defaults возвращает конфигурацию со значениями по умолчанию.
// Временные параметры хранятся в миллисекундах.
func defaults() *Config {
	return &Config{
		App: AppConfig{
			Timezone:  "GMT",
			Debug:     false,
			LogLevel:  "info",
			Playlists: "./playlists.ini",
			Tags:      "./channels.json",
		},
		Server: ServerConfig{
			Port: 8800,
			Host: "",
		},
		Site: SiteConfig{
			Title:    "IPTV Checker",
			BaseUrl:  "http://localhost:8800",
			PageSize: 0,
			RepoUrl:  "https://git.axenov.dev/IPTV",
			Meta: MetaConfig{
				Description: "Самообновляемые бесплатные IPTV-плейлисты для домашнего просмотра по коротким ссылкам, списки каналов, проверка доступности",
				Keywords:    "самообновляемые,бесплатные,iptv-плейлисты,iptv,плейлисты",
			},
			Tabs: TabsConfig{
				Raw:   TabConfig{Visible: true},
				Legal: TabConfig{Visible: true},
			},
			Header: HeaderConfig{
				Menu: []Link{
					{Title: "Помощь", Icon: "help-circle-outline", Children: []Link{
						{Title: "Документация", Url: "https://m3u.su/docs", Icon: "book-open-variant-outline"},
						{Title: "Исходники", Url: "https://git.axenov.dev/IPTV", Icon: "code-braces"},
						{Title: "@iptv_aggregator", Url: "https://t.me/iptv_aggregator", Icon: "bullhorn-variant-outline"},
					}},
				},
			},
			Footer: FooterConfig{
				Links: []Link{
					{Title: "Исходники", Url: "https://git.axenov.dev/IPTV", Icon: "code-braces"},
					{Title: "axenov.dev", Url: "https://axenov.dev", Icon: "account-outline"},
					{Title: "@iptv_aggregator", Url: "https://t.me/iptv_aggregator", Icon: "bullhorn-variant-outline"},
				},
			},
		},
		Check: CheckConfig{
			StartOnServe: false,
			Playlists: CheckPlaylistsConfig{
				UserAgent:   UserAgents{"Mozilla/5.0 WINK/1.31.1 (AndroidTV/9) HlsWinkPlayer"},
				Timeout:     defaultTimeoutSeconds * 1000,
				AllCooldown: IntRange{Min: defaultAllCooldownSeconds * 1000, Max: defaultAllCooldownSeconds * 1000},
				OneCooldown: IntRange{Min: defaultOneCooldownSeconds * 1000, Max: defaultOneCooldownSeconds * 1000},
				MaxRoutines: 1,
				PerRoutine:  1,
			},
			Channels: CheckChannelsConfig{
				UserAgent:   UserAgents{"Mozilla/5.0 WINK/1.31.1 (AndroidTV/9) HlsWinkPlayer"},
				Timeout:     defaultTimeoutSeconds * 1000,
				ByteRange:   512,
				Cooldown:    IntRange{Min: defaultChannelsCooldownSeconds * 1000, Max: defaultChannelsCooldownSeconds * 1000},
				MaxRoutines: 50,
				PerRoutine:  10,
			},
		},
		Cache: CacheConfig{
			Enabled:  true,
			Host:     "cache",
			Port:     6379,
			Username: "",
			Password: "",
			Db:       0,
			Ttl:      defaultCacheTtlSeconds,
		},
	}
}

// Init инициализирует объект конфигурации.
// Порядок приоритета (от низшего к высшему):
//  1. Значения по умолчанию
//  2. config.yml (путь можно задать через configPath)
//  3. Переменные окружения (.env загружается автоматически)
func Init(configPath string) *Config {
	_ = godotenv.Load(".env")

	cfg := defaults()

	// загружаем config.yml
	if configPath == "" {
		configPath = "config.yml"
	}
	if data, err := os.ReadFile(configPath); err == nil {
		if err := yaml.Unmarshal(data, cfg); err != nil {
			log.Printf("Warning: cannot parse %s: %s", configPath, err)
		}
	} else if !os.IsNotExist(err) {
		log.Printf("Warning: cannot read %s: %s", configPath, err)
	}

	// переопределяем значениями из переменных окружения
	applyEnv(cfg)

	// валидируем и исправляем
	validate(cfg)

	return cfg
}

// applyEnv переопределяет поля конфигурации значениями из переменных окружения.
func applyEnv(cfg *Config) {
	// app
	if v, ok := os.LookupEnv("APP_DEBUG"); ok {
		cfg.App.Debug = parseBool(v, cfg.App.Debug)
	}
	if v, ok := os.LookupEnv("APP_LOG_LEVEL"); ok && v != "" {
		cfg.App.LogLevel = v
	}
	if v, ok := os.LookupEnv("APP_TIMEZONE"); ok && v != "" {
		cfg.App.Timezone = v
	}
	if v, ok := os.LookupEnv("APP_PLAYLISTS"); ok && v != "" {
		cfg.App.Playlists = v
	}
	if v, ok := os.LookupEnv("APP_TAGS"); ok && v != "" {
		cfg.App.Tags = v
	}

	// server
	if v, ok := os.LookupEnv("SERVER_PORT"); ok && v != "" {
		cfg.Server.Port = parseUint(v, cfg.Server.Port)
	}
	if v, ok := os.LookupEnv("SERVER_HOST"); ok {
		cfg.Server.Host = v
	}

	// site
	if v, ok := os.LookupEnv("SITE_BASE_URL"); ok && v != "" {
		cfg.Site.BaseUrl = v
	}
	if v, ok := os.LookupEnv("SITE_PAGE_SIZE"); ok && v != "" {
		cfg.Site.PageSize = parseUint(v, cfg.Site.PageSize)
	}
	if v, ok := os.LookupEnv("SITE_REPO_URL"); ok && v != "" {
		cfg.Site.RepoUrl = v
	}
	if v, ok := os.LookupEnv("SITE_FAVICON"); ok {
		cfg.Site.Favicon = v
	}
	if v, ok := os.LookupEnv("SITE_META_DESCRIPTION"); ok && v != "" {
		cfg.Site.Meta.Description = v
	}
	if v, ok := os.LookupEnv("SITE_META_KEYWORDS"); ok && v != "" {
		cfg.Site.Meta.Keywords = v
	}
	if v, ok := os.LookupEnv("SITE_TITLE"); ok && v != "" {
		cfg.Site.Title = v
	}

	// check
	if v, ok := os.LookupEnv("CHECK_START_ON_SERVE"); ok {
		cfg.Check.StartOnServe = parseBool(v, cfg.Check.StartOnServe)
	}

	// check.playlists
	pp := &cfg.Check.Playlists
	if v, ok := os.LookupEnv("CHECK_PLAYLISTS_TIMEOUT"); ok && v != "" {
		pp.Timeout = DurationSeconds(parseSecondsToMillis(v, int(pp.Timeout)))
	}
	applyEnvIntRange(&pp.AllCooldown, "CHECK_PLAYLISTS_ALL_COOLDOWN")
	applyEnvIntRange(&pp.OneCooldown, "CHECK_PLAYLISTS_ONE_COOLDOWN")
	if v, ok := os.LookupEnv("CHECK_PLAYLISTS_MAX_ROUTINES"); ok && v != "" {
		pp.MaxRoutines = parseInt(v, pp.MaxRoutines)
	}
	if v, ok := os.LookupEnv("CHECK_PLAYLISTS_PER_ROUTINE"); ok && v != "" {
		pp.PerRoutine = parseInt(v, pp.PerRoutine)
	}
	if uas := applyEnvStringSlice("CHECK_PLAYLISTS_USER_AGENT"); uas != nil {
		pp.UserAgent = UserAgents(uas)
	}

	// check.channels
	cc := &cfg.Check.Channels
	if v, ok := os.LookupEnv("CHECK_CHANNELS_TIMEOUT"); ok && v != "" {
		cc.Timeout = DurationSeconds(parseSecondsToMillis(v, int(cc.Timeout)))
	}
	if v, ok := os.LookupEnv("CHECK_CHANNELS_BYTE_RANGE"); ok && v != "" {
		cc.ByteRange = parseInt(v, cc.ByteRange)
	}
	applyEnvIntRange(&cc.Cooldown, "CHECK_CHANNELS_COOLDOWN")
	if v, ok := os.LookupEnv("CHECK_CHANNELS_MAX_ROUTINES"); ok && v != "" {
		cc.MaxRoutines = parseInt(v, cc.MaxRoutines)
	}
	if v, ok := os.LookupEnv("CHECK_CHANNELS_PER_ROUTINE"); ok && v != "" {
		cc.PerRoutine = parseInt(v, cc.PerRoutine)
	}
	if uas := applyEnvStringSlice("CHECK_CHANNELS_USER_AGENT"); uas != nil {
		cc.UserAgent = UserAgents(uas)
	}

	// cache
	if v, ok := os.LookupEnv("CACHE_ENABLED"); ok {
		cfg.Cache.Enabled = parseBool(v, cfg.Cache.Enabled)
	}
	if v, ok := os.LookupEnv("CACHE_HOST"); ok && v != "" {
		cfg.Cache.Host = v
	}
	if v, ok := os.LookupEnv("CACHE_PORT"); ok && v != "" {
		cfg.Cache.Port = parseUint(v, cfg.Cache.Port)
	}
	if v, ok := os.LookupEnv("CACHE_USERNAME"); ok {
		cfg.Cache.Username = v
	}
	if v, ok := os.LookupEnv("CACHE_PASSWORD"); ok {
		cfg.Cache.Password = v
	}
	if v, ok := os.LookupEnv("CACHE_DB"); ok && v != "" {
		cfg.Cache.Db = parseUint(v, cfg.Cache.Db)
	}
	if v, ok := os.LookupEnv("CACHE_TTL"); ok && v != "" {
		cfg.Cache.Ttl = parseUint(v, cfg.Cache.Ttl)
	}
}

// parseBool разбирает строку в bool, при ошибке возвращает значение по умолчанию
func parseBool(s string, defaultValue bool) bool {
	val, err := strconv.ParseBool(s)
	if err == nil {
		return val
	}
	return defaultValue
}

// parseUint разбирает строку в uint, при ошибке возвращает значение по умолчанию
func parseUint(s string, defaultValue uint) uint {
	val, err := strconv.Atoi(s)
	if err == nil {
		return uint(val)
	}
	return defaultValue
}

// parseInt разбирает строку в int, при ошибке возвращает значение по умолчанию
func parseInt(s string, defaultValue int) int {
	val, err := strconv.Atoi(s)
	if err == nil {
		return val
	}
	return defaultValue
}

// parseSecondsToMillis разбирает строку с секундами и возвращает миллисекунды.
// При ошибке возвращает значение по умолчанию.
func parseSecondsToMillis(s string, defaultValue int) int {
	val, err := strconv.ParseFloat(s, 64)
	if err == nil && val >= 0 {
		return int(val * 1000)
	}
	return defaultValue
}

// applyEnvIntRange читает одно env-значение (в секундах) для IntRange,
// умножая результат на 1000, так как внутри значения хранятся в миллисекундах.
// Одно число задаёт фиксированное значение, два числа через запятую — диапазон.
func applyEnvIntRange(r *IntRange, name string) {
	value, ok := os.LookupEnv(name)
	if !ok || strings.TrimSpace(value) == "" {
		return
	}

	parse := func(s string) (int, bool) {
		s = strings.TrimSpace(s)
		v, err := strconv.ParseFloat(s, 64)
		if err != nil || v < 0 {
			return 0, false
		}
		return int(v * 1000), true
	}

	parts := strings.Split(value, ",")
	if len(parts) == 1 {
		n, ok := parse(parts[0])
		if !ok {
			return
		}
		r.Min = n
		r.Max = n
		return
	}
	if len(parts) != 2 {
		return
	}

	min, minOk := parse(parts[0])
	max, maxOk := parse(parts[1])
	if !minOk || !maxOk {
		return
	}
	r.Min = min
	r.Max = max
}

// applyEnvStringSlice читает индексированные env-переменные PREFIX_1, PREFIX_2, ...
// Возвращает nil, если ни одна не задана.
func applyEnvStringSlice(prefix string) []string {
	var result []string
	for i := 1; ; i++ {
		key := fmt.Sprintf("%s_%d", prefix, i)
		v, ok := os.LookupEnv(key)
		if !ok {
			break
		}
		if v != "" {
			result = append(result, v)
		}
	}
	return result
}

// validate проверяет значения конфигурации и исправляет некорректные,
// логируя предупреждения для каждого исправления.
func validate(cfg *Config) {
	// server
	if cfg.Server.Port == 0 || cfg.Server.Port > 65535 {
		log.Println("Config: server.port is not in the 0..65535 range, using default 8800")
		cfg.Server.Port = 8800
	}
	if cfg.Site.BaseUrl == "" {
		log.Println("Config: site.baseUrl is empty, using default")
		cfg.Site.BaseUrl = fmt.Sprintf("http://localhost:%d", cfg.Server.Port)
	}

	// cache
	if cfg.Cache.Enabled {
		if cfg.Cache.Host == "" {
			log.Println("Config: cache.host is empty, using default 'cache'")
			cfg.Cache.Host = "cache"
		}
		if cfg.Cache.Port == 0 {
			log.Println("Config: cache.port is 0, using default 6379")
			cfg.Cache.Port = 6379
		}
		if cfg.Cache.Ttl == 0 {
			log.Printf("Config: cache.ttl is 0, using default %d s", defaultCacheTtlSeconds)
			cfg.Cache.Ttl = defaultCacheTtlSeconds
		}
	}

	// check.playlists
	pp := &cfg.Check.Playlists
	if pp.Timeout <= 0 {
		log.Printf("Config: check.playlists.timeout <= 0, using default %d s", defaultTimeoutSeconds)
		pp.Timeout = defaultTimeoutSeconds * 1000
	}
	validateIntRange(&pp.AllCooldown, "check.playlists.allCooldown", 0, 3600000)
	validateIntRange(&pp.OneCooldown, "check.playlists.oneCooldown", 0, 3600000)
	if pp.MaxRoutines < 1 {
		log.Println("Config: check.playlists.maxRoutines < 1, using default 1")
		pp.MaxRoutines = 1
	}
	if pp.PerRoutine < 1 {
		log.Println("Config: check.playlists.perRoutine < 1, using default 1")
		pp.PerRoutine = 1
	}
	if len(pp.UserAgent) == 0 {
		log.Println("Config: check.playlists.userAgent is empty, using default")
		pp.UserAgent = UserAgents{"Mozilla/5.0 WINK/1.31.1 (AndroidTV/9) HlsWinkPlayer"}
	}

	// check.channels
	cc := &cfg.Check.Channels
	if cc.Timeout <= 0 {
		log.Printf("Config: check.channels.timeout <= 0, using default %d s", defaultTimeoutSeconds)
		cc.Timeout = defaultTimeoutSeconds * 1000
	}
	validateIntRange(&cc.Cooldown, "check.channels.cooldown", 0, 3600000)
	if cc.MaxRoutines < 1 {
		log.Println("Config: check.channels.maxRoutines < 1, using default 50")
		cc.MaxRoutines = 50
	}
	if cc.PerRoutine < 1 {
		log.Println("Config: check.channels.perRoutine < 1, using default 10")
		cc.PerRoutine = 10
	}
	if cc.ByteRange <= 0 {
		log.Println("Config: check.channels.byteRange <= 0, using default 512")
		cc.ByteRange = 512
	}
	if len(cc.UserAgent) == 0 {
		log.Println("Config: check.channels.userAgent is empty, using default")
		cc.UserAgent = UserAgents{"Mozilla/5.0 WINK/1.31.1 (AndroidTV/9) HlsWinkPlayer"}
	}
}

// validateIntRange проверяет IntRange: Min <= Max, оба в пределах [minVal, maxVal].
// Некорректные значения исправляются с логированием предупреждения.
func validateIntRange(r *IntRange, name string, minVal, maxVal int) {
	if r.Min > r.Max {
		log.Printf("Config: %s min=%d > max=%d, swapping", name, r.Min, r.Max)
		r.Min, r.Max = r.Max, r.Min
	}
	if r.Min < minVal {
		log.Printf("Config: %s min=%d < %d, clamped", name, r.Min, minVal)
		r.Min = minVal
	}
	if r.Max > maxVal {
		log.Printf("Config: %s max=%d > %d, clamped", name, r.Max, maxVal)
		r.Max = maxVal
	}
	if r.Min > r.Max {
		r.Min = minVal
		r.Max = minVal
	}
}
