//
//  LiveRoomViewModel.swift
//  SodaLive
//
//  Created by klaus on 2023/08/14.
//

import Foundation
import Moya
import Combine

import AgoraRtcKit
import AgoraRtmKit

import FirebaseDynamicLinks

final class LiveRoomViewModel: NSObject, ObservableObject {
    
    private var agora: Agora = Agora.shared
    
    private let repository = LiveRepository()
    private let userRepository = UserRepository()
    private let reportRepository = ReportRepository()
    private let rouletteRepository = RouletteRepository()
    private var subscription = Set<AnyCancellable>()
    
    @Published var isSpeakerMute = false
    @Published var isMute = false
    @Published var role = LiveRoomMemberRole.LISTENER
    
    @Published var messageChangeFlag = false
    @Published var messages = [LiveRoomChat]()
    @Published var activeSpeakers = [UInt]()
    @Published var muteSpeakers = [UInt]()
    @Published var liveRoomInfo: GetRoomInfoResponse?
    @Published var userProfile: GetLiveRoomUserProfileResponse?
    
    @Published var coverImageUrl: String?
    
    @Published var isLoadingLikeHeart = false
    @Published var isLoading = false
    @Published var errorMessage = ""
    @Published var reportMessage = ""
    @Published var isShowReportPopup = false
    @Published var isShowErrorPopup = false
    @Published var isShowUserProfilePopup = false
    @Published var changeIsAdult = false {
        didSet {
            if changeIsAdult && !UserDefaults.bool(forKey: .auth) {
                agora.speakerMute(true)
            }
        }
    }
    
    @Published var popupContent = ""
    @Published var popupCancelTitle: String? = nil
    @Published var popupCancelAction: (() -> Void)? = nil
    @Published var popupConfirmTitle: String? = nil
    @Published var popupConfirmAction: (() -> Void)? = nil
    @Published var isShowPopup = false {
        didSet {
            if !isShowPopup {
                resetPopupContent()
            }
        }
    }
    
    @Published var isShowProfileList = false
    @Published var isShowProfilePopup = false {
        didSet {
            if !isShowProfilePopup {
                selectedProfile = nil
            }
        }
    }
    @Published var selectedProfile: LiveRoomMember?
    
    @Published var isShowNotice = false {
        didSet {
            if isShowNotice {
                isShowMenuPan = false
            }
        }
    }
    
    @Published var isShowMenuPan = false {
        didSet {
            if isShowMenuPan {
                isShowNotice = false
            }
        }
    }
    
    @Published var isShowDonationPopup = false
    
    @Published var isShowDonationMessagePopup = false
    
    @Published var isShowDonationRankingPopup = false
    
    @Published var isSpeakerFold = false
    
    @Published var isShowQuitPopup = false
    
    @Published var isShowLiveEndPopup = false
    
    @Published var isShowEditRoomInfoDialog = false
    
    @Published var isShowShareView = false
    
    @Published var shareMessage = ""
    
    @Published var isShowKickOutPopup = false
    @Published var kickOutDesc = ""
    @Published var kickOutId = 0 {
        didSet {
            kickOutDesc = "\(getUserNicknameAndProfileUrl(accountId: kickOutId).nickname)님을 내보내시겠어요?"
        }
    }
    
    @Published var totalDonationCan = 0
    @Published var totalHeartCount = 0
    @Published var donationMessageList = [LiveRoomDonationMessage]()
    @Published var donationMessageCount = 0
    
    @Published var isShowingNewChat = false
    @Published var isShowPhotoPicker = false
    @Published var noticeViewWidth: CGFloat = UIFont.systemFontSize
    @Published var noticeViewHeight: CGFloat = UIFont.systemFontSize
    
    @Published var isBgOn = true
    @Published var isSignatureOn = true
    @Published var donationStatus: GetLiveRoomDonationStatusResponse?
    
    @Published private(set) var offset: CGFloat = 0
    @Published private(set) var originOffset: CGFloat = 0
    private var isCheckedOriginOffset: Bool = false
    
    @Published var coverImage: UIImage? = nil
    
    @Published var isShowReportMenu = false
    @Published var isShowUesrBlockConfirm = false
    @Published var isShowUesrReportView = false
    @Published var isShowProfileReportConfirm = false
    @Published var isShowNoChattingConfirm = false
    @Published var isShowNoticeLikeHeart = false {
        didSet {
            if !isShowNoticeLikeHeart {
                isAvailableLikeHeart = true
            }
        }
    }
    
    @Published var reportUserId = 0
    @Published var reportUserNickname = ""
    @Published var reportUserIsBlocked = false
    
    @Published var noChattingUserId = 0
    @Published var noChattingUserNickname = ""
    @Published var noChattingUserProfileUrl = ""
    
    private let noChattingTime = 180
    @Published var isNoChatting = false
    @Published var remainingNoChattingTime = 0
    
    @Published var isActiveRoulette = false
    
    @Published var isShowRouletteSettings = false
    
    @Published var isShowRoulettePreview = false
    @Published var roulettePreviewList = [RoulettePreview]()
    
    @Published var isShowRoulette = false
    @Published var rouletteItems = [String]()
    @Published var rouletteSelectedItem = ""
    var rouletteCan = 0
    
    @Published var signatureImageUrl = "" {
        didSet {
            showSignatureImage()
        }
    }
    
    @Published var signature: LiveRoomDonationResponse? = nil {
        didSet {
            showSignatureImage()
        }
    }
    
    private var menuId = 0
    @Published var menu = ""
    @Published var menuList = [GetMenuPresetResponse]()
    
    @Published var isActivateMenu = false {
        didSet {
            if !isActivateMenu {
                menu = ""
            }
        }
    }
    @Published var selectedMenu: SelectedMenu? = nil
    
    @Published var hearts: [Heart] = []
    
    var signatureImageUrls = [String]()
    var signatureList = [LiveRoomDonationResponse]()
    var isShowSignatureImage = false
    
    var timer: DispatchSourceTimer?
    var heartTimer: DispatchSourceTimer?
    
    var isAvailableLikeHeart = false
    
    private var blockedMemberIdList = Set<Int>()
    
    func getBlockedMemberIdList() {
        userRepository.getBlockedMemberIdList()
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponse<[Int]>.self, from: responseData)
                    
                    if let data = decoded.data, decoded.success {
                        self.blockedMemberIdList.removeAll()
                        self.blockedMemberIdList.formUnion(data)
                    }
                } catch {
                }
            }
            .store(in: &subscription)
    }
    
    func setOriginOffset(_ offset: CGFloat) {
        guard !isCheckedOriginOffset else { return }
        self.originOffset = offset
        self.offset = offset
        isCheckedOriginOffset = true
    }
    
    func setOffset(_ offset: CGFloat) {
        guard isCheckedOriginOffset else { return }
        self.offset = offset
    }
    
    func initAgoraEngine() {
        agora.rtcEngineDelegate = self
        agora.rtmDelegate = self
        agora.initialize()
    }
    
    private func deInitAgoraEngine() {
        agora.deInit()
    }
    
    func agoraConnectSuccess(isManager: Bool) {
        self.isLoading = false
        if isManager {
            role = .SPEAKER
        } else {
            role = .LISTENER
        }
        
        DEBUG_LOG("agoraConnectSuccess")
        
        if containNoChatRoom() {
            startNoChatting()
        }
    }
    
    func agoraConnectFail() {
        self.isLoading = false
        DEBUG_LOG("agoraConnectFail")
        AppState.shared.roomId = 0
        AppState.shared.isShowPlayer = false
    }
    
    func quitRoom() {
        isLoading = true
        
        if let index = muteSpeakers.firstIndex(of: UInt(UserDefaults.int(forKey: .userId))) {
            muteSpeakers.remove(at: index)
        }
        
        repository.quitRoom(roomId: AppState.shared.roomId)
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
                    
                    if decoded.success {
                        self.deInitAgoraEngine()
                        self.liveRoomInfo = nil
                        AppState.shared.roomId = 0
                    } else {
                        if let message = decoded.message {
                            self.errorMessage = message
                        } else {
                            self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                        }
                        
                        self.isShowErrorPopup = true
                    }
                } catch {
                    self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                    self.isShowErrorPopup = true
                }
                
                self.isLoading = false
            }
            .store(in: &subscription)
    }
    
    func getRoomInfo(userId: Int = 0, onSuccess: @escaping (String) -> Void = { _ in }) {
        isLoading = true
        
        repository.getRoomInfo(roomId: AppState.shared.roomId)
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponse<GetRoomInfoResponse>.self, from: responseData)
                    
                    if let data = decoded.data, decoded.success {
                        self.liveRoomInfo = data
                        
                        if self.coverImageUrl != data.coverImageUrl {
                            self.coverImageUrl = data.coverImageUrl
                        }
                        
                        self.isActiveRoulette = data.isActiveRoulette
                        self.isLoading = true
                        self.agora.joinChannel(
                            roomInfo: data,
                            rtmChannelDelegate: self,
                            onConnectSuccess: self.agoraConnectSuccess,
                            onConnectFail: self.agoraConnectFail
                        )
                        
                        getTotalDonationCan()
                        getTotalHeartCount()
                        
                        if data.isAdult && !UserDefaults.bool(forKey: .auth) {
                            changeIsAdult = true
                        }
                        
                        if (userId > 0 && data.creatorId == UserDefaults.int(forKey: .userId)) {
                            let nickname = getUserNicknameAndProfileUrl(accountId: userId).nickname
                            onSuccess(nickname)
                        }
                    } else {
                        if let message = decoded.message {
                            self.errorMessage = message
                        } else {
                            self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                        }
                        
                        self.isShowErrorPopup = true
                    }
                    
                    self.isLoading = false
                } catch {
                    self.isLoading = false
                    self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                    self.isShowErrorPopup = true
                }
            }
            .store(in: &subscription)
    }
    
    func toggleMute() {
        isMute.toggle()
        agora.mute(isMute)
        
        if isMute {
            muteSpeakers.append(UInt(UserDefaults.int(forKey: .userId)))
        } else {
            if let index = muteSpeakers.firstIndex(of: UInt(UserDefaults.int(forKey: .userId))) {
                muteSpeakers.remove(at: index)
            }
        }
    }
    
    func toggleSpeakerMute() {
        isSpeakerMute.toggle()
        agora.speakerMute(isSpeakerMute)
    }
    
    func sendMessage(chatMessage: String, onSuccess: @escaping () -> Void) {
        DispatchQueue.main.async {[unowned self] in
            if isNoChatting {
                self.popupContent = "\(remainingNoChattingTime)초 동안 채팅하실 수 없습니다"
                self.isShowPopup = true
            } else if chatMessage.count > 0 {
                agora.sendMessageToGroup(textMessage: chatMessage, completion: { [unowned self] errorCode in
                    if errorCode == .errorOk {
                        let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
                        let rank = getUserRank(userId: UserDefaults.int(forKey: .userId))
                        self.messages.append(LiveRoomNormalChat(userId: UserDefaults.int(forKey: .userId), profileUrl: profileUrl, nickname: nickname, rank: rank, chat: chatMessage))
                        
                        self.messageChangeFlag.toggle()
                        if self.messages.count > 100 {
                            self.messages.remove(at: 0)
                        }
                    }
                    
                    onSuccess()
                })
            }
        }
    }
    
    func donation(can: Int, message: String = "", isSecret: Bool = false) {
        if can > 0 {
            isLoading = true
            
            repository.donation(roomId: AppState.shared.roomId, can: can, message: message, isSecret: isSecret)
                .sink { result in
                    switch result {
                    case .finished:
                        DEBUG_LOG("finish")
                    case .failure(let error):
                        ERROR_LOG(error.localizedDescription)
                    }
                } receiveValue: { [unowned self] response in
                    let responseData = response.data
                    
                    do {
                        let jsonDecoder = JSONDecoder()
                        let decoded = try jsonDecoder.decode(ApiResponse<LiveRoomDonationResponse>.self, from: responseData)
                        
                        self.isLoading = false
                        
                        if decoded.success {
                            var rawMessage = ""
                            
                            if isSecret {
                                rawMessage = "\(can)캔을 비밀후원하셨습니다.💰🪙"
                            } else {
                                rawMessage = "\(can)캔을 후원하셨습니다.💰🪙"
                            }
                            
                            let donationRawMessage = LiveRoomChatRawMessage(
                                type: isSecret ? .SECRET_DONATION : .DONATION,
                                message: rawMessage,
                                can: can,
                                signature: decoded.data,
                                signatureImageUrl: decoded.data?.imageUrl,
                                donationMessage: message
                            )
                            
                            UserDefaults.set(UserDefaults.int(forKey: .can) - can, forKey: .can)
                            
                            if isSecret {
                                agora.sendRawMessageToPeer(
                                    peerId: String(liveRoomInfo!.creatorId), rawMessage: donationRawMessage,
                                    completion: { [unowned self] errorCode in
                                        if errorCode == .ok {
                                            let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
                                            self.messages.append(
                                                LiveRoomDonationChat(
                                                    profileUrl: profileUrl,
                                                    nickname: nickname,
                                                    chat: rawMessage,
                                                    can: can,
                                                    donationMessage: message
                                                )
                                            )
                                            
                                            addSignature(signature: decoded.data)
                                            
                                            self.messageChangeFlag.toggle()
                                            if self.messages.count > 100 {
                                                self.messages.remove(at: 0)
                                            }
                                        } else {
                                            refundDonation()
                                        }
                                    },
                                    fail: { [unowned self] in
                                        refundDonation()
                                    }
                                )
                            } else {
                                agora.sendRawMessageToGroup(
                                    rawMessage: donationRawMessage,
                                    completion: { [unowned self] errorCode in
                                        if errorCode == .errorOk {
                                            let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
                                            self.messages.append(
                                                LiveRoomDonationChat(
                                                    profileUrl: profileUrl,
                                                    nickname: nickname,
                                                    chat: rawMessage,
                                                    can: can,
                                                    donationMessage: message
                                                )
                                            )
                                            
                                            totalDonationCan += can
                                            addSignature(signature: decoded.data)
                                            
                                            self.messageChangeFlag.toggle()
                                            if self.messages.count > 100 {
                                                self.messages.remove(at: 0)
                                            }
                                        } else {
                                            refundDonation()
                                        }
                                    },
                                    fail: { [unowned self] in
                                        refundDonation()
                                    }
                                )
                            }
                        } else {
                            if let message = decoded.message {
                                self.popupContent = message
                            } else {
                                self.popupContent = "후원에 실패했습니다.\n다시 후원해주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                            }
                            
                            self.isShowPopup = true
                        }
                    } catch {
                        self.isLoading = false
                        self.popupContent = "후원에 실패했습니다.\n다시 후원해주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                        self.isShowPopup = true
                    }
                }
                .store(in: &subscription)
        } else {
            popupContent = "1캔 이상 후원하실 수 있습니다."
            isShowPopup = true
        }
    }
    
    private func refundDonation() {
        isLoading = true
        
        repository.refundDonation(roomId: AppState.shared.roomId)
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
                    
                    self.isLoading = false
                    
                    if decoded.success {
                        self.popupContent = "후원에 실패했습니다.\n다시 후원해주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                        self.isShowPopup = true
                    } else {
                        if let message = decoded.message {
                            self.errorMessage = message
                        } else {
                            self.popupContent = "후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요."
                        }
                        
                        self.isShowPopup = true
                    }
                } catch {
                    self.isLoading = false
                    self.popupContent = "후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요."
                    self.isShowPopup = true
                }
            }
            .store(in: &subscription)
    }
    
    func inviteSpeaker(peerId: Int) {
        agora.sendMessageToPeer(peerId: String(peerId), rawMessage: LiveRoomRequestType.INVITE_SPEAKER.rawValue.data(using: .utf8)!, completion: { [unowned self] errorCode in
            if errorCode == .ok {
                self.popupContent = "스피커 요청을 보냈습니다.\n잠시만 기다려 주세요."
                self.isShowPopup = true
            }
        })
    }
    
    func changeListener(peerId: Int, isFromManager: Bool = false) {
        agora.sendMessageToPeer(peerId: String(peerId), rawMessage: LiveRoomRequestType.CHANGE_LISTENER.rawValue.data(using: .utf8)!, completion: { [unowned self] errorCode in
            if errorCode == .ok {
                if isFromManager {
                    getRoomInfo()
                    setManagerMessage()
                    releaseManagerMessageToPeer(userId: peerId)
                    self.popupContent = "\(getUserNicknameAndProfileUrl(accountId: peerId).nickname)님을 스탭에서 해제했어요."
                } else {
                    self.popupContent = "\(getUserNicknameAndProfileUrl(accountId: peerId).nickname)님을 리스너로 변경했어요."
                }
                
                self.isShowPopup = true
            }
        })
    }
    
    private func getUserNicknameAndProfileUrl(accountId: Int) -> (nickname: String, profileUrl: String) {
        for staff in liveRoomInfo!.managerList {
            if staff.id == accountId {
                return (staff.nickname, staff.profileImage)
            }
        }
        
        for speaker in liveRoomInfo!.speakerList {
            if speaker.id == accountId {
                return (speaker.nickname, speaker.profileImage)
            }
        }
        
        for listener in liveRoomInfo!.listenerList {
            if listener.id == accountId {
                return (listener.nickname, listener.profileImage)
            }
        }
        
        return ("", "")
    }
    
    func isEqualToStaffId(creatorId: Int) -> Bool {
        for staff in liveRoomInfo!.managerList {
            if staff.id == creatorId {
                return true
            }
        }
        
        return false
    }
    
    func setListener() {
        repository.setListener(roomId: AppState.shared.roomId, userId: UserDefaults.int(forKey: .userId))
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
                    
                    if decoded.success {
                        self.role = .LISTENER
                        self.agora.setRole(role: .audience)
                        self.isMute = false
                        self.agora.mute(isMute)
                        if let index = self.muteSpeakers.firstIndex(of: UInt(UserDefaults.int(forKey: .userId))) {
                            self.muteSpeakers.remove(at: index)
                        }
                        self.getRoomInfo()
                    }
                } catch {
                }
            }
            .store(in: &subscription)
    }
    
    private func setSpeaker() {
        repository.setSpeaker(roomId: AppState.shared.roomId, userId: UserDefaults.int(forKey: .userId))
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
                    
                    if decoded.success {
                        self.role = .SPEAKER
                        self.agora.setRole(role: .broadcaster)
                        self.popupContent = "스피커가 되었어요!"
                        self.isShowPopup = true
                        self.isMute = false
                        self.getRoomInfo()
                    }
                } catch {
                }
            }
            .store(in: &subscription)
    }
    
    private func resetPopupContent() {
        errorMessage = ""
        popupContent = ""
        popupCancelTitle = nil
        popupCancelAction = nil
        popupConfirmTitle = nil
        popupConfirmAction = nil
    }
    
    private func requestSpeakerAllow(_ peerId: String) {
        agora.sendMessageToPeer(peerId: peerId, rawMessage: LiveRoomRequestType.REQUEST_SPEAKER_ALLOW.rawValue.data(using: .utf8)!, completion: nil)
    }
    
    func editLiveRoomInfo(title: String, notice: String, isAdult: Bool) {
        let request = EditLiveRoomInfoRequest(
            title: liveRoomInfo!.title != title ? title : nil,
            notice: liveRoomInfo!.notice != notice ? notice : nil,
            numberOfPeople: nil,
            beginDateTimeString: nil,
            timezone: nil,
            menuPanId: isActivateMenu ? menuId : 0,
            menuPan: isActivateMenu ? menu : "",
            isActiveMenuPan: isActivateMenu,
            isAdult: liveRoomInfo!.isAdult != isAdult ? isAdult : nil
        )
        
        if (request.title == nil && request.notice == nil && coverImage == nil && menu == liveRoomInfo?.menuPan && request.isAdult == nil) {
            self.errorMessage = "변경사항이 없습니다."
            self.isShowErrorPopup = true
            return
        }
        
        var multipartData = [MultipartFormData]()
        
        let encoder = JSONEncoder()
        encoder.outputFormatting = .withoutEscapingSlashes
        
        if (request.title != nil || request.notice != nil || request.isAdult != nil || menu != liveRoomInfo?.menuPan) {
            let jsonData = try? encoder.encode(request)
            if let jsonData = jsonData {
                multipartData.append(MultipartFormData(provider: .data(jsonData), name: "request"))
            }
        }
        
        if let coverImage = coverImage, let imageData = coverImage.jpegData(compressionQuality: 0.8) {
            multipartData.append(
                MultipartFormData(
                    provider: .data(imageData),
                    name: "coverImage",
                    fileName: "\(UUID().uuidString)_\(Date().timeIntervalSince1970 * 1000).jpg",
                    mimeType: "image/*")
            )
        }
        
        repository.editLiveRoomInfo(roomId: AppState.shared.roomId, parameters: multipartData)
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
                    
                    if decoded.success {
                        self.coverImage = nil
                        self.menuList.removeAll()
                        self.isActivateMenu = false
                        self.selectedMenu = nil
                        self.menu = ""
                        self.isShowMenuPan = false
                        self.getRoomInfo()
                        
                        let editRoomInfoMessage = LiveRoomChatRawMessage(
                            type: .EDIT_ROOM_INFO,
                            message: "",
                            can: 0,
                            donationMessage: ""
                        )
                        
                        self.agora.sendRawMessageToGroup(rawMessage: editRoomInfoMessage)
                        self.errorMessage = "라이브 정보가 수정되었습니다."
                        self.isShowErrorPopup = true
                    } else {
                        self.errorMessage = decoded.message ?? "라이브 정보를 수정하지 못했습니다.\n다시 시도해 주세요."
                        self.isShowErrorPopup = true
                    }
                } catch {
                    self.errorMessage = "라이브 정보를 수정하지 못했습니다.\n다시 시도해 주세요."
                    self.isShowErrorPopup = true
                }
            }
            .store(in: &subscription)
    }
    
    func selectMenuPreset(selectedMenuPreset: SelectedMenu) {
        if menuList.isEmpty && (selectedMenuPreset == .MENU_2 || selectedMenuPreset == .MENU_3) {
            errorMessage = "메뉴 1을 먼저 설정하세요"
            isShowPopup = true
            return
        }
        
        if menuList.count == 1 && selectedMenuPreset == .MENU_3 {
            errorMessage = "메뉴 1과 메뉴 2를 먼저 설정하세요"
            isShowPopup = true
            return
        }
        
        if self.selectedMenu != selectedMenuPreset {
            self.selectedMenu = selectedMenuPreset
            
            if menuList.count > selectedMenuPreset.rawValue {
                let menu = menuList[selectedMenuPreset.rawValue]
                self.menu = menu.menu
                self.menuId = menu.id
            } else {
                self.menu = ""
                self.menuId = 0
            }
        }
    }
    
    func getAllMenuPreset(onFailure: @escaping () -> Void) {
        isLoading = true
        
        repository.getAllMenuPreset(creatorId: UserDefaults.int(forKey: .userId))
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                self.isLoading = false
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponse<[GetMenuPresetResponse]>.self, from: responseData)
                    
                    if let data = decoded.data, decoded.success {
                        self.menuList.removeAll()
                        self.menuList.append(contentsOf: data)
                        self.isActivateMenu = false
                        self.selectedMenu = nil
                        
                        for (index, menuPreset) in self.menuList.enumerated() {
                            if menuPreset.isActive {
                                switch index {
                                case 1:
                                    self.selectMenuPreset(selectedMenuPreset: .MENU_2)
                                    
                                case 2:
                                    self.selectMenuPreset(selectedMenuPreset: .MENU_3)
                                    
                                default:
                                    self.selectMenuPreset(selectedMenuPreset: .MENU_1)
                                }
                                
                                self.isActivateMenu = true
                            }
                            
                        }
                    } else {
                        onFailure()
                        if let message = decoded.message {
                            self.errorMessage = message
                        } else {
                            self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                        }
                        
                        self.isShowPopup = true
                    }
                } catch {
                    onFailure()
                    self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                    self.isShowPopup = true
                }
            }
            .store(in: &subscription)
    }
    
    func shareRoom() {
        guard let link = URL(string: "https://sodalive.net/?room_id=\(AppState.shared.roomId)") else { return }
        let dynamicLinksDomainURIPrefix = "https://sodalive.page.link"
        guard let linkBuilder = DynamicLinkComponents(link: link, domainURIPrefix: dynamicLinksDomainURIPrefix) else {
            self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요."
            self.isShowErrorPopup = true
            return
        }
        
        linkBuilder.iOSParameters = DynamicLinkIOSParameters(bundleID: "kr.co.vividnext.sodalive")
        linkBuilder.iOSParameters?.appStoreID = "6461721697"
        
        linkBuilder.androidParameters = DynamicLinkAndroidParameters(packageName: "kr.co.vividnext.sodalive")
        
        guard let longDynamicLink = linkBuilder.url else {
            self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요."
            self.isShowErrorPopup = true
            return
        }
        DEBUG_LOG("The long URL is: \(longDynamicLink)")
        
        DynamicLinkComponents.shortenURL(longDynamicLink, options: nil) { [unowned self] url, warnings, error in
            let shortUrl = url?.absoluteString
            
            if let liveRoomInfo = self.liveRoomInfo {
                let urlString = shortUrl != nil ? shortUrl! : longDynamicLink.absoluteString
                if liveRoomInfo.isPrivateRoom {
                    shareMessage = "\(UserDefaults.string(forKey: .nickname))님이 귀하를 보이스온 비공개라이브에 초대하였습니다.\n" +
                    "※ 라이브 참여: \(urlString)\n" +
                    "(입장 비밀번호: \(liveRoomInfo.password!))"
                } else {
                    shareMessage = "\(UserDefaults.string(forKey: .nickname))님이 귀하를 보이스온 공개라이브에 초대하였습니다.\n" +
                    "※ 라이브 참여: \(urlString)"
                }
                
                isShowShareView = true
            } else {
                self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요."
                self.isShowErrorPopup = true
                return
            }
        }
    }
    
    func kickOut() {
        if UserDefaults.int(forKey: .userId) == liveRoomInfo?.creatorId {
            repository.kickOut(roomId: AppState.shared.roomId, userId: kickOutId)
                .sink { result in
                    switch result {
                    case .finished:
                        DEBUG_LOG("finish")
                    case .failure(let error):
                        ERROR_LOG(error.localizedDescription)
                    }
                } receiveValue: { _ in
                    
                }
                .store(in: &subscription)
            
            let nickname = getUserNicknameAndProfileUrl(accountId: kickOutId).nickname
            agora.sendMessageToPeer(peerId: String(kickOutId), rawMessage: LiveRoomRequestType.KICK_OUT.rawValue.data(using: .utf8)!, completion: { [unowned self] errorCode in
                if errorCode == .ok {
                    self.popupContent = "\(nickname)님을 내보냈습니다."
                    self.isShowPopup = true
                }
            })
        }
        
        if let index = muteSpeakers.firstIndex(of: UInt(kickOutId)) {
            muteSpeakers.remove(at: index)
        }
        
        isShowKickOutPopup = false
        kickOutDesc = ""
        kickOutId = 0
    }
    
    func getDonationStatus() {
        isLoading = true
        repository.donationStatus(roomId: AppState.shared.roomId)
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                self.isLoading = false
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponse<GetLiveRoomDonationStatusResponse>.self, from: responseData)
                    
                    if let data = decoded.data, decoded.success {
                        self.donationStatus = data
                    } else {
                        self.errorMessage = "후원현황을 가져오지 못했습니다\n다시 시도해 주세요."
                        self.isShowPopup = true
                    }
                } catch {
                    self.isLoading = false
                    self.errorMessage = "후원현황을 가져오지 못했습니다\n다시 시도해 주세요."
                    self.isShowPopup = true
                }
            }
            .store(in: &subscription)
    }
    
    func creatorFollow(creatorId: Int? = nil, isGetUserProfile: Bool = false) {
        var userId = 0
        
        if let creatorId = creatorId {
            userId = creatorId
        } else if let liveRoomInfo = liveRoomInfo {
            userId = liveRoomInfo.creatorId
        }
        
        if userId > 0 {
            isLoading = true
            
            userRepository.creatorFollow(creatorId: userId)
                .sink { result in
                    switch result {
                    case .finished:
                        DEBUG_LOG("finish")
                    case .failure(let error):
                        ERROR_LOG(error.localizedDescription)
                    }
                } receiveValue: { [unowned self] response in
                    self.isLoading = false
                    let responseData = response.data
                    
                    do {
                        let jsonDecoder = JSONDecoder()
                        let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
                        
                        if decoded.success {
                            self.getRoomInfo()
                            
                            if isGetUserProfile {
                                getUserProfile(userId: userId)
                            }
                        } else {
                            if let message = decoded.message {
                                self.errorMessage = message
                            } else {
                                self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                            }
                            
                            self.isShowPopup = true
                        }
                    } catch {
                        self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                        self.isShowPopup = true
                    }
                }
                .store(in: &subscription)
        }
    }
    
    func creatorUnFollow(creatorId: Int? = nil, isGetUserProfile: Bool = false) {
        var userId = 0
        
        if let creatorId = creatorId {
            userId = creatorId
        } else if let liveRoomInfo = liveRoomInfo {
            userId = liveRoomInfo.creatorId
        }
        
        if userId > 0 {
            isLoading = true
            
            userRepository.creatorUnFollow(creatorId: userId)
                .sink { result in
                    switch result {
                    case .finished:
                        DEBUG_LOG("finish")
                    case .failure(let error):
                        ERROR_LOG(error.localizedDescription)
                    }
                } receiveValue: { [unowned self] response in
                    self.isLoading = false
                    let responseData = response.data
                    
                    do {
                        let jsonDecoder = JSONDecoder()
                        let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
                        
                        if decoded.success {
                            self.getRoomInfo()
                            
                            if isGetUserProfile {
                                getUserProfile(userId: userId)
                            }
                        } else {
                            if let message = decoded.message {
                                self.errorMessage = message
                            } else {
                                self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                            }
                            
                            self.isShowPopup = true
                        }
                    } catch {
                        self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                        self.isShowPopup = true
                    }
                }
                .store(in: &subscription)
        }
    }
    
    func getUserRank(userId: Int) -> Int {
        // 방장 -> -2
        // 스탭 -> -3
        // 나머지 -> 체크
        if userId == liveRoomInfo!.creatorId {
            return -2
        } else if isEqualToStaffId(creatorId: userId) {
            return -3
        } else {
            return liveRoomInfo!.donationRankingTop3UserIds.firstIndex(of: userId) ?? -1
        }
    }
    
    private func getTotalHeartCount() {
        repository.getTotalHeartCount(roomId: AppState.shared.roomId)
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                self.isLoading = false
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponse<GetLiveRoomHeartTotalResponse>.self, from: responseData)
                    
                    if let data = decoded.data, decoded.success {
                        self.totalHeartCount = data.totalHeartCount
                    }
                } catch {
                }
            }
            .store(in: &subscription)
    }
    
    func getTotalDonationCan() {
        repository.getTotalDoantionCan(roomId: AppState.shared.roomId)
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                self.isLoading = false
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponse<GetLiveRoomDonationTotalResponse>.self, from: responseData)
                    
                    if let data = decoded.data, decoded.success {
                        self.totalDonationCan = data.totalDonationCan
                    }
                } catch {
                }
            }
            .store(in: &subscription)
    }
    
    func getMemberCan() {
        userRepository.getMemberCan()
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { response in
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponse<GetMemberCanResponse>.self, from: responseData)
                    
                    if let data = decoded.data, decoded.success {
                        UserDefaults.set(data.can, forKey: .can)
                    }
                } catch {
                }
            }
            .store(in: &subscription)
    }
    
    func getDonationMessageList() {
        isLoading = true
        repository.getDonationMessageList(roomId: AppState.shared.roomId)
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                self.isLoading = false
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponse<[LiveRoomDonationMessage]>.self, from: responseData)
                    
                    if let data = decoded.data, decoded.success {
                        self.donationMessageList.removeAll()
                        self.donationMessageList.append(contentsOf: data)
                        self.donationMessageCount = data.count
                    } else {
                        if let message = decoded.message {
                            self.errorMessage = message
                        } else {
                            self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                        }
                        
                        self.isShowPopup = true
                    }
                } catch {
                    self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                    self.isShowPopup = true
                }
            }
            .store(in: &subscription)
    }
    
    func deleteDonationMessage(uuid: String) {
        isLoading = true
        repository.deleteDonationMessage(roomId: AppState.shared.roomId, messageUUID: uuid)
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                self.isLoading = false
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
                    
                    if decoded.success {
                        self.donationMessageCount -= 1
                        let filteredDonationMessageList = self.donationMessageList.filter { $0.uuid != uuid }
                        self.donationMessageList.removeAll()
                        self.donationMessageList.append(contentsOf: filteredDonationMessageList)
                    } else {
                        self.errorMessage = "메시지를 삭제하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                        self.isShowPopup = true
                    }
                } catch {
                    self.errorMessage = "메시지를 삭제하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                    self.isShowPopup = true
                }
            }
            .store(in: &subscription)
    }
    
    func getUserProfile(userId: Int) {
        isLoading = true
        repository.getUserProfile(roomId: AppState.shared.roomId, userId: userId)
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                self.isLoading = false
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponse<GetLiveRoomUserProfileResponse>.self, from: responseData)
                    
                    if let data = decoded.data, decoded.success {
                        userProfile = data
                        isShowUserProfilePopup = true
                    } else {
                        if let message = decoded.message {
                            self.errorMessage = message
                        } else {
                            self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                        }
                        
                        self.isShowPopup = true
                    }
                } catch {
                    self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                    self.isShowPopup = true
                }
            }
            .store(in: &subscription)
    }
    
    func setManager(userId: Int) {
        isLoading = true
        repository.setManager(roomId: AppState.shared.roomId, userId: userId)
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                self.isLoading = false
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
                    
                    if decoded.success {
                        getRoomInfo()
                        setManagerMessage()
                        
                        self.popupContent = "\(getUserNicknameAndProfileUrl(accountId: userId).nickname)님을 스탭으로 지정했습니다."
                        self.isShowPopup = true
                    } else {
                        if let message = decoded.message {
                            self.errorMessage = message
                        } else {
                            self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                        }
                        
                        self.isShowPopup = true
                    }
                } catch {
                    self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                    self.isShowPopup = true
                }
            }
            .store(in: &subscription)
    }
    
    func releaseManagerMessageToPeer(userId: Int) {
        agora.sendMessageToPeer(
            peerId: String(userId),
            rawMessage: LiveRoomRequestType.RELEASE_MANAGER.rawValue.data(using: .utf8)!,
            completion: nil
        )
    }
    
    func setManagerMessageToPeer(userId: Int) {
        agora.sendMessageToPeer(
            peerId: String(userId),
            rawMessage: LiveRoomRequestType.SET_MANAGER.rawValue.data(using: .utf8)!,
            completion: nil
        )
    }
    
    func setNoChatting() {
        agora.sendMessageToPeer(
            peerId: String(noChattingUserId),
            rawMessage: LiveRoomRequestType.NO_CHATTING.rawValue.data(using: .utf8)!,
            completion: { [unowned self] errorCode in
                if errorCode == .ok {
                    self.popupContent = "\(noChattingUserNickname)님을 3분간 채팅금지를 하였습니다."
                    self.isShowPopup = true
                    
                    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                        self.noChattingUserId = 0
                        self.noChattingUserNickname = ""
                        self.noChattingUserProfileUrl = ""
                    }
                }
            }
        )
    }
    
    private func setManagerMessage() {
        let setManagerMessage = LiveRoomChatRawMessage(
            type: .SET_MANAGER,
            message: "",
            can: 0,
            donationMessage: ""
        )
        
        self.agora.sendRawMessageToGroup(rawMessage: setManagerMessage)
    }
    
    func userBlock(onSuccess: @escaping (Int) -> Void) {
        blockedMemberIdList.insert(reportUserId)
        
        isLoading = true
        userRepository.memberBlock(userId: reportUserId)
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                self.isLoading = false
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
                    
                    if decoded.success {
                        self.reportMessage = "차단하였습니다."
                        self.getUserProfile(userId: reportUserId)
                        onSuccess(reportUserId)
                        
                        self.reportUserId = 0
                        self.reportUserNickname = ""
                        self.reportUserIsBlocked = false
                    } else {
                        if let message = decoded.message {
                            self.reportMessage = message
                        } else {
                            self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                        }
                    }
                    
                    self.isShowReportPopup = true
                } catch {
                    self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                    self.isShowReportPopup = true
                }
            }
            .store(in: &subscription)
    }
    
    func userUnBlock() {
        blockedMemberIdList.remove(reportUserId)
        
        isLoading = true
        userRepository.memberUnBlock(userId: reportUserId)
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                self.isLoading = false
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
                    
                    if decoded.success {
                        self.reportMessage = "차단이 해제 되었습니다."
                        self.getUserProfile(userId: reportUserId)
                        self.reportUserId = 0
                        self.reportUserNickname = ""
                        self.reportUserIsBlocked = false
                    } else {
                        if let message = decoded.message {
                            self.reportMessage = message
                        } else {
                            self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                        }
                    }
                    
                    self.isShowReportPopup = true
                } catch {
                    self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                    self.isShowReportPopup = true
                }
            }
            .store(in: &subscription)
    }
    
    func report(type: ReportType, reason: String = "프로필 신고") {
        isLoading = true
        
        let request = ReportRequest(type: type, reason: reason, reportedMemberId: reportUserId, cheersId: nil, audioContentId: nil)
        reportRepository.report(request: request)
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                self.isLoading = false
                let responseData = response.data
                
                self.reportUserId = 0
                self.reportUserNickname = ""
                self.reportUserIsBlocked = false
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
                    
                    if let message = decoded.message {
                        self.reportMessage = message
                    } else {
                        self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                    }
                    
                    self.isShowReportPopup = true
                } catch {
                    self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                    self.isShowReportPopup = true
                }
            }
            .store(in: &subscription)
    }
    
    private func containNoChatRoom() -> Bool {
        let noChatRoomList = getNoChatRoomListFromUserDefaults()
        if let _ = noChatRoomList.firstIndex(of: liveRoomInfo!.roomId) {
            return true
        }
        
        return false
    }
    
    private func startNoChatting() {
        isNoChatting = true
        remainingNoChattingTime = noChattingTime
        popupContent = "\(self.getUserNicknameAndProfileUrl(accountId: liveRoomInfo!.creatorId).nickname)님이 3분간 채팅을 금지하였습니다."
        isShowPopup = true
        
        startCountDown()
    }
    
    private func startCountDown() {
        // 1초마다 타이머를 실행
        let queue = DispatchQueue.global(qos: .background)
        timer = DispatchSource.makeTimerSource(queue: queue)
        timer?.schedule(deadline: .now(), repeating: 1.0)
        timer?.setEventHandler { [unowned self] in
            DispatchQueue.main.async {
                self.remainingNoChattingTime -= 1
                
                if self.remainingNoChattingTime <= 0 {
                    self.isNoChatting = false
                    self.timer?.cancel()
                    self.removeNoChatRoom()
                    self.popupContent = "채팅금지가 해제되었습니다."
                    self.isShowPopup = true
                }
            }
        }
        timer?.resume()
    }
    
    private func addNoChatRoom() {
        var noChatRoomList = getNoChatRoomListFromUserDefaults()
        noChatRoomList.append(liveRoomInfo!.roomId)
        saveNoChatRoomListToUserDefaults(noChatRoomList: noChatRoomList)
    }
    
    private func removeNoChatRoom() {
        var noChatRoomList = getNoChatRoomListFromUserDefaults()
        if let index = noChatRoomList.firstIndex(of: liveRoomInfo!.roomId) {
            noChatRoomList.remove(at: index)
        }
        saveNoChatRoomListToUserDefaults(noChatRoomList: noChatRoomList)
    }
    
    private func getNoChatRoomListFromUserDefaults() -> [Int] {
        if let noChatRoomListData = UserDefaults.data(forKey: .noChatRoomList) {
            let jsonDecoder = JSONDecoder()
            if let noChatRoomList = try? jsonDecoder.decode([Int].self, from: noChatRoomListData) {
                return noChatRoomList
            }
        }
        return []
    }
    
    private func saveNoChatRoomListToUserDefaults(noChatRoomList: [Int]) {
        let jsonEncoder = JSONEncoder()
        if let jsonData = try? jsonEncoder.encode(noChatRoomList) {
            UserDefaults.set(jsonData, forKey: .noChatRoomList)
        }
    }
    
    func setActiveRoulette(isActiveRoulette: Bool, message: String) {
        self.popupContent = message
        self.isShowPopup = true
        self.agora.sendRawMessageToGroup(
            rawMessage: LiveRoomChatRawMessage(
                type: .TOGGLE_ROULETTE,
                message: "",
                can: 0,
                donationMessage: "",
                isActiveRoulette: isActiveRoulette
            )
        )
    }
    
    func showRoulette() {
        if let liveRoomInfo = liveRoomInfo, !isLoading {
            self.roulettePreviewList.removeAll()
            isLoading = true
            
            rouletteRepository.getRoulette(creatorId: liveRoomInfo.creatorId)
                .sink { result in
                    switch result {
                    case .finished:
                        DEBUG_LOG("finish")
                    case .failure(let error):
                        ERROR_LOG(error.localizedDescription)
                    }
                } receiveValue: { [unowned self] response in
                    self.isLoading = false
                    let responseData = response.data
                    
                    do {
                        let jsonDecoder = JSONDecoder()
                        let decoded = try jsonDecoder.decode(ApiResponse<[GetRouletteResponse]>.self, from: responseData)
                        
                        if let data = decoded.data, decoded.success, !data.isEmpty {
                            let roulettePreviewList = data
                                .filter { $0.isActive }
                                .filter { !$0.items.isEmpty}
                                .map { RoulettePreview(id: $0.id, can: $0.can, items: calculatePercentages(options: $0.items)) }
                            self.roulettePreviewList.append(contentsOf: roulettePreviewList)
                            self.isShowRoulettePreview = true
                        } else {
                            if let message = decoded.message {
                                self.errorMessage = message
                            } else {
                                self.errorMessage = "룰렛을 사용할 수 없습니다. 다시 시도해 주세요."
                            }
                            self.isShowErrorPopup = true
                        }
                    } catch {
                        self.errorMessage = "룰렛을 사용할 수 없습니다. 다시 시도해 주세요."
                        self.isShowErrorPopup = true
                    }
                }
                .store(in: &subscription)
        }
    }
    
    func spinRoulette(rouletteId: Int) {
        if !isLoading {
            isLoading = true
            rouletteRepository.spinRoulette(request: SpinRouletteRequest(roomId: AppState.shared.roomId, rouletteId: rouletteId))
                .sink { result in
                    switch result {
                    case .finished:
                        DEBUG_LOG("finish")
                    case .failure(let error):
                        ERROR_LOG(error.localizedDescription)
                    }
                } receiveValue: { [unowned self] response in
                    self.isLoading = false
                    let responseData = response.data
                    
                    do {
                        let jsonDecoder = JSONDecoder()
                        let decoded = try jsonDecoder.decode(ApiResponse<SpinRouletteResponse>.self, from: responseData)
                        
                        if let data = decoded.data, decoded.success, !data.items.isEmpty {
                            UserDefaults.set(UserDefaults.int(forKey: .can) - data.can, forKey: .can)
                            self.rouletteItems.removeAll()
                            self.rouletteItems.append(contentsOf: data.items.map { $0.title })
                            self.rouletteSelectedItem = data.result
                            self.rouletteCan = data.can
                            self.isShowRoulette = true
                        } else {
                            if let message = decoded.message {
                                self.errorMessage = message
                            } else {
                                self.errorMessage = "룰렛을 사용할 수 없습니다. 다시 시도해 주세요."
                            }
                            self.isShowErrorPopup = true
                        }
                    } catch {
                        self.errorMessage = "룰렛을 사용할 수 없습니다. 다시 시도해 주세요."
                        self.isShowErrorPopup = true
                    }
                }
                .store(in: &subscription)
        }
    }
    
    func sendRouletteDonation() {
        let rouletteRawMessage = LiveRoomChatRawMessage(
            type: .ROULETTE_DONATION,
            message: rouletteSelectedItem,
            can: rouletteCan,
            donationMessage: ""
        )
        
        self.agora.sendRawMessageToGroup(
            rawMessage: rouletteRawMessage,
            completion: { [unowned self] errorCode in
                if errorCode == .errorOk {
                    let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
                    self.messages.append(
                        LiveRoomRouletteDonationChat(
                            profileUrl: profileUrl,
                            nickname: nickname,
                            rouletteResult: rouletteSelectedItem
                        )
                    )
                    
                    totalDonationCan += rouletteCan
                    self.rouletteItems.removeAll()
                    self.rouletteSelectedItem = ""
                    self.rouletteCan = 0
                    
                    self.messageChangeFlag.toggle()
                    if self.messages.count > 100 {
                        self.messages.remove(at: 0)
                    }
                } else {
                    self.refundRouletteDonation()
                }
            },
            fail: { [unowned self] in
                self.refundRouletteDonation()
            }
        )
    }
    
    private func calculatePercentages(options: [RouletteItem]) -> [RoulettePreviewItem] {
        let updatedOptions = options.map { option in
            return RoulettePreviewItem(title: option.title, percent: "\(String(format: "%.2f", option.percentage))%")
        }
        
        return updatedOptions
    }
    
    private func refundRouletteDonation() {
        isLoading = true
        
        rouletteRepository.refundRouletteDonation(roomId: AppState.shared.roomId)
            .sink { result in
                switch result {
                case .finished:
                    DEBUG_LOG("finish")
                case .failure(let error):
                    ERROR_LOG(error.localizedDescription)
                }
            } receiveValue: { [unowned self] response in
                let responseData = response.data
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
                    
                    self.isLoading = false
                    
                    if decoded.success {
                        self.popupContent = "후원에 실패했습니다.\n다시 후원해주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
                        self.isShowPopup = true
                    } else {
                        if let message = decoded.message {
                            self.errorMessage = message
                        } else {
                            self.popupContent = "후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요."
                        }
                        
                        self.isShowPopup = true
                    }
                } catch {
                    self.isLoading = false
                    self.popupContent = "후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요."
                    self.isShowPopup = true
                }
            }
            .store(in: &subscription)
    }
    
    private func addSignatureImage(imageUrl: String) {
        if imageUrl.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 {
            if !isShowSignatureImage {
                isShowSignatureImage = true
                signatureImageUrl = imageUrl
            } else {
                signatureImageUrls.append(imageUrl)
            }
        }
    }
    
    private func addSignature(signature: LiveRoomDonationResponse?) {
        if let signature = signature {
            if !isShowSignatureImage {
                self.signature = signature
                isShowSignatureImage = true
            } else {
                self.signatureList.append(signature)
            }
        }
    }
    
    private func showSignatureImage() {
        if let signature = signature {
            DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(signature.time)) {
                if let nextSignature = self.signatureList.first {
                    self.signature = nextSignature
                    self.signatureList.removeFirst()
                } else {
                    self.signature = nil
                    self.isShowSignatureImage = false
                }
            }
        } else if signatureImageUrl.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 {
            DispatchQueue.main.asyncAfter(deadline: .now() + 7) {
                if let imageUrl = self.signatureImageUrls.first {
                    self.signatureImageUrl = imageUrl
                    self.signatureImageUrls.removeFirst()
                } else {
                    self.signatureImageUrl = ""
                    self.isShowSignatureImage = false
                }
            }
        }
    }
    
    func likeHeart() {
        if isAvailableLikeHeart {
            if !isLoadingLikeHeart {
                isLoadingLikeHeart = true
                
                repository.likeHeart(roomId: AppState.shared.roomId)
                    .sink { result in
                        switch result {
                        case .finished:
                            DEBUG_LOG("finish")
                        case .failure(let error):
                            ERROR_LOG(error.localizedDescription)
                        }
                    } receiveValue: { [unowned self] response in
                        self.isLoadingLikeHeart = false
                        let responseData = response.data
                        
                        do {
                            let jsonDecoder = JSONDecoder()
                            let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
                            
                            if decoded.success {
                                UserDefaults.set(UserDefaults.int(forKey: .can) - 1, forKey: .can)
                                
                                let donationRawMessage = LiveRoomChatRawMessage(
                                    type: .HEART_DONATION,
                                    message: "",
                                    can: 1,
                                    donationMessage: nil
                                )
                                
                                agora.sendRawMessageToGroup(
                                    rawMessage: donationRawMessage,
                                    completion: { [unowned self] errorCode in
                                        if errorCode == .errorOk {
                                            let (nickname, _) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
                                            self.messages.append(LiveRoomHeartDonationChat(nickname: nickname))
                                            
                                            totalHeartCount += 1
                                            addHeart()
                                            
                                            self.messageChangeFlag.toggle()
                                            if self.messages.count > 100 {
                                                self.messages.remove(at: 0)
                                            }
                                        } else {
                                            refundDonation()
                                        }
                                    },
                                    fail: { [unowned self] in
                                        refundDonation()
                                    }
                                )
                            }
                        } catch {
                            refundDonation()
                        }
                    }
                    .store(in: &subscription)
            }
        } else {
            isShowNoticeLikeHeart = true
        }
    }
    
    private func addHeart() {
        let heart = Heart(
            id: UUID(),
            offsetX: 0,
            offsetY: 0,
            opacity: 1,
            speed: CGFloat.random(in: 1...3),
            scale: 0.5,
            direction: Bool.random() ? "left" : "right"
        )
        hearts.append(heart)
        
        if hearts.count == 1 {
            startHeartTimer()
        }
    }
    
    private func updateHearts() {
        for i in (0..<hearts.count).reversed() {
            hearts[i].offsetY -= hearts[i].speed * 2 // Y축으로 이동
            hearts[i].opacity -= hearts[i].speed * 0.004444444444 // 투명도 감소
            hearts[i].scale += 0.0067
            
            if hearts[i].direction == "left" {
                hearts[i].offsetX -= 0.8
                
                if hearts[i].offsetX <= -22 {
                    hearts[i].direction = "right"
                }
            } else {
                hearts[i].offsetX += 0.8
                
                if hearts[i].offsetX >= 22 {
                    hearts[i].direction = "left"
                }
            }
            
            // 화면을 벗어나거나 완전히 사라진 하트는 삭제
            if hearts[i].scale >= 1 || hearts[i].opacity <= 0 || hearts[i].offsetY < -450 {
                hearts.remove(at: i)
                
                if hearts.isEmpty {
                    stopHeartTimer()
                }
            }
        }
        
        // 최대 하트 개수 제한
        if hearts.count > 100 {
            hearts.removeFirst()
        }
    }
    
    func startHeartTimer() {
        if heartTimer == nil {
            let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
            timer.schedule(deadline: .now(), repeating: 0.033) // 30 FPS
            timer.setEventHandler { [unowned self] in
                DispatchQueue.main.async {
                    self.updateHearts()
                }
            }
            timer.resume()
            self.heartTimer = timer
        }
    }
    
    func stopHeartTimer() {
        heartTimer?.cancel()
        heartTimer = nil
    }
}

extension LiveRoomViewModel: AgoraRtcEngineDelegate {
    func rtcEngine(_ engine: AgoraRtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo], totalVolume: Int) {
        let activeSpeakerIds = speakers
            .filter { $0.volume > 0 }
            .map { $0.uid }
        
        DEBUG_LOG("activeSpeakerIds::: \(activeSpeakerIds)")
        activeSpeakers.removeAll()
        activeSpeakers.append(contentsOf: activeSpeakerIds)
    }
    
    func rtcEngine(_ engine: AgoraRtcEngineKit, didAudioMuted muted: Bool, byUid uid: UInt) {
        if muted && !muteSpeakers.contains(uid){
            muteSpeakers.append(uid)
        } else {
            if let index = muteSpeakers.firstIndex(of: uid) {
                muteSpeakers.remove(at: index)
            }
        }
    }
    
    func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
        getRoomInfo()
    }
    
    func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) {
        DispatchQueue.main.async {[unowned self] in
            if uid == UInt(self.liveRoomInfo!.creatorId) {
                // 라이브 종료
                self.liveRoomInfo = nil
                self.errorMessage = "라이브가 종료되었습니다."
                self.isShowErrorPopup = true
            } else {
                // get room info
                self.getRoomInfo()
            }
        }
    }
}

extension LiveRoomViewModel: AgoraRtmDelegate {
    func rtmKit(_ kit: AgoraRtmKit, messageReceived message: AgoraRtmMessage, fromPeer peerId: String) {
        if message.type == .raw, let rawMessage = message as? AgoraRtmRawMessage {
            let rawMessageString = String(data: rawMessage.rawData, encoding: .utf8)
            
            DispatchQueue.main.async { [unowned self] in
                if rawMessageString == LiveRoomRequestType.CHANGE_LISTENER.rawValue {
                    self.setListener()
                    return
                }
                
                if rawMessageString == LiveRoomRequestType.REQUEST_SPEAKER.rawValue {
                    self.popupContent = "\(getUserNicknameAndProfileUrl(accountId: Int(peerId)!).nickname)님이 스피커 요청을 했어요!\n스퍼커로 초대할까요?"
                    self.popupCancelTitle = "건너뛰기"
                    self.popupCancelAction = {
                        self.isShowPopup = false
                    }
                    self.popupConfirmTitle = "스피커로 초대"
                    self.popupConfirmAction = {
                        self.isShowPopup = false
                        if self.liveRoomInfo!.speakerList.count <= 5 {
                            self.requestSpeakerAllow(peerId)
                        } else {
                            self.errorMessage = "스피커 정원이 초과되었습니다."
                            self.isShowErrorPopup = true
                        }
                    }
                    self.isShowPopup = true
                    return
                }
                
                if rawMessageString == LiveRoomRequestType.INVITE_SPEAKER.rawValue && self.role == .LISTENER {
                    self.popupContent = "스피커로 초대되었어요"
                    self.popupCancelTitle = "다음에요"
                    self.popupCancelAction = {
                        self.isShowPopup = false
                    }
                    self.popupConfirmTitle = "스피커로 참여하기"
                    self.popupConfirmAction = {
                        self.isShowPopup = false
                        self.setSpeaker()
                    }
                    self.isShowPopup = true
                    return
                }
                
                if rawMessageString == LiveRoomRequestType.REQUEST_SPEAKER_ALLOW.rawValue && self.role == .LISTENER {
                    self.setSpeaker()
                    return
                }
                
                if rawMessageString == LiveRoomRequestType.KICK_OUT.rawValue {
                    if let roomInfo = self.liveRoomInfo {
                        self.popupContent = "\(self.getUserNicknameAndProfileUrl(accountId: roomInfo.creatorId).nickname)님이 라이브에서 내보냈습니다."
                    } else {
                        self.popupContent = "방장님이 라이브에서 내보냈습니다."
                    }
                    self.isShowPopup = true
                    
                    DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [unowned self] in
                        self.quitRoom()
                    }
                    return
                }
                
                if rawMessageString == LiveRoomRequestType.SET_MANAGER.rawValue {
                    if self.role == .SPEAKER {
                        self.role = .LISTENER
                        self.isMute = false
                        self.agora.mute(isMute)
                        self.agora.setRole(role: .audience)
                    }
                    
                    if let roomInfo = self.liveRoomInfo {
                        self.popupContent = "\(self.getUserNicknameAndProfileUrl(accountId: roomInfo.creatorId).nickname)님이 스탭으로 지정했습니다."
                    } else {
                        self.popupContent = "방장님이 스탭으로 지정했습니다"
                    }
                    self.isShowPopup = true
                }
                
                if rawMessageString == LiveRoomRequestType.RELEASE_MANAGER.rawValue {
                    if let roomInfo = self.liveRoomInfo {
                        self.popupContent = "\(self.getUserNicknameAndProfileUrl(accountId: roomInfo.creatorId).nickname)님이 스탭에서 해제했습니다."
                    } else {
                        self.popupContent = "방장님이 스탭에서 해제했습니다."
                    }
                    self.isShowPopup = true
                }
                
                if rawMessageString == LiveRoomRequestType.NO_CHATTING.rawValue {
                    DispatchQueue.main.async {
                        self.addNoChatRoom()
                        self.startNoChatting()
                    }
                }
                
                do {
                    let jsonDecoder = JSONDecoder()
                    let decoded = try jsonDecoder.decode(LiveRoomChatRawMessage.self, from: rawMessage.rawData)
                    let (nickname, profileUrl) = getUserNicknameAndProfileUrl(accountId: Int(peerId)!)
                    
                    if decoded.type == .SECRET_DONATION {
                        self.messages.append(
                            LiveRoomDonationChat(
                                profileUrl: profileUrl,
                                nickname: nickname,
                                chat: decoded.message,
                                can: decoded.can,
                                donationMessage: decoded.donationMessage ?? ""
                            )
                        )
                        
                        self.totalDonationCan += decoded.can
                        
                        if let signature = decoded.signature {
                            self.addSignature(signature: signature)
                        } else if let imageUrl = decoded.signatureImageUrl {
                            self.addSignatureImage(imageUrl: imageUrl)
                        }
                    }
                } catch {
                }
            }
        }
    }
}

extension LiveRoomViewModel: AgoraRtmChannelDelegate {
    func channel(_ channel: AgoraRtmChannel, messageReceived message: AgoraRtmMessage, from member: AgoraRtmMember) {
        let (nickname, profileUrl) = getUserNicknameAndProfileUrl(accountId: Int(member.userId)!)
        
        if message.type == .raw, let rawMessage = message as? AgoraRtmRawMessage {
            do {
                let jsonDecoder = JSONDecoder()
                let decoded = try jsonDecoder.decode(LiveRoomChatRawMessage.self, from: rawMessage.rawData)
                
                if decoded.type == .DONATION {
                    self.messages.append(
                        LiveRoomDonationChat(
                            profileUrl: profileUrl,
                            nickname: nickname,
                            chat: decoded.message,
                            can: decoded.can,
                            donationMessage: decoded.donationMessage ?? ""
                        )
                    )
                    
                    self.totalDonationCan += decoded.can
                    
                    if let signature = decoded.signature {
                        self.addSignature(signature: signature)
                    } else if let imageUrl = decoded.signatureImageUrl {
                        self.addSignatureImage(imageUrl: imageUrl)
                    }
                } else if decoded.type == .ROULETTE_DONATION {
                    self.messages.append(
                        LiveRoomRouletteDonationChat(
                            profileUrl: profileUrl,
                            nickname: nickname,
                            rouletteResult: decoded.message
                        )
                    )
                    
                    self.totalDonationCan += decoded.can
                } else if decoded.type == .TOGGLE_ROULETTE && decoded.isActiveRoulette != nil {
                    self.isActiveRoulette = decoded.isActiveRoulette!
                } else if decoded.type == .EDIT_ROOM_INFO || decoded.type == .SET_MANAGER {
                    self.getRoomInfo()
                } else if decoded.type == .HEART_DONATION {
                    self.messages.append(LiveRoomHeartDonationChat(nickname: nickname))
                    self.totalHeartCount += decoded.can
                    self.addHeart()
                }
            } catch {
            }
        } else {
            let memberId = Int(member.userId) ?? 0
            let chat = message.text
            let rank = getUserRank(userId: memberId)
            
            if !chat.trimmingCharacters(in: .whitespaces).isEmpty && !blockedMemberIdList.contains(memberId) {
                messages.append(LiveRoomNormalChat(userId: memberId, profileUrl: profileUrl, nickname: nickname, rank: rank, chat: chat))
            }
        }
        
        DispatchQueue.main.async { [unowned self] in
            self.messageChangeFlag.toggle()
            if self.messages.count > 100 {
                self.messages.remove(at: 0)
            }
        }
    }
    
    func channel(_ channel: AgoraRtmChannel, memberJoined member: AgoraRtmMember) {
        getRoomInfo(userId: Int(member.userId)!) { [unowned self] nickname in
            if !nickname.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
                DispatchQueue.main.async { [unowned self] in
                    self.messages.append(LiveRoomJoinChat(nickname: nickname))
                    self.messageChangeFlag.toggle()
                    if self.messages.count > 100 {
                        self.messages.remove(at: 0)
                    }
                }
            }
        }
    }
    
    func channel(_ channel: AgoraRtmChannel, memberLeft member: AgoraRtmMember) {
        if let liveRoomInfo = liveRoomInfo, liveRoomInfo.creatorId != Int(member.userId)! {
            getRoomInfo()
        }
    }
}
