import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
import { dirname, join } from 'node:path';
import { homedir } from 'node:os';
import { sessionCompletionSchema, } from '../../../../modules/claude-invocation/entities/sessionCompletion/sessionCompletion.schema.js';
// Deferred to runtime so test files that mock `node:os` (which makes
// `homedir()` return undefined at import time) can still load this module
// without crashing their unrelated test suites.
function defaultDirectory() {
    return join(homedir(), '.claude-review', 'completions');
}
const DEFAULT_POLL_INTERVAL_MS = 1000;
/**
 * Cross-process implementation of the MCP completion bridge. The MCP server
 * runs as a sub-process spawned by `claude --bg`, so an in-memory pub/sub
 * cannot reach the Fastify host. Instead, the publisher (MCP handler) writes
 * a small JSON file under ~/.claude-review/completions/<jobId>.json, and the
 * subscriber (Fastify, inside awaitSessionCompletion) polls that path until
 * it appears, then notifies its listener and removes the file.
 *
 * The 1s poll cadence is much faster than the 30s agents-json fallback so the
 * MCP signal remains "primary" in practice. If the host crashes between
 * publish and subscribe consumption, the file lingers; the next subscription
 * picks it up as a buffered event (same semantics as the in-memory variant).
 */
export class FileSystemMcpCompletionBridge {
    directory;
    pollIntervalMs;
    setIntervalImpl;
    clearIntervalImpl;
    subscriptions = new Map();
    constructor(options = {}) {
        this.directory = options.directory ?? defaultDirectory();
        this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
        this.setIntervalImpl = options.setIntervalImpl ?? setInterval;
        this.clearIntervalImpl = options.clearIntervalImpl ?? clearInterval;
        ensureDirectory(this.directory);
    }
    publish(jobId, completion) {
        ensureDirectory(this.directory);
        const path = this.fileFor(jobId);
        const tempPath = `${path}.tmp`;
        writeFileSync(tempPath, JSON.stringify(completion), 'utf-8');
        // Rename is atomic on POSIX, avoiding partial reads by a concurrent
        // subscriber polling the file.
        renameAtomic(tempPath, path);
    }
    subscribe(jobId, listener) {
        if (this.subscriptions.has(jobId)) {
            // Replace any previous subscription (last-writer-wins semantics, same
            // as the in-memory bridge).
            this.unsubscribe(jobId);
        }
        const tryConsume = () => {
            const path = this.fileFor(jobId);
            if (!existsSync(path))
                return false;
            const parsed = readAndParse(path);
            if (parsed === null) {
                // Malformed file: remove it and report a failed completion so callers
                // do not wait forever on garbage.
                safeUnlink(path);
                listener({ source: 'mcp', outcome: 'failed', reason: 'completion-bridge-malformed' });
                return true;
            }
            safeUnlink(path);
            listener(parsed);
            return true;
        };
        if (tryConsume())
            return;
        const timer = this.setIntervalImpl(() => {
            tryConsume();
        }, this.pollIntervalMs);
        this.subscriptions.set(jobId, { timer, listener });
    }
    unsubscribe(jobId) {
        const subscription = this.subscriptions.get(jobId);
        if (subscription) {
            this.clearIntervalImpl(subscription.timer);
            this.subscriptions.delete(jobId);
        }
        // Always drop any buffered event so a future subscribe for the same
        // jobId does not pick a stale completion. Aligned with InMemoryMcp-
        // CompletionBridge's "no pending event after unsubscribe" semantics.
        safeUnlink(this.fileFor(jobId));
    }
    fileFor(jobId) {
        const safe = jobId.replace(/[^A-Za-z0-9._-]/g, '_');
        return join(this.directory, `${safe}.json`);
    }
}
function ensureDirectory(directoryPath) {
    if (!existsSync(directoryPath)) {
        mkdirSync(directoryPath, { recursive: true });
    }
}
function renameAtomic(source, destination) {
    // node:fs.renameSync is atomic on POSIX. The retry covers the rare case
    // where the destination directory has been removed between the write and
    // the rename (e.g. an external cleanup process). One retry is enough — if
    // it still fails the caller's enclosing try/catch surfaces the error.
    try {
        renameSync(source, destination);
    }
    catch {
        ensureDirectory(dirname(destination));
        renameSync(source, destination);
    }
}
function readAndParse(filePath) {
    try {
        const raw = readFileSync(filePath, 'utf-8');
        const json = JSON.parse(raw);
        const result = sessionCompletionSchema.safeParse(json);
        return result.success ? result.data : null;
    }
    catch {
        return null;
    }
}
function safeUnlink(filePath) {
    try {
        if (existsSync(filePath)) {
            unlinkSync(filePath);
        }
    }
    catch {
        // Best-effort cleanup.
    }
}
//# sourceMappingURL=mcpCompletion.fileSystem.gateway.js.map