import SwiftUI
import SwiftData

@MainActor
final class HomeViewModel: ObservableObject {
    struct InvalidDropContext: Identifiable {
        let id = UUID()
        let dish: Dish
        let slot: MealSlot
    }

    private struct SlotSnapshot {
        let dishId: UUID?
        let isRuleOverridden: Bool
    }

    @Published var currentWeekStart: Date
    @Published var isAutoCompleting: Bool = false
    @Published var showConfetti: Bool = false
    @Published var showToast: Bool = false
    @Published var toastMessage: String = ""
    @Published var showResetAlert: Bool = false
    @Published var showDishForm: Bool = false
    @Published var draggedDish: Dish?
    @Published var invalidDropContext: InvalidDropContext?
    @Published private(set) var hasUndoSnapshot: Bool = false

    private var lastWeekStartSnapshot: Date?
    private var lastSlotsSnapshot: [UUID: SlotSnapshot] = [:]
    private var isApplyingUndo: Bool = false

    init() {
        self.currentWeekStart = Date().startOfWeek()
    }

    func goToPreviousWeek() {
        withAnimation(.easeInOut(duration: 0.3)) {
            currentWeekStart = currentWeekStart.addingDays(-7)
        }
    }

    func goToNextWeek() {
        withAnimation(.easeInOut(duration: 0.3)) {
            currentWeekStart = currentWeekStart.addingDays(7)
        }
    }

    func jumpToWeek(startDate: Date) {
        withAnimation(.easeInOut(duration: 0.25)) {
            currentWeekStart = startDate.startOfWeek()
        }
    }

    var weekRangeText: String {
        currentWeekStart.formattedWeekRange()
    }

    var canEditCurrentWeek: Bool {
        currentWeekStart >= Date().startOfWeek()
    }

    func getOrCreateWeekPlan(context: ModelContext, settings: AppSettings) -> WeekPlan? {
        let start = currentWeekStart
        let descriptor = FetchDescriptor<WeekPlan>(
            predicate: #Predicate<WeekPlan> { plan in
                plan.weekStartDate == start
            }
        )

        if let existing = try? context.fetch(descriptor).first {
            return existing
        }

        return DefaultDataService.createWeekPlan(for: currentWeekStart, settings: settings, context: context)
    }

    func reconcileSlotsIfNeeded(plan: WeekPlan, settings: AppSettings, context: ModelContext) {
        syncSlotsIfNeeded(plan: plan, settings: settings, context: context)
    }

    func assignDish(_ dish: Dish, to slot: MealSlot, plan: WeekPlan, settings: AppSettings, isOverride: Bool = false) {
        captureUndoSnapshot(plan: plan)

        if let oldEventId = slot.calendarEventId {
            CalendarService.shared.deleteEvent(eventId: oldEventId)
            slot.calendarEventId = nil
        }

        slot.dishId = dish.id
        slot.isRuleOverridden = isOverride
        plan.updatedAt = Date()

        applyCalendarSyncPolicy(plan: plan, settings: settings)

        HapticManager.shared.notification(type: .success)
        showToastMessage(localizedString("toast_dish_assigned", language: settings.languageEnum.resolved()))
    }

    func removeDish(from slot: MealSlot, plan: WeekPlan, settings: AppSettings) {
        captureUndoSnapshot(plan: plan)

        if let eventId = slot.calendarEventId {
            CalendarService.shared.deleteEvent(eventId: eventId)
            slot.calendarEventId = nil
        }
        slot.dishId = nil
        slot.isRuleOverridden = false
        plan.updatedAt = Date()
        applyCalendarSyncPolicy(plan: plan, settings: settings, shouldNotify: false)
        HapticManager.shared.notification(type: .warning)
    }

    func autoComplete(plan: WeekPlan, dishes: [Dish], tags: [Tag], settings: AppSettings) {
        captureUndoSnapshot(plan: plan)
        isAutoCompleting = true

        let emptySlots = plan.slots.filter { $0.dishId == nil }
        let result = AutocompleteEngine.autocomplete(
            emptySlots: emptySlots,
            currentPlan: plan,
            allDishes: dishes,
            allTags: tags
        )

        plan.updatedAt = Date()
        applyCalendarSyncPolicy(plan: plan, settings: settings, shouldNotify: false)

        Task {
            for _ in result.filledSlots {
                try? await Task.sleep(nanoseconds: 150_000_000)
                HapticManager.shared.impact(style: .light)
            }

            isAutoCompleting = false

            if result.unfilledCount > 0 {
                showToastMessage(localizedString("toast_cannot_complete", language: settings.languageEnum.resolved()))
            } else {
                showConfetti = true
                HapticManager.shared.notification(type: .success)
                showToastMessage(localizedString("toast_week_complete", language: settings.languageEnum.resolved()))

                DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                    self.showConfetti = false
                }
            }
        }
    }

    func resetWeek(plan: WeekPlan, settings: AppSettings) {
        captureUndoSnapshot(plan: plan)

        for slot in plan.slots {
            if let eventId = slot.calendarEventId {
                CalendarService.shared.deleteEvent(eventId: eventId)
                slot.calendarEventId = nil
            }
            slot.dishId = nil
            slot.isRuleOverridden = false
        }
        plan.updatedAt = Date()
        HapticManager.shared.notification(type: .warning)
        showToastMessage(localizedString("toast_week_reset", language: settings.languageEnum.resolved()))
    }

    func assignDishToFirstFreeSlot(
        _ dish: Dish,
        plan: WeekPlan,
        settings: AppSettings,
        allTags: [Tag],
        allDishes: [Dish]
    ) {
        guard let slot = firstFreeSlot(in: plan) else {
            showToastMessage(localizedString("toast_no_free_slots", language: settings.languageEnum.resolved()))
            HapticManager.shared.notification(type: .warning)
            return
        }

        let isValid = AutocompleteEngine.validateDrop(
            dish: dish,
            slot: slot,
            plan: plan,
            allTags: allTags,
            allDishes: allDishes
        )

        if isValid {
            assignDish(dish, to: slot, plan: plan, settings: settings)
        } else {
            confirmInvalidDrop(dish, to: slot, plan: plan, settings: settings)
        }
    }

    func syncWeekToCalendar(plan: WeekPlan, dishes: [Dish], settings: AppSettings) {
        guard settings.syncEnabled else { return }
        syncAllAssignedSlotsToCalendar(plan: plan, dishes: dishes, settings: settings)
        showToastMessage(localizedString("toast_calendar_synced", language: settings.languageEnum.resolved()))
    }

    func moveOrSwapDish(
        from sourceSlot: MealSlot,
        to targetSlot: MealSlot,
        plan: WeekPlan,
        settings: AppSettings,
        allTags: [Tag],
        allDishes: [Dish]
    ) {
        guard sourceSlot.id != targetSlot.id else { return }
        guard let sourceDishId = sourceSlot.dishId else { return }
        captureUndoSnapshot(plan: plan)

        if let sourceEventId = sourceSlot.calendarEventId {
            CalendarService.shared.deleteEvent(eventId: sourceEventId)
            sourceSlot.calendarEventId = nil
        }
        if let targetEventId = targetSlot.calendarEventId {
            CalendarService.shared.deleteEvent(eventId: targetEventId)
            targetSlot.calendarEventId = nil
        }

        let targetDishId = targetSlot.dishId
        sourceSlot.dishId = targetDishId
        targetSlot.dishId = sourceDishId
        sourceSlot.isRuleOverridden = false
        targetSlot.isRuleOverridden = false

        updateRuleOverrideFlag(for: sourceSlot, plan: plan, allTags: allTags, allDishes: allDishes)
        updateRuleOverrideFlag(for: targetSlot, plan: plan, allTags: allTags, allDishes: allDishes)

        plan.updatedAt = Date()
        applyCalendarSyncPolicy(plan: plan, settings: settings)
        HapticManager.shared.notification(type: .success)
        showToastMessage(localizedString("toast_dish_assigned", language: settings.languageEnum.resolved()))
    }

    func copyFromPreviousWeek(
        currentPlan: WeekPlan,
        previousPlan: WeekPlan?,
        settings: AppSettings,
        allTags: [Tag],
        allDishes: [Dish]
    ) {
        guard let previousPlan else {
            showToastMessage(localizedString("toast_previous_week_empty", language: settings.languageEnum.resolved()))
            HapticManager.shared.notification(type: .warning)
            return
        }

        captureUndoSnapshot(plan: currentPlan)

        var previousByKey: [SlotKey: MealSlot] = [:]
        for slot in previousPlan.slots {
            previousByKey[SlotKey(dayOfWeek: slot.dayOfWeek, mealType: slot.mealType)] = slot
        }

        let dishIds = Set(allDishes.map(\.id))
        for slot in currentPlan.slots {
            if let eventId = slot.calendarEventId {
                CalendarService.shared.deleteEvent(eventId: eventId)
                slot.calendarEventId = nil
            }

            let key = SlotKey(dayOfWeek: slot.dayOfWeek, mealType: slot.mealType)
            if let previousDishId = previousByKey[key]?.dishId, dishIds.contains(previousDishId) {
                slot.dishId = previousDishId
            } else {
                slot.dishId = nil
            }
            slot.isRuleOverridden = false
            updateRuleOverrideFlag(for: slot, plan: currentPlan, allTags: allTags, allDishes: allDishes)
        }

        currentPlan.updatedAt = Date()
        applyCalendarSyncPolicy(plan: currentPlan, settings: settings)
        HapticManager.shared.notification(type: .success)
        showToastMessage(localizedString("toast_copied_previous_week", language: settings.languageEnum.resolved()))
    }

    func canUndo(for plan: WeekPlan) -> Bool {
        hasUndoSnapshot && lastWeekStartSnapshot == plan.weekStartDate
    }

    func undoLastAction(plan: WeekPlan, settings: AppSettings) {
        guard canUndo(for: plan) else { return }
        isApplyingUndo = true

        for slot in plan.slots {
            if let eventId = slot.calendarEventId {
                CalendarService.shared.deleteEvent(eventId: eventId)
            }
            slot.calendarEventId = nil

            if let snap = lastSlotsSnapshot[slot.id] {
                slot.dishId = snap.dishId
                slot.isRuleOverridden = snap.isRuleOverridden
            } else {
                slot.dishId = nil
                slot.isRuleOverridden = false
            }
        }

        plan.updatedAt = Date()
        applyCalendarSyncPolicy(plan: plan, settings: settings, shouldNotify: false)
        clearUndoSnapshot()
        isApplyingUndo = false

        HapticManager.shared.notification(type: .success)
        showToastMessage(localizedString("toast_undo_applied", language: settings.languageEnum.resolved()))
    }

    private func showToastMessage(_ message: String) {
        toastMessage = message
        withAnimation(.spring()) {
            showToast = true
        }
    }

    private func captureUndoSnapshot(plan: WeekPlan) {
        guard !isApplyingUndo else { return }
        lastWeekStartSnapshot = plan.weekStartDate
        lastSlotsSnapshot = Dictionary(uniqueKeysWithValues: plan.slots.map { slot in
            (slot.id, SlotSnapshot(dishId: slot.dishId, isRuleOverridden: slot.isRuleOverridden))
        })
        hasUndoSnapshot = true
    }

    private func clearUndoSnapshot() {
        lastWeekStartSnapshot = nil
        lastSlotsSnapshot.removeAll()
        hasUndoSnapshot = false
    }

    private func firstFreeSlot(in plan: WeekPlan) -> MealSlot? {
        plan.slots
            .filter { $0.dishId == nil }
            .sorted { lhs, rhs in
                if lhs.dayOfWeek != rhs.dayOfWeek {
                    return lhs.dayOfWeek < rhs.dayOfWeek
                }
                return mealTypeOrder(lhs.mealType) < mealTypeOrder(rhs.mealType)
            }
            .first
    }

    private func mealTypeOrder(_ raw: String) -> Int {
        raw == MealType.lunch.rawValue ? 0 : 1
    }

    private func applyCalendarSyncPolicy(plan: WeekPlan, settings: AppSettings, shouldNotify: Bool = true) {
        guard settings.syncEnabled else { return }

        switch settings.syncModeEnum {
        case .weekComplete:
            if plan.slots.allSatisfy({ $0.dishId != nil }) {
                let descriptor = FetchDescriptor<Dish>()
                let dishes = (try? plan.modelContext?.fetch(descriptor)) ?? []
                syncAllAssignedSlotsToCalendar(plan: plan, dishes: dishes, settings: settings)
                if shouldNotify {
                    showToastMessage(localizedString("toast_calendar_synced", language: settings.languageEnum.resolved()))
                }
            } else {
                clearCalendarEvents(for: plan)
            }
        case .manual:
            break
        }
    }

    private func clearCalendarEvents(for plan: WeekPlan) {
        for slot in plan.slots {
            if let eventId = slot.calendarEventId {
                CalendarService.shared.deleteEvent(eventId: eventId)
                slot.calendarEventId = nil
            }
        }
    }

    private func syncAllAssignedSlotsToCalendar(plan: WeekPlan, dishes: [Dish], settings: AppSettings) {
        let dishById = Dictionary(uniqueKeysWithValues: dishes.map { ($0.id, $0) })
        for slot in plan.slots {
            if let oldEventId = slot.calendarEventId {
                CalendarService.shared.deleteEvent(eventId: oldEventId)
                slot.calendarEventId = nil
            }

            guard let dishId = slot.dishId, let dish = dishById[dishId] else { continue }
            slot.calendarEventId = CalendarService.shared.createEvent(
                slot: slot,
                dishName: dish.name,
                dishDescription: dish.descriptionText,
                weekStartDate: currentWeekStart,
                settings: settings
            )
        }
    }

    private func updateRuleOverrideFlag(for slot: MealSlot, plan: WeekPlan, allTags: [Tag], allDishes: [Dish]) {
        guard let dishId = slot.dishId, let dish = allDishes.first(where: { $0.id == dishId }) else {
            slot.isRuleOverridden = false
            return
        }
        let valid = AutocompleteEngine.validateDrop(
            dish: dish,
            slot: slot,
            plan: plan,
            allTags: allTags,
            allDishes: allDishes
        )
        slot.isRuleOverridden = !valid
    }

    func confirmInvalidDrop(_ dish: Dish, to slot: MealSlot, plan: WeekPlan, settings: AppSettings) {
        invalidDropContext = InvalidDropContext(dish: dish, slot: slot)
        HapticManager.shared.notification(type: .warning)
    }

    private struct SlotKey: Hashable {
        let dayOfWeek: Int
        let mealType: String
    }

    private func syncSlotsIfNeeded(plan: WeekPlan, settings: AppSettings, context: ModelContext) {
        let maxDay = settings.includeWeekends ? 6 : 4
        let mealTypes: [String] = {
            switch settings.mealWindowsEnum {
            case .dinnerOnly: return ["dinner"]
            case .lunchOnly: return ["lunch"]
            case .both: return ["lunch", "dinner"]
            }
        }()

        var desired = Set<SlotKey>()
        for day in 0...maxDay {
            for mealType in mealTypes {
                desired.insert(SlotKey(dayOfWeek: day, mealType: mealType))
            }
        }

        var existingByKey: [SlotKey: MealSlot] = [:]
        for slot in plan.slots {
            let key = SlotKey(dayOfWeek: slot.dayOfWeek, mealType: slot.mealType)
            if existingByKey[key] == nil {
                existingByKey[key] = slot
            }
        }
        var changed = false

        let toDelete = plan.slots.filter { slot in
            !desired.contains(SlotKey(dayOfWeek: slot.dayOfWeek, mealType: slot.mealType))
        }
        for slot in toDelete {
            if let eventId = slot.calendarEventId {
                CalendarService.shared.deleteEvent(eventId: eventId)
            }
            plan.slots.removeAll { $0.id == slot.id }
            context.delete(slot)
            changed = true
        }

        for key in desired where existingByKey[key] == nil {
            let slot = MealSlot(dayOfWeek: key.dayOfWeek, mealType: key.mealType)
            slot.weekPlan = plan
            plan.slots.append(slot)
            context.insert(slot)
            changed = true
        }

        if changed {
            plan.updatedAt = Date()
            try? context.save()
        }
    }
}
