/**
 * rMeets module — video meetings powered by Jitsi + Meeting Intelligence.
 *
 * Hub page with Quick Meet + room name input,
 * room pages embed Jitsi via renderExternalAppShell,
 * recordings/search pages pull from the Meeting Intelligence API.
 */

import { Hono } from "hono";
import { renderShell, renderExternalAppShell, escapeHtml } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { renderLanding } from "./landing";
import * as Automerge from "@automerge/automerge";
import { verifyToken, extractToken } from "../../server/auth";
import type { SyncServer } from '../../server/local-first/sync-server';
import { meetsSchema, meetsDocId } from './schemas';
import type { MeetsDoc, Meeting } from './schemas';

let _syncServer: SyncServer | null = null;

function ensureMeetsDoc(space: string): MeetsDoc {
    const docId = meetsDocId(space);
    let doc = _syncServer!.getDoc<MeetsDoc>(docId);
    if (!doc) {
        doc = Automerge.change(Automerge.init<MeetsDoc>(), 'init meets', (d) => {
            const init = meetsSchema.init();
            Object.assign(d, init);
            d.meta.spaceSlug = space;
        });
        _syncServer!.setDoc(docId, doc);
    }
    return doc;
}

const JITSI_URL = process.env.JITSI_URL || "https://jeffsi.localvibe.live";
const MI_API_URL = process.env.MEETING_INTELLIGENCE_API_URL || "http://meeting-intelligence-api:8000";
const MI_INTERNAL_KEY = process.env.MI_INTERNAL_KEY || "";
const routes = new Hono();

// Subdomain-aware base for this module: `/rmeets` on {space}.rspace.online,
// `/{space}/rmeets` on localhost or bare domain.
function rmeetsBase(c: any): string {
	const space = c.req.param("space") || "demo";
	return c.get("isSubdomain") ? "/rmeets" : `/${escapeHtml(space)}/rmeets`;
}

// ── Meeting Intelligence API helper ──

async function miApiFetch(path: string, options?: { method?: string; body?: any }): Promise<{ ok: boolean; data?: any; error?: string }> {
	try {
		const controller = new AbortController();
		const timeout = setTimeout(() => controller.abort(), 8000);
		const headers: Record<string, string> = {};
		if (MI_INTERNAL_KEY) headers["X-MI-Internal-Key"] = MI_INTERNAL_KEY;
		const fetchOpts: RequestInit = { signal: controller.signal, headers };
		if (options?.method) fetchOpts.method = options.method;
		if (options?.body) {
			headers["Content-Type"] = "application/json";
			fetchOpts.body = JSON.stringify(options.body);
		}
		const res = await fetch(`${MI_API_URL}${path}`, fetchOpts);
		clearTimeout(timeout);
		if (!res.ok) return { ok: false, error: `API returned ${res.status}` };
		return { ok: true, data: await res.json() };
	} catch (e: any) {
		return { ok: false, error: e?.message || "Connection failed" };
	}
}

function miUnavailableHtml(message = "Meeting intelligence is temporarily unavailable. Please try again later."): string {
	return `<div style="max-width:600px;margin:3rem auto;padding:2rem;text-align:center;color:var(--rs-text-secondary)">
	<div style="font-size:2.5rem;margin-bottom:1rem">&#128268;</div>
	<p>${escapeHtml(message)}</p>
</div>`;
}

// ── Shared styles for MI pages ──

const MI_STYLES = `<style>
.mi-page{max-width:900px;margin:2rem auto;padding:0 1.5rem}
.mi-page h1{font-size:1.8rem;margin-bottom:.25rem;color:var(--rs-text-primary)}
.mi-page .mi-subtitle{color:var(--rs-text-secondary);margin-bottom:2rem}
.mi-cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1rem}
.mi-card{padding:1.25rem;border-radius:12px;background:var(--rs-bg-surface);border:1px solid var(--rs-border);text-decoration:none;color:inherit;transition:border-color .15s,background .15s;display:block}
.mi-card:hover{border-color:var(--rs-accent);background:var(--rs-bg-hover)}
.mi-card h3{margin:0 0 .5rem;font-size:1.05rem;color:var(--rs-text-primary)}
.mi-card .mi-meta{font-size:.8rem;color:var(--rs-text-secondary);margin-bottom:.5rem}
.mi-chips{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.75rem}
.mi-chip{display:inline-block;padding:.15rem .6rem;border-radius:99px;font-size:.75rem;font-weight:500}
.mi-chip--ok{background:var(--rs-accent);color:#fff;opacity:.8}
.mi-chip--pending{background:var(--rs-border);color:var(--rs-text-secondary)}
.mi-badge{display:inline-block;padding:.2rem .6rem;border-radius:6px;font-size:.75rem;font-weight:600;text-transform:uppercase}
.mi-badge--completed{background:#22c55e22;color:#22c55e}
.mi-badge--processing{background:#eab30822;color:#eab308}
.mi-badge--recording{background:#ef444422;color:#ef4444}
.mi-badge--failed{background:#ef444422;color:#ef4444}
.mi-badge--transcribing,.mi-badge--diarizing,.mi-badge--summarizing,.mi-badge--extracting_audio{background:#3b82f622;color:#3b82f6}
.mi-tabs{display:flex;gap:0;border-bottom:1px solid var(--rs-border);margin-bottom:1.5rem}
.mi-tab{padding:.75rem 1.25rem;text-decoration:none;color:var(--rs-text-secondary);font-size:.9rem;font-weight:500;border-bottom:2px solid transparent;transition:color .15s,border-color .15s}
.mi-tab:hover{color:var(--rs-text-primary)}
.mi-tab--active{color:var(--rs-accent);border-bottom-color:var(--rs-accent)}
.mi-detail-header{margin-bottom:1.5rem}
.mi-detail-header h1{margin-bottom:.5rem}
.mi-detail-meta{display:flex;gap:1.5rem;flex-wrap:wrap;font-size:.85rem;color:var(--rs-text-secondary)}
.mi-transcript{display:flex;flex-direction:column;gap:1rem}
.mi-segment{padding:.75rem 1rem;border-left:3px solid var(--rs-accent);background:var(--rs-bg-surface);border-radius:0 8px 8px 0}
.mi-segment .mi-speaker{font-weight:600;color:var(--rs-accent);font-size:.85rem}
.mi-segment .mi-timestamp{font-size:.75rem;color:var(--rs-text-secondary);margin-left:.75rem}
.mi-segment .mi-text{margin-top:.35rem;color:var(--rs-text-primary);line-height:1.5}
.mi-summary{color:var(--rs-text-primary);line-height:1.6}
.mi-summary h2{font-size:1.2rem;margin:1.5rem 0 .5rem;color:var(--rs-text-primary)}
.mi-summary h2:first-child{margin-top:0}
.mi-summary ul{padding-left:1.5rem;margin:.5rem 0}
.mi-summary li{margin:.25rem 0}
.mi-speakers{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:1rem}
.mi-speaker-card{padding:1rem;border-radius:10px;background:var(--rs-bg-surface);border:1px solid var(--rs-border)}
.mi-speaker-card h4{margin:0 0 .25rem;color:var(--rs-text-primary)}
.mi-speaker-card p{margin:0;font-size:.85rem;color:var(--rs-text-secondary)}
.mi-search-form{display:flex;gap:.75rem;margin-bottom:2rem}
.mi-search-form input{flex:1;padding:.65rem 1rem;border-radius:8px;border:1px solid var(--rs-input-border);background:var(--rs-input-bg);color:var(--rs-input-text);font-size:.95rem}
.mi-search-form input:focus{outline:none;border-color:var(--rs-accent)}
.mi-search-form button{padding:.65rem 1.5rem;border-radius:8px;border:none;background:var(--rs-accent);color:#fff;font-weight:600;cursor:pointer;font-size:.95rem}
.mi-search-form button:hover{background:var(--rs-accent-hover)}
.mi-result{padding:1rem 1.25rem;border-radius:10px;background:var(--rs-bg-surface);border:1px solid var(--rs-border);margin-bottom:.75rem;display:block;text-decoration:none;color:inherit;transition:border-color .15s}
.mi-result:hover{border-color:var(--rs-accent)}
.mi-result h3{margin:0 0 .25rem;font-size:1rem;color:var(--rs-text-primary)}
.mi-result .mi-excerpt{font-size:.85rem;color:var(--rs-text-secondary);line-height:1.4}
.mi-result mark{background:var(--rs-accent);color:#fff;padding:0 .2rem;border-radius:3px}
@media(max-width:600px){.mi-page{margin:1rem auto;padding:0 .75rem}.mi-cards{grid-template-columns:1fr}}
</style>`;

// ── CRUD: Meetings ──

routes.get("/api/meetings", (c) => {
    if (!_syncServer) return c.json({ meetings: [] });
    const space = c.req.param("space") || "demo";
    const doc = ensureMeetsDoc(space);
    const meetings = Object.values(doc.meetings || {}).sort((a, b) => b.createdAt - a.createdAt);
    return c.json({ meetings });
});

routes.post("/api/meetings", async (c) => {
    const token = extractToken(c.req.raw.headers);
    if (!token) return c.json({ error: "Authentication required" }, 401);
    try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
    if (!_syncServer) return c.json({ error: "Not initialized" }, 503);
    const space = c.req.param("space") || "demo";
    const { title, roomName, scheduledAt, participants = [] } = await c.req.json();
    if (!title) return c.json({ error: "title required" }, 400);
    const id = crypto.randomUUID();
    const docId = meetsDocId(space);
    ensureMeetsDoc(space);
    _syncServer.changeDoc<MeetsDoc>(docId, `create meeting ${id}`, (d) => {
        d.meetings[id] = { id, roomName: roomName || id, title, scheduledAt: scheduledAt || Date.now(), hostDid: null, participants, createdAt: Date.now() };
    });
    const updated = _syncServer.getDoc<MeetsDoc>(docId)!;
    return c.json(updated.meetings[id], 201);
});

routes.delete("/api/meetings/:id", async (c) => {
    const token = extractToken(c.req.raw.headers);
    if (!token) return c.json({ error: "Authentication required" }, 401);
    try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
    if (!_syncServer) return c.json({ error: "Not initialized" }, 503);
    const space = c.req.param("space") || "demo";
    const id = c.req.param("id");
    const docId = meetsDocId(space);
    const doc = ensureMeetsDoc(space);
    if (!doc.meetings[id]) return c.json({ error: "Not found" }, 404);
    _syncServer.changeDoc<MeetsDoc>(docId, `delete meeting ${id}`, (d) => { delete d.meetings[id]; });
    return c.json({ ok: true });
});

// ── Direct Jitsi lobby ──

routes.get("/meet", (c) => {
	const space = c.req.param("space") || "demo";
	return c.html(renderExternalAppShell({
		title: `Jitsi Meet — rMeets | rSpace`,
		moduleId: "rmeets",
		spaceSlug: space,
		modules: getModuleInfoList(),
		appUrl: JITSI_URL,
		appName: "Jitsi Meet",
		theme: "dark",
	}));
});

// ── Recordings list ──

routes.get("/recordings", async (c) => {
	const space = c.req.param("space") || "demo";
	const base = rmeetsBase(c);
	const prefix = encodeURIComponent(space + "_");
	const result = await miApiFetch(`/meetings?limit=50&sort=-created_at&conference_prefix=${prefix}`);

	let body: string;
	if (!result.ok) {
		body = miUnavailableHtml();
	} else {
		const meetings = Array.isArray(result.data) ? result.data : (result.data?.meetings ?? result.data?.items ?? []);
		if (meetings.length === 0) {
			body = `<div class="mi-page"><h1>Recordings</h1><p class="mi-subtitle">No recorded meetings yet. Start a meeting and enable recording to see it here.</p></div>`;
		} else {
			const cards = meetings.map((m: any) => {
				const date = m.created_at ? new Date(m.created_at).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : "";
				const duration = m.duration_seconds ? `${Math.round(m.duration_seconds / 60)}min` : "";
				const participants = m.participant_count ?? m.participants?.length ?? "";
				const status = (m.status || "completed").toLowerCase();
				const badgeClass = `mi-badge--${status}`;
				const hasTranscript = m.has_transcript ?? (m.segment_count != null && m.segment_count > 0) ?? m.transcript_status === "completed";
				const hasSummary = m.has_summary ?? m.summary_status === "completed";
				return `<a class="mi-card" href="${base}/recordings/${escapeHtml(String(m.id))}">
	<h3>${escapeHtml(m.title || m.room_name || `Meeting ${m.id}`)}</h3>
	<div class="mi-meta">${[date, duration, participants ? `${participants} participants` : ""].filter(Boolean).join(" &middot; ")}</div>
	<span class="mi-badge ${badgeClass}">${escapeHtml(status)}</span>
	<div class="mi-chips">
		${hasTranscript ? `<span class="mi-chip mi-chip--ok">Transcript</span>` : `<span class="mi-chip mi-chip--pending">No transcript</span>`}
		${hasSummary ? `<span class="mi-chip mi-chip--ok">Summary</span>` : `<span class="mi-chip mi-chip--pending">No summary</span>`}
	</div>
</a>`;
			}).join("\n");
			body = `<div class="mi-page"><h1>Recordings</h1><p class="mi-subtitle">Past meetings with transcripts and AI summaries</p><div class="mi-cards">${cards}</div></div>`;
		}
	}

	return c.html(renderShell({
		title: `Recordings — rMeets | rSpace`,
		moduleId: "rmeets",
		spaceSlug: space,
		modules: getModuleInfoList(),
		styles: MI_STYLES,
		body,
	}));
});

// ── Recording detail redirect ──

routes.get("/recordings/:id", (c) => {
	const space = c.req.param("space") || "demo";
	const id = c.req.param("id");
	return c.redirect(c.get("isSubdomain") ? `/rmeets/recordings/${id}/overview` : `/${space}/rmeets/recordings/${id}/overview`);
});

// ── Recording detail with tabs ──

routes.get("/recordings/:id/:tab", async (c) => {
	const space = c.req.param("space") || "demo";
	const base = rmeetsBase(c);
	const id = c.req.param("id");
	const tab = c.req.param("tab") || "overview";
	const validTabs = ["overview", "transcript", "summary", "speakers"];
	const activeTab = validTabs.includes(tab) ? tab : "overview";

	const skip = { ok: false as const, data: undefined, error: "skipped" };
	const [meetingRes, transcriptRes, summaryRes, speakersRes] = await Promise.all([
		miApiFetch(`/meetings/${id}`),
		activeTab === "transcript" ? miApiFetch(`/meetings/${id}/transcript`) : Promise.resolve(skip),
		activeTab === "summary" ? miApiFetch(`/meetings/${id}/summary`) : Promise.resolve(skip),
		(activeTab === "speakers" || activeTab === "overview") ? miApiFetch(`/meetings/${id}/speakers`) : Promise.resolve(skip),
	]);

	if (!meetingRes.ok) {
		return c.html(renderShell({
			title: `Recording — rMeets | rSpace`,
			moduleId: "rmeets",
			spaceSlug: space,
			modules: getModuleInfoList(),
			styles: MI_STYLES,
			body: miUnavailableHtml("Could not load this recording. The meeting intelligence service may be temporarily unavailable."),
		}));
	}

	const m = meetingRes.data;
	const title = m.title || m.room_name || `Meeting ${id}`;
	const date = m.created_at ? new Date(m.created_at).toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", year: "numeric" }) : "";
	const duration = m.duration_seconds ? `${Math.round(m.duration_seconds / 60)} minutes` : "";
	const participants = m.participant_count ?? m.participants?.length ?? "";

	const tabs = validTabs.map(t => {
		const label = t.charAt(0).toUpperCase() + t.slice(1);
		const cls = t === activeTab ? "mi-tab mi-tab--active" : "mi-tab";
		return `<a class="${cls}" href="${base}/recordings/${escapeHtml(String(id))}/${t}">${label}</a>`;
	}).join("");

	let tabContent: string;

	if (activeTab === "overview") {
		const speakers = speakersRes.ok ? (Array.isArray(speakersRes.data) ? speakersRes.data : speakersRes.data?.speakers ?? []) : [];
		const speakerList = speakers.length > 0
			? `<div class="mi-speakers">${speakers.map((s: any) => {
				const time = s.speaking_time ? `${Math.round(s.speaking_time / 60)}min speaking` : s.speaking_time_seconds ? `${Math.round(s.speaking_time_seconds / 60)}min speaking` : "";
				return `<div class="mi-speaker-card"><h4>${escapeHtml(s.name || s.speaker_label || s.label || `Speaker ${s.speaker_id || s.id}`)}</h4><p>${escapeHtml(time)}</p></div>`;
			}).join("")}</div>`
			: `<p style="color:var(--rs-text-secondary)">No speaker data available.</p>`;
		tabContent = `<div class="mi-detail-meta">
	${date ? `<span>${escapeHtml(date)}</span>` : ""}
	${duration ? `<span>${escapeHtml(duration)}</span>` : ""}
	${participants ? `<span>${participants} participants</span>` : ""}
</div>
<h2 style="font-size:1.1rem;margin:1.5rem 0 .75rem;color:var(--rs-text-primary)">Speakers</h2>
${speakerList}`;
	} else if (activeTab === "transcript") {
		if (!transcriptRes.ok) {
			tabContent = `<p style="color:var(--rs-text-secondary)">Transcript not available yet.</p>`;
		} else {
			const segments = Array.isArray(transcriptRes.data) ? transcriptRes.data : (transcriptRes.data?.segments ?? transcriptRes.data?.transcript ?? []);
			if (segments.length === 0) {
				tabContent = `<p style="color:var(--rs-text-secondary)">Transcript is empty.</p>`;
			} else {
				tabContent = `<div class="mi-transcript">${segments.map((seg: any) => {
					const speaker = seg.speaker || seg.speaker_name || seg.speaker_label || seg.label || "Unknown";
					const ts = seg.start_time != null ? formatTimestamp(seg.start_time) : "";
					const text = seg.text || seg.content || "";
					return `<div class="mi-segment">
	<span class="mi-speaker">${escapeHtml(speaker)}</span><span class="mi-timestamp">${escapeHtml(ts)}</span>
	<div class="mi-text">${escapeHtml(text)}</div>
</div>`;
				}).join("")}</div>`;
			}
		}
	} else if (activeTab === "summary") {
		if (!summaryRes.ok) {
			tabContent = `<p style="color:var(--rs-text-secondary)">Summary not available yet.</p>`;
		} else {
			const summary = summaryRes.data;
			const overview = summary.summary_text || summary.overview || summary.summary || summary.text || "";
			const keyPoints = summary.key_points ?? summary.keyPoints ?? [];
			const actionItems = summary.action_items ?? summary.actionItems ?? [];
			const decisions = summary.decisions ?? [];

			let html = `<div class="mi-summary">`;
			if (overview) html += `<h2>Overview</h2><p>${escapeHtml(overview)}</p>`;
			if (keyPoints.length > 0) html += `<h2>Key Points</h2><ul>${keyPoints.map((p: string) => `<li>${escapeHtml(p)}</li>`).join("")}</ul>`;
			if (actionItems.length > 0) html += `<h2>Action Items</h2><ul>${actionItems.map((a: any) => `<li>${escapeHtml(typeof a === "string" ? a : a.task || a.text || a.description || "")}</li>`).join("")}</ul>`;
			if (decisions.length > 0) html += `<h2>Decisions</h2><ul>${decisions.map((d: any) => `<li>${escapeHtml(typeof d === "string" ? d : d.text || d.description || "")}</li>`).join("")}</ul>`;
			if (!overview && keyPoints.length === 0 && actionItems.length === 0) html += `<p style="color:var(--rs-text-secondary)">Summary content is empty.</p>`;
			html += `</div>`;
			tabContent = html;
		}
	} else if (activeTab === "speakers") {
		const speakers = speakersRes.ok ? (Array.isArray(speakersRes.data) ? speakersRes.data : speakersRes.data?.speakers ?? []) : [];
		if (speakers.length === 0) {
			tabContent = `<p style="color:var(--rs-text-secondary)">No speaker data available.</p>`;
		} else {
			tabContent = `<div class="mi-speakers">${speakers.map((s: any) => {
				const time = s.speaking_time ? `${Math.round(s.speaking_time / 60)}min speaking` : s.speaking_time_seconds ? `${Math.round(s.speaking_time_seconds / 60)}min speaking` : "";
				const segments = s.segment_count ?? s.num_segments ?? "";
				return `<div class="mi-speaker-card"><h4>${escapeHtml(s.name || s.speaker_label || s.label || `Speaker ${s.speaker_id || s.id}`)}</h4><p>${[time, segments ? `${segments} segments` : ""].filter(Boolean).join(" &middot; ")}</p></div>`;
			}).join("")}</div>`;
		}
	} else {
		tabContent = "";
	}

	return c.html(renderShell({
		title: `${title} — rMeets | rSpace`,
		moduleId: "rmeets",
		spaceSlug: space,
		modules: getModuleInfoList(),
		styles: MI_STYLES,
		body: `<div class="mi-page">
	<div class="mi-detail-header"><h1>${escapeHtml(title)}</h1></div>
	<div class="mi-tabs">${tabs}</div>
	${tabContent}
</div>`,
	}));
});

// ── Search ──

routes.get("/search", async (c) => {
	const space = c.req.param("space") || "demo";
	const base = rmeetsBase(c);
	const q = c.req.query("q") || "";

	let resultsHtml = "";
	if (q) {
		const result = await miApiFetch("/search", { method: "POST", body: { query: q, limit: 30 } });
		if (!result.ok) {
			resultsHtml = miUnavailableHtml();
		} else {
			const hits = Array.isArray(result.data) ? result.data : (result.data?.results ?? result.data?.hits ?? []);
			if (hits.length === 0) {
				resultsHtml = `<p style="color:var(--rs-text-secondary)">No results found for "${escapeHtml(q)}".</p>`;
			} else {
				resultsHtml = hits.map((h: any) => {
					const meetingId = h.meeting_id ?? h.meetingId ?? h.id;
					const title = h.meeting_title || h.title || `Meeting ${meetingId}`;
					const excerpt = h.highlight || h.excerpt || h.text || "";
					return `<a class="mi-result" href="${base}/recordings/${escapeHtml(String(meetingId))}/transcript">
	<h3>${escapeHtml(title)}</h3>
	<div class="mi-excerpt">${excerpt}</div>
</a>`;
				}).join("");
			}
		}
	}

	return c.html(renderShell({
		title: `Search — rMeets | rSpace`,
		moduleId: "rmeets",
		spaceSlug: space,
		modules: getModuleInfoList(),
		styles: MI_STYLES,
		body: `<div class="mi-page">
	<h1>Search Transcripts</h1>
	<p class="mi-subtitle">Full-text search across all meeting transcripts</p>
	<form class="mi-search-form" method="get" action="${base}/search">
		<input type="text" name="q" placeholder="Search meetings..." value="${escapeHtml(q)}">
		<button type="submit">Search</button>
	</form>
	${resultsHtml}
</div>`,
	}));
});

// ── MI API proxy (for client-side JS, avoids CORS) ──

routes.all("/api/mi-proxy/*", async (c) => {
	const proxyPath = c.req.path.replace(/^\/[^/]+\/rmeets\/api\/mi-proxy/, "");
	const url = new URL(`${MI_API_URL}${proxyPath}`);
	// Forward query params
	const query = c.req.query();
	for (const [k, v] of Object.entries(query)) {
		if (typeof v === "string") url.searchParams.set(k, v);
	}
	try {
		const controller = new AbortController();
		const timeout = setTimeout(() => controller.abort(), 8000);
		const proxyHeaders: Record<string, string> = { "Content-Type": "application/json" };
		if (MI_INTERNAL_KEY) proxyHeaders["X-MI-Internal-Key"] = MI_INTERNAL_KEY;
		const upstream = await fetch(url.toString(), {
			method: c.req.method,
			headers: proxyHeaders,
			body: c.req.method !== "GET" && c.req.method !== "HEAD" ? await c.req.text() : undefined,
			signal: controller.signal,
		});
		clearTimeout(timeout);
		const body = await upstream.text();
		return new Response(body, {
			status: upstream.status,
			headers: { "Content-Type": upstream.headers.get("Content-Type") || "application/json" },
		});
	} catch {
		return c.json({ error: "Meeting intelligence service unavailable" }, 502);
	}
});

// ── Hub page ──

routes.get("/", (c) => {
	const space = c.req.param("space") || "demo";
	const base = rmeetsBase(c);
	const randomId = Math.random().toString(36).slice(2, 10);
	return c.html(renderShell({
		title: `rMeets — ${space} | rSpace`,
		moduleId: "rmeets",
		spaceSlug: space,
		modules: getModuleInfoList(),
		styles: `<style>
.rs-hub{max-width:720px;margin:2rem auto;padding:0 1.5rem}
.rs-hub h1{font-size:1.8rem;margin-bottom:.5rem;color:var(--rs-text-primary)}
.rs-hub p{color:var(--rs-text-secondary);margin-bottom:2rem}
.rs-nav{display:flex;flex-direction:column;gap:1rem}
.rs-nav a,.rs-nav .room-form{display:flex;align-items:center;gap:1rem;padding:1.25rem 1.5rem;border-radius:12px;background:var(--rs-bg-surface);border:1px solid var(--rs-border);text-decoration:none;color:inherit;transition:border-color .15s,background .15s}
.rs-nav a:hover,.rs-nav .room-form:hover{border-color:var(--rs-accent);background:var(--rs-bg-hover)}
.rs-nav .nav-icon{font-size:2rem;flex-shrink:0}
.rs-nav .nav-body h3{margin:0 0 .25rem;font-size:1.1rem;color:var(--rs-text-primary)}
.rs-nav .nav-body p{margin:0;font-size:.85rem;color:var(--rs-text-secondary)}
.room-form{flex-wrap:wrap}
.room-form input[type="text"]{flex:1;min-width:140px;padding:.5rem .75rem;border-radius:8px;border:1px solid var(--rs-input-border);background:var(--rs-input-bg);color:var(--rs-input-text);font-size:.95rem}
.room-form input[type="text"]:focus{outline:none;border-color:var(--rs-accent)}
.room-form button{padding:.5rem 1.25rem;border-radius:8px;border:none;background:var(--rs-accent);color:#fff;font-weight:600;cursor:pointer;font-size:.95rem}
.room-form button:hover{background:var(--rs-accent-hover)}
@media(max-width:600px){.rs-hub{margin:1rem auto;padding:0 .75rem}.rs-nav a,.rs-nav .room-form{padding:1rem;gap:.75rem}.rs-nav .nav-icon{font-size:1.5rem}}
</style>`,
		body: `<div class="rs-hub">
  <h1>rMeets</h1>
  <p>Video meetings powered by Jitsi — no account required</p>
  <nav class="rs-nav">
    <a href="${base}/${randomId}">
      <span class="nav-icon">&#128640;</span>
      <div class="nav-body">
        <h3>Quick Meet</h3>
        <p>Start an instant meeting with a random room name</p>
      </div>
    </a>
    <form class="room-form" onsubmit="event.preventDefault();var n=this.querySelector('input').value.trim();if(n)location.href='${base}/'+encodeURIComponent(n)">
      <span class="nav-icon">&#128279;</span>
      <div class="nav-body">
        <h3>Join a Room</h3>
        <p>Enter a room name to join or create a meeting</p>
      </div>
      <input type="text" placeholder="room-name" required>
      <button type="submit">Join</button>
    </form>
    <a href="${base}/meet">
      <span class="nav-icon">&#128249;</span>
      <div class="nav-body">
        <h3>Jitsi Lobby</h3>
        <p>Open the full Jitsi Meet interface directly</p>
      </div>
    </a>
    <a href="${base}/meeting-intelligence">
      <span class="nav-icon">&#129504;</span>
      <div class="nav-body">
        <h3>Meeting Intelligence</h3>
        <p>Knowledge objects: action items, decisions, and insights across all meetings</p>
      </div>
    </a>
    <a href="${base}/recordings">
      <span class="nav-icon">&#127909;</span>
      <div class="nav-body">
        <h3>Recordings</h3>
        <p>Browse past meetings, transcripts and AI summaries</p>
      </div>
    </a>
    <a href="${base}/search">
      <span class="nav-icon">&#128269;</span>
      <div class="nav-body">
        <h3>Search</h3>
        <p>Full-text search across all meeting transcripts</p>
      </div>
    </a>
  </nav>
</div>`,
	}));
});

// ── Meeting Intelligence knowledge page ──

routes.get("/meeting-intelligence", async (c) => {
	const space = c.req.param("space") || "demo";
	const base = rmeetsBase(c);
	const prefix = encodeURIComponent(space + "_");

	// Fetch space-scoped meetings from MI API
	const result = await miApiFetch(`/meetings?limit=50&sort=-created_at&conference_prefix=${prefix}`);

	let meetingCards = "";
	let aggregateHtml = "";

	if (!result.ok) {
		meetingCards = miUnavailableHtml();
	} else {
		const meetings = Array.isArray(result.data) ? result.data : (result.data?.meetings ?? result.data?.items ?? []);

		if (meetings.length === 0) {
			meetingCards = `<p style="color:var(--rs-text-secondary)">No meetings recorded yet for this space. Start a meeting and enable recording to see it here.</p>`;
		} else {
			// Build meeting cards
			meetingCards = `<div class="mi-cards">${meetings.map((m: any) => {
				const date = m.created_at ? new Date(m.created_at).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : "";
				const duration = m.duration_seconds ? `${Math.round(m.duration_seconds / 60)}min` : "";
				const status = (m.status || "completed").toLowerCase();
				const badgeClass = `mi-badge--${status}`;
				const hasTranscript = m.has_transcript ?? (m.segment_count != null && m.segment_count > 0);
				const hasSummary = m.has_summary ?? m.summary_status === "completed";
				return `<a class="mi-card" href="${base}/recordings/${escapeHtml(String(m.id))}">
	<h3>${escapeHtml(m.title || m.room_name || m.conference_name || `Meeting ${m.id}`)}</h3>
	<div class="mi-meta">${[date, duration].filter(Boolean).join(" &middot; ")}</div>
	<span class="mi-badge ${badgeClass}">${escapeHtml(status)}</span>
	<div class="mi-chips">
		${hasTranscript ? `<span class="mi-chip mi-chip--ok">Transcript</span>` : `<span class="mi-chip mi-chip--pending">No transcript</span>`}
		${hasSummary ? `<span class="mi-chip mi-chip--ok">Summary</span>` : `<span class="mi-chip mi-chip--pending">No summary</span>`}
	</div>
</a>`;
			}).join("\n")}</div>`;

			// Aggregate knowledge from completed meetings with summaries
			const readyMeetings = meetings.filter((m: any) => (m.status === "completed" || m.has_summary));
			if (readyMeetings.length > 0) {
				// Fetch summaries for up to 10 completed meetings
				const summaryPromises = readyMeetings.slice(0, 10).map((m: any) =>
					miApiFetch(`/meetings/${m.id}/summary`).then(r => r.ok ? r.data : null)
				);
				const summaries = (await Promise.all(summaryPromises)).filter(Boolean);

				const allActionItems: string[] = [];
				const allDecisions: string[] = [];
				const allTopics: string[] = [];

				for (const s of summaries) {
					const actions = s.action_items ?? s.actionItems ?? [];
					for (const a of actions) {
						const text = typeof a === "string" ? a : (a.task || a.text || a.description || "");
						if (text) allActionItems.push(text);
					}
					const decisions = s.decisions ?? [];
					for (const d of decisions) {
						const text = typeof d === "string" ? d : (d.text || d.description || "");
						if (text) allDecisions.push(text);
					}
					const topics = s.topics ?? [];
					for (const t of topics) {
						const text = typeof t === "string" ? t : (t.name || t.topic || t.text || "");
						if (text) allTopics.push(text);
					}
				}

				if (allActionItems.length > 0 || allDecisions.length > 0 || allTopics.length > 0) {
					aggregateHtml = `<div class="mi-aggregate">`;
					if (allActionItems.length > 0) {
						aggregateHtml += `<h2>Action Items</h2><ul>${allActionItems.map(a => `<li>${escapeHtml(a)}</li>`).join("")}</ul>`;
					}
					if (allDecisions.length > 0) {
						aggregateHtml += `<h2>Decisions</h2><ul>${allDecisions.map(d => `<li>${escapeHtml(d)}</li>`).join("")}</ul>`;
					}
					if (allTopics.length > 0) {
						aggregateHtml += `<h2>Topics</h2><div class="mi-chips">${allTopics.map(t => `<span class="mi-chip mi-chip--ok">${escapeHtml(t)}</span>`).join("")}</div>`;
					}
					aggregateHtml += `</div>`;
				}
			}
		}
	}

	const extraStyles = `<style>
.mi-aggregate{margin-bottom:2rem;padding:1.5rem;border-radius:12px;background:var(--rs-bg-surface);border:1px solid var(--rs-border)}
.mi-aggregate h2{font-size:1.1rem;margin:1rem 0 .5rem;color:var(--rs-text-primary)}
.mi-aggregate h2:first-child{margin-top:0}
.mi-aggregate ul{padding-left:1.5rem;margin:.5rem 0}
.mi-aggregate li{margin:.25rem 0;color:var(--rs-text-primary);line-height:1.5}
.mi-section-title{font-size:1.2rem;margin:2rem 0 1rem;color:var(--rs-text-primary)}
</style>`;

	return c.html(renderShell({
		title: `Meeting Intelligence — rMeets | rSpace`,
		moduleId: "rmeets",
		spaceSlug: space,
		modules: getModuleInfoList(),
		styles: MI_STYLES + extraStyles,
		body: `<div class="mi-page">
	<h1>Meeting Intelligence</h1>
	<p class="mi-subtitle">Knowledge objects, action items, and insights across all meetings</p>
	<form class="mi-search-form" method="get" action="${base}/search">
		<input type="text" name="q" placeholder="Search across all meetings...">
		<button type="submit">Search</button>
	</form>
	${aggregateHtml ? `<h2 class="mi-section-title">Aggregate Knowledge</h2>${aggregateHtml}` : ""}
	<h2 class="mi-section-title">Meetings</h2>
	${meetingCards}
</div>`,
	}));
});

// ── Room embed (catch-all — must be LAST so /meet, /recordings, /search match first) ──

routes.get("/:room", (c) => {
	const space = c.req.param("space") || "demo";
	const room = c.req.param("room");
	const director = c.req.query("director") === "1";
	const sessionId = c.req.query("session") || "";

	// Full rSpace shell mode — only when explicitly requested via ?shell=1 or director mode
	if (c.req.query("shell") === "1" || director) {
		return c.html(renderShell({
			title: `${room} — rMeets | rSpace`,
			moduleId: "rmeets",
			spaceSlug: space,
			modules: getModuleInfoList(),
			theme: "dark",
			body: `<folk-jitsi-room room="${escapeHtml(room)}" jitsi-url="${escapeHtml(JITSI_URL)}" space="${escapeHtml(space)}"${director ? ` director="1" session="${escapeHtml(sessionId)}"` : ""}></folk-jitsi-room>`,
			scripts: `<script type="module" src="/modules/rmeets/components/folk-jitsi-room.js?v=3"></script>`,
		}));
	}

	if (c.req.query("iframe") === "1") {
		return c.html(renderExternalAppShell({
			title: `${room} — rMeets | rSpace`,
			moduleId: "rmeets",
			spaceSlug: space,
			modules: getModuleInfoList(),
			appUrl: `${JITSI_URL}/${encodeURIComponent(room)}`,
			appName: "Jitsi Meet",
			theme: "dark",
		}));
	}

	// Default: clean full-screen Jitsi — no rSpace shell, mobile-friendly
	const jitsiRoom = encodeURIComponent(space + "_" + room);
	const meetsBase = rmeetsBase(c);
	return c.html(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#111111">
<title>${escapeHtml(room)} — Meeting</title>
<style>
  *{margin:0;padding:0;box-sizing:border-box}
  html,body{height:100%;overflow:hidden;background:#111;font-family:system-ui,-apple-system,sans-serif;touch-action:manipulation}
  #jitsi-container{width:100%;height:100%;height:100dvh;padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)}
  #jitsi-container iframe{width:100%!important;height:100%!important;border:none!important}
  .loading{display:flex;align-items:center;justify-content:center;height:100%;color:#94a3b8;flex-direction:column;gap:12px;font-size:0.95rem}
  .loading .spinner{width:32px;height:32px;border:3px solid #334155;border-top-color:#6366f1;border-radius:50%;animation:spin 0.8s linear infinite}
  .ended{display:flex;align-items:center;justify-content:center;height:100%;color:#e2e8f0;flex-direction:column;gap:16px;font-size:1.1rem}
  .ended a{color:#6366f1;text-decoration:none;padding:10px 24px;border:1px solid #6366f1;border-radius:8px;font-size:0.95rem;transition:background 0.15s}
  .ended a:hover,.ended a:active{background:#6366f122}
  @keyframes spin{to{transform:rotate(360deg)}}
  /* MI overlay */
  .mi-fab{position:fixed;top:12px;right:12px;z-index:10000;display:flex;align-items:center;gap:6px}
  .mi-fab-btn{width:40px;height:40px;border-radius:50%;border:none;background:rgba(99,102,241,0.85);color:#fff;font-size:1.1rem;cursor:pointer;backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;transition:background 0.15s,transform 0.15s;box-shadow:0 2px 8px rgba(0,0,0,0.3)}
  .mi-fab-btn:hover{background:rgba(99,102,241,1);transform:scale(1.08)}
  .mi-fab-btn.active{background:rgba(99,102,241,1);transform:scale(1.08)}
  .mi-dropdown{display:none;position:absolute;top:48px;right:0;background:rgba(30,30,46,0.95);backdrop-filter:blur(12px);border:1px solid rgba(99,102,241,0.3);border-radius:12px;padding:8px 0;min-width:200px;box-shadow:0 8px 24px rgba(0,0,0,0.4)}
  .mi-dropdown.open{display:block}
  .mi-dropdown a,.mi-dropdown button{display:flex;align-items:center;gap:10px;width:100%;padding:10px 16px;border:none;background:none;color:#e2e8f0;font-size:0.9rem;text-decoration:none;cursor:pointer;text-align:left;font-family:inherit;transition:background 0.12s}
  .mi-dropdown a:hover,.mi-dropdown button:hover{background:rgba(99,102,241,0.15)}
  .mi-dropdown .mi-icon{font-size:1.1rem;width:22px;text-align:center;flex-shrink:0}
  .mi-dropdown .mi-sep{height:1px;background:rgba(99,102,241,0.15);margin:4px 0}
  /* Live captions overlay */
  #mi-captions{position:fixed;left:50%;bottom:92px;transform:translateX(-50%);z-index:9999;max-width:min(780px,94vw);display:flex;flex-direction:column;gap:4px;pointer-events:none;align-items:center}
  #mi-captions.hidden{display:none}
  .mi-cap-line{background:rgba(0,0,0,0.72);color:#f8fafc;padding:6px 14px;border-radius:10px;font-size:1rem;line-height:1.35;text-align:center;max-width:100%;backdrop-filter:blur(6px);box-shadow:0 2px 10px rgba(0,0,0,0.4);word-wrap:break-word}
  .mi-cap-line .mi-cap-speaker{color:#a5b4fc;font-weight:600;margin-right:6px;font-size:0.85rem;text-transform:uppercase;letter-spacing:0.03em}
  .mi-cap-line.interim{opacity:0.75;font-style:italic}
  .mi-cap-status{position:fixed;left:12px;bottom:12px;z-index:10000;font-size:0.72rem;color:rgba(226,232,240,0.7);background:rgba(0,0,0,0.5);padding:4px 10px;border-radius:999px;pointer-events:none;max-width:80vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .mi-cap-status.hidden{display:none}
  @media(max-width:600px){#mi-captions{bottom:72px}.mi-cap-line{font-size:0.9rem;padding:5px 10px}}
</style>
</head>
<body>
<div id="jitsi-container">
  <div class="loading"><div class="spinner"></div><span>Connecting to meeting...</span></div>
</div>
<div class="mi-fab">
  <button class="mi-fab-btn" id="mi-fab-toggle" title="Meeting Intelligence">&#129504;</button>
  <div class="mi-dropdown" id="mi-dropdown">
    <button type="button" id="mi-cc-toggle"><span class="mi-icon">&#128488;</span> <span id="mi-cc-label">Turn on live captions</span></button>
    <div class="mi-sep"></div>
    <a href="${meetsBase}/meeting-intelligence"><span class="mi-icon">&#129504;</span> Meeting Intelligence</a>
    <a href="${meetsBase}/recordings"><span class="mi-icon">&#127909;</span> Recordings</a>
    <a href="${meetsBase}/search"><span class="mi-icon">&#128269;</span> Search Transcripts</a>
    <div class="mi-sep"></div>
    <a href="${meetsBase}"><span class="mi-icon">&#127968;</span> rMeets Hub</a>
  </div>
</div>
<div id="mi-captions" class="hidden"></div>
<div id="mi-cap-status" class="mi-cap-status hidden"></div>
<script src="${escapeHtml(JITSI_URL)}/libs/external_api.min.js"></script>
<script>
  // MI dropdown toggle
  (function() {
    var fab = document.getElementById("mi-fab-toggle");
    var dd = document.getElementById("mi-dropdown");
    fab.addEventListener("click", function(e) {
      e.stopPropagation();
      var open = dd.classList.toggle("open");
      fab.classList.toggle("active", open);
    });
    document.addEventListener("click", function() {
      dd.classList.remove("open");
      fab.classList.remove("active");
    });
  })();

  try {
    var api = new JitsiMeetExternalAPI("${escapeHtml(JITSI_URL.replace(/^https?:\/\//, ""))}", {
      roomName: decodeURIComponent("${jitsiRoom}"),
      parentNode: document.getElementById("jitsi-container"),
      width: "100%",
      height: "100%",
      configOverwrite: {
        startWithAudioMuted: false,
        startWithVideoMuted: false,
        prejoinConfig: { enabled: true },
        requireDisplayName: true,
        disableDeepLinking: true,
        disableThirdPartyRequests: false,
        enableClosePage: false,
        disableVirtualBackground: false,
        disableProfile: false,
        notifications: ['chat'],
        toolbarButtons: [
          "microphone","camera","desktop","hangup",
          "raisehand","tileview","toggle-camera",
          "fullscreen","chat","settings",
          "participants-pane","select-background",
          "sharedvideo","shareaudio","recording",
        ],
      },
      customToolbarButtons: [
        { icon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-4h2v-2h-2v2zm1-10c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z'/%3E%3C/svg%3E", id: "mi-btn", text: "Intelligence" },
      ],
      interfaceConfigOverwrite: {
        SHOW_JITSI_WATERMARK: false,
        SHOW_BRAND_WATERMARK: false,
        SHOW_POWERED_BY: false,
        MOBILE_APP_PROMO: false,
        HIDE_DEEP_LINKING_LOGO: true,
        DISABLE_JOIN_LEAVE_NOTIFICATIONS: false,
        CHAT_PANEL_POSITION: 'right',
        SETTINGS_SECTIONS: ['devices', 'language', 'moderator', 'profile', 'sounds', 'more'],
      },
    });
    // Remove loading spinner — iframe is already injected by the constructor
    var loadingEl = document.querySelector(".loading");
    if (loadingEl) loadingEl.remove();
    // Custom toolbar button → open our MI page
    api.addEventListener("toolbarButtonClicked", function(e) {
      if (e.key === "mi-btn") {
        window.open("${meetsBase}/meeting-intelligence", "_blank");
      }
    });
    api.addEventListener("readyToClose", function() {
      try { window.close(); } catch(e) {}
      document.getElementById("jitsi-container").innerHTML =
        '<div class="ended"><span>Meeting ended</span>'
        + '<a href="${meetsBase}/recordings">View Transcript &amp; Summary</a>'
        + '<a href="${meetsBase}">Back to rMeets</a></div>';
    });

    // ── Live captions (Web Speech API, peer-broadcast via Jitsi data channel) ──
    (function setupCaptions() {
      var capsEl = document.getElementById("mi-captions");
      var statusEl = document.getElementById("mi-cap-status");
      var ccBtn = document.getElementById("mi-cc-toggle");
      var ccLabel = document.getElementById("mi-cc-label");
      var myName = "Me";
      var myId = null;
      var enabled = false;
      var recognition = null;
      var wantRunning = false;
      var lines = [];  // { id, speaker, text, interim, ts }
      var lineSeq = 0;
      var MAX_LINES = 3;
      var FADE_MS = 6000;

      var SR = window.SpeechRecognition || window.webkitSpeechRecognition;
      if (!SR) {
        ccLabel.textContent = "Captions not supported in this browser";
        ccBtn.disabled = true;
        ccBtn.style.opacity = "0.5";
        return;
      }

      api.addEventListener("videoConferenceJoined", function(e) {
        myId = e.id;
        if (e.displayName) myName = e.displayName;
      });
      api.addEventListener("displayNameChange", function(e) {
        if (e.id === myId && e.displayname) myName = e.displayname;
      });

      function render() {
        var now = Date.now();
        lines = lines.filter(function(l) { return l.interim || (now - l.ts) < FADE_MS; });
        capsEl.innerHTML = "";
        var shown = lines.slice(-MAX_LINES);
        shown.forEach(function(l) {
          var d = document.createElement("div");
          d.className = "mi-cap-line" + (l.interim ? " interim" : "");
          d.innerHTML = '<span class="mi-cap-speaker">' + escapeText(l.speaker) + '</span>' + escapeText(l.text);
          capsEl.appendChild(d);
        });
      }
      function escapeText(s) {
        return String(s || '').replace(/[&<>"']/g, function(c) {
          return { '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' }[c];
        });
      }
      function pushLine(speaker, text, interim, senderKey) {
        // Replace the most recent interim line from the same sender, otherwise append
        var key = senderKey || speaker;
        for (var i = lines.length - 1; i >= 0; i--) {
          if (lines[i].interim && lines[i].senderKey === key) {
            lines[i].text = text;
            lines[i].interim = !!interim;
            lines[i].ts = Date.now();
            if (!interim) lines[i].interim = false;
            render();
            return;
          }
        }
        lines.push({ id: ++lineSeq, speaker: speaker, text: text, interim: !!interim, ts: Date.now(), senderKey: key });
        if (lines.length > 20) lines = lines.slice(-20);
        render();
      }
      setInterval(render, 1500);

      function broadcast(text, isFinal) {
        try {
          var msg = JSON.stringify({ type: "caption", v: 1, final: !!isFinal, text: text });
          api.executeCommand("sendEndpointTextMessage", "", msg);
        } catch(e) { /* pre-join; ignore */ }
      }

      api.addEventListener("endpointTextMessageReceived", function(event) {
        try {
          var raw = event && event.data && event.data.eventData && event.data.eventData.text;
          if (!raw) return;
          var msg = JSON.parse(raw);
          if (!msg || msg.type !== "caption" || !msg.text) return;
          var sender = event.data.senderInfo || {};
          var senderId = sender.id || sender.jid || "peer";
          var speaker = "";
          try { speaker = api.getDisplayName && api.getDisplayName(senderId); } catch(_) {}
          if (!speaker) speaker = sender.displayName || "Guest";
          pushLine(speaker, msg.text, !msg.final, "peer:" + senderId);
        } catch(_) { /* ignore malformed */ }
      });

      function startRecognition() {
        if (recognition) return;
        recognition = new SR();
        recognition.continuous = true;
        recognition.interimResults = true;
        recognition.lang = (navigator.language || "en-US");
        recognition.onresult = function(evt) {
          for (var i = evt.resultIndex; i < evt.results.length; i++) {
            var res = evt.results[i];
            var text = (res[0] && res[0].transcript || "").trim();
            if (!text) continue;
            var isFinal = !!res.isFinal;
            pushLine(myName + " (you)", text, !isFinal, "me");
            broadcast(text, isFinal);
          }
        };
        recognition.onerror = function(e) {
          if (e.error === "no-speech" || e.error === "aborted") return;
          setStatus("Captions paused: " + (e.error || "error"));
        };
        recognition.onend = function() {
          recognition = null;
          if (wantRunning) {
            // Chrome stops after ~60s of continuous; restart
            setTimeout(function() { if (wantRunning) startRecognition(); }, 300);
          }
        };
        try { recognition.start(); setStatus("Captions on — your voice is being shared"); }
        catch(_) { /* already started — ignore */ }
      }

      function stopRecognition() {
        wantRunning = false;
        if (recognition) { try { recognition.stop(); } catch(_){} recognition = null; }
      }

      function setStatus(txt) {
        if (!txt) { statusEl.classList.add("hidden"); statusEl.textContent = ""; return; }
        statusEl.classList.remove("hidden");
        statusEl.textContent = txt;
      }

      ccBtn.addEventListener("click", function(e) {
        e.stopPropagation();
        enabled = !enabled;
        if (enabled) {
          capsEl.classList.remove("hidden");
          ccLabel.textContent = "Turn off live captions";
          wantRunning = true;
          startRecognition();
        } else {
          stopRecognition();
          ccLabel.textContent = "Turn on live captions";
          setStatus("");
          capsEl.classList.add("hidden");
          lines = [];
          render();
        }
      });

      window.addEventListener("beforeunload", stopRecognition);
    })();
  } catch(e) {
    document.getElementById("jitsi-container").innerHTML =
      '<div class="loading" style="color:#ef4444">Failed to connect: ' + e.message + '</div>';
  }
</script>
</body>
</html>`);
});

// ── Helpers ──

function formatTimestamp(seconds: number): string {
	const h = Math.floor(seconds / 3600);
	const m = Math.floor((seconds % 3600) / 60);
	const s = Math.floor(seconds % 60);
	if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
	return `${m}:${String(s).padStart(2, "0")}`;
}

export function getRecentMeetingsForMI(space: string, limit = 5): { id: string; title: string; roomName: string; scheduledAt: number; participantCount: number; createdAt: number }[] {
    if (!_syncServer) return [];
    const docId = meetsDocId(space);
    const doc = _syncServer.getDoc<MeetsDoc>(docId);
    if (!doc?.meetings) return [];
    return Object.values(doc.meetings)
        .sort((a, b) => b.createdAt - a.createdAt)
        .slice(0, limit)
        .map(m => ({ id: m.id, title: m.title, roomName: m.roomName, scheduledAt: m.scheduledAt, participantCount: m.participants.length, createdAt: m.createdAt }));
}

// ── Module export ──

export const meetsModule: RSpaceModule = {
	id: "rmeets",
	name: "rMeets",
	icon: "📹",
	description: "Video meetings powered by Jitsi",
	scoping: { defaultScope: "space", userConfigurable: false },
	routes,
	docSchemas: [{ pattern: '{space}:meets:meetings', description: 'Meeting scheduling per space', init: meetsSchema.init }],
	async onInit(ctx) { _syncServer = ctx.syncServer; },
	landingPage: renderLanding,
	externalApp: { url: JITSI_URL, name: "Jitsi Meet" },
	outputPaths: [
		{ path: "meet", name: "Create Call", icon: "🚀", description: "Start a new video meeting" },
		{ path: "rooms", name: "Rooms", icon: "🚪", description: "Meeting rooms and video calls" },
	],
	onboardingActions: [
		{ label: "Start a Meeting", icon: "🚀", description: "Jump into an instant video call", type: "create", href: "/{space}/rmeets/quick" },
		{ label: "View Recordings", icon: "🎥", description: "Browse past meetings with transcripts and AI summaries", type: "link", href: "/{space}/rmeets/recordings" },
	],
};
