//
//  BackendSwitcher.swift
//  Yattee
//
//  Handles seamless switching between player backends during playback.
//

import Foundation

#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif

// MARK: - Switch Animation

/// Animation style for backend switching.
enum BackendSwitchAnimation: Sendable {
    case instant        // No animation, immediate swap
    case crossfade      // Crossfade between views
    case slide          // Slide transition

    var duration: TimeInterval {
        switch self {
        case .instant: return 0
        case .crossfade: return 0.3
        case .slide: return 0.4
        }
    }
}

// MARK: - Switch Result

/// Result of a backend switch operation.
struct BackendSwitchResult: Sendable {
    let success: Bool
    let sourceBackend: PlayerBackendType
    let targetBackend: PlayerBackendType
    let timeDrift: TimeInterval  // Difference between expected and actual time after switch
    let switchDuration: TimeInterval  // How long the switch took
}

// MARK: - Backend Switcher Delegate

/// Delegate for switch progress callbacks.
@MainActor
protocol BackendSwitcherDelegate: AnyObject {
    func switcherWillBeginSwitch(from source: PlayerBackendType, to target: PlayerBackendType)
    func switcherDidPrepareTarget(_ switcher: BackendSwitcher)
    func switcherDidCompleteSwitch(_ result: BackendSwitchResult)
    func switcherDidFailSwitch(_ error: Error)
}

// MARK: - Backend Switcher

/// Manages seamless hot-swapping between player backends.
@MainActor
final class BackendSwitcher {
    // MARK: - Properties

    weak var delegate: BackendSwitcherDelegate?

    /// Whether a switch is currently in progress.
    private(set) var isSwitching: Bool = false

    // MARK: - Dependencies

    /// Factory for creating backend instances.
    private let backendFactory: BackendFactory

    /// Settings manager for quality preferences.
    weak var settingsManager: SettingsManager?

    init(backendFactory: BackendFactory, settingsManager: SettingsManager?) {
        self.backendFactory = backendFactory
        self.settingsManager = settingsManager
    }

    // MARK: - Public Methods

    /// Switch from one backend to another during active playback.
    ///
    /// This method:
    /// 1. Captures the current playback state from the source backend
    /// 2. Selects a compatible stream for the target backend
    /// 3. Initializes the target backend and loads the stream
    /// 4. Seeks to the captured position
    /// 5. Waits for the target to be ready
    /// 6. Performs a smooth visual transition
    /// 7. Resumes playback on the target backend
    /// 8. Cleans up the source backend
    ///
    /// - Parameters:
    ///   - source: The currently active backend
    ///   - targetType: The type of backend to switch to
    ///   - streams: Available streams for the current video
    ///   - animation: Animation style for the transition
    /// - Returns: The new active backend
    /// - Throws: BackendError if the switch fails
    func switchBackend(
        from source: any PlayerBackend,
        to targetType: PlayerBackendType,
        streams: [Stream],
        animation: BackendSwitchAnimation = .crossfade
    ) async throws -> any PlayerBackend {
        guard !isSwitching else {
            throw BackendError.switchFailed("Switch already in progress")
        }

        let startTime = Date()
        isSwitching = true

        defer { isSwitching = false }

        LoggingService.shared.logPlayer("Backend switch starting", details: "From \(source.backendType.rawValue) to \(targetType.rawValue)")
        delegate?.switcherWillBeginSwitch(from: source.backendType, to: targetType)

        // Step 1: Capture current state
        let capturedState = source.captureState()

        // Step 2: Find compatible stream for target backend
        guard let selection = selectStream(for: targetType, from: streams) else {
            throw BackendError.switchFailed("No compatible stream found for \(targetType.displayName)")
        }
        let targetStream = selection.video
        let targetAudioStream = selection.audio

        // Step 3: Prepare source for handoff (pause but keep state)
        source.prepareForHandoff()

        // Step 4: Create and initialize target backend
        let target = try backendFactory.createBackend(type: targetType)

        // Step 5: Load stream on target (without autoplay)
        do {
            let useEDL = settingsManager?.mpvUseEDLStreams ?? true
            try await target.load(stream: targetStream, audioStream: targetAudioStream, autoplay: false, useEDL: useEDL)
        } catch {
            // Rollback: resume source backend
            if capturedState.isPlaying {
                source.play()
            }
            throw BackendError.switchFailed("Failed to load stream on target: \(error.localizedDescription)")
        }

        delegate?.switcherDidPrepareTarget(self)

        // Step 6: Seek to captured position
        if capturedState.currentTime > 0 {
            await target.seek(to: capturedState.currentTime, showLoading: false)
        }

        // Step 7: Restore other state (volume, rate, mute)
        target.volume = capturedState.volume
        target.isMuted = capturedState.isMuted
        target.rate = capturedState.rate

        // Step 8: Perform visual transition
        await performTransition(
            from: source,
            to: target,
            animation: animation
        )

        // Step 9: Resume playback if was playing
        if capturedState.isPlaying {
            target.play()
        }

        // Step 10: Stop source backend
        source.stop()

        // Calculate result metrics
        let switchDuration = Date().timeIntervalSince(startTime)
        let timeDrift = abs(target.currentTime - capturedState.currentTime)

        let result = BackendSwitchResult(
            success: true,
            sourceBackend: source.backendType,
            targetBackend: targetType,
            timeDrift: timeDrift,
            switchDuration: switchDuration
        )

        LoggingService.shared.logPlayer("Backend switch completed", details: "Duration: \(String(format: "%.2f", switchDuration))s, drift: \(String(format: "%.3f", timeDrift))s")
        delegate?.switcherDidCompleteSwitch(result)

        return target
    }

    /// Check if switching to a given backend type is possible.
    func canSwitch(to targetType: PlayerBackendType, streams: [Stream]) -> Bool {
        // Check if we have a compatible stream
        selectStream(for: targetType, from: streams) != nil
    }

    // MARK: - Private Methods

    /// Select the best compatible stream for a backend type.
    private func selectStream(for backendType: PlayerBackendType, from streams: [Stream]) -> (video: Stream, audio: Stream?)? {
        let supportedFormats = backendType.supportedFormats
        let preferredQuality = settingsManager?.preferredQuality ?? .auto

        // Separate streams by type
        let videoOnlyStreams = streams.filter { stream in
            guard !stream.isAudioOnly && stream.isVideoOnly else { return false }
            let format = StreamFormat.detect(from: stream)
            return supportedFormats.contains(format)
        }

        let muxedStreams = streams.filter { stream in
            let format = StreamFormat.detect(from: stream)
            guard supportedFormats.contains(format) else { return false }
            return stream.isMuxed || format == .hls || format == .dash
        }

        let audioStreams = streams.filter { $0.isAudioOnly }

        // Get the maximum resolution based on user's quality preference
        let maxResolution = preferredQuality.maxResolution

        // For live streams, always prefer HLS/DASH (designed for live streaming)
        let isLiveStream = streams.contains(where: { $0.isLive })
        if isLiveStream {
            if let hlsStream = muxedStreams.first(where: { StreamFormat.detect(from: $0) == .hls }) {
                return (hlsStream, nil)
            }
            if let dashStream = muxedStreams.first(where: { StreamFormat.detect(from: $0) == .dash }) {
                return (dashStream, nil)
            }
        }

        // Note: For non-live videos, we prefer progressive formats (MP4/WebM) over HLS/DASH
        // because they typically offer better quality. HLS/DASH are only used as last resort.

        // Try to find the best video-only stream + audio
        if !videoOnlyStreams.isEmpty && !audioStreams.isEmpty {
            let filteredVideoStreams: [Stream]
            if let maxRes = maxResolution {
                filteredVideoStreams = videoOnlyStreams.filter { stream in
                    guard let resolution = stream.resolution else { return true }
                    return resolution <= maxRes
                }
            } else {
                filteredVideoStreams = videoOnlyStreams
            }

            // Sort by resolution first, then by codec quality (AV1 > VP9 > H.264)
            let sortedVideo = filteredVideoStreams.sorted { s1, s2 in
                let res1 = s1.resolution ?? .p360
                let res2 = s2.resolution ?? .p360
                if res1 != res2 {
                    return res1 > res2
                }
                // Same resolution - prefer better codec
                return videoCodecPriority(s1.videoCodec) > videoCodecPriority(s2.videoCodec)
            }

            if let bestVideo = sortedVideo.first {
                // Select best audio stream based on preferred language, codec, and bitrate
                let preferredAudioLanguage = settingsManager?.preferredAudioLanguage
                let bestAudio = audioStreams
                    .sorted { stream1, stream2 in
                        // First priority: preferred language or original audio
                        if let preferred = preferredAudioLanguage {
                            // User selected a specific language
                            let lang1 = stream1.audioLanguage ?? ""
                            let lang2 = stream2.audioLanguage ?? ""
                            let matches1 = lang1.hasPrefix(preferred)
                            let matches2 = lang2.hasPrefix(preferred)
                            if matches1 != matches2 { return matches1 }
                        } else {
                            // No preference set - prefer original audio track
                            if stream1.isOriginalAudio != stream2.isOriginalAudio {
                                return stream1.isOriginalAudio
                            }
                        }

                        // Second priority: prefer Opus > AAC for MPV (better quality/compression)
                        let codecPriority1 = audioCodecPriority(stream1.audioCodec)
                        let codecPriority2 = audioCodecPriority(stream2.audioCodec)
                        if codecPriority1 != codecPriority2 {
                            return codecPriority1 > codecPriority2
                        }

                        // Third priority: higher bitrate
                        return (stream1.bitrate ?? 0) > (stream2.bitrate ?? 0)
                    }
                    .first

                if let audio = bestAudio {
                    return (bestVideo, audio)
                }
            }
        }

        // Fallback to muxed streams - prefer progressive formats over HLS/DASH for non-live content
        let filteredMuxed: [Stream]
        if let maxRes = maxResolution {
            filteredMuxed = muxedStreams.filter { stream in
                guard let resolution = stream.resolution else { return true }
                return resolution <= maxRes
            }
        } else {
            filteredMuxed = muxedStreams
        }

        // Sort: prefer progressive formats, then HLS, and only fall back to DASH as a last
        // resort (DASH should never be chosen when any other format is available).
        let sortedMuxed = filteredMuxed.sorted { s1, s2 in
            let rank1 = muxedFormatRank(StreamFormat.detect(from: s1))
            let rank2 = muxedFormatRank(StreamFormat.detect(from: s2))
            if rank1 != rank2 {
                return rank1 < rank2
            }
            return (s1.resolution ?? .p360) > (s2.resolution ?? .p360)
        }

        if let bestMuxed = sortedMuxed.first {
            return (bestMuxed, nil)
        }

        // Last resort: any muxed stream (HLS/DASH will be selected here if nothing else available)
        if let anyMuxed = muxedStreams.sorted(by: { ($0.resolution ?? .p360) > ($1.resolution ?? .p360) }).first {
            return (anyMuxed, nil)
        }

        return nil
    }

    /// Ranks muxed formats for fallback selection (lower = preferred).
    /// Progressive formats win, then HLS; DASH is the last resort and is only
    /// selected when it is the only format available.
    private func muxedFormatRank(_ format: StreamFormat) -> Int {
        switch format {
        case .dash: return 2
        case .hls: return 1
        default: return 0 // progressive
        }
    }

    /// Returns codec priority for video streams (higher = better for MPV).
    /// AV1 > VP9 > H.264/AVC
    private func videoCodecPriority(_ codec: String?) -> Int {
        guard let codec = codec?.lowercased() else { return 0 }
        if codec.contains("av1") || codec.contains("av01") {
            return 3 // Best compression, modern codec
        } else if codec.contains("vp9") || codec.contains("vp09") {
            return 2 // Good compression, widely supported
        } else if codec.contains("avc") || codec.contains("h264") || codec.contains("h.264") {
            return 1 // Most compatible, less efficient
        }
        return 0
    }

    /// Returns codec priority for audio streams (higher = better for MPV).
    /// Opus > AAC
    private func audioCodecPriority(_ codec: String?) -> Int {
        guard let codec = codec?.lowercased() else { return 0 }
        if codec.contains("opus") {
            return 2 // Best quality/compression ratio
        } else if codec.contains("aac") || codec.contains("mp4a") {
            return 1 // Good compatibility
        }
        return 0
    }

    /// Perform visual transition between backends.
    private func performTransition(
        from source: any PlayerBackend,
        to target: any PlayerBackend,
        animation: BackendSwitchAnimation
    ) async {
        guard animation != .instant else { return }

        #if canImport(UIKit)
        guard let sourceView = source.playerView,
              let targetView = target.playerView,
              let containerView = sourceView.superview else {
            return
        }

        // Add target view behind source
        targetView.frame = containerView.bounds
        targetView.alpha = 0
        containerView.insertSubview(targetView, belowSubview: sourceView)

        // Animate transition
        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
            UIView.animate(withDuration: animation.duration, animations: {
                switch animation {
                case .crossfade:
                    sourceView.alpha = 0
                    targetView.alpha = 1

                case .slide:
                    sourceView.transform = CGAffineTransform(translationX: -sourceView.bounds.width, y: 0)
                    targetView.alpha = 1

                case .instant:
                    break
                }
            }, completion: { _ in
                sourceView.removeFromSuperview()
                continuation.resume()
            })
        }

        #elseif canImport(AppKit)
        guard let sourceView = source.playerView,
              let targetView = target.playerView,
              let containerView = sourceView.superview else {
            return
        }

        // Add target view
        targetView.frame = containerView.bounds
        targetView.alphaValue = 0
        containerView.addSubview(targetView, positioned: .below, relativeTo: sourceView)

        // Animate transition
        await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
            NSAnimationContext.runAnimationGroup({ context in
                context.duration = animation.duration
                sourceView.animator().alphaValue = 0
                targetView.animator().alphaValue = 1
            }, completionHandler: {
                sourceView.removeFromSuperview()
                continuation.resume()
            })
        }
        #endif
    }
}

// MARK: - Backend Factory

/// Factory for creating player backend instances with pre-warming pool.
@MainActor
final class BackendFactory {
    /// Pool of pre-warmed backends ready for instant playback (1 per type)
    private var backendPool: [PlayerBackendType: any PlayerBackend] = [:]
    
    /// Statistics for monitoring pool efficiency
    private var poolHits = 0
    private var poolMisses = 0
    
    /// Create or retrieve a pre-warmed backend.
    func createBackend(type: PlayerBackendType) throws -> any PlayerBackend {
        // Try to get from pool first
        if let backend = backendPool[type] {
            backendPool[type] = nil  // Remove from pool
            poolHits += 1
            
            LoggingService.shared.debug("BackendFactory: pool hit for \(type.displayName) (hits=\(poolHits), misses=\(poolMisses))", category: .mpv)
            
            // Immediately start warming a replacement in background
            Task {
                await prewarmBackend(type: type)
            }
            
            return backend
        }
        
        poolMisses += 1
        LoggingService.shared.debug("BackendFactory: pool miss for \(type.displayName) (hits=\(poolHits), misses=\(poolMisses))", category: .mpv)
        
        // Create new backend and begin setup
        let backend = createBackendInstance(type: type)
        if let mpvBackend = backend as? MPVBackend {
            mpvBackend.beginSetup()
        }
        return backend
    }
    
    /// Create a backend instance (without pool).
    private func createBackendInstance(type: PlayerBackendType) -> any PlayerBackend {
        switch type {
        case .mpv:
            return MPVBackend()
        }
    }
    
    /// Pre-warm a backend and add to pool.
    func prewarmBackend(type: PlayerBackendType) async {
        let startTime = Date()
        LoggingService.shared.debug("BackendFactory: pre-warming \(type.displayName)", category: .mpv)
        
        let backend = await MainActor.run {
            createBackendInstance(type: type)
        }
        
        // Begin async setup
        if let mpvBackend = backend as? MPVBackend {
            await MainActor.run {
                mpvBackend.beginSetup()
            }
            // Wait for setup to complete
            do {
                try await mpvBackend.waitForSetup()
            } catch {
                LoggingService.shared.debug("BackendFactory: pre-warm failed for \(type.displayName): \(error)", category: .mpv)
                return
            }
        }
        
        let duration = Date().timeIntervalSince(startTime)
        LoggingService.shared.debug("BackendFactory: \(type.displayName) pre-warmed in \(String(format: "%.3f", duration))s", category: .mpv)
        
        // Add to pool (only if slot is empty - don't accumulate)
        await MainActor.run {
            if backendPool[type] == nil {
                backendPool[type] = backend
                LoggingService.shared.debug("BackendFactory: \(type.displayName) added to pool", category: .mpv)
            } else {
                LoggingService.shared.debug("BackendFactory: \(type.displayName) pool already full, discarding", category: .mpv)
            }
        }
    }
    
    /// Pre-warm all available backends in parallel.
    func prewarmAllBackends() async {
        let startTime = Date()
        LoggingService.shared.debug("BackendFactory: pre-warming all backends", category: .mpv)
        
        // Pre-warm in parallel
        await withTaskGroup(of: Void.self) { group in
            for type in availableBackends {
                group.addTask {
                    await self.prewarmBackend(type: type)
                }
            }
        }
        
        let duration = Date().timeIntervalSince(startTime)
        LoggingService.shared.debug("BackendFactory: all backends pre-warmed in \(String(format: "%.3f", duration))s", category: .mpv)
    }
    
    /// Drain the pool (called on memory warning).
    func drainPool() {
        let count = backendPool.count
        backendPool.removeAll()
        LoggingService.shared.debug("BackendFactory: pool drained (\(count) backends released)", category: .mpv)
    }

    /// Check if a backend type is available on this platform.
    func isAvailable(_ type: PlayerBackendType) -> Bool {
        switch type {
        case .mpv:
            return true
        }
    }

    /// Get all available backend types.
    var availableBackends: [PlayerBackendType] {
        PlayerBackendType.allCases.filter { isAvailable($0) }
    }
}
