//
//  CanonRemoteCaptureService.swift
//  otg_swift
//

import Foundation
@preconcurrency import ImageCaptureCore

/// 佳能 EOS 边拍边传：GetEvent 轮询 + PTP 下载 + TransferComplete + 写入相册。
final class CanonRemoteCaptureService {

    private weak var camera: ICCameraDevice?
    private var transactionID: UInt32 = 1
    private(set) var isReady = false

    private var albumID: Int?
    private let photoRepository: PhotoRepositoryProtocol
    /// 下载成功时回调本地照片记录，供 ConnectionManager 转发给 ViewModel。
    var onShotSaved: ((TravelAlbumOTGPhotoRecord) -> Void)?

    private var eventPollTimer: Timer?
    private var catalogPollTimer: Timer?
    private var isDownloading = false
    private var isDrainingQueue = false
    private var isPollingGetEvent = false

    private var lastDownloadedSignature: String?
    private var knownDownloadedHandles: Set<UInt32> = []
    private var knownCatalogIDs: Set<String> = []
    private var catalogBaselineEstablished = false
    private var savedShotCount = 0

    private var downloadQueue: [PendingDownload] = []

    private struct PendingDownload {
        let handle: UInt32
        let storageID: UInt32?
        let filename: String?
        let objectSize: UInt32?
        let reason: String
    }

    private let eventPollInterval: TimeInterval = 0.2
    private let catalogPollInterval: TimeInterval = 1.5
    private var pollTickCount = 0
    private var catalogScanCount = 0

    init(photoRepository: PhotoRepositoryProtocol = TravelAlbumOTGPhotoRepository()) {
        self.photoRepository = photoRepository
    }

    // MARK: - Lifecycle

    /// catalog 就绪且远程会话初始化成功后启动。
    func start(
        camera: ICCameraDevice,
        startingTransactionID: UInt32,
        albumID: Int?
    ) {
        stop()

        self.camera = camera
        self.transactionID = startingTransactionID
        self.albumID = albumID
        self.isReady = true
        self.lastDownloadedSignature = nil
        self.knownDownloadedHandles = []
        self.knownCatalogIDs = []
        self.catalogBaselineEstablished = false
        self.savedShotCount = 0
        self.downloadQueue = []
        self.pollTickCount = 0
        self.isPollingGetEvent = false

        establishCatalogBaseline(from: camera)

        OTGLog.info(.canon, "Remote capture started, transactionID=\(startingTransactionID), album=\(albumID.map(String.init) ?? "nil")")
        startEventPolling()
        startCatalogPolling()
    }

    func stop() {
        isReady = false
        if Thread.isMainThread {
            stopEventPolling()
            stopCatalogPolling()
        } else {
            DispatchQueue.main.sync {
                stopEventPolling()
                stopCatalogPolling()
            }
        }
        isDownloading = false
        isDrainingQueue = false
        isPollingGetEvent = false
        downloadQueue = []
        knownCatalogIDs = []
        catalogBaselineEstablished = false
        albumID = nil
        camera = nil
    }

    // MARK: - Catalog Fallback

    /// ImageCaptureCore 在 catalog 就绪后新增的文件（仅保存到 SD 卡时 GetEvent 可能无传图事件）。
    func handleCatalogItemsAdded(_ items: [ICCameraItem]) {
        guard isReady, catalogBaselineEstablished else { return }

        for item in items {
            guard let file = item as? ICCameraFile else { continue }
            Task { await downloadCatalogFileIfNew(file, reason: "didAdd") }
        }
    }

    private func establishCatalogBaseline(from camera: ICCameraDevice) {
        var known = Set<String>()
        var newDuringConnect = 0

        for file in ICCameraItemScanner.collectImageFiles(from: camera) {
            if file.wasAddedAfterContentCatalogCompleted {
                newDuringConnect += 1
                Task { await downloadCatalogFileIfNew(file, reason: "baseline-new") }
            } else {
                known.insert(ICCameraItemScanner.cameraObjectID(for: file))
            }
        }

        knownCatalogIDs = known
        catalogBaselineEstablished = true
        OTGLog.info(.canon, "Catalog baseline: known=\(known.count), newDuringConnect=\(newDuringConnect)")
    }

    private func scanCatalogForNewFiles() async {
        guard isReady, catalogBaselineEstablished, let camera else { return }

        let files = ICCameraItemScanner.collectImageFiles(from: camera)
        catalogScanCount += 1
        if catalogScanCount == 1 || catalogScanCount % 10 == 0 {
            OTGLog.debug(.canon, "Catalog scan #\(catalogScanCount): files=\(files.count), known=\(knownCatalogIDs.count)")
        }

        for file in files {
            await downloadCatalogFileIfNew(file, reason: "catalog-poll")
        }
    }

    private func downloadCatalogFileIfNew(_ file: ICCameraFile, reason: String) async {
        guard isReady else { return }

        let objectID = ICCameraItemScanner.cameraObjectID(for: file)
        guard !knownCatalogIDs.contains(objectID) else { return }
        knownCatalogIDs.insert(objectID)

        if isDownloading {
            knownCatalogIDs.remove(objectID)
            return
        }

        isDownloading = true
        defer { isDownloading = false }

        let filename = file.name ?? UUID().uuidString
        if objectID == lastDownloadedSignature {
            return
        }

        OTGLog.info(.canon, "\(reason): downloading \(filename) id=\(objectID)")

        do {
            let tempDir = FileManager.default.temporaryDirectory
                .appendingPathComponent("canon-live", isDirectory: true)
            let url = try await ICCameraFileDownloader.download(file, to: tempDir)
            let data = try Data(contentsOf: url)
            try? FileManager.default.removeItem(at: url)

            lastDownloadedSignature = objectID

            guard let record = await saveToAlbum(data: data, filename: filename, reason: reason) else {
                knownCatalogIDs.remove(objectID)
                return
            }

            savedShotCount += 1
            OTGLog.info(.canon, "Live shot saved (#\(savedShotCount)): \(filename), bytes=\(data.count)")
            onShotSaved?(record)
        } catch {
            knownCatalogIDs.remove(objectID)
            OTGLog.error(.canon, "\(reason): catalog download failed \(filename): \(error.localizedDescription)")
        }
    }

    // MARK: - PTP Events

    /// 处理 ImageCaptureCore 被动 PTP 事件（ObjectAdded 等）。
    func handlePTPEvent(_ eventData: Data) {
        guard isReady else { return }

        let hex = eventData.prefix(16).map { String(format: "%02X", $0) }.joined(separator: " ")
        OTGLog.info(.canon, "PTP event received: \(hex) (\(eventData.count)B)")

        if let handle = CanonPTPHelper.parsePTPEventHandle(eventData), handle != 0 {
            enqueueDownload(
                handle: handle,
                storageID: nil,
                filename: nil,
                objectSize: nil,
                reason: "PTPEvent"
            )
        }
    }

    // MARK: - GetEvent Polling

    private func startEventPolling() {
        let schedule = { [weak self] in
            guard let self else { return }
            self.stopEventPolling()
            OTGLog.info(.canon, "GetEvent polling started (interval=\(self.eventPollInterval)s)")

            let timer = Timer(timeInterval: self.eventPollInterval, repeats: true) { [weak self] _ in
                guard let self, self.isReady else { return }
                Task { await self.pollCanonEvents() }
            }
            RunLoop.main.add(timer, forMode: .common)
            self.eventPollTimer = timer
        }

        if Thread.isMainThread {
            schedule()
        } else {
            DispatchQueue.main.async(execute: schedule)
        }
    }

    private func stopEventPolling() {
        eventPollTimer?.invalidate()
        eventPollTimer = nil
    }

    private func startCatalogPolling() {
        let schedule = { [weak self] in
            guard let self else { return }
            self.stopCatalogPolling()
            OTGLog.info(.canon, "Catalog polling started (interval=\(self.catalogPollInterval)s)")

            let timer = Timer(timeInterval: self.catalogPollInterval, repeats: true) { [weak self] _ in
                guard let self, self.isReady else { return }
                Task { await self.scanCatalogForNewFiles() }
            }
            RunLoop.main.add(timer, forMode: .common)
            self.catalogPollTimer = timer
        }

        if Thread.isMainThread {
            schedule()
        } else {
            DispatchQueue.main.async(execute: schedule)
        }
    }

    private func stopCatalogPolling() {
        catalogPollTimer?.invalidate()
        catalogPollTimer = nil
    }

    private func pollCanonEvents() async {
        guard isReady, let camera else { return }
        guard !isDownloading else { return }
        guard !isPollingGetEvent else { return }

        isPollingGetEvent = true
        defer { isPollingGetEvent = false }

        pollTickCount += 1

        var localTransactionID = transactionID
        let events = await CanonPTPHelper.pollEvents(on: camera, transactionID: &localTransactionID)
        transactionID = localTransactionID

        if pollTickCount == 1 || pollTickCount % 25 == 0 {
            OTGLog.debug(.canon, "GetEvent poll #\(pollTickCount), parsedEvents=\(events.count)")
        }

        for event in events where CanonPTPHelper.isTransferEvent(event) {
            guard let handle = event.objectHandle else { continue }
            enqueueDownload(
                handle: handle,
                storageID: event.storageID,
                filename: event.filename,
                objectSize: event.objectSize,
                reason: "GetEvent-0x\(String(format: "%04X", event.code))"
            )
        }
    }

    // MARK: - Download Queue

    private func enqueueDownload(
        handle: UInt32,
        storageID: UInt32?,
        filename: String?,
        objectSize: UInt32?,
        reason: String
    ) {
        if knownDownloadedHandles.contains(handle) { return }

        downloadQueue.append(
            PendingDownload(
                handle: handle,
                storageID: storageID,
                filename: filename,
                objectSize: objectSize,
                reason: reason
            )
        )
        OTGLog.info(
            .canon,
            "\(reason): enqueued handle=0x\(String(format: "%08X", handle)) file=\(filename ?? "?") queue=\(downloadQueue.count)"
        )
        startDrainIfNeeded()
    }

    private func startDrainIfNeeded() {
        guard !isDrainingQueue, !downloadQueue.isEmpty else { return }
        isDrainingQueue = true
        Task { await drainDownloadQueue() }
    }

    private func drainDownloadQueue() async {
        defer {
            isDrainingQueue = false
            if !downloadQueue.isEmpty {
                startDrainIfNeeded()
            }
        }

        while !downloadQueue.isEmpty {
            let item = downloadQueue.removeFirst()
            _ = await downloadAndSave(item: item, maxAttempts: 6)
            try? await Task.sleep(nanoseconds: 100_000_000)
        }
    }

    @discardableResult
    private func downloadAndSave(item: PendingDownload, maxAttempts: Int) async -> Bool {
        guard isReady, let camera else { return false }

        if knownDownloadedHandles.contains(item.handle) { return false }

        if isDownloading {
            downloadQueue.insert(item, at: 0)
            return false
        }

        isDownloading = true
        defer { isDownloading = false }

        var localTransactionID = transactionID

        for attempt in 1...maxAttempts {
            if attempt > 1 {
                try? await Task.sleep(nanoseconds: UInt64(attempt) * 300_000_000)
            }

            var filename = item.filename
            var resolvedSize = item.objectSize ?? 0

            if filename == nil || resolvedSize == 0 {
                if let probe = await CanonPTPHelper.probeObject(
                    handle: item.handle,
                    storageID: item.storageID,
                    on: camera,
                    transactionID: &localTransactionID
                ) {
                    if filename == nil { filename = probe.filename }
                    if resolvedSize == 0 { resolvedSize = probe.size }
                } else if filename == nil, resolvedSize == 0 {
                    if attempt == maxAttempts {
                        OTGLog.warning(.canon, "\(item.reason): object not readable handle=0x\(String(format: "%08X", item.handle))")
                    }
                    continue
                }
            }

            let resolvedFilename = filename ?? PTPHelper.sanitizeFilename("IMG_\(item.handle).JPG")
            let signature = resolvedSize > 0
                ? CanonPTPHelper.objectSignature(filename: resolvedFilename, size: resolvedSize)
                : "\(resolvedFilename)_handle_\(item.handle)"

            if signature == lastDownloadedSignature {
                knownDownloadedHandles.insert(item.handle)
                return false
            }

            guard let result = await CanonPTPHelper.downloadObject(
                handle: item.handle,
                storageID: item.storageID,
                preferredFilename: filename,
                on: camera,
                transactionID: &localTransactionID
            ) else {
                if attempt == maxAttempts {
                    OTGLog.error(.canon, "\(item.reason): download failed handle=0x\(String(format: "%08X", item.handle))")
                }
                continue
            }

            transactionID = localTransactionID

            await CanonPTPHelper.acknowledgeTransfer(
                handle: item.handle,
                on: camera,
                transactionID: &localTransactionID
            )
            transactionID = localTransactionID

            lastDownloadedSignature = signature
            knownDownloadedHandles.insert(item.handle)

            guard let record = await saveToAlbum(data: result.data, filename: result.filename, reason: item.reason) else {
                return false
            }

            savedShotCount += 1
            OTGLog.info(.canon, "Live shot saved (#\(savedShotCount)): \(result.filename), bytes=\(result.data.count)")
            onShotSaved?(record)
            return true
        }

        return false
    }

    private func saveToAlbum(data: Data, filename: String, reason: String) async -> TravelAlbumOTGPhotoRecord? {
        guard let albumID else {
            OTGLog.warning(.canon, "\(reason): live transfer album not configured, skip save")
            return nil
        }

        do {
            let fileURL = try AlbumPhotoStorage.writeImage(
                data,
                filename: filename,
                albumID: albumID
            )
            return try photoRepository.addPhoto(
                albumID: albumID,
                localPath: fileURL.path,
                createdAt: Date()
            )
        } catch {
            OTGLog.error(.canon, "\(reason): save failed: \(error.localizedDescription)")
            return nil
        }
    }
}
