import { createCaptureState, meetsQualityThreshold, } from "../session/captureStateMachine.js";
import { ensureSessionDir, slapFileName, rollFileName, buildResultSchema, writeCaptureResult, writeImageBuffer, } from "../storage/captureStore.js";
import { forwardCaptureToSessionService } from "../sessionService/forwardCapture.js";
import { v4 as uuidv4 } from "uuid";
const SLAP_TYPE_TO_PROTO = {
    right_four_fingers: 1,
    left_four_fingers: 2,
    two_thumbs: 3,
};
const PROTO_TO_SLAP = {
    1: "right_four_fingers",
    2: "left_four_fingers",
    3: "two_thumbs",
};
const FINGER_NAME_TO_PROTO = {
    right_thumb: 1,
    right_index: 2,
    right_middle: 3,
    right_ring: 4,
    right_little: 5,
    left_thumb: 6,
    left_index: 7,
    left_middle: 8,
    left_ring: 9,
    left_little: 10,
};
export function createWsHandlers(client, logger, cacheDir) {
    const stateBySession = new Map();
    const previewBySession = new Map();
    function stopPreview(sessionId) {
        const p = previewBySession.get(sessionId);
        if (!p)
            return;
        try {
            p.ac.abort();
        }
        catch {
            // ignore
        }
        try {
            p.stream.cancel();
        }
        catch {
            // ignore
        }
        previewBySession.delete(sessionId);
    }
    function send(ws, msg) {
        try {
            ws.send(JSON.stringify(msg));
        }
        catch (e) {
            logger.warn({ err: e }, "WS send failed");
        }
    }
    function ack(ws, id, success, error) {
        send(ws, { event: "command_ack", id, success, error });
    }
    function toDeviceMsg(d) {
        return {
            device_id: String(d.device_id ?? ""),
            name: String(d.name ?? ""),
            serial_number: String(d.serial_number ?? ""),
            firmware_version: String(d.firmware_version ?? ""),
            is_available: d.is_available !== false,
        };
    }
    function tsFromProto(ts) {
        if (!ts)
            return new Date().toISOString();
        const ms = (Number(ts.seconds ?? 0) * 1000) + (Number(ts.nanos ?? 0) / 1e6);
        return new Date(ms).toISOString();
    }
    return {
        handleMessage(ws, raw, requestId) {
            let msg;
            try {
                msg = JSON.parse(raw);
            }
            catch {
                send(ws, { event: "error", code: "INVALID_JSON", message: "Invalid JSON", request_id: requestId });
                return;
            }
            const id = "id" in msg ? msg.id : undefined;
            if (msg.type === "ping") {
                send(ws, { event: "pong", id });
                return;
            }
            if (!client) {
                ack(ws, id, false, "Agent not connected");
                send(ws, { event: "error", code: "AGENT_DISCONNECTED", message: "Agent not connected", request_id: requestId });
                return;
            }
            const c = client;
            switch (msg.type) {
                case "list_devices":
                    c.listDevices({}, (err, res) => {
                        if (err) {
                            ack(ws, id, false, err.message);
                            return;
                        }
                        const devices = (res?.devices ?? []).map((d) => toDeviceMsg(d));
                        send(ws, { event: "device_status", devices });
                        ack(ws, id, true);
                    });
                    break;
                case "start_session": {
                    c.startSession({ device_id: msg.device_id, applicant_id: msg.applicant_id, metadata: msg.metadata }, (err, res) => {
                        if (err) {
                            ack(ws, id, false, err.message);
                            return;
                        }
                        const r = res;
                        if (r.error?.message) {
                            ack(ws, id, false, r.error.message);
                            return;
                        }
                        const sessionId = msg.session_id ?? r.session_id ?? uuidv4();
                        stateBySession.set(sessionId, createCaptureState());
                        stateBySession.get(sessionId).session_id = sessionId;
                        send(ws, { event: "session_status", session_id: sessionId, status: "started", device_id: msg.device_id });
                        ack(ws, id, true);
                    });
                    break;
                }
                case "end_session": {
                    stopPreview(msg.session_id);
                    c.endSession({ session_id: msg.session_id }, (err) => {
                        if (err)
                            ack(ws, id, false, err.message);
                        else
                            ack(ws, id, true);
                    });
                    send(ws, { event: "session_status", session_id: msg.session_id, status: "ended" });
                    stateBySession.delete(msg.session_id);
                    break;
                }
                case "end_all_sessions": {
                    for (const sid of Array.from(previewBySession.keys())) {
                        stopPreview(sid);
                        send(ws, { event: "session_status", session_id: sid, status: "ended" });
                    }
                    stateBySession.clear();
                    c.EndAllSessions({}, (err, res) => {
                        if (err) {
                            ack(ws, id, false, err.message);
                            return;
                        }
                        const r = res;
                        if (r.error?.message) {
                            ack(ws, id, false, r.error.message);
                            return;
                        }
                        ack(ws, id, true);
                    });
                    break;
                }
                case "start_preview": {
                    stopPreview(msg.session_id);
                    const ac = new AbortController();
                    const stream = c.streamPreview({
                        session_id: msg.session_id,
                        max_fps: 15,
                        jpeg_quality: 80,
                    });
                    previewBySession.set(msg.session_id, { ac, stream });
                    stream.on("data", (data) => {
                        if (ac.signal.aborted)
                            return;
                        const d = data;
                        // Node gRPC may expose proto fields as jpeg_image (keepCase) or JpegImage (PascalCase)
                        const rawImage = d.jpeg_image ?? d.JpegImage ?? d.jpegImage;
                        let buf = Buffer.from([]);
                        if (rawImage instanceof Buffer) {
                            buf = rawImage;
                        }
                        else if (rawImage instanceof Uint8Array) {
                            buf = Buffer.from(rawImage);
                        }
                        else if (rawImage instanceof ArrayBuffer) {
                            buf = Buffer.from(new Uint8Array(rawImage));
                        }
                        else if (rawImage && typeof rawImage === "object" && ArrayBuffer.isView(rawImage)) {
                            buf = Buffer.from(rawImage);
                        }
                        if (buf.length === 0)
                            return; // skip empty frames
                        send(ws, {
                            event: "preview_frame",
                            session_id: (d.session_id ?? msg.session_id),
                            data: buf.toString("base64"),
                            width: (d.width ?? 640),
                            height: (d.height ?? 480),
                            captured_at: tsFromProto(d.captured_at),
                        });
                    });
                    stream.on("error", (err) => {
                        if (err && !ac.signal.aborted)
                            logger.warn({ err }, "Preview stream error");
                    });
                    stream.on("end", () => previewBySession.delete(msg.session_id));
                    ack(ws, id, true);
                    break;
                }
                case "stop_preview": {
                    stopPreview(msg.session_id);
                    ack(ws, id, true);
                    break;
                }
                case "capture_slap": {
                    const protoSlap = SLAP_TYPE_TO_PROTO[msg.slap_type] ?? 1;
                    send(ws, { event: "capture_progress", session_id: msg.session_id, step: "capture_slap", message: msg.slap_type });
                    c.captureSlap({ session_id: msg.session_id, slap_type: protoSlap, timeout_seconds: 30 }, (err, res) => {
                        if (err) {
                            send(ws, { event: "error", code: "CAPTURE_FAILED", message: err.message, request_id: requestId });
                            ack(ws, id, false, err.message);
                            return;
                        }
                        const r = res;
                        if (r.error?.message) {
                            send(ws, { event: "error", code: "CAPTURE_ERROR", message: r.error.message, request_id: requestId });
                            ack(ws, id, false, r.error.message);
                            return;
                        }
                        const state = stateBySession.get(msg.session_id);
                        const slapType = PROTO_TO_SLAP[r.slap_type ?? 1] ?? msg.slap_type;
                        const attempt = (state?.current_attempt[slapType] ?? 0) + 1;
                        if (state)
                            state.current_attempt[slapType] = attempt;
                        const qualityScores = r.quality_scores ?? [];
                        const accepted = meetsQualityThreshold(qualityScores);
                        const capturedAt = tsFromProto(r.captured_at);
                        const dir = ensureSessionDir(msg.session_id, cacheDir);
                        const imgFileName = slapFileName(slapType, attempt, "png");
                        if (r.image_bmp && r.image_bmp.length > 0) {
                            writeImageBuffer(dir, imgFileName, Buffer.isBuffer(r.image_bmp) ? r.image_bmp : Buffer.from(r.image_bmp));
                        }
                        const relPath = `sessions/${msg.session_id}/${imgFileName}`;
                        const resultSchema = buildResultSchema({
                            capture_id: r.capture_id ?? uuidv4(),
                            session_id: msg.session_id,
                            kind: "slap",
                            slap_type: slapType,
                            finger: null,
                            attempt,
                            accepted,
                            paths: { image_raw: relPath },
                            width: r.width ?? 0,
                            height: r.height ?? 0,
                            quality_scores: qualityScores,
                            segments: r.segments ?? [],
                            captured_at: capturedAt,
                        });
                        writeCaptureResult(cacheDir, resultSchema);
                        if (state && accepted) {
                            if (!state.accepted_slaps[slapType])
                                state.accepted_slaps[slapType] = [];
                            state.accepted_slaps[slapType].push(r.capture_id ?? "");
                            state.slap_index++;
                        }
                        const payload = {
                            slap_type: slapType,
                            width: r.width ?? 0,
                            height: r.height ?? 0,
                            quality_scores: qualityScores,
                            segments: r.segments ?? [],
                            captured_at: capturedAt,
                        };
                        if (r.image_bmp && r.image_bmp.length > 0) {
                            payload.image_bmp_base64 = (Buffer.isBuffer(r.image_bmp) ? r.image_bmp : Buffer.from(r.image_bmp)).toString("base64");
                        }
                        send(ws, {
                            event: "capture_result",
                            session_id: msg.session_id,
                            capture_id: r.capture_id ?? "",
                            kind: "slap",
                            payload,
                        });
                        forwardCaptureToSessionService({
                            session_id: msg.session_id,
                            kind: "slap",
                            finger_position: slapType,
                            attempt,
                            quality_scores: qualityScores,
                            width: r.width ?? 0,
                            height: r.height ?? 0,
                            captured_at: capturedAt,
                            wsq_buffer: r.image_wsq ? (Buffer.isBuffer(r.image_wsq) ? r.image_wsq : Buffer.from(r.image_wsq)) : null,
                        }, logger);
                        ack(ws, id, true);
                    });
                    break;
                }
                case "capture_roll": {
                    const protoFinger = FINGER_NAME_TO_PROTO[msg.finger] ?? 1;
                    send(ws, { event: "capture_progress", session_id: msg.session_id, step: "capture_roll", message: msg.finger });
                    c.captureRoll({ session_id: msg.session_id, finger: protoFinger, timeout_seconds: 30 }, (err, res) => {
                        if (err) {
                            send(ws, { event: "error", code: "CAPTURE_FAILED", message: err.message, request_id: requestId });
                            ack(ws, id, false, err.message);
                            return;
                        }
                        const r = res;
                        if (r.error?.message) {
                            send(ws, { event: "error", code: "CAPTURE_ERROR", message: r.error.message, request_id: requestId });
                            ack(ws, id, false, r.error.message);
                            return;
                        }
                        const fingerName = msg.finger;
                        const state = stateBySession.get(msg.session_id);
                        const attempt = (state?.current_attempt[fingerName] ?? 0) + 1;
                        if (state)
                            state.current_attempt[fingerName] = attempt;
                        const qualityScores = r.quality_scores ?? [];
                        const accepted = meetsQualityThreshold(qualityScores);
                        const capturedAt = tsFromProto(r.captured_at);
                        const dir = ensureSessionDir(msg.session_id, cacheDir);
                        const imgFileName = rollFileName(fingerName, attempt, "png");
                        if (r.image_bmp && r.image_bmp.length > 0) {
                            writeImageBuffer(dir, imgFileName, Buffer.isBuffer(r.image_bmp) ? r.image_bmp : Buffer.from(r.image_bmp));
                        }
                        const relPath = `sessions/${msg.session_id}/${imgFileName}`;
                        const resultSchema = buildResultSchema({
                            capture_id: r.capture_id ?? uuidv4(),
                            session_id: msg.session_id,
                            kind: "roll",
                            slap_type: null,
                            finger: fingerName,
                            attempt,
                            accepted,
                            paths: { image_raw: relPath },
                            width: r.width ?? 0,
                            height: r.height ?? 0,
                            quality_scores: qualityScores,
                            captured_at: capturedAt,
                        });
                        writeCaptureResult(cacheDir, resultSchema);
                        if (state && accepted) {
                            if (!state.accepted_rolls[fingerName])
                                state.accepted_rolls[fingerName] = [];
                            state.accepted_rolls[fingerName].push(r.capture_id ?? "");
                            state.roll_index++;
                        }
                        const payload = {
                            finger: fingerName,
                            width: r.width ?? 0,
                            height: r.height ?? 0,
                            quality_scores: qualityScores,
                            captured_at: capturedAt,
                        };
                        if (r.image_bmp && r.image_bmp.length > 0) {
                            payload.image_bmp_base64 = (Buffer.isBuffer(r.image_bmp) ? r.image_bmp : Buffer.from(r.image_bmp)).toString("base64");
                        }
                        send(ws, {
                            event: "capture_result",
                            session_id: msg.session_id,
                            capture_id: r.capture_id ?? "",
                            kind: "roll",
                            payload,
                        });
                        forwardCaptureToSessionService({
                            session_id: msg.session_id,
                            kind: "roll",
                            finger_position: fingerName,
                            attempt,
                            quality_scores: qualityScores,
                            width: r.width ?? 0,
                            height: r.height ?? 0,
                            captured_at: capturedAt,
                            wsq_buffer: r.image_wsq ? (Buffer.isBuffer(r.image_wsq) ? r.image_wsq : Buffer.from(r.image_wsq)) : null,
                        }, logger);
                        ack(ws, id, true);
                    });
                    break;
                }
                case "get_diagnostics":
                    c.getDiagnostics({}, (err, res) => {
                        if (err) {
                            ack(ws, id, false, err.message);
                            return;
                        }
                        const r = res;
                        const devices = (r.devices ?? []).map((d) => toDeviceMsg(d));
                        send(ws, { event: "device_status", devices });
                        ack(ws, id, true);
                    });
                    break;
                default:
                    ack(ws, id, false, "Unknown command");
            }
        },
    };
}
//# sourceMappingURL=handlers.js.map