//
//  VoicePlayer.swift
//  ylgamehall
//
//  远端语音播放器：AVAudioPlayer 包装 + 反向 callback `gameui_stop_voice` 触发点。
//
//  契约：docs/H5-Native-Contract.md §3.1 ［5］mediaTypeAudio + §3.2 ［7］［8］
//  gameui_play_voice / gameui_stop_voice
//
//  ## 与原项目的行为差异（用户反馈驱动的修正）
//
//  daoqi msext NewRootVC.m / gameController.m 把 voicePlayer / User_id 作为
//  VC 成员变量，覆盖式播放时直接 `voicePlayer = [[AVAudioPlayer alloc]...]`
//  替换 ivar，没有触发任何停旧 player 的回调；自然结束时再用最新的
//  `self.User_id`（已被新一条消息覆盖）调 stopradio → 发给 H5 的
//  `gameui_stop_voice` payload 实际是**新一条消息的 user_id**，导致旧消息
//  对应的 H5 语音动画永远停不掉。
//
//  本类把 user 与 player 一一绑定（`currentUser`）：
//  - 覆盖式 play：在替换 player 之前主动 stop 旧 player 并触发
//    `onFinish(currentUser)`，让 H5 收到正确的 `gameui_stop_voice(被打断 user)`
//  - 自然结束 / 解码失败：用 `currentUser` 而非任何后写入的"最近 user"
//    触发 onFinish，保证停的是真正结束那条
//  - 主动 stop()：不触发 onFinish（保留 daoqi 语义；本项目目前也没有外部
//    主动 stop 调用路径）
//

import AVFoundation
import os.log

public final class VoicePlayer: NSObject, AVAudioPlayerDelegate {

    public static let shared = VoicePlayer()

    private static let log = Logger(subsystem: "ylgamehall", category: "VoicePlayer")

    private var player: AVAudioPlayer?

    /// 当前正在播放的语音对应的 user。与 `player` 一一绑定：`player != nil` 时一定有值，
    /// `player == nil` 时为 nil。覆盖式 play / didFinishPlaying / decodeError 都用这个
    /// 值触发 onFinish，确保 `gameui_stop_voice` payload 与真正结束 / 被中断的语音对应。
    private var currentUser: String?

    /// 播放完成 / 被中断 / 解码失败统一回调。
    /// mediaTypeAudio handler 注册此 closure → 反向调 H5 `gameui_stop_voice`。
    /// payload 始终是与刚结束 / 被打断 player 绑定的 user（不是入参中最新的 user）。
    public var onFinish: ((String) -> Void)?

    private override init() { super.init() }

    /// 播放 wav。若当前已有正在播放的 player，先 stop 旧 player 并触发
    /// `onFinish(被打断的 user)`，让 H5 把旧动画停掉；再启动新 player。
    /// - Throws: AVAudioPlayer 创建 / 启动失败
    public func play(_ wav: URL, user: String) throws {
        // 1. 若已有 player 正在播放，主动通知"被中断"，确保 H5 把旧动画停掉。
        //    delegate 先置 nil 防止旧 player 的 stop / dealloc 后续异步触发
        //    didFinishPlaying 又再发一次 onFinish。
        if let old = player {
            old.delegate = nil
            old.stop()
            player = nil
            if let interrupted = currentUser {
                currentUser = nil
                Self.log.debug("interrupted user=\(interrupted, privacy: .public)")
                onFinish?(interrupted)
            } else {
                currentUser = nil
            }
        }

        // 2. 创建并启动新 player
        let p = try AVAudioPlayer(contentsOf: wav)
        p.delegate = self
        guard p.prepareToPlay(), p.play() else {
            throw PlayerError.startFailed
        }
        player = p
        currentUser = user
        Self.log.debug("play \(wav.lastPathComponent, privacy: .public) user=\(user, privacy: .public)")
    }

    /// 主动停止当前播放（不触发 onFinish）。
    /// 外部清理使用（如 H5 BackGameData 退出子游戏时）；
    /// 业务正常路径下不会主动调，由 didFinishPlaying 自然完成。
    public func stop() {
        player?.delegate = nil
        player?.stop()
        player = nil
        currentUser = nil
    }

    public enum PlayerError: Error, Sendable {
        case startFailed
    }

    // MARK: - AVAudioPlayerDelegate
    //
    // AVAudioPlayerDelegate 方法签名是 nonisolated（AVFoundation API 约束）。
    // 内部访问 @MainActor 隔离的状态需要跳转回主线程。

    nonisolated public func audioPlayerDidFinishPlaying(
        _ player: AVAudioPlayer,
        successfully flag: Bool
    ) {
        // AVAudioPlayer 是 non-Sendable，不能直接跨 actor 边界。用 ObjectIdentifier
        // （Sendable）传递身份，handleFinish 内重新查 self.player 比对。
        let id = ObjectIdentifier(player)
        Task { @MainActor in
            self.handleFinish(playerIdentity: id, success: flag)
        }
    }

    nonisolated public func audioPlayerDecodeErrorDidOccur(
        _ player: AVAudioPlayer,
        error: (any Error)?
    ) {
        let id = ObjectIdentifier(player)
        Task { @MainActor in
            self.handleFinish(playerIdentity: id, success: false)
        }
    }

    /// 处理 didFinishPlaying / decodeError。
    /// 防御覆盖式 play 时旧 player stop 触发延迟 delegate：仅当 didFinish
    /// 的 player 与当前 self.player 同身份时才清理 + 触发 onFinish。
    private func handleFinish(playerIdentity: ObjectIdentifier, success: Bool) {
        guard let current = player, ObjectIdentifier(current) == playerIdentity else { return }
        let user = currentUser
        player = nil
        currentUser = nil
        Self.log.debug("didFinish user=\(user ?? "<nil>", privacy: .public) success=\(success, privacy: .public)")
        if let user { onFinish?(user) }
    }
}
