import Combine
import Foundation
import UIKit

@MainActor
class ServerSyncProvider: SyncProvider {
    var type: SyncProviderType = .server

    private let userDefaults = UserDefaults.standard
    private let logTag = "ServerSyncProvider"

    private enum Keys {
        static let serverURL = "sync_server_url"
        static let authToken = "sync_auth_token"
        static let lastSyncTime = "last_sync_time"
        static let isConnected = "is_connected"
    }

    var serverURL: String {
        get { userDefaults.string(forKey: Keys.serverURL) ?? "" }
        set { userDefaults.set(newValue, forKey: Keys.serverURL) }
    }

    var authToken: String {
        get { userDefaults.string(forKey: Keys.authToken) ?? "" }
        set { userDefaults.set(newValue, forKey: Keys.authToken) }
    }

    var isConfigured: Bool {
        return !serverURL.isEmpty && !authToken.isEmpty
    }

    var isConnected: Bool {
        get { userDefaults.bool(forKey: Keys.isConnected) }
        set { userDefaults.set(newValue, forKey: Keys.isConnected) }
    }

    var lastSyncTime: Date? {
        get { userDefaults.object(forKey: Keys.lastSyncTime) as? Date }
        set { userDefaults.set(newValue, forKey: Keys.lastSyncTime) }
    }

    func disconnect() {
        isConnected = false
        lastSyncTime = nil
    }

    func testConnection() async throws {
        guard let url = URL(string: "\(serverURL)/health") else {
            throw SyncError.invalidURL
        }

        var request = URLRequest(url: url)
        request.httpMethod = "GET"

        let (_, response) = try await URLSession.shared.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            throw SyncError.serverError(500)
        }

        isConnected = true
    }

    func sync(dataManager: ClimbingDataManager) async throws {
        guard isConfigured else {
            throw SyncError.notConfigured
        }

        guard isConnected else {
            throw SyncError.notConnected
        }

        if lastSyncTime != nil {
            AppLogger.info("Last sync time found, attempting delta sync", tag: logTag)
            do {
                try await performDeltaSync(dataManager: dataManager)
                lastSyncTime = Date()
                return
            } catch {
                AppLogger.error("Delta sync failed, falling back to full sync check: \(error)", tag: logTag)
            }
        }

        let localBackup = createBackupFromDataManager(dataManager)
        let serverBackup = try await downloadData()

        let hasLocalData =
            !dataManager.gyms.isEmpty || !dataManager.problems.isEmpty
            || !dataManager.sessions.isEmpty || !dataManager.attempts.isEmpty

        let hasServerData =
            !serverBackup.gyms.isEmpty || !serverBackup.problems.isEmpty
            || !serverBackup.sessions.isEmpty || !serverBackup.attempts.isEmpty

        if !hasLocalData && hasServerData {
            AppLogger.info("Performing full restore from server", tag: logTag)
            AppLogger.info("Syncing images from server first...", tag: logTag)
            let imagePathMapping = try await syncImagesFromServer(
                backup: serverBackup, dataManager: dataManager)
            AppLogger.info("Importing data after images...", tag: logTag)
            try importBackupToDataManager(
                serverBackup, dataManager: dataManager, imagePathMapping: imagePathMapping)
            AppLogger.info("Full restore completed", tag: logTag)
        } else if hasLocalData && !hasServerData {
            AppLogger.info("Uploading local data to server", tag: logTag)
            let currentBackup = createBackupFromDataManager(dataManager)
            _ = try await uploadData(currentBackup)
            AppLogger.info("Uploading local images to server...", tag: logTag)
            try await syncImagesToServer(dataManager: dataManager)
            AppLogger.info("Initial upload completed", tag: logTag)
        } else if hasLocalData && hasServerData {
            AppLogger.info("Merging local and server data", tag: logTag)
            try await mergeDataSafely(
                localBackup: localBackup,
                serverBackup: serverBackup,
                dataManager: dataManager)
            AppLogger.info("Safe merge completed", tag: logTag)
        } else {
            AppLogger.info("No data to sync", tag: logTag)
        }

        // Update last sync time
        lastSyncTime = Date()
    }

    // MARK: - Private Helpers

    private func downloadData() async throws -> ClimbDataBackup {
        guard let url = URL(string: "\(serverURL)/sync") else {
            throw SyncError.invalidURL
        }

        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Accept")

        let (data, response) = try await URLSession.shared.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse else {
            throw SyncError.invalidResponse
        }

        switch httpResponse.statusCode {
        case 200:
            break
        case 401:
            throw SyncError.unauthorized
        default:
            throw SyncError.serverError(httpResponse.statusCode)
        }

        do {
            let backup = try JSONDecoder().decode(ClimbDataBackup.self, from: data)
            return backup
        } catch {
            throw SyncError.decodingError(error)
        }
    }

    private func uploadData(_ backup: ClimbDataBackup) async throws -> ClimbDataBackup {
        guard let url = URL(string: "\(serverURL)/sync") else {
            throw SyncError.invalidURL
        }

        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601

        let jsonData = try encoder.encode(backup)

        var request = URLRequest(url: url)
        request.httpMethod = "PUT"
        request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.httpBody = jsonData

        let (data, response) = try await URLSession.shared.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse else {
            throw SyncError.invalidResponse
        }

        switch httpResponse.statusCode {
        case 200:
            break
        case 401:
            throw SyncError.unauthorized
        case 400:
            throw SyncError.badRequest
        default:
            throw SyncError.serverError(httpResponse.statusCode)
        }

        do {
            let responseBackup = try JSONDecoder().decode(ClimbDataBackup.self, from: data)
            return responseBackup
        } catch {
            throw SyncError.decodingError(error)
        }
    }

    private func performDeltaSync(dataManager: ClimbingDataManager) async throws {
        guard let url = URL(string: "\(serverURL)/sync/delta") else {
            throw SyncError.invalidURL
        }

        let lastSync = lastSyncTime ?? Date(timeIntervalSince1970: 0)
        let formatter = ISO8601DateFormatter()
        let lastSyncString = formatter.string(from: lastSync)

        var modifiedGyms = dataManager.gyms.filter { gym in
            gym.updatedAt > lastSync
        }.map { BackupGym(from: $0) }

        var modifiedProblems = dataManager.problems.filter { problem in
            problem.updatedAt > lastSync
        }.map { problem -> BackupProblem in
            let backupProblem = BackupProblem(from: problem)
            if !problem.imagePaths.isEmpty {
                let normalizedPaths = problem.imagePaths.indices.map { index in
                    ImageNamingUtils.generateImageFilename(
                        problemId: problem.id.uuidString, imageIndex: index)
                }
                return backupProblem.withUpdatedImagePaths(normalizedPaths)
            }
            return backupProblem
        }

        var modifiedSessions = dataManager.sessions.filter { session in
            session.status != .active && session.updatedAt > lastSync
        }.map { BackupClimbSession(from: $0) }

        let activeSessionIds = Set(
            dataManager.sessions.filter { $0.status == .active }.map { $0.id })

        var modifiedAttempts = dataManager.attempts.filter { attempt in
            !activeSessionIds.contains(attempt.sessionId) && attempt.createdAt > lastSync
        }.map { BackupAttempt(from: $0) }

        let deletedItems = dataManager.getDeletedItems().filter { item in
            if let deletedDate = formatter.date(from: item.deletedAt) {
                return deletedDate > lastSync
            }
            return false
        }

        for item in deletedItems {
            guard let deletedDate = formatter.date(from: item.deletedAt) else { continue }
            switch item.type {
            case "gym":
                modifiedGyms.append(BackupGym.createTombstone(id: item.id, deletedAt: deletedDate))
            case "problem":
                modifiedProblems.append(BackupProblem.createTombstone(id: item.id, deletedAt: deletedDate))
            case "session":
                modifiedSessions.append(BackupClimbSession.createTombstone(id: item.id, deletedAt: deletedDate))
            case "attempt":
                modifiedAttempts.append(BackupAttempt.createTombstone(id: item.id, deletedAt: deletedDate))
            default:
                break
            }
        }

        AppLogger.info(
            "Delta Sync: Sending gyms=\(modifiedGyms.count), problems=\(modifiedProblems.count), sessions=\(modifiedSessions.count), attempts=\(modifiedAttempts.count)",
            tag: logTag
        )

        let deltaRequest = DeltaSyncRequest(
            lastSyncTime: lastSyncString,
            gyms: modifiedGyms,
            problems: modifiedProblems,
            sessions: modifiedSessions,
            attempts: modifiedAttempts
        )

        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        let jsonData = try encoder.encode(deltaRequest)

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.httpBody = jsonData

        let (data, response) = try await URLSession.shared.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse else {
            throw SyncError.invalidResponse
        }

        switch httpResponse.statusCode {
        case 200:
            break
        case 401:
            throw SyncError.unauthorized
        default:
            throw SyncError.serverError(httpResponse.statusCode)
        }

        let decoder = JSONDecoder()
        let deltaResponse = try decoder.decode(DeltaSyncResponse.self, from: data)

        if let requestFullSync = deltaResponse.requestFullSync, requestFullSync {
            AppLogger.info("Server requested full sync", tag: logTag)
            throw SyncError.serverError(412)
        }

        AppLogger.info(
            "Delta Sync: Received gyms=\(deltaResponse.gyms.count), problems=\(deltaResponse.problems.count), sessions=\(deltaResponse.sessions.count), attempts=\(deltaResponse.attempts.count)",
            tag: logTag
        )

        try await applyDeltaResponse(deltaResponse, dataManager: dataManager)

        try await syncModifiedImages(modifiedProblems: modifiedProblems, dataManager: dataManager)

        if let serverTime = formatter.date(from: deltaResponse.serverTime) {
            lastSyncTime = serverTime
        }
    }

    private func applyDeltaResponse(_ response: DeltaSyncResponse, dataManager: ClimbingDataManager) async throws {
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]

        var imagePathMapping: [String: String] = [:]
        for problem in response.problems {
            if let isDeleted = problem.isDeleted, isDeleted { continue }

            guard let imagePaths = problem.imagePaths, !imagePaths.isEmpty else { continue }

            for (index, imagePath) in imagePaths.enumerated() {
                let serverFilename = URL(fileURLWithPath: imagePath).lastPathComponent
                do {
                    let imageData = try await downloadImage(filename: serverFilename)
                    let consistentFilename = ImageNamingUtils.generateImageFilename(
                        problemId: problem.id, imageIndex: index)
                    // Save image using ImageManager
                    _ = try ImageManager.shared.saveImportedImage(imageData, filename: consistentFilename)
                    imagePathMapping[serverFilename] = consistentFilename
                } catch {
                    AppLogger.info("Failed to download image \(serverFilename): \(error)", tag: logTag)
                }
            }
        }

        for backupGym in response.gyms {
            if let isDeleted = backupGym.isDeleted, isDeleted {
                if let index = dataManager.gyms.firstIndex(where: { $0.id.uuidString == backupGym.id }) {
                    let existing = dataManager.gyms[index]
                    if let serverUpdate = formatter.date(from: backupGym.updatedAt),
                       serverUpdate >= existing.updatedAt {
                         dataManager.gyms.remove(at: index)
                    }
                }
                continue
            }

            if let index = dataManager.gyms.firstIndex(where: { $0.id.uuidString == backupGym.id }) {
                let existing = dataManager.gyms[index]
                if let serverUpdate = formatter.date(from: backupGym.updatedAt),
                   serverUpdate >= existing.updatedAt {
                    dataManager.gyms[index] = try backupGym.toGym()
                }
            } else {
                dataManager.gyms.append(try backupGym.toGym())
            }
        }

        for backupProblem in response.problems {
            if let isDeleted = backupProblem.isDeleted, isDeleted {
                if let index = dataManager.problems.firstIndex(where: { $0.id.uuidString == backupProblem.id }) {
                    let existing = dataManager.problems[index]
                    if let serverUpdate = formatter.date(from: backupProblem.updatedAt),
                       serverUpdate >= existing.updatedAt {
                        dataManager.problems.remove(at: index)
                    }
                }
                continue
            }

            var problemToMerge = backupProblem
            if !imagePathMapping.isEmpty, let imagePaths = backupProblem.imagePaths {
                let updatedPaths = imagePaths.compactMap { imagePathMapping[$0] ?? $0 }
                problemToMerge = backupProblem.withUpdatedImagePaths(updatedPaths)
            }

            if let index = dataManager.problems.firstIndex(where: { $0.id.uuidString == problemToMerge.id }) {
                let existing = dataManager.problems[index]
                if let serverUpdate = formatter.date(from: problemToMerge.updatedAt),
                   serverUpdate >= existing.updatedAt {
                    dataManager.problems[index] = try problemToMerge.toProblem()
                }
            } else {
                dataManager.problems.append(try problemToMerge.toProblem())
            }
        }

        for backupSession in response.sessions {
             if let isDeleted = backupSession.isDeleted, isDeleted {
                if let index = dataManager.sessions.firstIndex(where: { $0.id.uuidString == backupSession.id }) {
                    let existing = dataManager.sessions[index]
                    if let serverUpdate = formatter.date(from: backupSession.updatedAt),
                       serverUpdate >= existing.updatedAt {
                        dataManager.sessions.remove(at: index)
                    }
                }
                continue
            }

            if let index = dataManager.sessions.firstIndex(where: { $0.id.uuidString == backupSession.id }) {
                let existing = dataManager.sessions[index]
                if let serverUpdate = formatter.date(from: backupSession.updatedAt),
                   serverUpdate >= existing.updatedAt {
                    dataManager.sessions[index] = try backupSession.toClimbSession()
                }
            } else {
                dataManager.sessions.append(try backupSession.toClimbSession())
            }
        }

        for backupAttempt in response.attempts {
            if let isDeleted = backupAttempt.isDeleted, isDeleted {
                if let index = dataManager.attempts.firstIndex(where: { $0.id.uuidString == backupAttempt.id }) {
                    let existing = dataManager.attempts[index]
                    let serverTimeStr = backupAttempt.updatedAt ?? backupAttempt.createdAt
                    if let serverTime = formatter.date(from: serverTimeStr),
                       serverTime >= existing.updatedAt {
                        dataManager.attempts.remove(at: index)
                    }
                }
                continue
            }

            if let index = dataManager.attempts.firstIndex(where: { $0.id.uuidString == backupAttempt.id }) {
                let existing = dataManager.attempts[index]
                 let serverTimeStr = backupAttempt.updatedAt ?? backupAttempt.createdAt
                 if let serverTime = formatter.date(from: serverTimeStr),
                    serverTime >= existing.updatedAt {
                    dataManager.attempts[index] = try backupAttempt.toAttempt()
                }
            } else {
                dataManager.attempts.append(try backupAttempt.toAttempt())
            }
        }

        dataManager.saveGyms()
        dataManager.saveProblems()
        dataManager.saveSessions()
        dataManager.saveAttempts()
    }

    // MARK: - Image Sync

    private func syncModifiedImages(modifiedProblems: [BackupProblem], dataManager: ClimbingDataManager) async throws {
         for problem in modifiedProblems {
             guard let imagePaths = problem.imagePaths else { continue }
             for path in imagePaths {
                 if let data = ImageManager.shared.loadImageData(fromPath: path) {
                      try await uploadImage(filename: path, imageData: data)
                 }
             }
         }
    }

    private func uploadImage(filename: String, imageData: Data) async throws {
        guard let url = URL(string: "\(serverURL)/images/upload?filename=\(filename)") else {
            throw SyncError.invalidURL
        }

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")
        request.httpBody = imageData

        let (_, response) = try await URLSession.shared.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            throw SyncError.serverError(500)
        }
    }

    private func downloadImage(filename: String) async throws -> Data {
        guard let url = URL(string: "\(serverURL)/images/download?filename=\(filename)") else {
            throw SyncError.invalidURL
        }

        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")

        let (data, response) = try await URLSession.shared.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse else {
             throw SyncError.invalidResponse
        }

        if httpResponse.statusCode == 404 {
            throw SyncError.imageNotFound
        }

        guard httpResponse.statusCode == 200 else {
            throw SyncError.serverError(httpResponse.statusCode)
        }

        return data
    }

    // MARK: - Full Sync Helpers (Simplified for reconstruction)

    private func syncImagesFromServer(backup: ClimbDataBackup, dataManager: ClimbingDataManager) async throws -> [String: String] {
        var mapping: [String: String] = [:]
        for problem in backup.problems {
            guard let paths = problem.imagePaths else { continue }
            for (index, path) in paths.enumerated() {
                 do {
                     let data = try await downloadImage(filename: path)
                     let localName = ImageNamingUtils.generateImageFilename(problemId: problem.id, imageIndex: index)
                     _ = try ImageManager.shared.saveImportedImage(data, filename: localName)
                     mapping[path] = localName
                 } catch {
                     continue
                 }
            }
        }
        return mapping
    }

    private func syncImagesToServer(dataManager: ClimbingDataManager) async throws {
        for problem in dataManager.problems {
            for path in problem.imagePaths {
                 if let data = ImageManager.shared.loadImageData(fromPath: path) {
                     try await uploadImage(filename: path, imageData: data)
                 }
            }
        }
    }

    private func createBackupFromDataManager(_ dataManager: ClimbingDataManager) -> ClimbDataBackup {
        let gyms = dataManager.gyms.map { BackupGym(from: $0) }
        let problems = dataManager.problems.map { BackupProblem(from: $0) }
        let sessions = dataManager.sessions.map { BackupClimbSession(from: $0) }
        let attempts = dataManager.attempts.map { BackupAttempt(from: $0) }

        return ClimbDataBackup(
            exportedAt: ISO8601DateFormatter().string(from: Date()),
            gyms: gyms,
            problems: problems,
            sessions: sessions,
            attempts: attempts
        )
    }

    private func mergeDataSafely(localBackup: ClimbDataBackup, serverBackup: ClimbDataBackup, dataManager: ClimbingDataManager) async throws {
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]

        for gym in serverBackup.gyms {
            if let isDeleted = gym.isDeleted, isDeleted {
                if let index = dataManager.gyms.firstIndex(where: { $0.id.uuidString == gym.id }) {
                    let existing = dataManager.gyms[index]
                    if let serverUpdate = formatter.date(from: gym.updatedAt), serverUpdate >= existing.updatedAt {
                         dataManager.gyms.remove(at: index)
                    }
                }
                continue
            }

             if let index = dataManager.gyms.firstIndex(where: { $0.id.uuidString == gym.id }) {
                 let existing = dataManager.gyms[index]
                 if let serverUpdate = formatter.date(from: gym.updatedAt), serverUpdate >= existing.updatedAt {
                     dataManager.gyms[index] = try gym.toGym()
                 }
             } else {
                 dataManager.gyms.append(try gym.toGym())
             }
        }

        for problem in serverBackup.problems {
            if let isDeleted = problem.isDeleted, isDeleted {
                if let index = dataManager.problems.firstIndex(where: { $0.id.uuidString == problem.id }) {
                    let existing = dataManager.problems[index]
                    if let serverUpdate = formatter.date(from: problem.updatedAt), serverUpdate >= existing.updatedAt {
                         dataManager.problems.remove(at: index)
                    }
                }
                continue
            }

             if let index = dataManager.problems.firstIndex(where: { $0.id.uuidString == problem.id }) {
                 let existing = dataManager.problems[index]
                 if let serverUpdate = formatter.date(from: problem.updatedAt), serverUpdate >= existing.updatedAt {
                     dataManager.problems[index] = try problem.toProblem()
                 }
             } else {
                 dataManager.problems.append(try problem.toProblem())
             }
        }

        for session in serverBackup.sessions {
            if let isDeleted = session.isDeleted, isDeleted {
                if let index = dataManager.sessions.firstIndex(where: { $0.id.uuidString == session.id }) {
                    let existing = dataManager.sessions[index]
                    if let serverUpdate = formatter.date(from: session.updatedAt), serverUpdate >= existing.updatedAt {
                         dataManager.sessions.remove(at: index)
                    }
                }
                continue
            }

             if let index = dataManager.sessions.firstIndex(where: { $0.id.uuidString == session.id }) {
                 let existing = dataManager.sessions[index]
                 if let serverUpdate = formatter.date(from: session.updatedAt), serverUpdate >= existing.updatedAt {
                     dataManager.sessions[index] = try session.toClimbSession()
                 }
             } else {
                 dataManager.sessions.append(try session.toClimbSession())
             }
        }

        for attempt in serverBackup.attempts {
            if let isDeleted = attempt.isDeleted, isDeleted {
                if let index = dataManager.attempts.firstIndex(where: { $0.id.uuidString == attempt.id }) {
                    let existing = dataManager.attempts[index]
                    let serverTimeStr = attempt.updatedAt ?? attempt.createdAt
                    if let serverUpdate = formatter.date(from: serverTimeStr), serverUpdate >= existing.updatedAt {
                         dataManager.attempts.remove(at: index)
                    }
                }
                continue
            }

             if let index = dataManager.attempts.firstIndex(where: { $0.id.uuidString == attempt.id }) {
                 let existing = dataManager.attempts[index]
                 let serverTimeStr = attempt.updatedAt ?? attempt.createdAt
                 if let serverUpdate = formatter.date(from: serverTimeStr), serverUpdate >= existing.updatedAt {
                     dataManager.attempts[index] = try attempt.toAttempt()
                 }
             } else {
                 dataManager.attempts.append(try attempt.toAttempt())
             }
        }

        dataManager.saveGyms()
        dataManager.saveProblems()
        dataManager.saveSessions()
        dataManager.saveAttempts()
    }

    private func importBackupToDataManager(
        _ backup: ClimbDataBackup, dataManager: ClimbingDataManager,
        imagePathMapping: [String: String] = [:]
    ) throws {
         let updatedProblems = backup.problems.map { problem in
            let updatedImagePaths = problem.imagePaths?.compactMap { oldPath in
                imagePathMapping[oldPath] ?? oldPath
            } ?? []
            return problem.withUpdatedImagePaths(updatedImagePaths)
        }

        dataManager.gyms = try backup.gyms.compactMap { gym in
            if let isDeleted = gym.isDeleted, isDeleted { return nil }
            return try gym.toGym()
        }

        dataManager.problems = try updatedProblems.compactMap { problem in
            if let isDeleted = problem.isDeleted, isDeleted { return nil }
            return try problem.toProblem()
        }

        dataManager.sessions = try backup.sessions.compactMap { session in
            if let isDeleted = session.isDeleted, isDeleted { return nil }
            return try session.toClimbSession()
        }

        dataManager.attempts = try backup.attempts.compactMap { attempt in
            if let isDeleted = attempt.isDeleted, isDeleted { return nil }
            return try attempt.toAttempt()
        }

        dataManager.saveGyms()
        dataManager.saveProblems()
        dataManager.saveSessions()
        dataManager.saveAttempts()
    }
}
