package com.seven.ikar.kotlin.feature.call

import android.content.Context
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.os.Build
import com.seven.ikar.kotlin.core.model.CallAnswerDto
import com.seven.ikar.kotlin.core.model.CallIceCandidateDto
import com.seven.ikar.kotlin.core.model.CallIceServerDto
import com.seven.ikar.kotlin.core.model.CallMediaType
import com.seven.ikar.kotlin.core.model.CallOfferDto
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.suspendCancellableCoroutine
import org.webrtc.AudioSource
import org.webrtc.AudioTrack
import org.webrtc.Camera1Enumerator
import org.webrtc.Camera2Enumerator
import org.webrtc.CameraEnumerator
import org.webrtc.CameraVideoCapturer
import org.webrtc.DataChannel
import org.webrtc.DefaultVideoDecoderFactory
import org.webrtc.DefaultVideoEncoderFactory
import org.webrtc.EglBase
import org.webrtc.IceCandidate
import org.webrtc.MediaConstraints
import org.webrtc.MediaStream
import org.webrtc.MediaStreamTrack
import org.webrtc.PeerConnection
import org.webrtc.PeerConnectionFactory
import org.webrtc.RtpReceiver
import org.webrtc.RtpTransceiver
import org.webrtc.SdpObserver
import org.webrtc.SessionDescription
import org.webrtc.SurfaceTextureHelper
import org.webrtc.VideoCapturer
import org.webrtc.VideoSource
import org.webrtc.VideoTrack
import org.webrtc.audio.JavaAudioDeviceModule
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

data class CallVideoRenderState(
    val eglBaseContext: EglBase.Context?,
    val localVideoTrack: VideoTrack? = null,
    val remoteVideoTrack: VideoTrack? = null
)

class AndroidWebRtcCallService(
    context: Context
) {
    private val appContext = context.applicationContext
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
    private val audioManager = appContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
    private val mutex = Mutex()
    private val eglBase: EglBase = EglBase.create()
    private val peerConnectionFactory: PeerConnectionFactory by lazy(::createPeerConnectionFactory)

    private var peerConnection: PeerConnection? = null
    private var callId: String? = null
    private var mediaType: CallMediaType? = null
    private var audioSource: AudioSource? = null
    private var localAudioTrack: AudioTrack? = null
    private var videoSource: VideoSource? = null
    private var localVideoTrack: VideoTrack? = null
    private var remoteVideoTrack: VideoTrack? = null
    private var surfaceTextureHelper: SurfaceTextureHelper? = null
    private var videoCapturer: VideoCapturer? = null
    private var sendOffer: (suspend (CallOfferDto) -> Unit)? = null
    private var sendAnswer: (suspend (CallAnswerDto) -> Unit)? = null
    private var sendIceCandidate: (suspend (CallIceCandidateDto) -> Unit)? = null
    private var remoteDescriptionApplied = false
    private var pendingRemoteOffer: CallOfferDto? = null
    private var pendingRemoteAnswer: CallAnswerDto? = null
    private val pendingRemoteIceCandidates = mutableListOf<CallIceCandidateDto>()
    private var previousAudioMode: Int = AudioManager.MODE_NORMAL
    private var previousSpeakerphoneState: Boolean = false
    private var previousCommunicationDeviceType: Int? = null

    var onRenderStateChanged: ((CallVideoRenderState?) -> Unit)? = null
    var onConnectionFailure: ((String) -> Unit)? = null

    suspend fun start(
        callId: String,
        mediaType: CallMediaType,
        iceServers: List<CallIceServerDto>,
        shouldCreateOffer: Boolean,
        sendOffer: suspend (CallOfferDto) -> Unit,
        sendAnswer: suspend (CallAnswerDto) -> Unit,
        sendIceCandidate: suspend (CallIceCandidateDto) -> Unit
    ) {
        mutex.withLock {
            stopInternal()

            this.callId = callId
            this.mediaType = mediaType
            this.sendOffer = sendOffer
            this.sendAnswer = sendAnswer
            this.sendIceCandidate = sendIceCandidate
            remoteDescriptionApplied = false
            previousAudioMode = audioManager.mode
            previousSpeakerphoneState = audioManager.isSpeakerphoneOn
            previousCommunicationDeviceType =
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                    audioManager.communicationDevice?.type
                } else {
                    null
                }

            preparePeerConnection(iceServers)
            prepareLocalMedia(mediaType)
            applyAudioRoutingForCurrentMediaType()
            publishRenderState()

            if (shouldCreateOffer) {
                createAndSendOffer(callId)
            } else {
                pendingRemoteOffer?.let {
                    pendingRemoteOffer = null
                    handleRemoteOfferLocked(it)
                }
            }

            pendingRemoteAnswer?.let {
                pendingRemoteAnswer = null
                handleRemoteAnswerLocked(it)
            }
        }
    }

    suspend fun handleRemoteOffer(payload: CallOfferDto) {
        mutex.withLock {
            if (callId != payload.callId || peerConnection == null) {
                pendingRemoteOffer = payload
                return
            }

            handleRemoteOfferLocked(payload)
        }
    }

    suspend fun handleRemoteAnswer(payload: CallAnswerDto) {
        mutex.withLock {
            if (callId != payload.callId || peerConnection == null) {
                pendingRemoteAnswer = payload
                return
            }

            handleRemoteAnswerLocked(payload)
        }
    }

    suspend fun handleRemoteIceCandidate(payload: CallIceCandidateDto) {
        mutex.withLock {
            if (callId != payload.callId || peerConnection == null) {
                pendingRemoteIceCandidates += payload
                return
            }

            if (!remoteDescriptionApplied) {
                pendingRemoteIceCandidates += payload
                return
            }

            peerConnection?.addIceCandidate(payload.toNative())
        }
    }

    suspend fun stop() {
        mutex.withLock {
            stopInternal()
        }
    }

    private fun preparePeerConnection(iceServers: List<CallIceServerDto>) {
        val rtcConfiguration = PeerConnection.RTCConfiguration(
            iceServers.map { it.toNative() }
        ).apply {
            sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
            continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY
        }
        val createdPeerConnection = peerConnectionFactory.createPeerConnection(
            rtcConfiguration,
            object : PeerConnection.Observer {
                override fun onSignalingChange(newState: PeerConnection.SignalingState?) = Unit

                override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) = Unit

                override fun onIceConnectionReceivingChange(receiving: Boolean) = Unit

                override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState?) = Unit

                override fun onIceCandidate(candidate: IceCandidate?) {
                    val activeCallId = callId ?: return
                    val activeCandidate = candidate ?: return
                    val sender = sendIceCandidate ?: return
                    scope.launch {
                        sender(
                            CallIceCandidateDto(
                                callId = activeCallId,
                                candidate = activeCandidate.sdp,
                                sdpMid = activeCandidate.sdpMid,
                                sdpMLineIndex = activeCandidate.sdpMLineIndex
                            )
                        )
                    }
                }

                override fun onIceCandidatesRemoved(candidates: Array<out IceCandidate>?) = Unit

                override fun onAddStream(stream: MediaStream?) = Unit

                override fun onRemoveStream(stream: MediaStream?) = Unit

                override fun onDataChannel(dataChannel: DataChannel?) = Unit

                override fun onRenegotiationNeeded() = Unit

                override fun onAddTrack(receiver: RtpReceiver?, mediaStreams: Array<out MediaStream>?) {
                    receiver?.track()?.let(::handleRemoteTrack)
                }

                override fun onTrack(transceiver: RtpTransceiver?) {
                    transceiver?.receiver?.track()?.let(::handleRemoteTrack)
                }

                override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) {
                    if (newState == PeerConnection.PeerConnectionState.FAILED ||
                        newState == PeerConnection.PeerConnectionState.CLOSED
                    ) {
                        onConnectionFailure?.invoke("WebRTC соединение завершилось: $newState")
                    }
                }
            }
        )
        check(createdPeerConnection != null) { "Не удалось создать PeerConnection." }
        peerConnection = createdPeerConnection
    }

    private suspend fun handleRemoteOfferLocked(payload: CallOfferDto) {
        val activePeerConnection = requireNotNull(peerConnection) { "PeerConnection не создан." }
        setRemoteDescription(activePeerConnection, payload.toSessionDescription())
        remoteDescriptionApplied = true
        flushPendingIceCandidatesLocked()
        val answer = createAnswer(activePeerConnection, mediaType == CallMediaType.Video)
        setLocalDescription(activePeerConnection, answer)
        val sender = requireNotNull(sendAnswer)
        sender(CallAnswerDto(payload.callId, answer.type.canonicalForm(), answer.description))
    }

    private suspend fun handleRemoteAnswerLocked(payload: CallAnswerDto) {
        val activePeerConnection = requireNotNull(peerConnection) { "PeerConnection не создан." }
        setRemoteDescription(activePeerConnection, payload.toSessionDescription())
        remoteDescriptionApplied = true
        flushPendingIceCandidatesLocked()
    }

    private suspend fun flushPendingIceCandidatesLocked() {
        val activePeerConnection = peerConnection ?: return
        val iterator = pendingRemoteIceCandidates.toList()
        pendingRemoteIceCandidates.clear()
        iterator.forEach { candidate ->
            activePeerConnection.addIceCandidate(candidate.toNative())
        }
    }

    private fun prepareLocalMedia(mediaType: CallMediaType) {
        val activePeerConnection = requireNotNull(peerConnection) { "PeerConnection не создан." }
        val newAudioSource = peerConnectionFactory.createAudioSource(MediaConstraints())
        val newLocalAudioTrack = peerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, newAudioSource)
        activePeerConnection.addTrack(newLocalAudioTrack)
        audioSource = newAudioSource
        localAudioTrack = newLocalAudioTrack

        if (mediaType != CallMediaType.Video) {
            return
        }

        val capturer = createVideoCapturer()
        val helper = SurfaceTextureHelper.create("IkarCaptureThread", eglBase.eglBaseContext)
        val newVideoSource = peerConnectionFactory.createVideoSource(capturer.isScreencast)
        capturer.initialize(helper, appContext, newVideoSource.capturerObserver)
        capturer.startCapture(VIDEO_WIDTH, VIDEO_HEIGHT, VIDEO_FPS)
        val newLocalVideoTrack = peerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, newVideoSource)
        activePeerConnection.addTrack(newLocalVideoTrack)
        videoCapturer = capturer
        surfaceTextureHelper = helper
        videoSource = newVideoSource
        localVideoTrack = newLocalVideoTrack
    }

    private suspend fun createAndSendOffer(callId: String) {
        val activePeerConnection = requireNotNull(peerConnection) { "PeerConnection не создан." }
        val offer = createOffer(activePeerConnection, mediaType == CallMediaType.Video)
        setLocalDescription(activePeerConnection, offer)
        val sender = requireNotNull(sendOffer)
        sender(CallOfferDto(callId, offer.type.canonicalForm(), offer.description))
    }

    private fun handleRemoteTrack(track: MediaStreamTrack) {
        when (track) {
            is AudioTrack -> {
                track.setEnabled(true)
                applyAudioRoutingForCurrentMediaType()
            }
            is VideoTrack -> {
                remoteVideoTrack = track
                applyAudioRoutingForCurrentMediaType()
                publishRenderState()
            }
        }
    }

    private fun publishRenderState() {
        onRenderStateChanged?.invoke(
            if (mediaType == CallMediaType.Video) {
                CallVideoRenderState(
                    eglBaseContext = eglBase.eglBaseContext,
                    localVideoTrack = localVideoTrack,
                    remoteVideoTrack = remoteVideoTrack
                )
            } else {
                null
            }
        )
    }

    private fun stopInternal() {
        pendingRemoteOffer = null
        pendingRemoteAnswer = null
        pendingRemoteIceCandidates.clear()
        remoteDescriptionApplied = false

        runCatching { videoCapturer?.stopCapture() }
        runCatching { videoCapturer?.dispose() }
        runCatching { localVideoTrack?.dispose() }
        runCatching { videoSource?.dispose() }
        runCatching { surfaceTextureHelper?.dispose() }
        runCatching { localAudioTrack?.dispose() }
        runCatching { audioSource?.dispose() }
        runCatching { peerConnection?.dispose() }

        peerConnection = null
        callId = null
        mediaType = null
        audioSource = null
        localAudioTrack = null
        videoSource = null
        localVideoTrack = null
        remoteVideoTrack = null
        surfaceTextureHelper = null
        videoCapturer = null
        sendOffer = null
        sendAnswer = null
        sendIceCandidate = null

        restorePreviousAudioRouting()
        audioManager.mode = previousAudioMode
        publishRenderState()
    }

    private fun applyAudioRoutingForCurrentMediaType() {
        audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
        audioManager.isMicrophoneMute = false

        if (mediaType == CallMediaType.Video) {
            runCatching { audioManager.stopBluetoothSco() }
            runCatching { audioManager.setBluetoothScoOn(false) }
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                val speakerDevice = audioManager.getAvailableCommunicationDevices().firstOrNull {
                    it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER ||
                        it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER_SAFE
                }
                if (speakerDevice != null) {
                    audioManager.setCommunicationDevice(speakerDevice)
                } else {
                    audioManager.clearCommunicationDevice()
                }
            }
            audioManager.isSpeakerphoneOn = true
        } else {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                audioManager.clearCommunicationDevice()
            }
            audioManager.isSpeakerphoneOn = false
        }
    }

    private fun restorePreviousAudioRouting() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            val previousDevice = previousCommunicationDeviceType?.let { previousType ->
                audioManager.getAvailableCommunicationDevices().firstOrNull { it.type == previousType }
            }
            if (previousDevice != null) {
                audioManager.setCommunicationDevice(previousDevice)
            } else {
                audioManager.clearCommunicationDevice()
            }
        }
        audioManager.isSpeakerphoneOn = previousSpeakerphoneState
        previousCommunicationDeviceType = null
    }

    private fun createPeerConnectionFactory(): PeerConnectionFactory {
        ensureFactoryInitialized(appContext)
        val audioDeviceModule = JavaAudioDeviceModule.builder(appContext)
            .setUseHardwareAcousticEchoCanceler(true)
            .setUseHardwareNoiseSuppressor(true)
            .createAudioDeviceModule()

        return try {
            PeerConnectionFactory.builder()
                .setAudioDeviceModule(audioDeviceModule)
                .setVideoEncoderFactory(DefaultVideoEncoderFactory(eglBase.eglBaseContext, true, true))
                .setVideoDecoderFactory(DefaultVideoDecoderFactory(eglBase.eglBaseContext))
                .createPeerConnectionFactory()
        } finally {
            audioDeviceModule.release()
        }
    }

    private fun createVideoCapturer(): VideoCapturer {
        val enumerator: CameraEnumerator =
            if (Camera2Enumerator.isSupported(appContext)) {
                Camera2Enumerator(appContext)
            } else {
                Camera1Enumerator(true)
            }

        val preferredDevice = enumerator.deviceNames.firstOrNull { enumerator.isFrontFacing(it) }
            ?: enumerator.deviceNames.firstOrNull()
            ?: error("На устройстве нет камеры для видеозвонка.")

        return (enumerator.createCapturer(preferredDevice, null) as? CameraVideoCapturer)
            ?: error("Не удалось подготовить видеокапчер для звонка.")
    }

    private suspend fun createOffer(
        peerConnection: PeerConnection,
        offerToReceiveVideo: Boolean
    ): SessionDescription =
        suspendCancellableCoroutine { continuation ->
            peerConnection.createOffer(object : SdpObserver {
                override fun onCreateSuccess(description: SessionDescription?) {
                    val activeDescription = description ?: return continuation.resumeWithException(
                        IllegalStateException("WebRTC offer не был создан.")
                    )
                    continuation.resume(activeDescription)
                }

                override fun onSetSuccess() = Unit

                override fun onCreateFailure(error: String?) {
                    continuation.resumeWithException(IllegalStateException(error ?: "Не удалось создать WebRTC offer."))
                }

                override fun onSetFailure(error: String?) = Unit
            }, buildSdpConstraints(offerToReceiveVideo))
        }

    private suspend fun createAnswer(
        peerConnection: PeerConnection,
        offerToReceiveVideo: Boolean
    ): SessionDescription =
        suspendCancellableCoroutine { continuation ->
            peerConnection.createAnswer(object : SdpObserver {
                override fun onCreateSuccess(description: SessionDescription?) {
                    val activeDescription = description ?: return continuation.resumeWithException(
                        IllegalStateException("WebRTC answer не был создан.")
                    )
                    continuation.resume(activeDescription)
                }

                override fun onSetSuccess() = Unit

                override fun onCreateFailure(error: String?) {
                    continuation.resumeWithException(IllegalStateException(error ?: "Не удалось создать WebRTC answer."))
                }

                override fun onSetFailure(error: String?) = Unit
            }, buildSdpConstraints(offerToReceiveVideo))
        }

    private suspend fun setLocalDescription(
        peerConnection: PeerConnection,
        description: SessionDescription
    ) {
        suspendCancellableCoroutine<Unit> { continuation ->
            peerConnection.setLocalDescription(object : SdpObserver {
                override fun onCreateSuccess(description: SessionDescription?) = Unit

                override fun onSetSuccess() {
                    continuation.resume(Unit)
                }

                override fun onCreateFailure(error: String?) = Unit

                override fun onSetFailure(error: String?) {
                    continuation.resumeWithException(
                        IllegalStateException(error ?: "Не удалось установить local description.")
                    )
                }
            }, description)
        }
    }

    private suspend fun setRemoteDescription(
        peerConnection: PeerConnection,
        description: SessionDescription
    ) {
        suspendCancellableCoroutine<Unit> { continuation ->
            peerConnection.setRemoteDescription(object : SdpObserver {
                override fun onCreateSuccess(description: SessionDescription?) = Unit

                override fun onSetSuccess() {
                    continuation.resume(Unit)
                }

                override fun onCreateFailure(error: String?) = Unit

                override fun onSetFailure(error: String?) {
                    continuation.resumeWithException(
                        IllegalStateException(error ?: "Не удалось установить remote description.")
                    )
                }
            }, description)
        }
    }

    private fun buildSdpConstraints(offerToReceiveVideo: Boolean): MediaConstraints =
        MediaConstraints().apply {
            mandatory += MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")
            mandatory += MediaConstraints.KeyValuePair(
                "OfferToReceiveVideo",
                if (offerToReceiveVideo) "true" else "false"
            )
        }

    private fun CallIceServerDto.toNative(): PeerConnection.IceServer =
        PeerConnection.IceServer.builder(urls)
            .setUsername(username.orEmpty())
            .setPassword(credential.orEmpty())
            .createIceServer()

    private fun CallOfferDto.toSessionDescription(): SessionDescription =
        SessionDescription(SessionDescription.Type.fromCanonicalForm(type), sdp)

    private fun CallAnswerDto.toSessionDescription(): SessionDescription =
        SessionDescription(SessionDescription.Type.fromCanonicalForm(type), sdp)

    private fun CallIceCandidateDto.toNative(): IceCandidate =
        IceCandidate(sdpMid, sdpMLineIndex, candidate)

    companion object {
        private const val AUDIO_TRACK_ID = "IKAR_AUDIO_TRACK"
        private const val VIDEO_TRACK_ID = "IKAR_VIDEO_TRACK"
        private const val VIDEO_WIDTH = 640
        private const val VIDEO_HEIGHT = 480
        private const val VIDEO_FPS = 24

        private val factoryInitializationLock = Any()
        @Volatile
        private var factoryInitialized = false

        private fun ensureFactoryInitialized(context: Context) {
            if (factoryInitialized) {
                return
            }

            synchronized(factoryInitializationLock) {
                if (factoryInitialized) {
                    return
                }

                PeerConnectionFactory.initialize(
                    PeerConnectionFactory.InitializationOptions.builder(context)
                        .createInitializationOptions()
                )
                factoryInitialized = true
            }
        }
    }
}
