// Package config загружает конфигурацию healthlog из TOML-файла.
package config

import (
	"errors"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/pelletier/go-toml/v2"
)

// DefaultPath — имя конфига по умолчанию: ищется в рабочей директории
// процесса. Переопределяется опцией --config=path.
const DefaultPath = "config.toml"

// Config — корневая конфигурация сервиса (см. config.example.toml).
type Config struct {
	Server  Server  `toml:"server"`
	Auth    Auth    `toml:"auth"`
	Storage Storage `toml:"storage"`
	Ingest  Ingest  `toml:"ingest"`
	Log     Log     `toml:"log"`
}

// Server — параметры HTTP-сервера.
type Server struct {
	Addr string `toml:"addr"`
	// ReadTimeout — на всё чтение запроса вместе с телом. Держим щедрым:
	// экспорт истории — это десятки мегабайт по мобильной сети.
	ReadTimeout  Duration `toml:"read_timeout"`
	WriteTimeout Duration `toml:"write_timeout"`
}

// Auth — токены доступа. Пустой список = проверка выключена (локальный
// запуск в доверенной сети); сервис предупреждает об этом на старте.
type Auth struct {
	WriteTokens []string `toml:"write_tokens"`
	ReadTokens  []string `toml:"read_tokens"`
}

// Storage — где лежат сырой архив и витрина.
type Storage struct {
	DBPath     string `toml:"db_path"`
	ArchiveDir string `toml:"archive_dir"`
}

// Ingest — параметры приёма.
type Ingest struct {
	MaxBodyMB int `toml:"max_body_mb"`
}

// Log — уровень и формат логов.
type Log struct {
	Level  string `toml:"level"`
	Format string `toml:"format"`
}

// Load читает TOML-конфиг, подставляет умолчания и валидирует результат.
// Отсутствующий файл — не ошибка: сервис поднимается на умолчаниях, что
// нужно для локального запуска.
func Load(path string) (*Config, error) {
	var cfg Config

	data, err := os.ReadFile(path)
	switch {
	case errors.Is(err, os.ErrNotExist):
		// Конфига нет — работаем на умолчаниях.
	case err != nil:
		return nil, fmt.Errorf("read config: %w", err)
	default:
		if err := toml.Unmarshal(data, &cfg); err != nil {
			return nil, fmt.Errorf("parse config: %w", err)
		}
	}

	cfg.applyDefaults()
	if err := cfg.validate(); err != nil {
		return nil, err
	}
	return &cfg, nil
}

func (c *Config) applyDefaults() {
	if c.Server.Addr == "" {
		c.Server.Addr = ":8080"
	}
	if c.Server.ReadTimeout == 0 {
		c.Server.ReadTimeout = Duration(5 * time.Minute)
	}
	if c.Server.WriteTimeout == 0 {
		c.Server.WriteTimeout = Duration(30 * time.Second)
	}
	if c.Storage.DBPath == "" {
		c.Storage.DBPath = "./healthlog.db"
	}
	if c.Storage.ArchiveDir == "" {
		c.Storage.ArchiveDir = "./raw"
	}
	if c.Ingest.MaxBodyMB == 0 {
		c.Ingest.MaxBodyMB = 64
	}
	if c.Log.Level == "" {
		c.Log.Level = "info"
	}
	if c.Log.Format == "" {
		c.Log.Format = "json"
	}
}

// validate собирает все проблемы разом (errors.Join), чтобы не чинить конфиг
// по одной строчке за перезапуск.
func (c *Config) validate() error {
	var errs []error

	if c.Ingest.MaxBodyMB <= 0 {
		errs = append(errs, fmt.Errorf("ingest.max_body_mb: %w: %d", ErrOutOfRange, c.Ingest.MaxBodyMB))
	}
	if c.Server.ReadTimeout <= 0 {
		errs = append(errs, fmt.Errorf("server.read_timeout: %w", ErrOutOfRange))
	}
	if c.Server.WriteTimeout <= 0 {
		errs = append(errs, fmt.Errorf("server.write_timeout: %w", ErrOutOfRange))
	}
	if !oneOf(c.Log.Level, "debug", "info", "warn", "error") {
		errs = append(errs, fmt.Errorf("log.level: %w: %q", ErrUnknownValue, c.Log.Level))
	}
	if !oneOf(c.Log.Format, "json", "text") {
		errs = append(errs, fmt.Errorf("log.format: %w: %q", ErrUnknownValue, c.Log.Format))
	}
	for i, t := range c.Auth.WriteTokens {
		if strings.TrimSpace(t) == "" {
			errs = append(errs, fmt.Errorf("auth.write_tokens[%d]: %w", i, ErrEmptyToken))
		}
	}
	for i, t := range c.Auth.ReadTokens {
		if strings.TrimSpace(t) == "" {
			errs = append(errs, fmt.Errorf("auth.read_tokens[%d]: %w", i, ErrEmptyToken))
		}
	}

	return errors.Join(errs...)
}

// Ошибки валидации конфига.
var (
	ErrOutOfRange   = errors.New("значение вне допустимого диапазона")
	ErrUnknownValue = errors.New("недопустимое значение")
	ErrEmptyToken   = errors.New("пустой токен")
)

func oneOf(v string, allowed ...string) bool {
	for _, a := range allowed {
		if strings.EqualFold(v, a) {
			return true
		}
	}
	return false
}

// Duration — time.Duration, читаемая из TOML строкой Go-формата ("30s", "5m").
type Duration time.Duration

// UnmarshalText разбирает Go-длительность из TOML-строки.
func (d *Duration) UnmarshalText(text []byte) error {
	v, err := time.ParseDuration(string(text))
	if err != nil {
		return fmt.Errorf("parse duration %q: %w", text, err)
	}
	*d = Duration(v)
	return nil
}

// D возвращает значение как time.Duration.
func (d Duration) D() time.Duration { return time.Duration(d) }
