// Package config liest die Konfigurationsdatei (/etc/tvwall/tvwall.conf) sowie
// die zur Laufzeit im Webinterface aenderbaren Einstellungen (settings.json).
package config

import (
	"bufio"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
)

// Config entspricht der Datei tvwall.conf. Die Werte werden beim Start gelesen
// und aendern sich zur Laufzeit nicht.
type Config struct {
	APSSID     string
	APPassword string
	APCountry  string
	APIP       string
	APChannel  string
	Hostname   string

	WebPort     int
	WebPassword string
	MaxUploadMB int64

	ImageDuration float64
	Mute          bool
	Audio         bool // false = Tonausgabe komplett abschalten

	ScanInterval int // Sekunden zwischen zwei Durchlaeufen des Medienordners

	DisplayMode  string // "auto", "drm" oder "desktop"
	RunUser      string
	MpvExtraArgs string
	MpvHwdec     string
	MpvGpuAPI    string

	BootLogo        string  // Bild, das beim Start zuerst gezeigt wird
	BootLogoSeconds float64 // wie lange
	FreshForVideo   bool    // mpv vor jedem Video neu starten
	StallTimeout    int     // Sekunden ohne Fortschritt, bevor ein Video uebersprungen wird

	StateDir   string
	SocketPath string
}

// Default liefert die Konfiguration mit allen Standardwerten.
func Default() *Config {
	return &Config{
		APSSID:          "TVWall-01",
		APPassword:      "tvwall1234",
		APCountry:       "DE",
		APIP:            "192.168.4.1",
		APChannel:       "7",
		Hostname:        "tvwall-01",
		WebPort:         80,
		MaxUploadMB:     2048,
		ImageDuration:   5,
		Audio:           true,
		ScanInterval:    10,
		DisplayMode:     "auto",
		MpvHwdec:        "no",
		MpvGpuAPI:       "opengl",
		BootLogoSeconds: 8,
		StallTimeout:    30,
		RunUser:         "tvwall",
		StateDir:        "/var/lib/tvwall",
		SocketPath:      "/run/tvwall/mpv.sock",
	}
}

// Abgeleitete Pfade.
func (c *Config) MediaDir() string     { return filepath.Join(c.StateDir, "media") }
func (c *Config) ThumbDir() string     { return filepath.Join(c.StateDir, "thumbs") }
func (c *Config) OrderFile() string    { return filepath.Join(c.StateDir, "order.json") }
func (c *Config) DisabledFile() string { return filepath.Join(c.StateDir, "disabled.json") }
func (c *Config) PlaylistFile() string { return filepath.Join(c.StateDir, "playlist.m3u") }
func (c *Config) SettingsFile() string { return filepath.Join(c.StateDir, "settings.json") }
func (c *Config) MpvSocket() string    { return c.SocketPath }

// Load liest die Konfigurationsdatei im Shell-Stil (KEY="value").
// Eine fehlende Datei ist kein Fehler - dann gelten die Standardwerte.
func Load(path string) (*Config, error) {
	c := Default()
	f, err := os.Open(path)
	if err != nil {
		if os.IsNotExist(err) {
			return c, nil
		}
		return nil, err
	}
	defer f.Close()

	sc := bufio.NewScanner(f)
	sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
	for lineNo := 1; sc.Scan(); lineNo++ {
		line := strings.TrimSpace(sc.Text())
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		line = strings.TrimPrefix(line, "export ")
		key, val, ok := strings.Cut(line, "=")
		if !ok {
			continue
		}
		key = strings.TrimSpace(key)
		val = unquote(strings.TrimSpace(val))

		switch key {
		case "AP_SSID":
			c.APSSID = val
		case "AP_PASSWORD":
			c.APPassword = val
		case "AP_COUNTRY":
			c.APCountry = val
		case "AP_IP":
			c.APIP = val
		case "AP_CHANNEL":
			c.APChannel = val
		case "HOSTNAME":
			c.Hostname = val
		case "WEB_PORT":
			if n, err := strconv.Atoi(val); err == nil && n > 0 && n < 65536 {
				c.WebPort = n
			}
		case "WEB_PASSWORD":
			c.WebPassword = val
		case "MAX_UPLOAD_MB":
			if n, err := strconv.ParseInt(val, 10, 64); err == nil && n > 0 {
				c.MaxUploadMB = n
			}
		case "IMAGE_DURATION":
			if f, err := strconv.ParseFloat(val, 64); err == nil && f > 0 {
				c.ImageDuration = f
			}
		case "MUTE":
			c.Mute = isTrue(val)
		case "AUDIO":
			c.Audio = isTrue(val)
		case "SCAN_INTERVAL":
			if n, err := strconv.Atoi(val); err == nil && n >= 1 {
				c.ScanInterval = n
			}
		case "DISPLAY_MODE":
			switch val {
			case "auto", "drm", "desktop":
				c.DisplayMode = val
			}
		case "RUN_USER":
			c.RunUser = val
		case "MPV_EXTRA_ARGS":
			c.MpvExtraArgs = val
		case "BOOT_LOGO":
			c.BootLogo = val
		case "BOOT_LOGO_SECONDS":
			if f, err := strconv.ParseFloat(val, 64); err == nil && f >= 0 {
				c.BootLogoSeconds = f
			}
		case "MPV_GPU_API":
			if val != "" {
				c.MpvGpuAPI = val
			}
		case "MPV_HWDEC":
			if val != "" {
				c.MpvHwdec = val
			}
		case "STALL_TIMEOUT":
			if n, err := strconv.Atoi(val); err == nil && n >= 0 {
				c.StallTimeout = n
			}
		case "MPV_SOCKET":
			if val != "" {
				c.SocketPath = val
			}
		case "STATE_DIR":
			if val != "" {
				c.StateDir = val
			}
		}
	}
	return c, sc.Err()
}

func unquote(s string) string {
	// Kommentar am Zeilenende nur ausserhalb von Anfuehrungszeichen entfernen.
	if !strings.HasPrefix(s, "\"") && !strings.HasPrefix(s, "'") {
		if i := strings.Index(s, " #"); i >= 0 {
			s = strings.TrimSpace(s[:i])
		}
		return s
	}
	quote := s[0]
	if end := strings.IndexByte(s[1:], quote); end >= 0 {
		return s[1 : end+1]
	}
	return strings.Trim(s, "\"'")
}

func isTrue(s string) bool {
	switch strings.ToLower(strings.TrimSpace(s)) {
	case "yes", "true", "1", "on", "ja":
		return true
	}
	return false
}

// ---------------------------------------------------------------------------
// Laufzeit-Einstellungen (ueber das Webinterface aenderbar)
// ---------------------------------------------------------------------------

// Settings sind die im Webinterface aenderbaren Werte. Sie liegen als JSON im
// StateDir und ueberschreiben die entsprechenden Werte aus tvwall.conf.
type Settings struct {
	ImageDuration float64 `json:"image_duration"`
	Mute          bool    `json:"mute"`
}

// Store haelt die Settings im Speicher und schreibt sie atomar auf die Platte.
type Store struct {
	path string
	mu   sync.RWMutex
	s    Settings
	subs []chan struct{}
}

// NewStore laedt settings.json; fehlt die Datei, gelten die Werte aus cfg.
func NewStore(cfg *Config) *Store {
	st := &Store{
		path: cfg.SettingsFile(),
		s:    Settings{ImageDuration: cfg.ImageDuration, Mute: cfg.Mute},
	}
	if data, err := os.ReadFile(st.path); err == nil {
		var loaded Settings
		if json.Unmarshal(data, &loaded) == nil {
			if loaded.ImageDuration > 0 {
				st.s.ImageDuration = loaded.ImageDuration
			}
			st.s.Mute = loaded.Mute
		}
	}
	st.s = clamp(st.s)
	return st
}

func clamp(s Settings) Settings {
	if s.ImageDuration < 0.5 {
		s.ImageDuration = 0.5
	}
	if s.ImageDuration > 3600 {
		s.ImageDuration = 3600
	}
	return s
}

// Get liefert eine Kopie der aktuellen Einstellungen.
func (st *Store) Get() Settings {
	st.mu.RLock()
	defer st.mu.RUnlock()
	return st.s
}

// Set speichert neue Einstellungen und benachrichtigt alle Abonnenten.
func (st *Store) Set(s Settings) (Settings, error) {
	s = clamp(s)
	st.mu.Lock()
	st.s = s
	subs := append([]chan struct{}(nil), st.subs...)
	st.mu.Unlock()

	data, err := json.MarshalIndent(s, "", "  ")
	if err != nil {
		return s, err
	}
	if err := WriteFileAtomic(st.path, append(data, '\n'), 0o664); err != nil {
		return s, fmt.Errorf("settings speichern: %w", err)
	}
	for _, ch := range subs {
		select {
		case ch <- struct{}{}:
		default:
		}
	}
	return s, nil
}

// Subscribe liefert einen Kanal, der bei jeder Aenderung ein Signal bekommt.
func (st *Store) Subscribe() <-chan struct{} {
	ch := make(chan struct{}, 1)
	st.mu.Lock()
	st.subs = append(st.subs, ch)
	st.mu.Unlock()
	return ch
}

// WriteFileAtomic schreibt ueber eine temporaere Datei + rename, damit ein
// mitlesender Prozess niemals halbe Inhalte sieht.
func WriteFileAtomic(path string, data []byte, perm os.FileMode) error {
	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0o775); err != nil {
		return err
	}
	tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*")
	if err != nil {
		return err
	}
	tmpName := tmp.Name()
	defer os.Remove(tmpName)

	if _, err := tmp.Write(data); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Sync(); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Close(); err != nil {
		return err
	}
	if err := os.Chmod(tmpName, perm); err != nil {
		return err
	}
	return os.Rename(tmpName, path)
}
