/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
/* eslint-disable @typescript-eslint/no-misused-promises */
import http from "node:http";
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import { logger } from "../logger.js";
import { getConfigDir } from "../config/index.js";
import { resolveOpenAIApiKey } from "../openai-credentials.js";
import { REALTIME_SIDECAR_SCHEMA_VERSION, REALTIME_SIDECAR_SOURCE } from "./types.js";
import { readBody, sendJson, sendText } from "./http-helpers.js";
import { verifyTelegramInitData } from "./telegram-auth.js";
import { TOOLS, executeTool, redactRealtimeToolPayload } from "./tools.js";
import { closeVoiceDb, getVoiceDb, buildRealtimeContextFromAgent, loadOwnerConfig } from "./context.js";
import { summarizeVoiceSession } from "./voice-summarizer.js";
const SESSION_TTL_MS = 2 * 60 * 60 * 1000;
const VOICE_MODEL = "gpt-realtime-2";
const VOICE_NAME = "cedar";
const VALID_VOICES = ["alloy", "ash", "ballad", "cedar", "coral", "echo", "marin", "sage", "shimmer", "verse"];
function randomToken() {
    return crypto.randomBytes(24).toString("base64url");
}
function parseJsonObject(value) {
    const parsed = JSON.parse(value);
    if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
        throw new Error("Expected JSON object");
    }
    return parsed;
}
function getSessionCredentials(url, body) {
    const sessionId = url.searchParams.get("sessionId") ?? (typeof body?.sessionId === "string" ? body.sessionId : null);
    const sessionSecret = url.searchParams.get("sessionSecret") ?? (typeof body?.sessionSecret === "string" ? body.sessionSecret : null);
    return { sessionId, sessionSecret };
}
function parseInitData(initData, botToken) {
    if (!verifyTelegramInitData(initData, botToken))
        return null;
    const params = new URLSearchParams(initData);
    const userRaw = params.get("user");
    if (!userRaw)
        return null;
    const user = JSON.parse(userRaw);
    if (typeof user.id !== "number")
        return null;
    return {
        id: user.id,
        first_name: typeof user.first_name === "string" ? user.first_name : undefined,
        username: typeof user.username === "string" ? user.username : undefined,
    };
}
function getTranscriptPath(sessionId) {
    return path.join(getConfigDir(), "voice-transcripts", `${sessionId}.json`);
}
function summarizeInBackground(config, sessionId, endedAt, emit) {
    const sidecarPath = getTranscriptPath(sessionId);
    void summarizeVoiceSession(config, sidecarPath)
        .then((summary) => {
        if (!summary)
            return;
        // Re-read sidecar to pick up the canonical summaryText written by the
        // summarizer.
        try {
            const sidecar = JSON.parse(fs.readFileSync(sidecarPath, "utf-8"));
            emit({
                sessionId,
                sidecarPath,
                summary,
                summaryText: sidecar.summaryText ?? "",
                endedAt,
            });
        }
        catch (err) {
            logger.warn(`[Realtime] Failed to read sidecar after summarization (${sessionId}): ${err instanceof Error ? err.message : String(err)}`);
        }
    })
        .catch((err) => {
        logger.warn(`[Realtime] Voice summarization failed: ${err instanceof Error ? err.message : String(err)}`);
    });
}
export function createRealtimeBackend(config) {
    const botToken = config.channels.telegram?.botToken;
    const openaiKey = resolveOpenAIApiKey(config);
    const sessions = new Map();
    const voiceFinishedListeners = [];
    function emitVoiceSessionFinished(event) {
        for (const listener of voiceFinishedListeners) {
            try {
                listener(event);
            }
            catch (err) {
                logger.warn(`[Realtime] voice session listener threw: ${err instanceof Error ? err.message : String(err)}`);
            }
        }
    }
    getVoiceDb();
    const moduleDir = path.dirname(new URL(import.meta.url).pathname);
    const assetsDir = fs.existsSync(path.join(moduleDir, "assets", "index.html"))
        ? path.join(moduleDir, "assets")
        : path.join(moduleDir, "realtime", "assets");
    const ownerConfig = loadOwnerConfig();
    const ownerChatId = ownerConfig.telegramChatId;
    function pruneSessions() {
        const now = Date.now();
        for (const [sessionId, session] of sessions) {
            if (session.expiresAt <= now)
                sessions.delete(sessionId);
        }
    }
    function requireSession(url, body) {
        pruneSessions();
        const { sessionId, sessionSecret } = getSessionCredentials(url, body);
        if (!sessionId || !sessionSecret)
            return null;
        const session = sessions.get(sessionId);
        if (!session)
            return null;
        if (session.sessionSecret !== sessionSecret)
            return null;
        session.lastActivityAt = Date.now();
        return session;
    }
    function getActiveSessionInfo() {
        pruneSessions();
        if (sessions.size === 0)
            return null;
        const session = [...sessions.values()]
            .sort((a, b) => b.lastActivityAt - a.lastActivityAt)[0];
        return {
            sessionId: session.sessionId,
            startedAt: new Date(session.startedAt).toISOString(),
            lastActivityAt: new Date(session.lastActivityAt).toISOString(),
            expiresAt: new Date(session.expiresAt).toISOString(),
            ownerName: session.telegramUser.first_name,
        };
    }
    async function handleRequest(req, res) {
        const url = new URL(req.url || "/", `http://${req.headers.host ?? "localhost"}`);
        const method = req.method || "GET";
        if (method === "OPTIONS") {
            res.writeHead(204, {
                "Access-Control-Allow-Origin": "*",
                "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
                "Access-Control-Allow-Headers": "Content-Type",
            });
            res.end();
            return;
        }
        if (method === "GET" && (url.pathname === "/" || url.pathname === "/index.html")) {
            const htmlPath = path.join(assetsDir, "index.html");
            if (fs.existsSync(htmlPath))
                sendText(res, 200, fs.readFileSync(htmlPath, "utf-8"), "text/html; charset=utf-8");
            else
                sendText(res, 404, "Voice UI not found");
            return;
        }
        if (method === "GET" && url.pathname.startsWith("/samples/")) {
            const sampleName = path.basename(url.pathname);
            if (!/^[a-z]+\.mp3$/.test(sampleName)) {
                sendText(res, 400, "Invalid sample name");
                return;
            }
            const samplePath = path.join(assetsDir, "samples", sampleName);
            if (fs.existsSync(samplePath)) {
                const data = fs.readFileSync(samplePath);
                res.writeHead(200, { "Content-Type": "audio/mpeg", "Content-Length": data.length, "Cache-Control": "public, max-age=86400" });
                res.end(data);
            }
            else {
                sendText(res, 404, "Sample not found");
            }
            return;
        }
        if (method === "GET" && url.pathname === "/api/health") {
            sendJson(res, 200, { status: "ok", tools: TOOLS.map((t) => t.name), model: VOICE_MODEL, interface: "unified" });
            return;
        }
        if (method === "POST" && url.pathname === "/api/session") {
            if (!openaiKey) {
                sendJson(res, 500, { error: "OpenAI API key not configured" });
                return;
            }
            if (!botToken || ownerChatId === undefined) {
                sendJson(res, 500, { error: "Telegram owner authentication is not configured" });
                return;
            }
            const initData = req.headers["x-telegram-init-data"];
            if (typeof initData !== "string") {
                sendJson(res, 401, { error: "Telegram initData required" });
                return;
            }
            const telegramUser = parseInitData(initData, botToken);
            if (telegramUser?.id !== ownerChatId) {
                sendJson(res, 403, { error: "Realtime is owner-only" });
                return;
            }
            const sdpOffer = await readBody(req);
            if (!sdpOffer) {
                sendJson(res, 400, { error: "SDP offer required in request body" });
                return;
            }
            try {
                const instructions = buildRealtimeContextFromAgent(config);
                logger.system(`[Realtime] Creating owner voice session with ${TOOLS.length} tools, voice: ${VOICE_NAME}`);
                const secretResponse = await fetch("https://api.openai.com/v1/realtime/client_secrets", {
                    method: "POST",
                    headers: { Authorization: `Bearer ${openaiKey}`, "Content-Type": "application/json" },
                    body: "{}",
                });
                if (!secretResponse.ok) {
                    const errText = await secretResponse.text();
                    logger.warn(`[Realtime] OpenAI client_secrets error: ${secretResponse.status} ${errText}`);
                    sendText(res, secretResponse.status, errText);
                    return;
                }
                const secretData = (await secretResponse.json());
                const ephemeralKey = secretData.value;
                if (!ephemeralKey) {
                    sendJson(res, 500, { error: "No ephemeral key returned" });
                    return;
                }
                const rtcFd = new FormData();
                rtcFd.set("sdp", sdpOffer);
                rtcFd.set("model", VOICE_MODEL);
                const rtcResponse = await fetch(`https://api.openai.com/v1/realtime/calls?model=${encodeURIComponent(VOICE_MODEL)}`, {
                    method: "POST",
                    headers: { Authorization: `Bearer ${ephemeralKey}` },
                    body: rtcFd,
                });
                if (!rtcResponse.ok) {
                    const errText = await rtcResponse.text();
                    logger.warn(`[Realtime] OpenAI WebRTC error: ${rtcResponse.status} ${errText}`);
                    sendText(res, rtcResponse.status, errText);
                    return;
                }
                const sessionId = `voice-${telegramUser.id}-${new Date().toISOString().replace(/[:.]/g, "-")}-${randomToken().slice(0, 8)}`;
                const sessionSecret = randomToken();
                const now = Date.now();
                for (const [existingSessionId, existingSession] of sessions) {
                    if (existingSession.telegramUser.id === telegramUser.id) {
                        sessions.delete(existingSessionId);
                    }
                }
                sessions.set(sessionId, {
                    sessionId,
                    sessionSecret,
                    instructions,
                    telegramUser,
                    startedAt: now,
                    expiresAt: now + SESSION_TTL_MS,
                    lastActivityAt: now,
                });
                const sdpAnswer = await rtcResponse.text();
                res.writeHead(200, {
                    "Content-Type": "application/json; charset=utf-8",
                    "Access-Control-Allow-Origin": "*",
                });
                res.end(JSON.stringify({ sdp: sdpAnswer, sessionId, sessionSecret }));
            }
            catch (err) {
                const msg = err instanceof Error ? err.message : String(err);
                logger.warn(`[Realtime] Session error: ${msg}`);
                sendJson(res, 500, { error: msg });
            }
            return;
        }
        if (method === "GET" && url.pathname === "/api/session-config") {
            const session = requireSession(url);
            if (!session) {
                sendJson(res, 401, { error: "Invalid or expired realtime session" });
                return;
            }
            const requestedVoice = url.searchParams.get("voice");
            const voice = requestedVoice && VALID_VOICES.includes(requestedVoice) ? requestedVoice : VOICE_NAME;
            const transcriptionLanguage = ownerConfig.language || "en";
            sendJson(res, 200, {
                type: "session.update",
                session: {
                    type: "realtime",
                    instructions: session.instructions,
                    reasoning: { effort: "low" },
                    audio: {
                        input: {
                            noise_reduction: { type: "near_field" },
                            transcription: { model: "gpt-4o-mini-transcribe", language: transcriptionLanguage },
                            turn_detection: { type: "semantic_vad", eagerness: "medium", create_response: true, interrupt_response: true },
                        },
                        output: { voice, speed: 1.0 },
                    },
                    tools: TOOLS,
                    tool_choice: "auto",
                },
            });
            return;
        }
        if (method === "POST" && url.pathname === "/api/execute-tool") {
            try {
                const body = parseJsonObject(await readBody(req));
                const session = requireSession(url, body);
                if (!session) {
                    sendJson(res, 401, { error: "Invalid or expired realtime session" });
                    return;
                }
                const name = typeof body.name === "string" ? body.name : "";
                const callId = typeof body.call_id === "string" ? body.call_id : undefined;
                if (!name) {
                    sendJson(res, 400, { error: "name is required" });
                    return;
                }
                const rawArgs = body.arguments;
                const args = typeof rawArgs === "string" ? parseJsonObject(rawArgs || "{}") : (typeof rawArgs === "object" && rawArgs !== null ? rawArgs : {});
                logger.system(`[Realtime] Tool call (HTTP): ${name} (${callId || "no-id"}) args=${JSON.stringify(redactRealtimeToolPayload(name, args))}`);
                const result = await executeTool(config, name, args);
                logger.system(`[Realtime] Tool result (HTTP): ${name} -> ${JSON.stringify(redactRealtimeToolPayload(name, result)).substring(0, 200)}`);
                sendJson(res, 200, { call_id: callId, result: JSON.stringify(result) });
            }
            catch (err) {
                const errMsg = err instanceof Error ? err.message : String(err);
                logger.warn(`[Realtime] Tool execution error: ${errMsg}`);
                sendJson(res, 500, { error: errMsg });
            }
            return;
        }
        if (method === "POST" && url.pathname === "/api/end-session") {
            try {
                const body = parseJsonObject(await readBody(req));
                const session = requireSession(url, body);
                if (!session) {
                    sendJson(res, 401, { error: "Invalid or expired realtime session" });
                    return;
                }
                const transcript = body.transcript;
                if (!transcript || !Array.isArray(transcript) || transcript.length === 0) {
                    sendJson(res, 400, { error: "Non-empty transcript array is required" });
                    return;
                }
                const startedAt = transcript[0]?.timestamp || new Date().toISOString();
                const endedAt = transcript[transcript.length - 1]?.timestamp || new Date().toISOString();
                const sessId = session.sessionId;
                const userName = session.telegramUser.first_name || null;
                const db = getVoiceDb();
                db.prepare(`
          INSERT OR REPLACE INTO voice_sessions
            (session_id, user_id, user_name, timestamp_start, timestamp_end, duration, summary, transcript, message_count)
          VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
        `).run(sessId, String(session.telegramUser.id), userName, startedAt, endedAt, body.duration || null, null, JSON.stringify(transcript), transcript.length);
                const transcriptsDir = path.join(getConfigDir(), "voice-transcripts");
                if (!fs.existsSync(transcriptsDir))
                    fs.mkdirSync(transcriptsDir, { recursive: true });
                fs.writeFileSync(getTranscriptPath(sessId), JSON.stringify({
                    schemaVersion: REALTIME_SIDECAR_SCHEMA_VERSION,
                    source: REALTIME_SIDECAR_SOURCE,
                    sessionId: sessId,
                    telegramUser: session.telegramUser,
                    duration: body.duration || null,
                    startedAt,
                    endedAt,
                    messageCount: transcript.length,
                    transcript,
                    processedByAgent: false,
                    summaryStatus: "pending",
                }, null, 2));
                logger.system(`[Realtime] Transcript saved for agent merge: ${sessId}.json`);
                summarizeInBackground(config, sessId, endedAt, emitVoiceSessionFinished);
                sessions.delete(sessId);
                sendJson(res, 200, { ok: true, sessionId: sessId, summaryStatus: "pending" });
            }
            catch (err) {
                const msg = err instanceof Error ? err.message : String(err);
                logger.warn(`[Realtime] End-session error: ${msg}`);
                sendJson(res, 500, { error: msg });
            }
            return;
        }
        if (method === "GET" && url.pathname === "/api/transcripts") {
            const session = requireSession(url);
            if (!session) {
                sendJson(res, 401, { error: "Invalid or expired realtime session" });
                return;
            }
            try {
                const rows = getVoiceDb().prepare(`
          SELECT session_id, user_id, user_name, timestamp_start, timestamp_end,
                 duration, summary, transcript, message_count
          FROM voice_sessions
          ORDER BY timestamp_start DESC
          LIMIT 50
        `).all();
                sendJson(res, 200, rows.map((r) => ({ sessionId: r.session_id, userId: r.user_id, userName: r.user_name, startedAt: r.timestamp_start, endedAt: r.timestamp_end, duration: r.duration, summary: r.summary, messageCount: r.message_count })));
            }
            catch (err) {
                sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
            }
            return;
        }
        const transcriptDetailMatch = /^\/api\/transcripts\/([\w-]+)$/.exec(url.pathname);
        if (method === "GET" && transcriptDetailMatch) {
            const session = requireSession(url);
            if (!session) {
                sendJson(res, 401, { error: "Invalid or expired realtime session" });
                return;
            }
            try {
                const row = getVoiceDb().prepare(`
          SELECT session_id, user_id, user_name, timestamp_start, timestamp_end,
                 duration, summary, transcript, message_count
          FROM voice_sessions WHERE session_id = ?
        `).get(transcriptDetailMatch[1]);
                if (!row)
                    sendJson(res, 404, { error: "Session not found" });
                else
                    sendJson(res, 200, { ...row, transcript: JSON.parse(row.transcript) });
            }
            catch (err) {
                sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
            }
            return;
        }
        if (method === "POST" && url.pathname === "/api/verify-telegram") {
            const body = parseJsonObject(await readBody(req));
            const initData = typeof body.initData === "string" ? body.initData : "";
            const user = botToken ? parseInitData(initData, botToken) : null;
            if (user && ownerChatId !== undefined && user.id === ownerChatId)
                sendJson(res, 200, { ok: true, user });
            else
                sendJson(res, 401, { ok: false, error: "Invalid Telegram owner data" });
            return;
        }
        sendJson(res, 404, { error: "Not found" });
    }
    logger.system(`[Realtime] Backend ready (model=${VOICE_MODEL} voice=${VOICE_NAME} tools=${TOOLS.map((t) => t.name).join(", ")})`);
    return {
        handleRequest,
        getActiveSessionInfo,
        onVoiceSessionFinished: (listener) => { voiceFinishedListeners.push(listener); },
        close: () => new Promise((resolve) => {
            sessions.clear();
            voiceFinishedListeners.length = 0;
            closeVoiceDb();
            resolve();
        }),
    };
}
export function startRealtimeServer(config) {
    const port = 3200;
    const backend = createRealtimeBackend(config);
    const pathPrefix = "/realtime";
    const server = http.createServer(async (req, res) => {
        try {
            if (req.url) {
                const url = new URL(req.url, `http://localhost:${port}`);
                if (url.pathname.startsWith(pathPrefix)) {
                    url.pathname = url.pathname.slice(pathPrefix.length) || "/";
                    req.url = url.pathname + url.search;
                }
            }
            await backend.handleRequest(req, res);
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            logger.warn(`[Realtime] Request error: ${msg}`);
            if (!res.headersSent)
                sendJson(res, 500, { error: msg });
        }
    });
    server.on("upgrade", (_req, socket) => {
        socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
        socket.destroy();
    });
    server.listen(port, "0.0.0.0", () => logger.system(`[Realtime] Standalone server running on http://0.0.0.0:${port}`));
    return {
        server,
        port,
        close: () => new Promise((resolve) => {
            void backend.close().catch(() => { }).finally(() => server.close(() => resolve()));
        }),
    };
}
//# sourceMappingURL=server.js.map