import AppKit
import AVFoundation
import Combine
@preconcurrency import MediaPlayer
import Observation
import os

@Observable
@MainActor
public final class PlaybackController {
    private let logger = Logger(subsystem: "com.spalencsar.BearWave", category: "Playback")
    private let player = AVPlayer()
    private let artworkProvider: (RadioStation?, String?, String?) -> NSImage
    var onMetadataChanged: ((String?, String?, RadioStation?) -> Void)?
    private var retryTask: Task<Void, Never>?
    private var retryAttempts = 0
    private var lastURL: URL?
    private var lastStation: RadioStation?
    private var cancellables: Set<AnyCancellable> = []
    private var itemCancellables: Set<AnyCancellable> = []
    private var metadataHandler: MetadataHandler?

    var state: PlaybackStatus = .stopped
    public var isPlaying: Bool { state.isPlaying }
    var currentStation: RadioStation?
    var currentTrackTitle = ""
    var currentTrackArtist = ""
    var currentArtwork: NSImage = BearWaveAssets.image(named: "bearwave") ?? NSImage()
    var currentTime: TimeInterval = 0
    var duration: TimeInterval = 0
    var volume: Double = 0.55 {
        didSet {
            player.volume = Float(min(max(volume, 0), 1))
            updateNowPlaying()
        }
    }

    init(artworkProvider: @escaping (RadioStation?, String?, String?) -> NSImage = { station, _, _ in
        BearWaveAssets.image(named: "bearwave") ?? NSImage()
    }) {
        self.artworkProvider = artworkProvider
        player.volume = Float(volume)
        configureRemoteCommands()
        observePlaybackNotifications()
        observePlayer()
        configureTimeObserver()
    }

    func play(_ station: RadioStation) {
        guard let url = URL(string: station.playbackURLString) else { return }
        retryTask?.cancel()
        retryAttempts = 0
        lastURL = url
        lastStation = station
        currentStation = station
        currentTrackTitle = ""
        currentTrackArtist = ""
        state = .connecting
        itemCancellables.removeAll()

        let item = AVPlayerItem(url: url)

        let metadataOutput = AVPlayerItemMetadataOutput(identifiers: nil)
        let handler = MetadataHandler { [weak self] artist, title in
            Task { @MainActor [weak self] in
                guard let self else { return }
                var changed = false
                if let title, title != self.currentTrackTitle {
                    self.currentTrackTitle = title
                    changed = true
                }
                if let artist, artist != self.currentTrackArtist {
                    self.currentTrackArtist = artist
                    changed = true
                }
                if changed {
                    self.onMetadataChanged?(self.currentTrackArtist, self.currentTrackTitle, self.currentStation)
                    self.updateNowPlaying()
                }
            }
        }
        metadataOutput.setDelegate(handler, queue: .main)
        item.add(metadataOutput)
        metadataHandler = handler

        observeItem(item)
        player.replaceCurrentItem(with: item)
        player.play()
        setInitialArtwork()
        updateNowPlaying()
    }

    func resume() {
        if let station = lastStation {
            play(station)
        } else {
            player.play()
            state = .connecting
            updateNowPlaying()
        }
    }

    func pause() {
        player.pause()
        state = .paused
        updateNowPlaying()
    }

    func stop() {
        retryTask?.cancel()
        player.pause()
        player.replaceCurrentItem(with: nil)
        state = .stopped
        currentStation = nil
        currentTrackTitle = ""
        currentTrackArtist = ""
        currentArtwork = BearWaveAssets.image(named: "bearwave") ?? NSImage()
        currentTime = 0
        duration = 0
        itemCancellables.removeAll()
        metadataHandler = nil
        onMetadataChanged?(nil, nil, nil)
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
        MPNowPlayingInfoCenter.default().playbackState = .stopped
    }

    public func togglePlayPause() {
        isPlaying ? pause() : resume()
    }

    private var timeObserverToken: Any?

    func configureTimeObserver() {
        let interval = CMTime(seconds: 1, preferredTimescale: 1)
        timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
            Task { @MainActor in
                guard let self else { return }
                let currentSeconds = time.seconds
                guard currentSeconds.isFinite, currentSeconds >= 0 else { return }

                self.currentTime = currentSeconds
                let itemDuration = self.player.currentItem?.duration
                if let itemDuration, itemDuration.timescale > 0, itemDuration.value > 0 {
                    let dur = CMTimeGetSeconds(itemDuration)
                    if dur.isFinite, dur > 0 {
                        self.duration = dur
                    }
                }
            }
        }
    }

    private func observePlaybackNotifications() {
        NotificationCenter.default.addObserver(
            forName: .AVPlayerItemFailedToPlayToEndTime,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            let error = (notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? NSError)
            Task { @MainActor in
                guard let self else { return }
                let resolvedError = error ?? self.player.currentItem?.error as NSError?
                let playbackError = Self.classifyError(resolvedError)
                self.logger.info("playback failed: \(playbackError.title) – \(playbackError.detail)")
                self.state = .failed(playbackError)
                if playbackError.isRetryable {
                    self.scheduleRetry()
                }
            }
        }

        NotificationCenter.default.addObserver(
            forName: .AVPlayerItemPlaybackStalled,
            object: nil,
            queue: .main
        ) { [weak self] _ in
            Task { @MainActor in
                guard let self else { return }
                self.state = .buffering
                self.scheduleRetry()
            }
        }
    }

    private func observePlayer() {
        player.publisher(for: \.timeControlStatus)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] status in
                Task { @MainActor in self?.handleTimeControlStatus(status) }
            }
            .store(in: &cancellables)
    }

    private func observeItem(_ item: AVPlayerItem) {
        item.publisher(for: \.status)
            .receive(on: DispatchQueue.main)
            .sink { [weak self, weak item] status in
                Task { @MainActor in self?.handleItemStatus(status, item: item) }
            }
            .store(in: &itemCancellables)
    }

    private func handleItemStatus(_ status: AVPlayerItem.Status, item: AVPlayerItem?) {
        switch status {
        case .unknown:
            state = .connecting
        case .readyToPlay:
            if player.timeControlStatus == .playing {
                state = .playing
            } else {
                state = .buffering
            }
        case .failed:
            let error = Self.classifyError(item?.error as NSError?)
            logger.info("item failed: \(error.title) – \(error.detail)")
            state = .failed(error)
            if error.isRetryable {
                scheduleRetry()
            }
        @unknown default:
            state = .failed(.unknown(String(localized: "Unsupported stream state")))
        }
        updateNowPlaying()
    }

    private func handleTimeControlStatus(_ status: AVPlayer.TimeControlStatus) {
        guard currentStation != nil else { return }
        switch status {
        case .paused:
            if state != .stopped, !isFailed {
                state = .paused
            }
        case .waitingToPlayAtSpecifiedRate:
            if !isFailed {
                state = player.currentItem?.status == .unknown ? .connecting : .buffering
            }
        case .playing:
            state = .playing
        @unknown default:
            break
        }
        updateNowPlaying()
    }

    private func scheduleRetry() {
        guard retryAttempts < 2, let url = lastURL else { return }
        if case let .failed(error) = state, !error.isRetryable {
            logger.info("skipping retry: error is not retryable")
            return
        }
        retryAttempts += 1
        logger.info("scheduling retry attempt \(self.retryAttempts)")
        retryTask?.cancel()
        retryTask = Task { [weak self] in
            try? await Task.sleep(for: .milliseconds(1200))
            await MainActor.run {
                guard let self else { return }
                let item = AVPlayerItem(url: url)
                self.itemCancellables.removeAll()
                self.observeItem(item)
                self.state = .connecting
                self.player.replaceCurrentItem(with: item)
                self.player.play()
                self.updateNowPlaying()
            }
        }
    }

    private func configureRemoteCommands() {
        let commandCenter = MPRemoteCommandCenter.shared()
        commandCenter.playCommand.addTarget { [weak self] _ in
            Task { @MainActor in self?.resume() }
            return .success
        }
        commandCenter.pauseCommand.addTarget { [weak self] _ in
            Task { @MainActor in self?.pause() }
            return .success
        }
        commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
            Task { @MainActor in self?.togglePlayPause() }
            return .success
        }
        commandCenter.stopCommand.addTarget { [weak self] _ in
            Task { @MainActor in self?.stop() }
            return .success
        }
    }

    func refreshNowPlaying() {
        logger.info("refreshNowPlaying called")
        currentArtwork = artworkProvider(currentStation, currentTrackArtist.isEmpty ? nil : currentTrackArtist, currentTrackTitle.isEmpty ? nil : currentTrackTitle)
        updateNowPlaying()
    }

    public func updateNowPlayingExternally() {
        updateNowPlaying()
    }

    private func setInitialArtwork() {
        currentArtwork = artworkProvider(currentStation, nil, nil)
    }

    private func updateNowPlaying() {
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo()
        MPNowPlayingInfoCenter.default().playbackState = state == .playing ? .playing : .paused
    }

    private func nowPlayingInfo() -> [String: Any] {
        var info: [String: Any] = [
            MPMediaItemPropertyTitle: currentTrackTitle.isEmpty ? currentStation?.name ?? "BearWave" : currentTrackTitle,
            MPMediaItemPropertyAlbumTitle: currentStation?.name ?? "BearWave",
            MPNowPlayingInfoPropertyPlaybackRate: state == .playing ? 1.0 : 0.0
        ]
        if !currentTrackArtist.isEmpty {
            info[MPMediaItemPropertyArtist] = currentTrackArtist
        }
        if let artwork = nowPlayingArtwork() {
            info[MPMediaItemPropertyArtwork] = artwork
        }
        return info
    }

    private func nowPlayingArtwork() -> MPMediaItemArtwork? {
        let image = artworkProvider(currentStation, currentTrackArtist.isEmpty ? nil : currentTrackArtist, currentTrackTitle.isEmpty ? nil : currentTrackTitle)
        guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil }
        let size = image.size
        return MPMediaItemArtwork(boundsSize: size, requestHandler: { @Sendable requestedSize in
            NSImage(cgImage: cgImage, size: requestedSize)
        })
    }

    private var isFailed: Bool {
        if case .failed = state { return true }
        return false
    }

    private static func classifyError(_ error: NSError?) -> PlaybackError {
        guard let error else { return .unknown(String(localized: "Stream failed")) }

        let domain = error.domain
        let code = error.code

        if domain == NSURLErrorDomain {
            switch code {
            case NSURLErrorTimedOut:
                return .connectionTimeout
            case NSURLErrorCannotFindHost, NSURLErrorDNSLookupFailed:
                return .dnsResolutionFailed
            case NSURLErrorCannotConnectToHost, NSURLErrorNetworkConnectionLost:
                return .stationOffline
            case NSURLErrorUnsupportedURL, NSURLErrorFileDoesNotExist:
                return .codecNotSupported
            default:
                if (500...599).contains(code) {
                    return .httpError(code)
                }
                return .unknown(error.localizedDescription)
            }
        }

        if domain == "CoreMediaErrorDomain" {
            switch code {
            case -12785:
                return .streamInterrupted
            case -12935, -16042, -15697:
                return .codecNotSupported
            default:
                return .unknown(error.localizedDescription)
            }
        }

        if domain == "AVFoundationErrorDomain" {
            switch code {
            case -11800, -11828, -11838:
                return .codecNotSupported
            default:
                return .unknown(error.localizedDescription)
            }
        }

        if (400...599).contains(code) {
            return .httpError(code)
        }

        return .unknown(error.localizedDescription)
    }
}

final class MetadataHandler: NSObject, AVPlayerItemMetadataOutputPushDelegate {
    private let onMetadata: @Sendable (String?, String?) -> Void

    init(onMetadata: @escaping @Sendable (String?, String?) -> Void) {
        self.onMetadata = onMetadata
    }

    func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
        guard let items = groups.first?.items else { return }
        let parsed = parseMetadataItems(items)
        onMetadata(parsed.artist, parsed.title)
    }
}

private func parseMetadataItems(_ items: [AVMetadataItem]) -> (artist: String?, title: String?) {
    var artist: String?
    var title: String?

    for item in items {
        let key = metadataKey(for: item)
        let value = metadataStringValue(for: item)
        guard let value, !value.isEmpty else { continue }

        if key.contains("artist") || key.contains("author") {
            artist = value
        } else if key.contains("title") || key.contains("song") {
            if let split = splitArtistTitle(value) {
                artist = artist ?? split.artist
                title = split.title
            } else {
                title = value
            }
        } else if artist == nil && title == nil, let split = splitArtistTitle(value) {
            artist = split.artist
            title = split.title
        }
    }

    return (artist, title)
}

private func metadataKey(for item: AVMetadataItem) -> String {
    let common = item.commonKey?.rawValue ?? ""
    let identifier = item.identifier?.rawValue ?? ""
    let key = item.key as? String ?? ""
    return [common, identifier, key].joined(separator: " ").lowercased()
}

private func metadataStringValue(for item: AVMetadataItem) -> String? {
    if let stringValue = item.stringValue {
        return cleanMetadataString(stringValue)
    }
    if let value = item.value as? String {
        return cleanMetadataString(value)
    }
    return nil
}

private func cleanMetadataString(_ value: String) -> String {
    value
        .replacingOccurrences(of: "StreamTitle=", with: "")
        .trimmingCharacters(in: CharacterSet(charactersIn: " ';\""))
        .trimmingCharacters(in: .whitespacesAndNewlines)
}

private func splitArtistTitle(_ value: String) -> (artist: String, title: String)? {
    let separators = [" - ", " – ", " — "]
    for separator in separators {
        let parts = value.components(separatedBy: separator)
        guard parts.count >= 2 else { continue }
        let artist = parts[0].trimmingCharacters(in: .whitespacesAndNewlines)
        let title = parts.dropFirst().joined(separator: separator).trimmingCharacters(in: .whitespacesAndNewlines)
        if !artist.isEmpty, !title.isEmpty {
            return (artist, title)
        }
    }
    return nil
}
