//
//  AudioPlayer.swift
//  humand
//
//  Created by Oleksandr Shumihin on 26/11/25.
//  Copyright © 2025 Humand. All rights reserved.
//

import AVFoundation
import Foundation
import React

private func audioPlayerLog(_ message: String) {
  print("[AudioPlayer] \(message)")
}

enum LoadTrackResult {
  case Success(Double)
  case Error(String)
}

@objc
final class HumandAudioPlayer: NSObject, AVAudioPlayerDelegate {
  private var player: AVAudioPlayer?
  private var isPaused: Bool = false
  private var isPlaying: Bool = false
  private var playbackRate: Float = 1.0

  @objc
  static let shared: HumandAudioPlayer = HumandAudioPlayer()

  deinit {
    reset()
  }

  override init() {
    super.init()

    NotificationCenter.default.addObserver(
      self,
      selector: #selector(audioRouteChanged),
      name: AVAudioSession.routeChangeNotification,
      object: nil
    )
  }

  private var state: [String: Any] {
    return [
      "isPlaying": isPlaying,
      "isPaused": isPaused,
      "playbackRate": player?.rate ?? 1.0,
      "duration": player?.duration ?? 0.0,
      "position": player?.currentTime ?? 0.0,
    ]
  }

  @inline(__always)
  private func isUsingExternalOutput() -> Bool {
    let outputs = AVAudioSession.sharedInstance().currentRoute.outputs
    return outputs.contains(where: {
      $0.portType != .builtInSpeaker &&
      $0.portType != .builtInReceiver
    })
  }

  // Configures AVAudioSession so playback ignores the iOS silent switch.
  // Matches the "phone away from face" config in handleProximityChange so
  // the existing proximity-based earpiece routing keeps working.
  private func configureAudioSessionForPlayback() {
    if CallManager.shared.isAudioSessionOwnedByCall() {
      audioPlayerLog("configureAudioSessionForPlayback: skipped (call active)")
      return
    }

    let audioSession = AVAudioSession.sharedInstance()
    let desiredOptions: AVAudioSession.CategoryOptions = [.allowBluetooth, .allowBluetoothA2DP, .defaultToSpeaker]

    do {
      // Skip setCategory (the expensive part — triggers route evaluation)
      // when category, mode AND options already match.
      if audioSession.category != .playAndRecord
          || audioSession.mode != .default
          || audioSession.categoryOptions != desiredOptions {
        try audioSession.setCategory(
          .playAndRecord,
          mode: .default,
          options: desiredOptions
        )
        audioPlayerLog("configureAudioSessionForPlayback: category set")
      } else {
        audioPlayerLog("configureAudioSessionForPlayback: category/mode/options already configured, only reactivating")
      }
      try audioSession.setActive(true)
      audioPlayerLog("configureAudioSessionForPlayback: session active")
    } catch {
      audioPlayerLog("configureAudioSessionForPlayback ERROR: \(error.localizedDescription)")
    }
  }

  @objc
  private func audioRouteChanged(notification: Notification) {
    audioPlayerLog("audioRouteChanged: isPlaying=\(isPlaying), callActive=\(CallManager.shared.isAudioSessionOwnedByCall())")
    if !isPlaying {
      return
    }

    if isUsingExternalOutput() {
      stopProximityObserving()
    } else {
      startProximityObserving()
    }
  }

  private func startProximityObserving() {
    if CallManager.shared.isAudioSessionOwnedByCall() {
      audioPlayerLog("startProximityObserving: skipped (StreamVideo client alive, proximity would modify audio session)")
      return
    }
    if isUsingExternalOutput() {
      audioPlayerLog("startProximityObserving: skipped (external output)")
      return
    }

    DispatchQueue.main.async { [weak self] in
      UIDevice.current.isProximityMonitoringEnabled = true
      audioPlayerLog("startProximityObserving: enabled")

      guard let self else { return }

      NotificationCenter.default.addObserver(
        self,
        selector: #selector(handleProximityChange),
        name: UIDevice.proximityStateDidChangeNotification,
        object: nil
      )
    }
  }

  private func stopProximityObserving() {
    DispatchQueue.main.async { [weak self] in
      UIDevice.current.isProximityMonitoringEnabled = false
      audioPlayerLog("stopProximityObserving: disabled")

      guard let self else { return }

      NotificationCenter.default.removeObserver(
        self,
        name: UIDevice.proximityStateDidChangeNotification,
        object: nil
      )
    }
  }

  @objc
  private func handleProximityChange() {
    let isNear = UIDevice.current.proximityState
    let callActive = CallManager.shared.isAudioSessionOwnedByCall()
    audioPlayerLog("handleProximityChange: isNear=\(isNear), callActive=\(callActive)")

    if callActive {
      audioPlayerLog("handleProximityChange: SKIPPED — call is active, not modifying audio session")
      return
    }

    let audioSession = AVAudioSession.sharedInstance()

    do {
      // .voiceChat routes audio through the earpiece (the desired behavior
      // when the phone is held to the ear). When the phone is away, switch
      // to .default mode so .defaultToSpeaker can route through the
      // loudspeaker.
      let targetMode: AVAudioSession.Mode = isNear ? .voiceChat : .default
      var targetOptions: AVAudioSession.CategoryOptions = [.allowBluetooth, .allowBluetoothA2DP]
      if !isNear {
        targetOptions.insert(.defaultToSpeaker)
      }

      if isPlaying {
        player?.pause()
        audioPlayerLog("handleProximityChange: paused playback before category change")
      }

      audioPlayerLog("handleProximityChange: setting category .playAndRecord, mode \(targetMode.rawValue), options=\(targetOptions)")
      try audioSession.setCategory(.playAndRecord, mode: targetMode, options: targetOptions)
      try audioSession.setActive(true)
      audioPlayerLog("handleProximityChange: audio session configured successfully")

      if isPlaying {
        player?.play()
        audioPlayerLog("handleProximityChange: resumed playback")
      }
    } catch {
      audioPlayerLog("handleProximityChange ERROR: \(error.localizedDescription)")
    }
  }

  @objc
  func replace(_ options: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
    let uri = options["uri"] as? String
    let position = options["position"] as? NSNumber
    let play = options["play"] as? Bool

    audioPlayerLog("replace: uri=\(uri ?? "nil"), position=\(position ?? 0), play=\(play ?? false), callActive=\(CallManager.shared.isAudioSessionOwnedByCall())")

    guard let uri else {
      reject("HUMAND_AUDIO_PLAYER_ERROR", "No path provided", nil)
      return
    }

    let res = load(uri)

    switch res {
    case .Success(let duration):
      if let position {
        player?.currentTime = position.doubleValue
      }

      if play == true {
        isPlaying = true
        configureAudioSessionForPlayback()
        startProximityObserving()
        player?.play()
        audioPlayerLog("replace: started playback, duration=\(duration)")
      }

      resolve([
        "duration": duration,
        "uri": uri,
      ])
    case .Error(let error):
      audioPlayerLog("replace ERROR: \(error)")
      reject("HUMAND_AUDIO_PLAYER_ERROR", error, nil)
    }
  }

  @objc
  func play(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
    audioPlayerLog("play: callActive=\(CallManager.shared.isAudioSessionOwnedByCall())")
    configureAudioSessionForPlayback()
    startProximityObserving()
    player?.play()
    isPaused = false
    isPlaying = true
    resolve(nil)
  }

  @objc
  func pause(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
    audioPlayerLog("pause")
    stopProximityObserving()
    player?.pause()
    isPaused = true
    isPlaying = false
    resolve(nil)
  }

  @objc
  func seekTo(_ seconds: NSNumber) {
    guard let player else { return }
    let time = seconds.doubleValue
    if time < 0 || time > player.duration { return }
    player.currentTime = time
    audioPlayerLog("seekTo: \(time)")
  }

  @objc
  func setPlaybackRate(_ rate: NSNumber) {
    guard let player else { return }
    playbackRate = rate.floatValue
    player.rate = playbackRate
    audioPlayerLog("setPlaybackRate: \(playbackRate)")
  }

  @objc
  func getState() -> [String: Any] {
    return state
  }

  @objc
  func reset() {
    audioPlayerLog("reset: wasPlaying=\(isPlaying)")
    isPaused = false
    isPlaying = false
    player?.stop()
    player = nil
    stopProximityObserving()
    NotificationCenter.default.removeObserver(self)
  }

  private func load(_ path: String) -> LoadTrackResult {
    audioPlayerLog("load: path=\(path)")
    let url: URL
    isPaused = false
    isPlaying = false
    stopProximityObserving()

    if path.hasPrefix("file://") {
      guard let fileUrl = URL(string: path) else {
        return .Error("Invalid file URL: \(path)")
      }
      url = fileUrl
    } else {
      url = URL(fileURLWithPath: path)
    }

    player?.stop()
    player = nil

    do {
      player = try AVAudioPlayer(contentsOf: url)
      player!.delegate = self
      player!.enableRate = true
      player!.rate = playbackRate
      player!.prepareToPlay()
      audioPlayerLog("load: success, duration=\(player!.duration)")
      return .Success(player!.duration)
    } catch {
      audioPlayerLog("load ERROR: \(error.localizedDescription)")
      return .Error("Failed to load audio file.")
    }
  }

  // MARK: - AVAudioPlayerDelegate

  func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
    audioPlayerLog("audioPlayerDidFinishPlaying: success=\(flag)")
    stopProximityObserving()
    player.currentTime = player.duration
    isPlaying = false
    isPaused = false
  }
}
