package mcp

import (
	"context"
	"fmt"
	"html"
	"io"
	"math"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/sundynix/sundynix-shared/contract"
)

// ===== current_datetime：当前日期时间（LLM 不知道"现在"）=====

var weekdaysCN = []string{"周日", "周一", "周二", "周三", "周四", "周五", "周六"}

func (g *Gateway) currentDatetime(_ context.Context, call *contract.ToolCall) *contract.ToolResult {
	loc := time.Local
	if tz, _ := call.Args["tz"].(string); strings.TrimSpace(tz) != "" {
		if l, err := time.LoadLocation(strings.TrimSpace(tz)); err == nil {
			loc = l
		}
	}
	now := time.Now().In(loc)
	out := fmt.Sprintf("%s %s（%s，时区 %s，Unix %d）",
		now.Format("2006-01-02"), now.Format("15:04:05"),
		weekdaysCN[int(now.Weekday())], now.Format("MST-07:00"), now.Unix())
	return &contract.ToolResult{OK: true, Content: out}
}

// ===== calculator：安全表达式求值（+ - * / % ^ 括号、一元负号）=====

func (g *Gateway) calculator(_ context.Context, call *contract.ToolCall) *contract.ToolResult {
	expr := strings.TrimSpace(fmt.Sprint(call.Args["expr"]))
	if expr == "" || expr == "<nil>" {
		return &contract.ToolResult{OK: false, Error: "calculator: expr 必填"}
	}
	v, err := evalArith(expr)
	if err != nil {
		return &contract.ToolResult{OK: false, Error: "calculator: " + err.Error()}
	}
	return &contract.ToolResult{OK: true, Content: strconv.FormatFloat(v, 'g', -1, 64)}
}

// evalArith 用调度场算法把中缀表达式转 RPN 再求值。仅支持数值与 + - * / % ^ ( )，杜绝任意代码执行。
func evalArith(s string) (float64, error) {
	toks, err := tokenizeArith(s)
	if err != nil {
		return 0, err
	}
	prec := map[string]int{"+": 1, "-": 1, "*": 2, "/": 2, "%": 2, "^": 3, "u-": 4}
	rightAssoc := map[string]bool{"^": true, "u-": true}
	var output, ops []string
	for i, t := range toks {
		switch {
		case isNumber(t):
			output = append(output, t)
		case t == "(":
			ops = append(ops, t)
		case t == ")":
			for len(ops) > 0 && ops[len(ops)-1] != "(" {
				output = append(output, ops[len(ops)-1])
				ops = ops[:len(ops)-1]
			}
			if len(ops) == 0 {
				return 0, fmt.Errorf("括号不匹配")
			}
			ops = ops[:len(ops)-1] // 弹出 "("
		default: // 运算符
			op := t
			// 一元负号：在表达式开头或运算符/左括号之后的 "-"。
			if op == "-" && (i == 0 || isOperator(toks[i-1]) || toks[i-1] == "(") {
				op = "u-"
			}
			for len(ops) > 0 {
				top := ops[len(ops)-1]
				if top == "(" {
					break
				}
				if prec[top] > prec[op] || (prec[top] == prec[op] && !rightAssoc[op]) {
					output = append(output, top)
					ops = ops[:len(ops)-1]
				} else {
					break
				}
			}
			ops = append(ops, op)
		}
	}
	for len(ops) > 0 {
		if ops[len(ops)-1] == "(" {
			return 0, fmt.Errorf("括号不匹配")
		}
		output = append(output, ops[len(ops)-1])
		ops = ops[:len(ops)-1]
	}
	return evalRPN(output)
}

func evalRPN(rpn []string) (float64, error) {
	var st []float64
	pop := func() (float64, error) {
		if len(st) == 0 {
			return 0, fmt.Errorf("表达式非法")
		}
		v := st[len(st)-1]
		st = st[:len(st)-1]
		return v, nil
	}
	for _, t := range rpn {
		if isNumber(t) {
			f, _ := strconv.ParseFloat(t, 64)
			st = append(st, f)
			continue
		}
		if t == "u-" {
			a, err := pop()
			if err != nil {
				return 0, err
			}
			st = append(st, -a)
			continue
		}
		b, err := pop()
		if err != nil {
			return 0, err
		}
		a, err := pop()
		if err != nil {
			return 0, err
		}
		switch t {
		case "+":
			st = append(st, a+b)
		case "-":
			st = append(st, a-b)
		case "*":
			st = append(st, a*b)
		case "/":
			if b == 0 {
				return 0, fmt.Errorf("除以零")
			}
			st = append(st, a/b)
		case "%":
			st = append(st, math.Mod(a, b))
		case "^":
			st = append(st, math.Pow(a, b))
		default:
			return 0, fmt.Errorf("未知运算符 %q", t)
		}
	}
	if len(st) != 1 {
		return 0, fmt.Errorf("表达式非法")
	}
	return st[0], nil
}

func tokenizeArith(s string) ([]string, error) {
	var toks []string
	r := []rune(s)
	for i := 0; i < len(r); {
		c := r[i]
		switch {
		case c == ' ' || c == '\t':
			i++
		case strings.ContainsRune("+-*/%^()", c):
			toks = append(toks, string(c))
			i++
		case (c >= '0' && c <= '9') || c == '.':
			j := i
			for j < len(r) && ((r[j] >= '0' && r[j] <= '9') || r[j] == '.' || r[j] == 'e' || r[j] == 'E' ||
				((r[j] == '+' || r[j] == '-') && j > i && (r[j-1] == 'e' || r[j-1] == 'E'))) {
				j++
			}
			num := string(r[i:j])
			if _, err := strconv.ParseFloat(num, 64); err != nil {
				return nil, fmt.Errorf("非法数字 %q", num)
			}
			toks = append(toks, num)
			i = j
		default:
			return nil, fmt.Errorf("非法字符 %q（仅支持数字与 + - * / %% ^ ( )）", string(c))
		}
	}
	if len(toks) == 0 {
		return nil, fmt.Errorf("空表达式")
	}
	return toks, nil
}

func isNumber(t string) bool { _, err := strconv.ParseFloat(t, 64); return err == nil }
func isOperator(t string) bool {
	return t == "+" || t == "-" || t == "*" || t == "/" || t == "%" || t == "^"
}

// ===== web_fetch：抓取网页并提取正文文本 =====

var (
	// RE2 不支持反向引用，逐标签枚举其配对闭合。
	reScriptStyle = regexp.MustCompile(`(?is)<script\b[^>]*>.*?</script\s*>|<style\b[^>]*>.*?</style\s*>|<head\b[^>]*>.*?</head\s*>|<noscript\b[^>]*>.*?</noscript\s*>`)
	reTag         = regexp.MustCompile(`(?s)<[^>]+>`)
	reWS          = regexp.MustCompile(`[ \t\x{00a0}]+`)
	reBlankLines  = regexp.MustCompile(`\n\s*\n\s*\n+`)
)

const webFetchMaxText = 8000 // 提取正文上限（rune），避免塞爆上下文

func (g *Gateway) webFetch(ctx context.Context, call *contract.ToolCall) *contract.ToolResult {
	raw := strings.TrimSpace(fmt.Sprint(call.Args["url"]))
	if raw == "" || raw == "<nil>" {
		return &contract.ToolResult{OK: false, Error: "web_fetch: url 必填"}
	}
	if reason, ok := validateExternalURL(raw, extAllowlist()); !ok {
		return &contract.ToolResult{OK: false, Error: "web_fetch: URL 被拦截 —— " + reason}
	}
	body, status, err := httpGet(ctx, raw)
	if err != nil {
		return &contract.ToolResult{OK: false, Error: "web_fetch: " + err.Error()}
	}
	text := htmlToText(body)
	if rs := []rune(text); len(rs) > webFetchMaxText {
		text = string(rs[:webFetchMaxText]) + "\n…（已截断）"
	}
	return &contract.ToolResult{OK: true, Content: fmt.Sprintf("URL: %s (HTTP %d)\n\n%s", raw, status, text)}
}

// htmlToText 把 HTML 粗提取为可读文本：去脚本/样式 → 去标签 → 解实体 → 收敛空白。
func htmlToText(h string) string {
	h = reScriptStyle.ReplaceAllString(h, " ")
	h = regexp.MustCompile(`(?i)<\s*(br|/p|/div|/li|/h[1-6]|/tr)\s*/?>`).ReplaceAllString(h, "\n")
	h = reTag.ReplaceAllString(h, "")
	h = html.UnescapeString(h)
	h = reWS.ReplaceAllString(h, " ")
	h = reBlankLines.ReplaceAllString(h, "\n\n")
	return strings.TrimSpace(h)
}

// ===== web_search：联网搜索（Tavily 有 key 则用，否则 DuckDuckGo HTML 兜底）=====

const webSearchDefaultTopK = 5

func (g *Gateway) webSearch(ctx context.Context, call *contract.ToolCall) *contract.ToolResult {
	q := strings.TrimSpace(fmt.Sprint(call.Args["q"]))
	if q == "" || q == "<nil>" {
		return &contract.ToolResult{OK: false, Error: "web_search: q 必填"}
	}
	topK := webSearchDefaultTopK
	if n, ok := toInt(call.Args["topK"]); ok && n > 0 && n <= 10 {
		topK = n
	}
	if key := strings.TrimSpace(os.Getenv("TAVILY_API_KEY")); key != "" {
		if out, err := tavilySearch(ctx, key, q, topK); err == nil {
			return &contract.ToolResult{OK: true, Content: out}
		}
		// Tavily 失败 → 落 DDG 兜底
	}
	out, err := ddgSearch(ctx, q, topK)
	if err != nil {
		return &contract.ToolResult{OK: false, Error: "web_search: " + err.Error()}
	}
	return &contract.ToolResult{OK: true, Content: out}
}

var (
	reDDGLink    = regexp.MustCompile(`(?is)<a[^>]+class="result__a"[^>]+href="([^"]+)"[^>]*>(.*?)</a>`)
	reDDGSnippet = regexp.MustCompile(`(?is)<a[^>]+class="result__snippet"[^>]*>(.*?)</a>`)
)

// ddgSearch 抓 DuckDuckGo HTML 版结果（免 key）。SERP 结构变动时尽力解析，解析不到则返回提示。
func ddgSearch(ctx context.Context, q string, topK int) (string, error) {
	u := "https://html.duckduckgo.com/html/?q=" + url.QueryEscape(q)
	body, _, err := httpGet(ctx, u)
	if err != nil {
		return "", err
	}
	links := reDDGLink.FindAllStringSubmatch(body, -1)
	snips := reDDGSnippet.FindAllStringSubmatch(body, -1)
	if len(links) == 0 {
		return "", fmt.Errorf("未解析到结果（DDG 可能限流或改版）")
	}
	var b strings.Builder
	for i := 0; i < len(links) && i < topK; i++ {
		title := strings.TrimSpace(htmlToText(links[i][2]))
		href := ddgUnwrap(links[i][1])
		snippet := ""
		if i < len(snips) {
			snippet = strings.TrimSpace(htmlToText(snips[i][1]))
		}
		fmt.Fprintf(&b, "%d. %s\n   %s\n   %s\n", i+1, title, href, snippet)
	}
	return strings.TrimSpace(b.String()), nil
}

// ddgUnwrap 还原 DDG 的跳转链接 /l/?uddg=<编码真实 url>。
func ddgUnwrap(href string) string {
	if i := strings.Index(href, "uddg="); i >= 0 {
		raw := href[i+len("uddg="):]
		if amp := strings.IndexByte(raw, '&'); amp >= 0 {
			raw = raw[:amp]
		}
		if dec, err := url.QueryUnescape(raw); err == nil {
			return dec
		}
	}
	if strings.HasPrefix(href, "//") {
		return "https:" + href
	}
	return href
}

// tavilySearch 走 Tavily Search API（需 TAVILY_API_KEY）—— 返回干净的标题/URL/摘要。
func tavilySearch(ctx context.Context, key, q string, topK int) (string, error) {
	payload := fmt.Sprintf(`{"api_key":%q,"query":%q,"max_results":%d}`, key, q, topK)
	req, _ := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.tavily.com/search", strings.NewReader(payload))
	req.Header.Set("Content-Type", "application/json")
	resp, err := (&http.Client{Timeout: extTimeout}).Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 400 {
		return "", fmt.Errorf("tavily HTTP %d", resp.StatusCode)
	}
	data, _ := io.ReadAll(io.LimitReader(resp.Body, extMaxBytes))
	// 轻量提取（不引 JSON 结构体）：用正则挑 title/url/content。
	reItem := regexp.MustCompile(`(?is)"title"\s*:\s*"(.*?)".*?"url"\s*:\s*"(.*?)".*?"content"\s*:\s*"(.*?)"`)
	items := reItem.FindAllStringSubmatch(string(data), -1)
	if len(items) == 0 {
		return "", fmt.Errorf("tavily 无结果")
	}
	var b strings.Builder
	for i, m := range items {
		if i >= topK {
			break
		}
		fmt.Fprintf(&b, "%d. %s\n   %s\n   %s\n", i+1, jsonUnesc(m[1]), jsonUnesc(m[2]), truncateRunes(jsonUnesc(m[3]), 240))
	}
	return strings.TrimSpace(b.String()), nil
}

// ===== 公共小工具 =====

func httpGet(ctx context.Context, raw string) (body string, status int, err error) {
	allow := extAllowlist()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, raw, nil)
	if err != nil {
		return "", 0, err
	}
	// 带个常见 UA，部分站点对空 UA 返回 403。
	req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; sundynix-agentix/1.0)")
	client := &http.Client{
		Timeout: extTimeout,
		CheckRedirect: func(r *http.Request, via []*http.Request) error {
			if len(via) >= 3 {
				return fmt.Errorf("重定向过多")
			}
			if reason, ok := validateExternalURL(r.URL.String(), allow); !ok {
				return fmt.Errorf("重定向被拦截：%s", reason)
			}
			return nil
		},
	}
	resp, err := client.Do(req)
	if err != nil {
		return "", 0, err
	}
	defer resp.Body.Close()
	data, _ := io.ReadAll(io.LimitReader(resp.Body, extMaxBytes))
	return string(data), resp.StatusCode, nil
}

func toInt(v any) (int, bool) {
	switch n := v.(type) {
	case float64:
		return int(n), true
	case int:
		return n, true
	case string:
		if i, err := strconv.Atoi(strings.TrimSpace(n)); err == nil {
			return i, true
		}
	}
	return 0, false
}

// truncateRunes 按 rune 截断（中文安全），超出加省略号。
func truncateRunes(s string, n int) string {
	r := []rune(s)
	if len(r) <= n {
		return s
	}
	return string(r[:n]) + "…"
}

// jsonUnesc 还原 JSON 字符串里的常见转义（够用即可，不做完整解码）。
func jsonUnesc(s string) string {
	r := strings.NewReplacer(`\"`, `"`, `\\`, `\`, `\n`, " ", `\t`, " ", `\/`, "/")
	return strings.TrimSpace(r.Replace(s))
}
