package subscription

import (
	"context"
	"encoding/base64"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"regexp"
	"strings"
	"time"
	"unicode"

	"vpnclient/internal/config"
	"vpnclient/internal/linknorm"
	"vpnclient/internal/protocols/hysteria2"
	"vpnclient/internal/protocols/naive"
)

// Item is one imported share link.
type Item struct {
	Name     string
	Protocol config.Protocol
	URI      string
}

var (
	reShareLine = regexp.MustCompile(`(?i)((?:hysteria2|hy2|awg|amneziawg|wireguard|naive\+https|naive\+quic|naive|quic|https|http)://[^\s"'<>]+)`)
	reClashHY2  = regexp.MustCompile(`(?is)type:\s*hysteria2\b.*?server:\s*(\S+).*?port:\s*(\d+).*?(?:password|auth):\s*["']?([^\s"']+)`)
)

// Fetch downloads a subscription body and parses proxy share links.
func Fetch(ctx context.Context, rawURL string) ([]Item, error) {
	rawURL = strings.TrimSpace(rawURL)
	if rawURL == "" {
		return nil, fmt.Errorf("пустой URL подписки")
	}
	u, err := url.Parse(rawURL)
	if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
		return nil, fmt.Errorf("нужен http(s) URL подписки")
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("User-Agent", "ClashMeta/1.18.0")
	req.Header.Set("Accept", "*/*")
	client := &http.Client{Timeout: 45 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("не удалось скачать подписку: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("подписка HTTP %d", resp.StatusCode)
	}
	body, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
	if err != nil {
		return nil, err
	}
	items, err := ParseBody(string(body))
	if err != nil {
		return nil, err
	}
	return items, nil
}

// ParseBody accepts plain text lines, base64, or Clash-like blobs with share links.
func ParseBody(body string) ([]Item, error) {
	body = strings.TrimSpace(body)
	if body == "" {
		return nil, fmt.Errorf("пустая подписка")
	}

	candidates := []string{body}
	if decoded, err := decodeMaybeBase64(body); err == nil && decoded != "" && decoded != body {
		candidates = append(candidates, decoded)
		// some panels double-encode
		if decoded2, err2 := decodeMaybeBase64(decoded); err2 == nil && decoded2 != "" && decoded2 != decoded {
			candidates = append(candidates, decoded2)
		}
	}

	var lastErr error
	for _, text := range candidates {
		items, err := parseText(text)
		if err == nil && len(items) > 0 {
			return items, nil
		}
		if err != nil {
			lastErr = err
		}
	}
	if lastErr != nil {
		return nil, lastErr
	}
	return nil, fmt.Errorf("в подписке нет поддерживаемых ссылок (naive / hysteria2)")
}

func parseText(text string) ([]Item, error) {
	lines := splitLines(text)
	// Also extract share URIs embedded in YAML/HTML/JSON.
	extracted := reShareLine.FindAllString(text, -1)
	for _, m := range extracted {
		lines = append(lines, strings.TrimRight(m, ".,;)]}\"'"))
	}
	for _, block := range extractClashHY2(text) {
		lines = append(lines, block)
	}

	out := make([]Item, 0, len(lines))
	seen := map[string]int{}
	seenURI := map[string]struct{}{}
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		line = strings.Trim(line, `"'`)
		proto := config.DetectProtocol(line)
		if proto == "" {
			continue
		}
		// Skip bare https:// without credentials (common noise in HTML/YAML).
		if proto == config.ProtocolNaive && !naiveLinkHasAuth(line) && !strings.HasPrefix(strings.ToLower(line), "naive+") {
			continue
		}
		normalized, detected, remark, err := linknorm.Normalize(proto, line)
		if err != nil {
			continue
		}
		if _, dup := seenURI[normalized]; dup {
			continue
		}
		seenURI[normalized] = struct{}{}
		name := remark
		if name == "" {
			name = defaultName(detected, normalized)
		}
		base := name
		for n := 2; ; n++ {
			if _, ok := seen[name]; !ok {
				break
			}
			name = fmt.Sprintf("%s-%d", base, n)
		}
		seen[name] = 1
		out = append(out, Item{Name: name, Protocol: detected, URI: normalized})
	}
	if len(out) == 0 {
		return nil, fmt.Errorf("в подписке нет поддерживаемых ссылок (naive / hysteria2)")
	}
	return out, nil
}

func naiveLinkHasAuth(line string) bool {
	lower := strings.ToLower(strings.TrimSpace(line))
	for _, p := range []string{"naive+https://", "naive+quic://", "naive://", "https://", "http://", "quic://"} {
		if strings.HasPrefix(lower, p) {
			rest := line[len(p):]
			at := strings.IndexByte(rest, '@')
			return at > 0
		}
	}
	return false
}

func extractClashHY2(text string) []string {
	// Very small helper for Clash YAML hysteria2 blocks when share URI is absent.
	lower := strings.ToLower(text)
	if !strings.Contains(lower, "type:") || !strings.Contains(lower, "hysteria2") {
		return nil
	}
	var out []string
	// Split roughly by list items.
	chunks := strings.Split(text, "\n- ")
	for _, chunk := range chunks {
		cl := strings.ToLower(chunk)
		if !strings.Contains(cl, "type:") || !strings.Contains(cl, "hysteria2") {
			continue
		}
		server := yamlField(chunk, "server")
		port := yamlField(chunk, "port")
		pass := yamlField(chunk, "password")
		if pass == "" {
			pass = yamlField(chunk, "auth")
		}
		if server == "" || port == "" || pass == "" {
			continue
		}
		name := yamlField(chunk, "name")
		sni := yamlField(chunk, "sni")
		obfs := yamlField(chunk, "obfs")
		obfsPass := yamlField(chunk, "obfs-password")
		if obfsPass == "" {
			obfsPass = yamlField(chunk, "obfs_password")
		}
		u := url.URL{
			Scheme: "hysteria2",
			User:   url.User(pass),
			Host:   server + ":" + port,
		}
		q := url.Values{}
		if sni != "" {
			q.Set("sni", sni)
		}
		if obfs != "" {
			q.Set("obfs", obfs)
		}
		if obfsPass != "" {
			q.Set("obfs-password", obfsPass)
		}
		u.RawQuery = q.Encode()
		link := u.String()
		if name != "" {
			link += "#" + url.PathEscape(name)
		}
		out = append(out, link)
	}
	_ = reClashHY2 // kept for possible future tightening
	return out
}

func yamlField(block, key string) string {
	re := regexp.MustCompile(`(?im)^\s*` + regexp.QuoteMeta(key) + `\s*:\s*["']?([^"'#\n\r]+)["']?`)
	m := re.FindStringSubmatch(block)
	if len(m) < 2 {
		return ""
	}
	return strings.TrimSpace(m[1])
}

func decodeMaybeBase64(s string) (string, error) {
	s = strings.Map(func(r rune) rune {
		if unicode.IsSpace(r) {
			return -1
		}
		return r
	}, s)
	if s == "" {
		return "", fmt.Errorf("empty")
	}
	raw, err := base64.StdEncoding.DecodeString(s)
	if err != nil {
		raw, err = base64.RawStdEncoding.DecodeString(s)
	}
	if err != nil {
		raw, err = base64.URLEncoding.DecodeString(s)
	}
	if err != nil {
		raw, err = base64.RawURLEncoding.DecodeString(s)
	}
	if err != nil {
		return "", err
	}
	return string(raw), nil
}

func splitLines(s string) []string {
	s = strings.ReplaceAll(s, "\r\n", "\n")
	s = strings.ReplaceAll(s, "\r", "\n")
	return strings.Split(s, "\n")
}

func defaultName(proto config.Protocol, uri string) string {
	host := ""
	switch proto {
	case config.ProtocolHysteria2:
		if h, _, err := hysteria2.HostPort(uri); err == nil {
			host = h
		}
	default:
		if u, _, err := naive.ParseShareLink(uri); err == nil {
			if parsed, err := url.Parse(u); err == nil {
				host = parsed.Hostname()
			}
		}
	}
	if host == "" {
		host = string(proto)
	}
	return string(proto) + "-" + host
}
