/**
 * install/version-header.mjs — parse / inject the per-file
 * `apt-skill-version` and `apt-hook-version` headers regardless of
 * whether the file is a markdown skill, a markdown agent, a .js hook,
 * or a .sh hook.
 *
 * Used by:
 *   - check-version: parses installed-file headers to detect drift.
 *   - pipeline: parses canonical-file headers when computing manifest
 *     entries (so we record what version the canonical file claimed).
 *   - The canonical-file injector script (scripts/inject-version-header.mjs)
 *     for the markdown side; this module exposes the regex helpers it
 *     uses so both stay in sync.
 *   - apt-check-update-worker.js (via inspectManifestFiles): shared
 *     per-file inspection loop so worker and check-version stay in lock-step.
 *
 * Note: fs-reading helpers (inspectManifestFiles) are contained in this
 * module so both the worker and check-version import from a single source
 * of truth. Pure header helpers remain pure.
 */
import { createHash } from 'node:crypto';
import { existsSync, readFileSync, statSync } from 'node:fs';
import { isAbsolute, relative, resolve } from 'node:path';
/**
 * Compute SHA-256 hex digest of a Buffer or string.
 *
 * @param {Buffer|string} buf
 * @returns {string}
 */
export function sha256(buf) {
    return createHash('sha256').update(buf).digest('hex');
}
/**
 * Resolve `entryPath` relative to `installRoot`, asserting containment.
 * Throws for absolute paths or paths that escape installRoot via '..'.
 *
 * @param {string} installRoot  absolute install root directory
 * @param {string} entryPath    relative path from manifest entry
 * @returns {string}  resolved absolute path
 */
export function safeResolveWithin(installRoot, entryPath) {
    if (isAbsolute(entryPath)) {
        throw new Error(`manifest entry has absolute path: ${entryPath}`);
    }
    const abs = resolve(installRoot, entryPath);
    const rel = relative(installRoot, abs);
    if (rel.startsWith('..') || isAbsolute(rel)) {
        throw new Error(`manifest entry escapes install root: ${entryPath}`);
    }
    return abs;
}
/**
 * Walk a parsed manifest's file list and return an array of stale-file
 * records. This is the single source of truth for the per-file inspection
 * loop; both `check-version` and the SessionStart worker delegate here.
 *
 * Each entry in the returned array has:
 *   `{ path: string, reason: 'missing_file'|'hash_mismatch'|'missing_header'|'header_mismatch'|'invalid_path' }`
 *
 * @param {object} manifest        parsed apt-file-manifest.json (files[], framework_version)
 * @param {string} installRoot     absolute path to the runtime install root
 * @param {{ installedAtMs?: number }} [opts]
 *   `installedAtMs` — when provided, files whose mtime <= this value skip the
 *   hash+header check (fast path: no drift since last install).
 * @returns {Array<{path: string, reason: string}>}
 */
export function inspectManifestFiles(manifest, installRoot, opts = {}) {
    const { installedAtMs } = opts;
    const stale = [];
    for (const entry of manifest.files) {
        let abs;
        try {
            abs = safeResolveWithin(installRoot, entry.path);
        }
        catch {
            stale.push({ path: entry.path, reason: 'invalid_path' });
            continue;
        }
        if (!existsSync(abs)) {
            stale.push({ path: entry.path, reason: 'missing_file' });
            continue;
        }
        // mtime short-circuit: if installedAtMs is set and the file's mtime has
        // not changed since install, skip the expensive read+hash (assume clean).
        if (installedAtMs !== undefined) {
            try {
                const { mtimeMs } = statSync(abs);
                if (mtimeMs <= installedAtMs)
                    continue;
            }
            catch {
                // stat failed — fall through to full check
            }
        }
        const buf = readFileSync(abs);
        if (sha256(buf) !== entry.installedHash) {
            stale.push({ path: entry.path, reason: 'hash_mismatch' });
            continue;
        }
        const declared = parseVersionHeader(buf.toString('utf-8'));
        // `headerless` entries are files whose runtime transform legitimately
        // strips the apt-skill-version header (e.g. Codex whitelists frontmatter
        // to {name, description} and rejects unknown keys). For those, the
        // installedHash check above is the sole drift signal — requiring a header
        // would false-positive every install. The flag is recorded at install
        // time by the pipeline from the post-transform content.
        if (declared === null && !entry.headerless && /\.(md|js|mjs|sh)$/.test(entry.path)) {
            stale.push({ path: entry.path, reason: 'missing_header' });
            continue;
        }
        if (declared !== null && declared !== manifest.framework_version) {
            stale.push({ path: entry.path, reason: 'header_mismatch' });
        }
    }
    return stale;
}
const SKILL_KEY_RE = /^\s*apt-skill-version\s*:\s*(.+?)\s*$/m;
const SKILL_FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
const JS_HOOK_RE = /^\/\/\s*apt-hook-version\s*:\s*(.+?)\s*$/m;
const SH_HOOK_RE = /^#\s*apt-hook-version\s*:\s*(.+?)\s*$/m;
/**
 * Stable placeholder swapped in for the canonical `{{APT_VERSION}}` token
 * before any code path parses canonical frontmatter with a YAML library.
 *
 * `{{APT_VERSION}}` is valid Mustache but invalid YAML: the outer `{...}`
 * is read as a flow-map and the inner `{APT_VERSION}` as a flow-map used as
 * a key, which triggers yaml@2's "Keys with collection values will be
 * stringified due to JS Object restrictions" warning (one per parse).
 * Silencing via `logLevel: 'silent'` would hide genuine YAML issues, so
 * instead we pre-substitute with a syntactically harmless semver-shaped
 * string before parse. Install-time transforms still replace
 * `{{APT_VERSION}}` with the real framework version — see
 * `transforms/_shared.mjs#substituteVersion` — so installed files are
 * unchanged by this helper.
 */
export const APT_VERSION_PARSE_PLACEHOLDER = '0.0.0-apt-placeholder';
/**
 * Pre-substitute the canonical `{{APT_VERSION}}` Mustache token with a
 * YAML-safe placeholder so callers can `yaml.parse` canonical
 * frontmatter without emitting the flow-map-key warning. Pure string
 * function; idempotent on already-substituted content.
 *
 * Use this at every canonical-file parse site that runs BEFORE install
 * substitution (e.g. `apt-tools check`, schema-validation tests). The
 * install pipeline itself does NOT need this — it calls `substituteVersion`
 * with a real version first (see `transforms/_shared.mjs`).
 *
 * @param {string} content
 * @returns {string}
 */
export function sanitizeAptVersionForYamlParse(content) {
    if (!content)
        return content;
    return content.split('{{APT_VERSION}}').join(APT_VERSION_PARSE_PLACEHOLDER);
}
/**
 * Parse a file's version header. Looks in:
 *   1. Markdown YAML frontmatter (`apt-skill-version: x.y.z`).
 *   2. JS/MJS topmost line (`// apt-hook-version: x.y.z`).
 *   3. SH topmost non-shebang line (`# apt-hook-version: x.y.z`).
 *
 * Returns null when no header is found.
 *
 * @param {string} content
 * @returns {string|null}
 */
export function parseVersionHeader(content) {
    if (typeof content !== 'string' || content.length === 0)
        return null;
    // Markdown: only look inside frontmatter so a body mention of
    // "apt-skill-version" doesn't false-positive.
    const fmMatch = content.match(SKILL_FRONTMATTER_RE);
    if (fmMatch) {
        const fmText = fmMatch[1];
        const skillMatch = fmText.match(SKILL_KEY_RE);
        if (skillMatch)
            return skillMatch[1];
    }
    const jsMatch = content.match(JS_HOOK_RE);
    if (jsMatch)
        return jsMatch[1];
    const shMatch = content.match(SH_HOOK_RE);
    if (shMatch)
        return shMatch[1];
    return null;
}
/**
 * Inject `apt-skill-version: <version>` into a markdown file's frontmatter
 * if absent. Idempotent. Used by canonical-file injector and by transforms
 * that need to materialize the header on the fly.
 *
 * Insertion point: after `description:` (incl. continuation lines), else
 * after `name:`, else as the first key.
 *
 * @param {string} content
 * @param {string} version
 * @returns {string}
 */
export function injectMarkdownHeader(content, version) {
    if (!content.startsWith('---\n') && !content.startsWith('---\r\n'))
        return content;
    const eol = /^---\r\n/.test(content) ? '\r\n' : '\n';
    const lines = content.split(/\r?\n/);
    let endIdx = -1;
    for (let i = 1; i < lines.length; i++) {
        if (lines[i] === '---') {
            endIdx = i;
            break;
        }
    }
    if (endIdx === -1)
        return content;
    const fmLines = lines.slice(1, endIdx);
    for (const l of fmLines) {
        if (/^apt-skill-version\s*:/.test(l)) {
            // Already present — only substitute the value.
            return content.replace(/^(apt-skill-version\s*:\s*).+$/m, `$1${version}`);
        }
    }
    let insertAt = -1;
    for (let i = 0; i < fmLines.length; i++) {
        if (/^description\s*:/.test(fmLines[i])) {
            let j = i + 1;
            while (j < fmLines.length && /^\s+\S/.test(fmLines[j]))
                j++;
            insertAt = j;
            break;
        }
    }
    if (insertAt === -1) {
        for (let i = 0; i < fmLines.length; i++) {
            if (/^name\s*:/.test(fmLines[i])) {
                insertAt = i + 1;
                break;
            }
        }
    }
    if (insertAt === -1)
        insertAt = 0;
    const newFm = [...fmLines];
    newFm.splice(insertAt, 0, `apt-skill-version: ${version}`);
    const head = lines.slice(0, 1);
    const tail = lines.slice(endIdx);
    return [...head, ...newFm, ...tail].join(eol);
}
/**
 * Strip `apt-skill-version` / `apt-hook-version` lines entirely. Used by
 * tests that need to assert "missing_header" reasons without manually
 * editing files.
 *
 * @param {string} content
 * @returns {string}
 */
export function stripVersionHeader(content) {
    return content
        .replace(/^\s*apt-skill-version\s*:\s*.+$\r?\n?/m, '')
        .replace(/^\/\/\s*apt-hook-version\s*:\s*.+$\r?\n?/m, '')
        .replace(/^#\s*apt-hook-version\s*:\s*.+$\r?\n?/m, '');
}
//# sourceMappingURL=version-header.mjs.map