import Foundation

struct AutocompleteEngine {

    struct AutocompleteResult {
        let filledSlots: [(slotId: UUID, dishId: UUID)]
        let unfilledCount: Int
    }

    static func autocomplete(
        emptySlots: [MealSlot],
        currentPlan: WeekPlan,
        allDishes: [Dish],
        allTags: [Tag]
    ) -> AutocompleteResult {
        let tagMap = Dictionary(uniqueKeysWithValues: allTags.map { ($0.id, $0) })
        let dishMap = Dictionary(uniqueKeysWithValues: allDishes.map { ($0.id, $0) })
        var filledSlots: [(slotId: UUID, dishId: UUID)] = []
        var unfilledCount = 0

        let sortedEmpty = emptySlots.sorted {
            ($0.dayOfWeek, $0.mealType) < ($1.dayOfWeek, $1.mealType)
        }

        for slot in sortedEmpty {
            let strictCandidates = allDishes.filter { dish in
                !violatesRules(
                    dish: dish,
                    slot: slot,
                    plan: currentPlan,
                    tagMap: tagMap,
                    dishMap: dishMap
                )
            }

            // Fallback: if strict rules cannot fill a slot, allow repeating dishes
            // that only fail the implicit "no repeat this week" rule.
            let candidates: [Dish]
            if strictCandidates.isEmpty {
                candidates = allDishes.filter { dish in
                    !violatesExplicitRules(
                        dish: dish,
                        slot: slot,
                        plan: currentPlan,
                        tagMap: tagMap,
                        dishMap: dishMap
                    )
                }
            } else {
                candidates = strictCandidates
            }

            if candidates.isEmpty {
                unfilledCount += 1
                continue
            }

            if let picked = weightedRandomPick(candidates: candidates, plan: currentPlan) {
                slot.dishId = picked.id
                filledSlots.append((slotId: slot.id, dishId: picked.id))
            } else {
                unfilledCount += 1
            }
        }

        return AutocompleteResult(filledSlots: filledSlots, unfilledCount: unfilledCount)
    }

    static func validateDrop(
        dish: Dish,
        slot: MealSlot,
        plan: WeekPlan,
        allTags: [Tag],
        allDishes: [Dish]
    ) -> Bool {
        let tagMap = Dictionary(uniqueKeysWithValues: allTags.map { ($0.id, $0) })
        let dishMap = Dictionary(uniqueKeysWithValues: allDishes.map { ($0.id, $0) })
        return !violatesRules(dish: dish, slot: slot, plan: plan, tagMap: tagMap, dishMap: dishMap)
    }

    private static func violatesRules(
        dish: Dish,
        slot: MealSlot,
        plan: WeekPlan,
        tagMap: [UUID: Tag],
        dishMap: [UUID: Dish]
    ) -> Bool {
        violatesDefaultNoRepeatRule(dish: dish, slot: slot, plan: plan, tagMap: tagMap) ||
        violatesExplicitRules(dish: dish, slot: slot, plan: plan, tagMap: tagMap, dishMap: dishMap)
    }

    private static func violatesDefaultNoRepeatRule(
        dish: Dish,
        slot: MealSlot,
        plan: WeekPlan,
        tagMap: [UUID: Tag]
    ) -> Bool {
        // Default behavior: if a dish has no explicit rules, avoid repeating it in the same week.
        if !dishHasExplicitRules(dish: dish, tagMap: tagMap) {
            let alreadyAssignedThisWeek = plan.slots.contains {
                $0.id != slot.id && $0.dishId == dish.id
            }
            if alreadyAssignedThisWeek { return true }
        }
        return false
    }

    private static func violatesExplicitRules(
        dish: Dish,
        slot: MealSlot,
        plan: WeekPlan,
        tagMap: [UUID: Tag],
        dishMap: [UUID: Dish]
    ) -> Bool {
        for tagId in dish.tagIds {
            guard let tag = tagMap[tagId] else { continue }

            // Max per week
            if let maxPerWeek = tag.maxPerWeek {
                var count = 0
                for s in plan.slots {
                    guard let did = s.dishId, let d = dishMap[did] else { continue }
                    if d.tagIds.contains(tagId) { count += 1 }
                }
                if count >= maxPerWeek { return true }
            }

            // No consecutive
            if tag.noConsecutive {
                for adjSlot in adjacentSlots(of: slot, in: plan) {
                    guard let did = adjSlot.dishId, let d = dishMap[did] else { continue }
                    if d.tagIds.contains(tagId) { return true }
                }
            }

            // No duplicate in day
            if tag.noDuplicateInDay {
                for sdSlot in plan.slots where sdSlot.dayOfWeek == slot.dayOfWeek && sdSlot.id != slot.id {
                    guard let did = sdSlot.dishId, let d = dishMap[did] else { continue }
                    if d.tagIds.contains(tagId) { return true }
                }
            }

            // Meal type restriction
            if let restriction = tag.mealTypeRestriction, slot.mealType != restriction {
                return true
            }
        }
        return false
    }

    private static func dishHasExplicitRules(dish: Dish, tagMap: [UUID: Tag]) -> Bool {
        for tagId in dish.tagIds {
            guard let tag = tagMap[tagId] else { continue }
            if tag.maxPerWeek != nil ||
                tag.noConsecutive ||
                tag.noDuplicateInDay ||
                tag.mealTypeRestriction != nil {
                return true
            }
        }
        return false
    }

    private static func adjacentSlots(of slot: MealSlot, in plan: WeekPlan) -> [MealSlot] {
        var result: [MealSlot] = []
        if slot.dayOfWeek > 0,
           let prev = plan.slots.first(where: { $0.dayOfWeek == slot.dayOfWeek - 1 && $0.mealType == slot.mealType }) {
            result.append(prev)
        }
        if slot.dayOfWeek < 6,
           let next = plan.slots.first(where: { $0.dayOfWeek == slot.dayOfWeek + 1 && $0.mealType == slot.mealType }) {
            result.append(next)
        }
        return result
    }

    private static func weightedRandomPick(candidates: [Dish], plan: WeekPlan) -> Dish? {
        guard !candidates.isEmpty else { return nil }
        let weights: [Double] = candidates.map { dish in
            let usage = plan.slots.filter { $0.dishId == dish.id }.count
            switch usage {
            case 0: return 3.0
            case 1: return 2.0
            default: return 1.0
            }
        }
        let total = weights.reduce(0, +)
        var r = Double.random(in: 0..<total)
        for (i, w) in weights.enumerated() {
            r -= w
            if r <= 0 { return candidates[i] }
        }
        return candidates.last
    }
}
