import AVFoundation
import Foundation
import Observation
import os

private let logger = Logger(subsystem: "com.mixboard.MixBoardiOS", category: "AudioEngine")

/// Core audio playback engine using AVAudioEngine for high-quality output.
/// Adapted for iOS with AVAudioSession configuration.
@MainActor
@Observable
final class AudioEngine {
   // MARK: - State

   var isPlaying = false
   var currentTime: TimeInterval = 0
   var duration: TimeInterval = 0
   var volume: Float = 0.8 {
       didSet { playerNode.volume = volume }
   }
   var currentTrack: Track?

   // MARK: - Private

   @ObservationIgnored private let engine = AVAudioEngine()
   @ObservationIgnored private let playerNode = AVAudioPlayerNode()
   @ObservationIgnored private let eqNode = AVAudioUnitEQ(numberOfBands: 3)

   @ObservationIgnored private var audioFile: AVAudioFile?
   @ObservationIgnored private var oggBuffer: AVAudioPCMBuffer?  // For OGG playback
   @ObservationIgnored private var isOGG = false
   @ObservationIgnored private var seekFrame: AVAudioFramePosition = 0
   @ObservationIgnored private var audioLengthFrames: AVAudioFramePosition = 0
   @ObservationIgnored private var audioSampleRate: Double = 44100
   @ObservationIgnored private var isSeeking = false
   @ObservationIgnored private var playbackGeneration: Int = 0

   /// Called on main actor when playback finishes naturally.
   @ObservationIgnored var onPlaybackFinished: (() -> Void)?

   // MARK: - Init

   init() {
       configureAudioSession()
       setupAudioChain()
       observeInterruptions()
       observeRouteChanges()
   }

   deinit {
       NotificationCenter.default.removeObserver(self)
       engine.stop()
   }

   // MARK: - iOS Audio Session

   private func configureAudioSession() {
       let session = AVAudioSession.sharedInstance()
       do {
           try session.setCategory(.playback, mode: .default, options: [])
           try session.setActive(true)
       } catch {
           print("AudioEngine: Failed to configure audio session: \(error)")
       }
   }

   // MARK: - Audio Chain Setup

   private func setupAudioChain() {
       engine.attach(playerNode)
       engine.attach(eqNode)

       configureBandEQ()

       // Connect with nil format — AVAudioEngine will negotiate the format
       // when we schedule a buffer or segment
       let mainMixer = engine.mainMixerNode
       engine.connect(playerNode, to: eqNode, format: nil)
       engine.connect(eqNode, to: mainMixer, format: nil)

       engine.prepare()
   }

   private func configureBandEQ() {
       guard eqNode.bands.count >= 3 else { return }
       let low = eqNode.bands[0]
       low.filterType = .lowShelf
       low.frequency = 100
       low.bandwidth = 1.0
       low.gain = 0
       low.bypass = false

       let mid = eqNode.bands[1]
       mid.filterType = .parametric
       mid.frequency = 1000
       mid.bandwidth = 1.0
       mid.gain = 0
       mid.bypass = false

       let high = eqNode.bands[2]
       high.filterType = .highShelf
       high.frequency = 10000
       high.bandwidth = 1.0
       high.gain = 0
       high.bypass = false
   }

   // MARK: - Playback Controls

   func loadTrack(_ track: Track) throws {
       playbackGeneration += 1
       playerNode.stop()
       isPlaying = false

       let url = track.fileURL
       guard FileManager.default.fileExists(atPath: url.path) else {
           throw AudioEngineError.fileNotFound(url.lastPathComponent)
       }

       // Reset OGG state
       oggBuffer = nil
       isOGG = false
       audioFile = nil

       var didDecodeBuffer = false
       if OGGDecoder.isOGGFile(url) {
           // Decode OGG Vorbis to PCM buffer
           logger.notice("Loading OGG file: \(url.lastPathComponent)")
           let (buffer, format) = try OGGDecoder.decode(url: url)
           oggBuffer = buffer
           isOGG = true
           audioLengthFrames = AVAudioFramePosition(buffer.frameLength)
           audioSampleRate = format.sampleRate
           didDecodeBuffer = true
       }
       #if !DISABLE_OPUS
       if !didDecodeBuffer && OpusDecoder.isOpusFile(url) {
           // Decode Opus to PCM buffer
           logger.notice("Loading Opus file: \(url.lastPathComponent)")
           let (buffer, format) = try OpusDecoder.decode(url: url)
           oggBuffer = buffer
           isOGG = true  // reuse the same buffer playback path
           audioLengthFrames = AVAudioFramePosition(buffer.frameLength)
           audioSampleRate = format.sampleRate
           didDecodeBuffer = true
       }
       #endif
       if !didDecodeBuffer {
           logger.notice("Loading audio file: \(url.lastPathComponent)")
           audioFile = try AVAudioFile(forReading: url)
           guard let file = audioFile else { return }
           audioLengthFrames = file.length
           audioSampleRate = file.processingFormat.sampleRate
       }

       duration = Double(audioLengthFrames) / audioSampleRate
       logger.notice("Loaded: duration=\(self.duration)s, sampleRate=\(self.audioSampleRate), frames=\(self.audioLengthFrames)")
       seekFrame = 0
       currentTime = 0
       currentTrack = track
   }

   func play() {
       guard audioFile != nil || oggBuffer != nil else { return }

       // Ensure audio session is active
       try? AVAudioSession.sharedInstance().setActive(true)

       if !engine.isRunning {
           do {
               try engine.start()
           } catch {
               print("AudioEngine: Failed to start engine: \(error)")
               return
           }
       }

       let remainingFrames = AVAudioFrameCount(audioLengthFrames - seekFrame)
       guard remainingFrames > 0 else { return }

       let gen = playbackGeneration
       let expectedDuration = Double(remainingFrames) / audioSampleRate

       if isOGG, let fullBuffer = oggBuffer {
           // OGG: schedule a slice of the decoded buffer from seekFrame
           guard let format = fullBuffer.format as AVAudioFormat?,
                 let sliceBuffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: remainingFrames) else { return }

           // Copy the relevant portion of the buffer
           let channels = Int(format.channelCount)
           for ch in 0..<channels {
               let src = fullBuffer.floatChannelData![ch].advanced(by: Int(seekFrame))
               let dst = sliceBuffer.floatChannelData![ch]
               dst.update(from: src, count: Int(remainingFrames))
           }
           sliceBuffer.frameLength = remainingFrames

           playerNode.scheduleBuffer(sliceBuffer, completionCallbackType: .dataPlayedBack) { [weak self] _ in
               Task { @MainActor in
                   guard let self else { return }
                   guard self.playbackGeneration == gen else { return }
                   guard expectedDuration < 1.0 || self.currentTime > 0.5 else { return }
                   self.playbackDidFinish()
               }
           }
       } else if let file = audioFile {
           // Standard file-based playback
           playerNode.scheduleSegment(
               file,
               startingFrame: seekFrame,
               frameCount: remainingFrames,
               at: nil,
               completionCallbackType: .dataPlayedBack
           ) { [weak self] _ in
               Task { @MainActor in
                   guard let self else { return }
                   guard self.playbackGeneration == gen else { return }
                   guard expectedDuration < 1.0 || self.currentTime > 0.5 else { return }
                   self.playbackDidFinish()
               }
           }
       }

       playerNode.play()
       isPlaying = true
   }

   func pause() {
       playerNode.pause()
       isPlaying = false
       updateCurrentTime()
   }

   func stop() {
       playbackGeneration += 1
       playerNode.stop()
       isPlaying = false
       seekFrame = 0
       currentTime = 0
   }

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

   func seek(to time: TimeInterval) {
       let wasPlaying = isPlaying
       isSeeking = true
       playbackGeneration += 1
       playerNode.stop()

       seekFrame = AVAudioFramePosition(time * audioSampleRate)
       seekFrame = max(0, min(seekFrame, audioLengthFrames))
       currentTime = Double(seekFrame) / audioSampleRate

       if wasPlaying {
           play()
       }
       DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
           self?.isSeeking = false
       }
   }

   func seek(by delta: TimeInterval) {
       let newTime = max(0, min(currentTime + delta, duration))
       seek(to: newTime)
   }

   // MARK: - EQ Controls

   func setEQ(band: Int, gain: Float) {
       guard band >= 0, band < eqNode.bands.count else { return }
       eqNode.bands[band].gain = gain
   }

   // MARK: - Time Tracking

   func updateCurrentTime() {
       guard !isSeeking else { return }
       guard isPlaying else { return }
       guard let nodeTime = playerNode.lastRenderTime,
             nodeTime.isSampleTimeValid,
             let playerTime = playerNode.playerTime(forNodeTime: nodeTime),
             playerTime.sampleTime >= 0 else { return }

       let newTime = Double(seekFrame + playerTime.sampleTime) / audioSampleRate
       currentTime = max(0, min(newTime, duration))
   }

   private func playbackDidFinish() {
       isPlaying = false
       seekFrame = 0
       currentTime = duration

       if let track = currentTrack {
           track.playCount += 1
           track.lastPlayed = Date()
       }

       onPlaybackFinished?()
   }

   // MARK: - Interruption Handling (phone calls, Siri, etc.)

   private func observeInterruptions() {
       NotificationCenter.default.addObserver(
           forName: AVAudioSession.interruptionNotification,
           object: AVAudioSession.sharedInstance(),
           queue: nil
       ) { [weak self] notification in
           Task { @MainActor in
               self?.handleInterruption(notification)
           }
       }
   }

   private func handleInterruption(_ notification: Notification) {
       guard let info = notification.userInfo,
             let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
             let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }

       switch type {
       case .began:
           // Pause playback when interrupted (phone call, etc.)
           if isPlaying {
               pause()
           }
       case .ended:
           guard let optionsValue = info[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
           let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
           if options.contains(.shouldResume) {
               play()
           }
       @unknown default:
           break
       }
   }

   // MARK: - Route Change Handling (headphones unplug, etc.)

   private func observeRouteChanges() {
       NotificationCenter.default.addObserver(
           forName: AVAudioSession.routeChangeNotification,
           object: AVAudioSession.sharedInstance(),
           queue: nil
       ) { [weak self] notification in
           Task { @MainActor in
               self?.handleRouteChange(notification)
           }
       }
   }

   private func handleRouteChange(_ notification: Notification) {
       guard let info = notification.userInfo,
             let reasonValue = info[AVAudioSessionRouteChangeReasonKey] as? UInt,
             let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return }

       // Pause when headphones are unplugged (standard iOS behavior)
       if reason == .oldDeviceUnavailable {
           if isPlaying {
               pause()
           }
       }
   }
}

// MARK: - Errors

enum AudioEngineError: LocalizedError {
   case fileNotFound(String)

   var errorDescription: String? {
       switch self {
       case .fileNotFound(let name):
           return "Audio file not found: \(name)"
       }
   }
}
