//
//  AchievementEngine.swift
//  SportsTime
//
//  Computes achievements based on stadium visits.
//  Recalculates and revokes achievements when visits are deleted.
//

import Foundation
import SwiftData

// MARK: - Achievement Delta

struct AchievementDelta: Sendable {
    let newlyEarned: [AchievementDefinition]
    let revoked: [AchievementDefinition]
    let stillEarned: [AchievementDefinition]

    var hasChanges: Bool {
        !newlyEarned.isEmpty || !revoked.isEmpty
    }
}

// MARK: - Achievement Engine

@MainActor
final class AchievementEngine {

    // MARK: - Properties

    private let modelContext: ModelContext
    private let dataProvider: AppDataProvider

    // MARK: - Initialization

    init(modelContext: ModelContext, dataProvider: AppDataProvider = AppDataProvider.shared) {
        self.modelContext = modelContext
        self.dataProvider = dataProvider
    }

    // MARK: - Public API

    /// Full recalculation (call after visit deleted or on app update)
    func recalculateAllAchievements() async throws -> AchievementDelta {
        // Get all visits
        let visits = try fetchAllVisits()
        let visitedStadiumIds = Set(visits.map { $0.stadiumId })

        // Get currently earned achievements
        let currentAchievements = try fetchEarnedAchievements()
        let currentAchievementIds = Set(currentAchievements.map { $0.achievementTypeId })

        // Calculate which achievements should be earned
        var shouldBeEarned: Set<String> = []
        var newlyEarnedDefinitions: [AchievementDefinition] = []
        var revokedDefinitions: [AchievementDefinition] = []
        var stillEarnedDefinitions: [AchievementDefinition] = []

        for definition in AchievementRegistry.all {
            let isEarned = checkRequirement(definition.requirement, visits: visits, visitedStadiumIds: visitedStadiumIds)

            if isEarned {
                shouldBeEarned.insert(definition.id)

                if currentAchievementIds.contains(definition.id) {
                    stillEarnedDefinitions.append(definition)
                } else {
                    newlyEarnedDefinitions.append(definition)
                }
            } else if currentAchievementIds.contains(definition.id) {
                revokedDefinitions.append(definition)
            }
        }

        // Apply changes
        // Grant new achievements
        for definition in newlyEarnedDefinitions {
            let visitIds = getContributingVisitIds(for: definition.requirement, visits: visits)
            let achievement = Achievement(
                achievementTypeId: definition.id,
                sport: definition.sport,
                visitIds: visitIds
            )
            modelContext.insert(achievement)
        }

        // Revoke achievements
        for definition in revokedDefinitions {
            if let achievement = currentAchievements.first(where: { $0.achievementTypeId == definition.id }) {
                achievement.revoke()
            }
        }

        // Restore previously revoked achievements that are now earned again
        for definition in stillEarnedDefinitions {
            if let achievement = currentAchievements.first(where: {
                $0.achievementTypeId == definition.id && $0.revokedAt != nil
            }) {
                achievement.restore()
            }
        }

        try modelContext.save()

        return AchievementDelta(
            newlyEarned: newlyEarnedDefinitions,
            revoked: revokedDefinitions,
            stillEarned: stillEarnedDefinitions
        )
    }

    /// Quick check after new visit (incremental)
    func checkAchievementsForNewVisit(_ visit: StadiumVisit) async throws -> [AchievementDefinition] {
        let visits = try fetchAllVisits()
        let visitedStadiumIds = Set(visits.map { $0.stadiumId })

        let currentAchievements = try fetchEarnedAchievements()
        let currentAchievementIds = Set(currentAchievements.map { $0.achievementTypeId })

        var newlyEarned: [AchievementDefinition] = []

        for definition in AchievementRegistry.all {
            // Skip already earned
            guard !currentAchievementIds.contains(definition.id) else { continue }

            let isEarned = checkRequirement(definition.requirement, visits: visits, visitedStadiumIds: visitedStadiumIds)

            if isEarned {
                newlyEarned.append(definition)

                let visitIds = getContributingVisitIds(for: definition.requirement, visits: visits)
                let achievement = Achievement(
                    achievementTypeId: definition.id,
                    sport: definition.sport,
                    visitIds: visitIds
                )
                modelContext.insert(achievement)
            }
        }

        try modelContext.save()

        return newlyEarned
    }

    /// Get all earned achievements
    func getEarnedAchievements() throws -> [AchievementDefinition] {
        let achievements = try fetchEarnedAchievements()
        return achievements.compactMap { AchievementRegistry.achievement(byId: $0.achievementTypeId) }
    }

    /// Get progress toward all achievements
    func getProgress() async throws -> [AchievementProgress] {
        let visits = try fetchAllVisits()
        let visitedStadiumIds = Set(visits.map { $0.stadiumId })
        let earnedAchievements = try fetchEarnedAchievements()
        let earnedIds = Set(earnedAchievements.map { $0.achievementTypeId })

        var progress: [AchievementProgress] = []

        for definition in AchievementRegistry.all {
            let (current, total) = calculateProgress(
                for: definition.requirement,
                visits: visits,
                visitedStadiumIds: visitedStadiumIds
            )

            let hasStoredAchievement = earnedIds.contains(definition.id)
            let earnedAt = earnedAchievements.first(where: { $0.achievementTypeId == definition.id })?.earnedAt

            progress.append(AchievementProgress(
                definition: definition,
                currentProgress: current,
                totalRequired: total,
                hasStoredAchievement: hasStoredAchievement,
                earnedAt: earnedAt
            ))
        }

        return progress
    }

    // MARK: - Requirement Checking

    private func checkRequirement(
        _ requirement: AchievementRequirement,
        visits: [StadiumVisit],
        visitedStadiumIds: Set<String>
    ) -> Bool {
        switch requirement {
        case .firstVisit:
            return !visits.isEmpty

        case .visitCount(let count):
            return visitedStadiumIds.count >= count

        case .visitCountForSport(let count, let sport):
            let sportVisits = visits.filter { $0.sport == sport.rawValue }
            let sportStadiums = Set(sportVisits.map { $0.stadiumId })
            return sportStadiums.count >= count

        case .completeDivision(let divisionId):
            return checkDivisionComplete(divisionId, visitedStadiumIds: visitedStadiumIds)

        case .completeConference(let conferenceId):
            return checkConferenceComplete(conferenceId, visitedStadiumIds: visitedStadiumIds)

        case .completeLeague(let sport):
            return checkLeagueComplete(sport, visitedStadiumIds: visitedStadiumIds)

        case .visitsInDays(let visitCount, let days):
            return checkVisitsInDays(visits: visits, requiredVisits: visitCount, withinDays: days)

        case .multipleLeagues(let leagueCount):
            return checkMultipleLeagues(visits: visits, requiredLeagues: leagueCount)

        case .specificStadium(let stadiumId):
            // Direct comparison - canonical IDs match everywhere
            return visitedStadiumIds.contains(stadiumId)
        }
    }

    private func checkDivisionComplete(_ divisionId: String, visitedStadiumIds: Set<String>) -> Bool {
        guard let division = LeagueStructure.division(byId: divisionId) else { return false }

        // Get stadium IDs for teams in this division
        let stadiumIds = getStadiumIdsForDivision(divisionId)
        guard !stadiumIds.isEmpty else { return false }

        return stadiumIds.allSatisfy { visitedStadiumIds.contains($0) }
    }

    private func checkConferenceComplete(_ conferenceId: String, visitedStadiumIds: Set<String>) -> Bool {
        guard let conference = LeagueStructure.conference(byId: conferenceId) else { return false }

        // Get stadium IDs for all teams in this conference
        let stadiumIds = getStadiumIdsForConference(conferenceId)
        guard !stadiumIds.isEmpty else { return false }

        return stadiumIds.allSatisfy { visitedStadiumIds.contains($0) }
    }

    private func checkLeagueComplete(_ sport: Sport, visitedStadiumIds: Set<String>) -> Bool {
        let stadiumIds = getStadiumIdsForLeague(sport)
        guard !stadiumIds.isEmpty else { return false }

        return stadiumIds.allSatisfy { visitedStadiumIds.contains($0) }
    }

    private func checkVisitsInDays(visits: [StadiumVisit], requiredVisits: Int, withinDays: Int) -> Bool {
        guard visits.count >= requiredVisits else { return false }

        // Sort visits by date
        let sortedVisits = visits.sorted { $0.visitDate < $1.visitDate }

        // Sliding window
        for i in 0...(sortedVisits.count - requiredVisits) {
            let windowStart = sortedVisits[i].visitDate
            let windowEnd = sortedVisits[i + requiredVisits - 1].visitDate

            let daysDiff = Calendar.current.dateComponents([.day], from: windowStart, to: windowEnd).day ?? Int.max
            if daysDiff < withinDays {
                // Check unique stadiums in window
                let windowVisits = Array(sortedVisits[i..<(i + requiredVisits)])
                let uniqueStadiums = Set(windowVisits.map { $0.stadiumId })
                if uniqueStadiums.count >= requiredVisits {
                    return true
                }
            }
        }

        return false
    }

    private func checkMultipleLeagues(visits: [StadiumVisit], requiredLeagues: Int) -> Bool {
        let leagues = Set(visits.compactMap { Sport(rawValue: $0.sport) })
        return leagues.count >= requiredLeagues
    }

    // MARK: - Progress Calculation

    private func calculateProgress(
        for requirement: AchievementRequirement,
        visits: [StadiumVisit],
        visitedStadiumIds: Set<String>
    ) -> (current: Int, total: Int) {
        switch requirement {
        case .firstVisit:
            return (visits.isEmpty ? 0 : 1, 1)

        case .visitCount(let count):
            return (visitedStadiumIds.count, count)

        case .visitCountForSport(let count, let sport):
            let sportVisits = visits.filter { $0.sport == sport.rawValue }
            let sportStadiums = Set(sportVisits.map { $0.stadiumId })
            return (sportStadiums.count, count)

        case .completeDivision(let divisionId):
            let stadiumIds = getStadiumIdsForDivision(divisionId)
            let visited = stadiumIds.filter { visitedStadiumIds.contains($0) }.count
            return (visited, stadiumIds.count)

        case .completeConference(let conferenceId):
            let stadiumIds = getStadiumIdsForConference(conferenceId)
            let visited = stadiumIds.filter { visitedStadiumIds.contains($0) }.count
            return (visited, stadiumIds.count)

        case .completeLeague(let sport):
            let stadiumIds = getStadiumIdsForLeague(sport)
            let visited = stadiumIds.filter { visitedStadiumIds.contains($0) }.count
            return (visited, stadiumIds.count)

        case .visitsInDays(let visitCount, _):
            // For journey achievements, show total unique stadiums vs required
            return (min(visitedStadiumIds.count, visitCount), visitCount)

        case .multipleLeagues(let leagueCount):
            let leagues = Set(visits.compactMap { Sport(rawValue: $0.sport) })
            return (leagues.count, leagueCount)

        case .specificStadium(let stadiumId):
            // Direct comparison - canonical IDs match everywhere
            return (visitedStadiumIds.contains(stadiumId) ? 1 : 0, 1)
        }
    }

    // MARK: - Contributing Visits

    private func getContributingVisitIds(for requirement: AchievementRequirement, visits: [StadiumVisit]) -> [UUID] {
        switch requirement {
        case .firstVisit:
            return visits.first.map { [$0.id] } ?? []

        case .visitCount, .visitCountForSport, .multipleLeagues:
            // All visits contribute
            return visits.map { $0.id }

        case .completeDivision(let divisionId):
            let stadiumIds = Set(getStadiumIdsForDivision(divisionId))
            return visits.filter { stadiumIds.contains($0.stadiumId) }.map { $0.id }

        case .completeConference(let conferenceId):
            let stadiumIds = Set(getStadiumIdsForConference(conferenceId))
            return visits.filter { stadiumIds.contains($0.stadiumId) }.map { $0.id }

        case .completeLeague(let sport):
            let stadiumIds = Set(getStadiumIdsForLeague(sport))
            return visits.filter { stadiumIds.contains($0.stadiumId) }.map { $0.id }

        case .visitsInDays(let requiredVisits, let days):
            // Find the qualifying window of visits
            let sortedVisits = visits.sorted { $0.visitDate < $1.visitDate }
            for i in 0...(sortedVisits.count - requiredVisits) {
                let windowStart = sortedVisits[i].visitDate
                let windowEnd = sortedVisits[i + requiredVisits - 1].visitDate
                let daysDiff = Calendar.current.dateComponents([.day], from: windowStart, to: windowEnd).day ?? Int.max
                if daysDiff < days {
                    return Array(sortedVisits[i..<(i + requiredVisits)]).map { $0.id }
                }
            }
            return []

        case .specificStadium(let stadiumId):
            // Direct comparison - canonical IDs match everywhere
            return visits.filter { $0.stadiumId == stadiumId }.map { $0.id }
        }
    }

    // MARK: - Stadium Lookups

    private func getStadiumIdsForDivision(_ divisionId: String) -> [String] {
        // Query CanonicalTeam to find teams in this division
        let descriptor = FetchDescriptor<CanonicalTeam>(
            predicate: #Predicate { $0.divisionId == divisionId && $0.deprecatedAt == nil }
        )

        guard let canonicalTeams = try? modelContext.fetch(descriptor) else {
            return []
        }

        // Get canonical stadium IDs for these teams
        return canonicalTeams.map { $0.stadiumCanonicalId }
    }

    private func getStadiumIdsForConference(_ conferenceId: String) -> [String] {
        guard let conference = LeagueStructure.conference(byId: conferenceId) else { return [] }

        var stadiumIds: [String] = []
        for divisionId in conference.divisionIds {
            stadiumIds.append(contentsOf: getStadiumIdsForDivision(divisionId))
        }
        return stadiumIds
    }

    private func getStadiumIdsForLeague(_ sport: Sport) -> [String] {
        // Get all stadium canonical IDs for this sport
        return dataProvider.stadiums
            .filter { stadium in
                // Check if stadium hosts teams of this sport
                dataProvider.teams.contains { team in
                    team.stadiumId == stadium.id && team.sport == sport
                }
            }
            .map { $0.id }
    }

    // MARK: - Data Fetching

    private func fetchAllVisits() throws -> [StadiumVisit] {
        let descriptor = FetchDescriptor<StadiumVisit>(
            sortBy: [SortDescriptor(\.visitDate, order: .forward)]
        )
        return try modelContext.fetch(descriptor)
    }

    private func fetchEarnedAchievements() throws -> [Achievement] {
        let descriptor = FetchDescriptor<Achievement>(
            predicate: #Predicate { $0.revokedAt == nil }
        )
        return try modelContext.fetch(descriptor)
    }
}

// MARK: - Achievement Progress

struct AchievementProgress: Identifiable {
    let definition: AchievementDefinition
    let currentProgress: Int
    let totalRequired: Int
    let hasStoredAchievement: Bool  // Whether an Achievement record exists in SwiftData
    let earnedAt: Date?

    var id: String { definition.id }

    /// Whether the achievement is earned (either stored or computed from progress)
    var isEarned: Bool {
        // Earned if we have a stored record OR if progress is complete
        hasStoredAchievement || (totalRequired > 0 && currentProgress >= totalRequired)
    }

    var progressPercentage: Double {
        guard totalRequired > 0 else { return 0 }
        return Double(currentProgress) / Double(totalRequired)
    }

    var progressText: String {
        if isEarned {
            return "Completed"
        }
        return "\(currentProgress)/\(totalRequired)"
    }
}
