"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TaskAttemptSupersedeError = void 0;
exports.requireSupersedeAttemptId = requireSupersedeAttemptId;
exports.requireSupersedeTaskRevision = requireSupersedeTaskRevision;
exports.requireSupersedeTimestamp = requireSupersedeTimestamp;
exports.requireSupersedeText = requireSupersedeText;
exports.readSupersessionBaseline = readSupersessionBaseline;
exports.inspectTaskAttemptSupersessionEvidence = inspectTaskAttemptSupersessionEvidence;
exports.inspectSupersessionTargetDelta = inspectSupersessionTargetDelta;
exports.supersedeCurrentAttempt = supersedeCurrentAttempt;
const node_child_process_1 = require("node:child_process");
const node_crypto_1 = require("node:crypto");
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const validation_1 = require("../../validation");
const task_cycle_runtime_attribution_1 = require("../task-cycle-runtime-attribution");
const task_lifecycle_1 = require("../task-lifecycle");
const ATTEMPT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
class TaskAttemptSupersedeError extends Error {
    code;
    constructor(code, message) {
        super(message);
        this.name = 'TaskAttemptSupersedeError';
        this.code = code;
    }
}
exports.TaskAttemptSupersedeError = TaskAttemptSupersedeError;
function requireSupersedeAttemptId(value) {
    const attemptId = requireSupersedeText(value, 'Attempt identity');
    if (!ATTEMPT_ID_PATTERN.test(attemptId)) {
        throw new TaskAttemptSupersedeError('invalid-input', 'Attempt identity must use 3-128 machine-safe identity characters.');
    }
    return attemptId;
}
function requireSupersedeTaskRevision(value) {
    const revision = requireSupersedeText(value, 'Task revision');
    if (!/^[a-f0-9]{64}$/.test(revision)) {
        throw new TaskAttemptSupersedeError('invalid-input', 'Task revision must be the exact 64-character lowercase SHA-256 revision emitted for the selected task.');
    }
    return revision;
}
function requireSupersedeTimestamp(value) {
    const timestamp = requireSupersedeText(value, 'Attempt-supersede time');
    if (!(0, task_lifecycle_1.isTaskLifecycleTimestamp)(timestamp)) {
        throw new TaskAttemptSupersedeError('invalid-input', 'Attempt-supersede time must be an ISO-8601 timestamp with an explicit timezone.');
    }
    return timestamp;
}
function requireSupersedeText(value, label) {
    if (typeof value !== 'string' || value.trim() === '') {
        throw new TaskAttemptSupersedeError('invalid-input', `${label} must be non-empty.`);
    }
    return value.trim();
}
function readSupersessionBaseline(summaryPath, summaryText, attemptId) {
    try {
        const baseline = (0, task_cycle_runtime_attribution_1.readAttemptRuntimeBaseline)(summaryPath, summaryText, attemptId);
        if (baseline !== null)
            return baseline;
    }
    catch (error) {
        throw new TaskAttemptSupersedeError('baseline-required', error instanceof Error ? error.message : 'Attempt-owned runtime baseline is invalid.');
    }
    throw new TaskAttemptSupersedeError('baseline-required', `Attempt '${attemptId}' has no valid attempt-owned runtime-attribution baseline.`);
}
/** Returns material post-baseline lifecycle evidence that makes supersession unsafe. */
function inspectTaskAttemptSupersessionEvidence(summaryText, attemptId) {
    const lines = frontMatterLines(summaryText);
    const range = locateAttemptRange(lines, attemptId);
    if (range === null)
        return ['the selected attempt is not materialized'];
    const attemptLines = lines.slice(range.start, range.end);
    const evidence = [];
    for (const field of ['validation_results', 'deviations', 'waivers', 'blockers']) {
        const raw = attemptFieldRaw(attemptLines, field);
        if (raw !== null && !isEmptyCollectionScalar(raw))
            evidence.push(`${field} is not empty`);
    }
    for (const field of [
        'disposition_by',
        'disposition_at',
        'disposition_reason',
        'blocker_reason',
        'blocker_next',
        'blocked_by',
        'blocked_at',
        'review_waiver_id',
        'closed_at',
    ]) {
        const raw = attemptFieldRaw(attemptLines, field);
        if (raw !== null && !isNullScalar(raw) && parseYamlScalar(raw).trim() !== '') {
            evidence.push(`${field} is present`);
        }
    }
    const implementationStatus = attemptFieldRaw(attemptLines, 'implementation_status');
    if (implementationStatus !== null && !['pending', 'in_progress'].includes(parseYamlScalar(implementationStatus))) {
        evidence.push(`implementation_status is ${parseYamlScalar(implementationStatus)}`);
    }
    const reviewDisposition = attemptFieldRaw(attemptLines, 'review_disposition');
    if (reviewDisposition !== null && parseYamlScalar(reviewDisposition) !== 'pending') {
        evidence.push(`review_disposition is ${parseYamlScalar(reviewDisposition)}`);
    }
    if (lines.some((line) => /^runtime_attribution:\s*$/.test(line))) {
        evidence.push('summary runtime closeout evidence is present');
    }
    return evidence;
}
function inspectSupersessionTargetDelta(repositoryRoot, taskDefinitionPath, taskText, baseline) {
    const declared = (0, validation_1.readTaskDeclaredTargetPathEvidence)(taskDefinitionPath, taskText);
    const invalid = declared.evidence.filter((entry) => entry.state !== 'explicit');
    if (invalid.length > 0 || declared.explicitTargetPaths.length === 0) {
        return invalid.length > 0
            ? invalid.map((entry) => entry.message)
            : ['The revised task has no concrete declared implementation target paths.'];
    }
    return compareSupersessionTargets(repositoryRoot, declared.explicitTargetPaths, baseline);
}
function compareSupersessionTargets(repositoryRoot, declaredTargetPaths, baseline) {
    const blockers = [];
    const baselineByPath = new Map(baseline.declaredTargets.map((target) => [target.path, target]));
    for (const target of baseline.declaredTargets) {
        const current = safelyInspectTarget(repositoryRoot, target.path, blockers);
        if (current !== null && !sameSupersessionTarget(target, current)) {
            blockers.push(`Baseline target '${target.path}' changed after capture.`);
        }
    }
    for (const targetPath of declaredTargetPaths) {
        if (baselineByPath.has(targetPath))
            continue;
        const current = safelyInspectTarget(repositoryRoot, targetPath, blockers);
        if (current?.dirty) {
            blockers.push(`Newly declared target '${targetPath}' already has repository changes that were not observed by the predecessor baseline.`);
        }
    }
    return blockers;
}
function safelyInspectTarget(repositoryRoot, targetPath, blockers) {
    try {
        return inspectSupersessionTarget(repositoryRoot, targetPath);
    }
    catch (error) {
        if (error instanceof TaskAttemptSupersedeError && error.code === 'git-inspection-unavailable')
            throw error;
        blockers.push(error instanceof Error ? error.message : `Target '${targetPath}' could not be inspected.`);
        return null;
    }
}
function inspectSupersessionTarget(repositoryRoot, targetPath) {
    const status = (0, node_child_process_1.spawnSync)('git', ['status', '--porcelain=v1', '--untracked-files=all', '--', targetPath], {
        cwd: repositoryRoot,
        encoding: 'utf8',
        maxBuffer: 2_000_000,
    });
    if (status.status !== 0 || status.error !== undefined) {
        throw new TaskAttemptSupersedeError('git-inspection-unavailable', `Git inspection was unavailable for declared target '${targetPath}'; no target-change conclusion was made.${status.error === undefined ? ` Git exited with status ${status.status}.` : ` ${status.error.message}`}`);
    }
    const absolutePath = node_path_1.default.join(repositoryRoot, targetPath);
    try {
        const stat = node_fs_1.default.lstatSync(absolutePath);
        if (stat.isSymbolicLink()) {
            throw new TaskAttemptSupersedeError('implementation-delta', `Declared target '${targetPath}' became a symbolic link after baseline capture.`);
        }
        if (stat.isFile()) {
            return {
                exists: true,
                kind: 'file',
                contentSha256: `sha256:${(0, node_crypto_1.createHash)('sha256').update(node_fs_1.default.readFileSync(absolutePath)).digest('hex')}`,
                dirty: status.stdout !== '',
            };
        }
        if (stat.isDirectory()) {
            return {
                exists: true,
                kind: 'directory',
                contentSha256: hashSupersessionDirectory(absolutePath),
                dirty: status.stdout !== '',
            };
        }
        throw new TaskAttemptSupersedeError('implementation-delta', `Declared target '${targetPath}' is no longer a regular file, directory, or absent path.`);
    }
    catch (error) {
        if (error.code !== 'ENOENT')
            throw error;
        return { exists: false, kind: 'absent', contentSha256: null, dirty: status.stdout !== '' };
    }
}
function sameSupersessionTarget(baseline, current) {
    return (baseline.exists === current.exists &&
        baseline.kind === current.kind &&
        baseline.contentSha256 === current.contentSha256 &&
        baseline.dirty === current.dirty);
}
function hashSupersessionDirectory(directoryPath) {
    const files = [];
    const pending = [directoryPath];
    let totalBytes = 0;
    while (pending.length > 0) {
        const directory = pending.pop();
        for (const name of node_fs_1.default.readdirSync(directory).sort((left, right) => right.localeCompare(left, 'en'))) {
            const entryPath = node_path_1.default.join(directory, name);
            const stat = node_fs_1.default.lstatSync(entryPath);
            if (stat.isSymbolicLink())
                continue;
            if (stat.isDirectory()) {
                pending.push(entryPath);
                continue;
            }
            if (!stat.isFile())
                continue;
            files.push(entryPath);
            totalBytes += stat.size;
            if (files.length > 5_000 || totalBytes > 128_000_000) {
                throw new TaskAttemptSupersedeError('implementation-delta', `Declared target directory '${directoryPath}' exceeds the bounded supersession hashing limit.`);
            }
        }
    }
    const hash = (0, node_crypto_1.createHash)('sha256');
    for (const file of files.sort((left, right) => left.localeCompare(right, 'en'))) {
        const relative = node_path_1.default.relative(directoryPath, file).split(node_path_1.default.sep).join('/');
        const bytes = node_fs_1.default.readFileSync(file);
        hash.update(`${relative.length}:${relative}:${bytes.byteLength}:`);
        hash.update(bytes);
        hash.update('\n');
    }
    return `sha256:${hash.digest('hex')}`;
}
function supersedeCurrentAttempt(summaryText, attemptId, evidence) {
    const newline = summaryText.includes('\r\n') ? '\r\n' : '\n';
    const hadFinalNewline = summaryText.endsWith('\n');
    const lines = summaryText.split(/\r?\n/);
    const closing = lines.findIndex((line, index) => index > 0 && line === '---');
    if (lines[0] !== '---' || closing < 0) {
        throw new TaskAttemptSupersedeError('artifact-unreadable', 'Implementation summary has malformed YAML front matter.');
    }
    const range = locateAttemptRanges(lines.slice(1, closing + 1)).find((candidate) => candidate.attemptId === attemptId);
    if (range === undefined) {
        throw new TaskAttemptSupersedeError('attempt-conflict', `Attempt '${attemptId}' is not materialized.`);
    }
    const start = range.start + 1;
    let end = range.end + 1;
    const updates = [
        ['closed_at', evidence.recordedAt, true],
        ['review_disposition', 'superseded', false],
        ['disposition_by', evidence.recordedBy, true],
        ['disposition_at', evidence.recordedAt, true],
        ['disposition_reason', evidence.reason, true],
        ['superseded_task_revision', evidence.taskRevision, true],
        ['superseded_summary_revision', evidence.summaryRevision, true],
    ];
    for (const [field, value, quoted] of updates) {
        const pattern = new RegExp(`^\\s{4}${field}:`);
        const index = lines.findIndex((line, lineIndex) => lineIndex > start && lineIndex < end && pattern.test(line));
        const rendered = `    ${field}: ${quoted ? quote(value) : value}`;
        if (index >= 0) {
            lines[index] = rendered;
        }
        else {
            lines.splice(end, 0, rendered);
            end += 1;
        }
    }
    const currentIndex = lines.findIndex((line, index) => index > 0 && index < closing && /^current_attempt_id:/.test(line));
    if (currentIndex < 0) {
        throw new TaskAttemptSupersedeError('pair-invalid', 'Implementation summary current_attempt_id is missing.');
    }
    lines[currentIndex] = 'current_attempt_id: null';
    const rendered = lines.join(newline);
    return hadFinalNewline && !rendered.endsWith(newline) ? `${rendered}${newline}` : rendered;
}
function frontMatterLines(markdown) {
    const lines = markdown.split(/\r?\n/);
    const closing = lines.findIndex((line, index) => index > 0 && line === '---');
    return lines[0] === '---' && closing > 0 ? lines.slice(1, closing) : [];
}
function locateAttemptRanges(lines) {
    const attemptsIndex = lines.findIndex((line) => /^attempts:\s*$/.test(line));
    if (attemptsIndex < 0)
        return [];
    const starts = [];
    for (let index = attemptsIndex + 1; index < lines.length; index += 1) {
        if (/^[^\s]/.test(lines[index]))
            break;
        if (/^\s{2}-\s+attempt_id:/.test(lines[index]))
            starts.push(index);
    }
    return starts.map((start, index) => {
        const end = starts[index + 1] ?? lines.findIndex((line, lineIndex) => lineIndex > start && /^[^\s]/.test(line));
        const boundedEnd = end < 0 ? lines.length : end;
        return {
            start,
            end: boundedEnd,
            attemptId: parseYamlScalar(attemptFieldRaw(lines.slice(start, boundedEnd), 'attempt_id') ?? ''),
        };
    });
}
function locateAttemptRange(lines, attemptId) {
    return locateAttemptRanges(lines).find((range) => range.attemptId === attemptId) ?? null;
}
function attemptFieldRaw(lines, field) {
    const pattern = new RegExp(`^\\s+(?:-\\s+)?${field}:\\s*(.*)$`);
    return lines.find((line) => pattern.test(line))?.match(pattern)?.[1] ?? null;
}
function isEmptyCollectionScalar(raw) {
    const normalized = parseYamlScalar(raw).trim();
    if (normalized === '[]')
        return true;
    try {
        const decoded = JSON.parse(normalized);
        return Array.isArray(decoded) && decoded.length === 0;
    }
    catch {
        return false;
    }
}
function isNullScalar(raw) {
    const normalized = raw.trim();
    return normalized === '' || normalized === 'null' || normalized === '~';
}
function quote(value) {
    return JSON.stringify(value);
}
function parseYamlScalar(value) {
    const trimmed = value.trim();
    if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
        try {
            const parsed = JSON.parse(trimmed);
            return typeof parsed === 'string' ? parsed : trimmed;
        }
        catch {
            return trimmed.slice(1, -1);
        }
    }
    if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
        return trimmed.slice(1, -1).replace(/''/g, "'");
    }
    return trimmed;
}
