/**
 * Lock file management — tracks generated file integrity via SHA-1 to detect user-modified files.
 * Preservation regions: markers `AURORA:<NAME>-START/END` in HTML comments let bodies survive
 * regeneration (see openspec/changes/preservation-regions).
 */
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { LOCK_JSON_VERSION } from '../domain/config.js';
/**
 * SHA-1 hex digest. Used as the integrity primitive for the lockfile.
 * Not cryptographically strong — purpose is change detection, not security.
 */
export function sha1(content) {
    return crypto.createHash('sha1').update(content).digest('hex');
}
const NAME_PATTERN = '[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*';
function buildHtmlTokenRegex() {
    return new RegExp(`<!--[ \\t]*#(?<kind>region|endregion)[ \\t]+AURORA:(?<name>${NAME_PATTERN})-(?<suffix>START|END)[ \\t]*-->`, 'g');
}
function lineOfIndex(content, index) {
    let line = 1;
    for (let i = 0; i < index; i++) {
        if (content.codePointAt(i) === 10)
            line++;
    }
    return line;
}
function scanTokens(content) {
    const tokens = [];
    for (const m of content.matchAll(buildHtmlTokenRegex())) {
        const groups = m.groups;
        tokens.push({
            index: m.index,
            kind: groups.kind,
            length: m[0].length,
            line: lineOfIndex(content, m.index),
            name: groups.name,
            suffix: groups.suffix,
        });
    }
    return tokens;
}
function parseRegions(content) {
    const tokens = scanTokens(content);
    const regions = [];
    let open = null;
    for (const tok of tokens) {
        if (tok.kind === 'region' && tok.suffix !== 'START') {
            throw new Error(`Inconsistent marker: #region must end with -START (line ${tok.line}, name ${tok.name})`);
        }
        if (tok.kind === 'endregion' && tok.suffix !== 'END') {
            throw new Error(`Inconsistent marker: #endregion must end with -END (line ${tok.line}, name ${tok.name})`);
        }
        if (tok.kind === 'region') {
            if (open !== null) {
                throw new Error(`Nested regions not supported (${open.name}, ${tok.name}) at line ${tok.line}`);
            }
            open = tok;
        }
        else {
            if (open === null) {
                throw new Error(`Unmatched END: ${tok.name} at line ${tok.line}`);
            }
            if (open.name !== tok.name) {
                throw new Error(`Mismatched END: expected ${open.name}, found ${tok.name} at line ${tok.line}`);
            }
            regions.push({
                bodyEnd: tok.index,
                bodyStart: open.index + open.length,
                name: tok.name,
            });
            open = null;
        }
    }
    if (open !== null) {
        throw new Error(`Unmatched START: ${open.name} at line ${open.line}`);
    }
    return regions;
}
/**
 * Extract preservation region bodies from content.
 * Returns a Map<regionName, bodyContent>. Throws on malformed markers.
 */
export function extractPreservationRegions(content) {
    const regions = parseRegions(content);
    const out = new Map();
    for (const r of regions) {
        out.set(r.name, content.slice(r.bodyStart, r.bodyEnd));
    }
    return out;
}
/**
 * Strip preservation region bodies from content, preserving markers.
 * Renaming a marker invalidates the hash because the marker name stays in
 * the stripped string. Files without regions are returned unchanged.
 */
export function stripPreservationRegions(content) {
    const regions = parseRegions(content);
    if (regions.length === 0)
        return content;
    let out = '';
    let cursor = 0;
    for (const r of regions) {
        out += content.slice(cursor, r.bodyStart);
        cursor = r.bodyEnd;
    }
    out += content.slice(cursor);
    return out;
}
/**
 * Compute integrity hash EXCLUDING preservation region bodies.
 * For files without regions this equals `sha1(content)` (backward compat).
 */
export function sha1WithPreservation(content) {
    return sha1(stripPreservationRegions(content));
}
/**
 * Normalize a region body for stable hashing: LF line endings and no trailing
 * whitespace. Applied ONLY to the hash input; the stored body keeps its
 * original formatting.
 */
export function normalizeRegionBody(body) {
    return body.replaceAll('\r\n', '\n').replace(/\s+$/, '');
}
/**
 * Hash each preservation region body in content. Returns a plain object
 * ready to serialize into the lockfile. Empty input → empty object.
 */
export function hashRegionBodies(content) {
    const out = {};
    for (const [name, body] of extractPreservationRegions(content)) {
        out[name] = sha1(normalizeRegionBody(body));
    }
    return out;
}
/**
 * Merge preservation regions: per-region decision based on priorHashes.
 * - If the existing body hash matches priorHashes[name] → use newContent body
 *   (user did not modify → template improvement propagates).
 * - Otherwise → preserve existingContent body byte-for-byte.
 * - If priorHashes is undefined → preserve all (fallback, legacy lockfile).
 * - Regions only in newContent keep their new body.
 * - Regions only in existingContent are dropped (body lost).
 */
export function mergePreservationRegions(newContent, existingContent, priorHashes) {
    const newRegions = parseRegions(newContent);
    const existingRegions = parseRegions(existingContent);
    const existingByName = new Map(existingRegions.map((r) => [r.name, r]));
    const preserved = [];
    const updated = [];
    let out = '';
    let cursor = 0;
    for (const nr of newRegions) {
        const er = existingByName.get(nr.name);
        if (!er) {
            // Region only in new — keep new body as-is
            continue;
        }
        const existingBody = existingContent.slice(er.bodyStart, er.bodyEnd);
        const existingHash = sha1(normalizeRegionBody(existingBody));
        const priorHash = priorHashes?.[nr.name];
        if (priorHash !== undefined && priorHash === existingHash) {
            // User did not touch → propagate new body (already in newContent)
            updated.push(nr.name);
            continue;
        }
        // User edited → preserve existing body byte-for-byte
        out += newContent.slice(cursor, nr.bodyStart);
        out += existingBody;
        cursor = nr.bodyEnd;
        preserved.push(nr.name);
    }
    out += newContent.slice(cursor);
    const newNames = new Set(newRegions.map((r) => r.name));
    const dropped = existingRegions
        .filter((r) => !newNames.has(r.name))
        .map((r) => r.name);
    return { content: out, dropped, preserved, updated };
}
/** Load lock files from `cliter/{boundedContext}/.locks/{scope}/{moduleName}.lock.json`. */
export function loadLockFiles(boundedContextName, moduleName, scope, cwd = process.cwd()) {
    const lockPath = path.join(cwd, 'cliter', boundedContextName, '.locks', scope, `${moduleName}.lock.json`);
    if (!fs.existsSync(lockPath)) {
        return [];
    }
    try {
        const raw = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
        return Array.isArray(raw.files) ? raw.files : [];
    }
    catch {
        return [];
    }
}
/** Save lock files to `cliter/{boundedContext}/.locks/{scope}/{moduleName}.lock.json`. */
// eslint-disable-next-line max-params
export function saveLockFiles(boundedContextName, moduleName, lockFiles, scope, cwd = process.cwd()) {
    const dir = path.join(cwd, 'cliter', boundedContextName, '.locks', scope);
    if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true });
    }
    const lockPath = path.join(dir, `${moduleName}.lock.json`);
    fs.writeFileSync(lockPath, JSON.stringify({
        files: lockFiles,
        version: LOCK_JSON_VERSION,
    }, null, 2), 'utf8');
}
export function findLockFile(lockFiles, relativePath) {
    // Normalize to posix separators for comparison
    const normalized = path.sep === '\\' ? relativePath.replaceAll('\\', '/') : relativePath;
    return lockFiles.find((lf) => lf.path === normalized);
}
/** True when the file's current content matches its lock entry — i.e. user did NOT modify it. */
export function isFileUnmodified(filePath, lockFiles, relativePath) {
    const entry = findLockFile(lockFiles, relativePath);
    if (!entry)
        return true; // No lock entry → unmodified (allow overwrite)
    if (!fs.existsSync(filePath))
        return true;
    const content = fs.readFileSync(filePath, 'utf8');
    return entry.integrity === `sha1:${sha1WithPreservation(content)}`;
}
