package sboxrun

import (
	"encoding/json"
	"fmt"
)

// Config captures the user-visible proxy settings + which processes
// to route through it. Everything else (TUN interface, log level,
// Clash API endpoint) is hard-coded sensible defaults.
type Config struct {
	ProxyHost    string // upstream SOCKS5 host
	ProxyPort    int    // upstream SOCKS5 port
	UseAuth      bool
	Login        string
	Password     string
	TargetProcs  []string // exe names to route via upstream (e.g. ["Discord.exe"])
	ClashAPIPort int      // 0 → 9090 default
	LogLevel     string   // "info" | "debug" | "warn" — empty → "info"
	LogPath      string   // absolute path for sing-box log output (empty = sing-box stdout, lost when admin-detached)
}

// BuildSingBoxConfig generates the sing-box JSON config string. It's
// a minimal config: TUN inbound (with auto_route + WFP per-process
// rule), SOCKS5 outbound to upstream, direct outbound for everything
// else, and a route rule that sends TargetProcs through the SOCKS5.
//
// Clash API on 127.0.0.1:9090 (or ClashAPIPort) lets the GUI poll
// connection stats live.
func BuildSingBoxConfig(c Config) (string, error) {
	if c.ProxyHost == "" || c.ProxyPort == 0 {
		return "", fmt.Errorf("ProxyHost and ProxyPort are required")
	}
	if len(c.TargetProcs) == 0 {
		return "", fmt.Errorf("at least one target process is required")
	}
	logLevel := c.LogLevel
	if logLevel == "" {
		logLevel = "info"
	}
	clashPort := c.ClashAPIPort
	if clashPort == 0 {
		clashPort = 9090
	}

	upstream := map[string]any{
		"type":        "socks",
		"tag":         "upstream",
		"server":      c.ProxyHost,
		"server_port": c.ProxyPort,
		"version":     "5",
		"udp_over_tcp": false,
	}
	if c.UseAuth {
		upstream["username"] = c.Login
		upstream["password"] = c.Password
	}

	logBlock := map[string]any{
		"level":     logLevel,
		"timestamp": true,
	}
	if c.LogPath != "" {
		logBlock["output"] = c.LogPath
	}
	cfg := map[string]any{
		"log": logBlock,
		"inbounds": []any{
			map[string]any{
				"type":           "tun",
				"tag":            "tun-in",
				"interface_name": "drover-tun",
				"address":        []string{"172.18.0.1/30"},
				"auto_route":     true,
				"strict_route":   false,
				"stack":          "system",
				"sniff":          true,
			},
		},
		"outbounds": []any{
			upstream,
			map[string]any{"type": "direct", "tag": "direct"},
		},
		"route": map[string]any{
			"auto_detect_interface": true,
			"final":                 "direct",
			"rules": []any{
				// 1. Domain rule for sniffed SNI (works when sniffing
				// catches the ClientHello before route decision).
				map[string]any{
					"domain_suffix": []string{
						"discord.com",
						"discord.gg",
						"discord.media",
						"discordapp.com",
						"discordapp.net",
						"discord.dev",
					},
					"outbound": "upstream",
				},
				// 2. IP-CIDR fallback — sing-box on Windows TUN
				// sometimes misattributes the source process for
				// Discord's in-process Rust updater (gets attributed
				// to steam.exe or similar), so even with the right
				// process_name list the updater's TLS connection to
				// updates.discord.com (Fastly: 199.232.x.x) goes
				// direct and gets RKN-blocked. Force the major
				// Discord/Cloudflare/Fastly ranges through upstream
				// regardless of which process the kernel claims sent
				// them.
				map[string]any{
					"ip_cidr": []string{
						// Fastly (updates.discord.com)
						"151.101.0.0/16",
						"199.232.0.0/16",
						"185.199.108.0/22",
						// Cloudflare (Discord gateway, CDN, media)
						"162.158.0.0/15",
						"162.159.0.0/16",
						"104.16.0.0/13",
						"104.24.0.0/14",
						"172.64.0.0/13",
						"131.0.72.0/22",
					},
					"outbound": "upstream",
				},
				// 3. Process-name rule — covers Discord traffic to
				// non-Cloudflare destinations (RTC voice, etc).
				map[string]any{
					"process_name": c.TargetProcs,
					"outbound":     "upstream",
				},
			},
		},
		"experimental": map[string]any{
			"clash_api": map[string]any{
				"external_controller": fmt.Sprintf("127.0.0.1:%d", clashPort),
			},
		},
	}

	out, err := json.MarshalIndent(cfg, "", "  ")
	if err != nil {
		return "", err
	}
	return string(out), nil
}
