import AVFoundation
import Observation

// With the project-wide `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor` setting,
// this class is implicitly @MainActor. That's intentional: @Observable properties
// (isPlaying, currentTime, duration, volume) drive the UI and must be updated on
// the main actor. `deinit` is marked `nonisolated` because Swift does not allow
// actor-isolated deinits — it simply nils out the player reference so ARC handles
// cleanup safely without crossing actor boundaries.
@Observable
final class AudioService {
    var isPlaying = false
    var currentTime: Double = 0
    var duration: Double = 0
    var volume: Float = 0.65 {
        didSet { player?.volume = volume }
    }

    private var player: AVPlayer?
    private var timeObserver: Any?
    private var endObserver: NSObjectProtocol?

    private(set) var isScrubbing = false
    private var seekInProgress = false
    private var pendingSeekTime: Double?

    var onTrackFinished: (() -> Void)?
    var onPlaybackStateChanged: (() -> Void)?

    func play(url: URL) {
        cleanup()

        let item = AVPlayerItem(url: url)
        player = AVPlayer(playerItem: item)
        player?.volume = volume

        timeObserver = player?.addPeriodicTimeObserver(
            forInterval: CMTime(seconds: 0.5, preferredTimescale: 600),
            queue: .main
        ) { [weak self] time in
            guard let self, !self.isScrubbing else { return }
            self.currentTime = time.seconds
            if let dur = self.player?.currentItem?.duration, dur.isValid, !dur.isIndefinite {
                self.duration = dur.seconds
            }
            self.onPlaybackStateChanged?()
        }

        endObserver = NotificationCenter.default.addObserver(
            forName: .AVPlayerItemDidPlayToEndTime,
            object: item,
            queue: .main
        ) { [weak self] _ in
            self?.isPlaying = false
            self?.currentTime = 0
            self?.onPlaybackStateChanged?()
            self?.onTrackFinished?()
        }

        player?.play()
        isPlaying = true
        onPlaybackStateChanged?()
    }

    func pause() {
        player?.pause()
        isPlaying = false
        onPlaybackStateChanged?()
    }

    func resume() {
        player?.play()
        isPlaying = true
        onPlaybackStateChanged?()
    }

    func togglePlayPause() {
        if isPlaying { pause() } else { resume() }
    }

    func seek(to time: Double) {
        let clamped = clampedTime(time)
        currentTime = clamped
        player?.seek(
            to: CMTime(seconds: clamped, preferredTimescale: 600),
            toleranceBefore: .zero,
            toleranceAfter: .zero
        )
    }

    func beginScrubbing() {
        isScrubbing = true
    }

    func scrub(to time: Double) {
        let clamped = clampedTime(time)
        currentTime = clamped
        chaseSeek(to: clamped)
    }

    func endScrubbing(at time: Double) {
        let clamped = clampedTime(time)
        currentTime = clamped
        pendingSeekTime = nil
        seekInProgress = false

        player?.seek(
            to: CMTime(seconds: clamped, preferredTimescale: 600),
            toleranceBefore: .zero,
            toleranceAfter: .zero
        ) { [weak self] _ in
            DispatchQueue.main.async {
                self?.isScrubbing = false
            }
        }
    }

    private func chaseSeek(to time: Double) {
        pendingSeekTime = time
        guard !seekInProgress else { return }
        performPendingSeek()
    }

    private func performPendingSeek() {
        guard let time = pendingSeekTime else { return }
        pendingSeekTime = nil
        seekInProgress = true

        player?.seek(
            to: CMTime(seconds: time, preferredTimescale: 600),
            toleranceBefore: CMTime(seconds: 0.1, preferredTimescale: 600),
            toleranceAfter: CMTime(seconds: 0.1, preferredTimescale: 600)
        ) { [weak self] _ in
            DispatchQueue.main.async {
                guard let self else { return }
                self.seekInProgress = false
                if self.pendingSeekTime != nil {
                    self.performPendingSeek()
                }
            }
        }
    }

    private func clampedTime(_ time: Double) -> Double {
        max(0, min(time, duration))
    }

    func stop() {
        cleanup()
        isPlaying = false
        currentTime = 0
        duration = 0
        onPlaybackStateChanged?()
    }

    private func cleanup() {
        if let obs = timeObserver {
            player?.removeTimeObserver(obs)
            timeObserver = nil
        }
        if let obs = endObserver {
            NotificationCenter.default.removeObserver(obs)
            endObserver = nil
        }
        player?.pause()
        player = nil
    }

    // `nonisolated` is required because Swift does not allow actor-isolated deinits.
    // The player and observers are nilled out here; ARC will handle actual deallocation
    // safely without needing to hop to the main actor.
    nonisolated deinit {
        // Cannot call the MainActor-isolated cleanup() from deinit.
        // The weak references in closures will be invalidated when self is deallocated,
        // and AVPlayer/NotificationCenter observers are released via ARC.
    }
}
