package logic

import (
	"fmt"
	"sort"
	"strings"
)

// RefineTaskCandidate 表示复合工具规划阶段可移动的任务候选。
//
// 职责边界：
// 1. 只承载“任务当前坐标 + 规划所需标签”；
// 2. 不承载冲突判断、窗口判断等执行期逻辑；
// 3. 由调用方保证 task_item_id 唯一且为正数。
type RefineTaskCandidate struct {
	TaskItemID  int
	Week        int
	DayOfWeek   int
	SectionFrom int
	SectionTo   int
	Name        string
	ContextTag  string
	OriginRank  int
}

// RefineSlotCandidate 表示复合工具可选落点（坑位）。
//
// 职责边界：
// 1. 只描述可候选的时段坐标；
// 2. 不描述“为什么可用”，可用性由调用方预先筛好；
// 3. Span 由 SectionFrom/SectionTo 推导，不单独存储。
type RefineSlotCandidate struct {
	Week        int
	DayOfWeek   int
	SectionFrom int
	SectionTo   int
}

// RefineMovePlanItem 表示“任务 -> 目标坑位”的确定性规划结果。
type RefineMovePlanItem struct {
	TaskItemID    int
	ToWeek        int
	ToDay         int
	ToSectionFrom int
	ToSectionTo   int
}

// RefineCompositePlanOptions 是复合规划器的可选辅助输入。
//
// 说明：
// 1. ExistingDayLoad 用于提供“目标范围内的既有负载基线”，用于均匀铺开打分；
// 2. key 约定为 "week-day"，例如 "16-3"；
// 3. 未提供时，规划器按 0 基线处理。
type RefineCompositePlanOptions struct {
	ExistingDayLoad map[string]int
}

// PlanEvenSpreadMoves 规划“均匀铺开”的确定性移动方案。
//
// 步骤化说明：
// 1. 先按稳定顺序归一化任务与坑位，保证同输入必同输出；
// 2. 逐任务选择“投放后日负载最小”的坑位，主目标是降低日负载离散度；
// 3. 同分时按时间更早优先，进一步保证确定性；
// 4. 若某任务不存在同跨度坑位，直接失败并返回明确错误。
func PlanEvenSpreadMoves(tasks []RefineTaskCandidate, slots []RefineSlotCandidate, options RefineCompositePlanOptions) ([]RefineMovePlanItem, error) {
	normalizedTasks, err := normalizeRefineTasks(tasks)
	if err != nil {
		return nil, err
	}
	normalizedSlots, err := normalizeRefineSlots(slots)
	if err != nil {
		return nil, err
	}
	if len(normalizedSlots) < len(normalizedTasks) {
		return nil, fmt.Errorf("可用坑位不足：tasks=%d, slots=%d", len(normalizedTasks), len(normalizedSlots))
	}

	// 1. dayLoad 记录“当前已占 + 本次规划已分配”的日负载。
	// 2. 这里先写入调用方提供的既有基线，再在循环中动态递增。
	dayLoad := make(map[string]int, len(options.ExistingDayLoad)+len(normalizedSlots))
	for key, value := range options.ExistingDayLoad {
		if value <= 0 {
			continue
		}
		dayLoad[strings.TrimSpace(key)] = value
	}

	used := make([]bool, len(normalizedSlots))
	moves := make([]RefineMovePlanItem, 0, len(normalizedTasks))
	selectedSlots := make([]RefineSlotCandidate, 0, len(normalizedTasks))

	for _, task := range normalizedTasks {
		taskSpan := sectionSpan(task.SectionFrom, task.SectionTo)
		bestIdx := -1
		bestScore := int(^uint(0) >> 1) // max int

		for idx, slot := range normalizedSlots {
			if used[idx] {
				continue
			}
			if sectionSpan(slot.SectionFrom, slot.SectionTo) != taskSpan {
				continue
			}
			if slotOverlapsAny(slot, selectedSlots) {
				continue
			}
			dayKey := composeDayKey(slot.Week, slot.DayOfWeek)
			projectedLoad := dayLoad[dayKey] + 1
			// 1. projectedLoad 是主目标（越小越均衡）；
			// 2. idx 是次级目标（越早的坑位越优先，保证稳定）。
			score := projectedLoad*10000 + idx
			if score < bestScore {
				bestScore = score
				bestIdx = idx
			}
		}
		if bestIdx < 0 {
			return nil, fmt.Errorf("任务 id=%d 无可用同跨度坑位", task.TaskItemID)
		}

		chosen := normalizedSlots[bestIdx]
		used[bestIdx] = true
		selectedSlots = append(selectedSlots, chosen)
		dayLoad[composeDayKey(chosen.Week, chosen.DayOfWeek)]++
		moves = append(moves, RefineMovePlanItem{
			TaskItemID:    task.TaskItemID,
			ToWeek:        chosen.Week,
			ToDay:         chosen.DayOfWeek,
			ToSectionFrom: chosen.SectionFrom,
			ToSectionTo:   chosen.SectionTo,
		})
	}
	return moves, nil
}

// PlanMinContextSwitchMoves 规划“同科目上下文切换最少”的确定性移动方案。
//
// 步骤化说明：
// 1. 先把任务按 context_tag 分组，目标是让同组任务尽量连续；
// 2. 分组顺序按“组大小降序 + 最早 origin_rank + 标签字典序”稳定排序；
// 3. 组内按任务稳定顺序排，再顺序填入时间上最早可用同跨度坑位；
// 4. 若某任务不存在同跨度坑位，立即失败并返回明确错误。
func PlanMinContextSwitchMoves(tasks []RefineTaskCandidate, slots []RefineSlotCandidate, _ RefineCompositePlanOptions) ([]RefineMovePlanItem, error) {
	normalizedTasks, err := normalizeRefineTasks(tasks)
	if err != nil {
		return nil, err
	}
	normalizedSlots, err := normalizeRefineSlots(slots)
	if err != nil {
		return nil, err
	}
	if len(normalizedSlots) < len(normalizedTasks) {
		return nil, fmt.Errorf("可用坑位不足：tasks=%d, slots=%d", len(normalizedTasks), len(normalizedSlots))
	}

	type taskGroup struct {
		ContextKey string
		Tasks      []RefineTaskCandidate
		MinRank    int
	}
	groupingKeys := buildMinContextGroupingKeys(normalizedTasks)
	groupMap := make(map[string]*taskGroup)
	groupOrder := make([]string, 0, len(normalizedTasks))

	for _, task := range normalizedTasks {
		key := groupingKeys[task.TaskItemID]
		group, exists := groupMap[key]
		if !exists {
			group = &taskGroup{
				ContextKey: key,
				MinRank:    normalizedOriginRank(task),
			}
			groupMap[key] = group
			groupOrder = append(groupOrder, key)
		}
		group.Tasks = append(group.Tasks, task)
		if rank := normalizedOriginRank(task); rank < group.MinRank {
			group.MinRank = rank
		}
	}

	groups := make([]taskGroup, 0, len(groupMap))
	for _, key := range groupOrder {
		group := groupMap[key]
		sort.SliceStable(group.Tasks, func(i, j int) bool {
			return compareTaskOrder(group.Tasks[i], group.Tasks[j]) < 0
		})
		groups = append(groups, *group)
	}
	sort.SliceStable(groups, func(i, j int) bool {
		if len(groups[i].Tasks) != len(groups[j].Tasks) {
			return len(groups[i].Tasks) > len(groups[j].Tasks)
		}
		if groups[i].MinRank != groups[j].MinRank {
			return groups[i].MinRank < groups[j].MinRank
		}
		return groups[i].ContextKey < groups[j].ContextKey
	})

	orderedTasks := make([]RefineTaskCandidate, 0, len(normalizedTasks))
	for _, group := range groups {
		orderedTasks = append(orderedTasks, group.Tasks...)
	}

	used := make([]bool, len(normalizedSlots))
	moves := make([]RefineMovePlanItem, 0, len(orderedTasks))
	selectedSlots := make([]RefineSlotCandidate, 0, len(orderedTasks))
	for _, task := range orderedTasks {
		taskSpan := sectionSpan(task.SectionFrom, task.SectionTo)
		chosenIdx := -1
		for idx, slot := range normalizedSlots {
			if used[idx] {
				continue
			}
			if sectionSpan(slot.SectionFrom, slot.SectionTo) != taskSpan {
				continue
			}
			if slotOverlapsAny(slot, selectedSlots) {
				continue
			}
			chosenIdx = idx
			break
		}
		if chosenIdx < 0 {
			return nil, fmt.Errorf("任务 id=%d 无可用同跨度坑位", task.TaskItemID)
		}
		chosen := normalizedSlots[chosenIdx]
		used[chosenIdx] = true
		selectedSlots = append(selectedSlots, chosen)
		moves = append(moves, RefineMovePlanItem{
			TaskItemID:    task.TaskItemID,
			ToWeek:        chosen.Week,
			ToDay:         chosen.DayOfWeek,
			ToSectionFrom: chosen.SectionFrom,
			ToSectionTo:   chosen.SectionTo,
		})
	}
	return moves, nil
}

func normalizeRefineTasks(tasks []RefineTaskCandidate) ([]RefineTaskCandidate, error) {
	if len(tasks) == 0 {
		return nil, fmt.Errorf("任务列表为空")
	}
	normalized := make([]RefineTaskCandidate, 0, len(tasks))
	seen := make(map[int]struct{}, len(tasks))
	for _, task := range tasks {
		if task.TaskItemID <= 0 {
			return nil, fmt.Errorf("存在非法 task_item_id=%d", task.TaskItemID)
		}
		if _, exists := seen[task.TaskItemID]; exists {
			return nil, fmt.Errorf("任务 id=%d 重复", task.TaskItemID)
		}
		if !isValidDay(task.DayOfWeek) {
			return nil, fmt.Errorf("任务 id=%d day_of_week 非法=%d", task.TaskItemID, task.DayOfWeek)
		}
		if !isValidSection(task.SectionFrom, task.SectionTo) {
			return nil, fmt.Errorf("任务 id=%d 节次区间非法=%d-%d", task.TaskItemID, task.SectionFrom, task.SectionTo)
		}
		seen[task.TaskItemID] = struct{}{}
		normalized = append(normalized, task)
	}
	sort.SliceStable(normalized, func(i, j int) bool {
		return compareTaskOrder(normalized[i], normalized[j]) < 0
	})
	return normalized, nil
}

func normalizeRefineSlots(slots []RefineSlotCandidate) ([]RefineSlotCandidate, error) {
	if len(slots) == 0 {
		return nil, fmt.Errorf("可用坑位为空")
	}
	normalized := make([]RefineSlotCandidate, 0, len(slots))
	seen := make(map[string]struct{}, len(slots))
	for _, slot := range slots {
		if slot.Week <= 0 {
			return nil, fmt.Errorf("存在非法 week=%d", slot.Week)
		}
		if !isValidDay(slot.DayOfWeek) {
			return nil, fmt.Errorf("存在非法 day_of_week=%d", slot.DayOfWeek)
		}
		if !isValidSection(slot.SectionFrom, slot.SectionTo) {
			return nil, fmt.Errorf("存在非法节次区间=%d-%d", slot.SectionFrom, slot.SectionTo)
		}
		key := fmt.Sprintf("%d-%d-%d-%d", slot.Week, slot.DayOfWeek, slot.SectionFrom, slot.SectionTo)
		if _, exists := seen[key]; exists {
			continue
		}
		seen[key] = struct{}{}
		normalized = append(normalized, slot)
	}
	sort.SliceStable(normalized, func(i, j int) bool {
		if normalized[i].Week != normalized[j].Week {
			return normalized[i].Week < normalized[j].Week
		}
		if normalized[i].DayOfWeek != normalized[j].DayOfWeek {
			return normalized[i].DayOfWeek < normalized[j].DayOfWeek
		}
		if normalized[i].SectionFrom != normalized[j].SectionFrom {
			return normalized[i].SectionFrom < normalized[j].SectionFrom
		}
		return normalized[i].SectionTo < normalized[j].SectionTo
	})
	return normalized, nil
}

func compareTaskOrder(a, b RefineTaskCandidate) int {
	rankA := normalizedOriginRank(a)
	rankB := normalizedOriginRank(b)
	if rankA != rankB {
		return rankA - rankB
	}
	if a.Week != b.Week {
		return a.Week - b.Week
	}
	if a.DayOfWeek != b.DayOfWeek {
		return a.DayOfWeek - b.DayOfWeek
	}
	if a.SectionFrom != b.SectionFrom {
		return a.SectionFrom - b.SectionFrom
	}
	if a.SectionTo != b.SectionTo {
		return a.SectionTo - b.SectionTo
	}
	return a.TaskItemID - b.TaskItemID
}

func normalizedOriginRank(task RefineTaskCandidate) int {
	if task.OriginRank > 0 {
		return task.OriginRank
	}
	// 1. 无 origin_rank 时回退到较大稳定值，避免把“未知顺序”抢到前面。
	// 2. 叠加 task_id 作为细粒度稳定因子，保证排序可复现。
	return 1_000_000 + task.TaskItemID
}

func normalizeContextKey(tag string) string {
	text := strings.TrimSpace(tag)
	if text == "" {
		return "General"
	}
	return text
}

// buildMinContextGroupingKeys 为 MinContextSwitch 生成“实际用于聚类”的分组键。
//
// 步骤化说明：
// 1. 先优先使用现有 ContextTag，避免影响已稳定的显式标签链路；
// 2. 若整批任务只剩一个粗粒度标签（例如全是 General/High-Logic），说明标签对“同科目连续”帮助不足；
// 3. 此时再基于任务名做学科关键词兜底，只在确实能拉开分组时启用；
// 4. 若任务名也无法识别，则继续回落到原 ContextTag，保证行为可预测。
func buildMinContextGroupingKeys(tasks []RefineTaskCandidate) map[int]string {
	keys := make(map[int]string, len(tasks))
	distinctExplicit := make(map[string]struct{}, len(tasks))
	distinctNonCoarse := make(map[string]struct{}, len(tasks))

	for _, task := range tasks {
		key := normalizeContextKey(task.ContextTag)
		keys[task.TaskItemID] = key
		distinctExplicit[key] = struct{}{}
		if !isCoarseContextKey(key) {
			distinctNonCoarse[key] = struct{}{}
		}
	}

	// 1. 当显式标签已经至少区分出两类“非粗标签”时，直接尊重上游语义；
	// 2. 避免把已稳定的 context_tag 分组再改写成名称启发式结果。
	if len(distinctNonCoarse) >= 2 {
		return keys
	}
	// 1. 若显式标签本来就有 2 类及以上，且不全是粗标签，也继续沿用；
	// 2. 只有“整批退化到同一个粗标签”时，才值得尝试名称兜底。
	if len(distinctExplicit) > 1 && len(distinctNonCoarse) > 0 {
		return keys
	}

	inferredKeys := make(map[int]string, len(tasks))
	distinctInferred := make(map[string]struct{}, len(tasks))
	for _, task := range tasks {
		inferred := inferSubjectContextKeyFromTaskName(task.Name)
		if inferred == "" {
			inferred = keys[task.TaskItemID]
		}
		inferredKeys[task.TaskItemID] = inferred
		distinctInferred[inferred] = struct{}{}
	}
	if len(distinctInferred) >= 2 {
		return inferredKeys
	}
	return keys
}

func isCoarseContextKey(key string) bool {
	switch strings.ToLower(strings.TrimSpace(key)) {
	case "", "general", "high-logic", "high_logic", "memory", "review":
		return true
	default:
		return false
	}
}

func inferSubjectContextKeyFromTaskName(name string) string {
	text := strings.ToLower(strings.TrimSpace(name))
	if text == "" {
		return ""
	}

	subjectKeywordGroups := []struct {
		keywords []string
		groupKey string
	}{
		{
			keywords: []string{
				"概率", "随机事件", "随机变量", "条件概率", "全概率", "贝叶斯",
				"分布", "大数定律", "中心极限定理", "参数估计", "期望", "方差", "协方差", "相关系数",
			},
			groupKey: "subject:probability",
		},
		{
			keywords: []string{
				"数制", "码制", "逻辑代数", "逻辑函数", "卡诺图", "译码器", "编码器",
				"数据选择器", "触发器", "时序电路", "状态图", "状态化简", "计数器", "寄存器", "数电",
			},
			groupKey: "subject:digital_logic",
		},
		{
			keywords: []string{
				"命题逻辑", "谓词逻辑", "量词", "等值演算", "集合", "关系", "函数",
				"图论", "欧拉回路", "哈密顿", "生成树", "离散", "组合数学", "容斥", "递推",
			},
			groupKey: "subject:discrete_math",
		},
	}
	for _, group := range subjectKeywordGroups {
		for _, keyword := range group.keywords {
			if strings.Contains(text, keyword) {
				return group.groupKey
			}
		}
	}
	return ""
}

func composeDayKey(week, day int) string {
	return fmt.Sprintf("%d-%d", week, day)
}

func sectionSpan(from, to int) int {
	return to - from + 1
}

func isValidDay(day int) bool {
	return day >= 1 && day <= 7
}

func isValidSection(from, to int) bool {
	if from < 1 || to > 12 {
		return false
	}
	return from <= to
}

func slotOverlapsAny(candidate RefineSlotCandidate, selected []RefineSlotCandidate) bool {
	for _, current := range selected {
		if current.Week != candidate.Week || current.DayOfWeek != candidate.DayOfWeek {
			continue
		}
		if current.SectionFrom <= candidate.SectionTo && candidate.SectionFrom <= current.SectionTo {
			return true
		}
	}
	return false
}
