// Package guardrail 实现 Harness 输入护栏的纯检测逻辑（与 HTTP 解耦，便于单测）。
//
// 两层设计：
//   - Tier1（本包，网关同步、无 LLM）：先把输入「归一化」——小写、去零宽字符、折叠同形字、
//     拆字间隔(i g n o r e)还原、base64 解码——再跑高精度注入正则，干掉编码/空格/同形字绕过；
//     env 黑名单命中即拦。命中即 Blocked。
//   - Tier2（dispatcher harness 的 LLM 分类器）：Tier1 判为「疑似但不确定」(Suspect) 的灰区输入
//     放行但打标，由 Dispatcher 执行前调 LLM 裁决（见 contract.MetaSafetyCheck）。
package guardrail

import (
	"encoding/base64"
	"os"
	"regexp"
	"strings"
	"unicode"
)

// MaxJSONBytes 是 JSON 请求体上限（文件上传走 multipart，不经此检查）。
const MaxJSONBytes = 256 * 1024

// injectionPatterns 是高精度的提示词注入 / 越权诱导模式（命中即硬拦截）。
// 在 原文 + 归一化 + 紧凑(去分隔) + base64 解码 多个视图上匹配，故无需把绕过变体逐一写进正则。
var injectionPatterns = []struct {
	label string
	re    *regexp.Regexp
}{
	{"忽略既定指令", regexp.MustCompile(`(?i)ignore\s*(all\s*|the\s*)*previous\s*(instructions?|prompts?)`)},
	{"忽略既定指令", regexp.MustCompile(`(?i)disregard\s*(the\s*)?(above|previous|prior)`)},
	{"忽略既定指令", regexp.MustCompile(`忽略(以上|之前|前面|上述|先前)[^。\n]{0,8}(指令|指示|提示|要求|规则|设定)`)},
	{"角色越权", regexp.MustCompile(`(?i)you\s*are\s*now\s*(a|an|the|no\s*longer)`)},
	{"诱导泄露提示词", regexp.MustCompile(`(?i)(reveal|show|print|repeat|expose)\s*(me\s*)?(your\s*|the\s*)*(system\s*)?prompt`)},
	{"诱导泄露提示词", regexp.MustCompile(`(泄露|显示|输出|告诉我|重复)[^。\n]{0,6}(系统)?(提示词|指令)`)},
}

// suspectLexicon 是「软信号」越狱词表：单独命中不足以拦截（误伤风险），但触发灰区升级（交 LLM 裁决）。
var suspectLexicon = []string{
	"jailbreak", "developer mode", "dan mode", "do anything now", "no restrictions",
	"without restrictions", "unfiltered", "ignore safety", "bypass", "sudo mode",
	"pretend you are", "roleplay as", "act as if", "越狱", "开发者模式", "无限制",
}

// bannedTerms 是敏感词黑名单，从 env GUARDRAIL_BANNED_TERMS 读（逗号分隔，已小写）；命中即拦截。
var bannedTerms = loadBannedTerms()

func loadBannedTerms() []string {
	var out []string
	for _, w := range strings.Split(os.Getenv("GUARDRAIL_BANNED_TERMS"), ",") {
		if w = strings.TrimSpace(strings.ToLower(w)); w != "" {
			out = append(out, w)
		}
	}
	return out
}

// Result 是一次输入检查结果。
type Result struct {
	Blocked bool     // 硬拦截（注入/黑名单/超体积）
	Reason  string   // 人类可读拦截原因
	Suspect bool     // 灰区：疑似但不确定，放行但标记升级 LLM 裁决（Tier2）
	Signals []string // 命中的软信号（可观测）
}

// Inspect 检查输入是否触发护栏。
func Inspect(body []byte) Result {
	if len(body) > MaxJSONBytes {
		return Result{Blocked: true, Reason: "请求体过大（超过 256KB）"}
	}
	raw := string(body)
	// 多视图：原文 + 归一化（小写/去零宽/同形字折叠/塌缩空白）+ 紧凑（去所有分隔）+ base64 解码。
	norm := normalize(raw)
	compact := compactize(norm)
	views := []string{raw, norm, compact}
	if dec := decodeBase64Blobs(raw); dec != "" {
		views = append(views, strings.ToLower(dec), compactize(normalize(dec)))
	}

	for _, p := range injectionPatterns {
		for _, v := range views {
			if p.re.MatchString(v) {
				return Result{Blocked: true, Reason: "疑似提示词注入（" + p.label + "）"}
			}
		}
	}
	for _, w := range bannedTerms {
		for _, v := range views {
			if strings.Contains(v, w) {
				return Result{Blocked: true, Reason: "命中敏感词"}
			}
		}
	}

	// 软信号 → 灰区升级（不拦，交 Tier2 LLM 裁决）。
	var signals []string
	for _, w := range suspectLexicon {
		if strings.Contains(norm, w) || strings.Contains(compact, compactize(w)) {
			signals = append(signals, w)
		}
	}
	if len(views) > 3 { // 含可解码的 base64 blob：本身可疑，升级
		signals = append(signals, "base64-payload")
	}
	return Result{Suspect: len(signals) > 0, Signals: signals}
}

// zeroWidth 是常见零宽 / 不可见字符（注入者用来打断关键词、骗过正则）：
// U+200B-200D 零宽空格/连接符、U+2060 word joiner、U+FEFF BOM、U+00AD 软连字符。
func zeroWidth(r rune) bool {
	switch r {
	case '\u200b', '\u200c', '\u200d', '\u2060', '\ufeff', '\u00ad':
		return true
	}
	return false
}

// homoglyphs 把常见西里尔/希腊同形字折回拉丁字母（如 'а'→'a'，肉眼一样、码位不同）。
var homoglyphs = map[rune]rune{
	'а': 'a', 'е': 'e', 'о': 'o', 'р': 'p', 'с': 'c', 'х': 'x', 'у': 'y',
	'і': 'i', 'ѕ': 's', 'ԁ': 'd', 'ո': 'n', 'ɡ': 'g', 'ⅼ': 'l',
	'ο': 'o', 'ρ': 'p', 'ε': 'e', 'α': 'a', 'ι': 'i', 'ν': 'v',
}

// normalize 归一化：小写 + 去零宽 + 去组合附加符(变音) + 同形字折叠 + 塌缩连续空白为单空格。
func normalize(s string) string {
	s = strings.ToLower(s)
	var b strings.Builder
	b.Grow(len(s))
	prevSpace := false
	for _, r := range s {
		if zeroWidth(r) || unicode.Is(unicode.Mn, r) { // Mn=组合附加符
			continue
		}
		if g, ok := homoglyphs[r]; ok {
			r = g
		}
		if unicode.IsSpace(r) {
			if !prevSpace {
				b.WriteRune(' ')
				prevSpace = true
			}
			continue
		}
		prevSpace = false
		b.WriteRune(r)
	}
	return strings.TrimSpace(b.String())
}

// compactize 去掉所有非「字母数字 / CJK」字符：把 "i.g.n.o.r.e" / "i g n o r e" 还原成 "ignore"，
// 干掉拆字间隔类绕过。注意会粘连多词，故 compact 视图配合的是同样去分隔的紧凑模式。
func compactize(s string) string {
	var b strings.Builder
	b.Grow(len(s))
	for _, r := range s {
		switch {
		case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
			b.WriteRune(r)
		case r >= 0x4e00 && r <= 0x9fff: // CJK
			b.WriteRune(r)
		}
	}
	return b.String()
}

// base64BlobRe 匹配较长的 base64 串（短串误报多，限 ≥24 字符）。
var base64BlobRe = regexp.MustCompile(`[A-Za-z0-9+/]{24,}={0,2}`)

// decodeBase64Blobs 解码输入里的长 base64 片段，拼成可扫描的明文（解码失败/非文本的丢弃）。
func decodeBase64Blobs(s string) string {
	var out strings.Builder
	for _, m := range base64BlobRe.FindAllString(s, -1) {
		dec, err := base64.StdEncoding.DecodeString(m)
		if err != nil {
			if dec, err = base64.RawStdEncoding.DecodeString(m); err != nil {
				continue
			}
		}
		if isMostlyText(dec) {
			out.Write(dec)
			out.WriteByte(' ')
		}
	}
	return out.String()
}

// isMostlyText 判断解码结果是否大体为可打印文本（过滤二进制/随机字节，降误报）。
func isMostlyText(b []byte) bool {
	if len(b) == 0 {
		return false
	}
	printable := 0
	for _, c := range b {
		if c == '\n' || c == '\t' || (c >= 0x20 && c < 0x7f) {
			printable++
		}
	}
	return printable*100/len(b) >= 85
}
