import AVFoundation
import Foundation
import UIKit

// MARK: - PreloadAdapter

/**
 * PreloadAdapter — Two-layer preload system as a CommandInterceptor.
 *
 * Layer 1 (Cache Pre-download): Downloads upcoming queue items into disk cache
 * via background URLSession requests. Uses CacheAdapter.isCached() to skip
 * already-cached content.
 *
 * Layer 2 (Warm Pool): Pre-constructs AVPlayerItem instances for upcoming items
 * by running them through the adapter pipeline (DRM + Cache interceptors).
 * Items are stored in warmPool and consumed during CMD_DECODER_LOAD via
 * LoadContext.preloadedItem.
 *
 * Eviction:
 *   - Queue change: stale items (no longer in upcoming set) evicted
 *   - Memory pressure: UIApplication.didReceiveMemoryWarningNotification clears all
 *   - TTL: 5-minute expiry on warm pool items
 *
 * Metrics:
 *   - PRELOAD_STARTED / PRELOAD_COMPLETED emitted via C event bus per layer
 *   - PRELOAD_FAILED emitted via metricEmitter callback (no dedicated C event type)
 *
 * Threading:
 *   All warm pool reads/writes on preloadQueue (serial).
 *   intercept() uses preloadQueue.sync for dictionary lookup (cheap).
 *   Background work uses preloadQueue.async.
 */
class PreloadAdapter: NSObject, CommandInterceptor {
    // MARK: - Types

    typealias MetricEmitter = (
        _ type: String,
        _ uri: String?,
        _ bytes: Double?,
        _ layer: Int?,
        _ durationMs: Double?,
        _ source: String?,
        _ error: String?
    ) -> Void

    // MARK: - Configuration

    /// Number of upcoming items to preload (configurable via setPreloadConfig)
    var itemCount: Int = 2

    /// Time-to-live for warm pool items in seconds (configurable via setPreloadConfig)
    var ttlSeconds: TimeInterval = 300

    /// Per-item size cap for Layer 1 pre-downloads, in bytes.
    /// If the server reports Content-Length above this, the download is skipped
    /// (we will still stream the item via the normal cache path at play time).
    /// Default 100 MB. Set to 0 to disable the cap.
    var maxBytesPerItem: Int64 = 100 * 1024 * 1024

    /// Whether preloading is enabled. When false, intercept() passes through
    /// and background preloading is skipped.
    var enabled: Bool = false

    // MARK: - Dependencies

    private var eventBusPtr: OpaquePointer?
    private var coordinatorPtr: OpaquePointer?
    private let cacheAdapter: CacheAdapter
    private weak var pipeline: AdapterPipeline?

    typealias QueueItemProvider = (Int) -> (any HybridMediaItemSpec)?
    private var queueItemProvider: QueueItemProvider?

    // MARK: - State

    private let preloadQueue = DispatchQueue(label: "aviation.preload")
    private var warmPool: [String: AVPlayerItem] = [:]
    private var warmPoolTimestamps: [String: Date] = [:]
    /// Tracks in-flight Layer 1 download tasks so teardown/pause can cancel them.
    private var activeDownloads: [String: URLSessionDownloadTask] = [:]
    private var domainSubscriptionId: UInt32 = 0
    private var memoryWarningObserver: NSObjectProtocol?
    private var backgroundObserver: NSObjectProtocol?
    private var foregroundObserver: NSObjectProtocol?
    private var isBackgrounded: Bool = false

    // MARK: - Initialization

    init(
        eventBus: OpaquePointer?,
        coordinator: OpaquePointer?,
        cacheAdapter: CacheAdapter,
        pipeline: AdapterPipeline,
        queueItemProvider: @escaping QueueItemProvider
    ) {
        eventBusPtr = eventBus
        coordinatorPtr = coordinator
        self.cacheAdapter = cacheAdapter
        self.pipeline = pipeline
        self.queueItemProvider = queueItemProvider
        super.init()

        subscribeToDomainEvents()
        subscribeToMemoryWarning()
        subscribeToBackgroundNotifications()
    }

    // MARK: - CommandInterceptor

    func intercept(event _: AviationCommandEvent, context: LoadContext, next: @escaping () -> Void) {
        // Skip entirely if preloading is disabled
        guard enabled else { next(); return }

        // Check for warm pool item synchronously (cheap dictionary lookup)
        preloadQueue.sync {
            if let warmItem = warmPool.removeValue(forKey: context.uri) {
                warmPoolTimestamps.removeValue(forKey: context.uri)
                context.preloadedItem = warmItem
                context.cacheHit = true
                #if DEBUG
                    NSLog("[Aviation.Preload] Warm pool hit for %@", context.uri)
                #endif
            }
        }
        next()
    }

    // MARK: - Domain Event Subscription

    /// Subscribe to C event bus domain events for preload triggers.
    private func subscribeToDomainEvents() {
        guard let bus = eventBusPtr else { return }

        let selfPtr = Unmanaged.passUnretained(self).toOpaque()
        domainSubscriptionId = aviation_event_bus_subscribe_domain(
            bus,
            { event, userData in
                guard let event, let userData else { return }
                let adapter = Unmanaged<PreloadAdapter>.fromOpaque(userData).takeUnretainedValue()
                let eventCopy = event.pointee
                adapter.preloadQueue.async { [weak adapter] in
                    adapter?.handleDomainEvent(eventCopy)
                }
            },
            selfPtr
        )
    }

    /// Handle domain events — react to item change and queue mutations.
    private func handleDomainEvent(_ event: AviationDomainEvent) {
        // Skip background preloading when disabled
        guard enabled else { return }

        let eventType = event.type
        guard eventType == Int32(AVIATION_EVT_CURRENT_ITEM_CHANGED.rawValue) ||
            eventType == Int32(AVIATION_EVT_QUEUE_ITEM_CHANGED.rawValue)
        else {
            return
        }
        preloadUpcomingItems()
    }

    // MARK: - Memory Warning

    private func subscribeToMemoryWarning() {
        memoryWarningObserver = NotificationCenter.default.addObserver(
            forName: UIApplication.didReceiveMemoryWarningNotification,
            object: nil,
            queue: nil
        ) { [weak self] _ in
            self?.handleMemoryWarning()
        }
    }

    private func handleMemoryWarning() {
        preloadQueue.async { [weak self] in
            guard let self else { return }
            let count = warmPool.count
            warmPool.removeAll()
            warmPoolTimestamps.removeAll()
            if count > 0 {
                #if DEBUG
                    NSLog("[Aviation.Preload] Memory warning — evicted %d warm pool items", count)
                #endif
            }
        }
    }

    // MARK: - Background/Foreground Notifications

    private func subscribeToBackgroundNotifications() {
        backgroundObserver = NotificationCenter.default.addObserver(
            forName: UIApplication.didEnterBackgroundNotification,
            object: nil,
            queue: .main
        ) { [weak self] _ in
            self?.pausePreloading()
        }

        foregroundObserver = NotificationCenter.default.addObserver(
            forName: UIApplication.willEnterForegroundNotification,
            object: nil,
            queue: .main
        ) { [weak self] _ in
            self?.resumePreloading()
        }
    }

    private func pausePreloading() {
        preloadQueue.async { [weak self] in
            guard let self else { return }
            isBackgrounded = true

            let downloadCount = activeDownloads.count

            // Cancel all tracked download tasks and drop the references.
            for (_, task) in activeDownloads {
                task.cancel()
            }
            activeDownloads.removeAll()

            #if DEBUG
                NSLog("[Aviation.Preload] Background pause - cancelled %d active downloads", downloadCount)
            #endif
        }
    }

    private func resumePreloading() {
        preloadQueue.async { [weak self] in
            guard let self else { return }
            isBackgrounded = false

            #if DEBUG
                NSLog("[Aviation.Preload] Foreground resume - preloading enabled")
            #endif

            // Resume processing preload queue from where it left off
            // This will be triggered naturally by the next domain event
        }
    }

    // MARK: - Preload Logic

    /// Preload upcoming queue items (both layers).
    /// Called on preloadQueue.
    private func preloadUpcomingItems() {
        guard let coord = coordinatorPtr else { return }

        // Resolve upcoming indices using C queue API (respects shuffle/repeat)
        var indices = [Int32](repeating: -1, count: itemCount)
        let count = aviation_coordinator_queue_resolve_next_n(coord, &indices, Int32(itemCount))
        guard count > 0 else { return }

        let validIndices = indices.prefix(Int(count)).filter { $0 >= 0 }

        // Look up items via provider (must dispatch to main for thread safety)
        DispatchQueue.main.async { [weak self] in
            guard let self, let provider = queueItemProvider else { return }

            var upcomingItems: [(uri: String, item: any HybridMediaItemSpec)] = []
            for idx in validIndices {
                if let item = provider(Int(idx)) {
                    upcomingItems.append((uri: item.uri, item: item))
                }
            }

            // Dispatch back to preload queue for warm pool work
            preloadQueue.async { [weak self] in
                guard let self else { return }

                let upcomingURIs = Set(upcomingItems.map(\.uri))

                // Evict stale items (not in upcoming set)
                evictStaleItems(upcomingURIs: upcomingURIs)

                // Evict expired TTL items
                evictExpiredItems()

                // Preload each upcoming item
                for (uri, item) in upcomingItems {
                    // Skip if already warm or actively downloading
                    guard warmPool[uri] == nil,
                          activeDownloads[uri] == nil else { continue }

                    // Layer 1: Pre-download to disk cache (if not already cached)
                    preloadLayer1(uri: uri, item: item)

                    // Layer 2: Construct warm AVPlayerItem (independent of L1)
                    preloadLayer2(uri: uri, item: item)
                }
            }
        }
    }

    // MARK: - Imperative Preload

    /// Imperatively preload a list of items (both L1 + L2).
    /// Called from the bridge's preload(items:) method.
    /// Unlike automatic preloading, this does NOT evict stale items
    /// (the caller explicitly chose these items).
    func preloadItems(_ items: [any HybridMediaItemSpec]) {
        guard enabled else { return }

        preloadQueue.async { [weak self] in
            guard let self else { return }

            evictExpiredItems()

            for item in items {
                let uri = item.uri
                guard warmPool[uri] == nil,
                      activeDownloads[uri] == nil else { continue }

                preloadLayer1(uri: uri, item: item)
                preloadLayer2(uri: uri, item: item)
            }
        }
    }

    // MARK: - Layer 1: Cache Pre-download

    /// Pre-download content to disk cache via a streaming URLSession download task.
    ///
    /// Uses `downloadTask` (streams to a temp file on disk) instead of `dataTask`
    /// (which buffers the entire response in RAM) so large assets cannot OOM the
    /// process during preload. The `maxBytesPerItem` budget short-circuits
    /// downloads whose Content-Length exceeds the cap.
    private func preloadLayer1(uri: String, item: any HybridMediaItemSpec) {
        // Skip if already cached or app is backgrounded
        guard !cacheAdapter.isCached(uri: uri) else { return }
        guard !isBackgrounded else { return }

        // Skip DRM and live content
        guard !item.hasDrm else { return }
        let isLive = item.mediaType == .audiolive || item.mediaType == .videolive
        guard !isLive else { return }

        // Skip content the cache interceptor never serves from disk on iOS:
        // video (resource-loader proxying is disabled for it) and adaptive
        // streams (segments bypass the delegate). Pre-downloading them wastes
        // disk and network. Warm-pool (L2) preload still covers them.
        let isVideo = item.mediaType == .videoondemand || item.mediaType == .videolive
        guard !isVideo else { return }

        guard let url = URL(string: uri) else { return }

        // Nothing to pre-download for a file already on disk.
        guard !url.isFileURL else { return }
        let ext = url.pathExtension.lowercased()
        guard ext != "m3u8", ext != "mpd" else { return }

        let startTime = CFAbsoluteTimeGetCurrent()
        emitPreloadEvent(code: AviationExtCode.preloadStarted, uri: uri, layer: 1)

        // Background download with low priority
        var request = URLRequest(url: url)
        request.networkServiceType = .background
        if let nativeItem = item as? HybridMediaItem,
           let headers = nativeItem.headers
        {
            for (key, value) in headers {
                request.setValue(value, forHTTPHeaderField: key)
            }
        }

        let budget = maxBytesPerItem
        let task = URLSession.shared.downloadTask(with: request) { [weak self] tempURL, response, error in
            guard let self else { return }

            preloadQueue.async {
                self.activeDownloads.removeValue(forKey: uri)

                if let error {
                    // Cancellations are expected during eviction/teardown — don't log at error level.
                    let nsErr = error as NSError
                    if nsErr.domain != NSURLErrorDomain || nsErr.code != NSURLErrorCancelled {
                        NSLog("[Aviation.Preload] L1 download failed for %@: %@", uri, error.localizedDescription)
                    }
                    return
                }

                guard let tempURL else {
                    NSLog("[Aviation.Preload] L1 empty response for %@", uri)
                    return
                }

                // Enforce the per-item byte budget using reported Content-Length.
                if budget > 0,
                   let http = response as? HTTPURLResponse,
                   http.expectedContentLength > 0,
                   http.expectedContentLength > budget
                {
                    NSLog(
                        "[Aviation.Preload] L1 skipped %@ — %lld bytes exceeds maxBytesPerItem (%lld)",
                        uri, http.expectedContentLength, budget
                    )
                    try? FileManager.default.removeItem(at: tempURL)
                    return
                }

                // Move the streamed temp file into the cache directory under the
                // cache key, then register it with the cache adapter so the file
                // participates in size accounting, LRU eviction, and content-type
                // resolution like any resource-loader-written entry.
                let filePath = self.cacheAdapter.cachedFileURL(for: url)

                do {
                    try? FileManager.default.createDirectory(
                        at: filePath.deletingLastPathComponent(),
                        withIntermediateDirectories: true
                    )
                    if FileManager.default.fileExists(atPath: filePath.path) {
                        try FileManager.default.removeItem(at: filePath)
                    }
                    try FileManager.default.moveItem(at: tempURL, to: filePath)
                    let attrs = try? FileManager.default.attributesOfItem(atPath: filePath.path)
                    let bytes = (attrs?[.size] as? NSNumber)?.int64Value ?? 0
                    let mimeType = (response as? HTTPURLResponse)?.mimeType
                    self.cacheAdapter.registerCachedFile(for: url, sizeBytes: bytes, mimeType: mimeType)
                    let durationMs = Int64((CFAbsoluteTimeGetCurrent() - startTime) * 1000)
                    self.emitPreloadEvent(code: AviationExtCode.preloadCompleted, uri: uri, layer: 1, durationMs: durationMs)
                    #if DEBUG
                        NSLog("[Aviation.Preload] L1 cached %@ (%lld bytes, %lldms)", uri, bytes, durationMs)
                    #endif
                } catch {
                    NSLog("[Aviation.Preload] L1 write failed for %@: %@", uri, error.localizedDescription)
                }
            }
        }

        activeDownloads[uri] = task
        task.resume()
    }

    // MARK: - Layer 2: Warm Pool

    /// Construct an AVPlayerItem through the adapter pipeline (DRM + Cache).
    private func preloadLayer2(uri: String, item: any HybridMediaItemSpec) {
        guard let pipeline else { return }
        guard !isBackgrounded else { return }

        // Skip DRM content for warm pool (DRM sessions can't be pre-shared easily)
        guard !item.hasDrm else { return }
        let isLive = item.mediaType == .audiolive || item.mediaType == .videolive
        guard !isLive else { return }

        let startTime = CFAbsoluteTimeGetCurrent()
        emitPreloadEvent(code: AviationExtCode.preloadStarted, uri: uri, layer: 2)

        // Create synthetic command event and load context
        var syntheticEvent = AviationCommandEvent()
        syntheticEvent.type = Int32(AVIATION_CMD_DECODER_LOAD.rawValue)
        let nativeItem = item as? HybridMediaItem

        // Copy URI into the event
        uri.withCString { uriPtr in
            withUnsafeMutablePointer(to: &syntheticEvent.uri) { dest in
                _ = strncpy(
                    UnsafeMutableRawPointer(dest).assumingMemoryBound(to: CChar.self),
                    uriPtr,
                    Int(AVIATION_MAX_URI_LEN) - 1
                )
            }
        }
        if let nativeItem {
            syntheticEvent.nativeHandle = Unmanaged.passUnretained(nativeItem).toOpaque()
        }

        let context = LoadContext(event: syntheticEvent, nativeItem: nativeItem)

        // Run through DRM + Cache interceptors (skipping PreloadAdapter)
        pipeline.dispatchPreload(event: syntheticEvent, context: context) { [weak self] ctx in
            guard let self else { return }

            preloadQueue.async {
                // Create AVPlayerItem from prepared asset or plain asset
                let playerItem: AVPlayerItem
                if let preparedAsset = ctx.preparedAsset {
                    playerItem = AVPlayerItem(asset: preparedAsset)
                } else {
                    guard let url = URL(string: uri) else {
                        NSLog("[Aviation.Preload] L2 failed: invalid URL %@", uri)
                        return
                    }
                    var opts: [String: Any]? = nil
                    if let headers = nativeItem?.headers, !headers.isEmpty {
                        opts = ["AVURLAssetHTTPHeaderFieldsKey": headers]
                    }
                    let asset = opts != nil ? AVURLAsset(url: url, options: opts!) : AVURLAsset(url: url)
                    playerItem = AVPlayerItem(asset: asset)
                }

                // Store in warm pool
                self.warmPool[uri] = playerItem
                self.warmPoolTimestamps[uri] = Date()

                let durationMs = Int64((CFAbsoluteTimeGetCurrent() - startTime) * 1000)
                self.emitPreloadEvent(code: AviationExtCode.preloadCompleted, uri: uri, layer: 2, durationMs: durationMs)
                #if DEBUG
                    NSLog("[Aviation.Preload] L2 warm pool ready for %@ (%lldms)", uri, durationMs)
                #endif
            }
        }
    }

    // MARK: - Eviction

    /// Remove warm pool items that are no longer in the upcoming set and
    /// cancel any in-flight Layer 1 downloads for the same URIs.
    private func evictStaleItems(upcomingURIs: Set<String>) {
        let staleKeys = warmPool.keys.filter { !upcomingURIs.contains($0) }
        for key in staleKeys {
            warmPool.removeValue(forKey: key)
            warmPoolTimestamps.removeValue(forKey: key)
            #if DEBUG
                NSLog("[Aviation.Preload] Evicted stale warm pool item: %@", key)
            #endif
        }

        let staleDownloadKeys = activeDownloads.keys.filter { !upcomingURIs.contains($0) }
        for key in staleDownloadKeys {
            if let task = activeDownloads.removeValue(forKey: key) {
                task.cancel()
                #if DEBUG
                    NSLog("[Aviation.Preload] Cancelled stale download: %@", key)
                #endif
            }
        }
    }

    /// Remove warm pool items older than TTL.
    private func evictExpiredItems() {
        let now = Date()
        let expiredKeys = warmPoolTimestamps.filter { now.timeIntervalSince($0.value) > ttlSeconds }.map(\.key)
        for key in expiredKeys {
            warmPool.removeValue(forKey: key)
            warmPoolTimestamps.removeValue(forKey: key)
            #if DEBUG
                NSLog("[Aviation.Preload] Evicted expired warm pool item: %@", key)
            #endif
        }
    }

    // MARK: - Preload Event Emission via C Event Bus

    /// Emit a preload lifecycle event as adapter telemetry through the C
    /// event bus (AVIATION_EXT_PRELOAD_*; slots in aviation_extension_events.h).
    private func emitPreloadEvent(code: Int32, uri: String, layer: Int32, durationMs: Int64 = 0, error: String? = nil) {
        guard let bus = eventBusPtr else { return }
        AviationExt.emit(
            AviationExt.event(
                code: code,
                values: [Int64(layer), durationMs],
                text: uri,
                aux: error ?? ""
            ),
            on: bus
        )
    }

    // MARK: - Teardown

    /// Evict all warm pool items. Called when cache is cleared to prevent
    /// stale preloaded items from referencing deleted cache files.
    func clearWarmPool() {
        preloadQueue.async { [weak self] in
            guard let self else { return }
            let count = warmPool.count
            warmPool.removeAll()
            warmPoolTimestamps.removeAll()
            if count > 0 {
                #if DEBUG
                    NSLog("[Aviation.Preload] Cache cleared — evicted %d warm pool items", count)
                #endif
            }
        }
    }

    /// Clean up all preload state. Called by engine on destroy.
    func teardown() {
        // Unsubscribe from domain events
        if domainSubscriptionId > 0, let bus = eventBusPtr {
            aviation_event_bus_unsubscribe_domain(bus, domainSubscriptionId)
            domainSubscriptionId = 0
        }

        // Remove memory warning observer
        if let observer = memoryWarningObserver {
            NotificationCenter.default.removeObserver(observer)
            memoryWarningObserver = nil
        }

        // Remove background/foreground observers
        if let observer = backgroundObserver {
            NotificationCenter.default.removeObserver(observer)
            backgroundObserver = nil
        }
        if let observer = foregroundObserver {
            NotificationCenter.default.removeObserver(observer)
            foregroundObserver = nil
        }

        // Cancel active downloads so the temp files are released promptly.
        for (_, task) in activeDownloads {
            task.cancel()
        }
        activeDownloads.removeAll()

        // Clear warm pool
        warmPool.removeAll()
        warmPoolTimestamps.removeAll()

        queueItemProvider = nil
        coordinatorPtr = nil

        #if DEBUG
            NSLog("[Aviation.Preload] Teardown complete")
        #endif
    }
}
