package harness

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

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

// Result 是一次输出评测的结果。各分 ∈ [0,1]。
type Result struct {
	Overall  float64  // 综合分（见 Score 的加权）
	Rule     float64  // 规则分
	LLM      float64  // LLM-as-judge 质量分（0=未评/失败）
	Faithful float64  // RAG 忠实度分（有检索来源时才评；0=未评/无来源）
	Flags    []string // 命中的问题（规则 + 未被来源支持的说法）
	Reason   string   // LLM 评语
}

// Evaluator 实现 LLM 自动化评测：规则检查（快、always-on）+ LLM-as-judge（模型就绪时）。
// 通过注入 ready/chat 解耦具体 LLM 后端，便于单测。
type Evaluator struct {
	ready func() bool
	chat  func(ctx context.Context, sys, user string) (string, error)
}

// NewEvaluator 注入"模型是否就绪"与"对话"两个能力；二者为 nil 时仅做规则评测。
func NewEvaluator(ready func() bool, chat func(ctx context.Context, sys, user string) (string, error)) *Evaluator {
	return &Evaluator{ready: ready, chat: chat}
}

// Score 对一次推理输出综合打分。sources 为检索到的资料（RAG 来源）：非空时额外评忠实度
// （回答是否基于来源、有无编造），并把"未被来源支持的说法"加进 Flags。
func (e *Evaluator) Score(ctx context.Context, input, output string, sources []string) Result {
	rule, flags := ruleScore(output)
	res := Result{Rule: rule, Flags: flags, Overall: rule}
	if e.ready == nil || e.chat == nil || !e.ready() {
		return res
	}
	if len(sources) > 0 {
		// RAG 路径：一次 judge 同时评质量 + 忠实度 + 列未支持说法。
		if q, f, unsupported, reason, ok := e.llmJudgeGrounded(ctx, input, output, sources); ok {
			res.LLM, res.Faithful, res.Reason = q, f, reason
			res.Overall = 0.3*rule + 0.35*q + 0.35*f
			for _, u := range unsupported {
				res.Flags = append(res.Flags, "未被来源支持："+u)
			}
		}
		return res
	}
	// 无来源：仅评质量（相关/准确/完整）。
	if s, reason, ok := e.llmJudge(ctx, input, output); ok {
		res.LLM = s
		res.Reason = reason
		res.Overall = 0.4*rule + 0.6*s
	}
	return res
}

// refusalMarkers 是疑似拒答/出错的特征串（小写匹配）。
var refusalMarkers = []string{"抱歉，我无法", "我无法回答", "无法完成", "撰写失败", "本章撰写失败", "i cannot", "i'm unable", "error:"}

// ruleScore 纯规则评测：空输出 / 过短 / 疑似拒答 / 重复啰嗦各扣分。返回分与命中标签。
func ruleScore(output string) (float64, []string) {
	out := strings.TrimSpace(output)
	if out == "" {
		return 0, []string{"空输出"}
	}
	score := 1.0
	var flags []string
	if len([]rune(out)) < 10 {
		score -= 0.4
		flags = append(flags, "输出过短")
	}
	low := strings.ToLower(out)
	for _, m := range refusalMarkers {
		if strings.Contains(low, strings.ToLower(m)) {
			score -= 0.3
			flags = append(flags, "疑似拒答/错误")
			break
		}
	}
	if hasHeavyRepeat(out) {
		score -= 0.3
		flags = append(flags, "重复啰嗦")
	}
	if score < 0 {
		score = 0
	}
	return score, flags
}

// hasHeavyRepeat 判断是否有非空行重复 ≥3 次（粗略的啰嗦/复读检测）。
func hasHeavyRepeat(s string) bool {
	counts := map[string]int{}
	for _, line := range strings.Split(s, "\n") {
		line = strings.TrimSpace(line)
		if len([]rune(line)) < 4 {
			continue
		}
		counts[line]++
		if counts[line] >= 3 {
			return true
		}
	}
	return false
}

// 评测提示词（校准版）：默认怀疑、先挑毛病再打分、给死评分基准并要求用满 1–5 区间。
// 旧版只说"严格"但无基准 → 模型恒锚定 4–5，恒温器从不触发。
const (
	evalQualitySys = "你是严苛的回答质量评审。默认怀疑：先找出回答的具体缺陷（跑题/不准确/不完整/空泛套话/啰嗦），再据此打分。" +
		"评分要吝啬——不要因为回答看起来认真或篇幅长就给高分。"
	evalQualityRubric = "评分基准（务必用满 1–5 区间，5 分极少给）：\n" +
		"5=准确、完整、切题，几乎无可挑剔\n4=好，但有可改进的小处\n3=基本可用但平庸/有遗漏/偏空泛\n" +
		"2=有明显缺陷（部分跑题/不准确/敷衍）\n1=跑题/答非所问/明显错误"
)

// normJudge 把 1–5 的 judge 评分归一到 [0,1]：(v-1)/4。
// 关键校准：最低分 1 → 0（而非旧的 0.2）。否则 0.4*rule 的底（流畅输出 rule≈1）叠加 0.2 的分下限，
// 会让无来源 Overall 恒 ≥0.52、poor(<0.5) 永不可达 —— "流畅但跑题/错误"也压不到纠偏区间。
func normJudge(v float64) float64 {
	s := (v - 1) / 4
	if s < 0 {
		s = 0
	}
	if s > 1 {
		s = 1
	}
	return s
}

// llmJudge 让模型对输出按基准 1–5 打分并给理由，归一化到 [0,1]。失败返回 ok=false。
func (e *Evaluator) llmJudge(ctx context.Context, input, output string) (float64, string, bool) {
	user := fmt.Sprintf("【用户输入】%s\n\n【模型输出】%s\n\n%s\n\n"+
		"只输出 JSON：{\"score\":1到5整数,\"reason\":\"先点最主要的缺陷(没有就说无明显缺陷)再给结论，一句话\"}，不要多余文字。",
		evalTruncate(input, 500), evalReviewOutput(output), evalQualityRubric)
	txt, err := e.chat(ctx, prompts.Get(prompts.EvalQuality), user)
	if err != nil {
		return 0, "", false
	}
	var j struct {
		Score  float64 `json:"score"`
		Reason string  `json:"reason"`
	}
	if json.Unmarshal([]byte(evalStripFence(txt)), &j) != nil || j.Score <= 0 {
		return 0, "", false
	}
	return normJudge(j.Score), j.Reason, true
}

// llmJudgeGrounded 给 judge 同时喂用户问题、检索资料、模型回答，一次返回：
// quality 质量分、faithful 忠实度分（均归一到 [0,1]）、unsupported 未被资料支持的说法、reason 评语。
func (e *Evaluator) llmJudgeGrounded(ctx context.Context, input, output string, sources []string) (quality, faithful float64, unsupported []string, reason string, ok bool) {
	sys := "你是严苛的 RAG 回答评审。默认怀疑：先逐条核查回答里每个关键说法能否在检索资料中找到依据，再据基准打分，不要轻易给高分。"
	src := evalTruncate(strings.Join(sources, "\n---\n"), 3000)
	user := fmt.Sprintf(
		"【用户问题】%s\n\n【检索资料】\n%s\n\n【模型回答】%s\n\n"+
			"评估两项，务必用满 1–5 区间：\n"+
			"quality(质量)：5=准确完整切题；3=平庸/有遗漏；1=跑题或错误。\n"+
			"faithfulness(忠实度)：5=每个关键说法都有资料依据；分数随无依据/编造的说法增多而降；只要有一处明显编造，faithfulness 不得高于 2。\n"+
			"unsupported：列出回答中未被检索资料支持的具体说法（无则空数组）。\n"+
			"只输出 JSON：{\"quality\":1到5整数,\"faithfulness\":1到5整数,\"unsupported\":[\"...\"],\"reason\":\"一句话\"}，不要多余文字。",
		evalTruncate(input, 400), src, evalReviewOutput(output))
	txt, err := e.chat(ctx, sys, user)
	if err != nil {
		return 0, 0, nil, "", false
	}
	var j struct {
		Quality      float64  `json:"quality"`
		Faithfulness float64  `json:"faithfulness"`
		Unsupported  []string `json:"unsupported"`
		Reason       string   `json:"reason"`
	}
	if json.Unmarshal([]byte(evalStripFence(txt)), &j) != nil || j.Quality <= 0 || j.Faithfulness <= 0 {
		return 0, 0, nil, "", false
	}
	return normJudge(j.Quality), normJudge(j.Faithfulness), j.Unsupported, j.Reason, true
}

func evalTruncate(s string, n int) string {
	r := []rune(s)
	if len(r) <= n {
		return s
	}
	return string(r[:n]) + "…"
}

// evalReviewLimit 是喂给 judge 的「模型回答」最长字数。不能太短：报告/多智能体综合等长答案
// 普遍 2000+ 字，截太短会让 judge 误判「回答不完整/截断」（而那是评审自己截的）。
const evalReviewLimit = 6000

// evalReviewOutput 把待评审的模型回答按 evalReviewLimit 截断；若确被截断，明确标注是评审所为，
// 让 judge 不要因结尾不完整而扣分（杜绝长答案被误判 warn/poor 的假阴性）。
func evalReviewOutput(s string) string {
	r := []rune(s)
	if len(r) <= evalReviewLimit {
		return s
	}
	return string(r[:evalReviewLimit]) + "\n…（注：回答过长，已被评审系统截断，请勿因结尾不完整而扣分）"
}

// evalStripFence 去掉模型可能包裹的 ```json … ``` 围栏。
func evalStripFence(s string) string {
	s = strings.TrimSpace(s)
	if strings.HasPrefix(s, "```") {
		if i := strings.IndexByte(s, '\n'); i >= 0 {
			s = s[i+1:]
		}
		s = strings.TrimSuffix(strings.TrimSpace(s), "```")
	}
	return strings.TrimSpace(s)
}
