package service

import (
	"context"
	"errors"
	"log"
	"sort"
	"strings"
	"time"

	"github.com/LoveLosita/smartflow/backend/conv"
	"github.com/LoveLosita/smartflow/backend/dao"
	"github.com/LoveLosita/smartflow/backend/logic"
	"github.com/LoveLosita/smartflow/backend/model"
	"github.com/LoveLosita/smartflow/backend/respond"
	"github.com/go-redis/redis/v8"
)

type ScheduleService struct {
	scheduleDAO  *dao.ScheduleDAO
	userDAO      *dao.UserDAO
	taskClassDAO *dao.TaskClassDAO
	repoManager  *dao.RepoManager // 统一管理多个 DAO 的事务
	cacheDAO     *dao.CacheDAO    // 需要在 ScheduleService 中使用缓存
}

func NewScheduleService(scheduleDAO *dao.ScheduleDAO, userDAO *dao.UserDAO, taskClassDAO *dao.TaskClassDAO, repoManager *dao.RepoManager, cacheDAO *dao.CacheDAO) *ScheduleService {
	return &ScheduleService{
		scheduleDAO:  scheduleDAO,
		userDAO:      userDAO,
		taskClassDAO: taskClassDAO,
		repoManager:  repoManager,
		cacheDAO:     cacheDAO,
	}
}

func (ss *ScheduleService) GetUserTodaySchedule(ctx context.Context, userID int) ([]model.UserTodaySchedule, error) {
	//1.先检查用户id是否存在(考虑移除)
	/*_, err := ss.userDAO.GetUserByID(userID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, respond.WrongUserID
		}
		return nil, err
	}*/
	//1.先尝试从缓存获取数据
	cachedResp, err := ss.cacheDAO.GetUserTodayScheduleFromCache(ctx, userID)
	if err == nil {
		// 缓存命中，直接返回
		return cachedResp, nil
	}
	// 如果是 redis.Nil 错误，说明缓存未命中，我们继续查库
	if !errors.Is(err, redis.Nil) {
		return nil, err
	}
	//2.获取当前日期
	/*curTime := time.Now().Format("2006-01-02")*/
	curTime := "2026-03-02" //测试数据
	week, dayOfWeek, err := conv.RealDateToRelativeDate(curTime)
	if err != nil {
		return nil, err
	}
	//3.查询用户当天的日程安排
	schedules, err := ss.scheduleDAO.GetUserTodaySchedule(ctx, userID, week, dayOfWeek) //测试数据
	if err != nil {
		return nil, err
	}
	//4.转换为前端需要的格式
	todaySchedules := conv.SchedulesToUserTodaySchedule(schedules)
	//5.将查询结果存入缓存，设置过期时间为当天结束
	err = ss.cacheDAO.SetUserTodayScheduleToCache(ctx, userID, todaySchedules)
	return todaySchedules, nil
}

func (ss *ScheduleService) GetUserWeeklySchedule(ctx context.Context, userID, week int) (*model.UserWeekSchedule, error) {
	//1.先检查 week 参数是否合法
	if week < 0 || week > 25 {
		return nil, respond.WeekOutOfRange
	}
	//2.先看看缓存里有没有数据（如果有的话直接返回，没有的话继续查库）
	cachedResp, err := ss.cacheDAO.GetUserWeeklyScheduleFromCache(ctx, userID, week)
	if err == nil {
		// 缓存命中，直接返回
		return cachedResp, nil
	}
	// 如果是 redis.Nil 错误，说明缓存未命中，我们继续查库
	if !errors.Is(err, redis.Nil) {
		return nil, err
	}
	//3.查询用户每周的日程安排
	//如果没有传入 week 参数，则默认查询当前周的日程安排
	if week == 0 {
		curTime := time.Now().Format("2006-01-02")
		var err error
		week, _, err = conv.RealDateToRelativeDate(curTime)
		if err != nil {
			return nil, err
		}
	}
	schedules, err := ss.scheduleDAO.GetUserWeeklySchedule(ctx, userID, week)
	if err != nil {
		return nil, err
	}
	//3.转换为前端需要的格式
	weeklySchedule := conv.SchedulesToUserWeeklySchedule(schedules)
	weeklySchedule.Week = week
	//4.将查询结果存入缓存，设置过期时间为一周（或者根据实际情况调整）
	err = ss.cacheDAO.SetUserWeeklyScheduleToCache(ctx, userID, weeklySchedule)
	return weeklySchedule, nil
}

func (ss *ScheduleService) DeleteScheduleEvent(ctx context.Context, requests []model.UserDeleteScheduleEvent, userID int) error {
	err := ss.repoManager.Transaction(ctx, func(txM *dao.RepoManager) error {
		for _, req := range requests {
			//1.如果要删课程和嵌入的事件
			if req.DeleteEmbeddedTask && req.DeleteCourse {
				//通过schedule表的embedded_task_id字段找到对应的task_id
				taskID, err := txM.Schedule.GetScheduleEmbeddedTaskID(ctx, req.ID)
				if err != nil {
					return err
				}
				//再将task_items表中对应的embedded_time字段设置为null
				if taskID != 0 {
					err = txM.TaskClass.DeleteTaskClassItemEmbeddedTime(ctx, taskID)
					if err != nil {
						return err
					}
				}
				//再删除课程事件和嵌入的事件（通过级联删除实现）
				err = txM.Schedule.DeleteScheduleEventAndSchedule(ctx, req.ID, userID)
				if err != nil {
					return err
				}
				continue
			}
			//2.只删课程/事件
			if req.DeleteCourse {
				//2.1.检查课程是否有嵌入的任务事件
				exists, err := txM.Schedule.IfScheduleEventIDExists(ctx, req.ID)
				if err != nil {
					return err
				}
				if !exists {
					return respond.WrongScheduleEventID
				}
				embeddedTaskID, err := txM.Schedule.GetScheduleEmbeddedTaskID(ctx, req.ID)
				if err != nil {
					return err
				}
				//2.2.如果有，则需另外为其创建新的scheduleEvent（type=task）
				//课程事件先删除后再创建任务事件
				if embeddedTaskID != 0 {
					//2.2.1.先通过id取出taskClassItem详情
					taskClassItem, err := txM.TaskClass.GetTaskClassItemByID(ctx, embeddedTaskID)
					if err != nil {
						return err
					}
					//下方开启事务，删除课程事件并创建新的任务事件
					//2.2.2.删除课程事件
					txErr := txM.Schedule.DeleteScheduleEventAndSchedule(ctx, req.ID, userID)
					if txErr != nil {
						return txErr
					}
					//2.2.3.再复用代码创建新的scheduleEvent，下方代码改编自AddTaskClassItemIntoSchedule函数
					//直接构造Schedule模型
					sections := make([]int, 0, taskClassItem.EmbeddedTime.SectionTo-taskClassItem.EmbeddedTime.SectionFrom+1)
					// 这里的 req 主要是为了传递 Week 和 DayOfWeek，其他字段不需要了
					schedules, scheduleEvent, err := conv.UserInsertTaskItemRequestToModel(
						&model.UserInsertTaskClassItemToScheduleRequest{
							Week:      taskClassItem.EmbeddedTime.Week,
							DayOfWeek: taskClassItem.EmbeddedTime.DayOfWeek},
						taskClassItem, nil, userID, taskClassItem.EmbeddedTime.SectionFrom, taskClassItem.EmbeddedTime.SectionTo)
					if err != nil {
						return err
					}
					//将节次区间转换为节次切片，方便后续检查冲突
					for section := taskClassItem.EmbeddedTime.SectionFrom; section <= taskClassItem.EmbeddedTime.SectionTo; section++ {
						sections = append(sections, section)
					}
					//单用户不存在删除时这个格子被占用的情况，所以不检查冲突了
					/*//4.1 统一检查冲突（避免逐条查库）
					conflict, err := ss.scheduleDAO.HasUserScheduleConflict(ctx, userID, req.Week, req.DayOfWeek, sections)
					if err != nil {
						return err
					}
					if conflict {
						return respond.ScheduleConflict
					}*/
					// 5. 写入数据库（通过 RepoManager 统一管理事务）
					// 这里的 sv.daoManager 是你在初始化 Service 时注入的全局 RepoManager 实例
					// 5.1 使用事务中的 ScheduleRepo 插入 Event
					eventID, txErr := txM.Schedule.AddScheduleEvent(scheduleEvent)
					if txErr != nil {
						return txErr // 触发回滚
					}
					// 5.2 关联 ID（纯内存操作，无需 tx）
					for i := range schedules {
						schedules[i].EventID = eventID
					}
					// 5.3 使用事务中的 ScheduleRepo 批量插入原子槽位
					if _, txErr = txM.Schedule.AddSchedules(schedules); txErr != nil {
						return txErr // 触发回滚
					}
					// 5.4 使用事务中的 TaskRepo 更新任务状态
					if txErr = txM.TaskClass.UpdateTaskClassItemEmbeddedTime(ctx, embeddedTaskID, taskClassItem.EmbeddedTime); txErr != nil {
						return txErr // 触发回滚
					}
					continue
				}
				//2.3.如果没有嵌入的事件，就直接删除课程事件
				err = txM.Schedule.DeleteScheduleEventAndSchedule(ctx, req.ID, userID)
				if err != nil {
					return err
				}
				//先通过rel_id找到对应的task_id
				taskID, txErr := txM.Schedule.GetRelIDByScheduleEventID(ctx, req.ID)
				if txErr != nil {
					return err
				}
				//2.4.如果是任务块，转而去清除task_items表中的嵌入时间
				if taskID != 0 {
					//再将task_items表中对应的embedded_time字段设置为null
					txErr = txM.TaskClass.DeleteTaskClassItemEmbeddedTime(ctx, taskID)
					if txErr != nil {
						return txErr
					}
				}
				continue
			}
			//3.只删嵌入的事件
			if req.DeleteEmbeddedTask {
				//下面先设置schedule表的embedded_task_id字段为null，再设置task_items表的embedded_time字段为null，实现删除嵌入事件的效果
				//3.1.先将schedule表的embedded_task_id字段设置为null
				taskID, txErr := txM.Schedule.SetScheduleEmbeddedTaskIDToNull(ctx, req.ID)
				if txErr != nil {
					return txErr
				}
				//3.2.再将task_items表的embedded_time字段设置为null
				txErr = txM.TaskClass.DeleteTaskClassItemEmbeddedTime(ctx, taskID)
				if txErr != nil {
					return txErr
				}
				continue
			}
		}
		return nil
	})
	if err != nil {
		return err
	}
	return nil
}

func (ss *ScheduleService) GetUserRecentCompletedSchedules(ctx context.Context, userID, index, limit int) (*model.UserRecentCompletedScheduleResponse, error) {
	//1.先查缓存
	cachedResp, err := ss.cacheDAO.GetUserRecentCompletedSchedulesFromCache(ctx, userID, index, limit)
	if err == nil {
		// 缓存命中，直接返回
		return cachedResp, nil
	}
	// 如果是 redis.Nil 错误，说明缓存未命中，我们继续查库
	if !errors.Is(err, redis.Nil) {
		return nil, err
	}
	//2.查询用户最近完成的日程安排
	//获取现在的时间
	/*nowTime := time.Now()*/
	nowTime := time.Date(2026, 6, 30, 12, 0, 0, 0, time.Local) //测试数据
	schedules, err := ss.scheduleDAO.GetUserRecentCompletedSchedules(ctx, nowTime, userID, index, limit)
	if err != nil {
		return nil, err
	}
	//3.转换为前端需要的格式
	result := conv.SchedulesToRecentCompletedSchedules(schedules)
	//4.将查询结果存入缓存，设置过期时间为30分钟（根据实际情况调整）
	err = ss.cacheDAO.SetUserRecentCompletedSchedulesToCache(ctx, userID, index, limit, result)
	if err != nil {
		return nil, err
	}
	return result, nil
}

func (ss *ScheduleService) GetUserOngoingSchedule(ctx context.Context, userID int) (*model.OngoingSchedule, error) {
	//1.先查缓存
	cachedResp, err := ss.cacheDAO.GetUserOngoingScheduleFromCache(ctx, userID)
	if err == nil && cachedResp == nil {
		// 之前缓存过没有正在进行的日程，直接返回 nil
		return nil, respond.NoOngoingOrUpcomingSchedule
	}
	if err == nil {
		// 缓存命中，直接返回
		return cachedResp, nil
	}
	// 如果是 redis.Nil 错误，说明缓存未命中，我们继续查库
	if !errors.Is(err, redis.Nil) {
		return nil, err
	}
	//2.查询用户正在进行的日程安排
	/*nowTime := time.Now()*/
	nowTime := time.Date(2026, 6, 30, 18, 50, 0, 0, time.Local) //测试数据
	schedules, err := ss.scheduleDAO.GetUserOngoingSchedule(ctx, userID, nowTime)
	if err != nil {
		return nil, err
	}
	//3.转换为前端需要的格式
	result := conv.SchedulesToUserOngoingSchedule(schedules)
	if result != nil {
		if result.StartTime.After(nowTime) {
			result.TimeStatus = "upcoming"
		} else {
			result.TimeStatus = "ongoing"
		}
	}
	//4.将查询结果存入缓存，设置过期时间直到此任务结束（根据实际情况调整）
	err = ss.cacheDAO.SetUserOngoingScheduleToCache(ctx, userID, result)
	if err != nil {
		return nil, err
	}
	if result == nil {
		// 没有正在进行或即将开始的日程，返回特定错误
		return nil, respond.NoOngoingOrUpcomingSchedule
	}
	return result, nil
}

func (ss *ScheduleService) RevocateUserTaskClassItem(ctx context.Context, userID, eventID int) error {
	//1.先查库，看看这个event是任务事件还是课程事件，以及判断它是否属于用户
	eventType, err := ss.scheduleDAO.GetScheduleTypeByEventID(ctx, eventID, userID)
	if err != nil {
		return err
	}
	//2.根据查询结果进行不同的撤销操作
	if eventType == "course" {
		//下面开启事务，撤销嵌入事件
		err := ss.repoManager.Transaction(ctx, func(txM *dao.RepoManager) error {
			//下面先设置schedule表的embedded_task_id字段为null，再设置task_items表的embedded_time字段为null，实现删除嵌入事件的效果
			//3.1.先将schedule表的embedded_task_id字段设置为null
			taskID, txErr := txM.Schedule.SetScheduleEmbeddedTaskIDToNull(ctx, eventID)
			if txErr != nil {
				return txErr
			}
			//3.2.再将task_items表的embedded_time字段设置为null
			txErr = txM.TaskClass.DeleteTaskClassItemEmbeddedTime(ctx, taskID)
			if txErr != nil {
				return txErr
			}
			//3.3.最后设置task_items表的status字段为已撤销
			txErr = txM.Schedule.RevocateSchedulesByEventID(ctx, eventID)
			if txErr != nil {
				return txErr
			}
			return nil
		})
		if err != nil {
			return err
		}
	} else if eventType == "task" {
		//下面开启事务，撤销任务事件
		err := ss.repoManager.Transaction(ctx, func(txM *dao.RepoManager) error {
			//先通过rel_id找到对应的task_id
			taskID, txErr := txM.Schedule.GetRelIDByScheduleEventID(ctx, eventID)
			if txErr != nil {
				return err
			}
			//再将task_items表中对应的embedded_time字段设置为null
			txErr = txM.TaskClass.DeleteTaskClassItemEmbeddedTime(ctx, taskID)
			if txErr != nil {
				return txErr
			}
			//最后将其从日程表中删除（通过级联删除实现）
			err = txM.Schedule.DeleteScheduleEventAndSchedule(ctx, eventID, userID)
			if err != nil {
				return err
			}
			return nil
		})
		if err != nil {
			return err
		}
	} else {
		log.Println("ScheduleService.RevocateUserTaskClassItem: eventType is neither embedded_task nor task, something must be wrong")
	}
	return nil
}

func (ss *ScheduleService) SmartPlanning(ctx context.Context, userID, taskClassID int) ([]model.UserWeekSchedule, error) {
	//1.通过任务类id获取任务类详情
	taskClass, err := ss.taskClassDAO.GetCompleteTaskClassByID(ctx, taskClassID, userID)
	if err != nil {
		return nil, err
	}
	//2.校验任务类的参数是否合法
	if taskClass == nil {
		return nil, respond.WrongTaskClassID
	}
	if *taskClass.Mode != "auto" {
		return nil, respond.TaskClassModeNotAuto
	}
	//3.获取任务类安排的时间范围内的全部周数信息(左右边界不足一周的情况也要算作一周)
	schedules, err := ss.scheduleDAO.GetUserSchedulesByTimeRange(ctx, userID, conv.CalculateFirstDayOfWeek(*taskClass.StartDate), conv.CalculateLastDayOfWeek(*taskClass.EndDate))
	if err != nil {
		return nil, err
	}
	//4.将多个周的信息传入智能排课算法，获取推荐的时间安排（周+周内的天+节次）
	result, err := logic.SmartPlanningMainLogic(schedules, taskClass)
	if err != nil {
		return nil, err
	}
	//5.将推荐的时间安排转换为前端需要的格式返回
	return result, nil
}

// SmartPlanningRaw 执行粗排算法并同时返回展示结构和已分配的任务项。
//
// 职责边界：
//  1. 与 SmartPlanning 共享完全相同的前置校验和粗排逻辑；
//  2. 额外返回 allocatedItems（每项的 EmbeddedTime 已由算法回填），
//     供 Agent 排程链路直接转换为 BatchApplyPlans 请求，无需再让模型"二次分配"。
func (ss *ScheduleService) SmartPlanningRaw(ctx context.Context, userID, taskClassID int) ([]model.UserWeekSchedule, []model.TaskClassItem, error) {
	// 1. 获取任务类详情。
	taskClass, err := ss.taskClassDAO.GetCompleteTaskClassByID(ctx, taskClassID, userID)
	if err != nil {
		return nil, nil, err
	}
	if taskClass == nil {
		return nil, nil, respond.WrongTaskClassID
	}
	if *taskClass.Mode != "auto" {
		return nil, nil, respond.TaskClassModeNotAuto
	}

	// 2. 获取时间范围内的全部日程。
	schedules, err := ss.scheduleDAO.GetUserSchedulesByTimeRange(ctx, userID, conv.CalculateFirstDayOfWeek(*taskClass.StartDate), conv.CalculateLastDayOfWeek(*taskClass.EndDate))
	if err != nil {
		return nil, nil, err
	}

	// 3. 执行粗排算法，拿到已分配的 items（EmbeddedTime 已回填）。
	allocatedItems, err := logic.SmartPlanningRawItems(schedules, taskClass)
	if err != nil {
		return nil, nil, err
	}

	// 4. 同时生成展示结构，供 SSE 阶段推送给前端预览。
	displayResult := conv.PlanningResultToUserWeekSchedules(schedules, allocatedItems)
	return displayResult, allocatedItems, nil
}

// SmartPlanningMulti 执行“多任务类智能粗排”，仅返回前端展示结构。
//
// 职责边界：
// 1. 负责把多任务类请求收口到统一粗排流程；
// 2. 负责返回展示结构；
// 3. 不返回底层分配细节（由 SmartPlanningMultiRaw 提供）。
func (ss *ScheduleService) SmartPlanningMulti(ctx context.Context, userID int, taskClassIDs []int) ([]model.UserWeekSchedule, error) {
	displayResult, _, err := ss.SmartPlanningMultiRaw(ctx, userID, taskClassIDs)
	if err != nil {
		return nil, err
	}
	return displayResult, nil
}

// SmartPlanningMultiRaw 执行“多任务类智能粗排”，同时返回展示结构和已分配任务项。
//
// 职责边界：
// 1. 负责多任务类请求的完整前置处理（归一化/校验/排序/时间窗收敛）；
// 2. 负责调用多任务类粗排主逻辑（共享资源池）；
// 3. 只计算建议，不负责落库。
func (ss *ScheduleService) SmartPlanningMultiRaw(ctx context.Context, userID int, taskClassIDs []int) ([]model.UserWeekSchedule, []model.TaskClassItem, error) {
	// 1. 输入归一化。
	normalizedIDs := normalizeTaskClassIDsForMultiPlanning(taskClassIDs)
	if len(normalizedIDs) == 0 {
		return nil, nil, respond.WrongTaskClassID
	}

	// 2. 批量读取完整任务类（含 Items）。
	taskClasses, err := ss.taskClassDAO.GetCompleteTaskClassesByIDs(ctx, userID, normalizedIDs)
	if err != nil {
		return nil, nil, err
	}

	// 3. 校验任务类并计算全局时间窗。
	orderedTaskClasses, globalStartDate, globalEndDate, err := prepareTaskClassesForMultiPlanning(taskClasses, normalizedIDs)
	if err != nil {
		return nil, nil, err
	}

	// 4. 拉取全局时间窗内的既有日程底板。
	schedules, err := ss.scheduleDAO.GetUserSchedulesByTimeRange(
		ctx,
		userID,
		conv.CalculateFirstDayOfWeek(globalStartDate),
		conv.CalculateLastDayOfWeek(globalEndDate),
	)
	if err != nil {
		return nil, nil, err
	}

	// 5. 执行多任务类粗排（共享资源池 + 增量占位）。
	allocatedItems, err := logic.SmartPlanningRawItemsMulti(schedules, orderedTaskClasses)
	if err != nil {
		return nil, nil, err
	}

	// 6. 转换前端展示结构。
	displayResult := conv.PlanningResultToUserWeekSchedules(schedules, allocatedItems)
	return displayResult, allocatedItems, nil
}

// ResolvePlanningWindowByTaskClasses 解析“多任务类排程窗口”的相对周/天边界。
//
// 职责边界：
// 1. 只负责根据 task_class_ids 计算全局起止日期并转换成相对周/天；
// 2. 不执行粗排、不查询课表、不生成 HybridEntries；
// 3. 供 Agent 周级 Move 工具做硬边界校验，防止越界移动。
//
// 返回语义：
// 1. startWeek/startDay：允许排程的起点（含）；
// 2. endWeek/endDay：允许排程的终点（含）；
// 3. error：任何校验或日期转换失败都返回错误。
func (ss *ScheduleService) ResolvePlanningWindowByTaskClasses(ctx context.Context, userID int, taskClassIDs []int) (int, int, int, int, error) {
	// 1. 输入归一化：过滤非法值并去重。
	normalizedIDs := normalizeTaskClassIDsForMultiPlanning(taskClassIDs)
	if len(normalizedIDs) == 0 {
		return 0, 0, 0, 0, respond.WrongTaskClassID
	}

	// 2. 批量查询任务类并复用统一校验逻辑，拿到全局起止日期。
	taskClasses, err := ss.taskClassDAO.GetCompleteTaskClassesByIDs(ctx, userID, normalizedIDs)
	if err != nil {
		return 0, 0, 0, 0, err
	}
	_, globalStartDate, globalEndDate, err := prepareTaskClassesForMultiPlanning(taskClasses, normalizedIDs)
	if err != nil {
		return 0, 0, 0, 0, err
	}

	// 3. 把绝对日期转换为“相对周/天”。
	// 3.1 这里统一复用 conv.RealDateToRelativeDate，确保和现有排程口径一致；
	// 3.2 若日期超出学期配置范围，直接返回错误，避免错误边界进入工具层。
	startWeek, startDay, err := conv.RealDateToRelativeDate(globalStartDate.Format(conv.DateFormat))
	if err != nil {
		return 0, 0, 0, 0, err
	}
	endWeek, endDay, err := conv.RealDateToRelativeDate(globalEndDate.Format(conv.DateFormat))
	if err != nil {
		return 0, 0, 0, 0, err
	}
	if endWeek < startWeek || (endWeek == startWeek && endDay < startDay) {
		return 0, 0, 0, 0, respond.InvalidDateRange
	}
	return startWeek, startDay, endWeek, endDay, nil
}

// normalizeTaskClassIDsForMultiPlanning 归一化 task_class_ids（过滤非法值、去重并保序）。
func normalizeTaskClassIDsForMultiPlanning(ids []int) []int {
	if len(ids) == 0 {
		return []int{}
	}
	normalized := make([]int, 0, len(ids))
	seen := make(map[int]struct{}, len(ids))
	for _, id := range ids {
		if id <= 0 {
			continue
		}
		if _, exists := seen[id]; exists {
			continue
		}
		seen[id] = struct{}{}
		normalized = append(normalized, id)
	}
	return normalized
}

// prepareTaskClassesForMultiPlanning 把 DAO 结果转成可直接粗排的数据集。
//
// 职责边界：
// 1. 校验每个任务类可参与自动排程；
// 2. 计算全局时间窗（最早开始 ~ 最晚结束）；
// 3. 执行多任务类排序策略。
func prepareTaskClassesForMultiPlanning(taskClasses []model.TaskClass, orderedIDs []int) ([]*model.TaskClass, time.Time, time.Time, error) {
	if len(orderedIDs) == 0 {
		return nil, time.Time{}, time.Time{}, respond.WrongTaskClassID
	}

	classByID := make(map[int]*model.TaskClass, len(taskClasses))
	for i := range taskClasses {
		tc := &taskClasses[i]
		classByID[tc.ID] = tc
	}

	ordered := make([]*model.TaskClass, 0, len(orderedIDs))
	var globalStart time.Time
	var globalEnd time.Time
	for idx, id := range orderedIDs {
		taskClass, exists := classByID[id]
		if !exists || taskClass == nil {
			return nil, time.Time{}, time.Time{}, respond.WrongTaskClassID
		}
		if taskClass.Mode == nil || *taskClass.Mode != "auto" {
			return nil, time.Time{}, time.Time{}, respond.TaskClassModeNotAuto
		}
		if taskClass.StartDate == nil || taskClass.EndDate == nil {
			return nil, time.Time{}, time.Time{}, respond.InvalidDateRange
		}
		start := *taskClass.StartDate
		end := *taskClass.EndDate
		if end.Before(start) {
			return nil, time.Time{}, time.Time{}, respond.InvalidDateRange
		}
		if idx == 0 || start.Before(globalStart) {
			globalStart = start
		}
		if idx == 0 || end.After(globalEnd) {
			globalEnd = end
		}
		ordered = append(ordered, taskClass)
	}

	sortTaskClassesForMultiPlanning(ordered, orderedIDs)
	return ordered, globalStart, globalEnd, nil
}

// sortTaskClassesForMultiPlanning 执行稳定排序：
// 1. end_date 早优先；
// 2. rapid 优先于 steady；
// 3. 输入顺序兜底。
func sortTaskClassesForMultiPlanning(taskClasses []*model.TaskClass, inputOrder []int) {
	if len(taskClasses) <= 1 {
		return
	}
	orderIndex := make(map[int]int, len(inputOrder))
	for idx, id := range inputOrder {
		orderIndex[id] = idx
	}

	sort.SliceStable(taskClasses, func(i, j int) bool {
		left := taskClasses[i]
		right := taskClasses[j]
		if left == nil || right == nil {
			return left != nil
		}
		if left.EndDate != nil && right.EndDate != nil && !left.EndDate.Equal(*right.EndDate) {
			return left.EndDate.Before(*right.EndDate)
		}
		leftRapid := left.Strategy != nil && *left.Strategy == "rapid"
		rightRapid := right.Strategy != nil && *right.Strategy == "rapid"
		if leftRapid != rightRapid {
			return leftRapid
		}
		leftOrder, leftOK := orderIndex[left.ID]
		rightOrder, rightOK := orderIndex[right.ID]
		if leftOK && rightOK && leftOrder != rightOrder {
			return leftOrder < rightOrder
		}
		return left.ID < right.ID
	})
}

// HybridScheduleWithPlan 构建“单任务类”混合日程（existing + suggested）。
func (ss *ScheduleService) HybridScheduleWithPlan(
	ctx context.Context, userID, taskClassID int,
) ([]model.HybridScheduleEntry, []model.TaskClassItem, error) {
	// 1. 校验并读取任务类。
	taskClass, err := ss.taskClassDAO.GetCompleteTaskClassByID(ctx, taskClassID, userID)
	if err != nil {
		return nil, nil, err
	}
	if taskClass == nil {
		return nil, nil, respond.WrongTaskClassID
	}
	if taskClass.Mode == nil || *taskClass.Mode != "auto" {
		return nil, nil, respond.TaskClassModeNotAuto
	}
	if taskClass.StartDate == nil || taskClass.EndDate == nil {
		return nil, nil, respond.InvalidDateRange
	}

	// 2. 拉取时间窗内既有日程。
	schedules, err := ss.scheduleDAO.GetUserSchedulesByTimeRange(
		ctx, userID,
		conv.CalculateFirstDayOfWeek(*taskClass.StartDate),
		conv.CalculateLastDayOfWeek(*taskClass.EndDate),
	)
	if err != nil {
		return nil, nil, err
	}

	// 3. 执行粗排。
	allocatedItems, err := logic.SmartPlanningRawItems(schedules, taskClass)
	if err != nil {
		return nil, nil, err
	}

	// 4. 统一合并。
	entries := buildHybridEntriesFromSchedulesAndAllocated(schedules, allocatedItems)
	return entries, allocatedItems, nil
}

// HybridScheduleWithPlanMulti 构建“多任务类”混合日程（existing + suggested）。
func (ss *ScheduleService) HybridScheduleWithPlanMulti(
	ctx context.Context,
	userID int,
	taskClassIDs []int,
) ([]model.HybridScheduleEntry, []model.TaskClassItem, error) {
	// 1. 归一化任务类 ID。
	normalizedIDs := normalizeTaskClassIDsForMultiPlanning(taskClassIDs)
	if len(normalizedIDs) == 0 {
		return nil, nil, respond.WrongTaskClassID
	}

	// 2. 拉取任务类并做校验/排序。
	taskClasses, err := ss.taskClassDAO.GetCompleteTaskClassesByIDs(ctx, userID, normalizedIDs)
	if err != nil {
		return nil, nil, err
	}
	orderedTaskClasses, globalStartDate, globalEndDate, err := prepareTaskClassesForMultiPlanning(taskClasses, normalizedIDs)
	if err != nil {
		return nil, nil, err
	}

	// 3. 拉取全局时间窗内既有日程。
	schedules, err := ss.scheduleDAO.GetUserSchedulesByTimeRange(
		ctx,
		userID,
		conv.CalculateFirstDayOfWeek(globalStartDate),
		conv.CalculateLastDayOfWeek(globalEndDate),
	)
	if err != nil {
		return nil, nil, err
	}

	// 4. 多任务类粗排。
	allocatedItems, err := logic.SmartPlanningRawItemsMulti(schedules, orderedTaskClasses)
	if err != nil {
		return nil, nil, err
	}

	// 5. 统一合并。
	entries := buildHybridEntriesFromSchedulesAndAllocated(schedules, allocatedItems)
	return entries, allocatedItems, nil
}

// buildHybridEntriesFromSchedulesAndAllocated 合并 existing/suggested 条目。
//
// 说明：
// 1. existing 按“事件 + 天 + 可嵌入语义 + 阻塞语义”分组，再按连续节次切块；
// 2. suggested 直接根据 allocatedItems 生成；
// 3. 仅做内存组装，不做数据库操作。
func buildHybridEntriesFromSchedulesAndAllocated(
	schedules []model.Schedule,
	allocatedItems []model.TaskClassItem,
) []model.HybridScheduleEntry {
	entries := make([]model.HybridScheduleEntry, 0, len(schedules)/2+len(allocatedItems))

	type eventGroupKey struct {
		EventID           int
		Week              int
		DayOfWeek         int
		CanBeEmbedded     bool
		BlockForSuggested bool
	}
	type eventGroup struct {
		Key      eventGroupKey
		Name     string
		Type     string
		Sections []int
	}
	groupMap := make(map[eventGroupKey]*eventGroup)

	// 1. 先处理 existing。
	for _, s := range schedules {
		name := "未知"
		typ := "course"
		canBeEmbedded := false
		if s.Event != nil {
			name = s.Event.Name
			typ = s.Event.Type
			canBeEmbedded = s.Event.CanBeEmbedded
		}

		// 1.1 阻塞语义：
		// 1.1.1 task 默认阻塞；
		// 1.1.2 course 且不可嵌入时阻塞；
		// 1.1.3 course 且可嵌入时，若当前原子格未被 embedded_task 占用，则不阻塞。
		blockForSuggested := true
		if typ == "course" && canBeEmbedded && s.EmbeddedTaskID == nil {
			blockForSuggested = false
		}

		key := eventGroupKey{
			EventID:           s.EventID,
			Week:              s.Week,
			DayOfWeek:         s.DayOfWeek,
			CanBeEmbedded:     canBeEmbedded,
			BlockForSuggested: blockForSuggested,
		}
		group, ok := groupMap[key]
		if !ok {
			group = &eventGroup{
				Key:  key,
				Name: name,
				Type: typ,
			}
			groupMap[key] = group
		}
		group.Sections = append(group.Sections, s.Section)
	}

	for _, group := range groupMap {
		if len(group.Sections) == 0 {
			continue
		}
		sort.Ints(group.Sections)

		runStart := group.Sections[0]
		prev := group.Sections[0]
		flushRun := func(from, to int) {
			entries = append(entries, model.HybridScheduleEntry{
				Week:              group.Key.Week,
				DayOfWeek:         group.Key.DayOfWeek,
				SectionFrom:       from,
				SectionTo:         to,
				Name:              group.Name,
				Type:              group.Type,
				Status:            "existing",
				EventID:           group.Key.EventID,
				CanBeEmbedded:     group.Key.CanBeEmbedded,
				BlockForSuggested: group.Key.BlockForSuggested,
			})
		}
		for i := 1; i < len(group.Sections); i++ {
			cur := group.Sections[i]
			if cur == prev+1 {
				prev = cur
				continue
			}
			flushRun(runStart, prev)
			runStart = cur
			prev = cur
		}
		flushRun(runStart, prev)
	}

	// 2. 再处理 suggested。
	for _, item := range allocatedItems {
		if item.EmbeddedTime == nil {
			continue
		}
		name := "未命名任务"
		if item.Content != nil && strings.TrimSpace(*item.Content) != "" {
			name = strings.TrimSpace(*item.Content)
		}
		entries = append(entries, model.HybridScheduleEntry{
			Week:              item.EmbeddedTime.Week,
			DayOfWeek:         item.EmbeddedTime.DayOfWeek,
			SectionFrom:       item.EmbeddedTime.SectionFrom,
			SectionTo:         item.EmbeddedTime.SectionTo,
			Name:              name,
			Type:              "task",
			Status:            "suggested",
			TaskItemID:        item.ID,
			BlockForSuggested: true,
		})
	}

	return entries
}
