//
//  BackgroundSyncManager.swift
//  SportsTime
//
//  Manages background tasks for nightly CloudKit sync and database cleanup.
//  Uses BGTaskScheduler to run sync operations even when app is not active.
//

import Foundation
import BackgroundTasks
import SwiftData
import os

/// Manages background refresh and processing tasks for CloudKit sync.
@MainActor
final class BackgroundSyncManager {

    enum SyncTriggerError: Error, LocalizedError {
        case modelContainerNotConfigured

        var errorDescription: String? {
            switch self {
            case .modelContainerNotConfigured:
                return "Sync is unavailable because the model container is not configured."
            }
        }
    }

    // MARK: - Task Identifiers

    /// Background app refresh task - runs periodically (system decides frequency)
    static let refreshTaskIdentifier = "com.88oakapps.SportsTime.refresh"

    /// Background processing task - runs during optimal conditions (plugged in, overnight)
    static let processingTaskIdentifier = "com.88oakapps.SportsTime.db-cleanup"

    // MARK: - Singleton

    static let shared = BackgroundSyncManager()

    // MARK: - Properties

    private var modelContainer: ModelContainer?
    private var currentCancellationToken: BackgroundTaskCancellationToken?
    private let logger = Logger(subsystem: "com.88oakapps.SportsTime", category: "BackgroundSyncManager")

    private init() {}

    // MARK: - Configuration

    /// Configure the manager with the model container.
    /// Must be called before registering tasks.
    func configure(with container: ModelContainer) {
        self.modelContainer = container
    }

    // MARK: - Task Registration

    /// Register all background tasks with the system.
    /// Must be called early in app lifecycle, before `applicationDidFinishLaunching` returns.
    func registerTasks() {
        // Register refresh task (for periodic CloudKit sync)
        BGTaskScheduler.shared.register(
            forTaskWithIdentifier: Self.refreshTaskIdentifier,
            using: nil
        ) { task in
            guard let task = task as? BGAppRefreshTask else { return }
            Task { @MainActor in
                await self.handleRefreshTask(task)
            }
        }

        // Register processing task (for overnight heavy sync/cleanup)
        BGTaskScheduler.shared.register(
            forTaskWithIdentifier: Self.processingTaskIdentifier,
            using: nil
        ) { task in
            guard let task = task as? BGProcessingTask else { return }
            Task { @MainActor in
                await self.handleProcessingTask(task)
            }
        }

        print("Background tasks registered")
    }

    // MARK: - Task Scheduling

    /// Schedule the next background refresh.
    /// System will run this periodically based on app usage patterns.
    func scheduleRefresh() {
        let request = BGAppRefreshTaskRequest(identifier: Self.refreshTaskIdentifier)

        // Request to run no earlier than 1 hour from now
        // System will actually schedule based on usage patterns, battery, network, etc.
        request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60)

        do {
            try BGTaskScheduler.shared.submit(request)
            print("Background refresh scheduled")
        } catch {
            print("Failed to schedule background refresh: \(error.localizedDescription)")
        }
    }

    /// Schedule overnight processing task for heavy sync/cleanup.
    /// Runs when device is charging and connected to WiFi.
    func scheduleProcessingTask() {
        let request = BGProcessingTaskRequest(identifier: Self.processingTaskIdentifier)

        // Schedule for overnight - earliest 2 AM
        let calendar = Calendar.current
        var components = calendar.dateComponents([.year, .month, .day], from: Date())
        components.hour = 2
        components.minute = 0

        // If it's past 2 AM, schedule for tomorrow
        var targetDate = calendar.date(from: components) ?? Date()
        if targetDate < Date() {
            targetDate = calendar.date(byAdding: .day, value: 1, to: targetDate) ?? Date()
        }

        request.earliestBeginDate = targetDate
        request.requiresNetworkConnectivity = true
        request.requiresExternalPower = false // Don't require power, but prefer it

        do {
            try BGTaskScheduler.shared.submit(request)
            print("Overnight processing task scheduled for \(targetDate)")
        } catch {
            print("Failed to schedule processing task: \(error.localizedDescription)")
        }
    }

    /// Schedule all background tasks.
    /// Call this after app finishes launching and when app enters background.
    func scheduleAllTasks() {
        scheduleRefresh()
        scheduleProcessingTask()
    }

    // MARK: - Task Handlers

    /// Handle the background refresh task.
    @MainActor
    private func handleRefreshTask(_ task: BGAppRefreshTask) async {
        logger.info("Background refresh task started")

        // Schedule the next refresh before we start (in case we get terminated)
        scheduleRefresh()

        // Create cancellation token for this task
        let cancellationToken = BackgroundTaskCancellationToken()
        currentCancellationToken = cancellationToken

        // Set up expiration handler - cancel sync gracefully
        task.expirationHandler = { [weak self] in
            self?.logger.warning("Background refresh task expiring - cancelling sync")
            cancellationToken.cancel()
        }

        guard let container = modelContainer else {
            logger.error("Background refresh: No model container configured")
            task.setTaskCompleted(success: false)
            return
        }

        do {
            let result = try await performSync(context: container.mainContext, cancellationToken: cancellationToken)
            currentCancellationToken = nil

            if result.wasCancelled {
                logger.info("Background refresh cancelled with partial progress: \(result.totalUpdated) items synced")
                task.setTaskCompleted(success: true) // Partial success - progress was saved
            } else {
                logger.info("Background refresh completed: \(result.totalUpdated) items updated")
                task.setTaskCompleted(success: true)
            }
        } catch {
            currentCancellationToken = nil
            logger.error("Background refresh failed: \(error.localizedDescription)")
            task.setTaskCompleted(success: false)
        }
    }

    /// Handle the background processing task (overnight heavy sync).
    @MainActor
    private func handleProcessingTask(_ task: BGProcessingTask) async {
        logger.info("Background processing task started")

        // Schedule the next processing task
        scheduleProcessingTask()

        // Create cancellation token for this task
        let cancellationToken = BackgroundTaskCancellationToken()
        currentCancellationToken = cancellationToken

        // Set up expiration handler - cancel sync gracefully
        task.expirationHandler = { [weak self] in
            self?.logger.warning("Background processing task expiring - cancelling sync")
            cancellationToken.cancel()
        }

        guard let container = modelContainer else {
            logger.error("Background processing: No model container configured")
            task.setTaskCompleted(success: false)
            return
        }

        do {
            // 1. Full sync from CloudKit
            let syncResult = try await performSync(context: container.mainContext, cancellationToken: cancellationToken)
            currentCancellationToken = nil

            // 2. Clean up old data (games older than 1 year) - only if sync wasn't cancelled
            var cleanupSuccess = false
            if !syncResult.wasCancelled {
                cleanupSuccess = await performCleanup(context: container.mainContext)
            }

            if syncResult.wasCancelled {
                logger.info("Background processing cancelled with partial progress: \(syncResult.totalUpdated) items synced")
                task.setTaskCompleted(success: true) // Partial success
            } else {
                logger.info("Background processing completed: sync=\(syncResult.totalUpdated), cleanup=\(cleanupSuccess)")
                task.setTaskCompleted(success: true)
            }
        } catch {
            currentCancellationToken = nil
            logger.error("Background processing failed: \(error.localizedDescription)")
            task.setTaskCompleted(success: false)
        }
    }

    // MARK: - Sync Operations

    /// Perform CloudKit sync operation.
    /// - Parameters:
    ///   - context: The ModelContext to use
    ///   - cancellationToken: Optional token for graceful cancellation
    /// - Returns: The sync result with counts and cancellation status
    @MainActor
    private func performSync(
        context: ModelContext,
        cancellationToken: SyncCancellationToken? = nil
    ) async throws -> CanonicalSyncService.SyncResult {
        let syncService = CanonicalSyncService()

        do {
            let result = try await syncService.syncAll(context: context, cancellationToken: cancellationToken)

            // Reload DataProvider if data changed
            if !result.isEmpty || result.wasCancelled {
                await AppDataProvider.shared.loadInitialData()
                logger.info("Sync completed: \(result.totalUpdated) items updated, cancelled: \(result.wasCancelled)")
            }

            return result

        } catch CanonicalSyncService.SyncError.cloudKitUnavailable {
            // No network - this is expected, not a failure
            logger.info("CloudKit unavailable (offline)")
            return CanonicalSyncService.SyncResult(
                stadiumsUpdated: 0, teamsUpdated: 0, gamesUpdated: 0,
                leagueStructuresUpdated: 0, teamAliasesUpdated: 0, stadiumAliasesUpdated: 0,
                sportsUpdated: 0, skippedIncompatible: 0, skippedOlder: 0,
                duration: 0, wasCancelled: false
            )
        } catch CanonicalSyncService.SyncError.syncAlreadyInProgress {
            // Another sync is running - that's fine
            logger.info("Sync already in progress")
            return CanonicalSyncService.SyncResult(
                stadiumsUpdated: 0, teamsUpdated: 0, gamesUpdated: 0,
                leagueStructuresUpdated: 0, teamAliasesUpdated: 0, stadiumAliasesUpdated: 0,
                sportsUpdated: 0, skippedIncompatible: 0, skippedOlder: 0,
                duration: 0, wasCancelled: false
            )
        }
    }

    /// Trigger sync from network restoration (called by NetworkMonitor).
    /// This is a non-background task sync, so no cancellation token is needed.
    @MainActor
    func triggerSyncFromNetworkRestoration() async {
        guard let container = modelContainer else {
            logger.error("Network sync: No model container configured")
            return
        }

        logger.info("Triggering sync from network restoration")

        do {
            let result = try await performSync(context: container.mainContext)

            if !result.isEmpty {
                logger.info("Network restoration sync completed: \(result.totalUpdated) items updated")
            } else {
                logger.info("Network restoration sync: no updates needed")
            }
        } catch {
            logger.error("Network restoration sync failed: \(error.localizedDescription)")
        }
    }

    /// Trigger sync from a CloudKit push notification.
    /// Returns true if local data changed and UI should refresh.
    @MainActor
    func triggerSyncFromPushNotification(subscriptionID: String?) async -> Bool {
        guard let container = modelContainer else {
            logger.error("Push sync: No model container configured")
            return false
        }

        if let subscriptionID {
            logger.info("Triggering sync from CloudKit push (\(subscriptionID, privacy: .public))")
        } else {
            logger.info("Triggering sync from CloudKit push")
        }

        do {
            let result = try await performSync(context: container.mainContext)
            return !result.isEmpty || result.wasCancelled
        } catch {
            logger.error("Push-triggered sync failed: \(error.localizedDescription)")
            return false
        }
    }

    /// Apply a targeted local deletion for records removed remotely.
    /// This covers deletions which are not returned by date-based delta queries.
    @MainActor
    func applyDeletionHint(recordType: String?, recordName: String) async -> Bool {
        guard let container = modelContainer else {
            logger.error("Deletion hint: No model container configured")
            return false
        }

        let context = container.mainContext
        var changed = false

        do {
            switch recordType {
            case CKRecordType.game:
                let descriptor = FetchDescriptor<CanonicalGame>(
                    predicate: #Predicate<CanonicalGame> { game in
                        game.canonicalId == recordName && game.deprecatedAt == nil
                    }
                )
                if let game = try context.fetch(descriptor).first {
                    game.deprecatedAt = Date()
                    game.deprecationReason = "Deleted in CloudKit"
                    changed = true
                }

            case CKRecordType.team:
                let descriptor = FetchDescriptor<CanonicalTeam>(
                    predicate: #Predicate<CanonicalTeam> { team in
                        team.canonicalId == recordName && team.deprecatedAt == nil
                    }
                )
                if let team = try context.fetch(descriptor).first {
                    team.deprecatedAt = Date()
                    team.deprecationReason = "Deleted in CloudKit"
                    changed = true
                }

                // Avoid dangling schedule entries when a referenced team disappears.
                let impactedGames = FetchDescriptor<CanonicalGame>(
                    predicate: #Predicate<CanonicalGame> { game in
                        game.deprecatedAt == nil &&
                        (game.homeTeamCanonicalId == recordName || game.awayTeamCanonicalId == recordName)
                    }
                )
                for game in try context.fetch(impactedGames) {
                    game.deprecatedAt = Date()
                    game.deprecationReason = "Dependent team deleted in CloudKit"
                    changed = true
                }

            case CKRecordType.stadium:
                let descriptor = FetchDescriptor<CanonicalStadium>(
                    predicate: #Predicate<CanonicalStadium> { stadium in
                        stadium.canonicalId == recordName && stadium.deprecatedAt == nil
                    }
                )
                if let stadium = try context.fetch(descriptor).first {
                    stadium.deprecatedAt = Date()
                    stadium.deprecationReason = "Deleted in CloudKit"
                    changed = true
                }

                // Avoid dangling schedule entries when a referenced stadium disappears.
                let impactedGames = FetchDescriptor<CanonicalGame>(
                    predicate: #Predicate<CanonicalGame> { game in
                        game.deprecatedAt == nil && game.stadiumCanonicalId == recordName
                    }
                )
                for game in try context.fetch(impactedGames) {
                    game.deprecatedAt = Date()
                    game.deprecationReason = "Dependent stadium deleted in CloudKit"
                    changed = true
                }

            case CKRecordType.leagueStructure:
                let descriptor = FetchDescriptor<LeagueStructureModel>(
                    predicate: #Predicate<LeagueStructureModel> { $0.id == recordName }
                )
                let records = try context.fetch(descriptor)
                for record in records {
                    context.delete(record)
                    changed = true
                }

            case CKRecordType.teamAlias:
                let descriptor = FetchDescriptor<TeamAlias>(
                    predicate: #Predicate<TeamAlias> { $0.id == recordName }
                )
                let records = try context.fetch(descriptor)
                for record in records {
                    context.delete(record)
                    changed = true
                }

            case CKRecordType.stadiumAlias:
                let descriptor = FetchDescriptor<StadiumAlias>(
                    predicate: #Predicate<StadiumAlias> { $0.aliasName == recordName }
                )
                let records = try context.fetch(descriptor)
                for record in records {
                    context.delete(record)
                    changed = true
                }

            case CKRecordType.sport:
                let descriptor = FetchDescriptor<CanonicalSport>(
                    predicate: #Predicate<CanonicalSport> { $0.id == recordName }
                )
                if let sport = try context.fetch(descriptor).first, sport.isActive {
                    sport.isActive = false
                    sport.lastModified = Date()
                    changed = true
                }

            default:
                break
            }

            guard changed else { return false }

            try context.save()
            await AppDataProvider.shared.loadInitialData()
            logger.info("Applied CloudKit deletion hint for \(recordType ?? "unknown", privacy: .public):\(recordName, privacy: .public)")
            return true

        } catch {
            logger.error("Failed to apply deletion hint (\(recordType ?? "unknown", privacy: .public):\(recordName, privacy: .public)): \(error.localizedDescription)")
            return false
        }
    }

    /// Trigger a manual full sync from settings or other explicit user action.
    @MainActor
    func triggerManualSync() async throws -> CanonicalSyncService.SyncResult {
        guard let container = modelContainer else {
            throw SyncTriggerError.modelContainerNotConfigured
        }

        let context = container.mainContext
        let syncService = CanonicalSyncService()
        let syncState = SyncState.current(in: context)

        if !syncState.syncEnabled {
            syncService.resumeSync(context: context)
        }

        return try await performSync(context: context)
    }

    /// Register CloudKit subscriptions for canonical data updates.
    @MainActor
    func ensureCanonicalSubscriptions() async {
        do {
            try await CloudKitService.shared.subscribeToAllUpdates()
            logger.info("CloudKit subscriptions ensured for canonical sync")
        } catch {
            logger.error("Failed to register CloudKit subscriptions: \(error.localizedDescription)")
        }
    }

    /// Clean up old game data (older than 1 year).
    @MainActor
    private func performCleanup(context: ModelContext) async -> Bool {
        let oneYearAgo = Calendar.current.date(byAdding: .year, value: -1, to: Date()) ?? Date()

        do {
            // Fetch old games
            let descriptor = FetchDescriptor<CanonicalGame>(
                predicate: #Predicate<CanonicalGame> { game in
                    game.dateTime < oneYearAgo && game.deprecatedAt == nil
                }
            )

            let oldGames = try context.fetch(descriptor)

            guard !oldGames.isEmpty else {
                return false
            }

            // Soft-delete old games (mark as deprecated)
            for game in oldGames {
                game.deprecatedAt = Date()
                game.deprecationReason = "Auto-archived: game older than 1 year"
            }

            try context.save()
            print("Background cleanup: Archived \(oldGames.count) old games")
            return true

        } catch {
            print("Background cleanup failed: \(error.localizedDescription)")
            return false
        }
    }
}

// MARK: - Debug Helpers

#if DEBUG
extension BackgroundSyncManager {
    /// Manually trigger refresh task for testing.
    /// Run in debugger: `e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.88oakapps.SportsTime.refresh"]`
    func debugTriggerRefresh() {
        print("Debug: Use lldb command to trigger background refresh:")
        print("e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@\"com.88oakapps.SportsTime.refresh\"]")
    }

    /// Manually trigger processing task for testing.
    func debugTriggerProcessing() {
        print("Debug: Use lldb command to trigger background processing:")
        print("e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@\"com.88oakapps.SportsTime.db-cleanup\"]")
    }
}
#endif
