import Foundation
import MediaPlayer
import NitroModules
import UIKit

/**
 * MPNowPlayingAdapter — Lock screen / Control Center / Bluetooth bridge.
 *
 * Wraps MPNowPlayingInfoCenter and MPRemoteCommandCenter as a hexagonal
 * adapter. This class has NO back-reference to the engine or any other
 * domain object. All data is pushed in via explicit method calls.
 *
 * The engine bridge:
 * 1. Creates this adapter at init (no engine reference needed)
 * 2. Calls setEnabledCommands() / setSkipInterval() from JS
 * 3. Pushes state changes via handleStateChange()
 * 4. Pushes metadata via updateMetadata()
 * 5. Pushes time via updateTime()
 * 6. Receives remote command callbacks via onRemoteCommand closure
 *
 * Time tracking:
 * iOS's MPNowPlayingInfoCenter automatically interpolates elapsed time
 * based on MPNowPlayingInfoPropertyPlaybackRate. We only need to update
 * the info center when state actually changes (play, pause, seek, track
 * change). No timer is needed.
 *
 * Threading: all MPNowPlayingInfoCenter / MPRemoteCommandCenter work
 * happens on the main thread.
 */
class MPNowPlayingAdapter: NowPlayingPort {
    // MARK: - System interfaces

    private let nowPlayingInfoCenter = MPNowPlayingInfoCenter.default()
    private let remoteCommandCenter = MPRemoteCommandCenter.shared()

    // MARK: - Remote command handling

    /// Called when a remote command is received. The engine bridge forwards
    /// this to JS callbacks and handles default actions.
    var onRemoteCommand: ((_ command: RemoteCommand, _ positionMs: Double?, _ intervalMs: Double?) -> Void)?

    private var commandTargets: [(Any, MPRemoteCommand)] = []
    private var skipIntervalSeconds: Double = 15.0
    private var overriddenCommandStrings: Set<String> = []

    // MARK: - Cached state (pushed by engine, no back-reference)

    private var currentState: PlaybackState = .idle
    private var currentMetadata: TrackMetadata?
    private var currentTimeInfo: TimeInfo = .init()
    private var currentRate: Double = 1.0

    // MARK: - Deduplication for energy optimization

    /// Last pushed nowPlayingInfo to avoid redundant IPC
    private var lastNowPlayingInfo: [String: Any] = [:]

    // MARK: - Artwork cache

    private var artworkCache: [String: MPMediaItemArtwork] = [:]
    private var pendingArtworkUri: String?
    private var lastMetadataItemUri: String?
    /// Generation counter incremented on each track change. Stale artwork
    /// fetches check this to avoid overwriting a newer track's metadata.
    private var metadataGeneration: UInt = 0

    // MARK: - Manual & ad mode overrides

    private var manualInfo: NowPlayingInfo?
    private var adInfo: (title: String, durationSec: Double)?

    // MARK: - AVPlayer ref for ad mode only

    /// Weak reference to the AVPlayer, used ONLY during ad mode to read
    /// the ad playhead position. During normal playback, time is pushed
    /// by the engine via updateTime().
    private weak var avPlayerForAdMode: AVPlayer?

    // MARK: - Data types

    /// Metadata for the current track, pushed by the engine.
    struct TrackMetadata {
        let uri: String
        let title: String?
        let artist: String?
        let album: String?
        let artworkUri: String?
    }

    /// Time info for the current track, pushed by the engine.
    struct TimeInfo {
        var currentMs: Double = 0
        var durationMs: Double = 0
        var isLive: Bool = false
    }

    // MARK: - Init

    init() {}

    // MARK: - Configuration

    /// Set the AVPlayer reference used only for ad mode time reads.
    /// Call this once after creating the decoder adapter.
    func setAVPlayerForAdMode(_ player: AVPlayer?) {
        avPlayerForAdMode = player
    }

    func setEnabledCommands(_ commands: [RemoteCommand]) {
        let capturedCommands = commands
        let capturedSkipInterval = skipIntervalSeconds

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

            // Disable all commands first
            let allCommands: [MPRemoteCommand] = [
                remoteCommandCenter.playCommand,
                remoteCommandCenter.pauseCommand,
                remoteCommandCenter.stopCommand,
                remoteCommandCenter.nextTrackCommand,
                remoteCommandCenter.previousTrackCommand,
                remoteCommandCenter.skipForwardCommand,
                remoteCommandCenter.skipBackwardCommand,
                remoteCommandCenter.changePlaybackPositionCommand,
                remoteCommandCenter.togglePlayPauseCommand,
            ]
            for cmd in allCommands {
                cmd.isEnabled = false
            }

            // Enable requested commands
            for command in capturedCommands {
                switch command {
                case .play:
                    addTarget(remoteCommandCenter.playCommand) { [weak self] _ in
                        self?.onRemoteCommand?(.play, nil, nil)
                        return .success
                    }
                case .pause:
                    addTarget(remoteCommandCenter.pauseCommand) { [weak self] _ in
                        self?.onRemoteCommand?(.pause, nil, nil)
                        return .success
                    }
                case .stop:
                    addTarget(remoteCommandCenter.stopCommand) { [weak self] _ in
                        self?.onRemoteCommand?(.stop, nil, nil)
                        return .success
                    }
                case .nexttrack:
                    addTarget(remoteCommandCenter.nextTrackCommand) { [weak self] _ in
                        self?.onRemoteCommand?(.nexttrack, nil, nil)
                        return .success
                    }
                case .previoustrack:
                    addTarget(remoteCommandCenter.previousTrackCommand) { [weak self] _ in
                        self?.onRemoteCommand?(.previoustrack, nil, nil)
                        return .success
                    }
                case .skipforward:
                    remoteCommandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: capturedSkipInterval)]
                    addTarget(remoteCommandCenter.skipForwardCommand) { [weak self] event in
                        guard let self else { return .commandFailed }
                        let interval = (event as? MPSkipIntervalCommandEvent)?.interval ?? skipIntervalSeconds
                        onRemoteCommand?(.skipforward, nil, interval * 1000.0)
                        return .success
                    }
                case .skipbackward:
                    remoteCommandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: capturedSkipInterval)]
                    addTarget(remoteCommandCenter.skipBackwardCommand) { [weak self] event in
                        guard let self else { return .commandFailed }
                        let interval = (event as? MPSkipIntervalCommandEvent)?.interval ?? skipIntervalSeconds
                        onRemoteCommand?(.skipbackward, nil, interval * 1000.0)
                        return .success
                    }
                case .seekto:
                    addTarget(remoteCommandCenter.changePlaybackPositionCommand) { [weak self] event in
                        guard let posEvent = event as? MPChangePlaybackPositionCommandEvent else { return .commandFailed }
                        self?.onRemoteCommand?(.seekto, posEvent.positionTime * 1000.0, nil)
                        return .success
                    }
                case .toggleplaypause:
                    addTarget(remoteCommandCenter.togglePlayPauseCommand) { [weak self] _ in
                        self?.onRemoteCommand?(.toggleplaypause, nil, nil)
                        return .success
                    }
                }
            }
        }
    }

    func setSkipInterval(_ seconds: Double) {
        skipIntervalSeconds = seconds
        DispatchQueue.main.async { [weak self] in
            guard let self else { return }
            remoteCommandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: seconds)]
            remoteCommandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: seconds)]
        }
    }

    func overrideCommand(_ command: RemoteCommand) {
        overriddenCommandStrings.insert(command.rawStringValue)
    }

    func clearCommandOverride(_ command: RemoteCommand) {
        overriddenCommandStrings.remove(command.rawStringValue)
    }

    func isCommandOverridden(_ command: RemoteCommand) -> Bool {
        overriddenCommandStrings.contains(command.rawStringValue)
    }

    // MARK: - Pushed updates (called by the engine)

    /// Called when playback state changes.
    func handleStateChange(_ state: PlaybackState) {
        currentState = state

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

            switch state {
            case .loading:
                applyMetadata()
            case .ready:
                applyMetadata()
                applyTime()
            case .playing:
                applyTime()
            case .paused:
                applyTime()
            case .buffering:
                applyTime()
            case .stopped:
                lastMetadataItemUri = nil
                updateNowPlayingInfoIfChanged(nil)
            case .idle, .error:
                break
            }
        }
    }

    /// Called when the current track changes. Pushes metadata to the adapter.
    func updateMetadata(_ metadata: TrackMetadata) {
        currentMetadata = metadata
        DispatchQueue.main.async { [weak self] in
            self?.applyMetadata()
        }
    }

    /// Called when time/position updates. Pushes time info to the adapter.
    /// Only needs to be called on meaningful changes (seek, track change,
    /// play/pause transitions). iOS interpolates between updates automatically.
    func updateTime(_ time: TimeInfo) {
        currentTimeInfo = time
        DispatchQueue.main.async { [weak self] in
            self?.applyTime()
        }
    }

    /// Called when playback rate changes.
    func updateRate(_ rate: Double) {
        currentRate = rate
        // Rate change affects the time display (iOS uses it for interpolation)
        DispatchQueue.main.async { [weak self] in
            self?.applyTime()
        }
    }

    // MARK: - Manual now playing info (JS override)

    func updateNowPlayingInfo(_ info: NowPlayingInfo) {
        manualInfo = info
        DispatchQueue.main.async { [weak self] in
            self?.applyManualNowPlayingInfo(info)
        }
    }

    // MARK: - Ad mode

    /// Enter ad mode — the now playing info shows ad title and duration
    /// instead of the content track info.
    func setAdMode(title: String, durationSeconds: Double) {
        DispatchQueue.main.async { [weak self] in
            guard let self else { return }
            adInfo = (title: title, durationSec: durationSeconds)
            var info = nowPlayingInfoCenter.nowPlayingInfo ?? [:]
            info[MPMediaItemPropertyTitle] = title
            info[MPMediaItemPropertyPlaybackDuration] = durationSeconds
            info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = 0.0
            info[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
            info[MPNowPlayingInfoPropertyIsLiveStream] = false
            updateNowPlayingInfoIfChanged(info)
        }
    }

    /// Exit ad mode and restore content metadata.
    func clearAdMode() {
        DispatchQueue.main.async { [weak self] in
            guard let self else { return }
            adInfo = nil
            lastMetadataItemUri = nil
            applyMetadata()
            applyTime()
        }
    }

    // MARK: - Force refresh (after ad breaks)

    /// Force a full metadata refresh, even if the track URI hasn't changed.
    /// Call this when external code (e.g., IMA ads) may have overwritten
    /// the now playing info and we need to restore the content metadata.
    func forceRefreshMetadata() {
        DispatchQueue.main.async { [weak self] in
            guard let self else { return }
            adInfo = nil
            lastMetadataItemUri = nil
            applyMetadata()
            applyTime()
        }
    }

    // MARK: - Teardown

    func teardown() {
        DispatchQueue.main.async { [weak self] in
            guard let self else { return }
            removeAllCommandTargets()
            updateNowPlayingInfoIfChanged(nil)
        }
    }

    // MARK: - Private: apply metadata to info center

    private func applyMetadata() {
        // Don't overwrite ad metadata while in ad mode
        if adInfo != nil { return }

        if let info = manualInfo {
            applyManualNowPlayingInfo(info)
            return
        }

        guard let metadata = currentMetadata else { return }

        let isTrackChange = (metadata.uri != lastMetadataItemUri)
        var info = lastNowPlayingInfo

        if isTrackChange {
            metadataGeneration &+= 1
            lastMetadataItemUri = metadata.uri
            manualInfo = nil
            info[MPMediaItemPropertyTitle] = metadata.title
            info[MPMediaItemPropertyArtist] = metadata.artist
            info[MPMediaItemPropertyAlbumTitle] = metadata.album

            if let artworkUri = metadata.artworkUri, let cached = artworkCache[artworkUri] {
                info[MPMediaItemPropertyArtwork] = cached
            } else {
                info[MPMediaItemPropertyArtwork] = nil
                if let artworkUri = metadata.artworkUri, let url = URL(string: artworkUri) {
                    fetchAndCacheArtwork(uri: artworkUri, url: url, generation: metadataGeneration)
                }
            }
        }

        let dur = currentTimeInfo.durationMs
        if dur > 0, dur.isFinite {
            info[MPMediaItemPropertyPlaybackDuration] = dur / 1000.0
            info[MPNowPlayingInfoPropertyIsLiveStream] = false
        } else if currentTimeInfo.isLive {
            info[MPNowPlayingInfoPropertyIsLiveStream] = true
        }

        updateNowPlayingInfoIfChanged(info)
    }

    private func applyTime() {
        var info = lastNowPlayingInfo

        if let ad = adInfo {
            // In ad mode: read elapsed time from the AVPlayer reference
            let currentTimeSec = avPlayerForAdMode?.currentTime().seconds ?? 0
            info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = max(0, currentTimeSec)
            info[MPMediaItemPropertyPlaybackDuration] = ad.durationSec
            info[MPNowPlayingInfoPropertyPlaybackRate] = (avPlayerForAdMode?.rate ?? 0) > 0 ? 1.0 : 0.0
        } else {
            // Normal mode: use pushed time info.
            // iOS interpolates automatically based on playback rate.
            info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTimeInfo.currentMs / 1000.0
            info[MPNowPlayingInfoPropertyPlaybackRate] = currentState == .playing ? currentRate : 0.0
            let dur = currentTimeInfo.durationMs
            if dur > 0, dur.isFinite {
                info[MPMediaItemPropertyPlaybackDuration] = dur / 1000.0
            }
        }

        updateNowPlayingInfoIfChanged(info)
    }

    private static let maxArtworkCacheSize = 20

    private func fetchAndCacheArtwork(uri: String, url: URL, generation: UInt) {
        guard pendingArtworkUri != uri else { return }
        pendingArtworkUri = uri

        URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
            DispatchQueue.main.async {
                guard let self else { return }
                if self.pendingArtworkUri == uri { self.pendingArtworkUri = nil }

                if error != nil { return }

                guard let data, !data.isEmpty,
                      let image = UIImage(data: data) else { return }

                if self.artworkCache.count >= Self.maxArtworkCacheSize {
                    if let oldest = self.artworkCache.keys.first {
                        self.artworkCache.removeValue(forKey: oldest)
                    }
                }

                let artwork = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
                self.artworkCache[uri] = artwork

                // Only update now playing if this fetch is still for the current track.
                // A newer track change increments metadataGeneration, making stale
                // fetches no-ops (prevents previous track's artwork from overwriting).
                guard self.metadataGeneration == generation else { return }

                var info = self.lastNowPlayingInfo
                info[MPMediaItemPropertyArtwork] = artwork
                self.updateNowPlayingInfoIfChanged(info)
            }
        }.resume()
    }

    private func applyManualNowPlayingInfo(_ info: NowPlayingInfo) {
        var nowPlayingInfo = lastNowPlayingInfo
        if let title = info.title { nowPlayingInfo[MPMediaItemPropertyTitle] = title }
        if let artist = info.artist { nowPlayingInfo[MPMediaItemPropertyArtist] = artist }
        if let album = info.album { nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = album }
        if let durationMs = info.durationMs { nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = durationMs / 1000.0 }
        if let currentTimeMs = info.currentTimeMs { nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTimeMs / 1000.0 }
        if let rate = info.rate { nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate }
        if let isLive = info.isLiveStream, isLive { nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = true }

        if let artworkUri = info.artworkUri {
            if let cached = artworkCache[artworkUri] {
                nowPlayingInfo[MPMediaItemPropertyArtwork] = cached
            } else if let url = URL(string: artworkUri) {
                fetchAndCacheArtwork(uri: artworkUri, url: url, generation: metadataGeneration)
            }
        }

        updateNowPlayingInfoIfChanged(nowPlayingInfo)
    }

    // MARK: - Private: command targets

    private func addTarget(_ command: MPRemoteCommand, handler: @escaping (MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus) {
        command.isEnabled = true
        let target = command.addTarget(handler: handler)
        commandTargets.append((target, command))
    }

    private func removeAllCommandTargets() {
        for (target, command) in commandTargets {
            command.removeTarget(target)
        }
        commandTargets.removeAll()
    }

    // MARK: - Deduplication Helper

    /// Update nowPlayingInfo only if it differs from last pushed state.
    /// Reduces redundant IPC overhead for energy optimization.
    private func updateNowPlayingInfoIfChanged(_ info: [String: Any]?) {
        // Compare new info with last pushed info
        let newInfo = info ?? [:]

        // Use NSDictionary for deep equality comparison
        if NSDictionary(dictionary: newInfo).isEqual(to: lastNowPlayingInfo) {
            // Skip redundant update
            return
        }

        // Update cached state
        lastNowPlayingInfo = newInfo

        // Push to system
        if newInfo.isEmpty {
            nowPlayingInfoCenter.nowPlayingInfo = nil
        } else {
            nowPlayingInfoCenter.nowPlayingInfo = newInfo
        }
    }

    // MARK: - Lifecycle

    deinit {
        removeAllCommandTargets()
        updateNowPlayingInfoIfChanged(nil)
    }
}

// `RemoteCommand.rawStringValue` is defined once (internal) in
// HybridPlaybackEngine.swift and shared across the module.
