import UIKit
import Flutter
import AVFoundation

@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
  private var iosAudioLifecycleChannel: FlutterMethodChannel?
  private var iosPlatformChannel: FlutterMethodChannel?

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    DispatchQueue.global(qos: .utility).async {
      ChanoraSileroSelfTest.run()
    }

    // Configure the iOS AVAudioSession **category + mode** at
    // app-launch time, but DEFER setActive(true) until the scene
    // is foregrounded. Calling setActive in didFinishLaunching is
    // racy on iOS 17+ devices: if the user launches the app from a
    // cold state, the UIApplication isn't yet `.active` and
    // setActive returns `AVAudioSessionErrorCodeCannotStartPlaying`
    // (561017449) — the iOS audio policy server refuses to grant
    // the audio session because the app is not yet considered the
    // foreground priority owner. Symptom in production builds:
    // 'AVAudioSession setup failed: Error 561017449 "Session
    // activation failed"' in NSLog, after which the audio engine
    // is unusable until the user backgrounds + foregrounds the
    // app.
    //
    // The category itself can be set whenever; only the active
    // state needs to be deferred. We listen for
    // didBecomeActiveNotification and activate then. Most
    // production iOS voice apps (Discord, Zoom, FaceTime) follow
    // this same shape.
    do {
      let session = AVAudioSession.sharedInstance()
      try session.setCategory(
        .playAndRecord,
        mode: .voiceChat,
        // Mode rationale (May 2026, .voiceChat reinstated):
        //
        // We previously used .default mode after discovering that
        // .voiceChat routed output through iOS's in-call audio
        // channel, which made speaker output barely audible. That
        // bug was caused by cpal's RemoteIO unit binding to a stale
        // physical transducer — after migrating to coreaudio-rs +
        // kAudioUnitSubType_VoiceProcessingIO (see
        // crates/chanora_audio/src/ios_voice_unit.rs) the route
        // binding is correct under either mode because VPIO re-binds
        // on overrideOutputAudioPort.
        //
        // .voiceChat advantages over .default:
        //   * Tells iOS this is a VoIP session — other apps' audio
        //     is properly ducked/paused instead of competing.
        //   * Enables correct Bluetooth HFP negotiation without
        //     manual workarounds.
        //   * iOS treats the audio session as a "call" for priority
        //     purposes (won't be interrupted by notification sounds).
        //   * System-level CallKit integration (lock-screen controls).
        //
        // .defaultToSpeaker ensures output goes to the main speaker
        // (not the earpiece) by default when no headphones are
        // connected, compensating for the in-call channel's tendency
        // to route to the earpiece.
        //
        // References:
        //   * https://github.com/twilio/video-quickstart-ios/issues/522
        //   * https://stackoverflow.com/questions/79834998 (Daily.co)
        //
        // Options:
        //   .defaultToSpeaker   : route output to the main speaker
        //                         (not the earpiece) by default
        //                         when no headphones are connected.
        //   .allowBluetoothHFP  : permit Bluetooth Hands-Free
        //                         Profile headsets as both input
        //                         and output.
        //   .allowBluetoothA2DP : permit higher-quality A2DP
        //                         output-only Bluetooth devices.
        options: [.defaultToSpeaker, .allowBluetoothHFP, .allowBluetoothA2DP]
      )
      // Match VPIO / Opus frame cadence to reduce callback pressure.
      try session.setPreferredIOBufferDuration(0.02)
      try session.setPreferredSampleRate(48000.0)
      logAudioSessionState(context: "setCategory")
    } catch {
      NSLog("chanora_flutter: AVAudioSession setCategory failed: \(error)")
    }

    // Activate the session once the app is actually foreground. The
    // notification fires immediately after the cold-launch settles,
    // and again on every resume-from-background — both safe
    // moments to call setActive(true). Repeated activation while
    // already-active is a no-op per the docs.
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(activateAudioSession),
      name: UIApplication.didBecomeActiveNotification,
      object: nil
    )

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

    NotificationCenter.default.addObserver(
      self,
      selector: #selector(handleInterruption(_:)),
      name: AVAudioSession.interruptionNotification,
      object: nil
    )

    NotificationCenter.default.addObserver(
      self,
      selector: #selector(handleMediaServicesReset(_:)),
      name: AVAudioSession.mediaServicesWereResetNotification,
      object: nil
    )

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  /// Called by `didBecomeActiveNotification` (cold-launch settle +
  /// every resume-from-background). Activates the AVAudioSession.
  /// Repeated activation is a no-op when the session is already
  /// active so this is safe to call on every foreground.
  @objc private func activateAudioSession() {
    do {
      try AVAudioSession.sharedInstance().setActive(true, options: [])
      NSLog("chanora_flutter: AVAudioSession activated on foreground")
      // Read back the ACTUAL session state. preferredSampleRate /
      // preferredIOBufferDuration are hints; iOS may pick something
      // else depending on hardware + currently-engaged effects.
      // Without these we can't tell whether VPIO is running at
      // 48 kHz mono (what our render callback assumes) or at e.g.
      // 44.1 kHz (which would explain the user's broken playback
      // \u2014 our render callback would be writing samples at the
      // wrong rate, causing pitch + timing artifacts).
      logAudioSessionState(context: "setActive")
      let s = AVAudioSession.sharedInstance()
      let ins = s.currentRoute.inputs.map { $0.portType.rawValue }.joined(separator: ",")
      NSLog(
        "chanora_flutter: AVAudioSession actual: " +
        "sampleRate=\(s.sampleRate) " +
        "ioBufferDuration=\(String(format: "%.4f", s.ioBufferDuration)) " +
        "inputs=[\(ins)] " +
        "outputVolume=\(s.outputVolume)"
      )
    } catch {
      NSLog("chanora_flutter: AVAudioSession setActive failed: \(error)")
    }
  }

  /// Reads back the actual AVAudioSession state and logs it for
  /// SDD-098 compliance. Called after both setCategory and setActive
  /// to verify that the session accepted the requested configuration.
  private func logAudioSessionState(context: String) {
    let s = AVAudioSession.sharedInstance()
    NSLog("chanora_flutter: [\(context)] category=\(s.category.rawValue) mode=\(s.mode.rawValue) options=\(s.categoryOptions.rawValue) route outputs=\(s.currentRoute.outputs.map { "\($0.portType.rawValue)" })")
    if s.sampleRate != 48000.0 {
      NSLog("chanora_flutter: WARNING: actual sample rate \(s.sampleRate) != requested 48000")
    }
    if s.ioBufferDuration > 0.025 {
      NSLog("chanora_flutter: WARNING: IO buffer duration \(s.ioBufferDuration) > 25ms, may cause latency")
    }
  }

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

    let routeDescription = AVAudioSession.sharedInstance().currentRoute
    let outputs = routeDescription.outputs.map { $0.portType.rawValue }.joined(separator: ",")
    NSLog("chanora_flutter: route change reason=\(reason.rawValue) outputs=\(outputs)")

    // P1: Send the detailed route class to Rust on every route change,
    // not just device plug/unplug. This covers:
    //   - .newDeviceAvailable / .oldDeviceUnavailable (headset plug/unplug)
    //   - .override (speaker/earpiece toggle)
    //   - .categoryChange (session category changed)
    //   - .wakeFromSleep (device woke from sleep)
    //   - .routeConfigurationChange (BT HFP connect/disconnect)
    // The Rust side uses the route class to recompute the processing
    // policy (route_policy.rs) and reset AEC delay state if needed.
    let routeClass = classifyAudioRoute(routeDescription)
    NSLog("chanora_flutter: route class=\(routeClass) reason=\(reason.rawValue)")
    iosAudioLifecycleChannel?.invokeMethod("handleRouteChange", arguments: routeClass)
  }

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

    switch type {
    case .began:
      NSLog("chanora_flutter: audio interruption began")
      iosAudioLifecycleChannel?.invokeMethod("handleInterruptionBegan", arguments: nil)
    case .ended:
      let shouldResume = (userInfo[AVAudioSessionInterruptionOptionKey] as? UInt)
        .map { $0 & AVAudioSession.InterruptionOptions.shouldResume.rawValue != 0 }
        ?? false
      NSLog("chanora_flutter: audio interruption ended shouldResume=\(shouldResume)")
      iosAudioLifecycleChannel?.invokeMethod("handleInterruptionEnded", arguments: shouldResume)
    @unknown default:
      break
    }
  }

  @objc private func handleMediaServicesReset(_ notification: Notification) {
    NSLog("chanora_flutter: media services reset")
    do {
      let session = AVAudioSession.sharedInstance()
      try session.setCategory(
        .playAndRecord,
        mode: .voiceChat,
        options: [.defaultToSpeaker, .allowBluetoothHFP, .allowBluetoothA2DP]
      )
      try session.setPreferredIOBufferDuration(0.02)
      try session.setPreferredSampleRate(48000.0)
      try session.setActive(true, options: [])
      logAudioSessionState(context: "mediaServicesWereReset")
    } catch {
      NSLog("chanora_flutter: AVAudioSession media-services reset rebuild failed: \(error)")
    }
    // P1: After rebuilding the session, send the current route class to
    // Rust so it can recompute the processing policy and reset the
    // AudioUnit. The Rust side handles this via ios_handle_media_services_reset
    // which calls ios_restart_voice_unit.
    let routeClass = classifyAudioRoute(AVAudioSession.sharedInstance().currentRoute)
    NSLog("chanora_flutter: media services reset complete, route=\(routeClass)")
    iosAudioLifecycleChannel?.invokeMethod("handleMediaServicesReset", arguments: routeClass)
  }

  override func applicationWillResignActive(_ application: UIApplication) {
    iosAudioLifecycleChannel?.invokeMethod("handleWillResignActive", arguments: nil)
  }

  override func applicationDidEnterBackground(_ application: UIApplication) {
    iosAudioLifecycleChannel?.invokeMethod("handleDidEnterBackground", arguments: nil)
  }

  override func applicationWillEnterForeground(_ application: UIApplication) {
    iosAudioLifecycleChannel?.invokeMethod("handleWillEnterForeground", arguments: nil)
  }

  override func applicationWillTerminate(_ application: UIApplication) {
    iosAudioLifecycleChannel?.invokeMethod("handleWillTerminate", arguments: nil)
  }

  func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
    GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
    iosAudioLifecycleChannel = FlutterMethodChannel(
      name: "chanora/ios_audio_lifecycle",
      binaryMessenger: engineBridge.applicationRegistrar.messenger()
    )
    iosPlatformChannel = FlutterMethodChannel(
      name: "chanora/ios_platform",
      binaryMessenger: engineBridge.applicationRegistrar.messenger()
    )
    iosPlatformChannel?.setMethodCallHandler { call, result in
      switch call.method {
      case "getMicrophonePermissionState":
        result(self.microphonePermissionStateString())
      case "requestMicrophonePermission":
        AVAudioSession.sharedInstance().requestRecordPermission { granted in
          DispatchQueue.main.async {
            result(granted ? "Granted" : self.microphonePermissionStateString())
          }
        }
      case "openAppSettings":
        guard let url = URL(string: UIApplication.openSettingsURLString) else {
          result(false)
          return
        }
        UIApplication.shared.open(url, options: [:]) { opened in
          result(opened)
        }
      default:
        result(FlutterMethodNotImplemented)
      }
    }
  }

  private func classifyAudioRoute(_ route: AVAudioSessionRouteDescription) -> String {
    for output in route.outputs {
      switch output.portType {
      case .builtInReceiver:
        return "Earpiece"
      case .builtInSpeaker:
        return "Speaker"
      case .headphones, .usbAudio:
        return "WiredHeadset"
      case .bluetoothHFP:
        return "BluetoothHfp"
      case .bluetoothA2DP:
        return "BluetoothA2dp"
      default:
        break
      }
    }
    return "Unknown"
  }

  private func microphonePermissionStateString() -> String {
    switch AVAudioSession.sharedInstance().recordPermission {
    case .granted:
      return "Granted"
    case .denied:
      return "Denied"
    case .undetermined:
      return "NotDetermined"
    @unknown default:
      return "Unknown"
    }
  }

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