/**
 * ThreadlineBootstrap — Auto-wires Threadline protocol into the agent server.
 *
 * Called during server boot to:
 *   1. Persist identity keys (Ed25519) across restarts
 *   2. Create HandshakeManager for crypto handshakes
 *   3. Register MCP tools into Claude Code's ~/.claude.json
 *   4. Announce agent presence for discovery
 *   5. Start discovery heartbeat
 *
 * Design: The user never sees any of this. Threadline is ON when the agent boots.
 * The agent IS the interface — users talk to their agent, the agent handles the rest.
 *
 * Part of Threadline Protocol integration (Principle #11: "I am the interface").
 */
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { HandshakeManager } from './HandshakeManager.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
import { AgentDiscovery } from './AgentDiscovery.js';
import { DEFAULT_RELAY_URL } from './constants.js';
import { resolveThreadlineMcpEntry } from './mcpEntry.js';
import { ThreadlineClient } from './client/ThreadlineClient.js';
import { InboundMessageGate } from './InboundMessageGate.js';
import { AgentTrustManager } from './AgentTrustManager.js';
import { SafeFsExecutor } from '../core/SafeFsExecutor.js';
import { IdentityManager } from './client/IdentityManager.js';
import { detectMachineName } from '../core/MachineIdentity.js';
// ── Implementation ───────────────────────────────────────────────────
/**
 * Bootstrap the Threadline protocol stack.
 *
 * Creates the HandshakeManager, registers MCP tools, announces presence,
 * and starts the discovery heartbeat. The ThreadlineRouter is created
 * separately in server.ts because it depends on messaging infrastructure.
 */
export async function bootstrapThreadline(config) {
    const threadlineDir = path.join(config.stateDir, 'threadline');
    fs.mkdirSync(threadlineDir, { recursive: true });
    // ── 1. HandshakeManager ──────────────────────────────────────────
    const handshakeManager = new HandshakeManager(config.stateDir, config.agentName);
    // ── 3. Agent Discovery ───────────────────────────────────────────
    const discovery = new AgentDiscovery({
        stateDir: config.stateDir,
        selfPath: config.projectDir,
        selfName: config.agentName,
        selfPort: config.port,
    });
    // ── Routing identity (the address the relay actually answers to) ─
    // Resolve via the SAME read-only API the relay client uses
    // (IdentityManager.get(), NOT getOrCreate — never fabricate a dead address
    // for a relay-less or locked-encrypted agent). Resolve up front, before the
    // relay client is constructed, so discovery advertises the routable identity
    // — not the orphan identity-keys.json hex key, which nothing on the relay
    // routing path reads.
    const routingIdentity = new IdentityManager(config.stateDir).get();
    // Announce presence for other agents to find us. When a routing identity
    // resolves, advertise its fingerprint (the routable relay address) AND set
    // publicKey to the SAME canonical key, so the two correspond
    // (fingerprint === computeFingerprint(publicKey)). When none resolves (no
    // identity on disk, or canonical identity.json is passphrase-encrypted and
    // locked at boot), OMIT both — an agent with no resolvable routing identity
    // is simply not relay-discoverable until it has one.
    discovery.announcePresence({
        capabilities: ['threadline', 'mcp'],
        description: config.agentDescription ?? `${config.agentName} Instar agent`,
        threadlineVersion: '1.0',
        publicKey: routingIdentity?.publicKey.toString('hex'),
        fingerprint: routingIdentity?.fingerprint,
        framework: 'instar',
        machine: detectMachineName(),
    });
    // Start heartbeat for liveness detection
    const stopHeartbeat = discovery.startPresenceHeartbeat();
    // ── 4. Register MCP server into framework config(s) ──────────────
    await registerThreadlineMcp(config.projectDir, config.agentName, config.stateDir);
    // ── 5. Cloud Relay Connection (opt-in) ─────────────────────────
    const relayEnabled = config.relayEnabled === true
        || process.env.THREADLINE_RELAY_ENABLED === 'true';
    // Check if the standalone listener daemon should handle the relay connection.
    // Three checks, in order:
    //   1. Config says listener.enabled = true → always defer to daemon
    //   2. Daemon PID file exists and process alive → defer
    //   3. Health file shows recent activity (last 5 min) → defer (daemon may be restarting)
    // This prevents the displacement race where the server briefly connects its own
    // relay client during restart, displacing the daemon.
    let daemonHandlingRelay = false;
    if (relayEnabled) {
        // Check 1: Config-level opt-in — if the user configured a daemon, always defer
        const listenerConfigPath = path.join(config.stateDir, 'config.json');
        let listenerConfigEnabled = false;
        try {
            if (fs.existsSync(listenerConfigPath)) {
                const rawConfig = JSON.parse(fs.readFileSync(listenerConfigPath, 'utf-8'));
                listenerConfigEnabled = rawConfig?.threadline?.listener?.enabled === true;
            }
        }
        catch { /* ignore parse errors */ }
        // Check 2: Daemon PID file exists and process alive
        const daemonPidPath = path.join(config.stateDir, 'listener-daemon.pid');
        let daemonAlive = false;
        if (fs.existsSync(daemonPidPath)) {
            try {
                const pid = parseInt(fs.readFileSync(daemonPidPath, 'utf-8').trim(), 10);
                process.kill(pid, 0); // Check if process is alive (doesn't actually kill)
                daemonAlive = true;
            }
            catch {
                // PID file exists but process is dead — clean up stale PID
                try {
                    SafeFsExecutor.safeUnlinkSync(daemonPidPath, { operation: 'src/threadline/ThreadlineBootstrap.ts:153' });
                }
                catch { /* ignore */ }
            }
        }
        // Check 3: Health file shows recent activity (daemon may be restarting)
        let recentHealth = false;
        const healthPath = path.join(config.stateDir, 'listener-health.json');
        if (fs.existsSync(healthPath)) {
            try {
                const healthMtime = fs.statSync(healthPath).mtimeMs;
                const fiveMinAgo = Date.now() - 5 * 60 * 1000;
                if (healthMtime > fiveMinAgo) {
                    const health = JSON.parse(fs.readFileSync(healthPath, 'utf-8'));
                    // Only count as recent if the daemon was previously connected (not just 'stopped')
                    if (health.state === 'connected' || health.state === 'authenticating' || health.state === 'connecting') {
                        recentHealth = true;
                    }
                }
            }
            catch { /* ignore */ }
        }
        if (daemonAlive) {
            daemonHandlingRelay = true;
            const pid = parseInt(fs.readFileSync(daemonPidPath, 'utf-8').trim(), 10);
            console.log(`Threadline: listener daemon running (pid: ${pid}) — server will NOT connect to relay (daemon handles it)`);
        }
        else if (listenerConfigEnabled) {
            daemonHandlingRelay = true;
            console.log(`Threadline: listener daemon configured (threadline.listener.enabled=true) — server will NOT connect to relay (start daemon with: instar listener start)`);
        }
        else if (recentHealth) {
            daemonHandlingRelay = true;
            console.log(`Threadline: listener daemon was recently active — server will NOT connect to relay (daemon may be restarting)`);
        }
    }
    let relayClient;
    let inboundGate;
    let trustManager;
    // Always create trust manager when relay is enabled — needed for processing
    // inbox entries even when the daemon handles the relay connection.
    if (relayEnabled) {
        trustManager = new AgentTrustManager({ stateDir: config.stateDir });
    }
    if (relayEnabled) {
        const relayUrl = config.relayUrl
            ?? process.env.THREADLINE_RELAY_URL
            ?? DEFAULT_RELAY_URL;
        // Always create relay client for outbound messages.
        // When daemon handles inbound, we still need the client for sending.
        relayClient = new ThreadlineClient({
            name: config.agentName,
            relayUrl,
            framework: config.framework ?? 'instar',
            capabilities: config.capabilities ?? ['chat', 'threadline'],
            visibility: config.visibility ?? 'public',
            stateDir: config.stateDir,
        });
        // Create inbound message gate (imports ThreadlineRouter lazily if needed)
        // For now, router is not available at bootstrap time — it's created in server.ts
        // The gate will be wired to the router after server setup
        inboundGate = new InboundMessageGate(trustManager, null, {
            maxPayloadBytes: 64 * 1024,
            // Bind THIS agent's routing fingerprint for pair-verify receipt verification +
            // the self-pair guard (FD12). Resolved above via the read-only IdentityManager.
            ownFingerprint: routingIdentity?.fingerprint,
            // Live verified-pairing config reader (spec §3.10) — supplied by server.ts.
            getVerifiedPairingConfig: config.getVerifiedPairingConfig,
        });
        // Only wire inbound message handling and connect if daemon is NOT handling relay.
        // When daemon handles inbound, the server only uses the relay client for outbound sends.
        if (!daemonHandlingRelay) {
            console.log(`Threadline: connecting to relay at ${relayUrl} (disable with THREADLINE_RELAY_ENABLED=false)`);
            // Route inbound relay messages through the gate
            relayClient.on('message', async (msg) => {
                if (!inboundGate)
                    return;
                const decision = await inboundGate.evaluate(msg);
                if (decision.action === 'pass' && decision.message) {
                    // Gate passed — emit for ThreadlineRouter to handle
                    relayClient.emit('gate-passed', decision);
                }
            });
            // Handle unknown senders — decode payload directly (relay-authenticated, not E2E encrypted)
            relayClient.on('unknown-sender', async (envelope) => {
                if (!inboundGate || !envelope)
                    return;
                // Attempt to decode the base64 payload as plaintext JSON
                let textContent;
                let msgType;
                try {
                    const payloadStr = Buffer.from(envelope.payload, 'base64').toString('utf-8');
                    const parsed = JSON.parse(payloadStr);
                    if (typeof parsed === 'object' && parsed !== null && 'text' in parsed) {
                        textContent = String(parsed.text);
                        msgType = parsed.type;
                    }
                    else if (typeof parsed === 'string') {
                        textContent = parsed;
                    }
                    else {
                        textContent = JSON.stringify(parsed);
                    }
                }
                catch {
                    textContent = `[undecryptable relay message from ${String(envelope.from).slice(0, 16)}]`;
                }
                const received = {
                    from: String(envelope.from ?? 'unknown'),
                    fromName: String(envelope.from ?? 'unknown').slice(0, 8),
                    threadId: String(envelope.threadId ?? `relay-${Date.now()}`),
                    messageId: String(envelope.messageId ?? `msg-${Date.now()}`),
                    content: { content: textContent, type: msgType },
                    timestamp: String(envelope.timestamp ?? new Date().toISOString()),
                    envelope: envelope,
                };
                // Relay-authenticated unknown senders bypass the trust manager gate.
                // The relay already verified their Ed25519 identity via challenge-response.
                // We still run payload size checks but skip trust/rate checks.
                const payloadSize = Buffer.byteLength(JSON.stringify(received.content), 'utf-8');
                if (payloadSize > 64 * 1024) {
                    console.log(`Threadline: relay message from ${received.from.slice(0, 8)} blocked (payload too large: ${payloadSize})`);
                    return;
                }
                // Record the interaction for trust building
                trustManager.recordMessageReceivedByFingerprint(received.from);
                // Emit gate-passed with relay-authenticated trust level
                relayClient.emit('gate-passed', {
                    action: 'pass',
                    reason: 'relay-authenticated',
                    trustLevel: 'verified',
                    fingerprint: received.from,
                    message: received,
                });
            });
            // Log auto-discovery results
            relayClient.on('auto-discovered', (info) => {
                console.log(`Threadline: auto-discovered ${info.count} agent(s) on relay`);
            });
            try {
                await relayClient.connect();
                console.log(`Threadline: relay connected (fingerprint: ${relayClient.fingerprint})`);
            }
            catch (err) {
                console.error(`Threadline: relay connection failed — ${err instanceof Error ? err.message : err}`);
                console.log('Threadline: agent will operate in local-only mode');
                relayClient = undefined;
            }
        }
        else {
            // Daemon handles the relay connection (both inbound and outbound).
            // The server's relay client is NOT connected — outbound messages are
            // sent by the daemon via a file-based outbox or via sessions using
            // the threadline_send MCP tool (which connects its own stdio subprocess).
            // The relay client object exists but is not connected.
            console.log(`Threadline: daemon handles relay — server relay client not connected (outbound via MCP tools)`);
        }
    }
    return {
        handshakeManager,
        discovery,
        trustManager,
        relayClient,
        inboundGate,
        shutdown: async () => {
            stopHeartbeat();
            if (relayClient) {
                relayClient.disconnect();
            }
            if (inboundGate) {
                inboundGate.shutdown();
            }
            if (trustManager) {
                trustManager.flush();
            }
        },
    };
}
// ── MCP Registration ─────────────────────────────────────────────────
/**
 * Register the Threadline MCP server into every framework config present.
 *
 * Claude Code: ~/.claude.json (local scope) + .mcp.json (project scope).
 * Codex CLI: `[mcp_servers."threadline"]` in ~/.codex/config.toml — added
 *   for the cross-framework portability audit (Gap 2). Before this, a
 *   Codex agent that joined the Threadline network had the relay running
 *   but no MCP tools advertised to its runtime. The Codex TOML format and
 *   path were empirically verified from a live ~/.codex/ (Codex CLI
 *   0.78.0); registration reuses the existing, tested
 *   OpenAiCodexMcpToolRegistry so the two writers cannot drift.
 *
 * The MCP server is a stdio process the framework launches as a subprocess.
 * Codex registration is gated on ~/.codex/ existing (Codex installed on
 * this machine) so a Claude-only host is never given a Codex config it
 * does not use.
 */
async function registerThreadlineMcp(projectDir, agentName, stateDir) {
    const absDir = path.resolve(projectDir);
    // The MCP server entry point — runs as a child process of the agent's
    // framework CLI. Single source of truth shared with the per-spawn Codex
    // override (see mcpEntry.ts / SessionManager).
    const mcpEntry = resolveThreadlineMcpEntry(projectDir, stateDir, agentName);
    // ── 1. Register in ~/.claude.json at local scope ──
    const claudeJsonPath = path.join(os.homedir(), '.claude.json');
    try {
        let claudeJson = {};
        if (fs.existsSync(claudeJsonPath)) {
            claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf-8'));
        }
        if (!claudeJson.projects || typeof claudeJson.projects !== 'object') {
            claudeJson.projects = {};
        }
        const projects = claudeJson.projects;
        if (!projects[absDir]) {
            projects[absDir] = {};
        }
        const projectEntry = projects[absDir];
        if (!projectEntry.mcpServers || typeof projectEntry.mcpServers !== 'object') {
            projectEntry.mcpServers = {};
        }
        const mcpServers = projectEntry.mcpServers;
        // Register (or update) the Threadline MCP server
        mcpServers.threadline = mcpEntry;
        // Pre-accept trust
        projectEntry.hasTrustDialogAccepted = true;
        // Write atomically
        const tmpPath = `${claudeJsonPath}.${process.pid}.tmp`;
        fs.writeFileSync(tmpPath, JSON.stringify(claudeJson, null, 2));
        fs.renameSync(tmpPath, claudeJsonPath);
    }
    catch {
        // Non-fatal — .mcp.json fallback below
    }
    // ── 2. Also add to .mcp.json in the project root ──
    const mcpJsonPath = path.join(projectDir, '.mcp.json');
    try {
        let mcpConfig = {};
        if (fs.existsSync(mcpJsonPath)) {
            mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8'));
        }
        if (!mcpConfig.mcpServers || typeof mcpConfig.mcpServers !== 'object') {
            mcpConfig.mcpServers = {};
        }
        const mcpServers = mcpConfig.mcpServers;
        mcpServers.threadline = mcpEntry;
        const tmpPath = `${mcpJsonPath}.${process.pid}.tmp`;
        fs.writeFileSync(tmpPath, JSON.stringify(mcpConfig, null, 2));
        fs.renameSync(tmpPath, mcpJsonPath);
    }
    catch {
        // Non-fatal
    }
    // ── 3. Also register in Codex's ~/.codex/config.toml (portability Gap 2) ──
    // Gated on ~/.codex/ existing so a Claude-only host never gets a Codex
    // config it does not use. Reuses the existing OpenAiCodexMcpToolRegistry
    // (idempotent remove-then-append of the [mcp_servers."threadline"] table)
    // so the TOML writer is never duplicated. Non-fatal, matching the Claude
    // registration blocks above.
    try {
        const codexHome = process.env['CODEX_HOME'] || path.join(os.homedir(), '.codex');
        if (fs.existsSync(codexHome)) {
            const { createMcpToolRegistry } = await import('../providers/adapters/openai-codex/integration/mcpToolRegistry.js');
            await createMcpToolRegistry().register({
                kind: 'stdio',
                name: 'threadline',
                command: mcpEntry.command,
                args: mcpEntry.args,
            }, { scope: 'user' });
        }
    }
    catch {
        // Non-fatal — Claude registration above is unaffected.
    }
}
//# sourceMappingURL=ThreadlineBootstrap.js.map