import Foundation
import SwiftData
import UniformTypeIdentifiers

struct RemoteAsset: Decodable {
    let id: String
    let clientId: String
    let kind: String
    let fileName: String
    let mimeType: String
    let sizeBytes: Int64
    let sha256: String
}

struct RemoteEvent: Decodable {
    let id: String
    let clientId: String?
    let relativeTimeMs: Int64
    let eventType: String
    let textContent: String?
    let voiceStartOffsetMs: Int64?
    let voiceEndOffsetMs: Int64?
}

struct RemoteSession: Decodable {
    let id: String
    let clientId: String?
    let title: String
    let startTime: Date
    let endTime: Date?
    let durationMs: Int64
    let revision: Int64
    let deletedAt: Date?
    let events: [RemoteEvent]?
    let assets: [RemoteAsset]?
}

private struct SessionUpload: Encodable {
    let clientId: String
    let title: String
    let startTime: Date
    let endTime: Date?
    let durationMs: Int64
    let events: [EventUpload]
}

private struct EventUpload: Encodable {
    let clientId: String
    let relativeTimeMs: Int64
    let eventType: String
    let textContent: String?
    let voiceStartOffsetMs: Int64?
    let voiceEndOffsetMs: Int64?
}

private struct SyncCheckpoint: Decodable {
    let syncedAt: Date
}

final class RemoteNetworkService: NetworkServiceProtocol {
    private let client: APIClient

    init(client: APIClient = .shared) {
        self.client = client
    }

    func fetchSessions() async throws -> [RemoteSession] {
        try await client.request("sessions?includeDeleted=true", authenticated: true)
    }

    func syncSession(_ session: CelestiaSession) async throws -> RemoteSession {
        let payload = SessionUpload(
            clientId: session.id.uuidString,
            title: session.title,
            startTime: session.startTime,
            endTime: session.endTime,
            durationMs: session.durationMs,
            events: session.events.map {
                EventUpload(
                    clientId: $0.id.uuidString,
                    relativeTimeMs: $0.relativeTimeMs,
                    eventType: $0.eventType,
                    textContent: $0.textContent,
                    voiceStartOffsetMs: $0.voiceStartOffsetMs,
                    voiceEndOffsetMs: $0.voiceEndOffsetMs
                )
            }
        )
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        return try await client.request("sessions", method: .post, body: try encoder.encode(payload), authenticated: true)
    }

    func uploadAsset(sessionID: String, clientID: String, kind: String, fileURL: URL) async throws -> RemoteAsset {
        let mimeType = UTType(filenameExtension: fileURL.pathExtension)?.preferredMIMEType ?? "application/octet-stream"
        return try await client.upload(
            "sessions/\(sessionID)/assets",
            fileURL: fileURL,
            fileName: fileURL.lastPathComponent,
            mimeType: mimeType,
            fields: ["clientId": clientID, "kind": kind]
        )
    }

    func downloadAsset(sessionID: String, asset: RemoteAsset, destinationURL: URL) async throws {
        try await client.download("sessions/\(sessionID)/assets/\(asset.id)", to: destinationURL)
    }

    func recordSyncCheckpoint() async throws {
        let _: SyncCheckpoint = try await client.request("sync/trigger", method: .post, authenticated: true)
    }
}

struct RemoteBoundDevice: Decodable {
    let id: String
    let name: String
    let peripheralUUID: String
}

private struct BindDeviceBody: Encodable {
    let name: String
    let peripheralUUID: String
    let hardwareMAC: String?
    let firmwareVersion: String?
    let batteryLevel: Int?
    let freeStorageMB: Int?
    let totalStorageMB: Int?
}

private struct UpdateDeviceBody: Encodable {
    let name: String?
    let batteryLevel: Int?
    let isConnected: Bool?
    let firmwareVersion: String?
    let freeStorageMB: Int?
    let totalStorageMB: Int?
}

final class DeviceCloudService {
    static let shared = DeviceCloudService()
    private let client: APIClient

    init(client: APIClient = .shared) {
        self.client = client
    }

    func register(_ device: BoundDevice) async throws -> RemoteBoundDevice {
        let body = BindDeviceBody(
            name: device.name,
            peripheralUUID: device.peripheralUUID,
            hardwareMAC: device.hardwareMAC,
            firmwareVersion: device.firmwareVersion,
            batteryLevel: device.batteryLevel,
            freeStorageMB: device.freeStorageMB,
            totalStorageMB: device.totalStorageMB
        )
        return try await client.request("devices", method: .post, body: try JSONEncoder().encode(body), authenticated: true)
    }

    func update(_ device: BoundDevice) async throws -> RemoteBoundDevice {
        guard let cloudID = device.cloudID else { return try await register(device) }
        let body = UpdateDeviceBody(
            name: device.name,
            batteryLevel: device.batteryLevel,
            isConnected: device.isConnected,
            firmwareVersion: device.firmwareVersion,
            freeStorageMB: device.freeStorageMB,
            totalStorageMB: device.totalStorageMB
        )
        return try await client.request("devices/\(cloudID)", method: .put, body: try JSONEncoder().encode(body), authenticated: true)
    }

    func remove(cloudID: String) async throws {
        try await client.requestVoid("devices/\(cloudID)", method: .delete, authenticated: true)
    }
}

@MainActor
final class SyncManager: ObservableObject {
    static let shared = SyncManager()

    @Published private(set) var isSyncing = false
    @Published private(set) var lastSyncDate: Date?
    @Published private(set) var lastErrorMessage: String?
    @Published private(set) var completedCount = 0
    @Published private(set) var totalCount = 0

    private let service: NetworkServiceProtocol

    init(service: NetworkServiceProtocol = RemoteNetworkService()) {
        self.service = service
        lastSyncDate = UserDefaults.standard.object(forKey: "com.celestia.trace.last_server_sync") as? Date
    }

    func sync(
        sessions: [CelestiaSession],
        modelContext: ModelContext,
        userID: String
    ) async -> Bool {
        guard !isSyncing else { return false }
        isSyncing = true
        lastErrorMessage = nil
        completedCount = 0
        totalCount = sessions.filter {
            ($0.ownerUserID == nil || $0.ownerUserID == userID) && (!$0.isSynced || $0.cloudSessionId == nil)
        }.count
        defer { isSyncing = false }

        do {
            let remoteSessions = try await service.fetchSessions()
            let mergedSessions = merge(remoteSessions, into: sessions, modelContext: modelContext, userID: userID)
            for (remote, local) in mergedSessions {
                try await downloadMissingAssets(for: local, remote: remote)
            }
            try modelContext.save()

            let eligible = sessions.filter {
                ($0.ownerUserID == nil || $0.ownerUserID == userID) && (!$0.isSynced || $0.cloudSessionId == nil)
            }
            var failures: [String] = []
            for session in eligible {
                session.ownerUserID = userID
                session.syncState = .syncing
                session.lastSyncError = nil
                do {
                    let remote = try await service.syncSession(session)
                    session.cloudSessionId = remote.id
                    session.serverRevision = remote.revision
                    try await uploadLocalAssets(for: session, remote: remote)
                    session.isSynced = true
                    session.syncState = .synced
                    session.lastSyncedAt = Date()
                    completedCount += 1
                } catch {
                    session.isSynced = false
                    session.syncState = .failed
                    session.lastSyncError = error.localizedDescription
                    failures.append("\(session.title)：\(error.localizedDescription)")
                }
                try modelContext.save()
            }
            guard failures.isEmpty else {
                lastErrorMessage = failures.joined(separator: "\n")
                return false
            }
            try await service.recordSyncCheckpoint()
            let now = Date()
            lastSyncDate = now
            UserDefaults.standard.set(now, forKey: "com.celestia.trace.last_server_sync")
            return true
        } catch {
            lastErrorMessage = error.localizedDescription
            return false
        }
    }

    private func uploadLocalAssets(for session: CelestiaSession, remote: RemoteSession) async throws {
        guard let cloudID = session.cloudSessionId else { throw APIError.missingData }
        let existingIDs = Set((remote.assets ?? []).map(\.clientId))
        if let path = session.localAudioPath {
            guard let url = AudioPathHelper.resolveURL(for: path) else {
                throw APIError.transport("本地录音文件不存在：\(path)")
            }
            let clientID = "\(session.id.uuidString)-audio"
            if !existingIDs.contains(clientID) {
                _ = try await service.uploadAsset(sessionID: cloudID, clientID: clientID, kind: "AUDIO", fileURL: url)
            }
        }
        for event in session.events where event.eventType == "PHOTO" {
            guard let path = event.localFilePath else { continue }
            guard let url = AudioPathHelper.resolveURL(for: path) else {
                throw APIError.transport("照片文件不存在：\(path)")
            }
            let clientID = "\(event.id.uuidString)-photo"
            if !existingIDs.contains(clientID) {
                _ = try await service.uploadAsset(sessionID: cloudID, clientID: clientID, kind: "PHOTO", fileURL: url)
            }
        }
    }

    private func downloadMissingAssets(for session: CelestiaSession, remote: RemoteSession) async throws {
        let assets = remote.assets ?? []
        if (session.localAudioPath == nil || AudioPathHelper.resolveURL(for: session.localAudioPath) == nil),
           let audio = assets.first(where: { $0.kind.uppercased() == "AUDIO" }) {
            let destination = downloadDestination(for: audio)
            try await service.downloadAsset(sessionID: remote.id, asset: audio, destinationURL: destination)
            session.localAudioPath = AudioPathHelper.relativePath(from: destination.path)
        }

        let photoAssets = Dictionary(uniqueKeysWithValues: assets
            .filter { $0.kind.uppercased() == "PHOTO" }
            .map { ($0.clientId.lowercased(), $0) })
        for event in session.events where event.eventType == "PHOTO" {
            guard event.localFilePath == nil || AudioPathHelper.resolveURL(for: event.localFilePath) == nil else { continue }
            let key = "\(event.id.uuidString)-photo".lowercased()
            guard let asset = photoAssets[key] else { continue }
            let destination = downloadDestination(for: asset)
            try await service.downloadAsset(sessionID: remote.id, asset: asset, destinationURL: destination)
            event.localFilePath = AudioPathHelper.relativePath(from: destination.path)
        }
    }

    private func downloadDestination(for asset: RemoteAsset) -> URL {
        let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let fileExtension = (asset.fileName as NSString).pathExtension
        let suffix = fileExtension.isEmpty ? "" : ".\(fileExtension.lowercased())"
        return documents.appendingPathComponent("cloud_\(asset.id)\(suffix)")
    }

    private func merge(
        _ remoteSessions: [RemoteSession],
        into localSessions: [CelestiaSession],
        modelContext: ModelContext,
        userID: String
    ) -> [(RemoteSession, CelestiaSession)] {
        var byClientID = Dictionary(uniqueKeysWithValues: localSessions.map { ($0.id.uuidString.lowercased(), $0) })
        var merged: [(RemoteSession, CelestiaSession)] = []
        for remote in remoteSessions {
            guard let clientID = remote.clientId?.lowercased() else { continue }
            if let local = byClientID[clientID] {
                if remote.deletedAt != nil, local.isSynced {
                    modelContext.delete(local)
                    continue
                }
                if local.isSynced, remote.revision > local.serverRevision {
                    apply(remote, to: local, userID: userID)
                }
                if remote.deletedAt == nil {
                    merged.append((remote, local))
                }
            } else if remote.deletedAt == nil {
                let id = UUID(uuidString: clientID) ?? UUID()
                let session = CelestiaSession(id: id, title: remote.title, startTime: remote.startTime)
                apply(remote, to: session, userID: userID)
                modelContext.insert(session)
                byClientID[clientID] = session
                merged.append((remote, session))
            }
        }
        try? modelContext.save()
        return merged
    }

    private func apply(_ remote: RemoteSession, to local: CelestiaSession, userID: String) {
        local.title = remote.title
        local.startTime = remote.startTime
        local.endTime = remote.endTime
        local.cloudSessionId = remote.id
        local.ownerUserID = userID
        local.serverRevision = remote.revision
        local.isSynced = true
        local.syncState = .synced
        local.lastSyncError = nil
        local.events.removeAll()
        for remoteEvent in remote.events ?? [] {
            let id = remoteEvent.clientId.flatMap(UUID.init(uuidString:)) ?? UUID()
            let event = CelestiaTimelineEvent(id: id, relativeTimeMs: remoteEvent.relativeTimeMs, eventType: remoteEvent.eventType)
            event.textContent = remoteEvent.textContent
            event.voiceStartOffsetMs = remoteEvent.voiceStartOffsetMs
            event.voiceEndOffsetMs = remoteEvent.voiceEndOffsetMs
            local.events.append(event)
        }
    }
}
