import AVFoundation
import AVKit
import NitroModules
import UIKit

/**
 * A UIView subclass that hosts an AVPlayerLayer for video rendering.
 *
 * When react-native-screens (or any navigation library) removes a screen
 * from the UIWindow hierarchy, AVPlayerLayer loses its internal connection
 * to the AVPlayer output. When the view is re-added to a window the layer
 * renders black even though the engine is still playing.
 *
 * Overriding `didMoveToWindow` and re-assigning `playerLayer.player` forces
 * AVKit to reconnect the layer to the decoder output, restoring the video
 * frame. This matches the fix used by react-native-video.
 */
private class VideoPlayerView: UIView {
    override class var layerClass: AnyClass {
        AVPlayerLayer.self
    }

    var playerLayer: AVPlayerLayer {
        layer as! AVPlayerLayer
    }

    override func didMoveToWindow() {
        super.didMoveToWindow()
        guard window != nil else { return }
        // Re-assign the player to force AVPlayerLayer to reconnect
        // its output pipeline after the layer was removed from the hierarchy.
        let current = playerLayer.player
        playerLayer.player = nil
        playerLayer.player = current
    }
}

/**
 * HybridVideoView — A Nitro HybridView that renders video from an AVPlayer.
 *
 * This view purely provides a rendering surface (AVPlayerLayer). It does NOT
 * own or control playback — the Player handles all that. Unmounting VideoView
 * does NOT stop playback; audio continues.
 *
 * Fullscreen uses window reparenting — the containerView is moved into a
 * fullscreen UIWindow, keeping the same AVPlayerLayer attached. No player
 * transfer, no pause.
 *
 * Threading contract:
 * All UIKit / AVPlayerLayer / AVPictureInPictureController mutations happen
 * on the main thread. The Nitro bridge calls methods from the JS thread;
 * methods that touch UIKit dispatch to main. Property getters return cached
 * values from backing fields (safe to read from any thread).
 */
class HybridVideoView: HybridVideoViewSpec, RecyclableView {
    // MARK: - HybridView

    let view: UIView
    private let containerView: UIView
    private let videoView: VideoPlayerView

    // MARK: - PiP

    private var pipController: AVPictureInPictureController?
    private let pipDelegate = PipDelegate()
    private let nativeFullscreenDelegate = NativeFullscreenDelegate()

    // MARK: - Fullscreen

    private var _isFullscreen: Bool = false
    private var _isNativeFullscreen: Bool = false
    /// The host view we reparented `containerView` into (the keyWindow's root
    /// view controller's view). Held weak — the host owns it.
    private weak var fullscreenHostView: UIView?
    private var inlineSuperview: UIView?
    private var inlineConstraints: [NSLayoutConstraint] = []
    private var fullscreenConstraints: [NSLayoutConstraint] = []
    private var fullscreenTransitionGeneration = 0
    private var isFullscreenTransitioning = false
    private weak var nativeFullscreenVC: AVPlayerViewController?
    /// True if `enterFullscreen` issued a `requestGeometryUpdate` to force
    /// landscape, so `exitFullscreen` knows to restore.
    private var _didForceRotation: Bool = false

    // MARK: - Observation for lazy PiP and video track (KVO)

    private var currentItemObservation: NSKeyValueObservation?
    private var itemStatusObservation: NSKeyValueObservation?
    private var trackCurrentItemObservation: NSKeyValueObservation?
    private var trackItemStatusObservation: NSKeyValueObservation?

    // MARK: - Props

    private var _resizeMode: ResizeMode = .contain
    var resizeMode: ResizeMode {
        get { _resizeMode }
        set {
            _resizeMode = newValue
            DispatchQueue.main.async { [weak self] in
                self?.applyResizeMode()
            }
        }
    }

    private var _pipEnabled: Bool = false
    var pipEnabled: Bool {
        get { _pipEnabled }
        set {
            _pipEnabled = newValue
            DispatchQueue.main.async { [weak self] in
                self?.updatePipController()
            }
        }
    }

    private var _playInBackground: Bool = false
    var playInBackground: Bool {
        get { _playInBackground }
        set { _playInBackground = newValue }
    }

    /// Player temporarily detached from the layer while backgrounded so its
    /// audio keeps playing. iOS pauses a video whose AVPlayerLayer is attached
    /// while offscreen; detaching makes it audio-only. Restored on foreground.
    private var backgroundedPlayer: AVPlayer?
    private var didEnterBackgroundObserver: NSObjectProtocol?
    private var willEnterForegroundObserver: NSObjectProtocol?

    var onPipStart: () -> Void = {}
    var onPipStop: () -> Void = {}

    var onFullscreenEnter: () -> Void = {}
    var onFullscreenExit: () -> Void = {}

    // surfaceType is Android-only (SurfaceView vs TextureView).
    // On iOS, AVPlayerLayer always uses GPU composition — no equivalent distinction.
    var surfaceType: SurfaceType = .surfaceview

    private var _selectedVideoTrack = VideoTrackSelection(type: .auto, value: nil)
    var selectedVideoTrack: VideoTrackSelection {
        get { _selectedVideoTrack }
        set {
            _selectedVideoTrack = newValue
            DispatchQueue.main.async { [weak self] in
                self?.applyVideoTrackSelection()
            }
        }
    }

    private var _selectedSubtitleTrack = SubtitleTrackSelection(type: .auto, value: nil)
    var selectedSubtitleTrack: SubtitleTrackSelection {
        get { _selectedSubtitleTrack }
        set {
            _selectedSubtitleTrack = newValue
            DispatchQueue.main.async { [weak self] in
                self?.applySubtitleTrackSelection()
            }
        }
    }

    private var _selectedAudioTrack = AudioTrackSelection(type: .auto, value: nil)
    var selectedAudioTrack: AudioTrackSelection {
        get { _selectedAudioTrack }
        set {
            _selectedAudioTrack = newValue
            DispatchQueue.main.async { [weak self] in
                self?.applyAudioTrackSelection()
            }
        }
    }

    private var _adTagUrl: String = ""
    var adTagUrl: String {
        get { _adTagUrl }
        set {
            _adTagUrl = newValue
            DispatchQueue.main.async { [weak self] in
                self?.currentVideoViewAttachment?.adTagUrl = newValue
            }
        }
    }

    // MARK: - Source & AutoPlay

    private var _autoPlay: Bool = true
    var autoPlay: Bool {
        get { _autoPlay }
        set { _autoPlay = newValue }
    }

    private var _source: VideoSource?
    private weak var _attachedEngineForSource: HybridPlaybackEngine?
    private var currentVideoViewAttachment: AviationVideoViewAttachment?
    private var attachmentGeneration = 0
    var source: VideoSource {
        get { _source ?? VideoSource(uri: "", mediaType: .videoondemand, headers: nil, drmType: nil, drmLicenseUri: nil, drmHeaders: nil, startPositionMs: nil, title: nil, artist: nil, artworkUri: nil) }
        set {
            guard !newValue.uri.isEmpty else {
                _source = nil
                return
            }
            // Dedupe by URI: consumers passing a fresh VideoSource object
            // every render must not retrigger a load. If the URI is
            // unchanged, swap in the new struct but skip loadSourceIfReady().
            let sameUri = _source?.uri == newValue.uri
            _source = newValue
            if sameUri { return }
            loadSourceIfReady()
        }
    }

    /// Called when either source or engine becomes available.
    /// Creates a MediaItem from the VideoSource and calls load or loadAndPlay.
    private func loadSourceIfReady() {
        guard let src = _source, !src.uri.isEmpty,
              let engine = _attachedEngineForSource else { return }

        let config = MediaItemConfig(
            uri: src.uri,
            mediaType: src.mediaType == .videolive ? .videolive : .videoondemand,
            mimeType: nil,
            title: src.title,
            artist: src.artist,
            album: nil,
            artworkUri: src.artworkUri,
            duration: nil,
            headers: src.headers,
            drmType: src.drmType,
            drmLicenseUri: src.drmLicenseUri,
            drmHeaders: src.drmHeaders,
            startPositionMs: src.startPositionMs,
            endPositionMs: nil
        )
        let item = HybridMediaItem(config: config)

        if _autoPlay {
            try? engine.loadAndPlay(item: item)
        } else {
            try? engine.load(item: item)
        }
    }

    // MARK: - Init

    override init() {
        let root = UIView()
        root.backgroundColor = .black
        root.clipsToBounds = true

        let container = UIView()
        container.backgroundColor = .black
        container.clipsToBounds = true
        container.translatesAutoresizingMaskIntoConstraints = false

        let v = VideoPlayerView()
        v.playerLayer.videoGravity = .resizeAspect
        v.translatesAutoresizingMaskIntoConstraints = false
        container.addSubview(v)

        NSLayoutConstraint.activate([
            v.topAnchor.constraint(equalTo: container.topAnchor),
            v.bottomAnchor.constraint(equalTo: container.bottomAnchor),
            v.leadingAnchor.constraint(equalTo: container.leadingAnchor),
            v.trailingAnchor.constraint(equalTo: container.trailingAnchor),
        ])

        containerView = container
        videoView = v
        view = root
        super.init()

        root.addSubview(container)
        inlineConstraints = [
            container.topAnchor.constraint(equalTo: root.topAnchor),
            container.bottomAnchor.constraint(equalTo: root.bottomAnchor),
            container.leadingAnchor.constraint(equalTo: root.leadingAnchor),
            container.trailingAnchor.constraint(equalTo: root.trailingAnchor),
        ]
        NSLayoutConstraint.activate(inlineConstraints)

        pipDelegate.sourceView = container
        pipDelegate.onStart = { [weak self] in self?.onPipStart() }
        pipDelegate.onStop = { [weak self] in self?.onPipStop() }

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

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

    /// When `playInBackground` is enabled, detach the player from the layer on
    /// background so iOS keeps playing it as audio-only. Skipped while PiP is
    /// active (the system keeps the video attached for PiP).
    private func handleDidEnterBackground() {
        guard _playInBackground else { return }
        if let pip = pipController, pip.isPictureInPictureActive { return }
        guard let player = videoView.playerLayer.player else { return }
        backgroundedPlayer = player
        videoView.playerLayer.player = nil
    }

    /// Reattach the player detached in `handleDidEnterBackground` so the video
    /// resumes rendering when the app returns to the foreground.
    private func handleWillEnterForeground() {
        guard let player = backgroundedPlayer else { return }
        videoView.playerLayer.player = player
        backgroundedPlayer = nil
    }

    // MARK: - Methods

    private func makeVideoViewAttachment(
        engine: HybridPlaybackEngine,
        player: AVPlayer
    ) -> AviationVideoViewAttachment {
        let attachment = AviationVideoViewAttachment(
            player: player,
            engineId: engine.engineId,
            adTagUrl: _adTagUrl
        )

        attachment.contentPauseBlock = { [weak engine] in
            engine?.contentPauseRequested()
        }
        attachment.contentResumeBlock = { [weak engine] in
            engine?.contentResumeRequested()
        }
        attachment.setAdModeBlock = { [weak engine] title, durationSeconds in
            engine?.setNowPlayingAdMode(
                title: title,
                durationSeconds: durationSeconds
            )
        }
        attachment.clearAdModeBlock = { [weak engine] in
            engine?.clearNowPlayingAdMode()
        }
        engine.configureEngineHooks(on: attachment)
        attachment.emitAdDomainEventBlock = { [weak engine] type, breakIndex, breakTimeMs, totalAdsInBreak, adId, title, durationMs, currentMs, adIndexInBreak, adTotalAdsInBreak, isSkippable, skipOffsetMs, errorMessage in
            engine?.emitAdDomainEvent(
                code: type,
                breakIndex: breakIndex,
                breakTimeMs: breakTimeMs,
                totalAdsInBreak: totalAdsInBreak,
                adId: adId,
                title: title,
                durationMs: durationMs,
                currentMs: currentMs,
                adIndexInBreak: adIndexInBreak,
                adTotalAdsInBreak: adTotalAdsInBreak,
                isSkippable: isSkippable,
                skipOffsetMs: skipOffsetMs,
                errorMessage: errorMessage
            )
        }
        return attachment
    }

    func attachEngine(engine: any HybridPlaybackEngineSpec) throws {
        guard let hybridEngine = engine as? HybridPlaybackEngine else {
            NSLog("[Aviation] VideoView.attachEngine — not a HybridPlaybackEngine")
            return
        }

        let avPlayer = hybridEngine.underlyingPlayer
        _attachedEngineForSource = hybridEngine
        attachmentGeneration += 1
        let generation = attachmentGeneration

        DispatchQueue.main.async { [weak self] in
            guard let self else { return }
            guard attachmentGeneration == generation,
                  _attachedEngineForSource === hybridEngine
            else { return }

            guard let avPlayer else {
                NSLog("[Aviation] VideoView.attachEngine — no underlying AVPlayer")
                videoView.playerLayer.player = nil
                currentVideoViewAttachment = nil
                teardownPipObservations()
                teardownPipController()
                return
            }

            #if DEBUG
                NSLog("[Aviation] VideoView.attachEngine — attaching AVPlayer")
            #endif
            videoView.playerLayer.player = avPlayer

            // Notify plugins (ads, analytics, etc.)
            var viewController: UIViewController? = nil
            var responder: UIResponder? = containerView
            while let r = responder {
                if let vc = r as? UIViewController { viewController = vc; break }
                responder = r.next
            }
            let attachment = makeVideoViewAttachment(
                engine: hybridEngine,
                player: avPlayer
            )
            currentVideoViewAttachment = attachment
            AviationEngineRegistry.notifyVideoViewAttached(
                with: attachment,
                container: containerView,
                viewController: viewController
            )

            if _pipEnabled {
                observePlayerForPip(avPlayer)
            }

            observePlayerForTrackSelection(avPlayer)

            // If source was set before engine was attached, load it now.
            loadSourceIfReady()
        }
    }

    func detachEngine() throws {
        _attachedEngineForSource = nil
        attachmentGeneration += 1
        DispatchQueue.main.async { [weak self] in
            guard let self else { return }
            #if DEBUG
                NSLog("[Aviation] VideoView.detachEngine")
            #endif
            teardownPipObservations()
            trackCurrentItemObservation?.invalidate()
            trackCurrentItemObservation = nil
            trackItemStatusObservation?.invalidate()
            trackItemStatusObservation = nil
            if let pip = pipController {
                pip.delegate = nil
                pipController = nil
            }
            videoView.playerLayer.player = nil
            currentVideoViewAttachment = nil

            // Notify plugins
            AviationEngineRegistry.notifyVideoViewDetached()
        }
    }

    // MARK: - RecyclableView

    /// Called by Fabric before this view is handed to a different logical
    /// instance. Restore the view to a pristine state: no player, no
    /// observers, no PiP controller, no fullscreen, default prop values.
    ///
    /// Safety model: we always do the full reset regardless of any
    /// JS-level `recycle` opt-in — Nitro's `RecyclableView` is a
    /// class-level capability, not a per-instance toggle, so Fabric can
    /// recycle *any* instance. Leaving stale state around would leak the
    /// previous engine/item into the next cell.
    func prepareForRecycle() {
        // Detach engine: invalidates KVO observations, tears down PiP,
        // clears the player on the layer, notifies the engine registry.
        try? detachEngine()

        // Reset all public props to their declared defaults so Fabric
        // cannot observe stale values on the recycled instance.
        _resizeMode = .contain
        _pipEnabled = false
        surfaceType = .surfaceview
        _selectedVideoTrack = VideoTrackSelection(type: .auto, value: nil)
        _selectedSubtitleTrack = SubtitleTrackSelection(type: .auto, value: nil)
        _selectedAudioTrack = AudioTrackSelection(type: .auto, value: nil)
        _adTagUrl = ""
        _autoPlay = true
        _source = nil

        // Reset JS callbacks so a previous cell's handlers don't fire on
        // the next logical instance before the new props are installed.
        onPipStart = {}
        onPipStop = {}
        onFullscreenEnter = {}
        onFullscreenExit = {}

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

            // If we ended up recycled while fullscreen, dismiss cleanly
            // before the view is reused.
            if _isNativeFullscreen {
                fullscreenTransitionGeneration += 1
                nativeFullscreenVC?.delegate = nil
                nativeFullscreenVC?.presentationController?.delegate = nil
                nativeFullscreenVC?.dismiss(animated: false, completion: nil)
                nativeFullscreenVC = nil
                _isNativeFullscreen = false
                isFullscreenTransitioning = false
            }
            if _isFullscreen {
                try? exitFullscreen()
            }

            // Reset the layer's visual config to defaults.
            videoView.playerLayer.videoGravity = .resizeAspect
            containerView.backgroundColor = .black
        }
    }

    func startPictureInPicture() throws {
        DispatchQueue.main.async { [weak self] in
            guard let self else { return }

            if pipController == nil {
                tryCreatePipController()
            }

            guard let pip = pipController else {
                NSLog("[Aviation] VideoView.startPictureInPicture — no controller")
                return
            }
            guard pip.isPictureInPicturePossible else {
                NSLog("[Aviation] VideoView.startPictureInPicture — not possible")
                return
            }
            pip.startPictureInPicture()
        }
    }

    func stopPictureInPicture() throws {
        DispatchQueue.main.async { [weak self] in
            self?.pipController?.stopPictureInPicture()
        }
    }

    // MARK: - Fullscreen Methods

    func isFullscreen() throws -> Bool {
        _isFullscreen
    }

    // MARK: - Imperative Track Selection (bypasses Fabric props)

    func applyVideoTrack(selection: VideoTrackSelection) throws {
        _selectedVideoTrack = selection
        DispatchQueue.main.async { [weak self] in
            self?.applyVideoTrackSelection()
        }
    }

    func applySubtitleTrack(selection: SubtitleTrackSelection) throws {
        _selectedSubtitleTrack = selection
        DispatchQueue.main.async { [weak self] in
            self?.applySubtitleTrackSelection()
        }
    }

    func applyAudioTrack(selection: AudioTrackSelection) throws {
        _selectedAudioTrack = selection
        DispatchQueue.main.async { [weak self] in
            self?.applyAudioTrackSelection()
        }
    }

    func enterFullscreen(config: FullscreenConfig?) throws {
        DispatchQueue.main.async { [weak self] in
            guard let self else { return }
            guard !_isFullscreen, !isFullscreenTransitioning else { return }
            fullscreenTransitionGeneration += 1
            let generation = fullscreenTransitionGeneration

            // Exit PiP if active (mutually exclusive)
            if let pip = pipController, pip.isPictureInPictureActive {
                pip.stopPictureInPicture()
            }

            // We reparent into the keyWindow's root view (rather than a
            // separate, higher-level UIWindow) so RN modals presented while
            // fullscreen is active — for example via VideoView's
            // `fullscreenControls` prop — render in the same window and stay
            // visible above the video. A separate UIWindow at a higher level
            // would trap RN modals behind it.
            guard let window = containerView.window,
                  let hostView = window.rootViewController?.view
            else {
                NSLog("[Aviation] VideoView.enterFullscreen — no host view")
                return
            }

            // Remember where we came from
            inlineSuperview = containerView.superview
            NSLayoutConstraint.deactivate(inlineConstraints)

            // Capture inline frame in the host view's coord space
            let inlineFrame = containerView.convert(containerView.bounds, to: hostView)

            // Detect landscape video for orientation forcing
            let autoRotate = config?.autoRotate ?? true
            var isLandscapeVideo = false
            if autoRotate, let player = videoView.playerLayer.player,
               let item = player.currentItem,
               let track = item.tracks.first(where: { $0.assetTrack?.mediaType == .video }),
               let naturalSize = track.assetTrack?.naturalSize,
               let transform = track.assetTrack?.preferredTransform
            {
                let size = naturalSize.applying(transform)
                isLandscapeVideo = abs(size.width) > abs(size.height)
            }

            // Reparent containerView into the host view at inline position
            containerView.translatesAutoresizingMaskIntoConstraints = true
            containerView.frame = inlineFrame
            containerView.backgroundColor = .black
            hostView.addSubview(containerView)

            fullscreenHostView = hostView
            _isFullscreen = true
            isFullscreenTransitioning = true

            // Force rotation on iOS 16+ via the scene geometry API.
            // Requires the app's rootVC to allow the requested orientation
            // (set in Info.plist `UISupportedInterfaceOrientations` and/or
            // `application:supportedInterfaceOrientationsFor:`).
            if autoRotate, isLandscapeVideo, #available(iOS 16.0, *),
               let scene = window.windowScene
            {
                let prefs = UIWindowScene.GeometryPreferences.iOS(
                    interfaceOrientations: .landscape
                )
                scene.requestGeometryUpdate(prefs) { error in
                    NSLog("[Aviation] requestGeometryUpdate(landscape) failed: %@",
                          error.localizedDescription)
                }
                _didForceRotation = true
            }

            UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) {
                self.containerView.frame = hostView.bounds
            } completion: { _ in
                guard self.fullscreenTransitionGeneration == generation,
                      self._isFullscreen,
                      self.fullscreenHostView === hostView,
                      self.containerView.superview === hostView
                else { return }
                self.containerView.translatesAutoresizingMaskIntoConstraints = false
                self.fullscreenConstraints = [
                    self.containerView.topAnchor.constraint(equalTo: hostView.topAnchor),
                    self.containerView.bottomAnchor.constraint(equalTo: hostView.bottomAnchor),
                    self.containerView.leadingAnchor.constraint(equalTo: hostView.leadingAnchor),
                    self.containerView.trailingAnchor.constraint(equalTo: hostView.trailingAnchor),
                ]
                NSLayoutConstraint.activate(self.fullscreenConstraints)
                self.isFullscreenTransitioning = false
                self.onFullscreenEnter()
            }
        }
    }

    func exitFullscreen() throws {
        DispatchQueue.main.async { [weak self] in
            guard let self else { return }
            guard _isFullscreen else { return }
            fullscreenTransitionGeneration += 1
            let generation = fullscreenTransitionGeneration
            isFullscreenTransitioning = true

            if _isNativeFullscreen {
                exitNativeFullscreen()
                return
            }

            _isFullscreen = false

            let hostView = fullscreenHostView
            let returnSuperview = inlineSuperview ?? view

            // Compute where the inline view currently is, in the host's coords
            let targetFrame: CGRect = if let hostView {
                returnSuperview.convert(returnSuperview.bounds, to: hostView)
            } else {
                returnSuperview.convert(returnSuperview.bounds, to: nil)
            }

            // Switch to manual frames for animation
            NSLayoutConstraint.deactivate(fullscreenConstraints)
            fullscreenConstraints.removeAll()
            containerView.translatesAutoresizingMaskIntoConstraints = true
            containerView.frame = hostView?.bounds ?? containerView.bounds

            // Restore unrestricted orientation if we forced one. The device
            // will rotate back to whatever the user's hardware orientation is.
            if _didForceRotation, #available(iOS 16.0, *),
               let scene = containerView.window?.windowScene
            {
                let prefs = UIWindowScene.GeometryPreferences.iOS(
                    interfaceOrientations: .all
                )
                scene.requestGeometryUpdate(prefs) { error in
                    NSLog("[Aviation] requestGeometryUpdate(restore) failed: %@",
                          error.localizedDescription)
                }
                _didForceRotation = false
            }

            UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) {
                self.containerView.frame = targetFrame
            } completion: { _ in
                guard self.fullscreenTransitionGeneration == generation,
                      !self._isFullscreen
                else { return }
                // Reparent back into the stable exported wrapper view.
                self.containerView.translatesAutoresizingMaskIntoConstraints = false
                returnSuperview.addSubview(self.containerView)
                NSLayoutConstraint.activate(self.inlineConstraints)

                self.fullscreenHostView = nil
                self.inlineSuperview = nil
                self.isFullscreenTransitioning = false
                self.onFullscreenExit()
            }
        }
    }

    // MARK: - Native Fullscreen (AVPlayerViewController)

    /// Presents a native AVPlayerViewController modally with full system controls.
    /// On dismiss, auto-resumes playback if it was playing (AVPlayerViewController
    /// pauses on dismiss — this is unavoidable but the resume is instant).
    func enterNativeFullscreen() {
        DispatchQueue.main.async { [weak self] in
            guard let self else { return }
            guard !_isFullscreen, !isFullscreenTransitioning else { return }
            fullscreenTransitionGeneration += 1
            let generation = fullscreenTransitionGeneration

            guard let player = videoView.playerLayer.player else {
                NSLog("[Aviation] VideoView.enterNativeFullscreen — no player attached")
                return
            }

            // Exit PiP if active
            if let pip = pipController, pip.isPictureInPictureActive {
                pip.stopPictureInPicture()
            }

            // Find presenting VC
            guard let presentingVC = findViewController(from: containerView) else {
                NSLog("[Aviation] VideoView.enterNativeFullscreen — no UIViewController")
                return
            }

            let playerVC = AVPlayerViewController()
            playerVC.player = player
            playerVC.delegate = nativeFullscreenDelegate
            playerVC.modalPresentationStyle = .fullScreen
            playerVC.updatesNowPlayingInfoCenter = false
            nativeFullscreenDelegate.onDismiss = { [weak self, weak playerVC] dismissedPlayerVC in
                DispatchQueue.main.async { [weak self, weak playerVC] in
                    guard let self,
                          let playerVC,
                          playerVC === dismissedPlayerVC
                    else { return }
                    handleNativeFullscreenDismissed(playerVC)
                }
            }

            if #available(iOS 16.0, *) {
                playerVC.allowsVideoFrameAnalysis = false
            }

            nativeFullscreenVC = playerVC
            _isFullscreen = true
            _isNativeFullscreen = true
            isFullscreenTransitioning = true

            playerVC.presentationController?.delegate = nativeFullscreenDelegate
            presentingVC.present(playerVC, animated: true) { [weak self] in
                guard let self,
                      fullscreenTransitionGeneration == generation,
                      nativeFullscreenVC === playerVC,
                      _isNativeFullscreen
                else { return }
                isFullscreenTransitioning = false
                onFullscreenEnter()
            }
        }
    }

    private func exitNativeFullscreen() {
        let generation = fullscreenTransitionGeneration
        guard let playerVC = nativeFullscreenVC else {
            _isFullscreen = false
            _isNativeFullscreen = false
            isFullscreenTransitioning = false
            return
        }

        let player = playerVC.player
        let wasPlaying = player?.rate != 0

        // Detach player before dismiss to minimize the pause window
        playerVC.player = nil
        playerVC.delegate = nil
        playerVC.presentationController?.delegate = nil

        playerVC.dismiss(animated: true) { [weak self] in
            guard let self else { return }
            guard fullscreenTransitionGeneration == generation else { return }
            completeNativeFullscreenExit(playerVC, player: player, wasPlaying: wasPlaying)
        }
    }

    private func handleNativeFullscreenDismissed(_ playerVC: AVPlayerViewController) {
        guard nativeFullscreenVC === playerVC,
              _isNativeFullscreen else { return }
        fullscreenTransitionGeneration += 1
        let player = playerVC.player
        let wasPlaying = player?.rate != 0
        playerVC.player = nil
        completeNativeFullscreenExit(playerVC, player: player, wasPlaying: wasPlaying)
    }

    private func completeNativeFullscreenExit(
        _ playerVC: AVPlayerViewController,
        player: AVPlayer?,
        wasPlaying: Bool
    ) {
        guard nativeFullscreenVC === playerVC else { return }
        playerVC.delegate = nil
        playerVC.presentationController?.delegate = nil
        nativeFullscreenDelegate.onDismiss = nil

        _isFullscreen = false
        _isNativeFullscreen = false
        isFullscreenTransitioning = false

        // Re-attach to our layer and auto-resume
        if let player {
            videoView.playerLayer.player = player
            if wasPlaying {
                player.play()
            }
        }

        nativeFullscreenVC = nil
        onFullscreenExit()
    }

    private func findViewController(from view: UIView) -> UIViewController? {
        var responder: UIResponder? = view
        while let r = responder {
            if let vc = r as? UIViewController { return vc }
            responder = r.next
        }
        return nil
    }

    // MARK: - Private

    private func observePlayerForTrackSelection(_ player: AVPlayer) {
        trackCurrentItemObservation?.invalidate()
        trackItemStatusObservation?.invalidate()
        trackCurrentItemObservation = player.observe(
            \.currentItem, options: [.new, .initial]
        ) { [weak self] player, _ in
            DispatchQueue.main.async { [weak self] in
                guard let self else { return }
                applyVideoTrackSelection()
                applySubtitleTrackSelection()
                applyAudioTrackSelection()

                // Observe the new item's status so subtitle/audio selection
                // is retried when the item transitions to readyToPlay.
                // Those methods guard on readyToPlay because AVMediaSelectionGroup
                // is only available after the asset is loaded.
                trackItemStatusObservation?.invalidate()
                trackItemStatusObservation = nil
                guard let item = player.currentItem,
                      item.status != .readyToPlay else { return }
                trackItemStatusObservation = item.observe(
                    \.status, options: [.new]
                ) { [weak self] item, _ in
                    guard item.status == .readyToPlay else { return }
                    DispatchQueue.main.async { [weak self] in
                        self?.trackItemStatusObservation?.invalidate()
                        self?.trackItemStatusObservation = nil
                        self?.applySubtitleTrackSelection()
                        self?.applyAudioTrackSelection()
                    }
                }
            }
        }
    }

    private func applyVideoTrackSelection() {
        guard let item = videoView.playerLayer.player?.currentItem else { return }

        switch _selectedVideoTrack.type {
        case .auto:
            item.preferredMaximumResolution = .zero
        case .disabled:
            item.preferredMaximumResolution = CGSize(width: 1, height: 1)
        case .resolution:
            let height = _selectedVideoTrack.value.flatMap { Int($0) } ?? 0
            item.preferredMaximumResolution = CGSize(width: Int.max, height: height)
        case .index:
            NSLog("[Aviation] VideoView — selectedVideoTrack 'index' is not supported on iOS (HLS). Use 'resolution' instead.")
        }
    }

    private func applySubtitleTrackSelection() {
        guard let item = videoView.playerLayer.player?.currentItem else { return }
        guard item.status == .readyToPlay else {
            // Item not ready yet — the item status KVO in
            // observePlayerForTrackSelection will retry once readyToPlay.
            return
        }

        guard let group = item.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else {
            #if DEBUG
                NSLog("[Aviation] VideoView — no legible media selection group (no subtitles available)")
            #endif
            return
        }

        switch _selectedSubtitleTrack.type {
        case .auto:
            item.selectMediaOptionAutomatically(in: group)
        case .disabled:
            // Deselect all subtitle options — hides subtitles
            item.select(nil, in: group)
        case .language:
            guard let lang = _selectedSubtitleTrack.value else {
                NSLog("[Aviation] VideoView — selectedSubtitleTrack 'language' requires a value (BCP-47 code)")
                return
            }
            let options = AVMediaSelectionGroup.mediaSelectionOptions(
                from: group.options,
                with: Locale(identifier: lang)
            )
            if let match = options.first {
                item.select(match, in: group)
            } else {
                NSLog("[Aviation] VideoView — no subtitle track found for language '\(lang)'")
            }
        case .index:
            guard let indexStr = _selectedSubtitleTrack.value,
                  let index = Int(indexStr)
            else {
                NSLog("[Aviation] VideoView — selectedSubtitleTrack 'index' requires a numeric value")
                return
            }
            if index >= 0, index < group.options.count {
                item.select(group.options[index], in: group)
            } else {
                NSLog("[Aviation] VideoView — subtitle track index \(index) out of range (0..\(group.options.count - 1))")
            }
        }
    }

    private func applyAudioTrackSelection() {
        guard let item = videoView.playerLayer.player?.currentItem else { return }
        guard item.status == .readyToPlay else {
            return
        }

        guard let group = item.asset.mediaSelectionGroup(forMediaCharacteristic: .audible) else {
            #if DEBUG
                NSLog("[Aviation] VideoView — no audible media selection group (no audio tracks available)")
            #endif
            return
        }

        switch _selectedAudioTrack.type {
        case .auto:
            item.selectMediaOptionAutomatically(in: group)
        case .language:
            guard let lang = _selectedAudioTrack.value else {
                NSLog("[Aviation] VideoView — selectedAudioTrack 'language' requires a value (BCP-47 code)")
                return
            }
            let options = AVMediaSelectionGroup.mediaSelectionOptions(
                from: group.options,
                with: Locale(identifier: lang)
            )
            if let match = options.first {
                item.select(match, in: group)
            } else {
                NSLog("[Aviation] VideoView — no audio track found for language '\(lang)'")
            }
        case .index:
            guard let indexStr = _selectedAudioTrack.value,
                  let index = Int(indexStr)
            else {
                NSLog("[Aviation] VideoView — selectedAudioTrack 'index' requires a numeric value")
                return
            }
            if index >= 0, index < group.options.count {
                item.select(group.options[index], in: group)
            } else {
                NSLog("[Aviation] VideoView — audio track index \(index) out of range (0..\(group.options.count - 1))")
            }
        }
    }

    private func applyResizeMode() {
        switch _resizeMode {
        case .contain:
            videoView.playerLayer.videoGravity = .resizeAspect
        case .cover:
            videoView.playerLayer.videoGravity = .resizeAspectFill
        case .stretch:
            videoView.playerLayer.videoGravity = .resize
        }
    }

    private func updatePipController() {
        if _pipEnabled, let player = videoView.playerLayer.player {
            observePlayerForPip(player)
        } else {
            teardownPipObservations()
            teardownPipController()
        }
    }

    // MARK: - Lazy PiP observation

    private func observePlayerForPip(_ player: AVPlayer) {
        teardownPipObservations()

        currentItemObservation = player.observe(
            \.currentItem, options: [.new, .initial]
        ) { [weak self] player, _ in
            DispatchQueue.main.async { [weak self] in
                self?.onCurrentItemChanged(player: player)
            }
        }
    }

    private func onCurrentItemChanged(player: AVPlayer) {
        itemStatusObservation?.invalidate()
        itemStatusObservation = nil

        guard let item = player.currentItem else {
            teardownPipController()
            return
        }

        if item.status == .readyToPlay {
            tryCreatePipController()
            return
        }

        itemStatusObservation = item.observe(
            \.status, options: [.new]
        ) { [weak self] item, _ in
            let status = item.status
            DispatchQueue.main.async { [weak self] in
                if status == .readyToPlay {
                    self?.itemStatusObservation?.invalidate()
                    self?.itemStatusObservation = nil
                    self?.tryCreatePipController()
                }
            }
        }
    }

    private func tryCreatePipController() {
        guard _pipEnabled else { return }
        guard pipController == nil else { return }

        guard AVPictureInPictureController.isPictureInPictureSupported() else {
            NSLog("[Aviation] VideoView — PiP not supported")
            return
        }

        guard let player = videoView.playerLayer.player,
              let item = player.currentItem,
              item.status == .readyToPlay
        else {
            NSLog("[Aviation] VideoView — player/item not ready for PiP")
            return
        }

        guard let controller = AVPictureInPictureController(playerLayer: videoView.playerLayer) else {
            NSLog("[Aviation] VideoView — Failed to create PiP controller")
            return
        }

        controller.delegate = pipDelegate
        if #available(iOS 14.2, *) {
            controller.canStartPictureInPictureAutomaticallyFromInline = true
        }

        pipController = controller
        #if DEBUG
            NSLog("[Aviation] VideoView — PiP controller created")
        #endif
    }

    private func teardownPipObservations() {
        currentItemObservation?.invalidate()
        currentItemObservation = nil
        itemStatusObservation?.invalidate()
        itemStatusObservation = nil
    }

    private func teardownPipController() {
        guard let pip = pipController else { return }
        if pip.isPictureInPictureActive, videoView.window != nil {
            pip.stopPictureInPicture()
        }
        pip.delegate = nil
        pipController = nil
    }

    // MARK: - Lifecycle

    deinit {
        // Notify plugins that this view is going away.
        AviationEngineRegistry.notifyVideoViewDetached()

        if let o = didEnterBackgroundObserver {
            NotificationCenter.default.removeObserver(o)
        }
        if let o = willEnterForegroundObserver {
            NotificationCenter.default.removeObserver(o)
        }

        let pip = pipController
        DispatchQueue.main.async {
            pip?.delegate = nil
        }
    }
}

// MARK: - Native Fullscreen Delegate

private class NativeFullscreenDelegate: NSObject, AVPlayerViewControllerDelegate, UIAdaptivePresentationControllerDelegate {
    var onDismiss: ((AVPlayerViewController) -> Void)?

    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        guard let playerVC = presentationController.presentedViewController as? AVPlayerViewController else {
            return
        }
        onDismiss?(playerVC)
    }

    func playerViewController(
        _ playerViewController: AVPlayerViewController,
        willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
    ) {
        coordinator.animate(alongsideTransition: nil) { [weak self, weak playerViewController] _ in
            guard let playerViewController,
                  playerViewController.isBeingDismissed || playerViewController.view.window == nil
            else { return }
            self?.onDismiss?(playerViewController)
        }
    }
}

// MARK: - PiP Delegate

private class PipDelegate: NSObject, AVPictureInPictureControllerDelegate {
    var onStart: (() -> Void)?
    var onStop: (() -> Void)?
    weak var sourceView: UIView?

    func pictureInPictureControllerWillStartPictureInPicture(
        _: AVPictureInPictureController
    ) {
        #if DEBUG
            NSLog("[Aviation] PiP — will start")
        #endif
    }

    func pictureInPictureControllerDidStartPictureInPicture(
        _: AVPictureInPictureController
    ) {
        #if DEBUG
            NSLog("[Aviation] PiP — did start")
        #endif
        onStart?()
    }

    func pictureInPictureControllerWillStopPictureInPicture(
        _: AVPictureInPictureController
    ) {
        #if DEBUG
            NSLog("[Aviation] PiP — will stop")
        #endif
    }

    func pictureInPictureControllerDidStopPictureInPicture(
        _: AVPictureInPictureController
    ) {
        #if DEBUG
            NSLog("[Aviation] PiP — did stop")
        #endif
        onStop?()
    }

    func pictureInPictureController(
        _: AVPictureInPictureController,
        restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
    ) {
        if let view = sourceView, view.window != nil {
            completionHandler(true)
        } else {
            completionHandler(false)
        }
    }
}
