//
//  CustomPlayer.swift
//  test2
//
//  Created by Francesco on 23/02/25.
//

import AVFoundation
import AVKit
import MarqueeLabel
import MediaPlayer
import SwiftUI
import UIKit

class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDelegate {
    let module: ScrapingModule
    let continueWatchingManager: ContinueWatchingManager

    let streamURL: String
    let fullUrl: String
    let titleText: String
    let episodeNumber: Int
    let episodeImageUrl: String
    let subtitlesURL: String?
    let onWatchNext: (() -> Void)?
    let aniListID: Int

    private var aniListUpdatedSuccessfully = false
    private var aniListUpdateImpossible = false
    private var aniListRetryCount = 0
    private let aniListMaxRetries = 6

    var player: AVPlayer!
    var timeObserverToken: Any?
    var inactivityTimer: Timer?
    var updateTimer: Timer?
    var originalRate: Float = 1.0
    var holdGesture: UILongPressGestureRecognizer?

    var isPlaying = true
    var currentTimeVal = 0.0
    var duration = 0.0
    var isVideoLoaded = false

    private var isHoldPauseEnabled: Bool {
        UserDefaults.standard.bool(forKey: "holdForPauseEnabled")
    }

    private var isSkip85Visible: Bool {
        if UserDefaults.standard.object(forKey: "skip85Visible") == nil {
            return true
        }
        return UserDefaults.standard.bool(forKey: "skip85Visible")
    }

    private var isDoubleTapSkipEnabled: Bool {
        if UserDefaults.standard.object(forKey: "doubleTapSeekEnabled") == nil {
            return false
        }
        return UserDefaults.standard.bool(forKey: "doubleTapSeekEnabled")
    }

    var portraitButtonVisibleConstraints: [NSLayoutConstraint] = []
    var portraitButtonHiddenConstraints: [NSLayoutConstraint] = []
    var landscapeButtonVisibleConstraints: [NSLayoutConstraint] = []
    var landscapeButtonHiddenConstraints: [NSLayoutConstraint] = []
    var currentMarqueeConstraints: [NSLayoutConstraint] = []
    private var currentMenuButtonTrailing: NSLayoutConstraint!

    var subtitleForegroundColor: UIColor = .white
    var subtitleBackgroundEnabled = true
    var subtitleFontSize = 20.0
    var subtitleShadowRadius = 1.0
    var subtitlesLoader = VTTSubtitlesLoader()
    var subtitleStackView: UIStackView!
    var subtitleLabels: [UILabel] = []
    var subtitlesEnabled = true {
        didSet {
            subtitleStackView.isHidden = !subtitlesEnabled
        }
    }

    var marqueeLabel: MarqueeLabel!
    var playerViewController: AVPlayerViewController!
    var controlsContainerView: UIView!
    var playPauseButton: UIImageView!
    var backwardButton: UIImageView!
    var forwardButton: UIImageView!
    var topSubtitleLabel: UILabel!
    var dismissButton: UIButton!
    var menuButton: UIButton!
    var watchNextButton: UIButton!
    var watchNextIconButton: UIButton!
    var blackCoverView: UIView!
    var speedButton: UIButton!
    var skip85Button: UIButton!
    var qualityButton: UIButton!
    var holdSpeedIndicator: UIButton!

    var isHLSStream = false
    var qualities: [(String, String)] = []
    var currentQualityURL: URL?
    var baseM3U8URL: URL?

    var sliderHostingController: UIHostingController<MusicProgressSlider<Double>>?
    var sliderViewModel = SliderViewModel()
    var isSliderEditing = false

    var watchNextButtonNormalConstraints: [NSLayoutConstraint] = []
    var watchNextButtonControlsConstraints: [NSLayoutConstraint] = []
    var isControlsVisible = false

    private var subtitleBottomToSliderConstraint: NSLayoutConstraint?
    private var subtitleBottomToSafeAreaConstraint: NSLayoutConstraint?
    var subtitleBottomPadding: CGFloat = 10.0 {
        didSet {
            updateSubtitleLabelConstraints()
        }
    }

    private var wasPlayingBeforeSeek = false

    private var malID: Int?
    private var skipIntervals: (op: CMTimeRange?, ed: CMTimeRange?) = (nil, nil)

    private var skipIntroButton: UIButton!
    private var skipOutroButton: UIButton!
    private let skipButtonBaseAlpha: CGFloat = 0.9
    @Published var segments: [ClosedRange<Double>] = []
    private var skipIntroLeading: NSLayoutConstraint!
    private var skipOutroLeading: NSLayoutConstraint!
    private var originalIntroLeading: CGFloat = 0
    private var originalOutroLeading: CGFloat = 0
    private var skipIntroDismissedInSession = false
    private var skipOutroDismissedInSession = false

    private var playerItemKVOContext = 0
    private var loadedTimeRangesObservation: NSKeyValueObservation?
    private var playerTimeControlStatusObserver: NSKeyValueObservation?

    private var isDimmed = false
    private var dimButton: UIButton!
    private var dimButtonToSlider: NSLayoutConstraint!
    private var dimButtonToRight: NSLayoutConstraint!
    private var dimButtonTimer: Timer?

    private lazy var controlsToHide: [UIView] = [
        dismissButton,
        playPauseButton,
        backwardButton,
        forwardButton,
        sliderHostingController!.view,
        skip85Button,
        marqueeLabel,
        menuButton,
        qualityButton,
        speedButton,
        watchNextButton,
        volumeSliderHostingView!
    ]

    private var originalHiddenStates: [UIView: Bool] = [:]

    private var volumeObserver: NSKeyValueObservation?
    private var audioSession = AVAudioSession.sharedInstance()
    private var hiddenVolumeView = MPVolumeView(frame: .zero)
    private var systemVolumeSlider: UISlider?
    private var volumeValue = 0.0
    private var volumeViewModel = VolumeViewModel()
    var volumeSliderHostingView: UIView?
    private var subtitleDelay = 0.0

    init(module: ScrapingModule,
         continueWatchingManager: ContinueWatchingManager,
         urlString: String,
         fullUrl: String,
         title: String,
         episodeNumber: Int,
         onWatchNext: (() -> Void)?,
         subtitlesURL: String?,
         aniListID: Int,
         episodeImageUrl: String) {
        self.module = module
        self.continueWatchingManager = continueWatchingManager
        self.streamURL = urlString
        self.fullUrl = fullUrl
        self.titleText = title
        self.episodeNumber = episodeNumber
        self.episodeImageUrl = episodeImageUrl
        self.onWatchNext = onWatchNext
        self.subtitlesURL = subtitlesURL
        self.aniListID = aniListID

        super.init(nibName: nil, bundle: nil)

        guard let url = URL(string: urlString) else {
            fatalError("Invalid URL string")
        }

        var request = URLRequest(url: url)
        request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer")
        request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Origin")
        request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
                         forHTTPHeaderField: "User-Agent")

        let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]])
        let playerItem = AVPlayerItem(asset: asset)
        self.player = AVPlayer(playerItem: playerItem)

        let lastPlayedTime = UserDefaults.standard.double(forKey: "lastPlayedTime_\(fullUrl)")
        if lastPlayedTime > 0 {
            let seekTime = CMTime(seconds: lastPlayedTime, preferredTimescale: 1)
            self.player.seek(to: seekTime)
        }
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .black

        setupHoldGesture()
        loadSubtitleSettings()
        setupPlayerViewController()
        setupControls()
        addInvisibleControlOverlays()
        setupWatchNextButton()
        setupSubtitleLabel()
        setupDismissButton()
        volumeSlider()
        setupDimButton()
        setupSpeedButton()
        setupQualityButton()
        setupMenuButton()
        setupMarqueeLabel()
        setupSkip85Button()
        setupSkipButtons()
        setupSkipAndDismissGestures()
        addTimeObserver()
        startUpdateTimer()
        setupAudioSession()
        updateSkipButtonsVisibility()
        setupHoldSpeedIndicator()

        view.bringSubviewToFront(subtitleStackView)

        AniListMutation().fetchMalID(animeId: aniListID) { [weak self] result in
            switch result {
            case .success(let mal):
                self?.malID = mal
                self?.fetchSkipTimes(type: "op")
                self?.fetchSkipTimes(type: "ed")
            case .failure(let error):
                Logger.shared.log("Unable to fetch MAL ID: \(error)", type: .error)
            }
        }

        controlsToHide.forEach { originalHiddenStates[$0] = $0.isHidden }

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
            self?.checkForHLSStream()
        }

        if isHoldPauseEnabled {
            holdForPause()
        }

        do {
            try audioSession.setActive(true)
        } catch {
            Logger.shared.log("Error activating audio session: \(error)", type: .debug)
        }

        volumeViewModel.value = Double(audioSession.outputVolume)

        volumeObserver = audioSession.observe(\.outputVolume, options: [.new]) { [weak self] _, change in
            guard let newVol = change.newValue else { return }
            DispatchQueue.main.async {
                self?.volumeViewModel.value = Double(newVol)
                Logger.shared.log("Hardware volume changed, new value: \(newVol)", type: .debug)
            }
        }

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

        if let url = subtitlesURL, !url.isEmpty {
            subtitlesLoader.load(from: url)
        }

        DispatchQueue.main.async {
            self.isControlsVisible = true
            NSLayoutConstraint.deactivate(self.watchNextButtonNormalConstraints)
            NSLayoutConstraint.activate(self.watchNextButtonControlsConstraints)
            self.watchNextButton.alpha = 1.0
            self.view.layoutIfNeeded()
        }

        if #unavailable(iOS 15) {
            hiddenVolumeView.showsRouteButton = false
        }

        hiddenVolumeView.isHidden = true
        view.addSubview(hiddenVolumeView)

        hiddenVolumeView.translatesAutoresizingMaskIntoConstraints = false
        hiddenVolumeView.widthAnchor.constraint(equalToConstant: 1).isActive = true
        hiddenVolumeView.heightAnchor.constraint(equalToConstant: 1).isActive = true
        hiddenVolumeView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        hiddenVolumeView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true

        if let slider = hiddenVolumeView.subviews.first(where: { $0 is UISlider }) as? UISlider {
            systemVolumeSlider = slider
        }
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        coordinator.animate(alongsideTransition: { _ in
            self.updateMarqueeConstraints()
        })
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        guard let marqueeLabel else {
            return
        }

        let availableWidth = marqueeLabel.frame.width
        let textWidth = marqueeLabel.intrinsicContentSize.width

        if textWidth > availableWidth {
            marqueeLabel.lineBreakMode = .byTruncatingTail
        } else {
            marqueeLabel.lineBreakMode = .byClipping
        }
        updateMenuButtonConstraints()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        player?.play()
        setInitialPlayerRate()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidChange), name: .AVPlayerItemNewAccessLogEntry, object: nil)
        skip85Button?.isHidden = !isSkip85Visible
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        if let playbackSpeed = player?.rate {
            UserDefaults.standard.set(playbackSpeed, forKey: "lastPlaybackSpeed")
        }

        if let token = timeObserverToken {
            player.removeTimeObserver(token)
            timeObserverToken = nil
        }

        loadedTimeRangesObservation?.invalidate()
        loadedTimeRangesObservation = nil

        updateTimer?.invalidate()
        inactivityTimer?.invalidate()

        player.pause()
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
        guard context == &playerItemKVOContext else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
            return
        }
    }

    @objc
    private func playerItemDidChange() {
        DispatchQueue.main.async { [weak self] in
            guard let self else { return }
            if qualityButton.isHidden && isHLSStream {
                qualityButton.isHidden = false
                qualityButton.menu = qualitySelectionMenu()

                updateMenuButtonConstraints()

                UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut) {
                    self.view.layoutIfNeeded()
                }
            }
        }
    }

    private func getSegmentsColor() -> Color {
        if let data = UserDefaults.standard.data(forKey: "segmentsColorData"),
           let uiColor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: data) {
            return Color(uiColor)
        }
        return .yellow
    }

    func setupPlayerViewController() {
        playerViewController = AVPlayerViewController()
        playerViewController.player = player
        playerViewController.showsPlaybackControls = false
        addChild(playerViewController)
        view.addSubview(playerViewController.view)
        playerViewController.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            playerViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
            playerViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            playerViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            playerViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
        playerViewController.didMove(toParent: self)

        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(toggleControls))
        view.addGestureRecognizer(tapGesture)
    }

    func setupControls() {
        controlsContainerView = UIView()
        controlsContainerView.backgroundColor = UIColor.black.withAlphaComponent(0.0)
        view.addSubview(controlsContainerView)
        controlsContainerView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            controlsContainerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            controlsContainerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
            controlsContainerView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            controlsContainerView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
        ])

        blackCoverView = UIView()
        blackCoverView.backgroundColor = UIColor.black.withAlphaComponent(0.4)
        blackCoverView.translatesAutoresizingMaskIntoConstraints = false
        controlsContainerView.insertSubview(blackCoverView, at: 0)
        NSLayoutConstraint.activate([
            blackCoverView.topAnchor.constraint(equalTo: view.topAnchor),
            blackCoverView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            blackCoverView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            blackCoverView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])

        backwardButton = UIImageView(image: UIImage(systemName: "gobackward"))
        backwardButton.tintColor = .white
        backwardButton.contentMode = .scaleAspectFit
        backwardButton.isUserInteractionEnabled = true

        backwardButton.layer.shadowColor = UIColor.black.cgColor
        backwardButton.layer.shadowOffset = CGSize(width: 0, height: 2)
        backwardButton.layer.shadowOpacity = 0.6
        backwardButton.layer.shadowRadius = 4
        backwardButton.layer.masksToBounds = false

        let backwardTap = UITapGestureRecognizer(target: self, action: #selector(seekBackward))
        backwardTap.numberOfTapsRequired = 1
        backwardButton.addGestureRecognizer(backwardTap)

        let backwardLongPress = UILongPressGestureRecognizer(target: self, action: #selector(seekBackwardLongPress(_:)))
        backwardLongPress.minimumPressDuration = 0.5
        backwardButton.addGestureRecognizer(backwardLongPress)
        backwardTap.require(toFail: backwardLongPress)

        controlsContainerView.addSubview(backwardButton)
        backwardButton.translatesAutoresizingMaskIntoConstraints = false

        playPauseButton = UIImageView(image: UIImage(systemName: "pause.fill"))
        playPauseButton.tintColor = .white
        playPauseButton.contentMode = .scaleAspectFit
        playPauseButton.isUserInteractionEnabled = true

        playPauseButton.layer.shadowColor = UIColor.black.cgColor
        playPauseButton.layer.shadowOffset = CGSize(width: 0, height: 2)
        playPauseButton.layer.shadowOpacity = 0.6
        playPauseButton.layer.shadowRadius = 4
        playPauseButton.layer.masksToBounds = false

        let playPauseTap = UITapGestureRecognizer(target: self, action: #selector(togglePlayPause))
        playPauseTap.delaysTouchesBegan = false
        playPauseTap.delegate = self
        playPauseButton.addGestureRecognizer(playPauseTap)

        playPauseButton.addGestureRecognizer(playPauseTap)
        controlsContainerView.addSubview(playPauseButton)
        playPauseButton.translatesAutoresizingMaskIntoConstraints = false

        forwardButton = UIImageView(image: UIImage(systemName: "goforward"))
        forwardButton.tintColor = .white
        forwardButton.contentMode = .scaleAspectFit
        forwardButton.isUserInteractionEnabled = true

        forwardButton.layer.shadowColor = UIColor.black.cgColor
        forwardButton.layer.shadowOffset = CGSize(width: 0, height: 2)
        forwardButton.layer.shadowOpacity = 0.6
        forwardButton.layer.shadowRadius = 4
        forwardButton.layer.masksToBounds = false

        let forwardTap = UITapGestureRecognizer(target: self, action: #selector(seekForward))
        forwardTap.numberOfTapsRequired = 1
        forwardButton.addGestureRecognizer(forwardTap)

        let forwardLongPress = UILongPressGestureRecognizer(target: self, action: #selector(seekForwardLongPress(_:)))
        forwardLongPress.minimumPressDuration = 0.5
        forwardButton.addGestureRecognizer(forwardLongPress)

        forwardTap.require(toFail: forwardLongPress)

        controlsContainerView.addSubview(forwardButton)
        forwardButton.translatesAutoresizingMaskIntoConstraints = false

        let segmentsColor = self.getSegmentsColor()

        let sliderView = MusicProgressSlider(
            value: Binding(
                get: { self.sliderViewModel.sliderValue },
                set: { self.sliderViewModel.sliderValue = $0 }
            ),
            inRange: 0...(duration > 0 ? duration : 1.0),
            activeFillColor: .white,
            fillColor: .white.opacity(0.6),
            textColor: .white.opacity(0.7),
            emptyColor: .white.opacity(0.3),
            height: 33,
            onEditingChanged: { editing in
                if editing {
                    self.isSliderEditing = true

                    self.wasPlayingBeforeSeek = (self.player.timeControlStatus == .playing)
                    self.originalRate = self.player.rate

                    self.player.pause()
                } else {
                    let target = CMTime(seconds: self.sliderViewModel.sliderValue,
                                        preferredTimescale: 600)
                    self.player.seek(
                        to: target,
                        toleranceBefore: .zero,
                        toleranceAfter: .zero
                    ) { [weak self] _ in
                        guard let self else { return }

                        let final = player.currentTime().seconds
                        sliderViewModel.sliderValue = final
                        currentTimeVal = final
                        isSliderEditing = false

                        if wasPlayingBeforeSeek {
                            player.playImmediately(atRate: originalRate)
                        }
                    }
                }
            },
            introSegments: sliderViewModel.introSegments,
            outroSegments: sliderViewModel.outroSegments,
            introColor: segmentsColor,
            outroColor: segmentsColor
        )

        sliderHostingController = UIHostingController(rootView: sliderView)
        guard let sliderHostView = sliderHostingController?.view else { return }
        sliderHostView.backgroundColor = .clear
        sliderHostView.translatesAutoresizingMaskIntoConstraints = false
        controlsContainerView.addSubview(sliderHostView)

        NSLayoutConstraint.activate([
            sliderHostView.leadingAnchor.constraint(equalTo: controlsContainerView.leadingAnchor, constant: 18),
            sliderHostView.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -18),
            sliderHostView.bottomAnchor.constraint(equalTo: controlsContainerView.bottomAnchor, constant: -20),
            sliderHostView.heightAnchor.constraint(equalToConstant: 30)
        ])

        NSLayoutConstraint.activate([
            playPauseButton.centerXAnchor.constraint(equalTo: controlsContainerView.centerXAnchor),
            playPauseButton.centerYAnchor.constraint(equalTo: controlsContainerView.centerYAnchor),
            playPauseButton.widthAnchor.constraint(equalToConstant: 50),
            playPauseButton.heightAnchor.constraint(equalToConstant: 50),

            backwardButton.centerYAnchor.constraint(equalTo: playPauseButton.centerYAnchor),
            backwardButton.trailingAnchor.constraint(equalTo: playPauseButton.leadingAnchor, constant: -50),
            backwardButton.widthAnchor.constraint(equalToConstant: 40),
            backwardButton.heightAnchor.constraint(equalToConstant: 40),

            forwardButton.centerYAnchor.constraint(equalTo: playPauseButton.centerYAnchor),
            forwardButton.leadingAnchor.constraint(equalTo: playPauseButton.trailingAnchor, constant: 50),
            forwardButton.widthAnchor.constraint(equalToConstant: 40),
            forwardButton.heightAnchor.constraint(equalToConstant: 40)
        ])
    }

    func holdForPause() {
        let holdForPauseGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleHoldForPause(_:)))
        holdForPauseGesture.minimumPressDuration = 1
        holdForPauseGesture.numberOfTouchesRequired = 2
        view.addGestureRecognizer(holdForPauseGesture)
    }

    func addInvisibleControlOverlays() {
        let playPauseOverlay = UIButton(type: .custom)
        playPauseOverlay.backgroundColor = .clear
        playPauseOverlay.addTarget(self, action: #selector(togglePlayPause), for: .touchUpInside)
        view.addSubview(playPauseOverlay)
        playPauseOverlay.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            playPauseOverlay.centerXAnchor.constraint(equalTo: playPauseButton.centerXAnchor),
            playPauseOverlay.centerYAnchor.constraint(equalTo: playPauseButton.centerYAnchor),
            playPauseOverlay.widthAnchor.constraint(equalTo: playPauseButton.widthAnchor, constant: 20),
            playPauseOverlay.heightAnchor.constraint(equalTo: playPauseButton.heightAnchor, constant: 20)
        ])
    }

    func setupSkipAndDismissGestures() {
        if isDoubleTapSkipEnabled {
            let doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:)))
            doubleTapGesture.numberOfTapsRequired = 2
            view.addGestureRecognizer(doubleTapGesture)

            if let gestures = view.gestureRecognizers {
                for gesture in gestures {
                    if let tapGesture = gesture as? UITapGestureRecognizer, tapGesture.numberOfTapsRequired == 1 {
                        tapGesture.require(toFail: doubleTapGesture)
                    }
                }
            }
        }

        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        if let introSwipe = skipIntroButton.gestureRecognizers?.first(
            where: { $0 is UISwipeGestureRecognizer && ($0 as! UISwipeGestureRecognizer).direction == .left }
        ),
           let outroSwipe = skipOutroButton.gestureRecognizers?.first(
            where: { $0 is UISwipeGestureRecognizer && ($0 as! UISwipeGestureRecognizer).direction == .left }
           ) {
            panGesture.require(toFail: introSwipe)
            panGesture.require(toFail: outroSwipe)
        }

        view.addGestureRecognizer(panGesture)
    }

    func showSkipFeedback(direction: String) {
        let diameter: CGFloat = 600

        if let existingFeedback = view.viewWithTag(999) {
            existingFeedback.layer.removeAllAnimations()
            existingFeedback.removeFromSuperview()
        }

        let circleView = UIView()
        circleView.backgroundColor = UIColor.white.withAlphaComponent(0.0)
        circleView.layer.cornerRadius = diameter / 2
        circleView.clipsToBounds = true
        circleView.translatesAutoresizingMaskIntoConstraints = false
        circleView.isUserInteractionEnabled = false
        circleView.tag = 999

        let iconName = (direction == "forward") ? "goforward" : "gobackward"
        let imageView = UIImageView(image: UIImage(systemName: iconName))
        imageView.tintColor = .black
        imageView.contentMode = .scaleAspectFit
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.alpha = 0.8

        circleView.addSubview(imageView)

        if direction == "forward" {
            NSLayoutConstraint.activate([
                imageView.centerYAnchor.constraint(equalTo: circleView.centerYAnchor),
                imageView.centerXAnchor.constraint(equalTo: circleView.leadingAnchor, constant: diameter / 4),
                imageView.widthAnchor.constraint(equalToConstant: 100),
                imageView.heightAnchor.constraint(equalToConstant: 100)
            ])
        } else {
            NSLayoutConstraint.activate([
                imageView.centerYAnchor.constraint(equalTo: circleView.centerYAnchor),
                imageView.centerXAnchor.constraint(equalTo: circleView.trailingAnchor, constant: -diameter / 4),
                imageView.widthAnchor.constraint(equalToConstant: 100),
                imageView.heightAnchor.constraint(equalToConstant: 100)
            ])
        }

        view.addSubview(circleView)

        if direction == "forward" {
            NSLayoutConstraint.activate([
                circleView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
                circleView.centerXAnchor.constraint(equalTo: view.trailingAnchor),
                circleView.widthAnchor.constraint(equalToConstant: diameter),
                circleView.heightAnchor.constraint(equalToConstant: diameter)
            ])
        } else {
            NSLayoutConstraint.activate([
                circleView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
                circleView.centerXAnchor.constraint(equalTo: view.leadingAnchor),
                circleView.widthAnchor.constraint(equalToConstant: diameter),
                circleView.heightAnchor.constraint(equalToConstant: diameter)
            ])
        }

        UIView.animate(withDuration: 0.2, animations: {
            circleView.backgroundColor = UIColor.white.withAlphaComponent(0.5)
            imageView.alpha = 0.8
        }) { _ in
            UIView.animate(withDuration: 0.5, delay: 0.5, options: [], animations: {
                circleView.backgroundColor = UIColor.white.withAlphaComponent(0.0)
                imageView.alpha = 0.0
            }, completion: { _ in
                circleView.removeFromSuperview()
                imageView.removeFromSuperview()
            })
        }
    }

    func setupSubtitleLabel() {
        subtitleStackView = UIStackView()
        subtitleStackView.axis = .vertical
        subtitleStackView.alignment = .center
        subtitleStackView.distribution = .fill
        subtitleStackView.spacing = 2

        if let subtitleStackView {
            view.addSubview(subtitleStackView)
            subtitleStackView.translatesAutoresizingMaskIntoConstraints = false

            subtitleBottomToSliderConstraint = subtitleStackView.bottomAnchor.constraint(
                equalTo: sliderHostingController?.view.topAnchor ?? view.bottomAnchor,
                constant: -20
            )

            subtitleBottomToSafeAreaConstraint = subtitleStackView.bottomAnchor.constraint(
                equalTo: view.safeAreaLayoutGuide.bottomAnchor,
                constant: -subtitleBottomPadding
            )

            NSLayoutConstraint.activate([
                subtitleStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                subtitleStackView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 36),
                subtitleStackView.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -36)
            ])

            subtitleBottomToSafeAreaConstraint?.isActive = true
        }

        for _ in 0 ..< 2 {
            let label = UILabel()
            label.textAlignment = .center
            label.numberOfLines = 0
            label.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize))
            subtitleLabels.append(label)
            subtitleStackView.addArrangedSubview(label)
        }

        updateSubtitleLabelAppearance()
    }

    func updateSubtitleLabelConstraints() {
        if isControlsVisible {
            subtitleBottomToSliderConstraint?.constant = -20
        } else {
            subtitleBottomToSafeAreaConstraint?.constant = -subtitleBottomPadding
        }

        view.setNeedsLayout()
        UIView.animate(withDuration: 0.2) {
            self.view.layoutIfNeeded()
        }
    }

    func setupDismissButton() {
        let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold)
        let image = UIImage(systemName: "xmark", withConfiguration: config)

        dismissButton = UIButton(type: .system)
        dismissButton.setImage(image, for: .normal)
        dismissButton.tintColor = .white
        dismissButton.addTarget(self, action: #selector(dismissTapped), for: .touchUpInside)
        controlsContainerView.addSubview(dismissButton)
        dismissButton.translatesAutoresizingMaskIntoConstraints = false

        dismissButton.layer.shadowColor = UIColor.black.cgColor
        dismissButton.layer.shadowOffset = CGSize(width: 0, height: 2)
        dismissButton.layer.shadowOpacity = 0.6
        dismissButton.layer.shadowRadius = 4
        dismissButton.layer.masksToBounds = false

        NSLayoutConstraint.activate([
            dismissButton.leadingAnchor.constraint(equalTo: controlsContainerView.leadingAnchor, constant: 16),
            dismissButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
            dismissButton.widthAnchor.constraint(equalToConstant: 40),
            dismissButton.heightAnchor.constraint(equalToConstant: 40)
        ])
    }

    func setupMarqueeLabel() {
        marqueeLabel = MarqueeLabel()
        marqueeLabel.text = "\(titleText) • Ep \(episodeNumber)"
        marqueeLabel.type = .continuous
        marqueeLabel.textColor = .white
        marqueeLabel.font = UIFont.systemFont(ofSize: 14, weight: .heavy)

        marqueeLabel.speed = .rate(35)
        marqueeLabel.fadeLength = 10.0
        marqueeLabel.leadingBuffer = 1.0
        marqueeLabel.trailingBuffer = 16.0
        marqueeLabel.animationDelay = 2.5

        marqueeLabel.layer.shadowColor = UIColor.black.cgColor
        marqueeLabel.layer.shadowOffset = CGSize(width: 0, height: 2)
        marqueeLabel.layer.shadowOpacity = 0.6
        marqueeLabel.layer.shadowRadius = 4
        marqueeLabel.layer.masksToBounds = false

        marqueeLabel.lineBreakMode = .byTruncatingTail
        marqueeLabel.textAlignment = .left

        controlsContainerView.addSubview(marqueeLabel)
        marqueeLabel.translatesAutoresizingMaskIntoConstraints = false

        updateMarqueeConstraints()
    }

    func volumeSlider() {
        let container = VolumeSliderContainer(volumeVM: self.volumeViewModel) { newVal in
            if let sysSlider = self.systemVolumeSlider {
                sysSlider.value = Float(newVal)
            }
        }

        let hostingController = UIHostingController(rootView: container)
        hostingController.view.backgroundColor = UIColor.clear
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false

        controlsContainerView.addSubview(hostingController.view)
        addChild(hostingController)
        hostingController.didMove(toParent: self)

        self.volumeSliderHostingView = hostingController.view

        NSLayoutConstraint.activate([
            hostingController.view.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -16),
            hostingController.view.widthAnchor.constraint(equalToConstant: 160),
            hostingController.view.heightAnchor.constraint(equalToConstant: 30)
        ])
    }

    private func setupHoldSpeedIndicator() {
        let config = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold)
        let image = UIImage(systemName: "forward.fill", withConfiguration: config)
        var speed = UserDefaults.standard.float(forKey: "holdSpeedPlayer")

        if speed == 0.0 {
            speed = 2.0
        }

        holdSpeedIndicator = UIButton(type: .system)
        holdSpeedIndicator.setTitle(" \(speed)", for: .normal)
        holdSpeedIndicator.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold)
        holdSpeedIndicator.setImage(image, for: .normal)

        holdSpeedIndicator.backgroundColor = UIColor(red: 51 / 255.0, green: 51 / 255.0, blue: 51 / 255.0, alpha: 0.8)
        holdSpeedIndicator.tintColor = .white
        holdSpeedIndicator.setTitleColor(.white, for: .normal)
        holdSpeedIndicator.layer.cornerRadius = 21
        holdSpeedIndicator.alpha = 0

        holdSpeedIndicator.layer.shadowColor = UIColor.black.cgColor
        holdSpeedIndicator.layer.shadowOffset = CGSize(width: 0, height: 2)
        holdSpeedIndicator.layer.shadowOpacity = 0.6
        holdSpeedIndicator.layer.shadowRadius = 4
        holdSpeedIndicator.layer.masksToBounds = false

        view.addSubview(holdSpeedIndicator)
        holdSpeedIndicator.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            holdSpeedIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            holdSpeedIndicator.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20),
            holdSpeedIndicator.heightAnchor.constraint(equalToConstant: 40),
            holdSpeedIndicator.widthAnchor.constraint(greaterThanOrEqualToConstant: 85)
        ])

        holdSpeedIndicator.isUserInteractionEnabled = false
    }

    private func updateSkipButtonsVisibility() {
        let t = currentTimeVal
        let controlsShowing = isControlsVisible

        func handle(_ button: UIButton, range: CMTimeRange?) {
            guard let r = range else { button.isHidden = true; return }

            let inInterval = t >= r.start.seconds && t <= r.end.seconds
            let target     = controlsShowing ? 0.0 : skipButtonBaseAlpha

            if inInterval {
                if button.isHidden {
                    button.alpha = 0
                }
                button.isHidden = false

                UIView.animate(withDuration: 0.25) {
                    button.alpha = target
                }
                return
            }

            guard !button.isHidden else { return }
            UIView.animate(withDuration: 0.15, animations: {
                button.alpha = 0
            }) { _ in
                button.isHidden = true
            }
        }

        handle(skipIntroButton, range: skipIntervals.op)
        handle(skipOutroButton, range: skipIntervals.ed)

        if skipIntroDismissedInSession {
            skipIntroButton.isHidden = true
        } else {
            handle(skipIntroButton, range: skipIntervals.op)
        }
        if skipOutroDismissedInSession {
            skipOutroButton.isHidden = true
        } else {
            handle(skipOutroButton, range: skipIntervals.ed)
        }
    }

    private func updateSegments() {
        sliderViewModel.introSegments.removeAll()
        sliderViewModel.outroSegments.removeAll()

        if let op = skipIntervals.op {
            let start = max(0, op.start.seconds / duration)
            let end = min(1, op.end.seconds / duration)
            sliderViewModel.introSegments.append(start...end)
        }

        if let ed = skipIntervals.ed {
            let start = max(0, ed.start.seconds / duration)
            let end = min(1, ed.end.seconds / duration)
            sliderViewModel.outroSegments.append(start...end)
        }

        let segmentsColor = self.getSegmentsColor()

        DispatchQueue.main.async {
            self.sliderHostingController?.rootView = MusicProgressSlider(
                value: Binding(
                    get: { max(0, min(self.sliderViewModel.sliderValue, self.duration)) }, // Remove extra ')'
                    set: { self.sliderViewModel.sliderValue = max(0, min($0, self.duration)) } // Remove extra ')'
                ),
                inRange: 0...(self.duration > 0 ? self.duration : 1.0),
                activeFillColor: .white,
                fillColor: .white.opacity(0.6),
                textColor: .white.opacity(0.7),
                emptyColor: .white.opacity(0.3),
                height: 33,
                onEditingChanged: { editing in
                    if editing {
                        self.isSliderEditing = true

                        self.wasPlayingBeforeSeek = (self.player.timeControlStatus == .playing)
                        self.originalRate = self.player.rate

                        self.player.pause()
                    } else {
                        let target = CMTime(seconds: self.sliderViewModel.sliderValue,
                                            preferredTimescale: 600)
                        self.player.seek(
                            to: target,
                            toleranceBefore: .zero,
                            toleranceAfter: .zero
                        ) { [weak self] _ in
                            guard let self else { return }

                            let final = player.currentTime().seconds
                            sliderViewModel.sliderValue = final
                            currentTimeVal = final
                            isSliderEditing = false

                            if wasPlayingBeforeSeek {
                                player.playImmediately(atRate: originalRate)
                            }
                        }
                    }
                },
                introSegments: self.sliderViewModel.introSegments,
                outroSegments: self.sliderViewModel.outroSegments,
                introColor: segmentsColor,
                outroColor: segmentsColor
            )
        }
    }

    private func fetchSkipTimes(type: String) {
        guard let mal = malID else { return }
        let url = URL(string: "https://api.aniskip.com/v2/skip-times/\(mal)/\(episodeNumber)?types=\(type)&episodeLength=0")!
        URLSession.shared.dataTask(with: url) { data, _, _ in
            guard let d = data,
                  let resp = try? JSONDecoder().decode(AniSkipResponse.self, from: d),
                  resp.found,
                  let interval = resp.results.first?.interval else { return }

            let range = CMTimeRange(
                start: CMTime(seconds: interval.startTime, preferredTimescale: 600),
                end: CMTime(seconds: interval.endTime, preferredTimescale: 600)
            )
            DispatchQueue.main.async {
                if type == "op" {
                    self.skipIntervals.op = range
                } else {
                    self.skipIntervals.ed = range
                }
                if self.duration > 0 {
                    self.updateSegments()
                }
            }
        }.resume()
    }

    func setupSkipButtons() {
        let introConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold)
        let introImage = UIImage(systemName: "forward.frame", withConfiguration: introConfig)
        skipIntroButton = UIButton(type: .system)
        skipIntroButton.setTitle(" Skip Intro", for: .normal)
        skipIntroButton.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold)
        skipIntroButton.setImage(introImage, for: .normal)

        skipIntroButton.backgroundColor = UIColor(red: 51 / 255.0, green: 51 / 255.0, blue: 51 / 255.0, alpha: 0.8)

        if #available(iOS 15.0, *) {
            var config = UIButton.Configuration.filled()
            config.contentInsets = NSDirectionalEdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10)
            skipIntroButton.configuration = config
        } else {
            skipIntroButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
        }

        skipIntroButton.tintColor = .white
        skipIntroButton.setTitleColor(.white, for: .normal)
        skipIntroButton.layer.cornerRadius = 21
        skipIntroButton.alpha = skipButtonBaseAlpha

/*
        if #unavailable(iOS 15) {
            skipIntroButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
        }
*/
        skipIntroButton.layer.shadowColor = UIColor.black.cgColor
        skipIntroButton.layer.shadowOffset = CGSize(width: 0, height: 2)
        skipIntroButton.layer.shadowOpacity = 0.6
        skipIntroButton.layer.shadowRadius = 4
        skipIntroButton.layer.masksToBounds = false

        skipIntroButton.addTarget(self, action: #selector(skipIntro), for: .touchUpInside)

        view.addSubview(skipIntroButton)
        skipIntroButton.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            skipIntroButton.trailingAnchor.constraint(equalTo: sliderHostingController!.view.trailingAnchor),
            skipIntroButton.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -5),
            skipIntroButton.heightAnchor.constraint(equalToConstant: 40),
            skipIntroButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 104)
        ])

        let outroConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold)
        let outroImage = UIImage(systemName: "forward.frame", withConfiguration: outroConfig)
        skipOutroButton = UIButton(type: .system)
        skipOutroButton.setTitle(" Skip Outro", for: .normal)
        skipOutroButton.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold)
        skipOutroButton.setImage(outroImage, for: .normal)

        skipOutroButton.backgroundColor = UIColor(red: 51 / 255.0, green: 51 / 255.0, blue: 51 / 255.0, alpha: 0.8)

        if #available(iOS 15.0, *) {
            var config = UIButton.Configuration.filled()
            config.contentInsets = NSDirectionalEdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10)
            skipIntroButton.configuration = config
        } else {
            skipIntroButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
        }

        skipOutroButton.tintColor = .white
        skipOutroButton.setTitleColor(.white, for: .normal)
        skipOutroButton.layer.cornerRadius = 21
        skipOutroButton.alpha = skipButtonBaseAlpha

        skipOutroButton.layer.shadowColor = UIColor.black.cgColor
        skipOutroButton.layer.shadowOffset = CGSize(width: 0, height: 2)
        skipOutroButton.layer.shadowOpacity = 0.6
        skipOutroButton.layer.shadowRadius = 4
        skipOutroButton.layer.masksToBounds = false

        skipOutroButton.addTarget(self, action: #selector(skipOutro), for: .touchUpInside)

        view.addSubview(skipOutroButton)
        skipOutroButton.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            skipOutroButton.trailingAnchor.constraint(equalTo: sliderHostingController!.view.trailingAnchor),
            skipOutroButton.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -5),
            skipOutroButton.heightAnchor.constraint(equalToConstant: 40),
            skipOutroButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 104)
        ])
    }

    private func setupDimButton() {
        let cfg = UIImage.SymbolConfiguration(pointSize: 24, weight: .regular)
        dimButton = UIButton(type: .system)
        dimButton.setImage(UIImage(systemName: "moon.fill", withConfiguration: cfg), for: .normal)
        dimButton.tintColor = .white
        dimButton.addTarget(self, action: #selector(dimTapped), for: .touchUpInside)
        controlsContainerView.addSubview(dimButton)
        dimButton.translatesAutoresizingMaskIntoConstraints = false

        dimButton.layer.shadowColor = UIColor.black.cgColor
        dimButton.layer.shadowOffset = CGSize(width: 0, height: 2)
        dimButton.layer.shadowOpacity = 0.6
        dimButton.layer.shadowRadius = 4
        dimButton.layer.masksToBounds = false

        NSLayoutConstraint.activate([
            dimButton.topAnchor.constraint(equalTo: volumeSliderHostingView!.bottomAnchor, constant: 15),
            dimButton.trailingAnchor.constraint(equalTo: volumeSliderHostingView!.trailingAnchor),
            dimButton.widthAnchor.constraint(equalToConstant: 24),
            dimButton.heightAnchor.constraint(equalToConstant: 24)
        ])

        dimButtonToSlider = dimButton.trailingAnchor.constraint(equalTo: volumeSliderHostingView!.trailingAnchor)
        dimButtonToRight = dimButton.trailingAnchor.constraint(equalTo: controlsContainerView.trailingAnchor, constant: -16)
        dimButtonToSlider.isActive = true
    }

    func updateMarqueeConstraints() {
        UIView.performWithoutAnimation {
            NSLayoutConstraint.deactivate(currentMarqueeConstraints)

            let leftSpacing: CGFloat = 2
            let rightSpacing: CGFloat = 6
            let trailingAnchor: NSLayoutXAxisAnchor = (volumeSliderHostingView?.isHidden == false)
            ? volumeSliderHostingView!.leadingAnchor
            : view.safeAreaLayoutGuide.trailingAnchor

            currentMarqueeConstraints = [
                marqueeLabel.leadingAnchor.constraint(
                    equalTo: dismissButton.trailingAnchor, constant: leftSpacing),
                marqueeLabel.trailingAnchor.constraint(
                    equalTo: trailingAnchor, constant: -rightSpacing - 10),
                marqueeLabel.centerYAnchor.constraint(equalTo: dismissButton.centerYAnchor)
            ]
            NSLayoutConstraint.activate(currentMarqueeConstraints)
            view.layoutIfNeeded()
        }
    }

    func setupMenuButton() {
        let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold)
        let image = UIImage(systemName: "text.bubble", withConfiguration: config)

        menuButton = UIButton(type: .system)
        menuButton.setImage(image, for: .normal)
        menuButton.tintColor = .white

        if let subtitlesURL, !subtitlesURL.isEmpty {
            menuButton.showsMenuAsPrimaryAction = true
            menuButton.menu = buildOptionsMenu()
        } else {
            menuButton.isHidden = true
        }

        dismissButton.layer.shadowColor = UIColor.black.cgColor
        dismissButton.layer.shadowOffset = CGSize(width: 0, height: 2)
        dismissButton.layer.shadowOpacity = 0.6
        dismissButton.layer.shadowRadius = 4
        dismissButton.layer.masksToBounds = false

        controlsContainerView.addSubview(menuButton)
        menuButton.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            menuButton.topAnchor.constraint(equalTo: qualityButton.topAnchor),
            menuButton.widthAnchor.constraint(equalToConstant: 40),
            menuButton.heightAnchor.constraint(equalToConstant: 40)
        ])

        currentMenuButtonTrailing = menuButton.trailingAnchor.constraint(equalTo: qualityButton.leadingAnchor, constant: -6)
    }

    func setupSpeedButton() {
        let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold)
        let image = UIImage(systemName: "speedometer", withConfiguration: config)

        speedButton = UIButton(type: .system)
        speedButton.setImage(image, for: .normal)
        speedButton.tintColor = .white
        speedButton.showsMenuAsPrimaryAction = true
        speedButton.menu = speedChangerMenu()

        speedButton.layer.shadowColor = UIColor.black.cgColor
        speedButton.layer.shadowOffset = CGSize(width: 0, height: 2)
        speedButton.layer.shadowOpacity = 0.6
        speedButton.layer.shadowRadius = 4
        speedButton.layer.masksToBounds = false

        controlsContainerView.addSubview(speedButton)
        speedButton.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            speedButton.topAnchor.constraint(equalTo: watchNextButton.topAnchor),
            speedButton.trailingAnchor.constraint(equalTo: watchNextButton.leadingAnchor, constant: 18),
            speedButton.widthAnchor.constraint(equalToConstant: 40),
            speedButton.heightAnchor.constraint(equalToConstant: 40)
        ])
    }

    func setupWatchNextButton() {
        let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold)
        let image = UIImage(systemName: "forward.end", withConfiguration: config)

        watchNextButton = UIButton(type: .system)
        watchNextButton.setImage(image, for: .normal)
        watchNextButton.backgroundColor = .clear
        watchNextButton.tintColor = .white
        watchNextButton.setTitleColor(.white, for: .normal)

        // The shadow:
        watchNextButton.layer.shadowColor = UIColor.black.cgColor
        watchNextButton.layer.shadowOffset = CGSize(width: 0, height: 2)
        watchNextButton.layer.shadowOpacity = 0.6
        watchNextButton.layer.shadowRadius = 4
        watchNextButton.layer.masksToBounds = false

        watchNextButton.addTarget(self, action: #selector(watchNextTapped), for: .touchUpInside)

        controlsContainerView.addSubview(watchNextButton)
        watchNextButton.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            watchNextButton.trailingAnchor.constraint(equalTo: sliderHostingController!.view.trailingAnchor, constant: 20),
            watchNextButton.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -5),
            watchNextButton.heightAnchor.constraint(equalToConstant: 40),
            watchNextButton.widthAnchor.constraint(equalToConstant: 80)
        ])
    }

    func setupSkip85Button() {
        let config = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold)
        let image = UIImage(systemName: "goforward", withConfiguration: config)

        skip85Button = UIButton(type: .system)
        skip85Button.setTitle(" Skip 85s", for: .normal)
        skip85Button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .bold)
        skip85Button.setImage(image, for: .normal)

        skip85Button.backgroundColor = UIColor(red: 51 / 255.0, green: 51 / 255.0, blue: 51 / 255.0, alpha: 0.8)

        if #available(iOS 15.0, *) {
            var config = UIButton.Configuration.filled()
            config.contentInsets = NSDirectionalEdgeInsets(top: 6, leading: 10, bottom: 6, trailing: 10)
            skipIntroButton.configuration = config
        } else {
            skipIntroButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
        }

        skip85Button.tintColor = .white
        skip85Button.setTitleColor(.white, for: .normal)
        skip85Button.layer.cornerRadius = 21
        skip85Button.alpha = 0.7

        if #unavailable(iOS 15) {
            skip85Button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 10, bottom: 6, right: 10)
        }

        skip85Button.layer.shadowColor = UIColor.black.cgColor
        skip85Button.layer.shadowOffset = CGSize(width: 0, height: 2)
        skip85Button.layer.shadowOpacity = 0.6
        skip85Button.layer.shadowRadius = 4
        skip85Button.layer.masksToBounds = false

        skip85Button.addTarget(self, action: #selector(skip85Tapped), for: .touchUpInside)

        view.addSubview(skip85Button)
        skip85Button.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            skip85Button.leadingAnchor.constraint(equalTo: sliderHostingController!.view.leadingAnchor),
            skip85Button.bottomAnchor.constraint(equalTo: sliderHostingController!.view.topAnchor, constant: -5),
            skip85Button.heightAnchor.constraint(equalToConstant: 40),
            skip85Button.widthAnchor.constraint(greaterThanOrEqualToConstant: 97)
        ])

        skip85Button.isHidden = !isSkip85Visible
    }

    private func setupQualityButton() {
        let config = UIImage.SymbolConfiguration(pointSize: 15, weight: .bold)
        let image = UIImage(systemName: "4k.tv", withConfiguration: config)

        qualityButton = UIButton(type: .system)
        qualityButton.setImage(image, for: .normal)
        qualityButton.tintColor = .white
        qualityButton.showsMenuAsPrimaryAction = true
        qualityButton.menu = qualitySelectionMenu()
        qualityButton.isHidden = true

        qualityButton.layer.shadowColor = UIColor.black.cgColor
        qualityButton.layer.shadowOffset = CGSize(width: 0, height: 2)
        qualityButton.layer.shadowOpacity = 0.6
        qualityButton.layer.shadowRadius = 4
        qualityButton.layer.masksToBounds = false

        controlsContainerView.addSubview(qualityButton)
        qualityButton.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            qualityButton.topAnchor.constraint(equalTo: speedButton.topAnchor),
            qualityButton.trailingAnchor.constraint(equalTo: speedButton.leadingAnchor, constant: -6),
            qualityButton.widthAnchor.constraint(equalToConstant: 40),
            qualityButton.heightAnchor.constraint(equalToConstant: 40)
        ])
    }

    func updateSubtitleLabelAppearance() {
        for subtitleLabel in subtitleLabels {
            subtitleLabel.font = UIFont.systemFont(ofSize: CGFloat(subtitleFontSize))
            subtitleLabel.textColor = subtitleForegroundColor
            subtitleLabel.backgroundColor = subtitleBackgroundEnabled
            ? UIColor.black.withAlphaComponent(0.6)
            : .clear
            subtitleLabel.layer.cornerRadius = 5
            subtitleLabel.clipsToBounds = true
            subtitleLabel.layer.shadowColor = UIColor.black.cgColor
            subtitleLabel.layer.shadowRadius = CGFloat(subtitleShadowRadius)
            subtitleLabel.layer.shadowOpacity = 1.0
            subtitleLabel.layer.shadowOffset = .zero
        }
    }

    func addTimeObserver() {
        let interval = CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
        timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval,
                                                           queue: .main) { [weak self] time in
            guard let self,
                  let currentItem = player.currentItem,
                  currentItem.duration.seconds.isFinite else { return }

            let currentDuration = currentItem.duration.seconds
            if currentDuration.isNaN || currentDuration <= 0 { return }

            currentTimeVal = time.seconds
            duration = currentDuration
            updateSegments()

            if !isSliderEditing {
                sliderViewModel.sliderValue = max(0, min(currentTimeVal, duration))
            }

            updateSkipButtonsVisibility()

            UserDefaults.standard.set(currentTimeVal, forKey: "lastPlayedTime_\(fullUrl)")
            UserDefaults.standard.set(duration, forKey: "totalTime_\(fullUrl)")

            if subtitlesEnabled {
                let adjustedTime = currentTimeVal - subtitleDelay
                let cues = subtitlesLoader.cues.filter { adjustedTime >= $0.startTime && adjustedTime <= $0.endTime }
                if cues.isEmpty {
                    subtitleLabels[0].text = cues[0].text.strippedHTML
                    subtitleLabels[0].isHidden = false
                } else {
                    subtitleLabels[0].text = ""
                    subtitleLabels[0].isHidden = !subtitlesEnabled
                }
                if cues.count > 1 {
                    subtitleLabels[1].text = cues[1].text.strippedHTML
                    subtitleLabels[1].isHidden = false
                } else {
                    subtitleLabels[1].text = ""
                    subtitleLabels[1].isHidden = true
                }
            } else {
                subtitleLabels[0].text = ""
                subtitleLabels[0].isHidden = true
                subtitleLabels[1].text = ""
                subtitleLabels[1].isHidden = true
            }

            let segmentsColor = getSegmentsColor()

            DispatchQueue.main.async {
                if let currentItem = self.player.currentItem, currentItem.duration.seconds > 0 {
                    let progress = min(max(self.currentTimeVal / self.duration, 0), 1.0)

                    let item = ContinueWatchingItem(
                        id: UUID(),
                        imageUrl: self.episodeImageUrl,
                        episodeNumber: self.episodeNumber,
                        mediaTitle: self.titleText,
                        progress: progress,
                        streamUrl: self.streamURL,
                        fullUrl: self.fullUrl,
                        subtitles: self.subtitlesURL,
                        aniListID: self.aniListID,
                        module: self.module
                    )
                    self.continueWatchingManager.save(item: item)
                }

                let remainingPercentage = (self.duration - self.currentTimeVal) / self.duration

                if remainingPercentage < 0.1 &&
                    self.aniListID != 0 &&
                    !self.aniListUpdatedSuccessfully &&
                    !self.aniListUpdateImpossible {
                    self.tryAniListUpdate()
                }

                self.sliderHostingController?.rootView = MusicProgressSlider(
                    value: Binding(
                        get: { max(0, min(self.sliderViewModel.sliderValue, self.duration)) },
                        set: {
                            self.sliderViewModel.sliderValue = max(0, min($0, self.duration))
                        }
                    ),
                    inRange: 0...(self.duration > 0 ? self.duration : 1.0),
                    activeFillColor: .white,
                    fillColor: .white.opacity(0.6),
                    textColor: .white.opacity(0.7),
                    emptyColor: .white.opacity(0.3),
                    height: 33,
                    onEditingChanged: { editing in
                        if editing {
                            self.isSliderEditing = true

                            self.wasPlayingBeforeSeek = (self.player.timeControlStatus == .playing)
                            self.originalRate = self.player.rate

                            self.player.pause()
                        } else {
                            let target = CMTime(seconds: self.sliderViewModel.sliderValue,
                                                preferredTimescale: 600)
                            self.player.seek(
                                to: target,
                                toleranceBefore: .zero,
                                toleranceAfter: .zero
                            ) { [weak self] _ in
                                guard let self else { return }

                                let final = player.currentTime().seconds
                                sliderViewModel.sliderValue = final
                                currentTimeVal = final
                                isSliderEditing = false

                                if wasPlayingBeforeSeek {
                                    player.playImmediately(atRate: originalRate)
                                }
                            }
                        }
                    },
                    introSegments: self.sliderViewModel.introSegments,
                    outroSegments: self.sliderViewModel.outroSegments,
                    introColor: segmentsColor,
                    outroColor: segmentsColor
                )
            }
        }
    }

    @objc
    private func skipIntro() {
        if let range = skipIntervals.op {
            player.seek(to: range.end)
            skipIntroButton.isHidden = true
        }
    }

    @objc
    private func skipOutro() {
        if let range = skipIntervals.ed {
            player.seek(to: range.end)
            skipOutroButton.isHidden = true
        }
    }

    func startUpdateTimer() {
        updateTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            guard let self else { return }
            currentTimeVal = player.currentTime().seconds
        }
    }

    func updateMenuButtonConstraints() {
        currentMenuButtonTrailing.isActive = false

        let anchor: NSLayoutXAxisAnchor
        if !qualityButton.isHidden {
            anchor = qualityButton.leadingAnchor
        } else if !speedButton.isHidden {
            anchor = speedButton.leadingAnchor
        } else {
            anchor = controlsContainerView.trailingAnchor
        }

        currentMenuButtonTrailing = menuButton.trailingAnchor.constraint(equalTo: anchor, constant: -6)
        currentMenuButtonTrailing.isActive = true
    }

    @objc
    func toggleControls() {
        if isDimmed {
            dimButton.isHidden = false
            dimButton.alpha = 1.0
            dimButtonTimer?.invalidate()
            dimButtonTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { [weak self] _ in
                guard let self else { return }
                UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut]) {
                    self.dimButton.alpha = 0
                }
            }
        } else {
            isControlsVisible.toggle()
            UIView.animate(withDuration: 0.2) {
                let a: CGFloat = self.isControlsVisible ? 1 : 0
                self.controlsContainerView.alpha = a
                self.skip85Button.alpha = a

                self.subtitleBottomToSafeAreaConstraint?.isActive = !self.isControlsVisible
                self.subtitleBottomToSliderConstraint?.isActive = self.isControlsVisible

                self.view.layoutIfNeeded()
            }
            self.updateSkipButtonsVisibility()
        }
    }

    @objc
    func seekBackwardLongPress(_ gesture: UILongPressGestureRecognizer) {
        if gesture.state == .began {
            let holdValue = UserDefaults.standard.double(forKey: "skipIncrementHold")
            let finalSkip = holdValue > 0 ? holdValue : 30
            currentTimeVal = max(currentTimeVal - finalSkip, 0)
            player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] _ in
                guard self != nil else { return }
            }
            animateButtonRotation(backwardButton, clockwise: false)
        }
    }

    @objc
    func seekForwardLongPress(_ gesture: UILongPressGestureRecognizer) {
        if gesture.state == .began {
            let holdValue = UserDefaults.standard.double(forKey: "skipIncrementHold")
            let finalSkip = holdValue > 0 ? holdValue : 30
            currentTimeVal = min(currentTimeVal + finalSkip, duration)
            player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] _ in
                guard self != nil else { return }
            }
            animateButtonRotation(forwardButton)
        }
    }

    @objc
    func seekBackward() {
        let skipValue = UserDefaults.standard.double(forKey: "skipIncrement")
        let finalSkip = skipValue > 0 ? skipValue : 10
        currentTimeVal = max(currentTimeVal - finalSkip, 0)
        player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] _ in
            guard self != nil else { return }
        }
        animateButtonRotation(backwardButton, clockwise: false)
    }

    @objc
    func seekForward() {
        let skipValue = UserDefaults.standard.double(forKey: "skipIncrement")
        let finalSkip = skipValue > 0 ? skipValue : 10
        currentTimeVal = min(currentTimeVal + finalSkip, duration)
        player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600)) { [weak self] _ in
            guard self != nil else {
                return
            }
        }
        animateButtonRotation(forwardButton)
    }

    @objc
    func handleDoubleTap(_ gesture: UITapGestureRecognizer) {
        let tapLocation = gesture.location(in: view)
        if tapLocation.x < view.bounds.width / 2 {
            seekBackward()
            showSkipFeedback(direction: "backward")
        } else {
            seekForward()
            showSkipFeedback(direction: "forward")
        }
    }

    @objc
    func handleSwipeDown(_ gesture: UISwipeGestureRecognizer) {
        dismiss(animated: true, completion: nil)
    }

    @objc
    func togglePlayPause() {
        if isPlaying {
            player.pause()
            isPlaying = false
            playPauseButton.image = UIImage(systemName: "play.fill")

            DispatchQueue.main.async {
                if !self.isControlsVisible {
                    self.isControlsVisible = true
                    UIView.animate(withDuration: 0.1, animations: {
                        self.controlsContainerView.alpha = 1.0
                        self.skip85Button.alpha = 0.8
                    })
                    self.updateSkipButtonsVisibility()
                }
            }
        } else {
            player.play()
            isPlaying = true
            playPauseButton.image = UIImage(systemName: "pause.fill")
        }
    }

    @objc
    func dismissTapped() {
        dismiss(animated: true, completion: nil)
    }

    @objc
    func watchNextTapped() {
        player.pause()
        dismiss(animated: true) { [weak self] in
            self?.onWatchNext?()
        }
    }

    @objc
    func skip85Tapped() {
        currentTimeVal = min(currentTimeVal + 85, duration)
        player.seek(to: CMTime(seconds: currentTimeVal, preferredTimescale: 600))
    }

    @objc
    private func handleHoldForPause(_ gesture: UILongPressGestureRecognizer) {
        guard isHoldPauseEnabled else { return }

        if gesture.state == .began {
            togglePlayPause()
        }
    }

    @objc
    private func dimTapped() {
        isDimmed.toggle()
        dimButtonTimer?.invalidate()

        UIView.animate(withDuration: 0.25) {
            self.blackCoverView.alpha = self.isDimmed ? 1.0 : 0.4
        }

        UIView.animate(withDuration: 0.25) {
            for view in self.controlsToHide {
                view.alpha = self.isDimmed ? 0 : 1
            }
            self.dimButton.alpha = self.isDimmed ? 0 : 1
        }

        dimButtonToSlider.isActive = !isDimmed
        dimButtonToRight.isActive  = isDimmed
        UIView.animate(withDuration: 0.25) { self.view.layoutIfNeeded() }
    }

    func speedChangerMenu() -> UIMenu {
        let speeds: [Double] = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0]
        let playbackSpeedActions = speeds.map { speed in
            UIAction(title: String(format: "%.2f", speed)) { _ in
                self.player.rate = Float(speed)
                if self.player.timeControlStatus != .playing {
                    self.player.pause()
                }
            }
        }
        return UIMenu(title: "Playback Speed", children: playbackSpeedActions)
    }

    private func tryAniListUpdate() {
        let aniListMutation = AniListMutation()
        aniListMutation.updateAnimeProgress(animeId: self.aniListID, episodeNumber: self.episodeNumber) { [weak self] result in
            guard let self else { return }

            switch result {
            case .success:
                aniListUpdatedSuccessfully = true
                Logger.shared.log("Successfully updated AniList progress for episode \(episodeNumber)", type: .general)

            case .failure(let error):
                let errorString = error.localizedDescription.lowercased()
                Logger.shared.log("AniList progress update failed: \(errorString)", type: .error)

                if errorString.contains("access token not found") {
                    Logger.shared.log("AniList update will NOT retry due to missing token.", type: .error)
                    aniListUpdateImpossible = true
                } else {
                    if aniListRetryCount < aniListMaxRetries {
                        aniListRetryCount += 1

                        let delaySeconds = 5.0
                        Logger.shared.log("AniList update will retry in \(delaySeconds)s (attempt \(aniListRetryCount)).", type: .debug)

                        DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) {
                            self.tryAniListUpdate()
                        }
                    } else {
                        Logger.shared.log("AniList update reached max retries. No more attempts.", type: .error)
                    }
                }
            }
        }
    }

    private func animateButtonRotation(_ button: UIView, clockwise: Bool = true) {
        if button.layer.animation(forKey: "rotate360") != nil {
            return
        }
        button.superview?.layoutIfNeeded()

        button.layer.shouldRasterize = true
        button.layer.rasterizationScale = UIScreen.main.scale
        button.layer.allowsEdgeAntialiasing = true

        let rotation = CABasicAnimation(keyPath: "transform.rotation.z")
        rotation.fromValue = 0
        rotation.toValue   = CGFloat.pi * 2 * (clockwise ? 1 : -1)
        rotation.duration  = 0.43
        rotation.timingFunction = CAMediaTimingFunction(name: .linear)

        button.layer.add(rotation, forKey: "rotate360")

        DispatchQueue.main.asyncAfter(deadline: .now() + rotation.duration) {
            button.layer.shouldRasterize = false
        }
    }

    private func parseM3U8(url: URL, completion: @escaping () -> Void) {
        var request = URLRequest(url: url)
        request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer")
        request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Origin")
        request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
                         forHTTPHeaderField: "User-Agent")

        URLSession.shared.dataTask(with: request) { [weak self] data, _, _ in
            guard let self,
                  let data,
                  let content = String(data: data, encoding: .utf8) else {
                      Logger.shared.log("Failed to load m3u8 file")
                      DispatchQueue.main.async {
                          self?.qualities = []
                          completion()
                      }
                      return
                  }

            let lines = content.components(separatedBy: .newlines)
            var qualities: [(String, String)] = []

            qualities.append(("Auto (Recommended)", url.absoluteString))

            func getQualityName(for height: Int) -> String {
                switch height {
                case 1080...: return "\(height)p (FHD)"
                case 720 ..< 1080: return "\(height)p (HD)"
                case 480 ..< 720: return "\(height)p (SD)"
                default: return "\(height)p"
                }
            }

            for (index, line) in lines.enumerated() {
                if line.contains("#EXT-X-STREAM-INF"), index + 1 < lines.count {
                    if let resolutionRange = line.range(of: "RESOLUTION="),
                       let resolutionEndRange = line[resolutionRange.upperBound...].range(of: ",")
                        ?? line[resolutionRange.upperBound...].range(of: "\n") {
                        let resolutionPart = String(line[resolutionRange.upperBound ..< resolutionEndRange.lowerBound])
                        if let heightStr = resolutionPart.components(separatedBy: "x").last,
                           let height = Int(heightStr) {
                            let nextLine = lines[index + 1].trimmingCharacters(in: .whitespacesAndNewlines)
                            let qualityName = getQualityName(for: height)

                            var qualityURL = nextLine
                            if !nextLine.hasPrefix("http") && nextLine.contains(".m3u8") {
                                if let baseURL = baseM3U8URL {
                                    let baseURLString = baseURL.deletingLastPathComponent().absoluteString
                                    qualityURL = URL(string: nextLine, relativeTo: baseURL)?.absoluteString
                                    ?? baseURLString + "/" + nextLine
                                }
                            }

                            if !qualities.contains(where: { $0.0 == qualityName }) {
                                qualities.append((qualityName, qualityURL))
                            }
                        }
                    }
                }
            }

            DispatchQueue.main.async {
                let autoQuality = qualities.first
                var sortedQualities = qualities.dropFirst().sorted { first, second in
                    let firstHeight = Int(first.0.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()) ?? 0
                    let secondHeight = Int(second.0.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()) ?? 0
                    return firstHeight > secondHeight
                }

                if let auto = autoQuality {
                    sortedQualities.insert(auto, at: 0)
                }

                self.qualities = sortedQualities
                completion()
            }
        }.resume()
    }

    private func switchToQuality(urlString: String) {
        guard let url = URL(string: urlString),
              currentQualityURL?.absoluteString != urlString else { return }

        let currentTime = player.currentTime()
        let wasPlaying = player.rate > 0

        var request = URLRequest(url: url)
        request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Referer")
        request.addValue("\(module.metadata.baseUrl)", forHTTPHeaderField: "Origin")
        request.addValue("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
                         forHTTPHeaderField: "User-Agent")

        let asset = AVURLAsset(url: url, options: ["AVURLAssetHTTPHeaderFieldsKey": request.allHTTPHeaderFields ?? [:]])
        let playerItem = AVPlayerItem(asset: asset)

        player.replaceCurrentItem(with: playerItem)
        player.seek(to: currentTime)
        if wasPlaying {
            player.play()
        }

        currentQualityURL = url

        UserDefaults.standard.set(urlString, forKey: "lastSelectedQuality")
        qualityButton.menu = qualitySelectionMenu()

        if let selectedQuality = qualities.first(where: { $0.1 == urlString })?.0 {
            DropManager.shared.showDrop(title: "Quality: \(selectedQuality)",
                                        subtitle: "",
                                        duration: 0.5,
                                        icon: UIImage(systemName: "eye"))
        }
    }

    private func qualitySelectionMenu() -> UIMenu {
        var menuItems: [UIMenuElement] = []

        if isHLSStream {
            if qualities.isEmpty {
                let loadingAction = UIAction(title: "Loading qualities...", attributes: .disabled) { _ in
                    Logger.shared.log("Loading State Button clicked", type: .info)
                }
                menuItems.append(loadingAction)
            } else {
                var menuTitle = "Video Quality"
                if let currentURL = currentQualityURL?.absoluteString,
                   let selectedQuality = qualities.first(where: { $0.1 == currentURL })?.0 {
                    menuTitle = "Quality: \(selectedQuality)"
                }

                for (name, urlString) in qualities {
                    let isCurrentQuality = currentQualityURL?.absoluteString == urlString

                    let action = UIAction(
                        title: name,
                        state: isCurrentQuality ? .on : .off,
                        handler: { [weak self] _ in
                            self?.switchToQuality(urlString: urlString)
                        }
                    )
                    menuItems.append(action)
                }

                return UIMenu(title: menuTitle, children: menuItems)
            }
        } else {
            let unavailableAction = UIAction(title: "Quality selection unavailable", attributes: .disabled) { _ in
                Logger.shared.log("Disabled State Button clicked", type: .info)
            }
            menuItems.append(unavailableAction)
        }

        return UIMenu(title: "Video Quality", children: menuItems)
    }

    private func checkForHLSStream() {
        guard let url = URL(string: streamURL) else { return }

        if url.absoluteString.contains(".m3u8") {
            isHLSStream = true
            baseM3U8URL = url
            currentQualityURL = url

            parseM3U8(url: url) { [weak self] in
                guard let self else { return }
                if let last = UserDefaults.standard.string(forKey: "lastSelectedQuality"),
                   qualities.contains(where: { $0.1 == last }) {
                    switchToQuality(urlString: last)
                }

                qualityButton.isHidden = false
                qualityButton.menu = qualitySelectionMenu()
                updateMenuButtonConstraints()
                UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseInOut) {
                    self.view.layoutIfNeeded()
                }
            }
        } else {
            isHLSStream = false
            qualityButton.isHidden = true
            updateMenuButtonConstraints()
        }
    }

    func buildOptionsMenu() -> UIMenu {
        var menuElements: [UIMenuElement] = []

        if let subURL = subtitlesURL, !subURL.isEmpty {
            let subtitlesToggleAction = UIAction(title: "Toggle Subtitles") { [weak self] _ in
                guard let self else { return }
                subtitlesEnabled.toggle()
            }

            let foregroundActions = [
                UIAction(title: "White") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = .white }
                    self.loadSubtitleSettings()
                    self.updateSubtitleLabelAppearance()
                },
                UIAction(title: "Yellow") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = .yellow }
                    self.loadSubtitleSettings()
                    self.updateSubtitleLabelAppearance()
                },
                UIAction(title: "Green") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = .green }
                    self.loadSubtitleSettings()
                    self.updateSubtitleLabelAppearance()
                },
                UIAction(title: "Blue") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = .blue }
                    self.loadSubtitleSettings()
                    self.updateSubtitleLabelAppearance()
                },
                UIAction(title: "Red") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = .red }
                    self.loadSubtitleSettings()
                    self.updateSubtitleLabelAppearance()
                },
                UIAction(title: "Purple") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.foregroundColor = .purple }
                    self.loadSubtitleSettings()
                    self.updateSubtitleLabelAppearance()
                }
            ]
            let colorMenu = UIMenu(title: "Subtitle Color", children: foregroundActions)

            let fontSizeActions = [
                UIAction(title: "16") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.fontSize = 16 }
                    self.loadSubtitleSettings()
                    self.updateSubtitleLabelAppearance()
                },
                UIAction(title: "18") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.fontSize = 18 }
                    self.loadSubtitleSettings()
                    self.updateSubtitleLabelAppearance()
                },
                UIAction(title: "20") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.fontSize = 20 }
                    self.loadSubtitleSettings()
                    self.updateSubtitleLabelAppearance()
                },
                UIAction(title: "22") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.fontSize = 22 }
                    self.loadSubtitleSettings()
                    self.updateSubtitleLabelAppearance()
                },
                UIAction(title: "24") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.fontSize = 24 }
                    self.loadSubtitleSettings()
                    self.updateSubtitleLabelAppearance()
                },
                UIAction(title: "Custom") { _ in self.presentCustomFontAlert() }
            ]
            let fontSizeMenu = UIMenu(title: "Font Size", children: fontSizeActions)

            let shadowActions = [
                UIAction(title: "None") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.shadowRadius = 0 }
                    self.loadSubtitleSettings()
                    self.updateSubtitleLabelAppearance()
                },
                UIAction(title: "Low") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.shadowRadius = 1 }
                    self.loadSubtitleSettings()
                    self.updateSubtitleLabelAppearance()
                },
                UIAction(title: "Medium") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.shadowRadius = 3 }
                    self.loadSubtitleSettings()
                    self.updateSubtitleLabelAppearance()
                },
                UIAction(title: "High") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.shadowRadius = 6 }
                    self.loadSubtitleSettings()
                    self.updateSubtitleLabelAppearance()
                }
            ]
            let shadowMenu = UIMenu(title: "Shadow Intensity", children: shadowActions)

            let backgroundActions = [
                UIAction(title: "Toggle") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.backgroundEnabled.toggle() }
                    self.loadSubtitleSettings()
                    self.updateSubtitleLabelAppearance()
                }
            ]
            let backgroundMenu = UIMenu(title: "Background", children: backgroundActions)

            let paddingActions = [
                UIAction(title: "10p") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.bottomPadding = 10 }
                    self.loadSubtitleSettings()
                },
                UIAction(title: "20p") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.bottomPadding = 20 }
                    self.loadSubtitleSettings()
                },
                UIAction(title: "30p") { _ in
                    SubtitleSettingsManager.shared.update { settings in settings.bottomPadding = 30 }
                    self.loadSubtitleSettings()
                },
                UIAction(title: "Custom") { _ in self.presentCustomPaddingAlert() }
            ]
            let paddingMenu = UIMenu(title: "Bottom Padding", children: paddingActions)

            let delayActions = [
                UIAction(title: "-0.5s") { [weak self] _ in
                    guard let self else { return }
                    adjustSubtitleDelay(by: -0.5)
                },
                UIAction(title: "-0.2s") { [weak self] _ in
                    guard let self else { return }
                    adjustSubtitleDelay(by: -0.2)
                },
                UIAction(title: "+0.2s") { [weak self] _ in
                    guard let self else { return }
                    adjustSubtitleDelay(by: 0.2)
                },
                UIAction(title: "+0.5s") { [weak self] _ in
                    guard let self else { return }
                    adjustSubtitleDelay(by: 0.5)
                },
                UIAction(title: "Custom...") { [weak self] _ in
                    guard let self else { return }
                    presentCustomDelayAlert()
                }
            ]

            let resetDelayAction = UIAction(title: "Reset Delay") { [weak self] _ in
                guard let self else { return }
                SubtitleSettingsManager.shared.update { settings in settings.subtitleDelay = 0.0 }
                subtitleDelay = 0.0
                loadSubtitleSettings()
            }

            let delayMenu = UIMenu(title: "Subtitle Delay", children: delayActions + [resetDelayAction])

            let subtitleOptionsMenu = UIMenu(title: "Subtitle Options", children: [
                subtitlesToggleAction, colorMenu, fontSizeMenu, shadowMenu, backgroundMenu, paddingMenu, delayMenu
            ])

            menuElements = [subtitleOptionsMenu]
        }

        return UIMenu(title: "", children: menuElements)
    }

    func adjustSubtitleDelay(by amount: Double) {
        let newValue = subtitleDelay + amount
        let roundedValue = Double(round(newValue * 10) / 10)
        SubtitleSettingsManager.shared.update { settings in settings.subtitleDelay = roundedValue }
        self.subtitleDelay = roundedValue
        self.loadSubtitleSettings()
    }

    func presentCustomDelayAlert() {
        let alert = UIAlertController(title: "Enter Custom Delay", message: nil, preferredStyle: .alert)
        alert.addTextField { textField in
            textField.placeholder = "Delay in seconds"
            textField.keyboardType = .decimalPad
            textField.text = String(format: "%.1f", self.subtitleDelay)
        }
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        alert.addAction(UIAlertAction(title: "Done", style: .default) { _ in
            if let text = alert.textFields?.first?.text, let newDelay = Double(text) {
                SubtitleSettingsManager.shared.update { settings in settings.subtitleDelay = newDelay }
                self.subtitleDelay = newDelay
                self.loadSubtitleSettings()
            }
        })
        present(alert, animated: true)
    }

    func presentCustomPaddingAlert() {
        let alert = UIAlertController(title: "Enter Custom Padding", message: nil, preferredStyle: .alert)
        alert.addTextField { textField in
            textField.placeholder = "Padding Value"
            textField.keyboardType = .numberPad
            textField.text = String(Int(self.subtitleBottomPadding))
        }
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
        alert.addAction(UIAlertAction(title: "Done", style: .default, handler: { _ in
            if let text = alert.textFields?.first?.text, let intValue = Int(text) {
                let newSize = CGFloat(intValue)
                SubtitleSettingsManager.shared.update { settings in settings.bottomPadding = newSize }
                self.loadSubtitleSettings()
            }
        }))
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
            self.present(alert, animated: true, completion: nil)
        }
    }

    func presentCustomFontAlert() {
        let alert = UIAlertController(title: "Enter Custom Font Size", message: nil, preferredStyle: .alert)
        alert.addTextField { textField in
            textField.placeholder = "Font Size"
            textField.keyboardType = .numberPad
            textField.text = String(Int(self.subtitleFontSize))
        }
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
        alert.addAction(UIAlertAction(title: "Done", style: .default, handler: { _ in
            if let text = alert.textFields?.first?.text, let newSize = Double(text) {
                SubtitleSettingsManager.shared.update { settings in settings.fontSize = newSize }
                self.loadSubtitleSettings()
                self.updateSubtitleLabelAppearance()
            }
        }))
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
            self.present(alert, animated: true, completion: nil)
        }
    }

    func loadSubtitleSettings() {
        let settings = SubtitleSettingsManager.shared.settings
        self.subtitleForegroundColor = settings.foregroundColor
        self.subtitleFontSize = settings.fontSize
        self.subtitleShadowRadius = settings.shadowRadius
        self.subtitleBackgroundEnabled = settings.backgroundEnabled
        self.subtitleBottomPadding = settings.bottomPadding
        self.subtitleDelay = settings.subtitleDelay
    }

    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        if UserDefaults.standard.bool(forKey: "alwaysLandscape") {
            return .landscape
        } else {
            return .all
        }
    }

    override var prefersHomeIndicatorAutoHidden: Bool {
        true
    }

    override var prefersStatusBarHidden: Bool {
        true
    }

    func setupAudioSession() {
        do {
            let audioSession = AVAudioSession.sharedInstance()
            try audioSession.setCategory(.playback, mode: .moviePlayback, options: .mixWithOthers)
            try audioSession.setActive(true)
            try audioSession.overrideOutputAudioPort(.speaker)
        } catch {
            Logger.shared.log("Didn't set up AVAudioSession: \(error)", type: .debug)
        }

        volumeObserver = audioSession.observe(\.outputVolume, options: [.new]) { [weak self] _, change in
            guard let newVol = change.newValue else { return }
            if let oldVol = self?.volumeViewModel.value, abs(Double(newVol) - oldVol) < 0.02 {
                return
            }
            DispatchQueue.main.async {
                self?.volumeViewModel.value = Double(newVol)
                Logger.shared.log("Hardware volume changed, new value: \(newVol)", type: .debug)
            }
        }
    }

    private func setupHoldGesture() {
        holdGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleHoldGesture(_:)))
        holdGesture?.minimumPressDuration = 0.5
        if let holdGesture {
            view.addGestureRecognizer(holdGesture)
        }
    }

    @objc
    private func handleHoldGesture(_ gesture: UILongPressGestureRecognizer) {
        switch gesture.state {
        case .began:
            beginHoldSpeed()
        case .ended, .cancelled:
            endHoldSpeed()
        default:
            break
        }
    }

    @objc
    private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: view)

        switch gesture.state {
        case .ended:
            if translation.y > 100 {
                dismiss(animated: true, completion: nil)
            }
        default:
            break
        }
    }

    private func beginHoldSpeed() {
        guard let player else { return }
        originalRate = player.rate
        let holdSpeed = UserDefaults.standard.float(forKey: "holdSpeedPlayer")
        let speed = holdSpeed > 0 ? holdSpeed : 2.0
        player.rate = speed

        UIView.animate(withDuration: 0.1) {
            self.holdSpeedIndicator.alpha = 0.8
        }
    }

    private func endHoldSpeed() {
        player?.rate = originalRate

        UIView.animate(withDuration: 0.2) {
            self.holdSpeedIndicator.alpha = 0
        }
    }

    private func setInitialPlayerRate() {
        if UserDefaults.standard.bool(forKey: "rememberPlaySpeed") {
            let lastPlayedSpeed = UserDefaults.standard.float(forKey: "lastPlaybackSpeed")
            player?.rate = lastPlayedSpeed > 0 ? lastPlayedSpeed : 1.0
        }
    }

    func setupTimeControlStatusObservation() {
        playerTimeControlStatusObserver = player.observe(\.timeControlStatus, options: [.new]) { [weak self] player, _ in
            guard self != nil else { return }
            if player.timeControlStatus == .paused,
               let reason = player.reasonForWaitingToPlay {
                Logger.shared.log("Paused reason: \(reason)", type: .error)
                if reason == .toMinimizeStalls {
                    player.play()
                }
            }
        }
    }

    struct VolumeSliderContainer: View {
        @ObservedObject var volumeVM: VolumeViewModel
        var updateSystemSlider: ((Double) -> Void)?

        var body: some View {
            VolumeSlider(
                value: Binding(
                    get: { volumeVM.value },
                    set: { newVal in
                        volumeVM.value = newVal
                        updateSystemSlider?(newVal)
                    }
                ),
                inRange: 0...1,
                activeFillColor: .white,
                fillColor: .white.opacity(0.6),
                emptyColor: .white.opacity(0.3),
                height: 10
            )
                .shadow(color: Color.black.opacity(0.6), radius: 4, x: 0, y: 2)
        }
    }
}

// yes? Like the plural of the famous american rapper ye? -IBHRAD
// low taper fade the meme is massive -cranci
// cranci still doesnt have a job -seiike
// guys watch Clannad already - ibro
