import ActivityKit
import AVFAudio
import Combine
import CryptoKit
import Foundation
import WebRTC

@MainActor
final class CallManager: NSObject, ObservableObject {

    static let shared = CallManager()

    @Published var uiState = CallUiState()

    var ownPublicKey: String = ""
    var role: CallRole?
    var roomId: String = ""
    var localPrivateKey: Curve25519.KeyAgreement.PrivateKey?
    var localPublicKeyHex: String = ""
    var sharedKey: Data?
    var offerSent = false
    var remoteDescriptionSet = false
    var lastPeerSharedPublicHex = ""

    var iceServers: [RTCIceServer] = []

    private var signalToken: UUID?
    private var webRtcToken: UUID?
    private var iceToken: UUID?

    var peerConnectionFactory: RTCPeerConnectionFactory?
    var peerConnection: RTCPeerConnection?
    var localAudioSource: RTCAudioSource?
    var localAudioTrack: RTCAudioTrack?
    var localAudioSender: RTCRtpSender?
    var bufferedRemoteCandidates: [RTCIceCandidate] = []
    var attachedReceiverIds: Set<String> = []

    var durationTask: Task<Void, Never>?
    var ringTimeoutTask: Task<Void, Never>?
    var liveActivity: Activity<CallActivityAttributes>?

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

    deinit {
        if let signalToken { ProtocolManager.shared.removeSignalPeerHandler(signalToken) }
        if let webRtcToken { ProtocolManager.shared.removeWebRtcHandler(webRtcToken) }
        if let iceToken { ProtocolManager.shared.removeIceServersHandler(iceToken) }
    }

    // MARK: - Public API

    func bindAccount(publicKey: String) {
        ownPublicKey = publicKey.trimmingCharacters(in: .whitespacesAndNewlines)
    }

    func onAuthenticated() {
        ProtocolManager.shared.requestIceServers()
    }

    func resetForSessionEnd() {
        finishCall(reason: nil, notifyPeer: false)
    }

    func startOutgoingCall(toPublicKey: String, title: String, username: String) -> CallActionResult {
        let target = toPublicKey.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !target.isEmpty, !DatabaseManager.isGroupDialogKey(target) else { return .invalidTarget }
        guard ownPublicKey.isEmpty == false else { return .accountNotBound }
        guard uiState.phase == .idle else { return .alreadyInCall }

        beginCallSession(peerPublicKey: target, title: title, username: username)
        role = .caller
        ensureLocalSessionKeys()
        uiState.phase = .outgoing
        uiState.statusText = "Calling..."

        ProtocolManager.shared.sendCallSignal(
            signalType: .call,
            src: ownPublicKey,
            dst: target
        )
        CallSoundManager.shared.playCalling()
        startRingTimeout()
        startLiveActivity()
        return .started
    }

    func acceptIncomingCall() -> CallActionResult {
        guard uiState.phase == .incoming else { return .notIncoming }
        guard ownPublicKey.isEmpty == false else { return .accountNotBound }
        guard uiState.peerPublicKey.isEmpty == false else { return .invalidTarget }

        cancelRingTimeout()
        CallSoundManager.shared.stopAll()
        role = .callee
        ensureLocalSessionKeys()
        guard localPublicKeyHex.isEmpty == false else { return .invalidTarget }

        ProtocolManager.shared.sendCallSignal(
            signalType: .keyExchange,
            src: ownPublicKey,
            dst: uiState.peerPublicKey,
            sharedPublic: localPublicKeyHex
        )

        uiState.phase = .keyExchange
        uiState.statusText = "Exchanging keys..."
        return .started
    }

    func declineIncomingCall() {
        guard uiState.phase == .incoming else { return }
        if ownPublicKey.isEmpty == false, uiState.peerPublicKey.isEmpty == false {
            ProtocolManager.shared.sendCallSignal(
                signalType: .endCall,
                src: ownPublicKey,
                dst: uiState.peerPublicKey
            )
        }
        finishCall(reason: nil, notifyPeer: false)
    }

    func endCall() {
        finishCall(reason: nil, notifyPeer: true)
    }

    func toggleMute() {
        let nextMuted = !uiState.isMuted
        uiState.isMuted = nextMuted
        localAudioTrack?.isEnabled = !nextMuted
        updateLiveActivity()
        print("[Call] toggleMute: isMuted=\(nextMuted), trackEnabled=\(localAudioTrack?.isEnabled ?? false), trackState=\(localAudioTrack?.readyState.rawValue ?? -1)")
    }

    func toggleSpeaker() {
        let nextSpeaker = !uiState.isSpeakerOn
        uiState.isSpeakerOn = nextSpeaker
        applyAudioOutputRouting()
        let route = AVAudioSession.sharedInstance().currentRoute
        print("[Call] toggleSpeaker: isSpeakerOn=\(nextSpeaker), outputs=\(route.outputs.map { $0.portName })")
    }

    // MARK: - Protocol handlers

    private func wireProtocolHandlers() {
        signalToken = ProtocolManager.shared.addSignalPeerHandler { [weak self] packet in
            Task { @MainActor [weak self] in
                self?.handleSignalPacket(packet)
            }
        }
        webRtcToken = ProtocolManager.shared.addWebRtcHandler { [weak self] packet in
            Task { @MainActor [weak self] in
                await self?.handleWebRtcPacket(packet)
            }
        }
        iceToken = ProtocolManager.shared.addIceServersHandler { [weak self] packet in
            Task { @MainActor [weak self] in
                self?.handleIceServersPacket(packet)
            }
        }
    }

    private func handleSignalPacket(_ packet: PacketSignalPeer) {
        switch packet.signalType {
        case .endCallBecauseBusy:
            finishCall(reason: "User is busy", notifyPeer: false)
            return
        case .endCallBecausePeerDisconnected:
            finishCall(reason: "Peer disconnected", notifyPeer: false)
            return
        case .endCall:
            finishCall(reason: "Call ended", notifyPeer: false)
            return
        default:
            break
        }

        if uiState.peerPublicKey.isEmpty == false, packet.src.isEmpty == false {
            if packet.src != uiState.peerPublicKey && packet.src != ownPublicKey {
                return
            }
        }

        switch packet.signalType {
        case .call:
            let incomingPeer = packet.src.trimmingCharacters(in: .whitespacesAndNewlines)
            guard incomingPeer.isEmpty == false else { return }
            guard uiState.phase == .idle else {
                ProtocolManager.shared.sendCallSignal(
                    signalType: .endCallBecauseBusy,
                    src: ownPublicKey,
                    dst: incomingPeer
                )
                return
            }
            beginCallSession(peerPublicKey: incomingPeer, title: "", username: "")
            role = .callee
            uiState.phase = .incoming
            uiState.statusText = "Incoming call..."
            hydratePeerIdentity(for: incomingPeer)
            CallSoundManager.shared.playRingtone()
            startRingTimeout()
            startLiveActivity()
        case .keyExchange:
            handleKeyExchange(packet)
        case .createRoom:
            let incomingRoomId = packet.roomId.trimmingCharacters(in: .whitespacesAndNewlines)
            guard incomingRoomId.isEmpty == false else { return }
            roomId = incomingRoomId
            uiState.phase = .webRtcExchange
            uiState.statusText = "Connecting..."
            Task { [weak self] in
                await self?.ensurePeerConnectionAndOffer()
            }
        case .activeCall:
            break
        case .endCall, .endCallBecausePeerDisconnected, .endCallBecauseBusy:
            break
        }
    }

    private func handleKeyExchange(_ packet: PacketSignalPeer) {
        let peerPublicHex = packet.sharedPublic.trimmingCharacters(in: .whitespacesAndNewlines)
        guard peerPublicHex.isEmpty == false else { return }
        if sharedKey != nil,
           peerPublicHex.caseInsensitiveCompare(lastPeerSharedPublicHex) == .orderedSame {
            return
        }
        lastPeerSharedPublicHex = peerPublicHex

        ensureLocalSessionKeys()
        guard let localPrivateKey else { return }
        guard let derivedSharedKey = CallMediaCrypto.deriveSharedKey(
            localPrivateKey: localPrivateKey,
            peerPublicHex: peerPublicHex
        ) else {
            return
        }

        sharedKey = derivedSharedKey
        uiState.keyCast = derivedSharedKey.hexString
        applySenderCryptorIfPossible()

        cancelRingTimeout()
        CallSoundManager.shared.stopAll()

        switch role {
        case .caller:
            ProtocolManager.shared.sendCallSignal(
                signalType: .keyExchange,
                src: ownPublicKey,
                dst: uiState.peerPublicKey,
                sharedPublic: localPublicKeyHex
            )
            ProtocolManager.shared.sendCallSignal(
                signalType: .createRoom,
                src: ownPublicKey,
                dst: uiState.peerPublicKey
            )
            uiState.phase = .webRtcExchange
            uiState.statusText = "Creating room..."
        case .callee:
            uiState.phase = .keyExchange
            uiState.statusText = "Waiting for room..."
        case .none:
            break
        }
    }

    private func handleIceServersPacket(_ packet: PacketIceServers) {
        let mapped = packet.iceServers.compactMap { server -> RTCIceServer? in
            let url = server.url.trimmingCharacters(in: .whitespacesAndNewlines)
            guard !url.isEmpty else { return nil }

            if url.hasPrefix("stun:") || url.hasPrefix("turn:") {
                return RTCIceServer(urlStrings: [url], username: server.username, credential: server.credential)
            }

            let transport = server.transport.trimmingCharacters(in: .whitespacesAndNewlines)
            if transport.isEmpty {
                return RTCIceServer(urlStrings: ["turn:\(url)"], username: server.username, credential: server.credential)
            }
            return RTCIceServer(urlStrings: ["turn:\(url)?transport=\(transport)"], username: server.username, credential: server.credential)
        }
        iceServers = mapped
    }

    // MARK: - Internal helpers used by delegate extension

    func setCallActiveIfNeeded() {
        guard uiState.phase != .active else { return }
        uiState.phase = .active
        uiState.statusText = "Call active"
        cancelRingTimeout()
        CallSoundManager.shared.playConnected()
        startDurationTimerIfNeeded()
        updateLiveActivity()
    }

    func startRingTimeout() {
        cancelRingTimeout()
        let isIncoming = uiState.phase == .incoming
        let timeout: Duration = isIncoming ? .seconds(45) : .seconds(60)
        ringTimeoutTask = Task { [weak self] in
            try? await Task.sleep(for: timeout)
            guard !Task.isCancelled else { return }
            guard let self else { return }
            // Verify phase hasn't changed during sleep
            if isIncoming, self.uiState.phase == .incoming {
                self.finishCall(reason: "No answer", notifyPeer: true)
            } else if !isIncoming, self.uiState.phase == .outgoing {
                self.endCall()
            }
        }
    }

    func cancelRingTimeout() {
        ringTimeoutTask?.cancel()
        ringTimeoutTask = nil
    }

    // MARK: - Live Activity

    func startLiveActivity() {
        // End any stale activities from previous calls / schema changes
        for activity in Activity<CallActivityAttributes>.activities {
            Task { await activity.end(nil, dismissalPolicy: .immediate) }
        }

        let authInfo = ActivityAuthorizationInfo()
        print("[Call] LiveActivity: areActivitiesEnabled=\(authInfo.areActivitiesEnabled), frequentPushesEnabled=\(authInfo.frequentPushesEnabled)")
        guard authInfo.areActivitiesEnabled else {
            print("[Call] LiveActivity DISABLED by user settings")
            return
        }
        // Create tiny avatar thumbnail (32x32) to embed directly in attributes (<4KB limit)
        var avatarThumb: Data?
        if let avatar = AvatarRepository.shared.loadAvatar(publicKey: uiState.peerPublicKey) {
            let thumbSize = CGSize(width: 32, height: 32)
            UIGraphicsBeginImageContextWithOptions(thumbSize, false, 1.0)
            avatar.draw(in: CGRect(origin: .zero, size: thumbSize))
            let tiny = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            if let jpeg = tiny?.jpegData(compressionQuality: 0.5) {
                avatarThumb = jpeg
                print("[Call] Avatar thumb: \(jpeg.count) bytes (32x32)")
            }
        } else {
            print("[Call] No avatar for peer")
        }
        let attributes = CallActivityAttributes(
            peerName: uiState.displayName,
            peerPublicKey: uiState.peerPublicKey,
            colorIndex: RosettaColors.avatarColorIndex(for: uiState.peerTitle, publicKey: uiState.peerPublicKey),
            avatarThumb: avatarThumb
        )
        let state = CallActivityAttributes.ContentState(
            durationSec: uiState.durationSec,
            isActive: uiState.phase == .active,
            isMuted: uiState.isMuted
        )
        print("[Call] LiveActivity starting: peerName=\(uiState.displayName), isActive=\(uiState.phase == .active)")
        do {
            liveActivity = try Activity.request(
                attributes: attributes,
                content: .init(state: state, staleDate: nil),
                pushType: nil
            )
            print("[Call] LiveActivity started: id=\(liveActivity?.id ?? "nil"), state=\(String(describing: liveActivity?.activityState))")
        } catch {
            print("[Call] LiveActivity FAILED: \(error)")
        }
    }

    func updateLiveActivity() {
        guard let liveActivity, liveActivity.activityState == .active else { return }
        // Avatar is embedded in attributes (set at startLiveActivity)
        let state = CallActivityAttributes.ContentState(
            durationSec: uiState.durationSec,
            isActive: uiState.phase == .active,
            isMuted: uiState.isMuted
        )
        Task {
            await liveActivity.update(.init(state: state, staleDate: nil))
        }
    }

    // Avatar is embedded directly in CallActivityAttributes (32x32 thumb)

    func endLiveActivity() {
        guard let liveActivity else { return }
        let finalState = CallActivityAttributes.ContentState(
            durationSec: uiState.durationSec,
            isActive: false,
            isMuted: false
        )
        Task {
            await liveActivity.end(.init(state: finalState, staleDate: nil), dismissalPolicy: .immediate)
        }
        self.liveActivity = nil
    }

    func attachReceiverCryptor(_ receiver: RTCRtpReceiver) {
        guard let sharedKey, sharedKey.count >= CallMediaCrypto.keyLength else { return }
        guard attachedReceiverIds.contains(receiver.receiverId) == false else { return }
        if WebRTCFrameCryptorBridge.attach(receiver, sharedKey: sharedKey) {
            attachedReceiverIds.insert(receiver.receiverId)
        }
    }

    func handleGeneratedCandidate(_ candidate: RTCIceCandidate) {
        let payload: [String: Any] = [
            "candidate": candidate.sdp,
            "sdpMid": candidate.sdpMid as Any,
            "sdpMLineIndex": Int(candidate.sdpMLineIndex),
        ]
        guard let data = try? JSONSerialization.data(withJSONObject: payload),
              let raw = String(data: data, encoding: .utf8) else {
            return
        }
        ProtocolManager.shared.sendWebRtcSignal(signalType: .iceCandidate, sdpOrCandidate: raw)
    }

    func handleIceConnectionStateChanged(_ state: RTCIceConnectionState) {
        switch state {
        case .connected, .completed:
            setCallActiveIfNeeded()
        case .failed, .closed, .disconnected:
            finishCall(reason: "Connection lost", notifyPeer: false)
        default:
            break
        }
    }
}

#if DEBUG
@MainActor
extension CallManager {
    /// Test-only hook to drive signal routing without a live transport.
    func testHandleSignalPacket(_ packet: PacketSignalPeer) {
        handleSignalPacket(packet)
    }

    /// Test-only helper for deterministic state setup in routing tests.
    func testSetUiState(_ state: CallUiState) {
        uiState = state
    }
}
#endif
