/**
 * @module sync-peer-websocket-sender-v1
 */
import { extractErrorMsg } from '@ibgib/helper-gib/dist/helpers/utils-helper.mjs';
import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs';
import { KeystoneStrategyFactory } from '../../../../keystone/strategy/keystone-strategy-factory.mjs';
import { deriveSessionSecret } from '../../../sync-helpers.mjs';
import { SyncPeer_V1 } from '../../sync-peer-v1.mjs';
import { validateContextAndSagaFrame } from '../../../sync-saga-context/sync-saga-context-helpers.mjs';
import { GLOBAL_LOG_A_LOT } from '../../../../core-constants.mjs';
import { SESSION_KEYSTONE_POLICY, getConnectChallenge } from '../sync-peer-websocket-receiver/sync-websocket-peer-helpers.mjs';
import { SyncWebSocketMsgType, isSyncWebSocketMsgType, SYNC_WEB_SOCKET_MSG_TYPE_VALID_VALUES } from '../sync-peer-websocket-constants.mjs';
import { toDto } from '../../../../common/other/ibgib-helper.mjs';
import { putInSpace, registerNewIbGib } from '../../../../witness/space/space-helper.mjs';
/**
 * helper in creating compile-time safety that we're handling all message types.
 */
function assertUnreachable(x) {
    throw new Error(`Unhandled message type: ${x} (E: e928a3f82cd7469a98ef1bc248a3f826)`);
}
const logalot = GLOBAL_LOG_A_LOT || true;
/**
 * WebSocket Sender Peer implementation running in browser/native environment.
 */
export class SyncPeerWebSocketSender_V1 extends SyncPeer_V1 {
    lc = `[${SyncPeerWebSocketSender_V1.name}]`;
    get classname() {
        return SyncPeerWebSocketSender_V1.name;
    }
    get isSocketOpen() {
        return this.ws !== undefined && this.ws.readyState === WebSocket.OPEN;
    }
    ws;
    activeResolve;
    activeReject;
    pendingPayloadsToSend = [];
    handshakeMessageListener;
    constructor(initialData, initialRel8ns) {
        super(initialData, initialRel8ns);
    }
    async preConnectCheck(opts) {
        const lc = `${this.lc}[${this.preConnectCheck.name}]`;
        if (!this.data?.wsUrl) {
            throw new Error(`Missing wsUrl in peer data (E: a3b2c1d0e9f8e7d6c5b4a3f2e1d0c910)`);
        }
        if (!this.data?.httpEvolveUrl) {
            throw new Error(`Missing httpEvolveUrl in peer data (E: a3b2c1d0e9f8e7d6c5b4a3f2e1d0c911)`);
        }
    }
    /**
     * Submits the evolved master identity (I1) and new session keystone (S) to the server's HTTP registry.
     */
    async postEstablishToReceiver({ newSenderIdentity, sessionIdentity, }) {
        const lc = `${this.lc}[${this.postEstablishToReceiver.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} posting evolved keystones to ${this.data.httpEvolveUrl}...`);
            }
            if (!this.data) {
                throw new Error(`(UNEXPECTED) this.data falsy? (E: d642e8a9af18b532c87c6f581aa53b26)`);
            }
            if (!this.data.httpEvolveUrl) {
                throw new Error(`(UNEXPECTED) this.data.httpEvolveUrl falsy? (E: 8589ddbb155914d85c09658881da2c26)`);
            }
            const response = await fetch(this.data.httpEvolveUrl, {
                method: 'PUT',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    keystoneIbGib: newSenderIdentity,
                    relatedIbGibs: [sessionIdentity]
                }),
            });
            if (!response.ok) {
                const data = await response.json().catch(() => ({}));
                const errorMsg = data.message || data.error || 'Unknown error';
                throw new Error(`HTTP ${response.status} evolution post rejected. errorMsg: ${errorMsg} (E: e8a478291b88d05c68cf6b385684b826)`);
            }
            if (logalot) {
                console.log(`${lc} evolve post accepted by server.`);
            }
        }
        catch (error) {
            console.error(`${lc} establish post failed: ${extractErrorMsg(error)}`);
            throw error;
        }
    }
    /**
     * Establishes the stateful WebSocket connection and performs the multi-turn cryptographic challenge connect.
     */
    async connectImpl(opts) {
        const lc = `${this.lc}[${this.connectImpl.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting...`);
            }
            const { senderIdentity, fnSenderSecret, sagaId, localMetaspace, localSpace } = this.opts;
            if (!senderIdentity || !fnSenderSecret || !sagaId) {
                throw new Error(`Missing identity parameters in peer options (E: ed49b6711ac82efcdb42b8a28c5b6826)`);
            }
            // 1. Solve upfront pre-filter solution
            const senderSecret = await fnSenderSecret();
            const sessionSecret = await deriveSessionSecret({ senderSecret, sagaId });
            // Fetch session identity S that establishSessionIdentity just created
            const sAddr_tjp = getIbGibAddr({ ibGib: senderIdentity }); // wait, S's past contains I
            // Actually, we can get the S keystone that was returned by establishSessionIdentity.
            // Let's resolve S from the sender localSpace using its target claim or the targetAddrs
            const sAddr = getIbGibAddr({ ibGib: senderIdentity.rel8ns?.past ? senderIdentity : senderIdentity }); // stub: get the actual generated S
            // To make sure we have the exact session identity S, we can query localSpace.
            // Since we know the sagaId, we can walk or locate it.
            // But wait, the coordinator calls establishSessionIdentity first, which returns the created S keystone!
            // Let's store a reference to the sessionIdentity S on this class during establish, or lookup from localSpace.
            // Wait, does the coordinator let the peer store S? No, but let's query the latest keystone in localSpace.
            const senderIdentityLatestAddr = await localMetaspace.getLatestAddr({ ibGib: senderIdentity, space: localSpace });
            if (!senderIdentityLatestAddr) {
                throw new Error(`senderIdentityLatestAddr falsy. we should have a latest addr that is different than the sender identity at this point (done in the peer establish phase) (E: 225607318bef485d7821f82de97b6826)`);
            }
            const resGet = await localMetaspace.get({ addrs: [senderIdentityLatestAddr], space: localSpace });
            const I_tip = resGet.ibGibs?.[0];
            // const targetSAddr = I_tip?.rel8ns?.past?.[0] ?? I_tip?.data?.proofs?.[0]?.claim?.target;
            const targetSAddr = I_tip.data.proofs.at(0)?.claim.target;
            if (!targetSAddr) {
                throw new Error(`Could not locate session keystone target from identity (E: 5ec4882f7e5e4c33fbd00ab8b3166726)`);
            }
            const resGetS = await localMetaspace.get({ addrs: [targetSAddr], space: localSpace });
            const sessionS = resGetS.ibGibs?.[0];
            if (!sessionS) {
                throw new Error(`Session keystone not found in local space: ${targetSAddr} (E: 9ba818e6622808bb4ae749be31f80b26)`);
            }
            // Solve dynamic upfront picket-fence challenge
            const connectPool = (sessionS.data?.challengePools ?? [])
                .find(p => p.id === SESSION_KEYSTONE_POLICY.CONNECT_POOL.ID);
            if (!connectPool) {
                throw new Error(`Session keystone missing connect pool (E: f50968afca04b6d38ec19824ea201826)`);
            }
            const { challengeId } = getConnectChallenge(sessionS);
            const strategy = KeystoneStrategyFactory.create({ config: connectPool.config });
            const poolSecret = await strategy.derivePoolSecret({ masterSecret: sessionSecret });
            const solution = await strategy.generateSolution({
                poolSecret,
                poolId: SESSION_KEYSTONE_POLICY.CONNECT_POOL.ID,
                challengeId
            });
            // 2. Open WebSocket connection
            const wsUrl = `${this.data.wsUrl}?sAddr=${encodeURIComponent(targetSAddr)}&solution=${encodeURIComponent(solution.value)}`;
            if (logalot) {
                console.log(`${lc} connecting to WebSocket: ${wsUrl}`);
            }
            const ws = new WebSocket(wsUrl);
            this.ws = ws;
            return new Promise((resolve, reject) => {
                let isResolved = false;
                ws.addEventListener('open', () => {
                    if (logalot) {
                        console.log(`${lc} WebSocket opened. Awaiting challenge...`);
                    }
                });
                this.handshakeMessageListener = async (ev) => {
                    try {
                        const msg = JSON.parse(ev.data);
                        if (logalot) {
                            console.log(`${lc} received handshake frame: ${msg.type}`);
                        }
                        const msgType = msg.type;
                        if (!isSyncWebSocketMsgType(msgType)) {
                            const validTypes = SYNC_WEB_SOCKET_MSG_TYPE_VALID_VALUES.join(', ');
                            throw new Error(`Unknown message type '${msgType}' received during connection handshake. Valid types are: ${validTypes} (E: e983271bc84f46928e4695be2409826)`);
                        }
                        switch (msgType) {
                            case SyncWebSocketMsgType.auth_challenge_init:
                                await this.handleHandshakeAuthChallengeInit(ws, targetSAddr);
                                break;
                            case SyncWebSocketMsgType.auth_challenge:
                                await this.handleHandshakeAuthChallenge(ws, msg, sessionS, sessionSecret);
                                break;
                            case SyncWebSocketMsgType.auth_ok:
                                isResolved = true;
                                this.handleHandshakeAuthOk(ws, resolve);
                                break;
                            case SyncWebSocketMsgType.auth_fail:
                                this.handleHandshakeAuthFail(msg, reject);
                                break;
                            case SyncWebSocketMsgType.sync_error:
                                this.handleHandshakeSyncError(msg, reject);
                                break;
                            // Protocol violations / unexpected messages during connection handshake
                            case SyncWebSocketMsgType.auth_init:
                            case SyncWebSocketMsgType.auth_proof:
                            case SyncWebSocketMsgType.sync_frame:
                            case SyncWebSocketMsgType.sync_frame_response:
                            case SyncWebSocketMsgType.sync_frame_authenticated:
                            case SyncWebSocketMsgType.sync_frame_response_authenticated:
                            case SyncWebSocketMsgType.domain_payload:
                                throw new Error(`Unexpected message type '${msgType}' during connection handshake (E: e3f80c68ab2a46c2b1858c8a1e2f8926)`);
                            default:
                                assertUnreachable(msgType);
                        }
                    }
                    catch (error) {
                        console.error(`${lc} ${extractErrorMsg(error)}`);
                        reject(error);
                    }
                };
                ws.addEventListener('message', this.handshakeMessageListener);
                ws.addEventListener('close', (event) => {
                    if (!isResolved) {
                        this.disconnect();
                        reject(new Error(`WebSocket closed before connect completed (code: ${event.code})`));
                    }
                });
                ws.addEventListener('error', (err) => {
                    if (!isResolved) {
                        this.disconnect();
                        reject(new Error(`WebSocket connection error`));
                    }
                });
            });
        }
        catch (error) {
            console.error(`${lc} connect failed: ${extractErrorMsg(error)}`);
            throw error;
        }
    }
    /**
     * Cleanly closes the socket and removes the handshake message listener.
     *
     * LOGS AND SWALLOWS ERRORS, DOES NOT THROW
     */
    disconnect() {
        const lc = `${this.lc}[${this.disconnect.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting... (I: 35c4b85fca0c6e9d042f5ff87cde3826)`);
            }
            if (this.ws) {
                if (this.handshakeMessageListener) {
                    this.ws.removeEventListener('message', this.handshakeMessageListener);
                    this.handshakeMessageListener = undefined;
                }
                try {
                    this.ws.close();
                }
                catch (error) {
                    console.error(`${lc} failed to close websocket: ${extractErrorMsg(error)}`);
                }
                this.ws = undefined;
            }
        }
        catch (error) {
            console.error(`${lc} ${extractErrorMsg(error)}`);
            // throw error; // disconnect does NOT rethrow
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    /**
     * Handles synchronizing messages and evolved context frames during active transaction turns.
     */
    async handleRuntimeMessage(event) {
        const lc = `${this.lc}[${this.handleRuntimeMessage.name}]`;
        try {
            const msg = JSON.parse(event.data);
            if (logalot) {
                console.log(`${lc} received runtime frame: ${msg.type}`);
            }
            const msgType = msg.type;
            if (!isSyncWebSocketMsgType(msgType)) {
                const validTypes = SYNC_WEB_SOCKET_MSG_TYPE_VALID_VALUES.join(', ');
                throw new Error(`Unknown message type '${msgType}' received during active sync saga loop. Valid types are: ${validTypes} (E: e23a8d10b74d47fb90518f8e3f4b826)`);
            }
            switch (msgType) {
                case SyncWebSocketMsgType.sync_frame_response:
                    await this.handleRuntimeSyncFrameResponse(msg);
                    break;
                case SyncWebSocketMsgType.sync_frame_authenticated:
                    this.handleRuntimeSyncFrameAuthenticated();
                    break;
                case SyncWebSocketMsgType.domain_payload:
                    this.handleRuntimeDomainPayload(msg);
                    break;
                case SyncWebSocketMsgType.sync_error:
                    this.handleRuntimeSyncError(msg);
                    break;
                // Handshake messages / unexpected messages during active sync saga loop
                case SyncWebSocketMsgType.auth_challenge_init:
                case SyncWebSocketMsgType.auth_init:
                case SyncWebSocketMsgType.auth_challenge:
                case SyncWebSocketMsgType.auth_proof:
                case SyncWebSocketMsgType.auth_ok:
                case SyncWebSocketMsgType.auth_fail:
                case SyncWebSocketMsgType.sync_frame:
                case SyncWebSocketMsgType.sync_frame_response_authenticated:
                    throw new Error(`Unexpected message type '${msgType}' during active sync saga loop (E: e982b12cf92c448bbad0e84b7263c826)`);
                default:
                    assertUnreachable(msgType);
            }
        }
        catch (error) {
            console.error(`${lc} failed parsing/handling runtime frame: ${extractErrorMsg(error)}`);
            this.disconnect();
            if (this.activeReject) {
                const reject = this.activeReject;
                this.activeResolve = undefined;
                this.activeReject = undefined;
                reject(error);
            }
        }
    }
    async handleHandshakeAuthChallengeInit(ws, targetSAddr) {
        ws.send(JSON.stringify({
            type: SyncWebSocketMsgType.auth_init,
            sAddr: targetSAddr
        }));
    }
    async handleHandshakeAuthChallenge(ws, msg, sessionS, sessionSecret) {
        const { challengeUuid, demandedIds } = msg;
        if (logalot) {
            console.log(`${this.lc} solving demanded challenges: ${demandedIds.join(', ')}`);
        }
        const proofFrame = await this.signContextConnect({
            challengeUuid,
            demandedIds
        });
        if (!proofFrame) {
            throw new Error(`Failed to sign connect challenge proof (E: e9807f28ae9248bbadd0a7f1a8e9826)`);
        }
        ws.send(JSON.stringify({
            type: SyncWebSocketMsgType.auth_proof,
            proofFrame
        }));
    }
    handleHandshakeAuthOk(ws, resolve) {
        if (logalot) {
            console.log(`${this.lc} WebSocket connect SUCCESS!`);
        }
        if (this.handshakeMessageListener) {
            ws.removeEventListener('message', this.handshakeMessageListener);
            this.handshakeMessageListener = undefined;
        }
        ws.addEventListener('message', (event) => this.handleRuntimeMessage(event));
        resolve();
    }
    handleHandshakeAuthFail(msg, reject) {
        this.disconnect();
        reject(new Error(`Handshake auth failed: ${msg.message || 'Unknown fail reason'} (E: f380b271a2be498db257bc8209fa8926)`));
    }
    handleHandshakeSyncError(msg, reject) {
        this.disconnect();
        reject(new Error(`Handshake sync error: ${msg.message || msg.error || 'Unknown sync error'} (E: f9208a01fe434cbbad83f210ea8f3426)`));
    }
    async handleRuntimeSyncFrameResponse(msg) {
        const responseContext = msg.context;
        const validationErrors = await validateContextAndSagaFrame({ context: responseContext });
        if (validationErrors.length > 0) {
            throw new Error(`Invalid response context received: ${validationErrors.join(', ')} (E: d7b5a283cf4c43ba8659c803800cf826)`);
        }
        // Put response control ibgibs into durable space immediately
        const allControlIbGibs = [
            toDto({ ibGib: responseContext }),
            responseContext.sagaFrame,
            responseContext.sagaFrameMsg
        ];
        if (responseContext.signedSessionIdentity) {
            allControlIbGibs.push(responseContext.signedSessionIdentity);
        }
        const { localSpace } = this.opts;
        for (const ibGib of allControlIbGibs) {
            await putInSpace({ space: localSpace, ibGibs: [ibGib] });
            await registerNewIbGib({ space: localSpace, ibGib });
        }
        const expectedPayloadAddrs = responseContext.data?.['@payloadAddrsDomain'] || [];
        if (expectedPayloadAddrs.length > 0) {
            this.ws.send(JSON.stringify({
                type: SyncWebSocketMsgType.sync_frame_response_authenticated,
                contextAddr: getIbGibAddr({ ibGib: responseContext })
            }));
        }
        if (this.activeResolve) {
            const resolve = this.activeResolve;
            this.activeResolve = undefined;
            this.activeReject = undefined;
            resolve(responseContext);
        }
    }
    handleRuntimeSyncFrameAuthenticated() {
        const payloads = this.pendingPayloadsToSend || [];
        this.pendingPayloadsToSend = [];
        for (const ibGib of payloads) {
            this.ws.send(JSON.stringify({
                type: SyncWebSocketMsgType.domain_payload,
                ibGib
            }));
        }
    }
    handleRuntimeDomainPayload(msg) {
        const payload = msg.ibGib;
        this.payloadIbGibsDomainReceived$.next(payload);
    }
    handleRuntimeSyncError(msg) {
        const errorDetail = msg.message || msg.error || 'Unknown sync runtime error';
        const error = new Error(`Sync runtime error from receiver: ${errorDetail} (E: e983bc1c828d447fa0581da2b8004f26)`);
        if (this.activeReject) {
            const reject = this.activeReject;
            this.activeResolve = undefined;
            this.activeReject = undefined;
            reject(error);
        }
        else {
            throw error;
        }
    }
    /**
     * Serializes the transaction context and payload down the active WebSocket connection.
     */
    async sendContextRequest(context) {
        const lc = `${this.lc}[${this.sendContextRequest.name}]`;
        try {
            if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
                throw new Error(`WebSocket is not connected or open (E: a3b2c1d0e9f8e7d6c5b4a3f2e1d0c915)`);
            }
            // 1. Separate payloads to send later
            const domainPayloads = context.payloadIbGibsDomain ?? [];
            this.pendingPayloadsToSend = [...domainPayloads];
            // 2. Clone context without payloads for transport
            const contextToSend = { ...context };
            delete contextToSend.payloadIbGibsDomain;
            // 3. Transmit the synchronizing transaction context
            return new Promise((resolve, reject) => {
                this.activeResolve = resolve;
                this.activeReject = reject;
                this.ws.send(JSON.stringify({
                    type: SyncWebSocketMsgType.sync_frame,
                    context: contextToSend
                }));
            });
        }
        catch (error) {
            console.error(`${lc} sendContextRequest failed: ${extractErrorMsg(error)}`);
            throw error;
        }
    }
}
//# sourceMappingURL=sync-peer-websocket-sender-v1.mjs.map