package httpapi

import (
	"encoding/json"
	"strings"

	"github.com/wangjia/pangolin/server/internal/dpcred"
	"github.com/wangjia/pangolin/server/internal/nodes"
)

// ClientConfigOpts carries per-request rendering options for BuildClientConfig.
type ClientConfigOpts struct {
	// SplitCN 开启国内分流:命中 geoip-cn/geosite-cn 的 IP/域名直连(不走隧道)。
	SplitCN bool
	// RulesBaseURL 是控制面对外公网基址(如 "http://node:8080"),rule_set 的 .srs
	// 从 <base>/v1/rules/*.srs 下载。SplitCN 生效需此项非空(否则分流静默跳过)。
	RulesBaseURL string
}

// BuildClientConfig renders a complete sing-box CLIENT configuration JSON that
// the client app passes verbatim to the local tunnel kernel.
//
// Design rule (ARCHITECTURE.md §3.1): the Dart/Flutter client MUST NOT assemble
// or modify the config — it is rendered here, server-side, and returned raw.
//
// Parameters:
//   - node: the target node row (provides endpoint, keys, ports)
//   - dpUUID: the authenticated user's data-plane UUID (used as VLESS uuid)
//   - deriveKey: shared HMAC key used by both server and agent to derive the
//     Hysteria2 password from dp_uuid (must equal PANGOLIN_AGENT_DERIVE_KEY)
//   - opts: 渲染选项(国内分流等),见 ClientConfigOpts
func BuildClientConfig(node *nodes.NodeRow, dpUUID, deriveKey string, opts ClientConfigOpts) ([]byte, error) {
	// Parse host:port from endpoint; endpoint format is "host:port".
	host, _ := splitHostPort(node.Endpoint)
	if host == "" {
		host = node.Endpoint
	}

	realityPublicKey := node.RealityPBK
	realityShortID := node.RealityShortID
	// Hysteria2 仅在节点确实配置了 hy2 端口时才下发。否则不出 hy2-out:
	// 它会指向 REALITY 的 TCP 端口、而服务端又无 Hy2 监听,导致 urltest
	// 一直探测一个死成员(connection reset by peer)。
	hy2Enabled := node.Hy2Port.Valid && node.Hy2Port.Int32 > 0
	hy2Password := dpcred.DeriveHy2Password(dpUUID, deriveKey)

	// REALITY outbound (VLESS + REALITY TLS, TCP 443).
	realityOut := map[string]any{
		"type":        "vless",
		"tag":         "reality-out",
		"server":      host,
		"server_port": 11443, // REALITY always uses port from endpoint
		"uuid":        dpUUID,
		"flow":        dpcred.DefaultFlow,
		"tls": map[string]any{
			"enabled":     true,
			"server_name": node.RealitySNI,
			"utls": map[string]any{
				"enabled":     true,
				"fingerprint": "chrome",
			},
			"reality": map[string]any{
				"enabled":    true,
				"public_key": realityPublicKey,
				"short_id":   realityShortID,
			},
		},
	}

	// Parse the REALITY listen port from endpoint.
	if _, portStr := splitHostPort(node.Endpoint); portStr != "" {
		port := 0
		for _, ch := range portStr {
			if ch >= '0' && ch <= '9' {
				port = port*10 + int(ch-'0')
			}
		}
		if port > 0 {
			realityOut["server_port"] = port
		}
	}

	// Hysteria2 outbound (UDP 443).
	hy2Out := map[string]any{
		"type":        "hysteria2",
		"tag":         "hy2-out",
		"server":      host,
		"server_port": node.Hy2Port.Int32,
		"password":    hy2Password,
		"tls": map[string]any{
			"enabled":     true,
			"alpn":        []string{"h3"},
			"server_name": node.RealitySNI,
			// 节点 hy2 用自签证书(方案①);两端自有,跳过 CA 校验,
			// 服务端鉴权靠 per-user 派生的 hy2 密码。
			"insecure": true,
		},
	}

	// TUN inbound with kill-switch (strict_route).
	tunIn := map[string]any{
		"type":         "tun",
		"tag":          "tun-in",
		"address":      []string{"172.19.0.1/30"},
		"mtu":          9000,
		"auto_route":   true,
		"strict_route": true,
		"stack":        "system",
	}

	// 代理出站集合:REALITY 必有;Hy2 仅在启用时加入(否则不进配置/探测组)。
	proxyTags := []string{"reality-out"}
	proxyOutbounds := []any{realityOut}
	if hy2Enabled {
		proxyTags = append(proxyTags, "hy2-out")
		proxyOutbounds = append(proxyOutbounds, hy2Out)
	}

	// urltest auto-select outbound.
	autoBest := map[string]any{
		"type":      "urltest",
		"tag":       "auto",
		"outbounds": proxyTags,
		"url":       "https://www.gstatic.com/generate_204",
		"interval":  "3m",
		"tolerance": 50,
	}

	// Route: LAN direct;(可选)国内 IP/域名直连;其余 via auto。
	routeRules := []any{
		map[string]any{
			"ip_cidr":  []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8"},
			"outbound": "direct",
		},
	}
	route := map[string]any{
		"final":                 "auto",
		"auto_detect_interface": true,
		// sing-box 1.12+ 要求显式声明出站域名用哪个 DNS 解析,缺失即 FATAL。
		"default_domain_resolver": map[string]any{"server": "local"},
	}
	// 国内分流(#5):命中 geoip-cn / geosite-cn → 直连(不走隧道),省流量 + 国内快。
	// rule_set 走控制面自托管(国内可达,客户端反正连控制面);download_detour:direct
	// 让 sing-box 直连下载 .srs(不经隧道,启动期隧道还没起)。
	if opts.SplitCN && opts.RulesBaseURL != "" {
		base := strings.TrimRight(opts.RulesBaseURL, "/")
		routeRules = append(routeRules, map[string]any{
			"rule_set": []string{"geoip-cn", "geosite-cn"},
			"outbound": "direct",
		})
		route["rule_set"] = []any{
			map[string]any{"tag": "geoip-cn", "type": "remote", "format": "binary",
				"url": base + "/v1/rules/geoip-cn.srs", "download_detour": "direct"},
			map[string]any{"tag": "geosite-cn", "type": "remote", "format": "binary",
				"url": base + "/v1/rules/geosite-cn.srs", "download_detour": "direct"},
		}
	}
	route["rules"] = routeRules

	// DNS: remote over tunnel, local for domestic.
	// sing-box 1.12+ DNS server format(type+server);旧的 address 串格式在
	// 1.13 已 FATAL 拒绝(legacy DNS servers deprecated)。
	dns := map[string]any{
		"servers": []any{
			map[string]any{"tag": "remote", "type": "tls", "server": "8.8.8.8", "detour": "auto"},
			// local 不带 detour:sing-box 1.12 拒绝 DNS detour 到空 direct 出站
			// (FATAL: detour to an empty direct outbound makes no sense)。
			map[string]any{"tag": "local", "type": "udp", "server": "223.5.5.5"},
		},
		"final":    "remote",
		"strategy": "ipv4_only",
	}

	cfg := map[string]any{
		// timestamp=false:客户端日志出口(logLine)已统一加时间戳,
		// 关掉 sing-box 自带时间戳避免一行打印两个时间。
		"log":      map[string]any{"level": "warn", "timestamp": false},
		"inbounds": []any{tunIn},
		"outbounds": append(proxyOutbounds,
			autoBest,
			map[string]any{"type": "block", "tag": "block"},
			map[string]any{"type": "direct", "tag": "direct"},
		),
		"route": route,
		"dns":   dns,
	}

	return json.Marshal(cfg)
}

// splitHostPort splits "host:port" into (host, port). Returns ("", "") on failure.
func splitHostPort(s string) (host, port string) {
	i := strings.LastIndexByte(s, ':')
	if i < 0 {
		return s, ""
	}
	return s[:i], s[i+1:]
}
