import AVFAudio
import CallKit
import os
import WebRTC

/// CallKit integration layer — wraps CXProvider and CXCallController.
/// Reports incoming/outgoing calls to the system so they appear in the native call UI,
/// integrate with CarPlay, and satisfy Apple's PushKit requirement (every VoIP push
/// MUST result in reportNewIncomingCall or the app gets terminated).
///
/// This class does NOT own call logic — it delegates to CallManager for actual call operations.
@MainActor
final class CallKitManager: NSObject {

    static let shared = CallKitManager()

    private static let logger = Logger(subsystem: "com.rosetta.messenger", category: "CallKit")

    private let provider: CXProvider
    private let callController = CXCallController()
    private(set) var currentCallUUID: UUID?

    /// Thread-safe UUID storage for synchronous PushKit access.
    /// Prevents race where WebSocket signal arrives before MainActor assigns currentCallUUID.
    /// Protected by uuidLock — accessed from nonisolated PushKit delegate methods.
    private nonisolated(unsafe) let uuidLock = NSLock()
    private nonisolated(unsafe) var _pendingCallUUID: UUID?

    private override init() {
        let config = CXProviderConfiguration()
        config.supportsVideo = false
        config.maximumCallsPerCallGroup = 1
        config.maximumCallGroups = 1
        config.supportedHandleTypes = [.generic]
        // Privacy: don't write peer public keys to system call log / iCloud.
        config.includesCallsInRecents = false
        provider = CXProvider(configuration: config)
        super.init()
        provider.setDelegate(self, queue: nil)
    }

    /// Thread-safe check if a call UUID is pending (set synchronously from PushKit).
    nonisolated func hasPendingCall() -> Bool {
        uuidLock.lock()
        let has = _pendingCallUUID != nil
        uuidLock.unlock()
        return has
    }

    // MARK: - Incoming Call (synchronous — for PushKit)

    /// Reports an incoming call to CallKit SYNCHRONOUSLY. Called directly from
    /// PushKit delegate (NOT via Task/@MainActor) to meet Apple's requirement
    /// that reportNewIncomingCall is invoked before the PushKit handler returns.
    nonisolated func reportIncomingCallSynchronously(
        callerKey: String,
        callerName: String,
        callId: String? = nil,
        completion: @escaping (Error?) -> Void
    ) {
        // Guard: if WebSocket already reported this call via reportIncomingCall(),
        // skip to prevent duplicate CallKit calls. Two calls with different UUIDs
        // cause CallKit (maximumCallsPerCallGroup=1) to auto-end the first one,
        // which sends endCall signal to desktop and drops the call.
        uuidLock.lock()
        let alreadyPending = _pendingCallUUID != nil
        uuidLock.unlock()

        if alreadyPending {
            Self.logger.info("Skipping duplicate CallKit report — call already pending from WebSocket")
            completion(nil)
            return
        }

        // Stable UUID from server callId (Telegram parity) — prevents UUID mismatch
        // between PushKit and WebSocket paths for the same call.
        let uuid: UUID = {
            if let callId, let parsed = UUID(uuidString: callId) {
                return parsed
            }
            return UUID()
        }()

        // Assign UUID synchronously to prevent race with WebSocket signal.
        uuidLock.lock()
        _pendingCallUUID = uuid
        uuidLock.unlock()

        let handleValue = callerName.isEmpty
            ? String(callerKey.prefix(7))
            : callerName

        let update = CXCallUpdate()
        update.remoteHandle = CXHandle(type: .generic, value: handleValue)
        update.localizedCallerName = callerName.isEmpty ? "Rosetta" : callerName
        update.hasVideo = false
        update.supportsHolding = false
        update.supportsGrouping = false
        update.supportsUngrouping = false
        update.supportsDTMF = false

        provider.reportNewIncomingCall(with: uuid, update: update) { [weak self] error in
            if let error {
                Self.logger.error("Failed to report incoming call: \(error.localizedDescription)")
                self?.uuidLock.lock()
                self?._pendingCallUUID = nil
                self?.uuidLock.unlock()
            } else {
                Self.logger.info("Incoming call reported to CallKit (uuid=\(uuid.uuidString.prefix(8)))")
            }
            // Assign to MainActor-isolated property.
            // Guard: only assign if no newer UUID exists — WebSocket signal
            // may have already reported a different call by the time this runs.
            Task { @MainActor in
                if error == nil, self?.currentCallUUID == nil {
                    self?.currentCallUUID = uuid
                }
            }
            completion(error)
        }
    }

    // MARK: - Incoming Call (from WebSocket signal)

    /// Reports an incoming call to CallKit. Called from CallManager when a `.call`
    /// signal arrives via WebSocket (app already running, MainActor available).
    func reportIncomingCall(
        callerKey: String,
        callerName: String,
        completion: ((Error?) -> Void)? = nil
    ) {
        let uuid = currentCallUUID ?? UUID()
        currentCallUUID = uuid

        // Sync _pendingCallUUID so VoIP push path can see we already have a call.
        // Without this, VoIP push creates a SECOND CallKit call with a different UUID,
        // and CallKit auto-ends the first one → sends endCall to desktop.
        uuidLock.lock()
        _pendingCallUUID = uuid
        uuidLock.unlock()

        // Use display-friendly handle value — CallKit shows this on lock screen.
        // Full public key is ugly; short prefix matches Android parity (7 chars).
        let handleValue = callerName.isEmpty
            ? String(callerKey.prefix(7))
            : callerName

        let update = CXCallUpdate()
        update.remoteHandle = CXHandle(type: .generic, value: handleValue)
        update.localizedCallerName = callerName.isEmpty ? "Rosetta" : callerName
        update.hasVideo = false
        update.supportsHolding = false
        update.supportsGrouping = false
        update.supportsUngrouping = false
        update.supportsDTMF = false

        provider.reportNewIncomingCall(with: uuid, update: update) { [weak self] error in
            if let error {
                Self.logger.error("Failed to report incoming call: \(error.localizedDescription)")
                Task { @MainActor in
                    self?.currentCallUUID = nil
                }
            } else {
                Self.logger.info("Incoming call reported to CallKit (uuid=\(uuid.uuidString.prefix(8)))")
            }
            completion?(error)
        }
    }

    /// Updates the caller display name on an active incoming call.
    /// Called when hydratePeerIdentity resolves a name that wasn't available during
    /// initial reportIncomingCall (e.g. DB not yet loaded on cold start from VoIP push).
    func updateCallerName(_ name: String) {
        guard !name.isEmpty else { return }
        // Use currentCallUUID if available; fall back to _pendingCallUUID
        // for PushKit race: currentCallUUID is set async via Task { @MainActor },
        // but _pendingCallUUID is set synchronously in reportIncomingCallSynchronously().
        // hydratePeerIdentity may run before the async Task assigns currentCallUUID.
        let uuid: UUID? = currentCallUUID ?? {
            uuidLock.lock()
            let pending = _pendingCallUUID
            uuidLock.unlock()
            return pending
        }()
        guard let uuid else { return }
        let update = CXCallUpdate()
        update.localizedCallerName = name
        update.remoteHandle = CXHandle(type: .generic, value: name)
        provider.reportCall(with: uuid, updated: update)
    }

    // MARK: - Outgoing Call

    func startOutgoingCall(peerKey: String, displayName: String) {
        let uuid = UUID()
        currentCallUUID = uuid

        let name = displayName.isEmpty ? "Rosetta" : displayName
        let handle = CXHandle(type: .generic, value: name)
        let action = CXStartCallAction(call: uuid, handle: handle)
        action.isVideo = false

        callController.request(CXTransaction(action: action)) { error in
            if let error {
                Self.logger.error("Failed to start outgoing call: \(error.localizedDescription)")
            }
        }

        // Report CXCallUpdate with localizedCallerName so CarPlay shows the
        // contact name instead of the raw public key.
        let update = CXCallUpdate()
        update.localizedCallerName = name
        update.remoteHandle = handle
        provider.reportCall(with: uuid, updated: update)

        provider.reportOutgoingCall(with: uuid, startedConnectingAt: nil)
    }

    func reportOutgoingCallConnected() {
        guard let uuid = currentCallUUID else { return }
        provider.reportOutgoingCall(with: uuid, connectedAt: nil)
    }

    // MARK: - End Call

    func endCall() {
        // Fall back to _pendingCallUUID — currentCallUUID is set asynchronously
        // via Task { @MainActor } in reportIncomingCallSynchronously completion.
        // On background PushKit calls, the Task may not have run yet when
        // finishCall() triggers endCall() → "no UUID" → CallKit never learns
        // the call ended → didDeactivate never fires → audio session stuck.
        let uuid: UUID? = currentCallUUID ?? {
            uuidLock.lock()
            let pending = _pendingCallUUID
            uuidLock.unlock()
            return pending
        }()
        guard let uuid else {
            Self.logger.notice("CallKit.endCall: no UUID — skipping")
            return
        }
        Self.logger.notice("CallKit.endCall uuid=\(uuid.uuidString.prefix(8), privacy: .public)")
        currentCallUUID = nil
        uuidLock.lock()
        _pendingCallUUID = nil
        uuidLock.unlock()

        let action = CXEndCallAction(call: uuid)
        callController.request(CXTransaction(action: action)) { error in
            if let error {
                Self.logger.warning("CXEndCallAction failed: \(error.localizedDescription)")
                self.provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
            }
        }
    }

    func reportCallEndedByRemote(reason: CXCallEndedReason = .remoteEnded) {
        let uuid: UUID? = currentCallUUID ?? {
            uuidLock.lock()
            let pending = _pendingCallUUID
            uuidLock.unlock()
            return pending
        }()
        guard let uuid else { return }
        Self.logger.notice("CallKit.reportCallEndedByRemote reason=\(reason.rawValue, privacy: .public) uuid=\(uuid.uuidString.prefix(8), privacy: .public)")
        currentCallUUID = nil
        uuidLock.lock()
        _pendingCallUUID = nil
        uuidLock.unlock()
        provider.reportCall(with: uuid, endedAt: nil, reason: reason)
    }

    // MARK: - Mute

    func setMuted(_ muted: Bool) {
        guard let uuid = currentCallUUID else { return }
        let action = CXSetMutedCallAction(call: uuid, muted: muted)
        callController.request(CXTransaction(action: action)) { error in
            if let error {
                Self.logger.warning("CXSetMutedCallAction failed: \(error.localizedDescription)")
            }
        }
    }
}

// MARK: - CXProviderDelegate

extension CallKitManager: CXProviderDelegate {

    nonisolated func providerDidReset(_ provider: CXProvider) {
        Self.logger.info("CXProvider did reset")
        Task { @MainActor in
            self.currentCallUUID = nil
            self.uuidLock.lock()
            self._pendingCallUUID = nil
            self.uuidLock.unlock()
            // notifyPeer: false — provider reset is system-initiated, peer connection
            // is already gone. Sending endCall signal would be spurious.
            CallManager.shared.finishCall(reason: nil, notifyPeer: false)
        }
    }

    nonisolated func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        Self.logger.info("CXAnswerCallAction — user tapped Accept")
        action.fulfill()
        Task { @MainActor in
            let phase = CallManager.shared.uiState.phase
            let mode = CallManager.shared.signalingMode
            let wsState = ProtocolManager.shared.connectionState
            Self.logger.info("CXAnswerCallAction: phase=\(phase.rawValue, privacy: .public) mode=\(mode.rawValue, privacy: .public) wsState=\(String(describing: wsState), privacy: .public)")

            let result = CallManager.shared.acceptIncomingCall()
            if result == .started {
                Self.logger.info("CXAnswerCallAction: accepted → keyExchange")
            } else {
                // Phase not .incoming yet — WebSocket hasn't delivered .call signal.
                // Set pending flag so handleSignalPacket(.call) auto-accepts when it arrives.
                Self.logger.info("CXAnswerCallAction: pending (result=\(String(describing: result), privacy: .public), waiting for WebSocket)")
                CallManager.shared.pendingCallKitAccept = true
            }
        }
    }

    nonisolated func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
        Self.logger.info("CXEndCallAction")
        action.fulfill()
        Task { @MainActor in
            self.currentCallUUID = nil
            self.uuidLock.lock()
            self._pendingCallUUID = nil
            self.uuidLock.unlock()
            CallManager.shared.endCall()
        }
    }

    nonisolated func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
        Task { @MainActor in
            if CallManager.shared.uiState.isMuted != action.isMuted {
                CallManager.shared.toggleMute()
            }
            action.fulfill()
        }
    }

    nonisolated func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
        Self.logger.info("CXStartCallAction")
        action.fulfill()
    }

    nonisolated func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
        Self.logger.info("Audio session activated by CallKit — category=\(audioSession.category.rawValue, privacy: .public) mode=\(audioSession.mode.rawValue, privacy: .public)")
        let rtcSession = RTCAudioSession.sharedInstance()
        // 1. Tell WebRTC the session is active.
        rtcSession.audioSessionDidActivate(audioSession)
        // 2. Toggle isAudioEnabled OFF first to force ADM restart.
        //    If force-activation retry loop already ran and ADM entered a
        //    permanent failed state (cached bad activation), this toggle
        //    forces a fresh AudioUnit initialization with the REAL
        //    CallKit-blessed audio session.
        rtcSession.isAudioEnabled = false
        // 3. Configure category BEFORE re-enabling audio.
        rtcSession.lockForConfiguration()
        do {
            try rtcSession.setCategory(
                .playAndRecord, mode: .voiceChat,
                options: [.allowBluetooth, .defaultToSpeaker]
            )
        } catch {
            Self.logger.error("Failed to set audio category: \(error.localizedDescription)")
        }
        rtcSession.unlockForConfiguration()
        // 4. Re-enable audio — ADM reinitializes from scratch.
        rtcSession.isAudioEnabled = true
        // ALWAYS run onAudioSessionActivated — it is idempotent.
        // If force-activation already ran but failed (setActive threw), this is
        // the first REAL activation with CallKit's audio entitlement.
        // If force-activation succeeded, this re-applies routing and re-enables
        // the audio track with the proper CallKit-blessed session — fixes cases
        // where force-activated audio was silent.
        Task { @MainActor in
            await CallManager.shared.onAudioSessionActivated()
        }
    }

    nonisolated func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
        Self.logger.info("Audio session deactivated by CallKit")
        let rtcSession = RTCAudioSession.sharedInstance()
        rtcSession.audioSessionDidDeactivate(audioSession)
        rtcSession.isAudioEnabled = false
        Task { @MainActor in
            CallManager.shared.didReceiveCallKitDeactivation = true
        }
    }
}
