package rag

import (
	"strings"
)

// 切块参数（针对中文 + embedding token 上限调过；后续可配置化）。
const (
	chunkTargetRunes  = 500  // 目标大小：贪心打包到接近此值就在句界收口
	chunkOverlapRunes = 80   // 块间重叠：保跨块上下文连续
	chunkMinRunes     = 100  // 最小块：低于则并入相邻，避免碎块稀释向量
	chunkMaxRunes     = 1000 // 原子硬上限：超大无标点段落兜底窗口切
)

// chunk 把文本切成检索友好的语义块。Markdown 文档走「标题感知」切块：
// 按 #/## 层级切段，块不跨章节，且每块前缀完整标题路径（如「部署 > 环境要求」）补全上下文；
// 非 Markdown 走纯语义切块。全程按 rune 操作，杜绝中文 UTF-8 被字节切碎。
func chunk(text string) []string {
	if hasMarkdownHeadings(text) {
		return chunkMarkdown(text)
	}
	return packSection(text)
}

// packSection 对一段纯文本做语义切块：原子 → 贪心打包（句末收口）→ 块间重叠。
func packSection(text string) []string {
	return addOverlap(packAtoms(splitToAtoms(text), chunkTargetRunes, chunkMinRunes), chunkOverlapRunes)
}

// section 是一个标题层级下的正文段：crumb 为标题路径面包屑（顶到本节，可空=前言）。
type section struct {
	crumb string
	body  string
}

// chunkMarkdown 标题感知切块：按标题层级切成 section（块绝不跨章节），每节内部再语义切块，
// 并给每块前缀该节的标题路径——使「## Redis 下的『端口配置』」这类块自带章节语境，显著提升检索命中。
func chunkMarkdown(text string) []string {
	var out []string
	for _, s := range splitMarkdownSections(text) {
		for _, body := range packSection(s.body) {
			if s.crumb != "" {
				out = append(out, s.crumb+"\n"+body) // 面包屑作为上下文前缀
			} else {
				out = append(out, body)
			}
		}
	}
	return out
}

// splitMarkdownSections 按 ATX 标题（# ~ ######）把文档切成带面包屑的章节，正确跳过代码围栏内的 #。
func splitMarkdownSections(text string) []section {
	lines := strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n")
	type heading struct {
		level int
		title string
	}
	var stack []heading
	var sections []section
	var body []string
	inFence, fenceMarker := false, ""

	crumb := func() string {
		parts := make([]string, len(stack))
		for i, h := range stack {
			parts[i] = h.title
		}
		return strings.Join(parts, " > ")
	}
	flush := func() {
		if b := strings.TrimSpace(strings.Join(body, "\n")); b != "" {
			sections = append(sections, section{crumb: crumb(), body: b})
		}
		body = body[:0]
	}

	for _, ln := range lines {
		t := strings.TrimSpace(ln)
		if m := fenceMark(t); m != "" { // 代码围栏开/合：围栏内的 # 不当标题
			if !inFence {
				inFence, fenceMarker = true, m
			} else if m == fenceMarker {
				inFence, fenceMarker = false, ""
			}
			body = append(body, ln)
			continue
		}
		if !inFence {
			if lvl, title, ok := parseHeading(t); ok {
				flush() // 上一节正文收口
				for len(stack) > 0 && stack[len(stack)-1].level >= lvl {
					stack = stack[:len(stack)-1] // 同级/更浅的标题出栈
				}
				stack = append(stack, heading{lvl, title})
				continue
			}
		}
		body = append(body, ln)
	}
	flush()
	return sections
}

// parseHeading 解析 ATX 标题行：返回层级(1-6)、标题文本、是否命中。要求 # 后跟空格（排除 #define 之类）。
func parseHeading(line string) (level int, title string, ok bool) {
	n := 0
	for n < len(line) && line[n] == '#' {
		n++
	}
	if n == 0 || n > 6 || n >= len(line) || line[n] != ' ' {
		return 0, "", false
	}
	title = strings.TrimRight(strings.TrimSpace(line[n:]), "# ") // 去掉闭合式 ATX 尾部的 #
	if title == "" {
		return 0, "", false
	}
	return n, title, true
}

// fenceMark 返回代码围栏标记（``` 或 ~~~），非围栏行返回空。
func fenceMark(t string) string {
	switch {
	case strings.HasPrefix(t, "```"):
		return "```"
	case strings.HasPrefix(t, "~~~"):
		return "~~~"
	default:
		return ""
	}
}

// hasMarkdownHeadings 快速判断文本是否含 ATX 标题（决定走标题感知还是纯语义切块）。
func hasMarkdownHeadings(text string) bool {
	for _, ln := range strings.Split(text, "\n") {
		if _, _, ok := parseHeading(strings.TrimSpace(ln)); ok {
			return true
		}
	}
	return false
}

// splitToAtoms 把文本切成"原子"（句子/行）：在换行与句末标点处断开，去空白；
// 超大无标点原子按 rune 窗口兜底切到 ≤ 目标大小。原子是打包的最小不可分单元。
func splitToAtoms(text string) []string {
	runes := []rune(strings.ReplaceAll(text, "\r\n", "\n"))
	var atoms []string
	start := 0
	flush := func(end int) {
		if s := strings.TrimSpace(string(runes[start:end])); s != "" {
			atoms = append(atoms, s)
		}
		start = end
	}
	for i := 0; i < len(runes); i++ {
		r := runes[i]
		switch {
		case r == '\n' || isCJKEnd(r):
			flush(i + 1)
		case r == '.' || r == '!' || r == '?' || r == ';':
			// ASCII 句末：仅当其后为空白/行尾才断（避开缩写、小数点）。
			if i+1 >= len(runes) || runes[i+1] == ' ' || runes[i+1] == '\n' {
				flush(i + 1)
			}
		}
	}
	flush(len(runes))

	out := make([]string, 0, len(atoms))
	for _, a := range atoms {
		if runeLen(a) <= chunkMaxRunes {
			out = append(out, a)
		} else {
			out = append(out, splitByRuneWindow(a, chunkTargetRunes)...)
		}
	}
	return out
}

// packAtoms 贪心打包：把原子拼进当前块，直到再加会超 target 就收口（块在句界结束）。
// 末尾过小的块（< min）并入前一块，避免碎块。
func packAtoms(atoms []string, target, min int) []string {
	var chunks []string
	var cur strings.Builder
	curLen := 0
	closeCur := func() {
		if curLen > 0 {
			chunks = append(chunks, cur.String())
			cur.Reset()
			curLen = 0
		}
	}
	for _, a := range atoms {
		al := runeLen(a)
		if curLen > 0 && curLen+al > target {
			closeCur()
		}
		if curLen > 0 {
			cur.WriteByte('\n')
			curLen++
		}
		cur.WriteString(a)
		curLen += al
	}
	if curLen > 0 {
		if n := len(chunks); n > 0 && curLen < min {
			chunks[n-1] = chunks[n-1] + "\n" + cur.String() // 尾块太小 → 并入上一块
		} else {
			chunks = append(chunks, cur.String())
		}
	}
	return chunks
}

// addOverlap 给每块前缀上一块尾部的 overlap 个 rune，保跨块上下文连续（首块不加）。
func addOverlap(chunks []string, overlap int) []string {
	if overlap <= 0 || len(chunks) <= 1 {
		return chunks
	}
	out := make([]string, len(chunks))
	out[0] = chunks[0]
	for i := 1; i < len(chunks); i++ {
		prev := []rune(chunks[i-1])
		tail := prev
		if len(prev) > overlap {
			tail = prev[len(prev)-overlap:]
		}
		out[i] = strings.TrimSpace(string(tail)) + "\n" + chunks[i]
	}
	return out
}

func isCJKEnd(r rune) bool {
	switch r {
	case '。', '！', '？', '；', '…':
		return true
	}
	return false
}

// splitByRuneWindow 按 rune 窗口硬切（兜底：超大无标点原子），保证不切碎多字节字符。
func splitByRuneWindow(s string, size int) []string {
	r := []rune(s)
	var out []string
	for len(r) > size {
		if t := strings.TrimSpace(string(r[:size])); t != "" {
			out = append(out, t)
		}
		r = r[size:]
	}
	if t := strings.TrimSpace(string(r)); t != "" {
		out = append(out, t)
	}
	return out
}

func runeLen(s string) int { return len([]rune(s)) }
