import Combine
import Foundation
import HealthKit
import SwiftUI
import UniformTypeIdentifiers

#if canImport(WidgetKit)
    import WidgetKit
#endif

#if canImport(ActivityKit)
    import ActivityKit
#endif

@MainActor
class ClimbingDataManager: ObservableObject {

    static let shared = ClimbingDataManager()

    @Published var gyms: [Gym] = []
    @Published var problems: [Problem] = []
    @Published var sessions: [ClimbSession] = []
    @Published var attempts: [Attempt] = []
    @Published var activeSession: ClimbSession?
    @Published var isLoading = false
    @Published var errorMessage: String?
    @Published var successMessage: String?

    private let userDefaults = UserDefaults.standard
    private let sharedUserDefaults = UserDefaults(suiteName: "group.com.atridad.Ascently")
    private let encoder = JSONEncoder()
    private let decoder = JSONDecoder()

    // Flag to track if migration has been performed
    private let migrationKey = "ascently_data_migrated_from_openclimb"
    nonisolated(unsafe) private var liveActivityObserver: NSObjectProtocol?
    nonisolated(unsafe) private var migrationObserver: NSObjectProtocol?

    let syncService = SyncService()
    let healthKitService = HealthKitService.shared
    let musicService = MusicService.shared

    @Published var isSyncing = false
    private enum Keys {
        static let gyms = "ascently_gyms"
        static let problems = "ascently_problems"
        static let sessions = "ascently_sessions"
        static let attempts = "ascently_attempts"
        static let activeSession = "ascently_active_session"
        static let deletedItems = "ascently_deleted_items"
    }

    // Legacy keys for migration
    private enum LegacyKeys {
        static let gyms = "openclimb_gyms"
        static let problems = "openclimb_problems"
        static let sessions = "openclimb_sessions"
        static let attempts = "openclimb_attempts"
        static let activeSession = "openclimb_active_session"
        static let deletedItems = "openclimb_deleted_items"
    }

    // Widget data models
    private struct WidgetAttempt: Codable {
        let id: String
        let sessionId: String
        let problemId: String
        let timestamp: Date
        let result: String
    }

    private struct WidgetSession: Codable {
        let id: String
        let gymId: String
        let date: Date
        let status: String
    }

    private struct WidgetGym: Codable {
        let id: String
        let name: String
    }

    fileprivate init() {
        _ = ImageManager.shared
        migrateFromOpenClimbIfNeeded()
        loadAllData()
        setupLiveActivityNotifications()
        setupMigrationNotifications()

        // Keep our published isSyncing in sync with syncService.isSyncing
        syncService.$isSyncing
            .assign(to: &$isSyncing)

        Task {
            try? await Task.sleep(for: .seconds(2))
            await performImageMaintenance()

            // Check if we need to restart Live Activity for active session
            await checkAndRestartLiveActivity()
        }
    }

    deinit {
        if let observer = liveActivityObserver {
            NotificationCenter.default.removeObserver(observer)
        }
        if let observer = migrationObserver {
            NotificationCenter.default.removeObserver(observer)
        }
    }

    /// Migrate data from OpenClimb keys to Ascently keys
    private func migrateFromOpenClimbIfNeeded() {
        // Check if migration has already been performed
        if userDefaults.bool(forKey: migrationKey) {
            return
        }

        AppLogger.info(
            "Starting migration from OpenClimb to Ascently keys...", tag: LogTag.climbingData)
        var migrationCount = 0

        // Migrate each data type if it exists in old format but not in new format
        let migrations = [
            (LegacyKeys.gyms, Keys.gyms),
            (LegacyKeys.problems, Keys.problems),
            (LegacyKeys.sessions, Keys.sessions),
            (LegacyKeys.attempts, Keys.attempts),
            (LegacyKeys.activeSession, Keys.activeSession),
            (LegacyKeys.deletedItems, Keys.deletedItems),
        ]

        for (oldKey, newKey) in migrations {
            if let oldData = userDefaults.data(forKey: oldKey),
                userDefaults.data(forKey: newKey) == nil
            {
                userDefaults.set(oldData, forKey: newKey)
                userDefaults.removeObject(forKey: oldKey)
                migrationCount += 1
                AppLogger.info("Migrated: \(oldKey) → \(newKey)", tag: LogTag.climbingData)
            }
        }

        // Also migrate shared UserDefaults for widgets
        if let sharedDefaults = sharedUserDefaults {
            for (oldKey, newKey) in migrations {
                if let oldData = sharedDefaults.data(forKey: oldKey),
                    sharedDefaults.data(forKey: newKey) == nil
                {
                    sharedDefaults.set(oldData, forKey: newKey)
                    sharedDefaults.removeObject(forKey: oldKey)
                    AppLogger.info(
                        "Migrated shared: \(oldKey) → \(newKey)", tag: LogTag.climbingData)
                }
            }
        }

        // Migrate DataStateManager keys
        let legacyDataStateKey = "openclimb_data_last_modified"
        let newDataStateKey = "ascently_data_last_modified"
        if let lastModified = userDefaults.string(forKey: legacyDataStateKey),
            userDefaults.string(forKey: newDataStateKey) == nil
        {
            userDefaults.set(lastModified, forKey: newDataStateKey)
            userDefaults.removeObject(forKey: legacyDataStateKey)
            migrationCount += 1
            AppLogger.info("Migrated data state timestamp", tag: LogTag.climbingData)
        }

        // Mark migration as completed
        userDefaults.set(true, forKey: migrationKey)

        if migrationCount > 0 {
            AppLogger.info(
                "Migration completed! Migrated \(migrationCount) data items from OpenClimb to Ascently",
                tag: LogTag.climbingData
            )
        } else {
            AppLogger.info("No OpenClimb data found to migrate", tag: LogTag.climbingData)
        }
    }

    private func loadAllData() {
        loadGyms()
        loadProblems()
        loadSessions()
        loadAttempts()
        loadActiveSession()

        // Clean up orphaned data after loading
        cleanupOrphanedData()
    }

    private func loadGyms() {
        if let data = userDefaults.data(forKey: Keys.gyms),
            let loadedGyms = try? decoder.decode([Gym].self, from: data)
        {
            self.gyms = loadedGyms
        }
    }

    private func loadProblems() {
        if let data = userDefaults.data(forKey: Keys.problems),
            let loadedProblems = try? decoder.decode([Problem].self, from: data)
        {
            self.problems = loadedProblems
        }
    }

    private func loadSessions() {
        if let data = userDefaults.data(forKey: Keys.sessions),
            let loadedSessions = try? decoder.decode([ClimbSession].self, from: data)
        {
            self.sessions = loadedSessions
        }
    }

    private func loadAttempts() {
        if let data = userDefaults.data(forKey: Keys.attempts),
            let loadedAttempts = try? decoder.decode([Attempt].self, from: data)
        {
            self.attempts = loadedAttempts
        }
    }

    private func loadActiveSession() {
        if let data = userDefaults.data(forKey: Keys.activeSession),
            let loadedActiveSession = try? decoder.decode(ClimbSession.self, from: data)
        {
            self.activeSession = loadedActiveSession
        }
    }

    internal func saveGyms() {
        if let data = try? encoder.encode(gyms) {
            userDefaults.set(data, forKey: Keys.gyms)
            // Share with widget - convert to widget format
            let widgetGyms = gyms.map { gym in
                WidgetGym(id: gym.id.uuidString, name: gym.name)
            }
            if let widgetData = try? encoder.encode(widgetGyms) {
                sharedUserDefaults?.set(widgetData, forKey: Keys.gyms)
            }
        }
    }

    internal func saveProblems() {
        if let data = try? encoder.encode(problems) {
            userDefaults.set(data, forKey: Keys.problems)
            // Share with widget
            sharedUserDefaults?.set(data, forKey: Keys.problems)
        }
    }

    internal func saveSessions() {
        if let data = try? encoder.encode(sessions) {
            userDefaults.set(data, forKey: Keys.sessions)
            // Share with widget - convert to widget format
            let widgetSessions = sessions.map { session in
                WidgetSession(
                    id: session.id.uuidString,
                    gymId: session.gymId.uuidString,
                    date: session.date,
                    status: session.status.rawValue
                )
            }
            if let widgetData = try? encoder.encode(widgetSessions) {
                sharedUserDefaults?.set(widgetData, forKey: Keys.sessions)
            }
        }
    }

    internal func saveAttempts() {
        if let data = try? encoder.encode(attempts) {
            userDefaults.set(data, forKey: Keys.attempts)
            // Share with widget - convert to widget format
            let widgetAttempts = attempts.map { attempt in
                WidgetAttempt(
                    id: attempt.id.uuidString,
                    sessionId: attempt.sessionId.uuidString,
                    problemId: attempt.problemId.uuidString,
                    timestamp: attempt.timestamp,
                    result: attempt.result.rawValue
                )
            }
            if let widgetData = try? encoder.encode(widgetAttempts) {
                sharedUserDefaults?.set(widgetData, forKey: Keys.attempts)
            }
            // Update widget timeline
            updateWidgetTimeline()
        }
    }

    internal func saveActiveSession() {
        if let activeSession = activeSession,
            let data = try? encoder.encode(activeSession)
        {
            userDefaults.set(data, forKey: Keys.activeSession)
        } else {
            userDefaults.removeObject(forKey: Keys.activeSession)
        }
    }

    func addGym(_ gym: Gym) {
        gyms.append(gym)
        saveGyms()
        DataStateManager.shared.updateDataState()

        // Trigger auto-sync if enabled
        syncService.triggerAutoSync(dataManager: self)
    }

    func updateGym(_ gym: Gym) {
        if let index = gyms.firstIndex(where: { $0.id == gym.id }) {
            gyms[index] = gym
            saveGyms()
            DataStateManager.shared.updateDataState()

            // Trigger auto-sync if enabled
            syncService.triggerAutoSync(dataManager: self)
        }
    }

    func deleteGym(_ gym: Gym) {
        // Delete associated problems and their attempts first
        let problemsToDelete = problems.filter { $0.gymId == gym.id }
        for problem in problemsToDelete {
            deleteProblem(problem)
        }

        // Delete associated sessions and their attempts
        let sessionsToDelete = sessions.filter { $0.gymId == gym.id }
        for session in sessionsToDelete {
            deleteSession(session)
        }

        // Delete the gym
        gyms.removeAll { $0.id == gym.id }
        trackDeletion(itemId: gym.id.uuidString, itemType: "gym")
        saveGyms()
        DataStateManager.shared.updateDataState()

        // Trigger auto-sync if enabled
        syncService.triggerAutoSync(dataManager: self)
    }

    func gym(withId id: UUID) -> Gym? {
        return gyms.first { $0.id == id }
    }

    func addProblem(_ problem: Problem) {
        problems.append(problem)
        saveProblems()
        DataStateManager.shared.updateDataState()

        // Trigger auto-sync if enabled
        syncService.triggerAutoSync(dataManager: self)
    }

    func updateProblem(_ problem: Problem) {
        if let index = problems.firstIndex(where: { $0.id == problem.id }) {
            problems[index] = problem
            saveProblems()
            DataStateManager.shared.updateDataState()

            // Trigger auto-sync if enabled
            syncService.triggerAutoSync(dataManager: self)
        }
    }

    func deleteProblem(_ problem: Problem) {
        // Track deletion of the problem
        trackDeletion(itemId: problem.id.uuidString, itemType: "problem")

        // Find and track all attempts for this problem as deleted
        let problemAttempts = attempts.filter { $0.problemId == problem.id }
        for attempt in problemAttempts {
            trackDeletion(itemId: attempt.id.uuidString, itemType: "attempt")
        }

        // Delete associated attempts
        attempts.removeAll { $0.problemId == problem.id }
        saveAttempts()

        // Delete associated images
        ImageManager.shared.deleteImages(atPaths: problem.imagePaths)

        // Delete the problem
        problems.removeAll { $0.id == problem.id }
        saveProblems()
        DataStateManager.shared.updateDataState()

        // Trigger auto-sync if enabled
        syncService.triggerAutoSync(dataManager: self)
    }

    func problem(withId id: UUID) -> Problem? {
        return problems.first { $0.id == id }
    }

    func problems(forGym gymId: UUID) -> [Problem] {
        return problems.filter { $0.gymId == gymId }
    }

    func activeProblems(forGym gymId: UUID) -> [Problem] {
        return problems.filter { $0.gymId == gymId && $0.isActive }
    }

    @discardableResult
    func startSession(gymId: UUID, notes: String? = nil) async -> ClimbSession? {
        if let currentActive = activeSession {
            await endSession(currentActive.id)
        }

        let newSession = ClimbSession(gymId: gymId, notes: notes)
        activeSession = newSession
        sessions.append(newSession)

        saveActiveSession()
        saveSessions()
        DataStateManager.shared.updateDataState()

        if let gym = gym(withId: gymId) {
            await LiveActivityManager.shared.startLiveActivity(
                for: newSession,
                gymName: gym.name)
        }

        musicService.resetSessionPlaybackState()
        musicService.playSelectedPlaylistIfHeadphonesConnected()

        if healthKitService.isEnabled {
            do {
                try await healthKitService.startWorkout(
                    startDate: newSession.startTime ?? Date(),
                    sessionId: newSession.id)
            } catch {
                AppLogger.error(
                    "Failed to start HealthKit workout: \(error.localizedDescription)",
                    tag: LogTag.climbingData)
            }
        }

        return newSession
    }

    @discardableResult
    func endSession(_ sessionId: UUID) async -> ClimbSession? {
        guard
            let session = sessions.first(where: { $0.id == sessionId && $0.status == .active }),
            let index = sessions.firstIndex(where: { $0.id == sessionId })
        else {
            return nil
        }

        let completedSession = session.completed()
        sessions[index] = completedSession

        if activeSession?.id == sessionId {
            activeSession = nil
        }

        saveActiveSession()
        saveSessions()
        DataStateManager.shared.updateDataState()

        // Trigger auto-sync if enabled
        syncService.triggerAutoSync(dataManager: self)

        await LiveActivityManager.shared.endLiveActivity()

        if UserDefaults.standard.bool(forKey: "isAutoStopMusicEnabled") {
            musicService.stopPlaybackIfEnabled()
        }
        musicService.stopPlaybackIfEnabled()

        if healthKitService.isEnabled {
            do {
                try await healthKitService.endWorkout(
                    endDate: completedSession.endTime ?? Date())
            } catch {
                AppLogger.error(
                    "Failed to end HealthKit workout: \(error.localizedDescription)",
                    tag: LogTag.climbingData)
            }
        }

        return completedSession
    }

    func updateSession(_ session: ClimbSession) {
        if let index = sessions.firstIndex(where: { $0.id == session.id }) {
            sessions[index] = session

            if activeSession?.id == session.id {
                activeSession = session
                saveActiveSession()
            }

            saveSessions()
            DataStateManager.shared.updateDataState()

            // Update Live Activity when session is updated
            updateLiveActivityForActiveSession()

            // Only trigger sync if session is completed
            if session.status != .active {
                syncService.triggerAutoSync(dataManager: self)
            }
        }
    }

    func deleteSession(_ session: ClimbSession) {
        // Track deletion of the session
        trackDeletion(itemId: session.id.uuidString, itemType: "session")

        // Find and track all attempts for this session as deleted
        let sessionAttempts = attempts.filter { $0.sessionId == session.id }
        for attempt in sessionAttempts {
            trackDeletion(itemId: attempt.id.uuidString, itemType: "attempt")
        }

        // Delete associated attempts
        attempts.removeAll { $0.sessionId == session.id }
        saveAttempts()

        // If this is the active session, clear it
        if activeSession?.id == session.id {
            activeSession = nil
            saveActiveSession()
        }

        // Delete the session
        sessions.removeAll { $0.id == session.id }
        saveSessions()
        DataStateManager.shared.updateDataState()

        // Update Live Activity when session is deleted
        updateLiveActivityForActiveSession()

        // Trigger auto-sync if enabled
        syncService.triggerAutoSync(dataManager: self)
    }

    func session(withId id: UUID) -> ClimbSession? {
        return sessions.first { $0.id == id }
    }

    func sessions(forGym gymId: UUID) -> [ClimbSession] {
        return sessions.filter { $0.gymId == gymId }
    }

    func getLastUsedGym() -> Gym? {
        let recentSessions = sessions.sorted { $0.date > $1.date }
        guard let lastSession = recentSessions.first else { return nil }
        return gym(withId: lastSession.gymId)
    }

    func addAttempt(_ attempt: Attempt) {
        attempts.append(attempt)
        saveAttempts()
        DataStateManager.shared.updateDataState()

        // Update Live Activity when new attempt is added
        updateLiveActivityForActiveSession()
    }

    func updateAttempt(_ attempt: Attempt) {
        if let index = attempts.firstIndex(where: { $0.id == attempt.id }) {
            attempts[index] = attempt
            saveAttempts()
            DataStateManager.shared.updateDataState()

            // Update Live Activity when attempt is updated
            updateLiveActivityForActiveSession()
        }
    }

    func deleteAttempt(_ attempt: Attempt) {
        attempts.removeAll { $0.id == attempt.id }
        trackDeletion(itemId: attempt.id.uuidString, itemType: "attempt")
        saveAttempts()
        DataStateManager.shared.updateDataState()

        // Update Live Activity when attempt is deleted
        updateLiveActivityForActiveSession()
    }

    func attempts(forSession sessionId: UUID) -> [Attempt] {
        return attempts.filter { $0.sessionId == sessionId }.sorted { $0.timestamp < $1.timestamp }
    }

    // MARK: - Deletion Tracking

    private func trackDeletion(itemId: String, itemType: String) {
        let deletion = DeletedItem(
            id: itemId,
            type: itemType,
            deletedAt: ISO8601DateFormatter().string(from: Date())
        )

        var currentDeletions = getDeletedItems()
        currentDeletions.append(deletion)

        if let data = try? encoder.encode(currentDeletions) {
            userDefaults.set(data, forKey: Keys.deletedItems)
        }
    }

    func getDeletedItems() -> [DeletedItem] {
        guard let data = userDefaults.data(forKey: Keys.deletedItems),
            let deletions = try? decoder.decode([DeletedItem].self, from: data)
        else {
            return []
        }
        return deletions
    }

    func clearDeletedItems() {
        userDefaults.removeObject(forKey: Keys.deletedItems)
    }

    func cleanupOldDeletions() {
        guard let data = userDefaults.data(forKey: Keys.deletedItems),
            let deletions = try? decoder.decode([DeletedItem].self, from: data)
        else {
            return
        }

        let cutoffDate = Date().addingTimeInterval(-90 * 24 * 60 * 60) // 90 days ago
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]

        let validDeletions = deletions.filter { item in
            if let date = formatter.date(from: item.deletedAt) {
                return date > cutoffDate
            }
            return false
        }

        if validDeletions.count < deletions.count {
            if let encodedData = try? encoder.encode(validDeletions) {
                userDefaults.set(encodedData, forKey: Keys.deletedItems)
                AppLogger.info(
                    "Cleaned up \(deletions.count - validDeletions.count) old deletion records",
                    tag: "ClimbingDataManager")
            }
        }
    }

    func attempts(forProblem problemId: UUID) -> [Attempt] {
        return attempts.filter { $0.problemId == problemId }.sorted { $0.timestamp > $1.timestamp }
    }

    func successfulAttempts(forProblem problemId: UUID) -> [Attempt] {
        return attempts.filter { $0.problemId == problemId && $0.result.isSuccessful }
    }

    func completedSessions() -> [ClimbSession] {
        return sessions.filter { $0.status == .completed }
    }

    func totalAttempts() -> Int {
        return attempts.count
    }

    func successfulAttempts() -> Int {
        return attempts.filter { $0.result.isSuccessful }.count
    }

    func completedProblems() -> Int {
        let completedProblemIds = Set(
            attempts.filter { $0.result.isSuccessful }.map { $0.problemId })
        return completedProblemIds.count
    }

    func favoriteGym() -> Gym? {
        let gymSessionCounts = Dictionary(grouping: sessions, by: { $0.gymId })
            .mapValues { $0.count }

        guard let mostUsedGymId = gymSessionCounts.max(by: { $0.value < $1.value })?.key else {
            return nil
        }

        return gym(withId: mostUsedGymId)
    }

    private func cleanupOrphanedData() {
        cleanupOldDeletions()
        let validSessionIds = Set(sessions.map { $0.id })
        let validProblemIds = Set(problems.map { $0.id })
        let validGymIds = Set(gyms.map { $0.id })

        let initialAttemptCount = attempts.count

        // Remove attempts that reference deleted sessions or problems
        let orphanedAttempts = attempts.filter { attempt in
            !validSessionIds.contains(attempt.sessionId)
                || !validProblemIds.contains(attempt.problemId)
        }

        if !orphanedAttempts.isEmpty {
            AppLogger.info(
                "🧹 Cleaning up \(orphanedAttempts.count) orphaned attempts",
                tag: LogTag.climbingData)

            // Track these as deleted to prevent sync from re-introducing them
            for attempt in orphanedAttempts {
                trackDeletion(itemId: attempt.id.uuidString, itemType: "attempt")
            }

            // Remove orphaned attempts
            attempts.removeAll { attempt in
                !validSessionIds.contains(attempt.sessionId)
                    || !validProblemIds.contains(attempt.problemId)
            }
        }

        // Remove duplicate attempts (same session, problem, and timestamp within 1 second)
        var seenAttempts: Set<String> = []
        var duplicateIds: [UUID] = []

        for attempt in attempts.sorted(by: { $0.timestamp < $1.timestamp }) {
            // Create a unique key based on session, problem, and rounded timestamp
            let timestampKey = Int(attempt.timestamp.timeIntervalSince1970)
            let key =
                "\(attempt.sessionId.uuidString)_\(attempt.problemId.uuidString)_\(timestampKey)"

            if seenAttempts.contains(key) {
                duplicateIds.append(attempt.id)
                AppLogger.info("🧹 Found duplicate attempt: \(attempt.id)", tag: LogTag.climbingData)
            } else {
                seenAttempts.insert(key)
            }
        }

        if !duplicateIds.isEmpty {
            AppLogger.info(
                "🧹 Removing \(duplicateIds.count) duplicate attempts", tag: LogTag.climbingData)

            // Track duplicates as deleted
            for attemptId in duplicateIds {
                trackDeletion(itemId: attemptId.uuidString, itemType: "attempt")
            }

            // Remove duplicates
            attempts.removeAll { duplicateIds.contains($0.id) }
        }

        if initialAttemptCount != attempts.count {
            saveAttempts()
            let removedCount = initialAttemptCount - attempts.count
            AppLogger.info(
                "Cleanup complete. Removed \(removedCount) attempts. Remaining: \(attempts.count)",
                tag: LogTag.climbingData
            )
        }

        // Remove problems that reference deleted gyms
        let orphanedProblems = problems.filter { problem in
            !validGymIds.contains(problem.gymId)
        }

        if !orphanedProblems.isEmpty {
            AppLogger.info(
                "🧹 Cleaning up \(orphanedProblems.count) orphaned problems",
                tag: LogTag.climbingData)

            for problem in orphanedProblems {
                trackDeletion(itemId: problem.id.uuidString, itemType: "problem")
            }

            problems.removeAll { problem in
                !validGymIds.contains(problem.gymId)
            }

            saveProblems()
        }

        // Remove sessions that reference deleted gyms
        let orphanedSessions = sessions.filter { session in
            !validGymIds.contains(session.gymId)
        }

        if !orphanedSessions.isEmpty {
            AppLogger.info(
                "🧹 Cleaning up \(orphanedSessions.count) orphaned sessions",
                tag: LogTag.climbingData)

            for session in orphanedSessions {
                trackDeletion(itemId: session.id.uuidString, itemType: "session")
            }

            sessions.removeAll { session in
                !validGymIds.contains(session.gymId)
            }

            saveSessions()
        }
    }

    func validateDataIntegrity() -> String {
        let validSessionIds = Set(sessions.map { $0.id })
        let validProblemIds = Set(problems.map { $0.id })
        let validGymIds = Set(gyms.map { $0.id })

        let orphanedAttempts = attempts.filter { attempt in
            !validSessionIds.contains(attempt.sessionId)
                || !validProblemIds.contains(attempt.problemId)
        }

        let orphanedProblems = problems.filter { problem in
            !validGymIds.contains(problem.gymId)
        }

        let orphanedSessions = sessions.filter { session in
            !validGymIds.contains(session.gymId)
        }

        var report = "Data Integrity Report:\n"
        report += "---------------------\n"
        report += "Gyms: \(gyms.count)\n"
        report += "Problems: \(problems.count)\n"
        report += "Sessions: \(sessions.count)\n"
        report += "Attempts: \(attempts.count)\n"
        report += "\nOrphaned Data:\n"
        report += "Orphaned Attempts: \(orphanedAttempts.count)\n"
        report += "Orphaned Problems: \(orphanedProblems.count)\n"
        report += "Orphaned Sessions: \(orphanedSessions.count)\n"

        if orphanedAttempts.isEmpty && orphanedProblems.isEmpty && orphanedSessions.isEmpty {
            report += "\nNo integrity issues found"
        } else {
            report += "\nIssues found - run cleanup to fix"
        }

        return report
    }

    func manualDataCleanup() {
        cleanupOrphanedData()
        successMessage = "Data cleanup completed"
        clearMessageAfterDelay()
    }

    func resetAllData(showSuccessMessage: Bool = true) {
        gyms.removeAll()
        problems.removeAll()
        sessions.removeAll()
        attempts.removeAll()
        activeSession = nil

        userDefaults.removeObject(forKey: Keys.gyms)
        userDefaults.removeObject(forKey: Keys.problems)
        userDefaults.removeObject(forKey: Keys.sessions)
        userDefaults.removeObject(forKey: Keys.attempts)
        userDefaults.removeObject(forKey: Keys.activeSession)

        DataStateManager.shared.reset()

        if showSuccessMessage {
            successMessage = "All data has been reset"
            clearMessageAfterDelay()
        }
    }

    func exportData() async -> Data? {
        do {
            // Create backup objects on main thread (they access MainActor-isolated properties)
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"

            let exportData = ClimbDataBackup(
                exportedAt: dateFormatter.string(from: Date()),
                version: "2.0",
                formatVersion: "2.0",
                gyms: gyms.map { BackupGym(from: $0) },
                problems: problems.map { BackupProblem(from: $0) },
                sessions: sessions.map { BackupClimbSession(from: $0) },
                attempts: attempts.map { BackupAttempt(from: $0) }
            )

            // Get image manager path info on main thread
            let imagesDirectory = ImageManager.shared.imagesDirectory.path
            let problemsForImages = problems

            // Move heavy I/O operations to background thread
            let logTag = LogTag.climbingData
            let zipData = try await Task.detached(priority: .userInitiated) {
                // Collect actual image paths from disk for the ZIP
                let imageSummary = Self.collectReferencedImagePathsStatic(
                    problems: problemsForImages,
                    imagesDirectory: imagesDirectory)
                let referencedImagePaths = imageSummary.paths

                await MainActor.run {
                    AppLogger.info(
                        "Starting export with \(referencedImagePaths.count) images (\(imageSummary.missingCount) missing)",
                        tag: logTag)
                }

                let zipData = try await ZipUtils.createExportZip(
                    exportData: exportData,
                    referencedImagePaths: referencedImagePaths
                )

                await MainActor.run {
                    AppLogger.info("Export completed successfully", tag: logTag)
                }

                return (zipData, referencedImagePaths.count)
            }.value

            successMessage = "Export completed with \(zipData.1) images"
            clearMessageAfterDelay()
            return zipData.0
        } catch {
            let errorMessage = "Export failed: \(error.localizedDescription)"
            AppLogger.error("ERROR: \(errorMessage)", tag: LogTag.climbingData)
            setError(errorMessage)
            return nil
        }
    }

    func importData(from data: Data, showSuccessMessage: Bool = true) throws {
        do {
            let importResult = try ZipUtils.extractImportZip(data: data)

            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"

            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .custom { decoder in
                let container = try decoder.singleValueContainer()
                let dateString = try container.decode(String.self)

                if let date = ISO8601DateFormatter().date(from: dateString) {
                    return date
                }

                if let date = dateFormatter.date(from: dateString) {
                    return date
                }

                return Date()
            }

            AppLogger.debug("Raw JSON content preview:", tag: LogTag.climbingData)
            AppLogger.debug(
                String(decoding: importResult.jsonData.prefix(500), as: UTF8.self) + "...",
                tag: LogTag.climbingData
            )

            let importData = try decoder.decode(ClimbDataBackup.self, from: importResult.jsonData)

            AppLogger.info(
                """
                Successfully decoded import data:
                - Gyms: \(importData.gyms.count)
                - Problems: \(importData.problems.count)
                - Sessions: \(importData.sessions.count)
                - Attempts: \(importData.attempts.count)
                """,
                tag: LogTag.climbingData
            )

            try validateImportData(importData)

            resetAllData(showSuccessMessage: showSuccessMessage)

            let updatedProblems = updateProblemImagePaths(
                problems: importData.problems,
                imagePathMapping: importResult.imagePathMapping
            )

            self.gyms = try importData.gyms.map { try $0.toGym() }
            self.problems = try updatedProblems.map { try $0.toProblem() }
            self.sessions = try importData.sessions.map { try $0.toClimbSession() }
            self.attempts = try importData.attempts.map { try $0.toAttempt() }

            saveGyms()
            saveProblems()
            saveSessions()
            saveAttempts()

            // Update data state to current time since we just imported new data
            DataStateManager.shared.updateDataState()

            if showSuccessMessage {
                successMessage =
                    "Data imported successfully with \(importResult.imagePathMapping.count) images"
                clearMessageAfterDelay()
            }
        } catch {
            setError("Import failed: \(error.localizedDescription)")
            throw error
        }
    }

    func clearMessages() {
        errorMessage = nil
        successMessage = nil
    }

    private func clearMessageAfterDelay() {
        Task {
            try? await Task.sleep(for: .seconds(3))
            successMessage = nil
            errorMessage = nil
        }
    }

    func setError(_ message: String) {
        errorMessage = message
        clearMessageAfterDelay()
    }
}

extension ClimbingDataManager {
    private func collectReferencedImagePaths() -> Set<String> {
        let imagesDirectory = ImageManager.shared.imagesDirectory.path
        let result = Self.collectReferencedImagePathsStatic(
            problems: problems,
            imagesDirectory: imagesDirectory)

        AppLogger.info(
            "Export: Collected \(result.paths.count) images (\(result.missingCount) missing)",
            tag: LogTag.climbingData)

        return result.paths
    }

    nonisolated private static func collectReferencedImagePathsStatic(
        problems: [Problem], imagesDirectory: String
    ) -> (paths: Set<String>, missingCount: Int) {
        var imagePaths = Set<String>()
        var missingCount = 0

        for problem in problems {
            if !problem.imagePaths.isEmpty {
                for imagePath in problem.imagePaths {
                    // Extract just the filename (migration should have normalized these)
                    let filename = URL(fileURLWithPath: imagePath).lastPathComponent
                    let fullPath = (imagesDirectory as NSString).appendingPathComponent(filename)

                    if FileManager.default.fileExists(atPath: fullPath) {
                        imagePaths.insert(fullPath)
                    } else {
                        missingCount += 1
                        imagePaths.insert(fullPath)
                    }
                }
            }
        }

        return (imagePaths, missingCount)
    }

    private func updateProblemImagePaths(
        problems: [BackupProblem],
        imagePathMapping: [String: String]
    ) -> [BackupProblem] {
        return problems.map { problem in
            guard let originalImagePaths = problem.imagePaths, !originalImagePaths.isEmpty else {
                return problem
            }

            var deterministicImagePaths: [String] = []

            for (index, oldPath) in originalImagePaths.enumerated() {
                let originalFileName = URL(fileURLWithPath: oldPath).lastPathComponent

                let deterministicName = ImageNamingUtils.generateImageFilename(
                    problemId: problem.id, imageIndex: index)

                if let tempFileName = imagePathMapping[originalFileName] {
                    let imageManager = ImageManager.shared
                    let tempPath = imageManager.imagesDirectory.appendingPathComponent(tempFileName)
                    let deterministicPath = imageManager.imagesDirectory.appendingPathComponent(
                        deterministicName)

                    do {
                        if FileManager.default.fileExists(atPath: tempPath.path) {
                            try FileManager.default.moveItem(at: tempPath, to: deterministicPath)

                            let tempBackupPath = imageManager.backupDirectory
                                .appendingPathComponent(tempFileName)
                            let deterministicBackupPath = imageManager.backupDirectory
                                .appendingPathComponent(deterministicName)

                            if FileManager.default.fileExists(atPath: tempBackupPath.path) {
                                try? FileManager.default.moveItem(
                                    at: tempBackupPath, to: deterministicBackupPath)
                            }

                            deterministicImagePaths.append(deterministicName)
                            AppLogger.debug(
                                "Renamed imported image: \(tempFileName) → \(deterministicName)",
                                tag: LogTag.climbingData)
                        }
                    } catch {
                        AppLogger.error(
                            "Failed to rename imported image \(tempFileName) to \(deterministicName): \(error)",
                            tag: LogTag.climbingData
                        )
                        deterministicImagePaths.append(tempFileName)
                    }
                } else {
                    deterministicImagePaths.append(deterministicName)
                }
            }

            return problem.withUpdatedImagePaths(deterministicImagePaths)
        }
    }

    private func migrateImagePaths() {
        var needsUpdate = false

        let updatedProblems = problems.map { problem in
            let migratedPaths = problem.imagePaths.compactMap { path in
                // If it's already a relative path, keep it
                if !path.hasPrefix("/") {
                    return path
                }

                // For absolute paths, try to migrate to relative
                let fileName = URL(fileURLWithPath: path).lastPathComponent
                if ImageManager.shared.imageExists(atPath: fileName) {
                    needsUpdate = true
                    return fileName
                }

                // If image doesn't exist, remove from paths
                needsUpdate = true
                return nil
            }

            if migratedPaths != problem.imagePaths {
                return problem.updated(imagePaths: migratedPaths)
            }
            return problem
        }

        if needsUpdate {
            problems = updatedProblems
            saveProblems()
            AppLogger.info(
                "Migrated image paths for \(problems.count) problems", tag: LogTag.climbingData)
        }
    }

    private func performImageMaintenance() async {
        // Run maintenance in background
        await Task.detached {
            await ImageManager.shared.performMaintenance()

            // Log storage information for debugging
            let info = await ImageManager.shared.getStorageInfo()
            await AppLogger.debug(
                "Image Storage: \(info.primaryCount) primary, \(info.backupCount) backup, \(info.totalSize / 1024)KB total",
                tag: LogTag.climbingData
            )
        }.value
    }

    func manualImageMaintenance() {
        Task {
            await performImageMaintenance()
        }
    }

    func getImageStorageInfo() -> String {
        let info = ImageManager.shared.getStorageInfo()
        return """
            Image Storage Status:
            • Primary: \(info.primaryCount) files
            • Backup: \(info.backupCount) files
            • Total Size: \(formatBytes(info.totalSize))
            """
    }

    func cleanupUnusedImages() {
        // Get all image paths currently referenced in problems
        let referencedImages = Set(
            problems.flatMap { $0.imagePaths.map { ImageManager.shared.getRelativePath(from: $0) } }
        )

        // Get all files in storage
        if let primaryFiles = try? FileManager.default.contentsOfDirectory(
            atPath: ImageManager.shared.getImagesDirectoryPath())
        {
            let orphanedFiles = primaryFiles.filter { !referencedImages.contains($0) }

            for fileName in orphanedFiles {
                _ = ImageManager.shared.deleteImage(atPath: fileName)
            }

            if !orphanedFiles.isEmpty {
                AppLogger.info(
                    "Cleaned up \(orphanedFiles.count) orphaned image files",
                    tag: LogTag.climbingData)
            }
        }
    }

    private func formatBytes(_ bytes: Int64) -> String {
        let kb = Double(bytes) / 1024.0
        let mb = kb / 1024.0

        if mb >= 1.0 {
            return String(format: "%.1f MB", mb)
        } else {
            return String(format: "%.0f KB", kb)
        }
    }

    func forceImageRecovery() {
        AppLogger.info("User initiated force image recovery", tag: LogTag.climbingData)
        ImageManager.shared.forceRecoveryMigration()

        // Refresh the UI after recovery
        objectWillChange.send()
    }

    func emergencyImageRestore() {
        AppLogger.info("User initiated emergency image restore", tag: LogTag.climbingData)
        ImageManager.shared.emergencyImageRestore()

        // Refresh the UI after restore
        objectWillChange.send()
    }

    func validateImageStorage() -> Bool {
        return ImageManager.shared.validateStorageIntegrity()
    }

    func getImageRecoveryStatus() -> String {
        let isValid = validateImageStorage()
        let info = ImageManager.shared.getStorageInfo()

        return """
            Image Storage Health: \(isValid ? "Good" : "Needs Recovery")
            Primary Files: \(info.primaryCount)
            Backup Files: \(info.backupCount)
            Total Size: \(formatBytes(info.totalSize))

            \(isValid ? "No action needed" : "Consider running Force Recovery")
            """
    }

    func testLiveActivity() {
        AppLogger.info("Testing Live Activity functionality...", tag: LogTag.climbingData)

        let status = LiveActivityManager.shared.checkLiveActivityAvailability()
        AppLogger.info(status, tag: LogTag.climbingData)

        guard let testGym = gyms.first else {
            AppLogger.error("No gyms available for testing", tag: LogTag.climbingData)
            return
        }

        let testSession = ClimbSession(gymId: testGym.id, notes: "Test session for Live Activity")

        Task {
            await LiveActivityManager.shared.startLiveActivity(
                for: testSession, gymName: testGym.name)

            try? await Task.sleep(for: .seconds(2))
            await LiveActivityManager.shared.updateLiveActivity(
                elapsed: 120, totalAttempts: 5, completedProblems: 1)

            try? await Task.sleep(for: .seconds(5))
            await LiveActivityManager.shared.endLiveActivity()
        }
    }

    private func checkAndRestartLiveActivity() async {
        guard let activeSession = activeSession else {
            // No active session, make sure all Live Activities are cleaned up
            await LiveActivityManager.shared.endLiveActivity()
            return
        }

        // Only restart if session is actually active
        guard activeSession.status == .active else {
            AppLogger.warning(
                "Session exists but is not active (status: \(activeSession.status)), ending Live Activity",
                tag: LogTag.climbingData
            )
            await LiveActivityManager.shared.endLiveActivity()
            return
        }

        if let gym = gym(withId: activeSession.gymId) {
            AppLogger.info(
                "Checking Live Activity for active session at \(gym.name)", tag: LogTag.climbingData
            )

            // First cleanup any dismissed activities
            await LiveActivityManager.shared.cleanupDismissedActivities()

            // Then attempt to restart if needed
            await LiveActivityManager.shared.restartLiveActivityIfNeeded(
                activeSession: activeSession,
                gymName: gym.name
            )
        }
    }

    /// Call this when app becomes active to check for Live Activity restart
    func onAppBecomeActive() {
        let logTag = "ClimbingData"
        AppLogger.info(
            "App became active - checking Live Activity status", tag: logTag)
        Task {
            await checkAndRestartLiveActivity()
        }
    }

    /// Call this when app enters background to update Live Activity
    func onAppEnterBackground() {
        let logTag = "ClimbingData"
        AppLogger.info(
            "App entering background - updating Live Activity if needed", tag: logTag)
        Task {
            await updateLiveActivityData()
        }
    }

    /// Setup notifications for Live Activity events
    nonisolated private func setupLiveActivityNotifications() {
        let notificationName = Notification.Name("liveActivityDismissed")
        let logTag = "ClimbingData"

        liveActivityObserver = NotificationCenter.default.addObserver(
            forName: notificationName,
            object: nil,
            queue: .main
        ) { [weak self] _ in
            Task { @MainActor in
                AppLogger.info(
                    "Received Live Activity dismissed notification - attempting restart",
                    tag: logTag)
                await self?.handleLiveActivityDismissed()
            }
        }
    }

    nonisolated private func setupMigrationNotifications() {
        let logTag = "ClimbingData"

        migrationObserver = NotificationCenter.default.addObserver(
            forName: NSNotification.Name("ImageMigrationCompleted"),
            object: nil,
            queue: .main
        ) { [weak self] notification in
            if let updateCount = notification.userInfo?["updateCount"] as? Int {
                Task { @MainActor in
                    AppLogger.info(
                        "Image migration completed with \(updateCount) updates - reloading data",
                        tag: logTag)
                    self?.loadProblems()
                }
            }
        }
    }

    /// Handle Live Activity being dismissed by user
    private func handleLiveActivityDismissed() async {
        guard let activeSession = activeSession,
            activeSession.status == .active,
            let gym = gym(withId: activeSession.gymId)
        else {
            return
        }

        AppLogger.info(
            "Attempting to restart dismissed Live Activity for \(gym.name)",
            tag: LogTag.climbingData)

        try? await Task.sleep(for: .seconds(2))

        await LiveActivityManager.shared.startLiveActivity(
            for: activeSession,
            gymName: gym.name
        )

        // Update with current data
        await updateLiveActivityData()
    }

    /// Update Live Activity with current session statistics
    private func updateLiveActivityData() async {
        guard let activeSession = activeSession,
            activeSession.status == .active
        else { return }

        let elapsed = Date().timeIntervalSince(activeSession.startTime ?? activeSession.date)
        let sessionAttempts = attempts.filter { $0.sessionId == activeSession.id }
        let totalAttempts = sessionAttempts.count
        let completedProblems = Set(
            sessionAttempts.filter { $0.result.isSuccessful }.map { $0.problemId }
        ).count

        await LiveActivityManager.shared.updateLiveActivity(
            elapsed: elapsed,
            totalAttempts: totalAttempts,
            completedProblems: completedProblems
        )
    }

    /// Update Live Activity with current session data
    private func updateLiveActivityForActiveSession() {
        guard let activeSession = activeSession,
            activeSession.status == .active,
            let gym = gym(withId: activeSession.gymId)
        else {
            AppLogger.warning(
                "Live Activity update skipped - no active session or gym",
                tag: LogTag.climbingData
            )
            if let session = activeSession {
                AppLogger.debug(
                    """
                    Skipped session details:
                      Session ID: \(session.id)
                      Session Status: \(session.status)
                      Gym ID: \(session.gymId)
                    """,
                    tag: LogTag.climbingData
                )
            }
            return
        }

        let attemptsForSession = attempts(forSession: activeSession.id)
        let totalAttempts = attemptsForSession.count

        let completedProblemIds = Set(
            attemptsForSession.filter { $0.result.isSuccessful }.map { $0.problemId }
        )
        let completedProblems = completedProblemIds.count

        let elapsedInterval: TimeInterval
        if let startTime = activeSession.startTime {
            elapsedInterval = Date().timeIntervalSince(startTime)
        } else {
            elapsedInterval = 0
        }

        AppLogger.debug(
            """
            Live Activity Update Debug:
              Session ID: \(activeSession.id)
              Gym: \(gym.name)
              Total attempts in session: \(totalAttempts)
              Completed problems: \(completedProblems)
              Elapsed time: \(elapsedInterval) seconds
              All attempts for session: \(attemptsForSession.map { "\($0.result) - Problem: \($0.problemId)" })
            """,
            tag: LogTag.climbingData
        )

        Task {
            await LiveActivityManager.shared.updateLiveActivity(
                elapsed: elapsedInterval,
                totalAttempts: totalAttempts,
                completedProblems: completedProblems
            )
        }
    }

    /// Manually force Live Activity update (useful for debugging)
    func forceLiveActivityUpdate() {
        updateLiveActivityForActiveSession()
    }

    /// Update widget timeline when data changes
    private func updateWidgetTimeline() {
        #if canImport(WidgetKit)
            WidgetCenter.shared.reloadTimelines(ofKind: "SessionStatusLive")
        #endif
    }

    /// Debug function to manually trigger widget data update
    func debugUpdateWidgetData() {
        // Force save all data to widget
        saveGyms()
        saveSessions()
        saveAttempts()
    }

    private func validateImportData(_ importData: ClimbDataBackup) throws {
        if importData.gyms.isEmpty {
            throw NSError(
                domain: "ImportError", code: 1,
                userInfo: [NSLocalizedDescriptionKey: "Import data is invalid: no gyms found"])
        }
    }
}

extension ClimbingDataManager {
    static var preview: ClimbingDataManager {
        let manager = ClimbingDataManager()

        let sampleGym = Gym(
            name: "Sample Climbing Gym",
            location: "123 Rock St, Boulder, CO",
            supportedClimbTypes: [.boulder, .rope],
            difficultySystems: [.vScale, .yds]
        )

        manager.gyms = [sampleGym]

        let sampleProblem = Problem(
            gymId: sampleGym.id,
            name: "Crimpy Overhang",
            description: "Technical overhang with small holds",
            climbType: .boulder,
            difficulty: DifficultyGrade(system: .vScale, grade: "V4"),
            tags: ["technical", "overhang"],
            location: "Cave area"
        )

        manager.problems = [sampleProblem]

        return manager
    }
}
