/**
 * Shared foundation for feature-agent graph nodes.
 *
 * Provides consistent logging, spec file reading, executor invocation,
 * and error handling so every node follows the same patterns.
 */
import yaml from 'js-yaml';
import { readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { interrupt, isGraphBubbleUp } from '@langchain/langgraph';
import { reportNodeStart } from '../heartbeat.js';
import { recordPhaseStart, recordPhaseEnd } from '../phase-timing-context.js';
/**
 * Create a scoped logger that prefixes messages with the node name.
 * Output goes to stdout which the worker redirects to the log file.
 */
export function createNodeLogger(nodeName) {
    return {
        info(message) {
            const ts = new Date().toISOString();
            process.stdout.write(`[${ts}] [${nodeName}] ${message}\n`);
        },
        error(message) {
            const ts = new Date().toISOString();
            process.stderr.write(`[${ts}] [${nodeName}] ERROR: ${message}\n`);
        },
    };
}
/**
 * Safely read a file from the spec directory. Returns empty string if file doesn't exist.
 */
export function readSpecFile(specDir, filename) {
    try {
        return readFileSync(join(specDir, filename), 'utf-8');
    }
    catch {
        return '';
    }
}
/**
 * Build executor options with cwd. Each node gets a clean agent context.
 */
export function buildExecutorOptions(state) {
    return {
        cwd: state.worktreePath || state.repositoryPath,
        maxTurns: 50,
        disableMcp: true,
    };
}
/**
 * Sanitize YAML content to handle common AI-generated issues.
 * Quotes unquoted list items containing `{` or `}` characters
 * which js-yaml interprets as flow mappings.
 */
function sanitizeYamlBraces(content) {
    return content.replace(/^(\s*- )(?!['"])(.+[{}].+)$/gm, (_match, prefix, value) => `${prefix}"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`);
}
/**
 * Parse YAML with automatic sanitization of common AI-generated issues.
 * First attempts normal parsing; on failure, sanitizes unquoted braces
 * in list items and retries.
 */
export function safeYamlLoad(content) {
    try {
        return yaml.load(content);
    }
    catch (firstError) {
        const sanitized = sanitizeYamlBraces(content);
        if (sanitized === content)
            throw firstError; // nothing to sanitize
        return yaml.load(sanitized);
    }
}
/**
 * Determine whether the current node should trigger an interrupt
 * for human approval given the configured approval gates.
 *
 * Gates control which phases auto-approve:
 * - allowPrd: when true, skip interrupt after requirements phase
 * - allowPlan: when true, skip interrupt after plan phase
 *
 * Nodes not covered by a gate (analyze, research) never interrupt.
 * The implement node always interrupts when gates are present
 * (unless both gates are true, meaning fully autonomous).
 */
export function shouldInterrupt(nodeName, gates) {
    if (!gates)
        return false;
    if (gates.allowPrd && gates.allowPlan)
        return false;
    if (nodeName === 'requirements')
        return !gates.allowPrd;
    if (nodeName === 'plan')
        return !gates.allowPlan;
    if (nodeName === 'implement')
        return true;
    return false;
}
const API_ERROR_RE = /API Error: (400|429|5\d{2})/;
const NETWORK_ERROR_RE = /ECONNREFUSED|ETIMEDOUT|ENOTFOUND|timed out/i;
const NON_RETRYABLE_RE = /Process exited with code|ENOENT|SyntaxError/;
/**
 * Classify an error message into a retry category.
 *
 * - **retryable-api**: Transient API errors (rate-limit, overload, bad request)
 * - **retryable-network**: Network-level failures (DNS, timeouts, connection refused)
 * - **non-retryable**: Logic / filesystem / syntax errors that won't resolve on retry
 * - **unknown**: Anything unrecognised (callers may choose to retry cautiously)
 */
export function classifyError(errorMessage) {
    if (API_ERROR_RE.test(errorMessage))
        return 'retryable-api';
    if (NETWORK_ERROR_RE.test(errorMessage))
        return 'retryable-network';
    if (NON_RETRYABLE_RE.test(errorMessage))
        return 'non-retryable';
    return 'unknown';
}
/**
 * Execute a prompt via the given executor with automatic retry and
 * exponential back-off for transient errors.
 *
 * Non-retryable errors are thrown immediately. Unknown errors are
 * retried (conservative stance: could be transient).
 */
export async function retryExecute(executor, prompt, options, retryOpts) {
    const maxAttempts = retryOpts?.maxAttempts ?? 3;
    const baseDelayMs = retryOpts?.baseDelayMs ?? 2000;
    const log = retryOpts?.logger;
    let lastError;
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
            return await executor.execute(prompt, options);
        }
        catch (err) {
            lastError = err instanceof Error ? err : new Error(String(err));
            const category = classifyError(lastError.message);
            if (category === 'non-retryable') {
                throw lastError;
            }
            if (attempt < maxAttempts) {
                const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
                log?.info(`Attempt ${attempt}/${maxAttempts} failed (${category}), retrying in ${delayMs}ms`);
                await new Promise((resolve) => setTimeout(resolve, delayMs));
            }
        }
    }
    throw lastError;
}
/**
 * Read completed phases from feature.yaml.
 */
export function getCompletedPhases(specDir) {
    const content = readSpecFile(specDir, 'feature.yaml');
    if (!content)
        return [];
    try {
        const data = yaml.load(content);
        const status = (data?.status ?? {});
        const phases = status.completedPhases;
        return Array.isArray(phases) ? phases : [];
    }
    catch {
        return [];
    }
}
/**
 * Mark a phase as complete in feature.yaml by adding its ID to completedPhases.
 */
export function markPhaseComplete(specDir, phaseId, log) {
    try {
        const content = readSpecFile(specDir, 'feature.yaml');
        if (!content)
            return;
        const data = yaml.load(content);
        if (!data || typeof data !== 'object')
            return;
        const status = (data.status ?? {});
        const completedPhases = Array.isArray(status.completedPhases)
            ? [...status.completedPhases]
            : [];
        if (!completedPhases.includes(phaseId)) {
            completedPhases.push(phaseId);
        }
        data.status = { ...status, completedPhases };
        writeFileSync(join(specDir, 'feature.yaml'), yaml.dump(data, { indent: 2, lineWidth: -1 }), 'utf-8');
    }
    catch (err) {
        log?.error(`Failed to mark phase complete: ${err instanceof Error ? err.message : String(err)}`);
    }
}
/**
 * Execute a node with consistent logging and error handling.
 *
 * Wraps the node's core logic with:
 * - Entry/exit logging with timing
 * - Error catching that returns error state instead of throwing
 * - Consistent state shape for success and failure
 */
export function executeNode(nodeName, executor, buildPrompt) {
    const log = createNodeLogger(nodeName);
    return async (state) => {
        log.info('Starting...');
        reportNodeStart(nodeName);
        // On resume from interrupt, LangGraph re-executes the node function from
        // the top. Skip the expensive executor call if this phase already completed
        // (markPhaseComplete is called before interrupt, so completed = done but
        // awaiting approval; the approval has now been granted).
        const completedPhases = getCompletedPhases(state.specDir);
        if (completedPhases.includes(nodeName)) {
            log.info('Phase already completed (resuming from approval), skipping execution');
            return {
                currentNode: nodeName,
                messages: [`[${nodeName}] Approved — continuing`],
            };
        }
        const startTime = Date.now();
        // Record phase start (no-op if timing context not set)
        const timingId = await recordPhaseStart(nodeName);
        try {
            const prompt = buildPrompt(state, log);
            const options = buildExecutorOptions(state);
            log.info(`Executing agent at cwd=${options.cwd}`);
            log.info(`Prompt length: ${prompt.length} chars`);
            const result = await executor.execute(prompt, options);
            const durationMs = Date.now() - startTime;
            const elapsed = (durationMs / 1000).toFixed(1);
            log.info(`Complete (${result.result.length} chars, ${elapsed}s)`);
            // Record phase completion
            await recordPhaseEnd(timingId, durationMs);
            // Mark phase complete BEFORE interrupting so that on resume the
            // node detects the work is already done and skips re-execution.
            markPhaseComplete(state.specDir, nodeName, log);
            const nodeResult = {
                currentNode: nodeName,
                messages: [`[${nodeName}] Complete (${result.result.length} chars, ${elapsed}s)`],
            };
            // Human-in-the-loop: interrupt after node execution for review
            if (shouldInterrupt(nodeName, state.approvalGates)) {
                log.info('Interrupting for human approval');
                interrupt({
                    node: nodeName,
                    result: result.result.slice(0, 500),
                    message: `Node "${nodeName}" completed. Approve to continue.`,
                });
            }
            return nodeResult;
        }
        catch (err) {
            // Re-throw LangGraph control-flow exceptions (interrupt, Command, etc.)
            if (isGraphBubbleUp(err))
                throw err;
            const message = err instanceof Error ? err.message : String(err);
            const durationMs = Date.now() - startTime;
            const elapsed = (durationMs / 1000).toFixed(1);
            log.error(`${message} (after ${elapsed}s)`);
            // Leave timingId without completedAt (partial timing for failed nodes)
            // Throw so LangGraph does NOT checkpoint this node as "completed".
            // The worker catch block marks the run as failed, and on resume
            // LangGraph re-executes from the last successfully checkpointed node.
            throw new Error(`[${nodeName}] ${message}`);
        }
    };
}
