import { Call } from "@/app/components/Call/Call";
import { useConsoleLogger } from "@/app/hooks/useConsoleLogger";
import { createContext, useEffect, useRef, useState } from "react";
import nacl from 'tweetnacl';
import { useSender } from "../ProtocolProvider/useSender";
import { PacketSignal, SignalType } from "../ProtocolProvider/protocol/packets/packet.signal";
import { usePacket } from "../ProtocolProvider/usePacket";
import { usePublicKey } from "../AccountProvider/usePublicKey";


export interface CallContextValue {
    call: (callable: string) => void;
    close: () => void;
    activeCall: string;
    callState: CallState;
    muted: boolean;
    sound: boolean;
    setMuted: (muted: boolean) => void;
    setSound: (sound: boolean) => void;
    duration: number;
    setShowCallView: (show: boolean) => void;
    getKeyCast: () => string;
    accept: () => void;
}

export enum CallState {
    CONNECTING,
    KEY_EXCHANGE,
    /**
     * Финальная стадия сигналинга, на которой обе стороны обменялись ключами и теперь устанавливают защищенный канал связи для звонка,
     * через WebRTC, и готовятся к активному звонку.
     */
    WEB_RTC_EXCHANGE,
    ACTIVE,
    ENDED,
    INCOMING
}

export enum CallRole {
    /**
     * Вызывающая сторона, которая инициирует звонок
     */
    CALLER,
    /**
     * Принимающая сторона, которая отвечает на звонок и принимает его
     */
    CALLEE
}

export const CallContext = createContext<CallContextValue | null>(null);
export interface CallProviderProps {
    children: React.ReactNode;
}

export function CallProvider(props : CallProviderProps) {
    const [activeCall, setActiveCall] = useState<string>("");
    const [callState, setCallState] = useState<CallState>(CallState.ENDED);
    const [muted, setMuted] = useState<boolean>(false);
    const [sound, setSound] = useState<boolean>(true);
    const [duration, setDuration] = useState<number>(0);
    const [showCallView, setShowCallView] = useState<boolean>(callState == CallState.INCOMING);
    const {info} = useConsoleLogger("CallProvider");
    const [sessionKeys, setSessionKeys] = useState<nacl.BoxKeyPair | null>(null);
    const send = useSender();
    const publicKey = usePublicKey();
    const peerConnectionRef = useRef<RTCPeerConnection | null>(null);

    const roleRef = useRef<CallRole | null>(null);
    const [sharedSecret, setSharedSecret] = useState<string>("");

    useEffect(() => {
        console.info("TRACE -> ", sharedSecret)
    }, [sharedSecret]);

    usePacket(26, (packet: PacketSignal) => {
        const signalType = packet.getSignalType();
        if(activeCall){
            /**
             * У нас уже есть активный звонок, игнорируем все сигналы, кроме сигналов от текущего звонка
             */
            if(packet.getSrc() != activeCall){
                console.info("Received signal from " + packet.getSrc() + " but active call is with " + activeCall + ", ignoring");
                info("Received signal for another call, ignoring");
                return;
            }
        }
        if(signalType == SignalType.END_CALL){
            /**
             * Сбросили звонок
             */
            setActiveCall("");
            setCallState(CallState.ENDED);
            setShowCallView(false);
            setSessionKeys(null);
            setSharedSecret("");
            setDuration(0);
            roleRef.current = null;
            return;
        }
        if(signalType == SignalType.CALL){
            /**
             * Нам поступает звонок
             */
            setActiveCall(packet.getSrc());
            setCallState(CallState.INCOMING);
            setShowCallView(true);
        }
        if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLER){
            console.info("EXCHANGE SIGNAL RECEIVED, CALLER ROLE");
            /**
             * Другая сторона сгенерировала ключи для сессии и отправила нам публичную часть, 
             * теперь мы можем создать общую секретную сессию для шифрования звонка
             */
            const sharedPublic = packet.getSharedPublic();
            if(!sharedPublic){
                info("Received key exchange signal without shared public key");
                return;
            }
            const sessionKeys = generateSessionKeys();
            const computedSharedSecret = nacl.box.before(Buffer.from(sharedPublic, 'hex'), sessionKeys.secretKey);
            setSharedSecret(Buffer.from(computedSharedSecret).toString('hex'));
            info("Generated shared secret for call session: " + Buffer.from(computedSharedSecret).toString('hex'));
            /**
             * Нам нужно отправить свой публичный ключ другой стороне, чтобы она тоже могла создать общую секретную сессию
             */
            const signalPacket = new PacketSignal();
            signalPacket.setSrc(publicKey);
            signalPacket.setDst(packet.getSrc());
            signalPacket.setSignalType(SignalType.KEY_EXCHANGE);
            signalPacket.setSharedPublic(Buffer.from(sessionKeys.publicKey).toString('hex'));
            send(signalPacket);
            setCallState(CallState.WEB_RTC_EXCHANGE);
        }
        if(signalType == SignalType.KEY_EXCHANGE && roleRef.current == CallRole.CALLEE){
            console.info("EXCHANGE SIGNAL RECEIVED, CALLEE ROLE");
            /**
             * Мы отправили свою публичную часть ключа другой стороне, 
             * теперь мы получили ее публичную часть и можем создать общую 
             * секретную сессию для шифрования звонка
             */
            const sharedPublic = packet.getSharedPublic();
            if(!sharedPublic){
                info("Received key exchange signal without shared public key");
                return;
            }
            if(!sessionKeys){
                info("Received key exchange signal but session keys are not generated");
                return;
            }
            const computedSharedSecret = nacl.box.before(Buffer.from(sharedPublic, 'hex'), sessionKeys.secretKey);
            info("Generated shared secret for call session: " + Buffer.from(computedSharedSecret).toString('hex'));
            setSharedSecret(Buffer.from(computedSharedSecret).toString('hex'));
            setCallState(CallState.WEB_RTC_EXCHANGE);
        }
    }, [activeCall, sessionKeys]);

    const generateSessionKeys = () => {
        const sessionKeys = nacl.box.keyPair();
        info("Generated keys for call session, len: " + sessionKeys.publicKey.length);
        setSessionKeys(sessionKeys);
        return sessionKeys;
    }

    const call = (dialog: string) => {
        setActiveCall(dialog);
        setCallState(CallState.CONNECTING);
        setShowCallView(true);
        const signalPacket = new PacketSignal();
        signalPacket.setSrc(publicKey);
        signalPacket.setDst(dialog);
        signalPacket.setSignalType(SignalType.CALL);
        send(signalPacket);
        roleRef.current = CallRole.CALLER;
    }

    const close = () => {
        const packetSignal = new PacketSignal();
        packetSignal.setSrc(publicKey);
        packetSignal.setDst(activeCall);
        packetSignal.setSignalType(SignalType.END_CALL);
        send(packetSignal);
        peerConnectionRef.current = null;
        setActiveCall("");
        setCallState(CallState.ENDED);
        setShowCallView(false);
        setSessionKeys(null);
        setDuration(0);
        roleRef.current = null;
    }

    const accept = () => {
        if(callState != CallState.INCOMING){
            /**
             * Нечего принимать
             */
            return;
        }
        /**
         * Звонок принят, генерируем ключи для сессии и отправляем их другой стороне для установления защищенного канала связи
         */
        const keys = generateSessionKeys();
        const signalPacket = new PacketSignal();
        signalPacket.setSrc(publicKey);
        signalPacket.setDst(activeCall);
        signalPacket.setSignalType(SignalType.KEY_EXCHANGE);
        signalPacket.setSharedPublic(Buffer.from(keys.publicKey).toString('hex'));
        send(signalPacket);
        setCallState(CallState.KEY_EXCHANGE);
        roleRef.current = CallRole.CALLEE;
    }

    /**
     * Получает слепок ключа для отображения в UI
     * чтобы не показывать настоящий ключ
     * @returns 
     */
    const getKeyCast = () => {
        if(!sharedSecret){
            return "";
        }
        return sharedSecret;
    }

    const context = { 
        call, 
        close, 
        activeCall, 
        callState, 
        muted, 
        sound,
        setMuted,
        setSound,
        duration,
        setShowCallView,
        getKeyCast,
        accept
    };

    return (
        <CallContext.Provider value={context}>
            {props.children}
            {showCallView && <Call context={context}></Call>}
        </CallContext.Provider> 
    )
}