// Package config loads olcrtc runtime configuration from YAML files.
//
// The YAML schema mirrors [session.Config]. Fields left unset in the file
// remain at their zero value. Use [Apply] to map a parsed [File] onto an
// existing [session.Config]; non-zero fields in the session config take
// precedence over the YAML values.
//
//nolint:tagliatelle // YAML keys are the documented config file schema.
package config

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"unicode/utf8"

	"github.com/openlibrecommunity/olcrtc/internal/app/session"
	"gopkg.in/yaml.v3"
)

var (
	// ErrConfigNotFound is returned when a config file path is set but the file does not exist.
	ErrConfigNotFound = errors.New("config file not found")
	// ErrConfigInvalidUTF8 is returned when a config file is not valid UTF-8.
	ErrConfigInvalidUTF8 = errors.New("config file is not valid UTF-8")
	// ErrCryptoKeyConflict is returned when both inline and file-backed keys are configured.
	ErrCryptoKeyConflict = errors.New("crypto.key and crypto.key_file cannot both be set")
	// ErrCryptoKeyFileEmpty is returned when crypto.key_file points to an empty file.
	ErrCryptoKeyFileEmpty = errors.New("crypto key file is empty")
)

// File is the on-disk YAML schema.
type File struct {
	Mode      string    `yaml:"mode"`
	Auth      Auth      `yaml:"auth"`
	Room      Room      `yaml:"room"`
	Crypto    Crypto    `yaml:"crypto"`
	Net       Net       `yaml:"net"`
	SOCKS     SOCKS     `yaml:"socks"`
	Engine    Engine    `yaml:"engine"`
	Video     Video     `yaml:"video"`
	VP8       VP8       `yaml:"vp8"`
	SEI       SEI       `yaml:"sei"`
	Liveness  Liveness  `yaml:"liveness"`
	Lifecycle Lifecycle `yaml:"lifecycle"`
	Traffic   Traffic   `yaml:"traffic"`
	Gen       Gen       `yaml:"gen"`
	Profiles  []Profile `yaml:"profiles"`
	Failover  Failover  `yaml:"failover"`
	Data      string    `yaml:"data"`
	Debug     bool      `yaml:"debug"`
}

// Profile is a failover entry that overrides top-level runtime fields.
type Profile struct {
	Name      string    `yaml:"name"`
	Auth      Auth      `yaml:"auth"`
	Room      Room      `yaml:"room"`
	Crypto    Crypto    `yaml:"crypto"`
	Net       Net       `yaml:"net"`
	SOCKS     SOCKS     `yaml:"socks"`
	Engine    Engine    `yaml:"engine"`
	Video     Video     `yaml:"video"`
	VP8       VP8       `yaml:"vp8"`
	SEI       SEI       `yaml:"sei"`
	Liveness  Liveness  `yaml:"liveness"`
	Lifecycle Lifecycle `yaml:"lifecycle"`
	Traffic   Traffic   `yaml:"traffic"`
}

// Failover controls ordered profile failover.
type Failover struct {
	RetryDelay string `yaml:"retry_delay"`
	MaxCycles  int    `yaml:"max_cycles"`
}

// Auth selects the auth provider.
type Auth struct {
	Provider string `yaml:"provider"` // telemost, wbstream, none
}

// Room identifies the conference room.
type Room struct {
	ID      string `yaml:"id"`
	Channel string `yaml:"channel"`
}

// Crypto holds the shared secret used to authenticate and encrypt the tunnel.
type Crypto struct {
	Key     string `yaml:"key"`      // 64-char hex (32 bytes)
	KeyFile string `yaml:"key_file"` // path to a file containing crypto.key
}

// Net groups network and transport selection.
type Net struct {
	Transport string `yaml:"transport"` // datachannel, videochannel, seichannel, vp8channel
	DNS       string `yaml:"dns"`
}

// SOCKS bundles SOCKS5 listener and outbound-proxy settings.
type SOCKS struct {
	Host      string `yaml:"host"`
	Port      int    `yaml:"port"`
	User      string `yaml:"user"`
	Pass      string `yaml:"pass"`
	ProxyAddr string `yaml:"proxy_addr"`
	ProxyPort int    `yaml:"proxy_port"`
	ProxyUser string `yaml:"proxy_user"`
	ProxyPass string `yaml:"proxy_pass"`
}

// Engine selects a direct SFU connection when Auth.Provider is "none".
type Engine struct {
	Name  string `yaml:"name"` // livekit, goolom, jitsi
	URL   string `yaml:"url"`
	Token string `yaml:"token"`
}

// Video tunes the videochannel transport.
type Video struct {
	Width      int    `yaml:"width"`
	Height     int    `yaml:"height"`
	FPS        int    `yaml:"fps"`
	Bitrate    string `yaml:"bitrate"`
	HW         string `yaml:"hw"`
	QRSize     int    `yaml:"qr_size"`
	QRRecovery string `yaml:"qr_recovery"`
	Codec      string `yaml:"codec"`
	TileModule int    `yaml:"tile_module"`
	TileRS     int    `yaml:"tile_rs"`
}

// VP8 tunes the vp8channel transport.
type VP8 struct {
	FPS       int `yaml:"fps"`
	BatchSize int `yaml:"batch_size"`
}

// SEI tunes the seichannel transport.
type SEI struct {
	FPS          int `yaml:"fps"`
	BatchSize    int `yaml:"batch_size"`
	FragmentSize int `yaml:"fragment_size"`
	AckTimeoutMS int `yaml:"ack_timeout_ms"`
}

// Liveness tunes the post-handshake control stream ping/pong checks.
type Liveness struct {
	Interval string `yaml:"interval"`
	Timeout  string `yaml:"timeout"`
	Failures int    `yaml:"failures"`
}

// Lifecycle controls planned session rebuilds.
type Lifecycle struct {
	MaxSessionDuration string `yaml:"max_session_duration"`
}

// Traffic controls optional reliability-oriented send shaping.
type Traffic struct {
	MaxPayloadSize int    `yaml:"max_payload_size"`
	MinDelay       string `yaml:"min_delay"`
	MaxDelay       string `yaml:"max_delay"`
}

// Gen controls room-generation mode.
type Gen struct {
	Amount int `yaml:"amount"`
}

// Load parses a YAML file from disk.
func Load(path string) (File, error) {
	// #nosec G304 -- config path is an explicit CLI/user input.
	data, err := os.ReadFile(path)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return File{}, fmt.Errorf("%w: %s", ErrConfigNotFound, path)
		}
		return File{}, fmt.Errorf("read config %s: %w", path, err)
	}
	if !utf8.Valid(data) {
		return File{}, fmt.Errorf("parse config %s: %w", path, ErrConfigInvalidUTF8)
	}
	var f File
	if err := yaml.Unmarshal(data, &f); err != nil {
		return File{}, fmt.Errorf("parse config %s: %w", path, err)
	}
	if err := loadExternalSecrets(path, &f); err != nil {
		return File{}, err
	}
	return f, nil
}

func loadExternalSecrets(configPath string, f *File) error {
	if f.Crypto.KeyFile == "" {
		return loadProfileSecrets(configPath, f.Profiles)
	}
	if f.Crypto.Key != "" {
		return ErrCryptoKeyConflict
	}

	key, err := readKeyFile(configPath, f.Crypto.KeyFile)
	if err != nil {
		return err
	}
	f.Crypto.Key = key
	return loadProfileSecrets(configPath, f.Profiles)
}

func loadProfileSecrets(configPath string, profiles []Profile) error {
	for i := range profiles {
		if profiles[i].Crypto.KeyFile == "" {
			continue
		}
		if profiles[i].Crypto.Key != "" {
			return fmt.Errorf("profiles[%d]: %w", i, ErrCryptoKeyConflict)
		}
		key, err := readKeyFile(configPath, profiles[i].Crypto.KeyFile)
		if err != nil {
			return fmt.Errorf("profiles[%d]: %w", i, err)
		}
		profiles[i].Crypto.Key = key
	}
	return nil
}

func readKeyFile(configPath, keyFile string) (string, error) {
	keyPath := keyFile
	if !filepath.IsAbs(keyPath) {
		keyPath = filepath.Join(filepath.Dir(configPath), keyPath)
	}

	// #nosec G304 -- key_file is an explicit path in the user's config file.
	data, err := os.ReadFile(keyPath)
	if err != nil {
		return "", fmt.Errorf("read crypto key file %s: %w", keyPath, err)
	}
	key := strings.TrimSpace(string(data))
	if key == "" {
		return "", ErrCryptoKeyFileEmpty
	}
	return key, nil
}

// Apply merges f onto dst. CLI-set fields (non-zero values in dst) win;
// YAML values fill in the rest.
func Apply(dst session.Config, f File) session.Config {
	dst.Mode = pickString(dst.Mode, f.Mode)
	dst.Transport = pickString(dst.Transport, f.Net.Transport)
	dst.Auth = pickString(dst.Auth, f.Auth.Provider)
	dst.Engine = pickString(dst.Engine, f.Engine.Name)
	dst.URL = pickString(dst.URL, f.Engine.URL)
	dst.Token = pickString(dst.Token, f.Engine.Token)
	dst.RoomID = pickString(dst.RoomID, f.Room.ID)
	dst.ChannelID = pickString(dst.ChannelID, f.Room.Channel)
	dst.KeyHex = pickString(dst.KeyHex, f.Crypto.Key)
	dst.SOCKSHost = pickString(dst.SOCKSHost, f.SOCKS.Host)
	dst.SOCKSPort = pickInt(dst.SOCKSPort, f.SOCKS.Port)
	dst.SOCKSUser = pickString(dst.SOCKSUser, f.SOCKS.User)
	dst.SOCKSPass = pickString(dst.SOCKSPass, f.SOCKS.Pass)
	dst.DNSServer = pickString(dst.DNSServer, f.Net.DNS)
	dst.SOCKSProxyAddr = pickString(dst.SOCKSProxyAddr, f.SOCKS.ProxyAddr)
	dst.SOCKSProxyPort = pickInt(dst.SOCKSProxyPort, f.SOCKS.ProxyPort)
	dst.SOCKSProxyUser = pickString(dst.SOCKSProxyUser, f.SOCKS.ProxyUser)
	dst.SOCKSProxyPass = pickString(dst.SOCKSProxyPass, f.SOCKS.ProxyPass)
	dst.Video.Width = pickInt(dst.Video.Width, f.Video.Width)
	dst.Video.Height = pickInt(dst.Video.Height, f.Video.Height)
	dst.Video.FPS = pickInt(dst.Video.FPS, f.Video.FPS)
	dst.Video.Bitrate = pickString(dst.Video.Bitrate, f.Video.Bitrate)
	dst.Video.HW = pickString(dst.Video.HW, f.Video.HW)
	dst.Video.QRSize = pickInt(dst.Video.QRSize, f.Video.QRSize)
	dst.Video.QRRecovery = pickString(dst.Video.QRRecovery, f.Video.QRRecovery)
	dst.Video.Codec = pickString(dst.Video.Codec, f.Video.Codec)
	dst.Video.TileModule = pickInt(dst.Video.TileModule, f.Video.TileModule)
	dst.Video.TileRS = pickInt(dst.Video.TileRS, f.Video.TileRS)
	dst.VP8.FPS = pickInt(dst.VP8.FPS, f.VP8.FPS)
	dst.VP8.BatchSize = pickInt(dst.VP8.BatchSize, f.VP8.BatchSize)
	dst.SEI.FPS = pickInt(dst.SEI.FPS, f.SEI.FPS)
	dst.SEI.BatchSize = pickInt(dst.SEI.BatchSize, f.SEI.BatchSize)
	dst.SEI.FragmentSize = pickInt(dst.SEI.FragmentSize, f.SEI.FragmentSize)
	dst.SEI.AckTimeoutMS = pickInt(dst.SEI.AckTimeoutMS, f.SEI.AckTimeoutMS)
	dst.LivenessInterval = pickString(dst.LivenessInterval, f.Liveness.Interval)
	dst.LivenessTimeout = pickString(dst.LivenessTimeout, f.Liveness.Timeout)
	dst.LivenessFailures = pickInt(dst.LivenessFailures, f.Liveness.Failures)
	dst.MaxSessionDuration = pickString(dst.MaxSessionDuration, f.Lifecycle.MaxSessionDuration)
	dst.TrafficMaxPayloadSize = pickInt(dst.TrafficMaxPayloadSize, f.Traffic.MaxPayloadSize)
	dst.TrafficMinDelay = pickString(dst.TrafficMinDelay, f.Traffic.MinDelay)
	dst.TrafficMaxDelay = pickString(dst.TrafficMaxDelay, f.Traffic.MaxDelay)
	dst.Amount = pickInt(dst.Amount, f.Gen.Amount)
	return dst
}

// ApplyProfile overlays a failover profile onto an already-applied base config.
func ApplyProfile(base session.Config, p Profile) session.Config {
	dst := base
	dst.Transport = overlayString(dst.Transport, p.Net.Transport)
	dst.Auth = overlayString(dst.Auth, p.Auth.Provider)
	dst.Engine = overlayString(dst.Engine, p.Engine.Name)
	dst.URL = overlayString(dst.URL, p.Engine.URL)
	dst.Token = overlayString(dst.Token, p.Engine.Token)
	dst.RoomID = overlayString(dst.RoomID, p.Room.ID)
	dst.ChannelID = overlayString(dst.ChannelID, p.Room.Channel)
	dst.KeyHex = overlayString(dst.KeyHex, p.Crypto.Key)
	dst.SOCKSHost = overlayString(dst.SOCKSHost, p.SOCKS.Host)
	dst.SOCKSPort = overlayInt(dst.SOCKSPort, p.SOCKS.Port)
	dst.SOCKSUser = overlayString(dst.SOCKSUser, p.SOCKS.User)
	dst.SOCKSPass = overlayString(dst.SOCKSPass, p.SOCKS.Pass)
	dst.DNSServer = overlayString(dst.DNSServer, p.Net.DNS)
	dst.SOCKSProxyAddr = overlayString(dst.SOCKSProxyAddr, p.SOCKS.ProxyAddr)
	dst.SOCKSProxyPort = overlayInt(dst.SOCKSProxyPort, p.SOCKS.ProxyPort)
	dst.SOCKSProxyUser = overlayString(dst.SOCKSProxyUser, p.SOCKS.ProxyUser)
	dst.SOCKSProxyPass = overlayString(dst.SOCKSProxyPass, p.SOCKS.ProxyPass)
	dst.Video.Width = overlayInt(dst.Video.Width, p.Video.Width)
	dst.Video.Height = overlayInt(dst.Video.Height, p.Video.Height)
	dst.Video.FPS = overlayInt(dst.Video.FPS, p.Video.FPS)
	dst.Video.Bitrate = overlayString(dst.Video.Bitrate, p.Video.Bitrate)
	dst.Video.HW = overlayString(dst.Video.HW, p.Video.HW)
	dst.Video.QRSize = overlayInt(dst.Video.QRSize, p.Video.QRSize)
	dst.Video.QRRecovery = overlayString(dst.Video.QRRecovery, p.Video.QRRecovery)
	dst.Video.Codec = overlayString(dst.Video.Codec, p.Video.Codec)
	dst.Video.TileModule = overlayInt(dst.Video.TileModule, p.Video.TileModule)
	dst.Video.TileRS = overlayInt(dst.Video.TileRS, p.Video.TileRS)
	dst.VP8.FPS = overlayInt(dst.VP8.FPS, p.VP8.FPS)
	dst.VP8.BatchSize = overlayInt(dst.VP8.BatchSize, p.VP8.BatchSize)
	dst.SEI.FPS = overlayInt(dst.SEI.FPS, p.SEI.FPS)
	dst.SEI.BatchSize = overlayInt(dst.SEI.BatchSize, p.SEI.BatchSize)
	dst.SEI.FragmentSize = overlayInt(dst.SEI.FragmentSize, p.SEI.FragmentSize)
	dst.SEI.AckTimeoutMS = overlayInt(dst.SEI.AckTimeoutMS, p.SEI.AckTimeoutMS)
	dst.LivenessInterval = overlayString(dst.LivenessInterval, p.Liveness.Interval)
	dst.LivenessTimeout = overlayString(dst.LivenessTimeout, p.Liveness.Timeout)
	dst.LivenessFailures = overlayInt(dst.LivenessFailures, p.Liveness.Failures)
	dst.MaxSessionDuration = overlayString(dst.MaxSessionDuration, p.Lifecycle.MaxSessionDuration)
	dst.TrafficMaxPayloadSize = overlayInt(dst.TrafficMaxPayloadSize, p.Traffic.MaxPayloadSize)
	dst.TrafficMinDelay = overlayString(dst.TrafficMinDelay, p.Traffic.MinDelay)
	dst.TrafficMaxDelay = overlayString(dst.TrafficMaxDelay, p.Traffic.MaxDelay)
	return dst
}

func pickString(cli, yamlVal string) string {
	if cli != "" {
		return cli
	}
	return yamlVal
}

func pickInt(cli, yamlVal int) int {
	if cli != 0 {
		return cli
	}
	return yamlVal
}

func overlayString(base, override string) string {
	if override != "" {
		return override
	}
	return base
}

func overlayInt(base, override int) int {
	if override != 0 {
		return override
	}
	return base
}
