package config

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
)

// Protocol identifies a VPN/proxy backend.
type Protocol string

const (
	ProtocolNaive     Protocol = "naive"
	ProtocolHysteria2 Protocol = "hysteria2"
)

// Config is the top-level client configuration.
type Config struct {
	// Active is the name of the profile to use by default.
	Active string `json:"active"`

	// SystemProxy enables Windows Internet Settings / WinHTTP proxy on connect.
	SystemProxy bool `json:"system_proxy"`

	// BinDir is where protocol binaries (naive.exe / hysteria.exe) are stored.
	BinDir string `json:"bin_dir,omitempty"`

	// SubscriptionURL pulls share links (one per line or base64) and imports profiles.
	SubscriptionURL string `json:"subscription_url,omitempty"`

	Profiles []Profile `json:"profiles"`
}

// Profile describes one connection endpoint.
type Profile struct {
	Name     string   `json:"name"`
	Protocol Protocol `json:"protocol"`

	// Naive: full proxy URI, e.g. https://user:pass@example.com or quic://...
	Proxy string `json:"proxy,omitempty"`

	// Local listen URIs passed to naive. Default: socks + http for system proxy.
	Listen []string `json:"listen,omitempty"`

	// Extra Naive options
	InsecureConcurrency int               `json:"insecure_concurrency,omitempty"`
	ExtraHeaders        string            `json:"extra_headers,omitempty"`
	HostResolverRules   string            `json:"host_resolver_rules,omitempty"`
	NoPostQuantum       bool              `json:"no_post_quantum,omitempty"`
	Log                 string            `json:"log,omitempty"`
	Env                 map[string]string `json:"env,omitempty"`

	// Hysteria 2 options (client-side). Masquerade on server is separate;
	// here SNI/insecure/pin emulate HTTPS look + salamander/gecko obfuscation.
	Hy2Congestion    string `json:"hy2_congestion,omitempty"`     // bbr | reno
	Hy2BBRProfile    string `json:"hy2_bbr_profile,omitempty"`    // standard | conservative | aggressive
	Hy2BandwidthUp   string `json:"hy2_bandwidth_up,omitempty"`   // empty = BBR; set for Brutal
	Hy2BandwidthDown string `json:"hy2_bandwidth_down,omitempty"`
	Hy2Obfs          string `json:"hy2_obfs,omitempty"` // salamander | gecko
	Hy2ObfsPassword  string `json:"hy2_obfs_password,omitempty"`
	Hy2SNI           string `json:"hy2_sni,omitempty"`
	Hy2Insecure      bool   `json:"hy2_insecure,omitempty"`
	Hy2PinSHA256     string `json:"hy2_pin_sha256,omitempty"`
	Hy2FastOpen      bool   `json:"hy2_fast_open,omitempty"`
	Hy2Lazy          bool   `json:"hy2_lazy,omitempty"`
	Hy2HopInterval   string `json:"hy2_hop_interval,omitempty"` // e.g. 30s
	Hy2ALPN          string `json:"hy2_alpn,omitempty"`
}

// DefaultListen returns listen addresses suitable for Windows system proxy.
func (p Profile) DefaultListen() []string {
	if len(p.Listen) > 0 {
		return p.Listen
	}
	return []string{
		"socks://127.0.0.1:1080",
		"http://127.0.0.1:1081",
	}
}

// HTTPListenHostPort returns host:port of the first http:// listen URI, if any.
func (p Profile) HTTPListenHostPort() (string, bool) {
	for _, u := range p.DefaultListen() {
		if hostPort, ok := parseListenHostPort(u, "http"); ok {
			return hostPort, true
		}
	}
	return "", false
}

// SOCKSListenHostPort returns host:port of the first socks:// listen URI, if any.
func (p Profile) SOCKSListenHostPort() (string, bool) {
	for _, u := range p.DefaultListen() {
		if hostPort, ok := parseListenHostPort(u, "socks"); ok {
			return hostPort, true
		}
	}
	return "", false
}

func parseListenHostPort(uri, wantScheme string) (string, bool) {
	// Minimal parse: scheme://[user:pass@]host:port
	schemeSep := "://"
	i := index(uri, schemeSep)
	if i < 0 {
		return "", false
	}
	scheme := uri[:i]
	if scheme != wantScheme {
		return "", false
	}
	rest := uri[i+len(schemeSep):]
	if at := lastIndex(rest, "@"); at >= 0 {
		rest = rest[at+1:]
	}
	if rest == "" {
		return "", false
	}
	return rest, true
}

func index(s, substr string) int {
	for i := 0; i+len(substr) <= len(s); i++ {
		if s[i:i+len(substr)] == substr {
			return i
		}
	}
	return -1
}

func lastIndex(s, substr string) int {
	for i := len(s) - len(substr); i >= 0; i-- {
		if s[i:i+len(substr)] == substr {
			return i
		}
	}
	return -1
}

// Load reads and validates a config file.
func Load(path string) (*Config, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("read config: %w", err)
	}
	data = stripUTF8BOM(data)
	var cfg Config
	if err := json.Unmarshal(data, &cfg); err != nil {
		return nil, fmt.Errorf("parse config: %w", err)
	}
	if cfg.BinDir == "" {
		cfg.BinDir = "bin"
	}
	if err := cfg.Validate(); err != nil {
		return nil, err
	}
	return &cfg, nil
}

func stripUTF8BOM(data []byte) []byte {
	if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
		return data[3:]
	}
	return data
}

// Validate checks required fields.
func (c *Config) Validate() error {
	if len(c.Profiles) == 0 {
		return fmt.Errorf("config: no profiles defined")
	}
	if c.Active == "" {
		c.Active = c.Profiles[0].Name
	}
	if _, err := c.ActiveProfile(); err != nil {
		return err
	}
	for i, p := range c.Profiles {
		if p.Name == "" {
			return fmt.Errorf("config: profile[%d] missing name", i)
		}
		switch p.Protocol {
		case ProtocolNaive, ProtocolHysteria2:
			// Proxy may be empty until the user pastes a share link in the UI.
		case "":
			c.Profiles[i].Protocol = ProtocolNaive
		default:
			return fmt.Errorf("config: profile %q: unsupported protocol %q", p.Name, p.Protocol)
		}
	}
	return nil
}

// ActiveProfile returns the selected profile.
func (c *Config) ActiveProfile() (*Profile, error) {
	for i := range c.Profiles {
		if c.Profiles[i].Name == c.Active {
			return &c.Profiles[i], nil
		}
	}
	return nil, fmt.Errorf("config: active profile %q not found", c.Active)
}

// ResolveBinDir returns an absolute bin directory.
// Relative paths are resolved next to the executable (portable install layout).
func ResolveBinDir(cfgPath, binDir string) (string, error) {
	if filepath.IsAbs(binDir) {
		return binDir, nil
	}
	if exe, err := os.Executable(); err == nil {
		return filepath.Abs(filepath.Join(filepath.Dir(exe), binDir))
	}
	base := filepath.Dir(cfgPath)
	if base == "." || base == "" {
		cwd, err := os.Getwd()
		if err != nil {
			return "", err
		}
		base = cwd
	} else if filepath.Base(base) == "configs" {
		base = filepath.Dir(base)
	}
	return filepath.Abs(filepath.Join(base, binDir))
}

// Example returns a starter configuration.
func Example() Config {
	return Config{
		Active:      "naive-main",
		SystemProxy: true,
		BinDir:      "bin",
		Profiles: []Profile{
			{
				Name:     "naive-main",
				Protocol: ProtocolNaive,
				Proxy:    "",
				Listen: []string{
					"socks://127.0.0.1:1080",
					"http://127.0.0.1:1081",
				},
			},
		},
	}
}

// WriteExample writes example config to path.
func WriteExample(path string) error {
	return Save(path, Example())
}

// Save writes config as indented JSON.
func Save(path string, cfg Config) error {
	if err := cfg.Validate(); err != nil {
		return err
	}
	data, err := json.MarshalIndent(cfg, "", "  ")
	if err != nil {
		return err
	}
	data = append(data, '\n')
	dir := filepath.Dir(path)
	if dir != "." && dir != "" {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return err
		}
	}
	return os.WriteFile(path, data, 0o600)
}

// SetActiveProxy updates the active profile proxy URI.
func (c *Config) SetActiveProxy(proxy string) error {
	for i := range c.Profiles {
		if c.Profiles[i].Name == c.Active {
			c.Profiles[i].Proxy = proxy
			return nil
		}
	}
	return fmt.Errorf("active profile %q not found", c.Active)
}

// LocateConfig finds configs/config.json next to the executable or in cwd.
func LocateConfig() (string, error) {
	candidates := []string{}
	if exe, err := os.Executable(); err == nil {
		dir := filepath.Dir(exe)
		candidates = append(candidates,
			filepath.Join(dir, "configs", "config.json"),
			filepath.Join(dir, "config.json"),
		)
	}
	if cwd, err := os.Getwd(); err == nil {
		candidates = append(candidates, filepath.Join(cwd, "configs", "config.json"))
	}
	for _, p := range candidates {
		if st, err := os.Stat(p); err == nil && !st.IsDir() {
			return p, nil
		}
	}
	// Default path next to executable for first-run create.
	if exe, err := os.Executable(); err == nil {
		return filepath.Join(filepath.Dir(exe), "configs", "config.json"), nil
	}
	return filepath.Join("configs", "config.json"), nil
}
