package brief

import (
	"context"
	"fmt"
	"sort"
	"strings"

	"github.com/yan1h/agent-coding-workflow/internal/infra/llm"
	"github.com/yan1h/agent-coding-workflow/internal/project"
)

// BriefKind 标识简报的使用场景，影响默认角色提示与是否做仓库定向。
type BriefKind string

const (
	// KindACPSession 用于 ACP 执行型 session 的初始 prompt（含仓库定向）。
	KindACPSession BriefKind = "acp_session"
	// KindChatRequirement 用于挂载到 requirement 的 chat 会话 FK 注入。
	KindChatRequirement BriefKind = "chat_requirement"
	// KindChatIssue 用于挂载到 issue 的 chat 会话 FK 注入。
	KindChatIssue BriefKind = "chat_issue"
)

// 默认 token 预算与有界参数。可被 Config 覆盖。
const (
	DefaultTokenBudget = 6000
	defaultMaxCommits  = 10
	defaultMaxDirty    = 20
	// minSectionTokens 是一个 section 至少要能容纳的 token 数；不足以放下哪怕
	// 这么多时直接跳过该 section（避免输出只剩一个截断标记的无意义碎片）。
	minSectionTokens = 16
	truncateMarker   = "\n\n……（已截断）"
)

// truncatedSectionTokens 是 truncateMarker 的 token 预算占用估算，预留给截断标记。
var truncatedSectionTokens = llm.EstimateTokens(truncateMarker)

// BriefInput 是组装一份简报所需的全部输入。所有数据由调用方（app.go 适配器）
// 预先解析好传入，brief 不主动访问仓储，从而不产生跨模块依赖。
type BriefInput struct {
	Project     *project.Project
	Requirement *project.Requirement // nil 表示 issue-only 或自由对话
	Issue       *project.Issue       // nil 表示 requirement-only
	Artifacts   []*project.Artifact  // 各阶段产物（可含多版本，composer 自行取每阶段最新）
	RepoCwd     string               // session CwdPath / workspace main path，用于 git 定向
	Branch      string               // 当前分支（session.Branch），填入 RepoOrientation.Branch
	UserPrompt  string               // 操作者的自由文本初始 prompt，永远追加在最后
	TokenBudget int                  // 硬上限；<=0 时用 Config.DefaultTokenBudget
	Kind        BriefKind
}

// BriefResult 是组装结果。
type BriefResult struct {
	Text      string   // 组装好的、token 受限的简报
	Tokens    int      // llm.EstimateTokens(Text)
	Truncated bool     // 是否有任意 section 被裁剪以适配预算
	Sections  []string // 实际纳入的 section 名（用于 audit/debug）
}

// Composer 是 brief 包的唯一入口。
type Composer interface {
	ComposeTaskBrief(ctx context.Context, in BriefInput) (BriefResult, error)
}

// Config 控制 Composer 的默认预算与定向参数。
type Config struct {
	DefaultTokenBudget int
	MaxCommits         int
	MaxDirtyFiles      int
}

func (c Config) withDefaults() Config {
	if c.DefaultTokenBudget <= 0 {
		c.DefaultTokenBudget = DefaultTokenBudget
	}
	if c.MaxCommits <= 0 {
		c.MaxCommits = defaultMaxCommits
	}
	if c.MaxDirtyFiles <= 0 {
		c.MaxDirtyFiles = defaultMaxDirty
	}
	return c
}

type composer struct {
	orienter RepoOrienter
	cfg      Config
}

// NewComposer 构造 Composer。orienter 可为 nil（chat 场景通常不做 git 定向以避免
// 请求路径阻塞）；nil 时跳过仓库定向 section。
func NewComposer(orienter RepoOrienter, cfg Config) Composer {
	return &composer{orienter: orienter, cfg: cfg.withDefaults()}
}

// section 是简报的一段，按 priority 升序加入（数字越小优先级越高、越先放入预算）。
type section struct {
	name        string
	body        string
	truncatable bool // true 时预算不足可截断；false 时只能整段纳入或整段丢弃
}

// ComposeTaskBrief 按优先级顺序组装 section 并做 section 粒度的 token 预算控制。
//
// 优先级（高→低）：
//
//	role/instructions > requirement/issue core > acceptance criteria >
//	latest auditing artifact > prototyping > planning > repo orientation > user prompt
//
// user prompt 永远最后追加且不被截断（即使它单独就超预算也保留——它是操作者的
// 真实意图，宁可超 budget 也不能丢）。
func (c *composer) ComposeTaskBrief(ctx context.Context, in BriefInput) (BriefResult, error) {
	budget := in.TokenBudget
	if budget <= 0 {
		budget = c.cfg.DefaultTokenBudget
	}

	secs := c.buildSections(ctx, in)

	var (
		included []string
		bodies   []string
		used     int
		trunc    bool
	)

	for _, s := range secs {
		if s.body == "" {
			continue
		}
		// user prompt 特殊：永远纳入、永不截断。
		if !s.truncatable {
			bodies = append(bodies, s.body)
			included = append(included, s.name)
			used += llm.EstimateTokens(s.body)
			continue
		}

		remaining := budget - used
		if remaining < minSectionTokens {
			trunc = true
			continue
		}

		cost := llm.EstimateTokens(s.body)
		if cost <= remaining {
			bodies = append(bodies, s.body)
			included = append(included, s.name)
			used += cost
			continue
		}

		// 预算不足：截断当前 section 以塞进剩余预算。
		cut := truncateToTokens(s.body, remaining-truncatedSectionTokens)
		if cut == "" {
			trunc = true
			continue
		}
		body := cut + truncateMarker
		bodies = append(bodies, body)
		included = append(included, s.name)
		used += llm.EstimateTokens(body)
		trunc = true
	}

	text := strings.Join(bodies, "\n\n")
	return BriefResult{
		Text:      text,
		Tokens:    llm.EstimateTokens(text),
		Truncated: trunc,
		Sections:  included,
	}, nil
}

// buildSections 把 BriefInput 解析成有序的 section 列表（已按优先级排好）。
func (c *composer) buildSections(ctx context.Context, in BriefInput) []section {
	var secs []section

	// 1. 角色 / 指令
	if role := c.rolePrompt(in); role != "" {
		secs = append(secs, section{name: "role", body: role, truncatable: false})
	}
	if in.Kind == KindACPSession || in.Requirement != nil || in.Issue != nil {
		secs = append(secs, section{name: "interactive_protocol", body: InteractiveQuestionProtocol, truncatable: false})
	}

	// 2. requirement / issue 核心
	if core := requirementCore(in.Requirement); core != "" {
		secs = append(secs, section{name: "requirement_core", body: core, truncatable: true})
	}
	if core := issueCore(in.Issue); core != "" {
		secs = append(secs, section{name: "issue_core", body: core, truncatable: true})
	}

	latest := latestArtifactsByPhase(in.Artifacts)

	// 3. 验收标准：优先取 requirement 的最新 planning 产物中的"验收标准"段落。
	if ac := acceptanceCriteria(in.Requirement, latest); ac != "" {
		secs = append(secs, section{name: "acceptance_criteria", body: ac, truncatable: true})
	}

	// 4-6. 阶段产物（auditing > prototyping > planning）
	if a := latest[project.PhaseAuditing]; a != nil {
		secs = append(secs, section{name: "artifact_auditing", body: artifactBody(a), truncatable: true})
	}
	if a := latest[project.PhasePrototyping]; a != nil {
		secs = append(secs, section{name: "artifact_prototyping", body: artifactBody(a), truncatable: true})
	}
	if a := latest[project.PhasePlanning]; a != nil {
		secs = append(secs, section{name: "artifact_planning", body: artifactBody(a), truncatable: true})
	}

	// 7. 仓库定向（仅 ACP 场景且有 orienter + cwd）
	if c.orienter != nil && in.RepoCwd != "" {
		ori := c.orienter.Orient(ctx, in.RepoCwd, in.Branch, c.cfg.MaxCommits, c.cfg.MaxDirtyFiles)
		if body := orientationBody(ori); body != "" {
			secs = append(secs, section{name: "repo_orientation", body: body, truncatable: true})
		}
	}

	// 8. 用户自由文本（最后，永不截断）
	if up := strings.TrimSpace(in.UserPrompt); up != "" {
		secs = append(secs, section{name: "user_prompt", body: "## 操作者指令\n" + up, truncatable: false})
	}

	return secs
}

func (c *composer) rolePrompt(in BriefInput) string {
	// requirement 有阶段时优先用阶段角色（与原前端 PHASE_ROLE_PROMPTS 行为一致）。
	if in.Requirement != nil {
		if rp, ok := PhaseRolePrompts[in.Requirement.Phase]; ok && rp != "" {
			return rp
		}
	}
	if in.Kind == KindACPSession {
		return ACPRolePrompt
	}
	return ""
}

func requirementCore(req *project.Requirement) string {
	if req == nil {
		return ""
	}
	var b strings.Builder
	fmt.Fprintf(&b, "## 当前需求\n需求 #%d：%s", req.Number, req.Title)
	if d := strings.TrimSpace(req.Description); d != "" {
		fmt.Fprintf(&b, "\n### 需求描述\n%s", d)
	}
	return b.String()
}

func issueCore(iss *project.Issue) string {
	if iss == nil {
		return ""
	}
	var b strings.Builder
	fmt.Fprintf(&b, "## 当前 Issue\nIssue #%d：%s", iss.Number, iss.Title)
	if d := strings.TrimSpace(iss.Description); d != "" {
		fmt.Fprintf(&b, "\n### Issue 描述\n%s", d)
	}
	return b.String()
}

func artifactBody(a *project.Artifact) string {
	if a == nil {
		return ""
	}
	content := strings.TrimSpace(a.Content)
	if content == "" {
		return ""
	}
	return fmt.Sprintf("## %s 阶段产物 v%d\n%s", a.Phase, a.Version, content)
}

func orientationBody(o RepoOrientation) string {
	if o.IsEmpty() {
		return ""
	}
	var b strings.Builder
	b.WriteString("## 仓库现状")
	if o.Branch != "" {
		fmt.Fprintf(&b, "\n当前分支：%s", o.Branch)
	}
	if len(o.RecentCommits) > 0 {
		b.WriteString("\n最近提交：")
		for _, c := range o.RecentCommits {
			fmt.Fprintf(&b, "\n- %s", c)
		}
	}
	if len(o.DirtyFiles) > 0 {
		b.WriteString("\n未提交改动：")
		for _, f := range o.DirtyFiles {
			fmt.Fprintf(&b, "\n- %s", f)
		}
	}
	return b.String()
}

// latestArtifactsByPhase 按 (phase) 取 version 最大的产物。输入可包含同阶段多版本。
func latestArtifactsByPhase(arts []*project.Artifact) map[project.Phase]*project.Artifact {
	out := make(map[project.Phase]*project.Artifact, len(arts))
	for _, a := range arts {
		if a == nil {
			continue
		}
		cur, ok := out[a.Phase]
		if !ok || a.Version > cur.Version {
			out[a.Phase] = a
		}
	}
	return out
}

// acceptanceCriteriaHeadings 是从 planning 产物中提取"验收标准"段落时识别的标题关键字。
var acceptanceCriteriaHeadings = []string{"验收标准", "acceptance criteria", "acceptance"}

// acceptanceCriteria 解析验收标准。当前 requirement 无一级字段，回退为从最新
// planning 产物的 Markdown 中提取"验收标准 / Acceptance Criteria"小节。
// 保守策略：找不到明确小节时返回空串（宁可省略也不误纳入）。
func acceptanceCriteria(_ *project.Requirement, latest map[project.Phase]*project.Artifact) string {
	planning := latest[project.PhasePlanning]
	if planning == nil {
		return ""
	}
	body := extractSection(planning.Content, acceptanceCriteriaHeadings)
	if body == "" {
		return ""
	}
	return "## 验收标准\n" + body
}

// extractSection 从 Markdown 文本中提取标题匹配 headings 之一的小节正文（到下一个
// 同级或更高级标题前为止）。匹配大小写不敏感、忽略标题中的标点与空白。
func extractSection(md string, headings []string) string {
	lines := strings.Split(md, "\n")
	startIdx := -1
	startLevel := 0
	for i, line := range lines {
		level, title, ok := parseHeading(line)
		if !ok {
			continue
		}
		if matchesHeading(title, headings) {
			startIdx = i
			startLevel = level
			break
		}
	}
	if startIdx < 0 {
		return ""
	}
	var bodyLines []string
	for i := startIdx + 1; i < len(lines); i++ {
		level, _, ok := parseHeading(lines[i])
		if ok && level <= startLevel {
			break
		}
		bodyLines = append(bodyLines, lines[i])
	}
	return strings.TrimSpace(strings.Join(bodyLines, "\n"))
}

// parseHeading 解析一行是否为 ATX Markdown 标题（# / ## / ...）。返回级别与标题文本。
func parseHeading(line string) (level int, title string, ok bool) {
	trimmed := strings.TrimLeft(line, " ")
	n := 0
	for n < len(trimmed) && trimmed[n] == '#' {
		n++
	}
	if n == 0 || n > 6 {
		return 0, "", false
	}
	if n < len(trimmed) && trimmed[n] != ' ' {
		return 0, "", false
	}
	return n, strings.TrimSpace(trimmed[n:]), true
}

func matchesHeading(title string, headings []string) bool {
	norm := normalizeHeading(title)
	for _, h := range headings {
		if norm == normalizeHeading(h) {
			return true
		}
	}
	return false
}

// normalizeHeading 归一化标题：小写、去掉首尾标点与空白、去掉常见编号前缀。
func normalizeHeading(s string) string {
	s = strings.ToLower(strings.TrimSpace(s))
	s = strings.Trim(s, " 　:：.。、")
	// 去掉形如 "6. " / "6、" 的编号前缀
	for i := 0; i < len(s); i++ {
		if s[i] >= '0' && s[i] <= '9' {
			continue
		}
		if s[i] == '.' || s[i] == ' ' || s[i] == '\t' {
			s = strings.TrimLeft(s[i:], ". \t")
		}
		break
	}
	return strings.TrimSpace(s)
}

// truncateToTokens 把 s 裁剪到不超过 maxTokens 的最长前缀（按 rune 与 EstimateTokens
// 的 rune/4 估算反推，再二分微调，保证不超预算）。maxTokens<=0 返回空串。
func truncateToTokens(s string, maxTokens int) string {
	if maxTokens <= 0 {
		return ""
	}
	runes := []rune(s)
	if llm.EstimateTokens(s) <= maxTokens {
		return s
	}
	// EstimateTokens(x) = ceil(len(runes)/4)，故 maxRunes ≈ maxTokens*4。
	hi := maxTokens * 4
	if hi > len(runes) {
		hi = len(runes)
	}
	// 用 sort.Search 找最大的 n（runes 前缀）使 EstimateTokens 仍 <= maxTokens。
	n := sort.Search(hi+1, func(k int) bool {
		return llm.EstimateTokens(string(runes[:k])) > maxTokens
	})
	if n > 0 {
		n--
	}
	return strings.TrimRight(string(runes[:n]), " \n\t")
}
