package config

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"gopkg.in/yaml.v3"
)

type Config struct {
	Version      int      `yaml:"version"`
	Gap          int      `yaml:"gap"`
	ShiftDrag    bool     `yaml:"shift_drag"`
	ActiveLayout string   `yaml:"active_layout"`
	Overlay      Overlay  `yaml:"overlay"`
	Layouts      []Layout `yaml:"layouts"`
	Excluded     []string `yaml:"excluded_apps"`
	Hotkeys      []Hotkey `yaml:"hotkeys"`
}

type Overlay struct {
	Enabled     bool   `yaml:"enabled"`
	Color       string `yaml:"color"`
	Opacity     int    `yaml:"opacity"`
	BorderWidth int    `yaml:"border_width"`
}

type Layout struct {
	Name    string `yaml:"name"`
	Monitor string `yaml:"monitor"`
	Zones   []Zone `yaml:"zones"`
}

type Zone struct {
	Name   string  `yaml:"name"`
	X      float64 `yaml:"x"`
	Y      float64 `yaml:"y"`
	Width  float64 `yaml:"width"`
	Height float64 `yaml:"height"`
}

type Hotkey struct {
	Keys   string `yaml:"keys"`
	Action string `yaml:"action"`
}

func Default() Config {
	return Config{
		Version: 1, Gap: 8, ShiftDrag: true,
		ActiveLayout: "columns",
		Overlay:      Overlay{Enabled: true, Color: "#00AEEF", Opacity: 90, BorderWidth: 10},
		Layouts: []Layout{{Name: "columns", Monitor: "*", Zones: []Zone{
			{Name: "left", X: 0, Y: 0, Width: 50, Height: 100},
			{Name: "right", X: 50, Y: 0, Width: 50, Height: 100},
		}}},
		Hotkeys: []Hotkey{
			{Keys: "win+alt+left", Action: "previous_zone"},
			{Keys: "win+alt+right", Action: "next_zone"},
		},
	}
}

func Load(path string) (Config, error) {
	f, err := os.Open(path)
	if err != nil {
		return Config{}, err
	}
	defer f.Close()
	var c Config
	d := yaml.NewDecoder(f)
	d.KnownFields(true)
	if err := d.Decode(&c); err != nil {
		return Config{}, fmt.Errorf("parse YAML: %w", err)
	}
	c.upgradeLegacyLayouts()
	if err := c.Validate(); err != nil {
		return Config{}, err
	}
	return c, nil
}

func (c Config) Validate() error {
	if c.Version != 1 {
		return fmt.Errorf("version must be 1 (got %d)", c.Version)
	}
	if c.Gap < 0 || c.Gap > 500 {
		return errors.New("gap must be between 0 and 500 pixels")
	}
	if c.Overlay.Enabled {
		if !validHexColor(c.Overlay.Color) {
			return errors.New("overlay.color must use #RRGGBB format")
		}
		if c.Overlay.Opacity < 1 || c.Overlay.Opacity > 100 {
			return errors.New("overlay.opacity must be between 1 and 100")
		}
		if c.Overlay.BorderWidth < 1 || c.Overlay.BorderWidth > 20 {
			return errors.New("overlay.border_width must be between 1 and 20 pixels")
		}
	}
	if len(c.Layouts) == 0 {
		return errors.New("at least one layout is required")
	}
	active := strings.TrimSpace(c.ActiveLayout)
	if active == "" {
		return errors.New("active_layout is required")
	}
	seenMonitors := map[string]bool{}
	activeExists := false
	for li, l := range c.Layouts {
		layoutName := strings.TrimSpace(l.Name)
		if layoutName == "" {
			return fmt.Errorf("layouts[%d].name is required", li)
		}
		if strings.EqualFold(layoutName, active) {
			activeExists = true
		}
		name := strings.TrimSpace(l.Monitor)
		if name == "" {
			return fmt.Errorf("layouts[%d].monitor is required", li)
		}
		key := strings.ToLower(layoutName) + "\x00" + strings.ToLower(name)
		if seenMonitors[key] {
			return fmt.Errorf("duplicate layout named %q for monitor %q", layoutName, name)
		}
		seenMonitors[key] = true
		if len(l.Zones) == 0 {
			return fmt.Errorf("layout %q needs at least one zone", name)
		}
		for zi, z := range l.Zones {
			if z.Width <= 0 || z.Height <= 0 || z.X < 0 || z.Y < 0 || z.X+z.Width > 100.000001 || z.Y+z.Height > 100.000001 {
				return fmt.Errorf("layout %q zone %d must fit within the 0..100 percent work area", name, zi+1)
			}
		}
	}
	if !activeExists {
		return fmt.Errorf("active_layout %q does not match any layout name", c.ActiveLayout)
	}
	for i, h := range c.Hotkeys {
		a := strings.ToLower(strings.TrimSpace(h.Action))
		if a != "next_zone" && a != "previous_zone" && !strings.HasPrefix(a, "zone_") {
			return fmt.Errorf("hotkeys[%d].action must be next_zone, previous_zone, or zone_N", i)
		}
		if strings.HasPrefix(a, "zone_") {
			n, err := strconv.Atoi(strings.TrimPrefix(a, "zone_"))
			if err != nil || n < 1 {
				return fmt.Errorf("hotkeys[%d].action must use a positive zone number", i)
			}
		}
		if strings.TrimSpace(h.Keys) == "" {
			return fmt.Errorf("hotkeys[%d].keys is required", i)
		}
	}
	return nil
}

func (c *Config) upgradeLegacyLayouts() {
	if strings.TrimSpace(c.ActiveLayout) != "" || len(c.Layouts) == 0 {
		return
	}
	for _, l := range c.Layouts {
		if strings.TrimSpace(l.Name) != "" {
			return
		}
	}
	c.ActiveLayout = "default"
	for i := range c.Layouts {
		c.Layouts[i].Name = "default"
	}
}

func validHexColor(s string) bool {
	if len(s) != 7 || s[0] != '#' {
		return false
	}
	_, err := strconv.ParseUint(s[1:], 16, 24)
	return err == nil
}

func DefaultPath(exePath string) string {
	return filepath.Join(filepath.Dir(exePath), "fancywin.yaml")
}

func (c Config) LayoutFor(device string) (Layout, bool) {
	active := strings.TrimSpace(c.ActiveLayout)
	for _, l := range c.Layouts {
		monitor := strings.TrimSpace(l.Monitor)
		if strings.EqualFold(strings.TrimSpace(l.Name), active) && monitor != "*" && strings.EqualFold(monitor, strings.TrimSpace(device)) {
			return l, true
		}
	}
	for _, l := range c.Layouts {
		if strings.EqualFold(strings.TrimSpace(l.Name), active) && strings.TrimSpace(l.Monitor) == "*" {
			return l, true
		}
	}
	return Layout{}, false
}

func (c Config) LayoutNames() []string {
	seen := make(map[string]bool)
	var names []string
	for _, l := range c.Layouts {
		name := strings.TrimSpace(l.Name)
		key := strings.ToLower(name)
		if name != "" && !seen[key] {
			seen[key] = true
			names = append(names, name)
		}
	}
	return names
}

// SetActiveLayout updates only the top-level active_layout line, preserving the
// user's comments, layout formatting, and ordering.
func SetActiveLayout(path, name string) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return err
	}
	encoded, err := yaml.Marshal(strings.TrimSpace(name))
	if err != nil {
		return err
	}
	value := strings.TrimSpace(string(encoded))
	newline := "\n"
	if strings.Contains(string(data), "\r\n") {
		newline = "\r\n"
	}
	lines := strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n")
	replaced := false
	for i, line := range lines {
		if strings.HasPrefix(line, "active_layout:") {
			lines[i] = "active_layout: " + value
			replaced = true
			break
		}
	}
	if !replaced {
		insertAt := 0
		for i, line := range lines {
			if strings.HasPrefix(line, "version:") || strings.HasPrefix(line, "gap:") || strings.HasPrefix(line, "shift_drag:") {
				insertAt = i + 1
			}
		}
		lines = append(lines, "")
		copy(lines[insertAt+1:], lines[insertAt:])
		lines[insertAt] = "active_layout: " + value
	}
	info, err := os.Stat(path)
	if err != nil {
		return err
	}
	return os.WriteFile(path, []byte(strings.Join(lines, newline)), info.Mode().Perm())
}

func (c Config) IsExcluded(exe string) bool {
	// Windows paths may be validated on another OS during cross-compilation.
	normalized := strings.ReplaceAll(exe, `\`, "/")
	base := strings.ToLower(normalized[strings.LastIndex(normalized, "/")+1:])
	for _, item := range c.Excluded {
		needle := strings.ToLower(strings.TrimSpace(item))
		if needle != "" && (base == needle || (!strings.Contains(needle, ".") && strings.Contains(base, needle))) {
			return true
		}
	}
	return false
}
