package agentnode

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"sort"
	"strconv"
	"strings"

	agentllm "github.com/LoveLosita/smartflow/backend/agent2/llm"
	"github.com/LoveLosita/smartflow/backend/model"
)

// SchedulePlanToolDeps 描述“智能排程 graph”运行所需的外部业务依赖。
//
// 职责边界：
// 1. 只负责声明“需要哪些能力”，不负责具体实现（实现由 service 层注入）。
// 2. 只收口函数签名，不承载业务状态，避免跨请求共享可变数据。
// 3. 当前统一采用 task_class_ids 语义，不再依赖单 task_class_id 主路径。
type SchedulePlanToolDeps struct {
	// SmartPlanningMultiRaw 是可选依赖：
	// 1) 用于需要单独输出“粗排预览”时复用；
	// 2) 当前主链路已由 HybridScheduleWithPlanMulti 覆盖，可不注入。
	SmartPlanningMultiRaw func(ctx context.Context, userID int, taskClassIDs []int) ([]model.UserWeekSchedule, []model.TaskClassItem, error)

	// HybridScheduleWithPlanMulti 把“既有日程 + 粗排结果”合并成统一的 HybridScheduleEntry 切片，
	// 供 daily/weekly ReAct 节点在内存中继续优化。
	HybridScheduleWithPlanMulti func(ctx context.Context, userID int, taskClassIDs []int) ([]model.HybridScheduleEntry, []model.TaskClassItem, error)

	// ResolvePlanningWindow 根据 task_class_ids 解析“全局排程窗口”的相对周/天边界。
	//
	// 返回语义：
	// 1. startWeek/startDay：窗口起点（含）；
	// 2. endWeek/endDay：窗口终点（含）；
	// 3. error：解析失败（如任务类不存在、日期非法）。
	//
	// 用途：
	// 1. 给周级 Move 工具加硬边界，避免把任务移动到窗口外的天数；
	// 2. 解决“首尾不足一周”场景下的周内越界问题。
	ResolvePlanningWindow func(ctx context.Context, userID int, taskClassIDs []int) (startWeek, startDay, endWeek, endDay int, err error)
}

// Validate 校验依赖完整性。
//
// 失败处理：
// 1. 任意依赖缺失都直接返回错误，避免 graph 运行到中途才 panic。
// 2. 调用方（runSchedulePlanFlow）收到错误后会走回退链路，不影响普通聊天可用性。
func (d SchedulePlanToolDeps) Validate() error {
	if d.HybridScheduleWithPlanMulti == nil {
		return errors.New("schedule plan tool deps: HybridScheduleWithPlanMulti is nil")
	}
	return nil
}

// ExtraInt 从 extra map 中安全提取整数值。
//
// 兼容策略：
// 1) JSON 数字默认解析为 float64，做 int 转换；
// 2) 兼容字符串形式（如 "42"），用 Atoi 解析；
// 3) 其余类型返回 false，由调用方决定后续处理。
func ExtraInt(extra map[string]any, key string) (int, bool) {
	v, ok := extra[key]
	if !ok {
		return 0, false
	}
	switch n := v.(type) {
	case float64:
		return int(n), true
	case int:
		return n, true
	case string:
		i, err := strconv.Atoi(n)
		return i, err == nil
	default:
		return 0, false
	}
}

// ExtraIntSlice 从 extra map 中安全提取整数切片。
//
// 兼容输入：
// 1) []any（JSON 数组反序列化后的常见类型）；
// 2) []int；
// 3) []float64；
// 4) 逗号分隔字符串（例如 "1,2,3"）。
//
// 返回语义：
// 1) ok=true：至少成功解析出一个整数；
// 2) ok=false：字段不存在或全部解析失败。
func ExtraIntSlice(extra map[string]any, key string) ([]int, bool) {
	v, exists := extra[key]
	if !exists {
		return nil, false
	}

	parseOne := func(raw any) (int, error) {
		switch n := raw.(type) {
		case int:
			return n, nil
		case float64:
			return int(n), nil
		case string:
			i, err := strconv.Atoi(n)
			if err != nil {
				return 0, err
			}
			return i, nil
		default:
			return 0, fmt.Errorf("unsupported type: %T", raw)
		}
	}

	out := make([]int, 0)
	switch arr := v.(type) {
	case []int:
		for _, item := range arr {
			out = append(out, item)
		}
	case []float64:
		for _, item := range arr {
			out = append(out, int(item))
		}
	case []any:
		for _, item := range arr {
			if parsed, err := parseOne(item); err == nil {
				out = append(out, parsed)
			}
		}
	case string:
		parts := strings.Split(arr, ",")
		for _, part := range parts {
			part = strings.TrimSpace(part)
			if part == "" {
				continue
			}
			if parsed, err := strconv.Atoi(part); err == nil {
				out = append(out, parsed)
			}
		}
	default:
		return nil, false
	}

	if len(out) == 0 {
		return nil, false
	}
	return out, true
}

// ── ReAct Tool 调用/结果结构 ──

// reactToolCall 是 LLM 输出的单个工具调用。
type reactToolCall = agentllm.ReactToolCall

// reactToolResult 是单个工具调用的执行结果。
type reactToolResult struct {
	Tool    string `json:"tool"`
	Success bool   `json:"success"`
	Result  string `json:"result"`
}

// reactLLMOutput 是 LLM 输出的完整 JSON 结构。
type reactLLMOutput = agentllm.ReactLLMOutput

// weeklyPlanningWindow 表示周级优化可用的全局周/天窗口。
//
// 语义：
// 1. Enabled=false：不启用窗口硬边界，仅做基础合法性校验；
// 2. Enabled=true：Move 必须落在 [StartWeek/StartDay, EndWeek/EndDay] 内；
// 3. 该窗口用于处理“首尾不足一周”场景下的越界移动问题。
type weeklyPlanningWindow struct {
	Enabled   bool
	StartWeek int
	StartDay  int
	EndWeek   int
	EndDay    int
}

// ── 工具分发器 ──

// dispatchReactTool 根据工具名分发调用，返回（可能修改后的）entries 和执行结果。
func dispatchReactTool(entries []model.HybridScheduleEntry, call reactToolCall) ([]model.HybridScheduleEntry, reactToolResult) {
	switch call.Tool {
	case "Swap":
		return reactToolSwap(entries, call.Params)
	case "Move":
		return reactToolMove(entries, call.Params)
	case "TimeAvailable":
		return entries, reactToolTimeAvailable(entries, call.Params)
	case "GetAvailableSlots":
		return entries, reactToolGetAvailableSlots(entries, call.Params)
	default:
		return entries, reactToolResult{Tool: call.Tool, Success: false, Result: fmt.Sprintf("未知工具: %s", call.Tool)}
	}
}

// dispatchWeeklySingleActionTool 是“周级单步动作模式”的专用分发器。
//
// 职责边界：
// 1. 仅允许 Move / Swap 两个工具，禁止 TimeAvailable / GetAvailableSlots；
// 2. 强制 Move 的目标周必须等于 currentWeek，避免并发周优化时发生跨周写穿；
// 3. 统一返回工具执行结果，供上层决定预算扣减与下一轮上下文拼接。
func dispatchWeeklySingleActionTool(entries []model.HybridScheduleEntry, call reactToolCall, currentWeek int, window weeklyPlanningWindow) ([]model.HybridScheduleEntry, reactToolResult) {
	tool := strings.TrimSpace(call.Tool)
	switch tool {
	case "Swap":
		return reactToolSwap(entries, call.Params)
	case "Move":
		// 1. 周级并发模式下，每个 worker 只负责单周数据。
		// 2. 为避免“一个 worker 改到别的周”导致并发写冲突，这里做硬约束。
		// 3. 失败时不抛异常，返回工具失败结果，让上层继续下一轮决策。
		toWeek, ok := paramInt(call.Params, "to_week")
		if !ok {
			return entries, reactToolResult{Tool: "Move", Success: false, Result: "参数缺失：需要 to_week"}
		}
		if toWeek != currentWeek {
			return entries, reactToolResult{
				Tool:    "Move",
				Success: false,
				Result:  fmt.Sprintf("当前仅允许优化本周：worker_week=%d，目标周=%d", currentWeek, toWeek),
			}
		}
		// 4. 若已配置全局窗口边界，再做“首尾不足一周”硬校验。
		// 4.1 这样可避免把任务移动到窗口外的天数（例如起始周的起始日前、结束周的结束日后）。
		// 4.2 窗口未启用时不阻断，保持兼容旧链路。
		if window.Enabled {
			toDay, ok := paramInt(call.Params, "to_day")
			if !ok {
				return entries, reactToolResult{Tool: "Move", Success: false, Result: "参数缺失：需要 to_day"}
			}
			allowed, dayFrom, dayTo := isDayWithinPlanningWindow(window, toWeek, toDay)
			if !allowed {
				return entries, reactToolResult{
					Tool:    "Move",
					Success: false,
					Result:  fmt.Sprintf("目标日期超出排程窗口：W%d 仅允许 D%d-D%d，当前目标为 D%d", toWeek, dayFrom, dayTo, toDay),
				}
			}
		}
		return reactToolMove(entries, call.Params)
	default:
		return entries, reactToolResult{
			Tool:    tool,
			Success: false,
			Result:  fmt.Sprintf("周级单步模式不支持工具: %s，仅允许 Move/Swap", tool),
		}
	}
}

// isDayWithinPlanningWindow 判断目标 week/day 是否落在窗口范围内。
//
// 返回值：
// 1. allowed：是否允许；
// 2. dayFrom/dayTo：该周允许的 day 区间（用于错误提示）。
func isDayWithinPlanningWindow(window weeklyPlanningWindow, week int, day int) (allowed bool, dayFrom int, dayTo int) {
	// 1. 窗口未启用时默认允许（调用方会跳过此分支，这里是兜底）。
	if !window.Enabled {
		return true, 1, 7
	}
	// 2. 先做周范围校验。
	if week < window.StartWeek || week > window.EndWeek {
		return false, 1, 7
	}
	// 3. 计算当前周允许的 day 边界。
	from := 1
	to := 7
	if week == window.StartWeek {
		from = window.StartDay
	}
	if week == window.EndWeek {
		to = window.EndDay
	}
	if day < from || day > to {
		return false, from, to
	}
	return true, from, to
}

// ── 参数提取辅助 ──

func paramInt(params map[string]any, key string) (int, bool) {
	v, ok := params[key]
	if !ok {
		return 0, false
	}
	switch n := v.(type) {
	case float64:
		return int(n), true
	case int:
		return n, true
	default:
		return 0, false
	}
}

// findSuggestedByID 在 entries 中查找指定 TaskItemID 的 suggested 条目索引。
func findSuggestedByID(entries []model.HybridScheduleEntry, taskItemID int) int {
	for i, e := range entries {
		if e.TaskItemID == taskItemID && e.Status == "suggested" {
			return i
		}
	}
	return -1
}

// sectionsOverlap 判断两个节次区间是否有交集。
func sectionsOverlap(aFrom, aTo, bFrom, bTo int) bool {
	return aFrom <= bTo && bFrom <= aTo
}

// entryBlocksSuggested 判断某条目是否应阻塞 suggested 任务占位。
//
// 规则：
// 1. suggested 任务永远阻塞（任务之间不能重叠）；
// 2. existing 条目按 BlockForSuggested 字段决定；
// 3. 其余场景默认阻塞（保守策略，避免放出脏可用槽）。
func entryBlocksSuggested(entry model.HybridScheduleEntry) bool {
	if entry.Status == "suggested" {
		return true
	}
	// existing 走显式字段语义。
	if entry.Status == "existing" {
		return entry.BlockForSuggested
	}
	// 未知状态兜底：按阻塞处理。
	return true
}

// hasConflict 检查目标时间段是否与 entries 中任何条目冲突（排除 excludeIdx）。
func hasConflict(entries []model.HybridScheduleEntry, week, day, sf, st, excludeIdx int) (bool, string) {
	for i, e := range entries {
		if i == excludeIdx {
			continue
		}
		// 1. 可嵌入且未占用的课程槽（BlockForSuggested=false）不参与冲突判断。
		// 2. 这样可以避免把“水课可嵌入位”误判为硬冲突。
		if !entryBlocksSuggested(e) {
			continue
		}
		if e.Week == week && e.DayOfWeek == day && sectionsOverlap(e.SectionFrom, e.SectionTo, sf, st) {
			return true, fmt.Sprintf("%s(%s)", e.Name, e.Type)
		}
	}
	return false, ""
}

// ══════════════════════════════════════════════════════════════
//  Tool 1: Swap — 交换两个 suggested 任务的时间
// ══════════════════════════════════════════════════════════════

func reactToolSwap(entries []model.HybridScheduleEntry, params map[string]any) ([]model.HybridScheduleEntry, reactToolResult) {
	idA, okA := paramInt(params, "task_a")
	idB, okB := paramInt(params, "task_b")
	if !okA || !okB {
		return entries, reactToolResult{Tool: "Swap", Success: false, Result: "参数缺失：需要 task_a 和 task_b（task_item_id）"}
	}
	if idA == idB {
		return entries, reactToolResult{Tool: "Swap", Success: false, Result: "task_a 和 task_b 不能相同"}
	}

	idxA := findSuggestedByID(entries, idA)
	idxB := findSuggestedByID(entries, idB)
	if idxA == -1 {
		return entries, reactToolResult{Tool: "Swap", Success: false, Result: fmt.Sprintf("找不到 task_item_id=%d 的 suggested 任务", idA)}
	}
	if idxB == -1 {
		return entries, reactToolResult{Tool: "Swap", Success: false, Result: fmt.Sprintf("找不到 task_item_id=%d 的 suggested 任务", idB)}
	}

	// 交换时间坐标
	a, b := &entries[idxA], &entries[idxB]
	a.Week, b.Week = b.Week, a.Week
	a.DayOfWeek, b.DayOfWeek = b.DayOfWeek, a.DayOfWeek
	a.SectionFrom, b.SectionFrom = b.SectionFrom, a.SectionFrom
	a.SectionTo, b.SectionTo = b.SectionTo, a.SectionTo

	return entries, reactToolResult{
		Tool: "Swap", Success: true,
		Result: fmt.Sprintf("已交换 [%s](id=%d) 和 [%s](id=%d) 的时间", a.Name, idA, b.Name, idB),
	}
}

// ══════════════════════════════════════════════════════════════
//  Tool 2: Move — 将一个 suggested 任务移动到新时间
// ══════════════════════════════════════════════════════════════

func reactToolMove(entries []model.HybridScheduleEntry, params map[string]any) ([]model.HybridScheduleEntry, reactToolResult) {
	taskID, ok := paramInt(params, "task_item_id")
	if !ok {
		return entries, reactToolResult{Tool: "Move", Success: false, Result: "参数缺失：需要 task_item_id"}
	}
	toWeek, ok1 := paramInt(params, "to_week")
	toDay, ok2 := paramInt(params, "to_day")
	toSF, ok3 := paramInt(params, "to_section_from")
	toST, ok4 := paramInt(params, "to_section_to")
	if !ok1 || !ok2 || !ok3 || !ok4 {
		return entries, reactToolResult{Tool: "Move", Success: false, Result: "参数缺失：需要 to_week, to_day, to_section_from, to_section_to"}
	}

	// 基础校验
	if toDay < 1 || toDay > 7 {
		return entries, reactToolResult{Tool: "Move", Success: false, Result: fmt.Sprintf("day_of_week=%d 不合法，应为 1-7", toDay)}
	}
	if toSF < 1 || toST > 12 || toSF > toST {
		return entries, reactToolResult{Tool: "Move", Success: false, Result: fmt.Sprintf("节次范围 %d-%d 不合法，应为 1-12 且 from<=to", toSF, toST)}
	}

	idx := findSuggestedByID(entries, taskID)
	if idx == -1 {
		return entries, reactToolResult{Tool: "Move", Success: false, Result: fmt.Sprintf("找不到 task_item_id=%d 的 suggested 任务", taskID)}
	}

	// 节次跨度必须一致
	origSpan := entries[idx].SectionTo - entries[idx].SectionFrom
	newSpan := toST - toSF
	if origSpan != newSpan {
		return entries, reactToolResult{Tool: "Move", Success: false,
			Result: fmt.Sprintf("节次跨度不一致：原任务占 %d 节，目标占 %d 节", origSpan+1, newSpan+1)}
	}

	// 冲突检测（排除自身）
	if conflict, name := hasConflict(entries, toWeek, toDay, toSF, toST, idx); conflict {
		return entries, reactToolResult{Tool: "Move", Success: false,
			Result: fmt.Sprintf("目标时间 W%dD%d 第%d-%d节 已被 %s 占用", toWeek, toDay, toSF, toST, name)}
	}

	// 执行移动
	e := &entries[idx]
	oldDesc := fmt.Sprintf("W%dD%d 第%d-%d节", e.Week, e.DayOfWeek, e.SectionFrom, e.SectionTo)
	e.Week, e.DayOfWeek, e.SectionFrom, e.SectionTo = toWeek, toDay, toSF, toST
	newDesc := fmt.Sprintf("W%dD%d 第%d-%d节", toWeek, toDay, toSF, toST)

	return entries, reactToolResult{
		Tool: "Move", Success: true,
		Result: fmt.Sprintf("已将 [%s](id=%d) 从 %s 移动到 %s", e.Name, taskID, oldDesc, newDesc),
	}
}

// ══════════════════════════════════════════════════════════════
//  Tool 3: TimeAvailable — 检查目标时间段是否可用
// ══════════════════════════════════════════════════════════════

func reactToolTimeAvailable(entries []model.HybridScheduleEntry, params map[string]any) reactToolResult {
	week, ok1 := paramInt(params, "week")
	day, ok2 := paramInt(params, "day_of_week")
	sf, ok3 := paramInt(params, "section_from")
	st, ok4 := paramInt(params, "section_to")
	if !ok1 || !ok2 || !ok3 || !ok4 {
		return reactToolResult{Tool: "TimeAvailable", Success: false, Result: "参数缺失：需要 week, day_of_week, section_from, section_to"}
	}

	if conflict, name := hasConflict(entries, week, day, sf, st, -1); conflict {
		return reactToolResult{Tool: "TimeAvailable", Success: true,
			Result: fmt.Sprintf(`{"available":false,"conflict_with":"%s"}`, name)}
	}
	return reactToolResult{Tool: "TimeAvailable", Success: true, Result: `{"available":true}`}
}

// ══════════════════════════════════════════════════════════════
//  Tool 4: GetAvailableSlots — 返回可用时间段列表
// ══════════════════════════════════════════════════════════════

func reactToolGetAvailableSlots(entries []model.HybridScheduleEntry, params map[string]any) reactToolResult {
	filterWeek, _ := paramInt(params, "week") // 0 表示不过滤

	// 1. 收集所有周次范围
	minW, maxW := 999, 0
	for _, e := range entries {
		if e.Week < minW {
			minW = e.Week
		}
		if e.Week > maxW {
			maxW = e.Week
		}
	}
	if minW > maxW {
		return reactToolResult{Tool: "GetAvailableSlots", Success: true, Result: "[]"}
	}

	// 2. 构建占用集合
	type slotKey struct{ W, D, S int }
	occupied := make(map[slotKey]bool)
	for _, e := range entries {
		if !entryBlocksSuggested(e) {
			continue
		}
		for s := e.SectionFrom; s <= e.SectionTo; s++ {
			occupied[slotKey{e.Week, e.DayOfWeek, s}] = true
		}
	}

	// 3. 遍历所有时间格，找出空闲并合并连续节次
	type availSlot struct {
		Week, Day, From, To int
	}
	var slots []availSlot

	startW, endW := minW, maxW
	if filterWeek > 0 {
		startW, endW = filterWeek, filterWeek
	}

	for w := startW; w <= endW; w++ {
		for d := 1; d <= 7; d++ {
			runStart := 0
			for s := 1; s <= 12; s++ {
				if !occupied[slotKey{w, d, s}] {
					if runStart == 0 {
						runStart = s
					}
				} else {
					if runStart > 0 {
						slots = append(slots, availSlot{w, d, runStart, s - 1})
						runStart = 0
					}
				}
			}
			if runStart > 0 {
				slots = append(slots, availSlot{w, d, runStart, 12})
			}
		}
	}

	// 4. 按自然顺序排序（已经是了，但确保）
	sort.Slice(slots, func(i, j int) bool {
		if slots[i].Week != slots[j].Week {
			return slots[i].Week < slots[j].Week
		}
		if slots[i].Day != slots[j].Day {
			return slots[i].Day < slots[j].Day
		}
		return slots[i].From < slots[j].From
	})

	// 5. 序列化
	type slotJSON struct {
		Week        int `json:"week"`
		DayOfWeek   int `json:"day_of_week"`
		SectionFrom int `json:"section_from"`
		SectionTo   int `json:"section_to"`
	}
	out := make([]slotJSON, 0, len(slots))
	for _, s := range slots {
		out = append(out, slotJSON{s.Week, s.Day, s.From, s.To})
	}

	data, _ := json.Marshal(out)
	return reactToolResult{Tool: "GetAvailableSlots", Success: true, Result: string(data)}
}

// ── 辅助：解析 LLM 输出 ──

// parseReactLLMOutput 解析 LLM 的 JSON 输出。
// 兼容 ```json ... ``` 包裹。
func parseReactLLMOutput(raw string) (*reactLLMOutput, error) {
	return agentllm.ParseScheduleReactOutput(raw)
}

// truncate 截断字符串到指定长度。
func truncate(s string, maxLen int) string {
	if maxLen <= 0 {
		return ""
	}
	runes := []rune(s)
	if len(runes) <= maxLen {
		return s
	}
	return string(runes[:maxLen]) + "..."
}
