package config

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

	"github.com/spf13/viper"
	"gopkg.in/yaml.v3"
)

type Config struct {
	Hosts map[string]HostConfig `yaml:"hosts"`
}

type HostConfig struct {
	Hostname    string   `yaml:"hostname"`
	Token       string   `yaml:"token"`
	User        string   `yaml:"user,omitempty"`
	GitProtocol string   `yaml:"git_protocol,omitempty"`
	MatchDirs   []string `yaml:"match_dirs,omitempty"`
	Order       int      `yaml:"-"` // config file order, set at load time
}

func GetConfigDir() (string, error) {
	if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
		return filepath.Join(xdgConfigHome, "fj"), nil
	}
	home, err := os.UserHomeDir()
	if err != nil {
		return "", err
	}
	return filepath.Join(home, ".config", "fj"), nil
}

func GetConfigPath() (string, error) {
	dir, err := GetConfigDir()
	if err != nil {
		return "", err
	}
	return filepath.Join(dir, "config.yaml"), nil
}

func Load() (*Config, error) {
	path, err := GetConfigPath()
	if err != nil {
		return nil, err
	}
	return LoadFromPath(path)
}

func LoadFromPath(path string) (*Config, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return &Config{Hosts: make(map[string]HostConfig)}, nil
		}
		return nil, err
	}

	var cfg Config
	if err := yaml.Unmarshal(data, &cfg); err != nil {
		return nil, err
	}

	if cfg.Hosts == nil {
		cfg.Hosts = make(map[string]HostConfig)
	}

	// Parse again with yaml.Node to capture config file order for hosts
	assignHostOrder(&cfg, data)

	return &cfg, nil
}

// assignHostOrder walks the YAML document tree to find the "hosts" mapping
// and stamps each HostConfig.Order with its position in the file.
func assignHostOrder(cfg *Config, data []byte) {
	var doc yaml.Node
	if err := yaml.Unmarshal(data, &doc); err != nil || len(doc.Content) == 0 {
		return
	}
	root := doc.Content[0] // mapping node
	if root.Kind != yaml.MappingNode {
		return
	}
	for i := 0; i+1 < len(root.Content); i += 2 {
		if root.Content[i].Value == "hosts" {
			hostsNode := root.Content[i+1]
			if hostsNode.Kind != yaml.MappingNode {
				return
			}
			order := 0
			for j := 0; j+1 < len(hostsNode.Content); j += 2 {
				key := hostsNode.Content[j].Value
				if h, ok := cfg.Hosts[key]; ok {
					h.Order = order
					cfg.Hosts[key] = h
					order++
				}
			}
			return
		}
	}
}

func (c *Config) Save() error {
	path, err := GetConfigPath()
	if err != nil {
		return err
	}
	return c.SaveToPath(path)
}

func (c *Config) SaveToPath(path string) error {
	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return err
	}

	data, err := yaml.Marshal(c)
	if err != nil {
		return err
	}

	return os.WriteFile(path, data, 0600)
}

// GetHost resolves the hostname to use for API client creation.
// Priority order:
// 1. Explicitly provided hostname parameter
// 2. CLI flag (--hostname)
// 3. Environment variable (FGJ_HOST)
// 4. Auto-detected hostname from git remote
// 5. match_dirs lookup (longest prefix match)
// 6. Default to codeberg.org
func (c *Config) GetHost(hostname string, detectedHost string, cwd string) (HostConfig, error) {
	if hostname == "" {
		hostname = viper.GetString("hostname")
	}

	if hostname == "" {
		hostname = os.Getenv("FGJ_HOST")
	}

	if hostname == "" {
		hostname = detectedHost
	}

	if hostname == "" {
		hostname = c.ResolveHostByPath(cwd)
	}

	if hostname == "" {
		hostname = "codeberg.org"
	}

	host, ok := c.Hosts[hostname]
	if !ok {
		return HostConfig{}, fmt.Errorf("no configuration found for host %s", hostname)
	}

	return host, nil
}

// ResolveHostByPath finds the host whose match_dirs entry is the longest
// prefix of cwd. Returns "" if no match is found.
// Both cwd and match_dirs entries are resolved through filepath.EvalSymlinks
// to handle symlinks (e.g. macOS /tmp → /private/tmp).
// On ties (same prefix length from multiple hosts), the host appearing first
// in the config file wins and a warning is printed to stderr.
func (c *Config) ResolveHostByPath(cwd string) string {
	if cwd == "" {
		return ""
	}

	// Resolve symlinks in cwd so /tmp becomes /private/tmp on macOS, etc.
	if resolved, err := filepath.EvalSymlinks(cwd); err == nil {
		cwd = resolved
	}

	bestHost := ""
	bestLen := 0
	bestOrder := 0
	tied := false

	for hostname, host := range c.Hosts {
		for _, dir := range host.MatchDirs {
			if dir == "" {
				continue
			}
			// Expand ~ to home directory
			dir = expandHome(dir)
			// Resolve symlinks in the configured dir as well
			if resolved, err := filepath.EvalSymlinks(dir); err == nil {
				dir = resolved
			}
			// Normalize: ensure trailing slash for prefix matching
			prefix := dir
			if !strings.HasSuffix(prefix, "/") {
				prefix += "/"
			}
			// Match if cwd equals dir exactly or is under it
			if cwd == dir || strings.HasPrefix(cwd, prefix) {
				if len(dir) > bestLen {
					bestLen = len(dir)
					bestHost = hostname
					bestOrder = host.Order
					tied = false
				} else if len(dir) == bestLen && hostname != bestHost {
					// Tie — pick the host with the lower Order (earlier in config)
					if host.Order < bestOrder {
						bestHost = hostname
						bestOrder = host.Order
					}
					tied = true
				}
			}
		}
	}

	if tied {
		fmt.Fprintf(os.Stderr, "warning: multiple hosts match directory %q with the same specificity; using %s (first in config)\n", cwd, bestHost)
	}

	return bestHost
}

// expandHome replaces a leading ~ with the user's home directory.
func expandHome(path string) string {
	if path == "~" || strings.HasPrefix(path, "~/") {
		home, err := os.UserHomeDir()
		if err != nil {
			return path
		}
		return filepath.Join(home, path[1:])
	}
	return path
}

func (c *Config) SetHost(hostname string, host HostConfig) {
	if c.Hosts == nil {
		c.Hosts = make(map[string]HostConfig)
	}
	c.Hosts[hostname] = host
}
