import AVFoundation
import Flutter
import Foundation
#if os(iOS) || os(tvOS)
  import AVKit
  import UIKit
#endif

/// Diagnostics harness for #1300: plays known assets through a bare AVPlayer
/// or through a bare AVSampleBufferAudioRenderer so a tester can read the
/// receiver's format display per test.
///
/// Modes:
///  - hlsAtmos:    Apple's public fMP4 Atmos example stream (device+AVR+MAT baseline)
///  - hlsControl:  Apple's public EC3 5.1 (non-JOC) example stream (control)
///  - rawEc3:      raw .ec3 elementary stream via AVAssetResourceLoader with an
///                 unbounded content length — a faithful rehearsal of the mpv
///                 AVPlayer audio sink's feeding model
///  - rawEc3Finite: same loader but passing through the real content length,
///                 isolating "loader trick" failures from "raw ES" failures
///  - asbarNative: the decisive arm. Reads the asset with AVAssetReader at
///                 `outputSettings: nil` and hands the resulting *native*
///                 compressed CMSampleBuffers and *native* CMFormatDescription
///                 straight to AVSampleBufferAudioRenderer, following Apple's
///                 flexible enhanced buffering sequence. No mpv, no FFmpeg
///                 spdif muxer, no generated ASBD, no hand-built dec3. If this
///                 reaches Atmos, the production sink has a construction bug;
///                 if it does not, AVSampleBufferAudioRenderer cannot carry
///                 JOC on this route and the architecture must change.
///  - asbarGenerated: same feed, but the format description is rebuilt the way
///                 the mpv AO builds it (generated ASBD + dec3 magic cookie +
///                 MPEG_5_1_C layout). A/B against asbarNative isolates
///                 descriptor construction from everything else.
public class AtmosProbePlugin: NSObject, FlutterPlugin {
  private static let hlsAtmosUrl =
    "https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8"
  private static let hlsControlUrl =
    "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_adv_example_hevc/master.m3u8"

  private var player: AVPlayer?
  private var loader: RawEc3Loader?
  private var asbar: AsbarProbe?
  #if os(iOS) || os(tvOS)
    private var routePicker: AVRoutePickerView?
  #endif

  public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(
      name: "plezy/atmos_probe", binaryMessenger: registrar.messenger())
    let instance = AtmosProbePlugin()
    registrar.addMethodCallDelegate(instance, channel: channel)
  }

  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    switch call.method {
    case "start":
      guard let args = call.arguments as? [String: Any],
        let mode = args["mode"] as? String
      else {
        result(FlutterError(code: "bad_args", message: "mode required", details: nil))
        return
      }
      start(
        mode: mode, url: args["url"] as? String,
        sessionMode: args["sessionMode"] as? String, result: result)
    case "stop":
      stopPlayback()
      result(nil)
    case "showRoutePicker":
      result(setRoutePickerVisible(true))
    case "hideRoutePicker":
      result(setRoutePickerVisible(false))
    case "getStatus":
      result(status())
    default:
      result(FlutterMethodNotImplemented)
    }
  }

  private func start(
    mode: String, url: String?, sessionMode: String?,
    result: @escaping FlutterResult
  ) {
    stopPlayback()

    let item: AVPlayerItem
    switch mode {
    case "hlsAtmos", "hlsControl":
      let raw = mode == "hlsAtmos" ? Self.hlsAtmosUrl : Self.hlsControlUrl
      guard let streamUrl = URL(string: raw) else {
        result(FlutterError(code: "bad_url", message: raw, details: nil))
        return
      }
      item = AVPlayerItem(url: streamUrl)
    case "rawEc3", "rawEc3Finite":
      guard let source = url.flatMap(URL.init(string:)) else {
        result(FlutterError(code: "bad_url", message: "rawEc3 needs a source url", details: nil))
        return
      }
      let loader = RawEc3Loader(source: source, finiteLength: mode == "rawEc3Finite")
      self.loader = loader
      item = AVPlayerItem(asset: loader.asset)
      item.preferredForwardBufferDuration = 1.0
      loader.begin()
    case "asbarNative", "asbarGenerated":
      guard let source = url.flatMap(URL.init(string:)) else {
        result(
          FlutterError(code: "bad_url", message: "\(mode) needs a source url", details: nil))
        return
      }
      let probe = AsbarProbe(
        source: source,
        regenerateFormatDescription: mode == "asbarGenerated",
        // Dolby's Figure 1 prescribes mode .default; .moviePlayback is what
        // the shipping AO used. Selectable so the tester can A/B it.
        sessionMode: sessionMode == "moviePlayback" ? .moviePlayback : .default)
      asbar = probe
      probe.start { error in
        if let error {
          result(FlutterError(code: "asbar_failed", message: error, details: nil))
        } else {
          result(nil)
        }
      }
      return
    default:
      result(FlutterError(code: "bad_mode", message: mode, details: nil))
      return
    }

    let player = AVPlayer(playerItem: item)
    player.automaticallyWaitsToMinimizeStalling = false
    // Must stay true: the route picker exists so these arms can be compared
    // against the sample-buffer arms on an AirPlay destination, and AirPlay is
    // the only route where the system resolves renderingMode and
    // supportedOutputChannelLayouts. Pinning playback local would leave the
    // AVPlayer controls on HDMI while the picker moved everything else.
    player.allowsExternalPlayback = true
    self.player = player
    player.play()
    result(nil)
  }

  /// Dolby's flow opens with an AVRoutePickerView so the tester can move
  /// playback to an AirPlay destination. That matters here beyond conformance:
  /// AVRoutePickerView.h states media from an AVSampleBufferAudioRenderer can
  /// be routed to AirPlay on tvOS, and AirPlay is the only route where
  /// `renderingMode` and `supportedOutputChannelLayouts` actually resolve — so
  /// this is what makes the AirPlay arm of the test matrix reachable from the
  /// device.
  private func setRoutePickerVisible(_ visible: Bool) -> Bool {
    #if os(iOS) || os(tvOS)
      guard visible else {
        routePicker?.removeFromSuperview()
        routePicker = nil
        return true
      }
      if routePicker != nil { return true }
      guard let window = Self.keyWindow() else { return false }
      let picker = AVRoutePickerView()
      picker.translatesAutoresizingMaskIntoConstraints = false
      window.addSubview(picker)
      NSLayoutConstraint.activate([
        picker.centerXAnchor.constraint(equalTo: window.centerXAnchor),
        picker.bottomAnchor.constraint(equalTo: window.centerYAnchor, constant: -40),
        picker.widthAnchor.constraint(equalToConstant: 120),
        picker.heightAnchor.constraint(equalToConstant: 80),
      ])
      routePicker = picker
      window.setNeedsFocusUpdate()
      window.updateFocusIfNeeded()
      return true
    #else
      return false
    #endif
  }

  #if os(iOS) || os(tvOS)
    private static func keyWindow() -> UIWindow? {
      UIApplication.shared.connectedScenes
        .compactMap { $0 as? UIWindowScene }
        .flatMap(\.windows)
        .first { $0.isKeyWindow }
        ?? UIApplication.shared.connectedScenes
        .compactMap { $0 as? UIWindowScene }
        .flatMap(\.windows)
        .first
    }
  #endif

  private func stopPlayback() {
    player?.pause()
    player?.replaceCurrentItem(with: nil)
    player = nil
    loader?.cancel()
    loader = nil
    asbar?.cancel()
    asbar = nil
    _ = setRoutePickerVisible(false)
    // Run isolation: never let a failed compressed route leak into the next
    // variant or into other apps.
    try? AVAudioSession.sharedInstance().setActive(
      false, options: .notifyOthersOnDeactivation)
  }

  private func status() -> [String: Any] {
    var out: [String: Any] = [:]

    let session = AVAudioSession.sharedInstance()
    out["maxOutputChannels"] = session.maximumOutputNumberOfChannels
    out["outputLatencyMs"] = Int(session.outputLatency * 1000)
    out["route"] = session.currentRoute.outputs.map { port in
      "\(port.portType.rawValue)/\(port.portName)/\(port.channels?.count ?? 0)ch"
    }.joined(separator: ", ")
    out["sessionCategory"] = session.category.rawValue
    out["sessionMode"] = session.mode.rawValue
    out["routeSharingPolicy"] = session.routeSharingPolicy.rawValue
    out["categoryOptions"] = session.categoryOptions.rawValue
    if #available(iOS 17.2, tvOS 17.2, *) {
      out["renderingMode"] = String(describing: session.renderingMode)
      out["renderingModeRawValue"] = session.renderingMode.rawValue
      // Empty on HDMI by design (CarPlay/AirPlay only). Reported so a tester
      // can tell "empty because HDMI" from "empty because inactive".
      out["supportedOutputChannelLayouts"] = session.supportedOutputChannelLayouts.map {
        String(format: "0x%08x/%uch", $0.layoutTag, $0.channelCount)
      }.joined(separator: ", ")
    }

    if let asbar {
      out.merge(asbar.statusSnapshot()) { _, new in new }
      return out
    }

    guard let player = player else {
      out["state"] = "idle"
      return out
    }

    let item = player.currentItem
    out["state"] =
      switch player.timeControlStatus {
      case .paused: "paused"
      case .waitingToPlayAtSpecifiedRate:
        "waiting(\(player.reasonForWaitingToPlay?.rawValue ?? "-"))"
      case .playing: "playing"
      @unknown default: "unknown"
      }
    out["itemStatus"] =
      switch item?.status {
      case .readyToPlay: "readyToPlay"
      case .failed: "failed"
      default: "unknown"
      }
    if let error = item?.error as NSError? {
      out["error"] = "\(error.domain):\(error.code) \(error.localizedDescription)"
    }
    if let item = item {
      out["currentTime"] = CMTimeGetSeconds(item.currentTime())
      out["tracks"] = item.tracks.compactMap { track -> String? in
        guard let assetTrack = track.assetTrack else { return nil }
        let formats = (assetTrack.formatDescriptions as! [CMFormatDescription]).map { desc in
          fourCC(CMFormatDescriptionGetMediaSubType(desc))
        }.joined(separator: "+")
        return "\(assetTrack.mediaType.rawValue):\(formats)"
      }.joined(separator: ", ")
    }
    if let loader = loader {
      let snapshot = loader.statusSnapshot()
      out["fedBytes"] = snapshot.bytesReceived
      out["loaderRequests"] = snapshot.requestLog
      out["loaderRetainedBytes"] = snapshot.retainedBytes
      out["loaderPendingRequests"] = snapshot.pendingRequestCount
      out["loaderMaximumBytes"] = snapshot.maximumBufferedBytes
      if let errorCode = snapshot.errorCode {
        out["loaderError"] = errorCode
      }
    }
    return out
  }

  private func fourCC(_ code: FourCharCode) -> String {
    let bytes = [
      UInt8((code >> 24) & 0xFF), UInt8((code >> 16) & 0xFF),
      UInt8((code >> 8) & 0xFF), UInt8(code & 0xFF),
    ]
    return String(bytes: bytes, encoding: .ascii) ?? String(code)
  }
}

/// Streams an HTTP source into memory and serves it to AVPlayer through an
/// AVAssetResourceLoader on a custom scheme, mirroring the mpv sink's model.
final class RawEc3Loader: NSObject, AVAssetResourceLoaderDelegate, URLSessionDataDelegate {
  static let defaultMaximumBufferedBytes = 64 * 1_024 * 1_024
  private static let maximumPendingRequests = 256

  struct StatusSnapshot {
    let bytesReceived: Int
    let requestLog: String
    let retainedBytes: Int
    let pendingRequestCount: Int
    let maximumBufferedBytes: Int
    let errorCode: String?
    let isFinished: Bool
  }

  private enum State {
    case active
    case finished
    case failed(code: String, error: Error)
    case cancelled

    var terminalError: Error? {
      switch self {
      case .failed(_, let error):
        return error
      case .cancelled:
        return URLError(.cancelled)
      case .active, .finished:
        return nil
      }
    }

    var errorCode: String? {
      if case .failed(let code, _) = self { return code }
      return nil
    }

    var isFinished: Bool {
      if case .finished = self { return true }
      return false
    }
  }

  let asset: AVURLAsset
  let maximumBufferedBytes: Int
  private let source: URL
  private let finiteLength: Bool
  private let sessionConfiguration: URLSessionConfiguration
  private let queue = DispatchQueue(label: "plezy.atmos.probe.loader")
  private var terminalHandlerForTesting: (() -> Void)?
  private let queueKey = DispatchSpecificKey<Void>()
  private var session: URLSession?
  private var buffer = Data()
  private var contentLength: Int64 = -1
  private var state: State = .active
  private var pending: [AVAssetResourceLoadingRequest] = []
  private var bytesReceived = 0
  private var requestLog = ""
  private var hasBegun = false

  init(
    source: URL,
    finiteLength: Bool,
    maximumBufferedBytes: Int = RawEc3Loader.defaultMaximumBufferedBytes,
    sessionConfiguration: URLSessionConfiguration = .default,
    terminalHandlerForTesting: (() -> Void)? = nil
  ) {
    precondition(maximumBufferedBytes > 0)
    self.source = source
    self.finiteLength = finiteLength
    self.maximumBufferedBytes = maximumBufferedBytes
    self.sessionConfiguration = sessionConfiguration
    self.terminalHandlerForTesting = terminalHandlerForTesting
    self.asset = AVURLAsset(url: URL(string: "plezy-ec3-probe://stream/audio.ec3")!)
    super.init()
    queue.setSpecific(key: queueKey, value: ())
    asset.resourceLoader.setDelegate(self, queue: queue)
  }

  func begin() {
    queue.async { [weak self] in
      guard let self, !self.hasBegun else { return }
      guard case .active = self.state else { return }
      self.hasBegun = true
      let session = URLSession(
        configuration: self.sessionConfiguration,
        delegate: self,
        delegateQueue: nil
      )
      self.session = session
      session.dataTask(with: self.source).resume()
    }
  }

  func cancel() {
    queue.async { [weak self] in
      self?.cancelOnQueue()
    }
  }

  /// Called only from the method-channel/main path, never from the loader queue.
  func statusSnapshot() -> StatusSnapshot {
    syncOnQueue {
      StatusSnapshot(
        bytesReceived: bytesReceived,
        requestLog: requestLog,
        retainedBytes: buffer.count,
        pendingRequestCount: pending.count,
        maximumBufferedBytes: maximumBufferedBytes,
        errorCode: state.errorCode,
        isFinished: state.isFinished
      )
    }
  }

  private func syncOnQueue<T>(_ body: () -> T) -> T {
    if DispatchQueue.getSpecific(key: queueKey) != nil {
      return body()
    }
    return queue.sync(execute: body)
  }

  private func notifyTerminalForTesting() {
    let handler = terminalHandlerForTesting
    terminalHandlerForTesting = nil
    handler?()
  }

  private func cancelOnQueue() {
    guard case .active = state else {
      if case .finished = state {
        state = .cancelled
        finishPending(with: URLError(.cancelled))
        releaseRetainedBytes()
      }
      return
    }
    state = .cancelled
    session?.invalidateAndCancel()
    session = nil
    finishPending(with: URLError(.cancelled))
    releaseRetainedBytes()
    notifyTerminalForTesting()
  }

  private func failOnQueue(code: String, error: Error) {
    guard case .active = state else { return }
    state = .failed(code: code, error: error)
    session?.invalidateAndCancel()
    session = nil
    finishPending(with: error)
    releaseRetainedBytes()
    notifyTerminalForTesting()
  }

  private func finishPending(with error: Error) {
    let requests = pending
    pending.removeAll(keepingCapacity: false)
    for request in requests where !request.isFinished {
      request.finishLoading(with: error)
    }
  }

  private func releaseRetainedBytes() {
    buffer.removeAll(keepingCapacity: false)
  }

  // MARK: URLSessionDataDelegate (background queue -> hop to `queue`)

  func urlSession(
    _ session: URLSession,
    dataTask: URLSessionDataTask,
    didReceive response: URLResponse,
    completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
  ) {
    queue.async { [weak self] in
      guard let self, case .active = self.state else { return }
      self.contentLength = response.expectedContentLength
      if response.expectedContentLength > Int64(self.maximumBufferedBytes) {
        self.failOnQueue(
          code: "response_too_large",
          error: URLError(.dataLengthExceedsMaximum)
        )
      } else {
        self.serve()
      }
    }
    completionHandler(.allow)
  }

  func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
    queue.async { [weak self] in
      guard let self, case .active = self.state else { return }
      guard data.count <= self.maximumBufferedBytes - self.buffer.count else {
        self.failOnQueue(
          code: "response_too_large",
          error: URLError(.dataLengthExceedsMaximum)
        )
        return
      }
      self.buffer.append(data)
      self.bytesReceived += data.count
      self.serve()
    }
  }

  func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    queue.async { [weak self] in
      guard let self, case .active = self.state else { return }
      self.session = nil
      if let error {
        self.state = .failed(code: "network_error", error: error)
        self.finishPending(with: error)
        self.releaseRetainedBytes()
      } else {
        self.state = .finished
        self.serve()
      }
      self.notifyTerminalForTesting()
      session.finishTasksAndInvalidate()
    }
  }

  // MARK: AVAssetResourceLoaderDelegate (on `queue`)

  func resourceLoader(
    _ resourceLoader: AVAssetResourceLoader,
    shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest
  ) -> Bool {
    if let terminalError = state.terminalError {
      loadingRequest.finishLoading(with: terminalError)
      return true
    }
    guard pending.count < Self.maximumPendingRequests else {
      loadingRequest.finishLoading(with: URLError(.resourceUnavailable))
      return true
    }
    if let dataRequest = loadingRequest.dataRequest {
      requestLog += "[\(dataRequest.requestedOffset)+\(dataRequest.requestedLength)]"
      if requestLog.count > 300 { requestLog = String(requestLog.suffix(300)) }
    }
    pending.append(loadingRequest)
    serve()
    return true
  }

  func resourceLoader(
    _ resourceLoader: AVAssetResourceLoader,
    didCancel loadingRequest: AVAssetResourceLoadingRequest
  ) {
    pending.removeAll { $0 === loadingRequest }
  }

  private func serve() {
    let isFinished: Bool
    switch state {
    case .active:
      isFinished = false
    case .finished:
      isFinished = true
    case .failed, .cancelled:
      return
    }

    // The logical 1-TiB length remains the raw probe contract. Retained bytes
    // are independently bounded by maximumBufferedBytes.
    let knownLength: Int64? =
      finiteLength
      ? (contentLength >= 0 ? contentLength : (isFinished ? Int64(buffer.count) : nil))
      : Int64(1) << 40

    var index = 0
    while index < pending.count {
      let request = pending[index]
      if let info = request.contentInformationRequest {
        guard let length = knownLength else {
          index += 1
          continue
        }
        info.contentType = "public.enhanced-ac3-audio"
        info.contentLength = length
        info.isByteRangeAccessSupported = true
        if request.dataRequest == nil {
          request.finishLoading()
          pending.remove(at: index)
          continue
        }
      }
      guard let dataRequest = request.dataRequest else {
        index += 1
        continue
      }

      let requestedOffset = dataRequest.requestedOffset
      let currentOffset = dataRequest.currentOffset
      let requestedLength = Int64(dataRequest.requestedLength)
      let (end, overflow) = requestedOffset.addingReportingOverflow(requestedLength)
      guard requestedOffset >= 0, currentOffset >= requestedOffset, requestedLength >= 0, !overflow else {
        request.finishLoading(with: URLError(.badServerResponse))
        pending.remove(at: index)
        continue
      }

      let bufferedCount = Int64(buffer.count)
      if currentOffset < bufferedCount {
        let chunkEnd = min(bufferedCount, end)
        guard currentOffset <= chunkEnd,
          let start = Int(exactly: currentOffset),
          let finish = Int(exactly: chunkEnd)
        else {
          request.finishLoading(with: URLError(.badServerResponse))
          pending.remove(at: index)
          continue
        }
        dataRequest.respond(with: buffer.subdata(in: start..<finish))
      }
      if dataRequest.currentOffset >= end
        || (isFinished && dataRequest.currentOffset >= bufferedCount)
      {
        request.finishLoading()
        pending.remove(at: index)
        continue
      }
      index += 1
    }
  }
}

/// Feeds compressed E-AC-3 access units to a bare `AVSampleBufferAudioRenderer`
/// using Apple's flexible enhanced buffering sequence.
///
/// This is deliberately the shortest possible path from file bytes to the
/// system decoder: `AVAssetReader` at `outputSettings: nil` yields the native
/// compressed sample buffers and the native `CMFormatDescription` that
/// AVFoundation itself would use. Nothing else touches the data, so a failure
/// here is a property of the renderer, not of our packetization.
final class AsbarProbe: NSObject {
  private static var statusContext = 0

  private let source: URL
  private let regenerateFormatDescription: Bool
  private let sessionMode: AVAudioSession.Mode
  private let queue = DispatchQueue(
    label: "plezy.atmos.probe.asbar", qos: .userInitiated)
  private let lock = NSLock()

  private var renderer: AVSampleBufferAudioRenderer?
  private var synchronizer: AVSampleBufferRenderSynchronizer?
  private var reader: AVAssetReader?
  private var output: AVAssetReaderTrackOutput?
  private var temporaryFile: URL?
  private var download: URLSessionDownloadTask?
  private var statusObserved = false
  private var cancelled = false

  // Guarded by `lock`; read from the method-channel thread.
  private var enqueuedSamples = 0
  private var nativeDescription = "unknown"
  private var usedDescription = "unknown"
  private var magicCookieHex = "none"
  private var layoutTagText = "none"
  private var phase = "starting"
  private var failure: String?

  init(
    source: URL, regenerateFormatDescription: Bool, sessionMode: AVAudioSession.Mode
  ) {
    self.source = source
    self.regenerateFormatDescription = regenerateFormatDescription
    self.sessionMode = sessionMode
    super.init()
  }

  /// `completion` reports only whether the run could be started.
  func start(completion: @escaping (String?) -> Void) {
    if source.isFileURL {
      queue.async { [weak self] in
        guard let self else { return }
        let error = self.beginReading(from: self.source)
        DispatchQueue.main.async { completion(error) }
      }
      return
    }

    // AVAssetReader needs a seekable local asset; stage the source first.
    setPhase("downloading")
    let task = URLSession.shared.downloadTask(with: source) { [weak self] url, _, error in
      guard let self else { return }
      guard let url else {
        let message = error?.localizedDescription ?? "download failed"
        self.fail(message)
        DispatchQueue.main.async { completion(message) }
        return
      }
      // The temporary file is removed as soon as this handler returns.
      let staged = FileManager.default.temporaryDirectory
        .appendingPathComponent("plezy-asbar-\(UUID().uuidString)")
        .appendingPathExtension(self.source.pathExtension.isEmpty ? "eac3" : self.source.pathExtension)
      do {
        try FileManager.default.moveItem(at: url, to: staged)
      } catch {
        let message = "failed to stage asset: \(error.localizedDescription)"
        self.fail(message)
        DispatchQueue.main.async { completion(message) }
        return
      }
      self.lock.lock()
      self.temporaryFile = staged
      let aborted = self.cancelled
      self.lock.unlock()
      if aborted {
        try? FileManager.default.removeItem(at: staged)
        return
      }
      self.queue.async {
        let failureMessage = self.beginReading(from: staged)
        DispatchQueue.main.async { completion(failureMessage) }
      }
    }
    lock.lock()
    download = task
    lock.unlock()
    task.resume()
  }

  func cancel() {
    lock.lock()
    cancelled = true
    let task = download
    let staged = temporaryFile
    download = nil
    temporaryFile = nil
    lock.unlock()
    task?.cancel()

    queue.sync {
      if statusObserved, let renderer {
        renderer.removeObserver(
          self, forKeyPath: "status", context: &AsbarProbe.statusContext)
        statusObserved = false
      }
      renderer?.stopRequestingMediaData()
      renderer?.flush()
      synchronizer?.rate = 0
      if let renderer, let synchronizer {
        synchronizer.removeRenderer(renderer, at: .zero, completionHandler: nil)
      }
      reader?.cancelReading()
      reader = nil
      output = nil
      renderer = nil
      synchronizer = nil
    }
    if let staged { try? FileManager.default.removeItem(at: staged) }
    setPhase("cancelled")
  }

  /// Runs on `queue`. Returns a message on failure.
  private func beginReading(from url: URL) -> String? {
    lock.lock()
    let aborted = cancelled
    lock.unlock()
    if aborted { return nil }

    let asset = AVURLAsset(url: url)
    guard let track = asset.tracks(withMediaType: .audio).first else {
      let message = "no audio track in \(url.lastPathComponent)"
      fail(message)
      return message
    }
    guard
      let nativeFormat = (track.formatDescriptions as? [CMFormatDescription])?.first
    else {
      let message = "audio track has no format description"
      fail(message)
      return message
    }
    describe(nativeFormat)

    let assetReader: AVAssetReader
    do {
      assetReader = try AVAssetReader(asset: asset)
    } catch {
      let message = "AVAssetReader init failed: \(error.localizedDescription)"
      fail(message)
      return message
    }
    // `nil` output settings is what keeps the samples compressed.
    let trackOutput = AVAssetReaderTrackOutput(track: track, outputSettings: nil)
    trackOutput.alwaysCopiesSampleData = false
    guard assetReader.canAdd(trackOutput) else {
      let message = "AVAssetReader rejected a compressed output"
      fail(message)
      return message
    }
    assetReader.add(trackOutput)
    guard assetReader.startReading() else {
      let message =
        "AVAssetReader.startReading failed: \(assetReader.error?.localizedDescription ?? "unknown")"
      fail(message)
      return message
    }

    var substituteFormat: CMFormatDescription?
    if regenerateFormatDescription {
      let rebuilt = Self.regenerate(from: nativeFormat)
      guard let format = rebuilt.format else {
        let message = rebuilt.error ?? "format regeneration failed"
        fail(message)
        return message
      }
      substituteFormat = format
      describeUsed(format)
    } else {
      describeUsed(nativeFormat)
    }

    if let message = configureSession() { return message }

    let renderer = AVSampleBufferAudioRenderer()
    let synchronizer = AVSampleBufferRenderSynchronizer()
    synchronizer.addRenderer(renderer)
    // Deliberately NOT disabling delaysRateChangeUntilHasSufficientMediaData:
    // the reliable-start preroll is part of the sequence under test.
    renderer.addObserver(
      self, forKeyPath: "status", options: [.new], context: &AsbarProbe.statusContext)

    self.renderer = renderer
    self.synchronizer = synchronizer
    self.reader = assetReader
    self.output = trackOutput
    statusObserved = true

    setPhase("feeding")
    // Apple's order: request media first, start the clock after.
    renderer.requestMediaDataWhenReady(on: queue) { [weak self] in
      self?.pump(substituteFormat: substituteFormat)
    }
    synchronizer.rate = 1
    return nil
  }

  /// Runs on `queue`.
  private func pump(substituteFormat: CMFormatDescription?) {
    guard let renderer, let output, let reader else { return }
    while renderer.isReadyForMoreMediaData {
      guard reader.status == .reading, let sample = output.copyNextSampleBuffer() else {
        renderer.stopRequestingMediaData()
        setPhase(reader.status == .completed ? "finished" : "stopped(\(reader.status.rawValue))")
        return
      }
      let enqueued: CMSampleBuffer
      if let substituteFormat {
        guard let rewrapped = Self.rewrap(sample, with: substituteFormat) else {
          fail("failed to rewrap a sample with the generated description")
          renderer.stopRequestingMediaData()
          return
        }
        enqueued = rewrapped
      } else {
        enqueued = sample
      }
      renderer.enqueue(enqueued)
      lock.lock()
      enqueuedSamples += 1
      lock.unlock()
    }
  }

  private func configureSession() -> String? {
    #if os(iOS) || os(tvOS)
      let session = AVAudioSession.sharedInstance()
      do {
        try session.setCategory(
          .playback, mode: sessionMode, policy: .longFormAudio, options: [])
      } catch {
        let message = "long-form session profile rejected: \(error.localizedDescription)"
        fail(message)
        return message
      }
      if #available(iOS 15.0, tvOS 15.0, *) {
        try? session.setSupportsMultichannelContent(true)
      }
      do {
        try session.setActive(true)
      } catch {
        let message = "session activation failed: \(error.localizedDescription)"
        fail(message)
        return message
      }
    #endif
    return nil
  }

  override func observeValue(
    forKeyPath keyPath: String?, of object: Any?,
    change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?
  ) {
    guard context == &AsbarProbe.statusContext else {
      super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
      return
    }
    guard let renderer = object as? AVSampleBufferAudioRenderer,
      renderer.status == .failed
    else { return }
    let error = renderer.error as NSError?
    fail(
      "renderer failed: \(error?.domain ?? "-"):\(error?.code ?? 0) "
        + (error?.localizedDescription ?? "unknown"))
  }

  func statusSnapshot() -> [String: Any] {
    lock.lock()
    defer { lock.unlock() }
    var out: [String: Any] = [
      "state": phase,
      "asbarEnqueuedSamples": enqueuedSamples,
      "asbarNativeFormat": nativeDescription,
      "asbarUsedFormat": usedDescription,
      "asbarMagicCookie": magicCookieHex,
      "asbarChannelLayout": layoutTagText,
    ]
    if let failure { out["error"] = failure }
    if let renderer {
      out["asbarRendererStatus"] =
        switch renderer.status {
        case .unknown: "unknown"
        case .rendering: "rendering"
        case .failed: "failed"
        @unknown default: "unrecognized"
        }
    }
    if let synchronizer {
      out["currentTime"] = CMTimeGetSeconds(synchronizer.currentTime())
    }
    return out
  }

  // MARK: - Format description helpers

  private func describe(_ format: CMFormatDescription) {
    let text = Self.summarize(format)
    lock.lock()
    nativeDescription = text.summary
    magicCookieHex = text.cookie
    layoutTagText = text.layout
    lock.unlock()
  }

  private func describeUsed(_ format: CMFormatDescription) {
    let text = Self.summarize(format)
    lock.lock()
    usedDescription = text.summary
    lock.unlock()
  }

  private static func summarize(
    _ format: CMFormatDescription
  ) -> (summary: String, cookie: String, layout: String) {
    var summary = "subType=\(fourCCText(CMFormatDescriptionGetMediaSubType(format)))"
    if let asbd = CMAudioFormatDescriptionGetStreamBasicDescription(format)?.pointee {
      summary +=
        String(
          format: " rate=%.0f ch=%u framesPerPacket=%u", asbd.mSampleRate,
          asbd.mChannelsPerFrame, asbd.mFramesPerPacket)
    }
    var cookieSize = 0
    var cookie = "none"
    if let bytes = CMAudioFormatDescriptionGetMagicCookie(format, sizeOut: &cookieSize),
      cookieSize > 0
    {
      cookie = Data(bytes: bytes, count: cookieSize).map { String(format: "%02x", $0) }
        .joined()
    }
    var layoutSize = 0
    var layout = "none"
    if let acl = CMAudioFormatDescriptionGetChannelLayout(format, sizeOut: &layoutSize) {
      layout = String(format: "0x%08x", acl.pointee.mChannelLayoutTag)
    }
    return (summary, cookie, layout)
  }

  private static func fourCCText(_ code: FourCharCode) -> String {
    let bytes = [
      UInt8((code >> 24) & 0xFF), UInt8((code >> 16) & 0xFF),
      UInt8((code >> 8) & 0xFF), UInt8(code & 0xFF),
    ]
    return String(bytes: bytes, encoding: .ascii) ?? String(code)
  }

  /// Rebuild the description the way the mpv AO does: same ASBD and magic
  /// cookie, but constructed by us rather than parsed out of the container,
  /// with the Dolby-order 5.1 layout attached.
  private static func regenerate(
    from native: CMFormatDescription
  ) -> (format: CMFormatDescription?, error: String?) {
    guard var asbd = CMAudioFormatDescriptionGetStreamBasicDescription(native)?.pointee else {
      return (nil, "native description has no ASBD")
    }
    var cookieSize = 0
    var cookieBytes: [UInt8] = []
    if let bytes = CMAudioFormatDescriptionGetMagicCookie(native, sizeOut: &cookieSize),
      cookieSize > 0
    {
      cookieBytes = Array(UnsafeRawBufferPointer(start: bytes, count: cookieSize))
    }
    var layout = AudioChannelLayout()
    layout.mChannelLayoutTag =
      asbd.mChannelsPerFrame == 8
      ? kAudioChannelLayoutTag_MPEG_7_1_C : kAudioChannelLayoutTag_MPEG_5_1_C

    var rebuilt: CMFormatDescription?
    let status = cookieBytes.withUnsafeBytes { cookie -> OSStatus in
      CMAudioFormatDescriptionCreate(
        allocator: kCFAllocatorDefault,
        asbd: &asbd,
        layoutSize: MemoryLayout<AudioChannelLayout>.size,
        layout: &layout,
        magicCookieSize: cookie.count,
        magicCookie: cookie.baseAddress,
        extensions: nil,
        formatDescriptionOut: &rebuilt)
    }
    guard status == noErr, let rebuilt else {
      return (nil, "CMAudioFormatDescriptionCreate failed (\(status))")
    }
    return (rebuilt, nil)
  }

  private static func rewrap(
    _ sample: CMSampleBuffer, with format: CMFormatDescription
  ) -> CMSampleBuffer? {
    guard let blockBuffer = CMSampleBufferGetDataBuffer(sample) else { return nil }
    var timing = CMSampleTimingInfo()
    guard CMSampleBufferGetSampleTimingInfo(sample, at: 0, timingInfoOut: &timing) == noErr
    else { return nil }
    var sizeOut = 0
    let sizeStatus = CMSampleBufferGetSampleSizeArray(
      sample, entryCount: 0, arrayToFill: nil, entriesNeededOut: &sizeOut)
    var sizes = [Int](repeating: 0, count: max(sizeOut, 1))
    if sizeStatus == noErr, sizeOut > 0 {
      _ = CMSampleBufferGetSampleSizeArray(
        sample, entryCount: sizeOut, arrayToFill: &sizes, entriesNeededOut: nil)
    } else {
      sizes[0] = CMBlockBufferGetDataLength(blockBuffer)
    }
    var rewrapped: CMSampleBuffer?
    let status = CMSampleBufferCreateReady(
      allocator: kCFAllocatorDefault,
      dataBuffer: blockBuffer,
      formatDescription: format,
      sampleCount: CMSampleBufferGetNumSamples(sample),
      sampleTimingEntryCount: 1,
      sampleTimingArray: &timing,
      sampleSizeEntryCount: sizes.count,
      sampleSizeArray: &sizes,
      sampleBufferOut: &rewrapped)
    return status == noErr ? rewrapped : nil
  }

  // MARK: - State

  private func setPhase(_ value: String) {
    lock.lock()
    phase = value
    lock.unlock()
  }

  private func fail(_ message: String) {
    lock.lock()
    if failure == nil { failure = message }
    phase = "failed"
    lock.unlock()
  }
}
