//
//  NikonCatalogLiveCaptureService.swift
//  otg_swift
//

import Foundation
@preconcurrency import ImageCaptureCore

/// 尼康边拍边传：监听 ImageCaptureCore catalog 新增文件并下载写入相册。
/// Z6 系列等在快门后会触发 `didAdd`，无需 Nikon 专用 PTP 协议。
final class NikonCatalogLiveCaptureService {

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

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

    private var catalogPollTimer: Timer?
    private var isDownloading = false

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

    private let catalogPollInterval: TimeInterval = 1.5
    private var catalogScanCount = 0

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

    // MARK: - Lifecycle

    func start(camera: ICCameraDevice, albumID: Int?) {
        stop()

        self.camera = camera
        self.albumID = albumID
        self.isReady = true
        self.lastDownloadedSignature = nil
        self.knownCatalogIDs = []
        self.catalogBaselineEstablished = false
        self.savedShotCount = 0
        self.catalogScanCount = 0

        establishCatalogBaseline(from: camera)

        OTGLog.info(.nikon, "Catalog live capture started, album=\(albumID.map(String.init) ?? "nil")")
        startCatalogPolling()
    }

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

    // MARK: - Catalog

    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(.nikon, "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(.nikon, "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(.nikon, "\(reason): downloading \(filename) id=\(objectID)")

        do {
            let tempDir = FileManager.default.temporaryDirectory
                .appendingPathComponent("nikon-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(.nikon, "Live shot saved (#\(savedShotCount)): \(filename), bytes=\(data.count)")
            onShotSaved?(record)
        } catch {
            knownCatalogIDs.remove(objectID)
            OTGLog.error(.nikon, "\(reason): catalog download failed \(filename): \(error.localizedDescription)")
        }
    }

    // MARK: - Polling

    private func startCatalogPolling() {
        let schedule = { [weak self] in
            guard let self else { return }
            self.stopCatalogPolling()
            OTGLog.info(.nikon, "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
    }

    // MARK: - Save

    private func saveToAlbum(data: Data, filename: String, reason: String) async -> TravelAlbumOTGPhotoRecord? {
        guard let albumID else {
            OTGLog.warning(.nikon, "\(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(.nikon, "\(reason): save failed: \(error.localizedDescription)")
            return nil
        }
    }
}
