import CoreGraphics
import Foundation
import ImageCaptureCore

final class ImageCaptureDeviceManager: NSObject {
    var onConnectionStateChange: ((CameraConnectionState) -> Void)?
    var onNewItems: (([ICCameraItem]) -> Void)?
    var onBaselineCatalogReady: (() -> Void)?
    var onPollTick: (() -> Void)?
    var onPTPAssetReady: ((CameraAsset, URL) -> Void)?
    var onError: ((String) -> Void)?

    private(set) var cameraDevice: ICCameraDevice?
    private(set) var isCatalogReady = false
    private var deviceBrowser: ICDeviceBrowser?
    private let downloadDelegate = CameraDownloadDelegate()
    private var transactionID: UInt32 = 1
    private var pendingDownloads: [String: CheckedContinuation<URL, Error>] = [:]
    private var ptpAssetURLs: [String: URL] = [:]
    private var pollTimer: Timer?
    private var baselineNotified = false
    private var isRemoteSessionReady = false
    private var isDownloadingPTPObject = false
    private var propertyCheckTask: Task<Void, Never>?
    private var shotBufferPollTimer: Timer?
    private var lastDownloadedObjectSignature: String?
    private var lastShootingFileInfoValue: Int64?
    private var shotDownloadQueue: [(handle: UInt32, reason: String)] = []
    private var isDrainingShotQueue = false
    private var isEvaluatingShotBuffer = false
    private var propertyCheckCoalesceScheduled = false
    private var isExternalAttach = false
    private var sessionSetupTask: Task<Void, Never>?
    private var sessionCloseContinuation: CheckedContinuation<Void, Never>?
    private var sessionCloseTimeoutTask: Task<Void, Never>?
    private var closingDeviceUUID: String?
    private var shotDownloadDrainTask: Task<Void, Never>?
    private var coalescedCheckTask: Task<Void, Never>?
    private var isDisconnecting = false

    var isBrowsing: Bool { deviceBrowser != nil || isExternalAttach }

    /// 跳过 Browser，直接连接 AutoDetect 已发现并授权的设备。
    func attach(to camera: ICCameraDevice) {
        guard cameraDevice == nil else {
            CameraTetheringLogger.log("Sony attach: 已有连接中的相机，忽略")
            return
        }
        isExternalAttach = true
        CameraTetheringLogger.log("Sony attach: \(camera.name ?? "Unknown")")
        openSession(for: camera)
    }

    func startBrowsing() {
        guard deviceBrowser == nil else {
            CameraTetheringLogger.log("已在监听 USB 相机")
            if cameraDevice == nil {
                updateState(.searching)
            }
            return
        }

        CameraTetheringLogger.log("开始搜索 USB 相机…")
        updateState(.searching)

        let browser = ICDeviceBrowser()
        browser.delegate = self
        if #available(iOS 15.2, *) {
            let mask = ICDeviceTypeMask(
                rawValue: ICDeviceTypeMask.camera.rawValue | ICDeviceLocationTypeMask.local.rawValue
            ) ?? .camera
            browser.browsedDeviceTypeMask = mask
            CameraTetheringLogger.log("已设置 browsedDeviceTypeMask = Camera | Local USB")
        }
        deviceBrowser = browser
        browser.start()
    }

    func stopBrowsing() async {
        CameraTetheringLogger.log("停止搜索并断开")
        propertyCheckTask?.cancel()
        shotDownloadQueue.removeAll()
        isDrainingShotQueue = false
        isEvaluatingShotBuffer = false
        propertyCheckCoalesceScheduled = false
        propertyCheckTask = nil
        stopShotBufferPolling()
        stopPolling()
        await disconnect()
        if !isExternalAttach {
            deviceBrowser?.stop()
        }
        deviceBrowser = nil
        isExternalAttach = false
        isCatalogReady = false
        baselineNotified = false
        updateState(.disconnected)
    }

    func disconnect() async {
        guard !isDisconnecting else { return }
        isDisconnecting = true
        defer { isDisconnecting = false }

        sessionSetupTask?.cancel()
        sessionSetupTask = nil
        shotDownloadDrainTask?.cancel()
        shotDownloadDrainTask = nil
        coalescedCheckTask?.cancel()
        coalescedCheckTask = nil
        propertyCheckTask?.cancel()
        propertyCheckTask = nil

        let shouldClosePTP = isRemoteSessionReady
        isRemoteSessionReady = false
        isDownloadingPTPObject = false

        shotDownloadQueue.removeAll()
        isDrainingShotQueue = false
        isEvaluatingShotBuffer = false
        propertyCheckCoalesceScheduled = false
        stopShotBufferPolling()
        stopPolling()
        cancelPendingDownloads()

        guard let camera = cameraDevice else {
            resetDisconnectLocalState()
            return
        }

        let deviceUUID = camera.uuidString ?? ""
        camera.ptpEventHandler = { _ in }

        if shouldClosePTP {
            var localTransactionID = transactionID
            await SonyPTPHelper.teardownRemoteSession(on: camera, transactionID: &localTransactionID)
        }

        await waitForSessionClose(on: camera, deviceUUID: deviceUUID)
        camera.delegate = nil
        resetDisconnectLocalState()
    }

    private func cancelPendingDownloads() {
        let disconnected = CameraServiceError.notConnected
        for (_, continuation) in pendingDownloads {
            continuation.resume(throwing: disconnected)
        }
        pendingDownloads.removeAll()
    }

    private func waitForSessionClose(on camera: ICCameraDevice, deviceUUID: String) async {
        sessionCloseTimeoutTask?.cancel()
        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
            sessionCloseContinuation = continuation
            closingDeviceUUID = deviceUUID
            camera.requestCloseSession()

            sessionCloseTimeoutTask = Task { [weak self] in
                try? await Task.sleep(nanoseconds: 3_000_000_000)
                guard let self, self.sessionCloseContinuation != nil else { return }
                CameraTetheringLogger.log("会话关闭超时，继续 teardown")
                self.finishSessionClose()
            }
        }
        sessionCloseTimeoutTask?.cancel()
        sessionCloseTimeoutTask = nil
    }

    private func finishSessionClose() {
        sessionCloseContinuation?.resume()
        sessionCloseContinuation = nil
        closingDeviceUUID = nil
    }

    private func resetDisconnectLocalState() {
        cameraDevice = nil
        transactionID = 1
        isCatalogReady = false
        baselineNotified = false
        isRemoteSessionReady = false
        ptpAssetURLs.removeAll()
        lastDownloadedObjectSignature = nil
        lastShootingFileInfoValue = nil
        shotDownloadQueue.removeAll()
        isDrainingShotQueue = false
        isEvaluatingShotBuffer = false
        propertyCheckCoalesceScheduled = false
        isExternalAttach = false
    }

    func localURL(forAssetID assetID: String) -> URL? {
        ptpAssetURLs[assetID]
    }

    func allFiles() -> [ICCameraFile] {
        guard let camera = cameraDevice else { return [] }

        if let mediaFiles = camera.mediaFiles, !mediaFiles.isEmpty {
            return mediaFiles.compactMap { $0 as? ICCameraFile }
        }
        return collectFiles(from: camera.contents ?? [])
    }

    func file(forAssetID assetID: String) -> ICCameraFile? {
        allFiles().first { assetIdentifier(for: $0) == assetID }
    }

    func download(file: ICCameraFile, to directory: URL) async throws -> URL {
        guard let camera = cameraDevice else {
            throw CameraServiceError.notConnected
        }

        let filename = file.name ?? "photo_\(UUID().uuidString).jpg"
        let assetID = assetIdentifier(for: file)

        CameraTetheringLogger.log("开始下载 \(filename), handle=\(file.ptpObjectHandle)", logger: CameraTetheringLogger.transfer)

        return try await withCheckedThrowingContinuation { continuation in
            pendingDownloads[assetID] = continuation

            downloadDelegate.onComplete = { [weak self] completedFile, url, error in
                guard let self else { return }
                let completedID = self.assetIdentifier(for: completedFile)
                guard let pending = self.pendingDownloads.removeValue(forKey: completedID) else { return }

                if let error {
                    pending.resume(throwing: error)
                } else if let url {
                    pending.resume(returning: url)
                } else {
                    pending.resume(throwing: CameraServiceError.downloadFailed("未知错误"))
                }
            }

            let options: [ICDownloadOption: Any] = [
                .downloadsDirectoryURL: directory,
                .savedFilename: filename,
                .overwrite: true
            ]

            camera.requestDownloadFile(
                file,
                options: options,
                downloadDelegate: downloadDelegate,
                didDownloadSelector: #selector(CameraDownloadDelegate.didDownloadFile(_:error:options:contextInfo:)),
                contextInfo: nil
            )
        }
    }

    func assetIdentifier(for file: ICCameraFile) -> String {
        if file.ptpObjectHandle != 0 {
            return "ptp_\(file.ptpObjectHandle)"
        }
        if let persistentID = file.value(forKey: "persistentID") as? String, !persistentID.isEmpty {
            return persistentID
        }
        let name = file.name ?? "unknown"
        let size = file.fileSize
        let date = file.creationDate?.timeIntervalSince1970 ?? 0
        return "\(name)_\(size)_\(Int(date))"
    }

    func makeAsset(from file: ICCameraFile) -> CameraAsset {
        let filename = file.name ?? "unknown"
        let isMovie = file.duration > 0 || Self.movieExtensions.contains(where: { filename.lowercased().hasSuffix($0) })
        return CameraAsset(
            id: assetIdentifier(for: file),
            filename: filename,
            fileSize: file.fileSize,
            creationDate: file.creationDate,
            isMovie: isMovie
        )
    }

    private static let movieExtensions = [".mp4", ".mov", ".m4v", ".avi"]

    private func collectFiles(from items: [ICCameraItem]) -> [ICCameraFile] {
        var files: [ICCameraFile] = []
        for item in items {
            if let file = item as? ICCameraFile {
                files.append(file)
            } else if let folder = item as? ICCameraFolder {
                files.append(contentsOf: collectFiles(from: folder.contents ?? []))
            }
        }
        return files
    }

    private func updateState(_ state: CameraConnectionState) {
        CameraTetheringLogger.log("连接状态: \(state.displayText)")
        onConnectionStateChange?(state)
    }

    private func requestAuthorizations(completion: @escaping (Bool, Bool) -> Void) {
        guard let browser = deviceBrowser else {
            completion(false, false)
            return
        }

        var contentsGranted = false
        var controlGranted = false
        let group = DispatchGroup()

        group.enter()
        browser.requestContentsAuthorization { status in
            contentsGranted = status == .authorized
            CameraTetheringLogger.log("内容授权: \(String(describing: status))")
            group.leave()
        }

        group.enter()
        browser.requestControlAuthorization { status in
            controlGranted = status == .authorized
            CameraTetheringLogger.log("控制授权: \(String(describing: status))")
            group.leave()
        }

        group.notify(queue: .main) {
            completion(contentsGranted, controlGranted)
        }
    }

    private func openSession(for camera: ICCameraDevice) {
        updateState(.connecting)
        camera.delegate = self
        cameraDevice = camera
        CameraTetheringLogger.log("打开相机会话: \(camera.name ?? "Unknown")")
        camera.requestOpenSession()
    }

    private func configureCameraAfterSession(_ camera: ICCameraDevice) {
        let capabilities = camera.capabilities.joined(separator: ", ")
        CameraTetheringLogger.log("相机能力: [\(capabilities)]")
        CameraTetheringLogger.log("tetheredCaptureEnabled=\(camera.tetheredCaptureEnabled)")

        camera.ptpEventHandler = { [weak self] eventData in
            self?.handlePTPEvent(eventData)
        }

        sessionSetupTask = Task { [weak self] in
            guard let self else { return }
            let updatedID = await SonyPTPHelper.initializeRemoteSession(
                on: camera,
                startingTransactionID: transactionID
            )
            guard !Task.isCancelled, !self.isDisconnecting else { return }
            transactionID = updatedID
            isRemoteSessionReady = true
            startShotBufferPolling()
            schedulePropertyBasedShotCheck(reason: "PostHandshake", delay: 0.2)
        }
    }

    private func handlePTPEvent(_ eventData: Data) {
        guard !isDisconnecting, isRemoteSessionReady else { return }
        let hex = eventData.prefix(16).map { String(format: "%02X", $0) }.joined(separator: " ")
        CameraTetheringLogger.log("收到 PTP 事件: \(hex)", logger: CameraTetheringLogger.ptp)

        guard let container = try? SonyPTPHelper.parseContainer(eventData) else {
            CameraTetheringLogger.log("PTP 事件解析失败", logger: CameraTetheringLogger.ptp)
            return
        }

        CameraTetheringLogger.log(
            "PTP 事件码: 0x\(String(format: "%04X", container.code)), params=\(container.params)",
            logger: CameraTetheringLogger.ptp
        )

        switch container.code {
        case SonyPTPCommand.objectAdded, SonyPTPCommand.sdieObjectAdded:
            guard isRemoteSessionReady else { break }
            let handle = container.params.first ?? SonyPTPCommand.shotObjectHandle
            enqueueShotDownload(handle: handle, reason: "ObjectAdded")
        case SonyPTPCommand.sdieCapturedEvent:
            guard isRemoteSessionReady else { break }
            enqueueShotDownload(handle: SonyPTPCommand.shotObjectHandle, reason: "CapturedEvent")
        case SonyPTPCommand.sdieObjectRemoved:
            lastShootingFileInfoValue = nil
        case SonyPTPCommand.sdieDevicePropChanged:
            guard isRemoteSessionReady else { break }
            schedulePropertyBasedShotCheck(reason: "DevicePropChanged")
        default:
            break
        }
    }

    private func schedulePropertyBasedShotCheck(reason: String, delay: TimeInterval = 0.12) {
        guard !isDisconnecting else { return }
        if isEvaluatingShotBuffer {
            propertyCheckCoalesceScheduled = true
            return
        }
        propertyCheckTask?.cancel()
        propertyCheckTask = Task { [weak self] in
            try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
            guard !Task.isCancelled else { return }
            await self?.runPropertyBasedShotCheck(reason: reason)
        }
    }

    private func runPropertyBasedShotCheck(reason: String) async {
        guard !isDisconnecting else { return }
        if isEvaluatingShotBuffer {
            propertyCheckCoalesceScheduled = true
            return
        }
        isEvaluatingShotBuffer = true
        defer {
            isEvaluatingShotBuffer = false
            if propertyCheckCoalesceScheduled, !isDisconnecting {
                propertyCheckCoalesceScheduled = false
                coalescedCheckTask?.cancel()
                coalescedCheckTask = Task { [weak self] in
                    try? await Task.sleep(nanoseconds: 80_000_000)
                    guard !Task.isCancelled else { return }
                    await self?.runPropertyBasedShotCheck(reason: "Coalesced")
                }
            } else {
                propertyCheckCoalesceScheduled = false
            }
        }
        await evaluateShotBufferFromProperties(reason: reason)
    }

    private func evaluateShotBufferFromProperties(reason: String) async {
        guard !isDisconnecting, isRemoteSessionReady, let camera = cameraDevice else { return }

        var localTransactionID = transactionID
        var properties = await SonyPTPHelper.fetchExtDeviceProperties(
            on: camera,
            transactionID: &localTransactionID,
            onlyDiff: true
        )
        transactionID = localTransactionID

        var shootingFileInfo = properties[SonyPTPCommand.shootingFileInfo]

        // diff 响应常不含 D215（仅其它属性变化），需 fallback 全量读取或直接 Probe
        if shootingFileInfo == nil, shouldFallbackFullPropertyFetch(for: reason) {
            properties = await SonyPTPHelper.fetchExtDeviceProperties(
                on: camera,
                transactionID: &localTransactionID,
                onlyDiff: false
            )
            transactionID = localTransactionID
            shootingFileInfo = properties[SonyPTPCommand.shootingFileInfo]
            CameraTetheringLogger.log("\(reason): diff 无 D215，已读取全量属性", logger: CameraTetheringLogger.ptp)
        }

        if let shootingFileInfo {
            CameraTetheringLogger.log(
                "\(reason): D215(Shooting_File_Info)=0x\(String(format: "%04X", shootingFileInfo))",
                logger: CameraTetheringLogger.ptp
            )
            lastShootingFileInfoValue = shootingFileInfo
        } else {
            CameraTetheringLogger.log("\(reason): 属性包无 D215，改用 Probe 检测缓冲", logger: CameraTetheringLogger.ptp)
        }

        if SonyPTPHelper.isShotBufferReady(shootingFileInfo) {
            _ = await attemptFetchPendingShot(reason: "\(reason)-D215")
            return
        }

        if shouldProbeWithoutD215Ready(for: reason) {
            _ = await attemptFetchPendingShot(reason: "\(reason)-Probe")
        }
    }

    private func shouldFallbackFullPropertyFetch(for reason: String) -> Bool {
        reason.contains("DevicePropChanged")
            || reason.hasPrefix("D215Poll")
            || reason.hasPrefix("Coalesced")
    }

    private func shouldProbeWithoutD215Ready(for reason: String) -> Bool {
        reason.contains("DevicePropChanged")
            || reason.hasPrefix("D215Poll")
            || reason.hasPrefix("Coalesced")
            || reason.hasPrefix("BufferDrain")
    }

    /// Probe 缓冲区内对象；若 filename+size 与上次不同则入队下载。
    @discardableResult
    private func attemptFetchPendingShot(reason: String) async -> Bool {
        guard !isDisconnecting, isRemoteSessionReady, let camera = cameraDevice else { return false }

        var localTransactionID = transactionID
        guard let probe = await SonyPTPHelper.probeShotObject(
            on: camera,
            handle: SonyPTPCommand.shotObjectHandle,
            transactionID: &localTransactionID
        ) else {
            return false
        }
        transactionID = localTransactionID

        let signature = SonyPTPHelper.shotObjectSignature(filename: probe.filename, size: probe.size)
        if signature == lastDownloadedObjectSignature {
            CameraTetheringLogger.log("\(reason): 缓冲无新片 signature=\(signature)", logger: CameraTetheringLogger.ptp)
            return false
        }

        enqueueShotDownload(handle: SonyPTPCommand.shotObjectHandle, reason: reason)
        return true
    }

    private func startShotBufferPolling() {
        stopShotBufferPolling()
        CameraTetheringLogger.log("启动 D215 缓冲轮询 (连拍时 0.25s / 空闲 1.0s)", logger: CameraTetheringLogger.ptp)
        refreshShotBufferPollInterval()
    }

    private func refreshShotBufferPollInterval() {
        guard !isDisconnecting, isRemoteSessionReady else { return }
        let interval = (isDrainingShotQueue || !shotDownloadQueue.isEmpty) ? 0.25 : 1.0
        stopShotBufferPolling()
        shotBufferPollTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
            guard let self, !self.isDisconnecting, self.isRemoteSessionReady else { return }
            Task { await self.evaluateShotBufferFromProperties(reason: "D215Poll") }
        }
    }

    private func stopShotBufferPolling() {
        shotBufferPollTimer?.invalidate()
        shotBufferPollTimer = nil
    }

    /// 连拍时多个 PTP 事件会连续到达；入队串行处理，避免「下载进行中直接丢弃」。
    private func enqueueShotDownload(handle: UInt32, reason: String) {
        guard !isDisconnecting else { return }
        shotDownloadQueue.append((handle: handle, reason: reason))
        CameraTetheringLogger.log(
            "\(reason): 入队 PTP 下载 (队列=\(shotDownloadQueue.count))",
            logger: CameraTetheringLogger.ptp
        )
        refreshShotBufferPollInterval()
        startShotDownloadDrainIfNeeded()
    }

    private func startShotDownloadDrainIfNeeded() {
        guard !isDisconnecting else { return }
        guard !isDrainingShotQueue else { return }
        guard !shotDownloadQueue.isEmpty else { return }

        isDrainingShotQueue = true
        shotDownloadDrainTask?.cancel()
        shotDownloadDrainTask = Task { [weak self] in
            await self?.drainShotDownloadQueue()
        }
    }

    private func drainShotDownloadQueue() async {
        defer {
            isDrainingShotQueue = false
            shotDownloadDrainTask = nil
            if !isDisconnecting {
                refreshShotBufferPollInterval()
                if !shotDownloadQueue.isEmpty {
                    startShotDownloadDrainIfNeeded()
                }
            }
        }

        while !shotDownloadQueue.isEmpty, !isDisconnecting {
            let item = shotDownloadQueue.removeFirst()
            _ = await downloadPTPObject(
                handle: item.handle,
                reason: item.reason,
                maxAttempts: 8,
                tryDirectGetObject: true
            )
            try? await Task.sleep(nanoseconds: 120_000_000)
        }

        guard !isDisconnecting else { return }
        await drainRemainingBuffer(maxExtraDownloads: 12)
    }

    /// 连拍时部分事件可能在队列排空期间才写入缓冲；按 D215/Probe 继续拉取直到无新片。
    private func drainRemainingBuffer(maxExtraDownloads: Int) async {
        for round in 1...maxExtraDownloads {
            guard !isDisconnecting, isRemoteSessionReady, let camera = cameraDevice else { break }

            var localTransactionID = transactionID
            guard let probe = await SonyPTPHelper.probeShotObject(
                on: camera,
                handle: SonyPTPCommand.shotObjectHandle,
                transactionID: &localTransactionID
            ) else {
                break
            }
            transactionID = localTransactionID

            let signature = SonyPTPHelper.shotObjectSignature(filename: probe.filename, size: probe.size)
            if signature == lastDownloadedObjectSignature {
                break
            }

            let downloaded = await downloadPTPObject(
                handle: SonyPTPCommand.shotObjectHandle,
                reason: "BufferDrain-\(round)",
                maxAttempts: 6,
                tryDirectGetObject: true
            )
            guard downloaded else { break }
            try? await Task.sleep(nanoseconds: 100_000_000)
        }
    }

    @discardableResult
    private func downloadPTPObject(
        handle: UInt32,
        reason: String,
        maxAttempts: Int = 3,
        tryDirectGetObject: Bool = false
    ) async -> Bool {
        guard !isDisconnecting, isRemoteSessionReady, let camera = cameraDevice else {
            CameraTetheringLogger.log("\(reason): Sony 远程会话尚未就绪", logger: CameraTetheringLogger.ptp)
            return false
        }
        guard !isDownloadingPTPObject else {
            CameraTetheringLogger.log("\(reason): 下载重入，重新入队", logger: CameraTetheringLogger.ptp)
            shotDownloadQueue.insert((handle: handle, reason: reason), at: 0)
            return false
        }

        isDownloadingPTPObject = true
        defer { isDownloadingPTPObject = false }

        var localTransactionID = transactionID

        for attempt in 1...maxAttempts {
            guard !isDisconnecting else { return false }
            if attempt > 1 {
                try? await Task.sleep(nanoseconds: UInt64(attempt) * 350_000_000)
            }

            var probeFilename = "DSC_\(Int(Date().timeIntervalSince1970)).JPG"
            var probeSize: UInt32 = 0

            if let probe = await SonyPTPHelper.probeShotObject(
                on: camera,
                handle: handle,
                transactionID: &localTransactionID
            ) {
                probeFilename = probe.filename
                probeSize = probe.size
            } else if !tryDirectGetObject {
                if attempt == maxAttempts {
                    CameraTetheringLogger.log("\(reason): 相机缓冲区内暂无可用照片", logger: CameraTetheringLogger.ptp)
                }
                continue
            }

            let signature: String
            if probeSize > 0 {
                signature = SonyPTPHelper.shotObjectSignature(filename: probeFilename, size: probeSize)
                if signature == lastDownloadedObjectSignature {
                    CameraTetheringLogger.log("\(reason): 照片已下载过 \(probeFilename)，跳过", logger: CameraTetheringLogger.ptp)
                    return false
                }
            } else {
                signature = "\(probeFilename)_attempt\(attempt)_\(Int(Date().timeIntervalSince1970))"
            }

            guard let result = await SonyPTPHelper.downloadObject(
                handle: handle,
                on: camera,
                transactionID: &localTransactionID,
                skipObjectInfo: tryDirectGetObject && probeSize == 0
            ) else {
                if attempt == maxAttempts {
                    CameraTetheringLogger.log("\(reason): PTP 下载失败 handle=0x\(String(format: "%08X", handle))", logger: CameraTetheringLogger.ptp)
                }
                continue
            }

            transactionID = localTransactionID
            lastDownloadedObjectSignature = probeSize > 0 ? signature : "\(result.filename)_\(result.data.count)"

            let assetID = "ptp_\(handle)_\(result.filename)_\(result.data.count)"
            let destination = CameraDownloadStorage.uniqueTemporaryURL(for: result.filename)
            do {
                try result.data.write(to: destination, options: .atomic)
            } catch {
                CameraTetheringLogger.log("PTP 文件写入失败: \(error.localizedDescription)", logger: CameraTetheringLogger.transfer)
                return false
            }

            let asset = CameraAsset(
                id: assetID,
                filename: result.filename,
                fileSize: Int64(result.data.count),
                creationDate: Date(),
                isMovie: false
            )
            ptpAssetURLs[assetID] = destination

            CameraTetheringLogger.log(
                "[\(reason)] PTP 下载完成: \(result.filename) -> \(destination.path)",
                logger: CameraTetheringLogger.transfer
            )
            onPTPAssetReady?(asset, destination)
            return true
        }

        return false
    }

    private func notifyBaselineOrPoll(reason: String) {
        guard isCatalogReady else {
            CameraTetheringLogger.log("\(reason): 目录尚未就绪，等待 catalog ready")
            return
        }
        onPollTick?()
    }

    private func notifyBaselineIfNeeded() {
        guard !baselineNotified else { return }
        baselineNotified = true
        isCatalogReady = true

        let fileCount = allFiles().count
        CameraTetheringLogger.log("目录就绪，当前 \(fileCount) 个文件，建立基线")
        onBaselineCatalogReady?()
        startPolling()
    }

    private func startPolling() {
        stopPolling()
        CameraTetheringLogger.log("启动轮询检测新照片 (每 1.5s)")

        pollTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: true) { [weak self] _ in
            guard let self, self.isCatalogReady else { return }
            self.onPollTick?()
        }
    }

    private func stopPolling() {
        pollTimer?.invalidate()
        pollTimer = nil
    }
}

extension ImageCaptureDeviceManager: ICDeviceBrowserDelegate {
    @objc(deviceBrowser:didAddDevice:moreComing:)
    func deviceBrowser(_ browser: ICDeviceBrowser, didAdd device: ICDevice, moreComing: Bool) {
        CameraTetheringLogger.log("发现设备: \(device.name ?? "?"), type=\(device.type), transport=\(device.transportType ?? "?")")

        guard let camera = device as? ICCameraDevice else {
            CameraTetheringLogger.log("非相机设备，忽略")
            return
        }
        guard cameraDevice == nil else {
            CameraTetheringLogger.log("已有连接中的相机，忽略")
            return
        }

        requestAuthorizations { [weak self] contentsGranted, controlGranted in
            guard let self else { return }
            guard contentsGranted else {
                let message = "未获得相机内容访问权限，请在系统设置中允许"
                CameraTetheringLogger.log(message)
                self.onError?(message)
                self.updateState(.error("权限被拒绝"))
                return
            }
            if !controlGranted {
                CameraTetheringLogger.log("警告: 控制授权未通过，仍尝试连接（下载通常只需内容授权）")
            }
            self.openSession(for: camera)
        }
    }

    @objc(deviceBrowser:didRemoveDevice:moreGoing:)
    func deviceBrowser(_ browser: ICDeviceBrowser, didRemove device: ICDevice, moreGoing: Bool) {
        guard !isDisconnecting else { return }
        CameraTetheringLogger.log("设备移除: \(device.name ?? "?")")
        if device.uuidString == cameraDevice?.uuidString {
            Task { await disconnect() }
            updateState(.disconnected)
        }
    }
}

extension ImageCaptureDeviceManager: ICDeviceDelegate {
    func didRemove(_ device: ICDevice) {
        guard !isDisconnecting else { return }
        CameraTetheringLogger.log("didRemove: \(device.name ?? "?")")
        if device.uuidString == cameraDevice?.uuidString {
            Task { await disconnect() }
            updateState(.disconnected)
        }
    }

    func device(_ device: ICDevice, didOpenSessionWithError error: Error?) {
        if let error {
            CameraTetheringLogger.log("会话打开失败: \(error.localizedDescription)")
            onError?(error.localizedDescription)
            updateState(.error(error.localizedDescription))
            return
        }

        guard let camera = device as? ICCameraDevice else { return }

        let name = camera.name ?? "Camera"
        CameraTetheringLogger.log("会话已打开: \(name)")
        updateState(.connected(deviceName: name))
        configureCameraAfterSession(camera)
    }

    func device(_ device: ICDevice, didCloseSessionWithError error: Error?) {
        if let error {
            CameraTetheringLogger.log("会话关闭: \(error.localizedDescription)")
        }
        if device.uuidString == closingDeviceUUID {
            finishSessionClose()
        }
        guard !isDisconnecting else { return }
        if device.uuidString == cameraDevice?.uuidString {
            cameraDevice = nil
            isCatalogReady = false
            baselineNotified = false
            isRemoteSessionReady = false
            stopPolling()
            stopShotBufferPolling()
            updateState(.disconnected)
        }
    }
}

extension ImageCaptureDeviceManager: ICCameraDeviceDelegate {
    @objc(cameraDevice:didAddItems:)
    func cameraDevice(_ camera: ICCameraDevice, didAdd items: [ICCameraItem]) {
        let names = items.compactMap { ($0 as? ICCameraFile)?.name ?? $0.name }
        CameraTetheringLogger.log("didAddItems: \(names.joined(separator: ", "))")

        for item in items {
            if let file = item as? ICCameraFile {
                CameraTetheringLogger.log("  + \(file.name ?? "?") addedAfterCatalog=\(file.wasAddedAfterContentCatalogCompleted)")
            }
        }

        onNewItems?(items)
    }

    @objc(cameraDevice:didRemoveItems:)
    func cameraDevice(_ camera: ICCameraDevice, didRemove items: [ICCameraItem]) {
        let names = items.compactMap { $0.name }
        CameraTetheringLogger.log("didRemoveItems: \(names.joined(separator: ", "))")
    }

    @objc(cameraDevice:didRenameItems:)
    func cameraDevice(_ camera: ICCameraDevice, didRenameItems items: [ICCameraItem]) {
    }

    func cameraDevice(_ camera: ICCameraDevice, didReceiveThumbnail thumbnail: CGImage?, for item: ICCameraItem, error: Error?) {
    }

    func cameraDevice(_ camera: ICCameraDevice, didReceiveMetadata metadata: [AnyHashable: Any]?, for item: ICCameraItem, error: Error?) {
    }

    func deviceDidBecomeReady(withCompleteContentCatalog device: ICCameraDevice) {
        CameraTetheringLogger.log("deviceDidBecomeReadyWithCompleteContentCatalog, 文件数=\(allFiles().count)")
        notifyBaselineIfNeeded()
    }

    func cameraDeviceDidChangeCapability(_ camera: ICCameraDevice) {
        CameraTetheringLogger.log("cameraDeviceDidChangeCapability")
    }

    func cameraDeviceDidRemoveAccessRestriction(_ device: ICDevice) {
        CameraTetheringLogger.log("cameraDeviceDidRemoveAccessRestriction")
    }

    func cameraDeviceDidEnableAccessRestriction(_ device: ICDevice) {
        CameraTetheringLogger.log("cameraDeviceDidEnableAccessRestriction")
    }

    func cameraDevice(_ camera: ICCameraDevice, didReceivePTPEvent eventData: Data) {
        handlePTPEvent(eventData)
    }
}
