package protocol

import (
	"fmt"
	"strings"
	"xboard-go/internal/model"
	"xboard-go/internal/service"

	"github.com/goccy/go-yaml"
)

func GenerateClash(servers []model.Server, user model.User) (string, error) {
	return GenerateClashWithTemplate("clash", servers, user)
}

func GenerateClashWithTemplate(templateName string, servers []model.Server, user model.User) (string, error) {
	template := strings.TrimSpace(service.GetSubscribeTemplate(templateName))
	if template == "" {
		return generateClashFallback(servers, user), nil
	}

	var config map[string]any
	if err := yaml.Unmarshal([]byte(template), &config); err != nil {
		return generateClashFallback(servers, user), nil
	}

	proxies := make([]any, 0, len(servers))
	proxyNames := make([]string, 0, len(servers))
	for _, s := range servers {
		conf := service.BuildNodeConfig(&s)
		password := service.GenerateServerPassword(&s, &user)
		proxy := buildClashProxy(templateName, conf, password)
		if proxy == nil {
			continue
		}
		proxies = append(proxies, proxy)
		proxyNames = append(proxyNames, conf.Name)
	}

	config["proxies"] = append(anySlice(config["proxies"]), proxies...)
	config["proxy-groups"] = mergeClashProxyGroups(anySlice(config["proxy-groups"]), proxyNames)

	if _, ok := config["rules"]; !ok {
		config["rules"] = []any{"MATCH,Proxy"}
	}

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

	output := strings.ReplaceAll(string(data), "$app_name", service.MustGetString("app_name", "XBoard"))
	return output, nil
}

func buildClashProxy(templateName string, conf service.NodeServerConfig, password string) map[string]any {
	switch conf.Protocol {
	case "shadowsocks":
		cipher, _ := conf.Cipher.(string)
		if templateName == "clash" {
			switch cipher {
			case "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305":
			default:
				return nil
			}
		}
		return map[string]any{
			"name":     conf.Name,
			"type":     "ss",
			"server":   conf.RawHost,
			"port":     conf.Port,
			"cipher":   cipher,
			"password": password,
			"udp":      true,
		}
	case "vmess":
		return map[string]any{
			"name":    conf.Name,
			"type":    "vmess",
			"server":  conf.RawHost,
			"port":    conf.Port,
			"uuid":    password,
			"alterId": 0,
			"cipher":  "auto",
			"udp":     true,
		}
	case "vless":
		if templateName != "clashmeta" {
			return nil
		}
		proxy := map[string]any{
			"name":       conf.Name,
			"type":       "vless",
			"server":     conf.RawHost,
			"port":       conf.Port,
			"uuid":       password,
			"alterId":    0,
			"cipher":     "auto",
			"udp":        true,
			"flow":       toClashString(conf.Flow),
			"encryption": "none",
			"tls":        false,
		}
		switch toClashInt(conf.Tls) {
		case 1:
			proxy["tls"] = true
			if fp := tlsFingerprint(conf.UTLS); fp != "" {
				proxy["client-fingerprint"] = fp
			}
			if tlsSettings, ok := conf.TlsSettings.(map[string]any); ok {
				proxy["skip-cert-verify"] = toClashBool(tlsSettings["allow_insecure"])
				if serverName := toClashString(tlsSettings["server_name"]); serverName != "" {
					proxy["servername"] = serverName
				}
			}
		case 2:
			proxy["tls"] = true
			if fp := tlsFingerprint(conf.UTLS); fp != "" {
				proxy["client-fingerprint"] = fp
			}
			if tlsSettings, ok := conf.TlsSettings.(map[string]any); ok {
				proxy["skip-cert-verify"] = toClashBool(tlsSettings["allow_insecure"])
				if serverName := toClashString(tlsSettings["server_name"]); serverName != "" {
					proxy["servername"] = serverName
				}
				proxy["reality-opts"] = map[string]any{
					"public-key": toClashString(tlsSettings["public_key"]),
					"short-id":   toClashString(tlsSettings["short_id"]),
				}
			}
		}
		proxy["network"] = "tcp"
		if settings, ok := conf.NetworkSettings.(map[string]any); ok {
			switch toClashString(conf.Network) {
			case "", "tcp":
				if header, ok := settings["header"].(map[string]any); ok && toClashString(header["type"]) == "http" {
					proxy["network"] = "http"
					httpOpts := map[string]any{}
					if request, ok := header["request"].(map[string]any); ok {
						if headers, ok := request["headers"].(map[string]any); ok && len(headers) > 0 {
							httpOpts["headers"] = headers
						}
						if paths := clashPathList(request["path"]); len(paths) > 0 {
							httpOpts["path"] = paths
						}
					}
					if len(httpOpts) > 0 {
						proxy["http-opts"] = httpOpts
					}
				}
			case "ws":
				proxy["network"] = "ws"
				wsOpts := map[string]any{}
				if path := toClashString(settings["path"]); path != "" {
					wsOpts["path"] = path
				}
				if headers, ok := settings["headers"].(map[string]any); ok {
					if host := toClashString(headers["Host"]); host != "" {
						wsOpts["headers"] = map[string]any{"Host": host}
					}
				}
				if len(wsOpts) > 0 {
					proxy["ws-opts"] = wsOpts
				}
			case "grpc":
				proxy["network"] = "grpc"
				if serviceName := toClashString(settings["serviceName"]); serviceName != "" {
					proxy["grpc-opts"] = map[string]any{"grpc-service-name": serviceName}
				}
			case "h2":
				proxy["network"] = "h2"
				h2Opts := map[string]any{}
				if path := toClashString(settings["path"]); path != "" {
					h2Opts["path"] = path
				}
				if hosts := clashHostList(settings["host"]); len(hosts) > 0 {
					h2Opts["host"] = hosts
				}
				if len(h2Opts) > 0 {
					proxy["h2-opts"] = h2Opts
				}
			case "httpupgrade":
				proxy["network"] = "ws"
				wsOpts := map[string]any{
					"v2ray-http-upgrade": true,
				}
				if path := toClashString(settings["path"]); path != "" {
					wsOpts["path"] = path
				}
				host := toClashString(settings["host"])
				if host == "" {
					host = conf.RawHost
				}
				if host != "" {
					wsOpts["headers"] = map[string]any{"Host": host}
				}
				proxy["ws-opts"] = wsOpts
			default:
				if network := toClashString(conf.Network); network != "" {
					proxy["network"] = network
				}
			}
		} else if network := toClashString(conf.Network); network != "" {
			proxy["network"] = network
		}
		if smux := buildClashSmux(conf.Multiplex); smux != nil {
			proxy["smux"] = smux
		}
		return proxy
	case "trojan":
		return map[string]any{
			"name":     conf.Name,
			"type":     "trojan",
			"server":   conf.RawHost,
			"port":     conf.Port,
			"password": password,
			"udp":      true,
		}
	default:
		return nil
	}
}

func mergeClashProxyGroups(groups []any, proxyNames []string) []any {
	if len(groups) == 0 {
		return []any{
			map[string]any{
				"name":    "Proxy",
				"type":    "select",
				"proxies": append([]any{"DIRECT"}, stringsToAny(proxyNames)...),
			},
		}
	}

	for index, item := range groups {
		group, ok := item.(map[string]any)
		if !ok {
			continue
		}

		group["proxies"] = appendUniqueAny(anySlice(group["proxies"]), stringsToAny(proxyNames)...)
		groups[index] = group
	}
	return groups
}

func anySlice(value any) []any {
	switch typed := value.(type) {
	case nil:
		return []any{}
	case []any:
		return append([]any{}, typed...)
	case []string:
		result := make([]any, 0, len(typed))
		for _, item := range typed {
			result = append(result, item)
		}
		return result
	default:
		return []any{}
	}
}

func stringsToAny(values []string) []any {
	result := make([]any, 0, len(values))
	for _, value := range values {
		result = append(result, value)
	}
	return result
}

func appendUniqueAny(base []any, values ...any) []any {
	existing := make(map[string]struct{}, len(base))
	for _, item := range base {
		existing[fmt.Sprint(item)] = struct{}{}
	}

	for _, item := range values {
		key := fmt.Sprint(item)
		if _, ok := existing[key]; ok {
			continue
		}
		base = append(base, item)
		existing[key] = struct{}{}
	}
	return base
}

func clashPathList(value any) []string {
	switch typed := value.(type) {
	case []string:
		return append([]string{}, typed...)
	case []any:
		result := make([]string, 0, len(typed))
		for _, item := range typed {
			text := toClashString(item)
			if text != "" {
				result = append(result, text)
			}
		}
		return result
	case string:
		if typed == "" {
			return nil
		}
		return []string{typed}
	default:
		return nil
	}
}

func clashHostList(value any) []string {
	switch typed := value.(type) {
	case []string:
		return append([]string{}, typed...)
	case []any:
		result := make([]string, 0, len(typed))
		for _, item := range typed {
			text := toClashString(item)
			if text != "" {
				result = append(result, text)
			}
		}
		return result
	case string:
		if typed == "" {
			return nil
		}
		return []string{typed}
	default:
		return nil
	}
}

func buildClashSmux(value any) map[string]any {
	settings, ok := value.(map[string]any)
	if !ok || !toClashBool(settings["enabled"]) {
		return nil
	}

	smux := map[string]any{
		"enabled": true,
	}
	if protocol := toClashString(settings["protocol"]); protocol != "" {
		smux["protocol"] = protocol
	}
	if maxConnections := toClashInt(settings["max_connections"]); maxConnections > 0 {
		smux["max-connections"] = maxConnections
	}
	if toClashBool(settings["padding"]) {
		smux["padding"] = true
	}
	if brutal, ok := settings["brutal"].(map[string]any); ok && toClashBool(brutal["enabled"]) {
		smux["brutal-opts"] = map[string]any{
			"enabled": true,
			"up":      toClashInt(brutal["up_mbps"]),
			"down":    toClashInt(brutal["down_mbps"]),
		}
	}
	return smux
}

func toClashString(value any) string {
	switch typed := value.(type) {
	case string:
		return typed
	case nil:
		return ""
	default:
		return fmt.Sprint(typed)
	}
}

func toClashInt(value any) int {
	switch typed := value.(type) {
	case int:
		return typed
	case int64:
		return int(typed)
	case float64:
		return int(typed)
	case string:
		if typed == "" {
			return 0
		}
		var result int
		fmt.Sscanf(typed, "%d", &result)
		return result
	default:
		return 0
	}
}

func toClashBool(value any) bool {
	switch typed := value.(type) {
	case bool:
		return typed
	case int:
		return typed != 0
	case int64:
		return typed != 0
	case float64:
		return typed != 0
	case string:
		return typed == "1" || strings.EqualFold(typed, "true")
	default:
		return false
	}
}

func generateClashFallback(servers []model.Server, user model.User) string {
	var builder strings.Builder

	builder.WriteString("proxies:\n")
	var proxyNames []string
	for _, s := range servers {
		conf := service.BuildNodeConfig(&s)
		password := service.GenerateServerPassword(&s, &user)
		proxy := buildClashProxy("clash", conf, password)
		if proxy == nil {
			continue
		}

		switch proxy["type"] {
		case "ss":
			builder.WriteString(fmt.Sprintf(
				"  - name: \"%s\"\n    type: ss\n    server: %s\n    port: %d\n    cipher: %s\n    password: %s\n",
				conf.Name,
				conf.RawHost,
				conf.ServerPort,
				fmt.Sprint(proxy["cipher"]),
				fmt.Sprint(proxy["password"]),
			))
		case "vmess":
			builder.WriteString(fmt.Sprintf(
				"  - name: \"%s\"\n    type: vmess\n    server: %s\n    port: %d\n    uuid: %s\n    alterId: 0\n    cipher: auto\n    udp: true\n",
				conf.Name,
				conf.RawHost,
				conf.ServerPort,
				fmt.Sprint(proxy["uuid"]),
			))
		case "trojan":
			builder.WriteString(fmt.Sprintf(
				"  - name: \"%s\"\n    type: trojan\n    server: %s\n    port: %d\n    password: %s\n    udp: true\n",
				conf.Name,
				conf.RawHost,
				conf.ServerPort,
				fmt.Sprint(proxy["password"]),
			))
		}
		proxyNames = append(proxyNames, fmt.Sprintf("\"%s\"", conf.Name))
	}

	builder.WriteString("\nproxy-groups:\n")
	builder.WriteString("  - name: Proxy\n    type: select\n    proxies:\n      - DIRECT\n")
	for _, name := range proxyNames {
		builder.WriteString("      - " + name + "\n")
	}

	builder.WriteString("\nrules:\n")
	builder.WriteString("  - MATCH,Proxy\n")

	return builder.String()
}
