/**
 * WikiIngestService — write canonical pages into a vault's `llm-curated/`.
 *
 * Phase A scope per v2.1 spec: append entries to `llm-curated/log.md` for
 * every chat/spec/learning source. Decisions/pattern pages (separate files
 * under `llm-curated/decisions/`) are gated behind the LLM routing layer
 * which lands in Phase B.
 *
 * Frozen-path contract (§2): refuses ANY write whose target lives under a
 * `hardcoded:` folder. SchemaLoaderService.isFrozenPath() is the gate.
 *
 * @module services/wiki/wiki-ingest.service
 */
import * as path from 'path';
import * as fs from 'fs/promises';
import { existsSync } from 'fs';
import { LoggerService } from '../core/logger.service.js';
import { SchemaLoaderService } from './schema-loader.service.js';
const DEFAULT_LOG_RELATIVE_PATH = 'llm-curated/log.md';
const MAX_BODY_BYTES = 64 * 1024;
// Spec note (Steve, 2026-05-22): the earlier keyword-based
// `detectMessageShape` heuristic + `buildDecisionSlug` + `ingestDecision`
// dual-write were REMOVED. Routing into `llm-curated/<folder>/<page>.md`
// is now agent-driven via `wiki-process-queue` + the agent's own LLM gate
// — see WikiQueueService for the queue + the orchestrator system prompt
// for the rule that says "queue worth-saving content as you see it." This
// service stays low-level: it only writes the path the caller (skill,
// route, queue processor) specifies, after the frozen-path gate.
/**
 * Writes ingest pages to a vault. Construct one per process; stateless.
 */
export class WikiIngestService {
    static instance = null;
    logger;
    schemaLoader;
    constructor(schemaLoader) {
        this.logger = LoggerService.getInstance().createComponentLogger('WikiIngest');
        this.schemaLoader = schemaLoader ?? new SchemaLoaderService();
    }
    static getInstance() {
        if (!this.instance) {
            this.instance = new WikiIngestService();
        }
        return this.instance;
    }
    /** Test-only reset. */
    static _resetForTesting() {
        this.instance = null;
    }
    /**
     * Ingest a single source into the vault.
     *
     * Errors are returned as structured outcomes — this method never throws
     * on legitimate refusals (frozen path, missing schema, empty body) so
     * callers can fold the result into their normal control flow.
     */
    async ingest(input) {
        const validation = this.validateInput(input);
        if (validation) {
            return validation;
        }
        let schema;
        try {
            schema = await this.schemaLoader.load(input.vaultPath);
        }
        catch (err) {
            return {
                ok: false,
                reason: 'schema_missing',
                message: err.message,
            };
        }
        const target = input.targetRelativePath ?? DEFAULT_LOG_RELATIVE_PATH;
        if (this.schemaLoader.isFrozenPath(schema, target)) {
            return {
                ok: false,
                reason: 'frozen_path',
                attemptedPath: target,
                frozenFolders: this.schemaLoader.getFrozenPaths(schema),
            };
        }
        const absoluteTarget = path.join(input.vaultPath, target);
        await fs.mkdir(path.dirname(absoluteTarget), { recursive: true });
        const logEntry = this.formatLogEntry(input);
        await this.appendOrCreate(absoluteTarget, logEntry, input);
        this.logger.info('WikiIngest wrote log entry', {
            vault: input.vaultPath,
            target,
            sourceType: input.sourceType,
            sourceRef: input.sourceRef,
            bodyBytes: input.sourceBody.length,
        });
        return {
            ok: true,
            pagesWritten: [target],
            logEntry,
            frozenPathsTouched: [],
        };
    }
    // ---------------------------------------------------------------------------
    // Internals
    // ---------------------------------------------------------------------------
    validateInput(input) {
        if (!input.vaultPath || !path.isAbsolute(input.vaultPath)) {
            return {
                ok: false,
                reason: 'invalid_input',
                message: `vaultPath must be an absolute path, got "${input.vaultPath ?? ''}"`,
            };
        }
        if (!input.sourceType) {
            return { ok: false, reason: 'invalid_input', message: 'sourceType is required' };
        }
        if (!input.sourceRef) {
            return { ok: false, reason: 'invalid_input', message: 'sourceRef is required' };
        }
        const body = input.sourceBody ?? '';
        if (body.trim().length === 0) {
            return {
                ok: false,
                reason: 'empty_body',
                message: 'sourceBody is empty after trimming whitespace',
            };
        }
        if (Buffer.byteLength(body, 'utf8') > MAX_BODY_BYTES) {
            return {
                ok: false,
                reason: 'invalid_input',
                message: `sourceBody exceeds ${MAX_BODY_BYTES} bytes`,
            };
        }
        return null;
    }
    /**
     * Build the markdown entry. Format is chosen to be safely append-only
     * (no preceding section to rewrite) and grep-friendly:
     *
     *   ## [<ISO>] <sourceType> | <callerSession or sourceRef>
     *
     *   ref: <sourceRef>
     *
     *   <body>
     */
    formatLogEntry(input) {
        const ts = new Date().toISOString();
        const header = this.sanitizeOneLine(input.callerSession ?? input.sourceRef, 80);
        const body = this.sanitizeBody(input.sourceBody);
        const ref = this.sanitizeOneLine(input.sourceRef, 200);
        return [
            '',
            `## [${ts}] ${input.sourceType} | ${header}`,
            '',
            `ref: ${ref}`,
            '',
            body,
            '',
        ].join('\n');
    }
    async appendOrCreate(absolutePath, entry, input) {
        if (!existsSync(absolutePath)) {
            const header = this.buildPageHeader(absolutePath, input);
            await fs.writeFile(absolutePath, header + entry, 'utf8');
            return;
        }
        await fs.appendFile(absolutePath, entry, 'utf8');
    }
    /**
     * Pick the right header for a freshly-created page:
     *   - `log.md`            → audit-log preamble (append-only)
     *   - `decisions/*.md`    → decision-page title + provenance block
     *   - anything else       → minimal title block from the source ref
     */
    buildPageHeader(absolutePath, input) {
        const basename = absolutePath.split(/[/\\]/).pop() ?? '';
        if (basename === 'log.md') {
            return '# Activity log\n\nAppend-only log of ingested sources. Each entry: `## [<ISO>] <sourceType> | <caller>`.\n';
        }
        if (absolutePath.includes('/decisions/')) {
            const title = this.sanitizeOneLine(input.sourceBody, 80);
            const caller = input.callerSession ?? input.sourceRef;
            return [
                `# ${title}`,
                '',
                `> **source:** \`${this.sanitizeOneLine(input.sourceRef, 200)}\`  `,
                `> **caller:** \`${this.sanitizeOneLine(caller, 80)}\`  `,
                `> **recorded:** ${new Date().toISOString()}`,
                '',
                '---',
                '',
            ].join('\n');
        }
        // Generic non-log target: minimal title block.
        return `# ${this.sanitizeOneLine(input.sourceRef, 80)}\n\n`;
    }
    /**
     * Defuse markers a future skill might mis-route on: `[CHAT]`, `[NOTIFY]`,
     * `[EVENT]`, `[ESCALATION]` get a zero-width space inserted so they're
     * still human-readable but don't trigger regex matchers downstream.
     * (Mirrors the escalation-router sanitizer from PR #606.)
     */
    sanitizeBody(body) {
        const trimmed = body.replace(/\r\n/g, '\n').trim();
        return trimmed.replace(/\[(CHAT|NOTIFY|EVENT|ESCALATION)\]/g, (_match, tag) => `[​${tag}]`);
    }
    sanitizeOneLine(value, maxLen) {
        const flat = value.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim();
        if (flat.length <= maxLen)
            return flat;
        return flat.slice(0, maxLen - 1) + '…';
    }
}
//# sourceMappingURL=wiki-ingest.service.js.map