package newagentconv

import (
	"sort"

	"github.com/LoveLosita/smartflow/backend/model"
	schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
)

// WindowDay 表示排课窗口中的一天（相对周 + 周几）。
type WindowDay struct {
	Week      int
	DayOfWeek int
}

// LoadScheduleState 将数据库层的 schedules + taskClasses 聚合为 newAgent 工具层可直接操作的 ScheduleState。
//
// 职责边界：
// 1. 只负责数据映射与状态归一，不做数据库读写；
// 2. 同时兼容三种“任务已落位”信号：event.rel_id、schedules.embedded_task_id、task_item.embedded_time；
// 3. 对嵌入课程任务优先判定为 existing，避免误挂回 pending。
func LoadScheduleState(
	schedules []model.Schedule,
	taskClasses []model.TaskClass,
	extraItemCategories map[int]string,
	windowDays []WindowDay,
) *schedule.ScheduleState {
	state := &schedule.ScheduleState{
		Window: schedule.ScheduleWindow{
			TotalDays:  len(windowDays),
			DayMapping: make([]schedule.DayMapping, len(windowDays)),
		},
		Tasks: make([]schedule.ScheduleTask, 0),
	}

	// 1. 构建 day_index 与 (week, day_of_week) 的双向转换基础索引。
	dayLookup := make(map[[2]int]int, len(windowDays))
	for i, wd := range windowDays {
		dayIndex := i + 1
		state.Window.DayMapping[i] = schedule.DayMapping{
			DayIndex:  dayIndex,
			Week:      wd.Week,
			DayOfWeek: wd.DayOfWeek,
		}
		dayLookup[[2]int{wd.Week, wd.DayOfWeek}] = dayIndex
	}

	// 2. 构建 task_item -> 分类名映射。
	// 2.1 先放 extraItemCategories（低优先级，兜底）；
	// 2.2 再用 taskClasses 覆盖（高优先级，确保本轮排课分类准确）。
	itemCategoryLookup := make(map[int]string)
	for id, name := range extraItemCategories {
		itemCategoryLookup[id] = name
	}
	for _, tc := range taskClasses {
		catName := "任务"
		if tc.Name != nil && *tc.Name != "" {
			catName = *tc.Name
		}
		for _, item := range tc.Items {
			itemCategoryLookup[item.ID] = catName
		}
	}

	// 3. 先把 schedules 聚合成 event 任务（existing）。
	type slotGroup struct {
		week      int
		dayOfWeek int
		sections  []int
	}
	eventSlotMap := make(map[int][]slotGroup) // eventID -> 多天多段槽位
	eventInfo := make(map[int]*model.ScheduleEvent)

	for i := range schedules {
		s := &schedules[i]
		if s.Event == nil {
			continue
		}
		if _, exists := eventInfo[s.EventID]; !exists {
			eventInfo[s.EventID] = s.Event
		}

		groups := eventSlotMap[s.EventID]
		found := false
		for gi := range groups {
			if groups[gi].week == s.Week && groups[gi].dayOfWeek == s.DayOfWeek {
				groups[gi].sections = append(groups[gi].sections, s.Section)
				found = true
				break
			}
		}
		if !found {
			groups = append(groups, slotGroup{
				week:      s.Week,
				dayOfWeek: s.DayOfWeek,
				sections:  []int{s.Section},
			})
		}
		eventSlotMap[s.EventID] = groups
	}

	nextStateID := 1
	eventStateIDs := make(map[int]int) // eventID -> stateID
	for eventID, groups := range eventSlotMap {
		event := eventInfo[eventID]
		if event == nil {
			continue
		}

		category := "课程"
		if event.Type == "task" {
			category = "任务"
			if event.RelID != nil {
				if cat, ok := itemCategoryLookup[*event.RelID]; ok && cat != "" {
					category = cat
				}
			}
		}

		locked := event.Type == "course" && !event.CanBeEmbedded
		var slots []schedule.TaskSlot
		for _, g := range groups {
			if len(g.sections) == 0 {
				continue
			}
			sort.Ints(g.sections)
			start, end := g.sections[0], g.sections[0]
			for _, sec := range g.sections[1:] {
				if sec == end+1 {
					end = sec
					continue
				}
				if day, ok := dayLookup[[2]int{g.week, g.dayOfWeek}]; ok {
					slots = append(slots, schedule.TaskSlot{Day: day, SlotStart: start, SlotEnd: end})
				}
				start, end = sec, sec
			}
			if day, ok := dayLookup[[2]int{g.week, g.dayOfWeek}]; ok {
				slots = append(slots, schedule.TaskSlot{Day: day, SlotStart: start, SlotEnd: end})
			}
		}
		sort.Slice(slots, func(i, j int) bool {
			if slots[i].Day != slots[j].Day {
				return slots[i].Day < slots[j].Day
			}
			return slots[i].SlotStart < slots[j].SlotStart
		})

		stateID := nextStateID
		state.Tasks = append(state.Tasks, schedule.ScheduleTask{
			StateID:   stateID,
			Source:    "event",
			SourceID:  eventID,
			Name:      event.Name,
			Category:  category,
			Status:    "existing",
			Locked:    locked,
			Slots:     slots,
			CanEmbed:  event.CanBeEmbedded,
			EventType: event.Type,
		})
		eventStateIDs[eventID] = stateID
		nextStateID++
	}

	// 4. 构建 task_item 占位索引（后续 pending 判定优先用这两个索引短路）。
	// 4.1 event.rel_id 占位：该 item 已有 task event；
	// 4.2 schedules.embedded_task_id 占位：该 item 已嵌入到课程槽位。
	itemIDToTaskEventStateID := make(map[int]int)
	for eventID, stateID := range eventStateIDs {
		event := eventInfo[eventID]
		if event == nil || event.Type != "task" || event.RelID == nil {
			continue
		}
		itemIDToTaskEventStateID[*event.RelID] = stateID
	}

	itemIDToEmbedHostStateID := make(map[int]int)
	for i := range schedules {
		s := &schedules[i]
		if s.EmbeddedTaskID == nil {
			continue
		}
		hostStateID, ok := eventStateIDs[s.EventID]
		if !ok {
			continue
		}
		itemIDToEmbedHostStateID[*s.EmbeddedTaskID] = hostStateID
	}

	// 5. 处理 task_items：
	// 5.1 先消化 existing（task event / 课程嵌入 / embedded_time）；
	// 5.2 剩余条目再按 status 判 pending。
	itemStateIDs := make(map[int]int) // task_item_id -> stateID
	for _, tc := range taskClasses {
		catName := "任务"
		if tc.Name != nil && *tc.Name != "" {
			catName = *tc.Name
		}
		defaultDuration := estimateTaskItemDuration(tc)
		pendingCount := 0

		for _, item := range tc.Items {
			if stateID, ok := itemIDToTaskEventStateID[item.ID]; ok {
				itemStateIDs[item.ID] = stateID
				continue
			}

			if hostStateID, ok := itemIDToEmbedHostStateID[item.ID]; ok {
				hostSlots := []schedule.TaskSlot(nil)
				if hostTask := state.TaskByStateID(hostStateID); hostTask != nil {
					hostSlots = cloneTaskSlots(hostTask.Slots)
				}
				stateID := nextStateID
				state.Tasks = append(state.Tasks, schedule.ScheduleTask{
					StateID:     stateID,
					Source:      "task_item",
					SourceID:    item.ID,
					Name:        taskItemName(item),
					Category:    catName,
					Status:      "existing",
					Slots:       hostSlots,
					CategoryID:  tc.ID,
					TaskClassID: tc.ID,
				})
				itemStateIDs[item.ID] = stateID
				nextStateID++
				continue
			}

			if slots, ok := slotsFromTargetTime(item.EmbeddedTime, dayLookup); ok {
				stateID := nextStateID
				state.Tasks = append(state.Tasks, schedule.ScheduleTask{
					StateID:     stateID,
					Source:      "task_item",
					SourceID:    item.ID,
					Name:        taskItemName(item),
					Category:    catName,
					Status:      "existing",
					Slots:       slots,
					CategoryID:  tc.ID,
					TaskClassID: tc.ID,
				})
				itemStateIDs[item.ID] = stateID
				nextStateID++
				continue
			}

			if !isTaskItemPending(item) {
				continue
			}

			stateID := nextStateID
			state.Tasks = append(state.Tasks, schedule.ScheduleTask{
				StateID:     stateID,
				Source:      "task_item",
				SourceID:    item.ID,
				Name:        taskItemName(item),
				Category:    catName,
				Status:      "pending",
				Duration:    defaultDuration,
				CategoryID:  tc.ID,
				TaskClassID: tc.ID,
			})
			itemStateIDs[item.ID] = stateID
			nextStateID++
			pendingCount++
		}

		// 仅当该任务类仍有 pending item 时，才把约束暴露给 LLM。
		if pendingCount > 0 {
			meta := schedule.TaskClassMeta{
				ID:   tc.ID,
				Name: catName,
			}
			if tc.Strategy != nil {
				meta.Strategy = *tc.Strategy
			}
			if tc.TotalSlots != nil {
				meta.TotalSlots = *tc.TotalSlots
			}
			if tc.AllowFillerCourse != nil {
				meta.AllowFillerCourse = *tc.AllowFillerCourse
			}
			if tc.ExcludedSlots != nil {
				meta.ExcludedSlots = []int(tc.ExcludedSlots)
			}
			if tc.StartDate != nil {
				meta.StartDate = tc.StartDate.Format("2006-01-02")
			}
			if tc.EndDate != nil {
				meta.EndDate = tc.EndDate.Format("2006-01-02")
			}
			state.TaskClasses = append(state.TaskClasses, meta)
		}
	}

	// 6. 统一回填嵌入关系：
	// 6.1 host 记录 EmbeddedBy；
	// 6.2 guest 记录 EmbedHost；
	// 6.3 guest 强制 existing + host slots，防止“嵌入任务残留 pending”。
	for i := range schedules {
		s := &schedules[i]
		if s.EmbeddedTaskID == nil {
			continue
		}
		hostStateID, ok := eventStateIDs[s.EventID]
		if !ok {
			continue
		}
		hostTask := state.TaskByStateID(hostStateID)
		itemID := *s.EmbeddedTaskID

		guestStateID, ok := itemStateIDs[itemID]
		if !ok {
			// 兜底：只在 schedules 层看到嵌入关系，taskClasses 不含该 item 时补建 guest。
			name := ""
			categoryID := 0
			taskClassID := 0
			if s.EmbeddedTask != nil {
				name = taskItemName(*s.EmbeddedTask)
				if s.EmbeddedTask.CategoryID != nil {
					categoryID = *s.EmbeddedTask.CategoryID
					taskClassID = *s.EmbeddedTask.CategoryID
				}
			}
			category := "任务"
			if cat, exists := itemCategoryLookup[itemID]; exists && cat != "" {
				category = cat
			}
			hostSlots := []schedule.TaskSlot(nil)
			if hostTask != nil {
				hostSlots = cloneTaskSlots(hostTask.Slots)
			}
			guestStateID = nextStateID
			state.Tasks = append(state.Tasks, schedule.ScheduleTask{
				StateID:     guestStateID,
				Source:      "task_item",
				SourceID:    itemID,
				Name:        name,
				Category:    category,
				Status:      "existing",
				Slots:       hostSlots,
				CategoryID:  categoryID,
				TaskClassID: taskClassID,
			})
			itemStateIDs[itemID] = guestStateID
			nextStateID++
		}

		if hostTask != nil && hostTask.EmbeddedBy == nil {
			v := guestStateID
			hostTask.EmbeddedBy = &v
		}

		guestTask := state.TaskByStateID(guestStateID)
		if guestTask == nil {
			continue
		}
		if guestTask.EmbedHost == nil {
			v := hostStateID
			guestTask.EmbedHost = &v
		}
		guestTask.Status = "existing"
		if hostTask != nil && len(guestTask.Slots) == 0 {
			guestTask.Slots = cloneTaskSlots(hostTask.Slots)
		}
		// existing 的 task_item 不应再携带 Duration，避免预览层误判成 suggested。
		guestTask.Duration = 0
	}

	return state
}

// isTaskItemPending 仅根据 status 判断是否应进入 pending 池。
//
// 说明：
// 1. status=nil 兼容历史数据，按“未安排”处理；
// 2. 仅 status=TaskItemStatusUnscheduled 进入 pending；
// 3. 其它“已安排”信号由 LoadScheduleState 主流程统一判定，避免多处口径不一致。
func isTaskItemPending(item model.TaskClassItem) bool {
	if item.Status == nil {
		return true
	}
	return *item.Status == model.TaskItemStatusUnscheduled
}

// estimateTaskItemDuration 估算 pending 任务默认时长。
//
// 规则：若任务类声明了 total_slots，则按 total_slots / item_count 取整（最少 1）；
// 否则回退到 2 节。
func estimateTaskItemDuration(tc model.TaskClass) int {
	duration := 2
	if tc.TotalSlots != nil && *tc.TotalSlots > 0 && len(tc.Items) > 0 {
		if d := *tc.TotalSlots / len(tc.Items); d > 0 {
			duration = d
		}
	}
	return duration
}

// taskItemName 读取任务项展示名。
func taskItemName(item model.TaskClassItem) string {
	if item.Content == nil {
		return ""
	}
	return *item.Content
}

// slotsFromTargetTime 将 task_items.embedded_time 转换为 state 的槽位结构。
// 若 target 为空、节次非法、或不在窗口内，返回 false。
func slotsFromTargetTime(
	target *model.TargetTime,
	dayLookup map[[2]int]int,
) ([]schedule.TaskSlot, bool) {
	if target == nil {
		return nil, false
	}
	if target.SectionFrom < 1 || target.SectionTo < target.SectionFrom {
		return nil, false
	}
	day, ok := dayLookup[[2]int{target.Week, target.DayOfWeek}]
	if !ok {
		return nil, false
	}
	return []schedule.TaskSlot{
		{
			Day:       day,
			SlotStart: target.SectionFrom,
			SlotEnd:   target.SectionTo,
		},
	}, true
}

// ScheduleChangeType 表示两份 ScheduleState 对比后的变更类型。
type ScheduleChangeType string

const (
	ChangePlace   ScheduleChangeType = "place"   // 从 pending 变为已放置
	ChangeMove    ScheduleChangeType = "move"    // 已有槽位发生移动
	ChangeUnplace ScheduleChangeType = "unplace" // 从已放置变回 pending
)

// SlotCoord 表示数据库坐标系中的单节槽位（week/day_of_week/section）。
type SlotCoord struct {
	Week      int
	DayOfWeek int
	Section   int
}

// ScheduleChange 描述单个任务在前后状态间的变化。
type ScheduleChange struct {
	Type       ScheduleChangeType
	StateID    int
	Source     string // "event" | "task_item"
	SourceID   int    // ScheduleEvent.ID 或 TaskClassItem.ID
	EventType  string // 仅 source=event 时有意义（course/task）
	CategoryID int    // 仅 source=task_item 时有意义
	Name       string

	// place/move 的新位置（展开到逐节坐标）。
	NewCoords []SlotCoord
	// move/unplace 的旧位置（展开到逐节坐标）。
	OldCoords []SlotCoord

	// HostEventID：变更后位置对应的宿主 event（非嵌入为 0）。
	HostEventID int
	// OldHostEventID：move 时旧位置对应的宿主 event（非嵌入为 0）。
	OldHostEventID int
}

// DiffScheduleState 比较 original 与 modified，返回需要持久化的变更集合。
func DiffScheduleState(
	original *schedule.ScheduleState,
	modified *schedule.ScheduleState,
) []ScheduleChange {
	if original == nil || modified == nil {
		return nil
	}

	origTasks := indexByStateID(original)
	var changes []ScheduleChange

	for i := range modified.Tasks {
		mod := &modified.Tasks[i]
		orig := origTasks[mod.StateID]

		wasPending := orig == nil || orig.Status == "pending"
		hasSlots := len(mod.Slots) > 0
		hadSlots := orig != nil && len(orig.Slots) > 0

		switch {
		case wasPending && hasSlots:
			changes = append(changes, ScheduleChange{
				Type:        ChangePlace,
				StateID:     mod.StateID,
				Source:      mod.Source,
				SourceID:    mod.SourceID,
				EventType:   mod.EventType,
				CategoryID:  mod.CategoryID,
				Name:        mod.Name,
				NewCoords:   expandToCoords(mod.Slots, modified),
				HostEventID: resolveHostEventID(mod, modified),
			})
		case hadSlots && hasSlots && !slotsEqual(orig.Slots, mod.Slots):
			changes = append(changes, ScheduleChange{
				Type:           ChangeMove,
				StateID:        mod.StateID,
				Source:         mod.Source,
				SourceID:       mod.SourceID,
				EventType:      mod.EventType,
				CategoryID:     mod.CategoryID,
				Name:           mod.Name,
				OldCoords:      expandToCoords(orig.Slots, original),
				NewCoords:      expandToCoords(mod.Slots, modified),
				HostEventID:    resolveHostEventID(mod, modified),
				OldHostEventID: resolveHostEventID(orig, original),
			})
		case hadSlots && !hasSlots:
			changes = append(changes, ScheduleChange{
				Type:        ChangeUnplace,
				StateID:     mod.StateID,
				Source:      orig.Source,
				SourceID:    orig.SourceID,
				EventType:   orig.EventType,
				Name:        orig.Name,
				OldCoords:   expandToCoords(orig.Slots, original),
				HostEventID: resolveHostEventID(orig, original),
			})
		}
	}

	return changes
}

// indexByStateID 将任务列表按 state_id 建立索引。
func indexByStateID(state *schedule.ScheduleState) map[int]*schedule.ScheduleTask {
	m := make(map[int]*schedule.ScheduleTask, len(state.Tasks))
	for i := range state.Tasks {
		m[state.Tasks[i].StateID] = &state.Tasks[i]
	}
	return m
}

// slotsEqual 判断两个压缩槽位切片是否完全一致。
func slotsEqual(a, b []schedule.TaskSlot) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

// cloneTaskSlots 深拷贝槽位切片。
func cloneTaskSlots(src []schedule.TaskSlot) []schedule.TaskSlot {
	if len(src) == 0 {
		return nil
	}
	dst := make([]schedule.TaskSlot, len(src))
	copy(dst, src)
	return dst
}

// resolveHostEventID 通过任务的 EmbedHost 反查宿主 event_id。
// 非嵌入任务或宿主不存在时返回 0。
func resolveHostEventID(task *schedule.ScheduleTask, state *schedule.ScheduleState) int {
	if task == nil || task.EmbedHost == nil {
		return 0
	}
	host := state.TaskByStateID(*task.EmbedHost)
	if host == nil {
		return 0
	}
	return host.SourceID
}

// expandToCoords 将压缩槽位展开成逐节坐标，便于后续持久化层处理。
func expandToCoords(slots []schedule.TaskSlot, state *schedule.ScheduleState) []SlotCoord {
	var coords []SlotCoord
	for _, slot := range slots {
		week, dow, ok := state.DayToWeekDay(slot.Day)
		if !ok {
			continue
		}
		for sec := slot.SlotStart; sec <= slot.SlotEnd; sec++ {
			coords = append(coords, SlotCoord{Week: week, DayOfWeek: dow, Section: sec})
		}
	}
	return coords
}
