//
//  CanonicalSyncService.swift
//  SportsTime
//
//  Orchestrates syncing canonical data from CloudKit into SwiftData.
//  Uses date-based delta sync for public database efficiency.
//

import Foundation
import SwiftData
import CloudKit

actor CanonicalSyncService {

    // MARK: - Errors

    enum SyncError: Error, LocalizedError {
        case cloudKitUnavailable
        case syncAlreadyInProgress
        case saveFailed(Error)
        case schemaVersionTooNew(Int)

        var errorDescription: String? {
            switch self {
            case .cloudKitUnavailable:
                return "CloudKit is not available. Check your internet connection and iCloud settings."
            case .syncAlreadyInProgress:
                return "A sync operation is already in progress."
            case .saveFailed(let error):
                return "Failed to save synced data: \(error.localizedDescription)"
            case .schemaVersionTooNew(let version):
                return "Data requires app version supporting schema \(version). Please update the app."
            }
        }
    }

    // MARK: - Sync Result

    struct SyncResult {
        let stadiumsUpdated: Int
        let teamsUpdated: Int
        let gamesUpdated: Int
        let leagueStructuresUpdated: Int
        let teamAliasesUpdated: Int
        let stadiumAliasesUpdated: Int
        let skippedIncompatible: Int
        let skippedOlder: Int
        let duration: TimeInterval

        var totalUpdated: Int {
            stadiumsUpdated + teamsUpdated + gamesUpdated + leagueStructuresUpdated + teamAliasesUpdated + stadiumAliasesUpdated
        }

        var isEmpty: Bool { totalUpdated == 0 }
    }

    // MARK: - Properties

    private let cloudKitService: CloudKitService

    // MARK: - Initialization

    init(cloudKitService: CloudKitService = .shared) {
        self.cloudKitService = cloudKitService
    }

    // MARK: - Public Sync Methods

    /// Perform a full sync of all canonical data types.
    /// This is the main entry point for background sync.
    @MainActor
    func syncAll(context: ModelContext) async throws -> SyncResult {
        let startTime = Date()
        let syncState = SyncState.current(in: context)

        // Prevent concurrent syncs
        guard !syncState.syncInProgress else {
            throw SyncError.syncAlreadyInProgress
        }

        // Check if sync is enabled
        guard syncState.syncEnabled else {
            return SyncResult(
                stadiumsUpdated: 0, teamsUpdated: 0, gamesUpdated: 0,
                leagueStructuresUpdated: 0, teamAliasesUpdated: 0, stadiumAliasesUpdated: 0,
                skippedIncompatible: 0, skippedOlder: 0,
                duration: 0
            )
        }

        // Check CloudKit availability
        guard await cloudKitService.isAvailable() else {
            throw SyncError.cloudKitUnavailable
        }

        // Mark sync in progress
        syncState.syncInProgress = true
        syncState.lastSyncAttempt = Date()

        var totalStadiums = 0
        var totalTeams = 0
        var totalGames = 0
        var totalLeagueStructures = 0
        var totalTeamAliases = 0
        var totalStadiumAliases = 0
        var totalSkippedIncompatible = 0
        var totalSkippedOlder = 0

        do {
            // Sync in dependency order
            let (stadiums, skipIncompat1, skipOlder1) = try await syncStadiums(
                context: context,
                since: syncState.lastSuccessfulSync
            )
            totalStadiums = stadiums
            totalSkippedIncompatible += skipIncompat1
            totalSkippedOlder += skipOlder1

            let (leagueStructures, skipIncompat2, skipOlder2) = try await syncLeagueStructure(
                context: context,
                since: syncState.lastSuccessfulSync
            )
            totalLeagueStructures = leagueStructures
            totalSkippedIncompatible += skipIncompat2
            totalSkippedOlder += skipOlder2

            let (teams, skipIncompat3, skipOlder3) = try await syncTeams(
                context: context,
                since: syncState.lastSuccessfulSync
            )
            totalTeams = teams
            totalSkippedIncompatible += skipIncompat3
            totalSkippedOlder += skipOlder3

            let (teamAliases, skipIncompat4, skipOlder4) = try await syncTeamAliases(
                context: context,
                since: syncState.lastSuccessfulSync
            )
            totalTeamAliases = teamAliases
            totalSkippedIncompatible += skipIncompat4
            totalSkippedOlder += skipOlder4

            let (stadiumAliases, skipIncompat5, skipOlder5) = try await syncStadiumAliases(
                context: context,
                since: syncState.lastSuccessfulSync
            )
            totalStadiumAliases = stadiumAliases
            totalSkippedIncompatible += skipIncompat5
            totalSkippedOlder += skipOlder5

            let (games, skipIncompat6, skipOlder6) = try await syncGames(
                context: context,
                since: syncState.lastSuccessfulSync
            )
            totalGames = games
            totalSkippedIncompatible += skipIncompat6
            totalSkippedOlder += skipOlder6

            // Mark sync successful
            syncState.syncInProgress = false
            syncState.lastSuccessfulSync = Date()
            syncState.lastSyncError = nil
            syncState.consecutiveFailures = 0

            try context.save()

        } catch {
            // Mark sync failed
            syncState.syncInProgress = false
            syncState.lastSyncError = error.localizedDescription
            syncState.consecutiveFailures += 1

            // Pause sync after too many failures
            if syncState.consecutiveFailures >= 5 {
                syncState.syncEnabled = false
                syncState.syncPausedReason = "Too many consecutive failures. Sync paused."
            }

            try? context.save()
            throw error
        }

        return SyncResult(
            stadiumsUpdated: totalStadiums,
            teamsUpdated: totalTeams,
            gamesUpdated: totalGames,
            leagueStructuresUpdated: totalLeagueStructures,
            teamAliasesUpdated: totalTeamAliases,
            stadiumAliasesUpdated: totalStadiumAliases,
            skippedIncompatible: totalSkippedIncompatible,
            skippedOlder: totalSkippedOlder,
            duration: Date().timeIntervalSince(startTime)
        )
    }

    /// Re-enable sync after it was paused due to failures.
    @MainActor
    func resumeSync(context: ModelContext) {
        let syncState = SyncState.current(in: context)
        syncState.syncEnabled = true
        syncState.syncPausedReason = nil
        syncState.consecutiveFailures = 0
        try? context.save()
    }

    // MARK: - Individual Sync Methods

    @MainActor
    private func syncStadiums(
        context: ModelContext,
        since lastSync: Date?
    ) async throws -> (updated: Int, skippedIncompatible: Int, skippedOlder: Int) {
        // Use sync method that returns canonical IDs directly from CloudKit
        let syncStadiums = try await cloudKitService.fetchStadiumsForSync()

        var updated = 0
        var skippedIncompatible = 0
        var skippedOlder = 0

        for syncStadium in syncStadiums {
            // Use canonical ID directly from CloudKit - no UUID-based generation!
            let result = try mergeStadium(
                syncStadium.stadium,
                canonicalId: syncStadium.canonicalId,
                context: context
            )

            switch result {
            case .applied: updated += 1
            case .skippedIncompatible: skippedIncompatible += 1
            case .skippedOlder: skippedOlder += 1
            }
        }

        return (updated, skippedIncompatible, skippedOlder)
    }

    @MainActor
    private func syncTeams(
        context: ModelContext,
        since lastSync: Date?
    ) async throws -> (updated: Int, skippedIncompatible: Int, skippedOlder: Int) {
        // Use sync method that returns canonical IDs directly from CloudKit
        var allSyncTeams: [CloudKitService.SyncTeam] = []
        for sport in Sport.allCases {
            let syncTeams = try await cloudKitService.fetchTeamsForSync(for: sport)
            allSyncTeams.append(contentsOf: syncTeams)
        }

        var updated = 0
        var skippedIncompatible = 0
        var skippedOlder = 0

        for syncTeam in allSyncTeams {
            // Use canonical IDs directly from CloudKit - no UUID lookups!
            let result = try mergeTeam(
                syncTeam.team,
                canonicalId: syncTeam.canonicalId,
                stadiumCanonicalId: syncTeam.stadiumCanonicalId,
                context: context
            )

            switch result {
            case .applied: updated += 1
            case .skippedIncompatible: skippedIncompatible += 1
            case .skippedOlder: skippedOlder += 1
            }
        }

        return (updated, skippedIncompatible, skippedOlder)
    }

    @MainActor
    private func syncGames(
        context: ModelContext,
        since lastSync: Date?
    ) async throws -> (updated: Int, skippedIncompatible: Int, skippedOlder: Int) {
        // Use sync method that returns canonical IDs directly from CloudKit
        let startDate = lastSync ?? Date()
        let endDate = Calendar.current.date(byAdding: .month, value: 6, to: Date()) ?? Date()

        let syncGames = try await cloudKitService.fetchGamesForSync(
            sports: Set(Sport.allCases),
            startDate: startDate,
            endDate: endDate
        )

        var updated = 0
        var skippedIncompatible = 0
        var skippedOlder = 0

        for syncGame in syncGames {
            // Use canonical IDs directly from CloudKit - no UUID lookups!
            let result = try mergeGame(
                syncGame.game,
                canonicalId: syncGame.canonicalId,
                homeTeamCanonicalId: syncGame.homeTeamCanonicalId,
                awayTeamCanonicalId: syncGame.awayTeamCanonicalId,
                stadiumCanonicalId: syncGame.stadiumCanonicalId,
                context: context
            )

            switch result {
            case .applied: updated += 1
            case .skippedIncompatible: skippedIncompatible += 1
            case .skippedOlder: skippedOlder += 1
            }
        }

        return (updated, skippedIncompatible, skippedOlder)
    }

    @MainActor
    private func syncLeagueStructure(
        context: ModelContext,
        since lastSync: Date?
    ) async throws -> (updated: Int, skippedIncompatible: Int, skippedOlder: Int) {
        let remoteStructures = try await cloudKitService.fetchLeagueStructureChanges(since: lastSync)

        var updated = 0
        var skippedIncompatible = 0
        var skippedOlder = 0

        for remoteStructure in remoteStructures {
            let result = try mergeLeagueStructure(remoteStructure, context: context)

            switch result {
            case .applied: updated += 1
            case .skippedIncompatible: skippedIncompatible += 1
            case .skippedOlder: skippedOlder += 1
            }
        }

        return (updated, skippedIncompatible, skippedOlder)
    }

    @MainActor
    private func syncTeamAliases(
        context: ModelContext,
        since lastSync: Date?
    ) async throws -> (updated: Int, skippedIncompatible: Int, skippedOlder: Int) {
        let remoteAliases = try await cloudKitService.fetchTeamAliasChanges(since: lastSync)

        var updated = 0
        var skippedIncompatible = 0
        var skippedOlder = 0

        for remoteAlias in remoteAliases {
            let result = try mergeTeamAlias(remoteAlias, context: context)

            switch result {
            case .applied: updated += 1
            case .skippedIncompatible: skippedIncompatible += 1
            case .skippedOlder: skippedOlder += 1
            }
        }

        return (updated, skippedIncompatible, skippedOlder)
    }

    @MainActor
    private func syncStadiumAliases(
        context: ModelContext,
        since lastSync: Date?
    ) async throws -> (updated: Int, skippedIncompatible: Int, skippedOlder: Int) {
        let remoteAliases = try await cloudKitService.fetchStadiumAliasChanges(since: lastSync)

        var updated = 0
        var skippedIncompatible = 0
        var skippedOlder = 0

        for remoteAlias in remoteAliases {
            let result = try mergeStadiumAlias(remoteAlias, context: context)

            switch result {
            case .applied: updated += 1
            case .skippedIncompatible: skippedIncompatible += 1
            case .skippedOlder: skippedOlder += 1
            }
        }

        return (updated, skippedIncompatible, skippedOlder)
    }

    // MARK: - Merge Logic

    private enum MergeResult {
        case applied
        case skippedIncompatible
        case skippedOlder
    }

    @MainActor
    private func mergeStadium(
        _ remote: Stadium,
        canonicalId: String,
        context: ModelContext
    ) throws -> MergeResult {
        // Look up existing
        let descriptor = FetchDescriptor<CanonicalStadium>(
            predicate: #Predicate { $0.canonicalId == canonicalId }
        )
        let existing = try context.fetch(descriptor).first

        if let existing = existing {
            // Preserve user fields
            let savedNickname = existing.userNickname
            let savedNotes = existing.userNotes
            let savedFavorite = existing.isFavorite

            // Update system fields
            existing.name = remote.name
            existing.city = remote.city
            existing.state = remote.state
            existing.latitude = remote.latitude
            existing.longitude = remote.longitude
            existing.capacity = remote.capacity
            existing.yearOpened = remote.yearOpened
            existing.imageURL = remote.imageURL?.absoluteString
            existing.sport = remote.sport.rawValue
            existing.source = .cloudKit
            existing.lastModified = Date()

            // Restore user fields
            existing.userNickname = savedNickname
            existing.userNotes = savedNotes
            existing.isFavorite = savedFavorite

            return .applied
        } else {
            // Insert new - let init() generate deterministic UUID from canonicalId
            let canonical = CanonicalStadium(
                canonicalId: canonicalId,
                // uuid: omitted - will be generated deterministically from canonicalId
                schemaVersion: SchemaVersion.current,
                lastModified: Date(),
                source: .cloudKit,
                name: remote.name,
                city: remote.city,
                state: remote.state,
                latitude: remote.latitude,
                longitude: remote.longitude,
                capacity: remote.capacity,
                yearOpened: remote.yearOpened,
                imageURL: remote.imageURL?.absoluteString,
                sport: remote.sport.rawValue
            )
            context.insert(canonical)
            return .applied
        }
    }

    @MainActor
    private func mergeTeam(
        _ remote: Team,
        canonicalId: String,
        stadiumCanonicalId: String,
        context: ModelContext
    ) throws -> MergeResult {
        let descriptor = FetchDescriptor<CanonicalTeam>(
            predicate: #Predicate { $0.canonicalId == canonicalId }
        )
        let existing = try context.fetch(descriptor).first

        // Stadium canonical ID is passed directly from CloudKit - no UUID lookup needed!

        if let existing = existing {
            // Preserve user fields
            let savedNickname = existing.userNickname
            let savedFavorite = existing.isFavorite

            // Update system fields
            existing.name = remote.name
            existing.abbreviation = remote.abbreviation
            existing.sport = remote.sport.rawValue
            existing.city = remote.city
            existing.stadiumCanonicalId = stadiumCanonicalId
            existing.logoURL = remote.logoURL?.absoluteString
            existing.primaryColor = remote.primaryColor
            existing.secondaryColor = remote.secondaryColor
            existing.source = .cloudKit
            existing.lastModified = Date()

            // Restore user fields
            existing.userNickname = savedNickname
            existing.isFavorite = savedFavorite

            return .applied
        } else {
            // Insert new - let init() generate deterministic UUID from canonicalId
            let canonical = CanonicalTeam(
                canonicalId: canonicalId,
                // uuid: omitted - will be generated deterministically from canonicalId
                schemaVersion: SchemaVersion.current,
                lastModified: Date(),
                source: .cloudKit,
                name: remote.name,
                abbreviation: remote.abbreviation,
                sport: remote.sport.rawValue,
                city: remote.city,
                stadiumCanonicalId: stadiumCanonicalId,
                logoURL: remote.logoURL?.absoluteString,
                primaryColor: remote.primaryColor,
                secondaryColor: remote.secondaryColor
            )
            context.insert(canonical)
            return .applied
        }
    }

    @MainActor
    private func mergeGame(
        _ remote: Game,
        canonicalId: String,
        homeTeamCanonicalId: String,
        awayTeamCanonicalId: String,
        stadiumCanonicalId: String,
        context: ModelContext
    ) throws -> MergeResult {
        let descriptor = FetchDescriptor<CanonicalGame>(
            predicate: #Predicate { $0.canonicalId == canonicalId }
        )
        let existing = try context.fetch(descriptor).first

        // All canonical IDs are passed directly from CloudKit - no UUID lookups needed!

        if let existing = existing {
            // Preserve user fields
            let savedAttending = existing.userAttending
            let savedNotes = existing.userNotes

            // Update system fields
            existing.homeTeamCanonicalId = homeTeamCanonicalId
            existing.awayTeamCanonicalId = awayTeamCanonicalId
            existing.stadiumCanonicalId = stadiumCanonicalId
            existing.dateTime = remote.dateTime
            existing.sport = remote.sport.rawValue
            existing.season = remote.season
            existing.isPlayoff = remote.isPlayoff
            existing.broadcastInfo = remote.broadcastInfo
            existing.source = .cloudKit
            existing.lastModified = Date()

            // Restore user fields
            existing.userAttending = savedAttending
            existing.userNotes = savedNotes

            return .applied
        } else {
            // Insert new - let init() generate deterministic UUID from canonicalId
            let canonical = CanonicalGame(
                canonicalId: canonicalId,
                // uuid: omitted - will be generated deterministically from canonicalId
                schemaVersion: SchemaVersion.current,
                lastModified: Date(),
                source: .cloudKit,
                homeTeamCanonicalId: homeTeamCanonicalId,
                awayTeamCanonicalId: awayTeamCanonicalId,
                stadiumCanonicalId: stadiumCanonicalId,
                dateTime: remote.dateTime,
                sport: remote.sport.rawValue,
                season: remote.season,
                isPlayoff: remote.isPlayoff,
                broadcastInfo: remote.broadcastInfo
            )
            context.insert(canonical)
            return .applied
        }
    }

    @MainActor
    private func mergeLeagueStructure(
        _ remote: LeagueStructureModel,
        context: ModelContext
    ) throws -> MergeResult {
        // Schema version check
        guard remote.schemaVersion <= SchemaVersion.current else {
            return .skippedIncompatible
        }

        let remoteId = remote.id
        let descriptor = FetchDescriptor<LeagueStructureModel>(
            predicate: #Predicate { $0.id == remoteId }
        )
        let existing = try context.fetch(descriptor).first

        if let existing = existing {
            // lastModified check
            guard remote.lastModified > existing.lastModified else {
                return .skippedOlder
            }

            // Update all fields (no user fields on LeagueStructure)
            existing.sport = remote.sport
            existing.structureTypeRaw = remote.structureTypeRaw
            existing.name = remote.name
            existing.abbreviation = remote.abbreviation
            existing.parentId = remote.parentId
            existing.displayOrder = remote.displayOrder
            existing.schemaVersion = remote.schemaVersion
            existing.lastModified = remote.lastModified

            return .applied
        } else {
            // Insert new
            context.insert(remote)
            return .applied
        }
    }

    @MainActor
    private func mergeTeamAlias(
        _ remote: TeamAlias,
        context: ModelContext
    ) throws -> MergeResult {
        // Schema version check
        guard remote.schemaVersion <= SchemaVersion.current else {
            return .skippedIncompatible
        }

        let remoteId = remote.id
        let descriptor = FetchDescriptor<TeamAlias>(
            predicate: #Predicate { $0.id == remoteId }
        )
        let existing = try context.fetch(descriptor).first

        if let existing = existing {
            // lastModified check
            guard remote.lastModified > existing.lastModified else {
                return .skippedOlder
            }

            // Update all fields (no user fields on TeamAlias)
            existing.teamCanonicalId = remote.teamCanonicalId
            existing.aliasTypeRaw = remote.aliasTypeRaw
            existing.aliasValue = remote.aliasValue
            existing.validFrom = remote.validFrom
            existing.validUntil = remote.validUntil
            existing.schemaVersion = remote.schemaVersion
            existing.lastModified = remote.lastModified

            return .applied
        } else {
            // Insert new
            context.insert(remote)
            return .applied
        }
    }

    @MainActor
    private func mergeStadiumAlias(
        _ remote: StadiumAlias,
        context: ModelContext
    ) throws -> MergeResult {
        // Schema version check
        guard remote.schemaVersion <= SchemaVersion.current else {
            return .skippedIncompatible
        }

        let remoteAliasName = remote.aliasName
        let descriptor = FetchDescriptor<StadiumAlias>(
            predicate: #Predicate { $0.aliasName == remoteAliasName }
        )
        let existing = try context.fetch(descriptor).first

        if let existing = existing {
            // lastModified check
            guard remote.lastModified > existing.lastModified else {
                return .skippedOlder
            }

            // Update all fields (no user fields on StadiumAlias)
            existing.stadiumCanonicalId = remote.stadiumCanonicalId
            existing.validFrom = remote.validFrom
            existing.validUntil = remote.validUntil
            existing.schemaVersion = remote.schemaVersion
            existing.lastModified = remote.lastModified

            return .applied
        } else {
            // Insert new
            context.insert(remote)
            return .applied
        }
    }
}
