import { randomUUID } from 'node:crypto';
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
import { listSessions, loadSession } from './native/session.js';
import { getOwlcodaLiveReplLeasePath } from './paths.js';
import { resolveClientHost } from './healthz-client.js';
const LIVE_REPL_REGISTRY_VERSION = 2;
const LIVE_REPL_REGISTRY_LOCK_TIMEOUT_MS = 2000;
const LIVE_REPL_REGISTRY_LOCK_STALE_MS = 5000;
const LIVE_REPL_REGISTRY_LOCK_RETRY_MS = 25;
const REGISTRY_SLEEP_BUFFER = new SharedArrayBuffer(4);
const REGISTRY_SLEEP_VIEW = new Int32Array(REGISTRY_SLEEP_BUFFER);
function isPidAlive(pid) {
    try {
        process.kill(pid, 0);
        return true;
    }
    catch {
        return false;
    }
}
function emptyRegistry() {
    return { version: LIVE_REPL_REGISTRY_VERSION, clients: [] };
}
function sleepSync(ms) {
    try {
        Atomics.wait(REGISTRY_SLEEP_VIEW, 0, 0, ms);
    }
    catch {
        const deadline = Date.now() + ms;
        while (Date.now() < deadline) {
            // Busy wait fallback if Atomics.wait is unavailable.
        }
    }
}
function getRegistryLockPath() {
    return `${getOwlcodaLiveReplLeasePath()}.lock`;
}
function acquireRegistryLock() {
    const lockPath = getRegistryLockPath();
    const start = Date.now();
    while (true) {
        try {
            mkdirSync(lockPath);
            return;
        }
        catch (error) {
            const code = error instanceof Error && 'code' in error ? String(error.code ?? '') : '';
            if (code !== 'EEXIST')
                throw error;
            try {
                const stats = statSync(lockPath);
                if ((Date.now() - stats.mtimeMs) >= LIVE_REPL_REGISTRY_LOCK_STALE_MS) {
                    rmSync(lockPath, { recursive: true, force: true });
                    continue;
                }
            }
            catch {
                continue;
            }
            if ((Date.now() - start) >= LIVE_REPL_REGISTRY_LOCK_TIMEOUT_MS) {
                throw new Error('Timed out acquiring live REPL registry lock');
            }
            sleepSync(LIVE_REPL_REGISTRY_LOCK_RETRY_MS);
        }
    }
}
function releaseRegistryLock() {
    rmSync(getRegistryLockPath(), { recursive: true, force: true });
}
function withRegistryLock(fn) {
    acquireRegistryLock();
    try {
        return fn();
    }
    finally {
        releaseRegistryLock();
    }
}
function parseClientLease(raw, index) {
    if (!raw || typeof raw !== 'object')
        return null;
    const data = raw;
    const clientPid = Number(data['clientPid']);
    const daemonPid = Number(data['daemonPid']);
    const port = Number(data['port']);
    const runtimeToken = String(data['runtimeToken'] ?? '').trim();
    const host = String(data['host'] ?? '').trim();
    const routerUrl = String(data['routerUrl'] ?? '').trim();
    const startedAt = String(data['startedAt'] ?? '').trim();
    if (!Number.isFinite(clientPid) || clientPid <= 0)
        return null;
    if (!Number.isFinite(daemonPid) || daemonPid <= 0)
        return null;
    if (!Number.isFinite(port) || port <= 0)
        return null;
    if (!runtimeToken || !host || !routerUrl || !startedAt)
        return null;
    const rawClientId = String(data['clientId'] ?? '').trim();
    const sessionId = typeof data['sessionId'] === 'string' && data['sessionId'].trim()
        ? data['sessionId'].trim()
        : undefined;
    return {
        clientId: rawClientId || `legacy-${clientPid}-${Date.parse(startedAt) || index}`,
        clientPid,
        daemonPid,
        runtimeToken,
        host,
        port,
        routerUrl,
        startedAt,
        sessionId,
    };
}
function normalizeRegistry(raw) {
    if (!raw || typeof raw !== 'object')
        return null;
    const data = raw;
    if (Array.isArray(data['clients'])) {
        const clients = data['clients']
            .map((entry, index) => parseClientLease(entry, index))
            .filter((entry) => entry !== null);
        return {
            version: LIVE_REPL_REGISTRY_VERSION,
            clients: dedupeClients(clients),
        };
    }
    const legacy = parseClientLease(raw, 0);
    if (!legacy)
        return null;
    return {
        version: LIVE_REPL_REGISTRY_VERSION,
        clients: [legacy],
    };
}
function dedupeClients(clients) {
    const deduped = new Map();
    for (const client of clients) {
        deduped.set(client.clientId, client);
    }
    return [...deduped.values()].sort((a, b) => a.startedAt.localeCompare(b.startedAt));
}
function registryEquals(a, b) {
    return JSON.stringify(a) === JSON.stringify(b);
}
function readRegistryFile() {
    const leasePath = getOwlcodaLiveReplLeasePath();
    if (!existsSync(leasePath))
        return null;
    try {
        const raw = JSON.parse(readFileSync(leasePath, 'utf-8'));
        return normalizeRegistry(raw);
    }
    catch {
        return null;
    }
}
function writeRegistryFile(registry) {
    const leasePath = getOwlcodaLiveReplLeasePath();
    if (registry.clients.length === 0) {
        try {
            if (existsSync(leasePath))
                unlinkSync(leasePath);
        }
        catch {
            // best effort
        }
        return;
    }
    const tempPath = `${leasePath}.tmp-${process.pid}-${randomUUID()}`;
    try {
        writeFileSync(tempPath, JSON.stringify(registry, null, 2) + '\n', 'utf-8');
        renameSync(tempPath, leasePath);
    }
    finally {
        try {
            if (existsSync(tempPath))
                unlinkSync(tempPath);
        }
        catch {
            // best effort
        }
    }
}
function loadActiveRegistryUnlocked() {
    const registry = readRegistryFile() ?? emptyRegistry();
    const pruned = {
        version: LIVE_REPL_REGISTRY_VERSION,
        clients: registry.clients.filter(client => isPidAlive(client.clientPid)),
    };
    if (!registryEquals(registry, pruned)) {
        writeRegistryFile(pruned);
    }
    return pruned;
}
function mutateActiveRegistry(mutator) {
    return withRegistryLock(() => {
        const registry = loadActiveRegistryUnlocked();
        const result = mutator(registry);
        const nextRegistry = {
            version: LIVE_REPL_REGISTRY_VERSION,
            clients: dedupeClients(result.registry.clients),
        };
        if (!registryEquals(registry, nextRegistry)) {
            writeRegistryFile(nextRegistry);
        }
        return result.value;
    });
}
function matchesRuntime(client, runtime) {
    if (!runtime)
        return true;
    if (resolveClientHost(client.host) !== resolveClientHost(runtime.host))
        return false;
    if (client.port !== runtime.port)
        return false;
    if (client.routerUrl !== runtime.routerUrl)
        return false;
    if (runtime.runtimeToken && client.runtimeToken !== runtime.runtimeToken)
        return false;
    return true;
}
function formatClientSessionSuffix(client) {
    return client.sessionId ? `, session ${client.sessionId}` : '';
}
export function createLiveReplClientId() {
    return `client-${randomUUID()}`;
}
export function writeLiveReplLease(lease) {
    upsertLiveReplClient(lease);
}
export function writeLiveReplRegistry(registry) {
    withRegistryLock(() => {
        writeRegistryFile({
            version: LIVE_REPL_REGISTRY_VERSION,
            clients: dedupeClients(registry.clients),
        });
    });
}
export function readLiveReplRegistry() {
    return withRegistryLock(() => loadActiveRegistryUnlocked());
}
export function readLiveReplLease() {
    return withRegistryLock(() => loadActiveRegistryUnlocked().clients[0] ?? null);
}
export function removeLiveReplLease() {
    withRegistryLock(() => {
        writeRegistryFile(emptyRegistry());
    });
}
export function removeLiveReplClient(clientId) {
    mutateActiveRegistry((registry) => ({
        registry: {
            version: LIVE_REPL_REGISTRY_VERSION,
            clients: registry.clients.filter(client => client.clientId !== clientId),
        },
        value: undefined,
    }));
}
export function removeLiveReplClientIfOwned(clientId, clientPid) {
    mutateActiveRegistry((registry) => ({
        registry: {
            version: LIVE_REPL_REGISTRY_VERSION,
            clients: registry.clients.filter((client) => {
                if (client.clientId !== clientId)
                    return true;
                if (clientPid !== undefined && client.clientPid !== clientPid)
                    return true;
                return false;
            }),
        },
        value: undefined,
    }));
}
// Backward-compatible wrapper for older call sites that only know PID ownership.
export function removeLiveReplLeaseIfOwned(clientPid) {
    mutateActiveRegistry((registry) => ({
        registry: {
            version: LIVE_REPL_REGISTRY_VERSION,
            clients: registry.clients.filter(client => client.clientPid !== clientPid),
        },
        value: undefined,
    }));
}
export function upsertLiveReplClient(lease) {
    return mutateActiveRegistry((registry) => {
        const nextClients = registry.clients.filter((client) => {
            if (client.clientId === lease.clientId)
                return false;
            return !(client.clientPid === lease.clientPid && matchesRuntime(client, lease));
        });
        nextClients.push({ ...lease });
        const nextRegistry = {
            version: LIVE_REPL_REGISTRY_VERSION,
            clients: nextClients,
        };
        return { registry: nextRegistry, value: { ...nextRegistry, clients: dedupeClients(nextClients) } };
    });
}
export function updateLiveReplClientSession(clientId, sessionId) {
    return mutateActiveRegistry((registry) => {
        const trimmedSessionId = sessionId?.trim() || undefined;
        let updated = false;
        const nextClients = registry.clients.map((client) => {
            if (client.clientId !== clientId)
                return client;
            updated = true;
            if (trimmedSessionId === client.sessionId)
                return client;
            if (!trimmedSessionId) {
                const { sessionId: _removed, ...rest } = client;
                return rest;
            }
            return { ...client, sessionId: trimmedSessionId };
        });
        return {
            registry: {
                version: LIVE_REPL_REGISTRY_VERSION,
                clients: nextClients,
            },
            value: updated,
        };
    });
}
export function detachLiveReplClient(clientId, opts) {
    return mutateActiveRegistry((registry) => {
        const client = registry.clients.find(entry => entry.clientId === clientId);
        if (!client) {
            return {
                registry,
                value: {
                    ok: false,
                    reason: 'not_found',
                },
            };
        }
        if (!opts?.force && isPidAlive(client.clientPid)) {
            return {
                registry,
                value: {
                    ok: false,
                    reason: 'active_requires_force',
                    client,
                },
            };
        }
        return {
            registry: {
                version: LIVE_REPL_REGISTRY_VERSION,
                clients: registry.clients.filter(entry => entry.clientId !== clientId),
            },
            value: {
                ok: true,
                reason: 'removed',
                client,
            },
        };
    });
}
export function detachLiveReplClientsForRuntime(runtime, opts) {
    return mutateActiveRegistry((registry) => {
        const removedClients = [];
        const blockedClients = [];
        const clients = [];
        for (const client of registry.clients) {
            if (!matchesRuntime(client, runtime)) {
                clients.push(client);
                continue;
            }
            if (!opts?.force && isPidAlive(client.clientPid)) {
                blockedClients.push(client);
                clients.push(client);
                continue;
            }
            removedClients.push(client);
        }
        return {
            registry: {
                version: LIVE_REPL_REGISTRY_VERSION,
                clients,
            },
            value: { removedClients, blockedClients },
        };
    });
}
export function listActiveLiveReplClients(runtime) {
    return withRegistryLock(() => loadActiveRegistryUnlocked().clients.filter(client => matchesRuntime(client, runtime)));
}
export function getActiveLiveReplLease() {
    return listActiveLiveReplClients()[0] ?? null;
}
export function getActiveLiveReplClientsForRuntime(runtime) {
    return listActiveLiveReplClients(runtime);
}
export function liveReplLeaseMatchesConfig(lease, config) {
    return resolveClientHost(lease.host) === resolveClientHost(config.host)
        && lease.port === config.port
        && lease.routerUrl === config.routerUrl;
}
export function formatLiveReplLeaseTarget(lease) {
    return `http://${resolveClientHost(lease.host)}:${lease.port}`;
}
export function formatLiveReplClientSummary(client) {
    return `PID ${client.clientPid} (${client.clientId})${formatClientSessionSuffix(client)}, started ${client.startedAt}`;
}
export function formatLiveReplClientAdminSummary(client) {
    return `${formatLiveReplClientSummary(client)}, daemon ${client.daemonPid}, target ${formatLiveReplLeaseTarget(client)}`;
}
// ─── Admin view: enriched client record for `owlcoda clients` ─────
//
// The raw `LiveReplClientLease` is the on-disk shape. The admin view
// adds derived fields that are cheap to compute but expensive to
// re-derive in every consumer: process liveness, formatted target,
// age since start, and optional session title/updatedAt (best-effort
// — enriched only when the session file is readable; missing sessions
// don't error, they just leave sessionTitle undefined).
//
// The schemaVersion lets `--json` consumers gate on a known shape;
// bump it when fields change semantics.
export const LIVE_REPL_CLIENT_ADMIN_SCHEMA_VERSION = 2;
/**
 * Produce the admin view for a single client. Caller passes `now` to
 * get a stable ageSeconds across a multi-client snapshot (avoids
 * drift from per-client Date.now() calls).
 */
export function enrichLiveReplClientForAdmin(client, now = Date.now()) {
    const startedMs = Date.parse(client.startedAt);
    const ageSeconds = Number.isFinite(startedMs)
        ? Math.max(0, Math.floor((now - startedMs) / 1000))
        : -1;
    let sessionTitle;
    let sessionUpdatedAt;
    if (client.sessionId) {
        try {
            const session = loadSession(client.sessionId);
            if (session) {
                sessionTitle = session.title;
                sessionUpdatedAt = new Date(session.updatedAt).toISOString();
            }
        }
        catch {
            // best-effort enrichment — missing/corrupt session file shouldn't
            // break the admin view or leak an error into JSON output
        }
    }
    return {
        ...client,
        alive: isPidAlive(client.clientPid),
        target: formatLiveReplLeaseTarget(client),
        ageSeconds,
        ...(sessionTitle !== undefined && { sessionTitle }),
        ...(sessionUpdatedAt !== undefined && { sessionUpdatedAt }),
    };
}
/**
 * Human-readable age: "42s", "5m 12s", "2h 18m", "3d". Keeps output
 * compact — reserved callers format their own precision if needed.
 */
export function formatLiveReplClientAge(ageSeconds) {
    if (ageSeconds < 0)
        return 'unknown';
    if (ageSeconds < 60)
        return `${ageSeconds}s`;
    if (ageSeconds < 3600) {
        const m = Math.floor(ageSeconds / 60);
        const s = ageSeconds % 60;
        return s > 0 ? `${m}m ${s}s` : `${m}m`;
    }
    if (ageSeconds < 86400) {
        const h = Math.floor(ageSeconds / 3600);
        const m = Math.floor((ageSeconds % 3600) / 60);
        return m > 0 ? `${h}h ${m}m` : `${h}h`;
    }
    return `${Math.floor(ageSeconds / 86400)}d`;
}
/**
 * Multi-line human detail block for a single client. Used by
 * `owlcoda clients` default text output. Intentionally verbose —
 * the one-line summary (`formatLiveReplClientAdminSummary`) stays
 * for compact contexts (detach messages).
 *
 *   [n] client <clientId>  alive|stale  started <age> ago
 *       PID <clientPid>    daemon <daemonPid>
 *       target <url>       router <routerUrl>
 *       session <id> — "title" (updated <age>)
 */
export function formatLiveReplClientDetail(view, index) {
    const header = index !== undefined
        ? `[${index}] client ${view.clientId}`
        : `client ${view.clientId}`;
    const aliveMark = view.alive ? 'alive' : 'stale';
    const age = formatLiveReplClientAge(view.ageSeconds);
    const lines = [
        `${header}  ${aliveMark}  started ${age} ago`,
        `    PID ${view.clientPid}    daemon ${view.daemonPid}`,
        `    target ${view.target}    router ${view.routerUrl}`,
    ];
    if (view.sessionId) {
        // Truncate UUID-shaped ids (conv-xxxx-xxxx-... or similar long ids)
        // for readability, but leave short human-friendly labels alone so
        // the operator can still grep / copy them directly.
        const shortId = view.sessionId.length > 16
            ? view.sessionId.slice(0, 16) + '…'
            : view.sessionId;
        const titlePart = view.sessionTitle ? ` — "${view.sessionTitle}"` : '';
        lines.push(`    session ${shortId}${titlePart}`);
    }
    else {
        lines.push(`    session none`);
    }
    return lines.join('\n');
}
export function resolveLiveReplResumeTarget(requestedTarget, opts) {
    const target = requestedTarget?.trim() || '';
    if (!target) {
        return {
            requestedTarget: target,
            resolvedTarget: null,
            session: null,
            reason: 'no_target',
            skippedLiveSessionIds: [],
        };
    }
    const blockedOwners = new Map();
    for (const client of listActiveLiveReplClients(opts?.runtime)) {
        if (client.clientId === opts?.currentClientId)
            continue;
        if (!client.sessionId)
            continue;
        blockedOwners.set(client.sessionId, client);
    }
    if (target === 'last') {
        const sessions = listSessions();
        const skippedLiveSessionIds = [];
        for (const session of sessions) {
            if (blockedOwners.has(session.id)) {
                skippedLiveSessionIds.push(session.id);
                continue;
            }
            return {
                requestedTarget: target,
                resolvedTarget: session.id,
                session,
                reason: 'ok',
                skippedLiveSessionIds,
            };
        }
        return {
            requestedTarget: target,
            resolvedTarget: null,
            session: null,
            reason: 'no_resumable_session',
            skippedLiveSessionIds,
        };
    }
    const session = loadSession(target);
    if (!session) {
        return {
            requestedTarget: target,
            resolvedTarget: null,
            session: null,
            reason: 'not_found',
            skippedLiveSessionIds: [],
        };
    }
    const blockedBy = blockedOwners.get(session.id);
    if (blockedBy) {
        return {
            requestedTarget: target,
            resolvedTarget: null,
            session: null,
            reason: 'owned_by_live_client',
            blockedBy,
            skippedLiveSessionIds: [session.id],
        };
    }
    return {
        requestedTarget: target,
        resolvedTarget: session.id,
        session,
        reason: 'ok',
        skippedLiveSessionIds: [],
    };
}
//# sourceMappingURL=repl-lease.js.map