//
//  LNIMManager.swift
//  Lanu
//
//  Created by OneeChan on 2025/11/12.
//

import Foundation
import RTCRoomEngine
import AVFAudio
import AtomicXCore


protocol LNIMManagerNotify {
    func onConversationListChanged()
    func onIMUserStatusChanged(uid: String, status: V2TIMUserStatusType)
    
    func onVoiceCallBegin()
    func onVoiceCallEnd()
    func onVoiceCallInfoChanged()
}
extension LNIMManagerNotify {
    func onConversationListChanged() {}
    func onIMUserStatusChanged(uid: String, status: V2TIMUserStatusType) {}
    
    func onVoiceCallBegin() { }
    func onVoiceCallEnd() { }
    func onVoiceCallInfoChanged() { }
}

extension String {
    var isImOfficialId: Bool {
        guard let intValue = Int(self) else { return false }
        return intValue <= LNIMManager.maxOfficialId
    }
}

enum LNIMCustomErrorCode: Int {
    case inBlackList = 120001
    case userNotExist = 120002
}

enum LNIMOfficialIds: String {
    case officialMessage = "9998"
}

enum LNIMVoiceCallSpeakerType {
    case speakerphone
    case earpiece
    case bluetooth
    
    var toTUIAudioPlaybackDevice: TUIAudioPlaybackDevice? {
        switch self {
        case .speakerphone: .speakerphone
        case .earpiece: .earpiece
        case .bluetooth: nil
        }
    }
}

class LNIMVoiceCallInfo {
    let uid: String
    var isInCome = false
    var beginTime: TimeInterval = 0
    var isMute = false
    var deviceType: LNIMVoiceCallSpeakerType = .earpiece
    
    init(uid: String) {
        self.uid = uid
    }
}

class LNIMManager: NSObject {
    private static var appId: Int32 {
        if LNAppConfig.shared.curEnv == .test {
            20034873
        } else {
            80000456
        }
    }
    var offlinePushAppId: Int32 {
        if LNAppConfig.shared.curEnv == .test {
//            15846 // 本地调试使用这个
            15847 // 打包 ipa 只能用这个
        } else {
            15845
        }
    }
    
    static var shared = LNIMManager()
    
    static let maxOfficialId = 10000
    static let maxMessageInput = 500
    static let maxRemarkLength = 16
    
    private(set) var conversationList: [V2TIMConversation] = []
    private(set) var userStatus: [String: V2TIMUserStatusType] = [:]
    
    private(set) var voiceCallAvailable = false
    
    private(set) var curCallInfo: LNIMVoiceCallInfo?
    private let bellPlayer = LNIMAudioCallBellPlayer()
    
    private override init() {
        super.init()
        _ = LNIMEmojiManager.shared
        LNEventDeliver.addObserver(self)
        V2TIMManager.sharedInstance().addConversationListener(listener: self)
        V2TIMManager.sharedInstance().addIMSDKListener(listener: self)
    }
}

// MARK: 会话列表
extension LNIMManager {
    func reloadConversationList(handler: ((Bool) -> Void)? = nil) {
        let filter = V2TIMConversationListFilter()
        filter.type = .C2C
        V2TIMManager.sharedInstance().getConversationListByFilter(
            filter: filter,
            nextSeq: 0, count: 1000)
        { [weak self] list, nextTag, isFinish in
            guard let self else { return }
            guard var list else {
                handler?(false)
                return
            }
            // 按照时间排序
            list.sort { $0.userID?.isImOfficialId == true || $0.orderKey > $1.orderKey }
            
            if list.firstIndex(where: { $0.userID?.isImOfficialId == true }) == nil {
                // 插入官方消息
                let officialId = "c2c_" + LNIMOfficialIds.officialMessage.rawValue
                saveDraftFor(officialId, draft: " ")
                saveDraftFor(officialId, draft: nil)
            }
            
            for item in list {
                item.extraInfo = conversationList.first(where: { $0.conversationID == item.conversationID })?.extraInfo ?? LNIMConversationExtraInfo()
            }
            
            conversationList = list
            handler?(true)
            notifyConversationListChanged()
            
            V2TIMManager.sharedInstance().subscribeUserStatus(userIDList: list.compactMap({ $0.userID }), succ: nil)
            loadUsersOnlineStatus()
            loadUsersRemark()
        } fail: { code, err in
            handler?(false)
        }
    }
    
    func cleanConversationUnread(uid: String) {
        let conversationId = "c2c_" + uid
        V2TIMManager.sharedInstance().cleanConversationUnreadMessageCount(
            conversationID: conversationId,
            cleanTimestamp: 0, cleanSequence: 0, succ: nil)
    }
    
    func saveDraftFor(_ conversationId: String?, draft: String?) {
        guard let conversationId else { return }
        V2TIMManager.sharedInstance().setConversationDraft(conversationID: conversationId, draftText: draft, succ: nil)
    }
}

// MARK: 会话变化回调 V2TIMConversationListener
extension LNIMManager: V2TIMConversationListener {
    func onNewConversation(conversationList: [V2TIMConversation]!) {
        reloadConversationList()
        loadUsersOnlineStatus()
    }
    
    func onConversationChanged(conversationList: [V2TIMConversation]!) {
        var isChanged = false
        var isOrderMessage = false
        for item in conversationList {
            if let old = self.conversationList.first(where: { $0.conversationID == item.conversationID }),
               old.unreadCount != item.unreadCount
                || old.lastDisplayDate != item.lastDisplayDate
                || old.lastMessage?.msgID != item.lastMessage?.msgID {
                isChanged = true
            }
            if let message = item.lastMessage,
                case .order = LNIMMessageData(imMessage: message).type {
                isOrderMessage = true
            }
        }
        if isChanged {
            reloadConversationList()
        }
        if isOrderMessage {
            LNOrderManager.shared.reloadMyOrderDiscountInfo()
        }
    }
    
    func onConversationDeleted(conversationIDList: [String]!) {
        reloadConversationList()
        loadUsersOnlineStatus()
    }
}

// MARK: 用户在线状态
extension LNIMManager {
    func isUserOnline(uid: String) -> Bool {
        userStatus[uid] == .USER_STATUS_ONLINE
    }
    
    func getUserOnlineState(uid: String, handler: @escaping (Bool) -> Void) {
        V2TIMManager.sharedInstance().getUserStatus(userIDList: [uid])
        { [weak self] userStatusList in
            guard let self else { return }
            userStatusList?.forEach {
                if let uid = $0.userID {
                    self.userStatus[uid] = $0.statusType
                    self.notifyUserStatusChanged(uid: uid, status: $0.statusType)
                }
            }
            handler(userStatusList?.first(where: { $0.userID == uid })?.statusType == .USER_STATUS_ONLINE)
        } fail: { _, _ in }
    }
    
    private func loadUsersOnlineStatus() {
        let uids = conversationList.compactMap { $0.userID }
        guard !uids.isEmpty else { return }
        V2TIMManager.sharedInstance().getUserStatus(userIDList: uids)
        { [weak self] userStatusList in
            guard let self else { return }
            userStatusList?.forEach {
                if let uid = $0.userID {
                    self.userStatus[uid] = $0.statusType
                    self.notifyUserStatusChanged(uid: uid, status: $0.statusType)
                }
            }
        } fail: { _, _ in }
    }
    
    private func loadUsersRemark() {
        let newItems = conversationList.filter({ $0.extraInfo?.remark == nil })
        guard !newItems.isEmpty else { return }
        
        getUsersRemark(uids: newItems.compactMap({ $0.userID })) { [weak self] remarks in
            guard let remarks else { return }
            guard let self else { return }
            for item in newItems {
                item.extraInfo?.remark = remarks.first(where: { $0.userNo == item.userID })?.note ?? ""
            }
            notifyConversationListChanged()
        }
    }
}

// MARK: 用户在线状态变化回调 V2TIMSDKListener
extension LNIMManager: V2TIMSDKListener {
    func onUserStatusChanged(userStatusList: [V2TIMUserStatus]!) {
        userStatusList.forEach {
            if let uid = $0.userID {
                userStatus[uid] = $0.statusType
                notifyUserStatusChanged(uid: uid, status: $0.statusType)
                
                // 如果我是被呼叫者，对方下线，不会触发回调，只能在这里进行挂断处理
                if uid == curCallInfo?.uid,
                   curCallInfo?.isInCome == true,
                   $0.statusType == .USER_STATUS_OFFLINE {
                    hangupVoiceCall()
                }
            }
        }
    }
}

// MARK: 语音通话
extension LNIMManager {
    private func checkIfCanCall(uid: String, queue: DispatchQueue = .main, handler: @escaping (Bool) -> Void) {
        LNHttpManager.shared.checkIfCanCall(uid: uid) { err in
            queue.asyncIfNotGlobal {
                handler(err == nil)
            }
            if let err {
                showToast(err.errorDesc)
            }
        }
    }
    
    func makeVoiceCall(uid: String) {
        guard !uid.isMyUid else {
            showToast(.init(key: "C00014"))
            return
        }
        
        guard LNRoomManager.shared.curRoom == nil else {
            showToast(.init(key: "A00387"))
            return
        }
        
        LNPermissionHelper.requestMicrophoneAccess { [weak self] granted in
            guard let self else { return }
            guard granted else {
                showToast(.init(key: "B00022"))
                return
            }
            guard curCallInfo == nil else { return }
            
            curCallInfo = .init(uid: uid)
            curCallInfo?.isInCome = false
            
            checkIfCanCall(uid: uid) { [weak self] can in
                guard let self else { return }
                guard can else {
                    curCallInfo = nil
                    return
                }
                let floatingView = LNAudioCallFloatingView()
                floatingView.show()
                
                let panel = LNAudioCallPanel()
                panel.toCallOut(uid: uid)
                panel.popup()
                
                let offlinePushInfo = createOfflinePushInfo()
                let param = TUICallParams()
                param.offlinePushInfo = offlinePushInfo
                TUICallEngine.createInstance().calls(userIdList: [uid], callMediaType: .audio, params: param) { [weak self] in
                    LNStatisticManager.shared.reportStartCall(uid: uid, success: true)
                    
                    guard let self else { return }
                    bellPlayer.startPlay(isInCome: false)
                } fail: { [weak panel, weak floatingView] _, err in
                    LNStatisticManager.shared.reportStartCall(uid: uid, success: false)
                    
                    showToast(err)
                    if let panel {
                        panel.dismiss()
                    }
                    if let floatingView {
                        floatingView.dismiss()
                    }
                }
            }
        }
    }
    
    func rejectVoiceCall() {
        TUICallEngine.createInstance().reject { }
        fail: { _, err in
            showToast(err)
        }
    }
    
    func acceptVoiceCall() {
        TUICallEngine.createInstance().accept { [weak self] in
            if let self, let curCallInfo {
                LNStatisticManager.shared.reportAcceptCall(uid: curCallInfo.uid, success: true)
            }
        }
        fail: { [weak self] _, err in
            if let self, let curCallInfo {
                LNStatisticManager.shared.reportAcceptCall(uid: curCallInfo.uid, success: false)
            }
            showToast(err)
        }
    }
    
    func hangupVoiceCall() {
        TUICallEngine.createInstance().hangup { }
        fail: { _, err in
            showToast(err)
        }
    }
    
    func switchVoiceCallMicrophone() {
        if curCallInfo?.isMute == true {
            TUICallEngine.createInstance().openMicrophone { [weak self] in
                guard let self else { return }
                curCallInfo?.isMute = false
                LNEventDeliver.notifyEvent {
                    ($0 as? LNIMManagerNotify)?.onVoiceCallInfoChanged()
                }
            }
            fail: { _, err in
                showToast(err)
            }
        } else {
            TUICallEngine.createInstance().closeMicrophone()
            curCallInfo?.isMute = true
            LNEventDeliver.notifyEvent {
                ($0 as? LNIMManagerNotify)?.onVoiceCallInfoChanged()
            }
        }
    }
    
    func switchVoiceCallSpeakerType(type: LNIMVoiceCallSpeakerType) {
        if let deviceType = type.toTUIAudioPlaybackDevice {
            TUICallEngine.createInstance().selectAudioPlaybackDevice(deviceType)
            curCallInfo?.deviceType = type
        } else {
            let session = AVAudioSession.sharedInstance()
            do {
                // 设置为播放类别，允许蓝牙音频路由
                try session.setCategory(.playback, mode: .default, options: [
                    .allowBluetoothA2DP,
                    .allowBluetoothHFP
                ])
                try session.setActive(true) // 激活音频会话
                curCallInfo?.deviceType = type
            } catch {
                print("音频会话配置失败：\(error.localizedDescription)")
            }
        }
        LNEventDeliver.notifyEvent {
            ($0 as? LNIMManagerNotify)?.onVoiceCallInfoChanged()
        }
    }
    
    private func createOfflinePushInfo() -> TUIOfflinePushInfo {
        let pushInfo: TUIOfflinePushInfo = TUIOfflinePushInfo()
        pushInfo.title = myUserInfo.nickname
        pushInfo.desc = .init(key: "C00013")
        // iOS push type: if you want user VoIP, please modify type to TUICallIOSOfflinePushTypeVoIP
        pushInfo.iOSPushType = .voIP
        pushInfo.ignoreIOSBadge = false
        pushInfo.iOSSound = "phone_bell.mp3"
        pushInfo.androidSound = "phone_ringing"
        // VIVO message type: 0-push message, 1-System message(have a higher delivery rate)
        pushInfo.androidVIVOClassification = 1
        // HuaWei message type: https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/message-classification-0000001149358835
        pushInfo.androidHuaWeiCategory = "IM"
        
        return pushInfo
    }
}

// MARK: 语音通话变化回调 TUICallObserver
extension LNIMManager: TUICallObserver {
    func onCallReceived(callerId: String, calleeIdList: [String],
                        groupId: String?, callMediaType: TUICallMediaType,
                        userData: String?)
    {
        guard curCallInfo == nil else {
            return
        }
        guard LNRoomManager.shared.curRoom == nil else {
            // 在直播间，直接拒绝
            showToast(.init(key: "A00388"))
            rejectVoiceCall()
            return
        }
        
        curCallInfo = .init(uid: callerId)
        curCallInfo?.isInCome = true
        
        bellPlayer.startPlay(isInCome: true)
        
        let floatingView = LNAudioCallFloatingView()
        floatingView.show()
        
        let panel = LNAudioCallPanel()
        panel.onCallIn(uid: callerId)
        panel.popup()
    }
    
    func onCallCancelled(callerId: String) {
        bellPlayer.stop()
        curCallInfo = nil
        LNEventDeliver.notifyEvent { ($0 as? LNIMManagerNotify)?.onVoiceCallEnd() }
    }
    
    func onCallBegin(roomId: TUIRoomId, callMediaType: TUICallMediaType, callRole: TUICallRole) {
        curCallInfo?.beginTime = curTime
        LNEventDeliver.notifyEvent { ($0 as? LNIMManagerNotify)?.onVoiceCallBegin() }
        TUICallEngine.createInstance().selectAudioPlaybackDevice(.earpiece)
        TUICallEngine.createInstance().openMicrophone { } fail: { _, _ in }
        bellPlayer.stop()
    }
    
    func onCallEnd(roomId: TUIRoomId, callMediaType: TUICallMediaType, callRole: TUICallRole, totalTime: Float) {
        if let curCallInfo {
            LNStatisticManager.shared.reportEndCall(uid: curCallInfo.uid, duration: totalTime)
        }
        curCallInfo = nil
        LNEventDeliver.notifyEvent { ($0 as? LNIMManagerNotify)?.onVoiceCallEnd() }
    }
    
    func onUserReject(userId: String) { }
    
    func onUserNoResponse(userId: String) { }
    
    func onUserLineBusy(userId: String) {
        curCallInfo = nil
        LNEventDeliver.notifyEvent { ($0 as? LNIMManagerNotify)?.onVoiceCallEnd() }
        bellPlayer.stop()
    }
}

// MARK: IM 备注
extension LNIMManager {
    func setUserRemark(uid: String, remark: String, queue: DispatchQueue = .main, handler: @escaping (Bool) -> Void) {
        LNHttpManager.shared.setUserRemark(uid: uid, remark: remark) { [weak self] err in
            queue.asyncIfNotGlobal {
                handler(err == nil)
            }
            if let err {
                showToast(err.errorDesc)
            } else {
                guard let self else { return }
                conversationList.first { $0.userID == uid }?.extraInfo?.remark = remark
                notifyConversationListChanged()
            }
        }
    }
    
    func getUsersRemark(uids: [String], queue: DispatchQueue = .main, handler: @escaping ([LNIMUserRemarkVO]?) -> Void) {
        LNHttpManager.shared.getUsersRemark(uids: uids) { res, err in
            queue.asyncIfNotGlobal {
                handler(res?.list)
            }
        }
    }
}

// MARK: IM 初始化
extension LNIMManager {
    private func getIMSignToken(handler: @escaping (String?) -> Void) {
        LNHttpManager.shared.getIMSign { token, err in
            handler(token)
        }
    }
}

extension LNIMManager: LNAccountManagerNotify, LNProfileManagerNotify {
    func onUserLogin() {
        // 初始化 SDK
        let config = V2TIMSDKConfig()
        config.logLevel = LNAppConfig.shared.curEnv == .test ? .LOG_DEBUG : .LOG_INFO
        config.logListener = { level, log in
            guard let log else { return }
            if LNAppConfig.shared.curEnv == .test {
                Log.d(log)
            } else {
                Log.i(log)
            }
        }
        guard V2TIMManager.sharedInstance().initSDK(Self.appId, config: config) else {
            Log.e("V2TIMManager initSDK failed")
            return
        }
        
        // 登录
        let loginSuccessBlock = { [weak self] in
            guard let self else { return }
            onUserInfoChanged(userInfo: myUserInfo)
            
            reloadConversationList()
            
            voiceCallAvailable = true
            TUICallEngine.createInstance().addObserver(self)
            
            TIMPushManager.registerPush(Self.appId, appKey: "") { _ in
                Log.i("TIMPushManager registerPush success")
            } fail: { _, err in
                Log.e("TIMPushManager registerPush failed:\(err)")
            }
        }
        
        getIMSignToken { token in
            guard let token else {
                Log.e("LNIMManager getIMSignToken failed")
                return
            }
            LoginStore.shared.login(sdkAppID: Self.appId, userID: myUid, userSig: token) { error in
                switch error {
                  case .success(let info):
                    Log.i("LoginStore", "login success \(info)")
                    loginSuccessBlock()
                  case .failure(let error):
                    Log.i("LoginStore", "login failed code:\(error.code), message:\(error.message)")
                }
            }
        }
    }
    
    func onUserLogout() {
        LoginStore.shared.logout(completion: nil)
        TIMPushManager.unRegisterPush { } fail: { _, _ in }
        TUICallEngine.destroyInstance()
        
        Self.shared = LNIMManager()
    }
    
    func onUserInfoChanged(userInfo: LNUserProfileVO) {
        guard userInfo.userNo.isMyUid, !myUserInfo.userNo.isEmpty else { return }
        let info = V2TIMUserFullInfo()
        info.nickName = myUserInfo.nickname
        info.faceURL = myUserInfo.avatar
        V2TIMManager.sharedInstance().setSelfInfo(info: info) { }
    }
}

extension LNIMManager {
    private func notifyConversationListChanged() {
        LNEventDeliver.notifyEvent {
            ($0 as? LNIMManagerNotify)?.onConversationListChanged()
        }
    }
    
    private func notifyUserStatusChanged(uid: String, status: V2TIMUserStatusType) {
        LNEventDeliver.notifyEvent {
            ($0 as? LNIMManagerNotify)?.onIMUserStatusChanged(uid: uid, status: status)
        }
    }
}
