"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.resumeFromCapsule = exports.RESUME_RUN_CONTEXT_COMPATIBILITY = exports.MAX_RESUME_CAPSULE_BYTES = exports.DEFAULT_RESUME_EXPIRY_MS = exports.DEFAULT_RESUME_SESSION = exports.RESUME_CAPSULE_SCHEMA_VERSION = void 0;
exports.createResumeCapsule = createResumeCapsule;
exports.inspectResumeCapsule = inspectResumeCapsule;
exports.clearResumeCapsule = clearResumeCapsule;
exports.cleanupResumeCapsule = cleanupResumeCapsule;
exports.resolveResumeCapsuleStatePath = resolveResumeCapsuleStatePath;
const node_crypto_1 = require("node:crypto");
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const output_safety_1 = require("../output/output-safety");
const errors_1 = require("../errors");
const paths_1 = require("../paths");
const path_resolution_1 = require("../runtime/path-resolution");
const trust_lanes_1 = require("../prompts/trust-lanes");
exports.RESUME_CAPSULE_SCHEMA_VERSION = 1;
exports.DEFAULT_RESUME_SESSION = 'default';
exports.DEFAULT_RESUME_EXPIRY_MS = 24 * 60 * 60 * 1000;
exports.MAX_RESUME_CAPSULE_BYTES = 64 * 1024;
exports.RESUME_RUN_CONTEXT_COMPATIBILITY = {
    capsuleSchemaVersion: exports.RESUME_CAPSULE_SCHEMA_VERSION,
    lifecycleRunContextRequired: false,
    storageRelationship: 'independent-evidence-surfaces',
};
const MAX_ITEMS = 128;
const SESSION_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
function hasUnsafeControlCharacters(value) {
    for (let index = 0; index < value.length; index += 1) {
        const codePoint = value.charCodeAt(index);
        if ((codePoint >= 0 && codePoint <= 8) ||
            codePoint === 11 ||
            codePoint === 12 ||
            (codePoint >= 14 && codePoint <= 31) ||
            codePoint === 127) {
            return true;
        }
    }
    return false;
}
function createResumeCapsule(options) {
    const session = normalizeSession(options.session);
    const now = options.now ?? new Date();
    const expiryMs = options.expiresInMs ?? exports.DEFAULT_RESUME_EXPIRY_MS;
    if (!Number.isSafeInteger(expiryMs) || expiryMs <= 0)
        throw (0, errors_1.createUsageError)('Resume expiry must be a positive integer number of milliseconds.');
    const artifact = readArtifact(options.repositoryRoot, options.artifactPath, true);
    const evidence = capturePaths(options.repositoryRoot, options.evidencePaths ?? []);
    const postconditions = capturePaths(options.repositoryRoot, options.postconditionPaths ?? []);
    const completedActionIds = normalizeStrings(options.completedActionIds ?? [], 'completed action ids');
    const unresolvedBlockers = normalizeStrings(options.unresolvedBlockers ?? [], 'unresolved blockers');
    validateAction(options.nextAction);
    const statePath = resolveResumeCapsuleStatePath(options.repositoryRoot, options.env);
    withLock(statePath, () => {
        const store = readStore(statePath, options.repositoryRoot);
        assertPrecondition(store, session, options.ifMatch);
        const revision = store.revision + 1;
        const record = (0, output_safety_1.sanitizeOutputValue)({
            schemaVersion: 1,
            session,
            artifactPath: artifact.path,
            artifactType: artifact.artifactType,
            artifactHash: artifact.hash,
            createdAt: now.toISOString(),
            expiresAt: new Date(now.getTime() + expiryMs).toISOString(),
            completedActionIds,
            evidence,
            postconditions,
            unresolvedBlockers,
            nextAction: options.nextAction,
            revision,
        }, { repositoryRoot: options.repositoryRoot, environment: options.env });
        if (Buffer.byteLength(JSON.stringify(record), 'utf8') > exports.MAX_RESUME_CAPSULE_BYTES) {
            throw (0, errors_1.createUsageError)(`Resume capsule exceeds the ${exports.MAX_RESUME_CAPSULE_BYTES}-byte limit.`);
        }
        store.revision = revision;
        store.sessions[session] = record;
        writeStore(statePath, store);
    });
    return inspectResumeCapsule({ ...options, session, now });
}
function inspectResumeCapsule(options) {
    const session = normalizeSession(options.session);
    const statePath = resolveResumeCapsuleStatePath(options.repositoryRoot, options.env);
    const store = readStore(statePath, options.repositoryRoot);
    const capsule = store.sessions[session] ?? null;
    const base = {
        session,
        capsule,
        workspaceKey: store.workspaceKey,
        storeRevision: store.revision,
        storageScope: 'user-local',
        preconditionToken: token(store, session),
        trustLanes: {
            capsuleClaims: (0, trust_lanes_1.getTrustLane)('generated-evidence'),
            selectedArtifact: (0, trust_lanes_1.getTrustLane)('selected-artifact'),
            capturedEvidence: (0, trust_lanes_1.getTrustLane)('repository-prose'),
            nextAction: (0, trust_lanes_1.getTrustLane)('command-metadata'),
        },
    };
    if (capsule === null)
        return { ...base, state: 'none', staleReason: null, conflict: null, completedActionIds: [] };
    const explicit = options.explicitArtifactPath === undefined
        ? null
        : (0, paths_1.parseRepositoryRelativePath)(options.explicitArtifactPath).normalizedPath;
    const selected = options.selectedArtifactPath == null
        ? null
        : (0, paths_1.parseRepositoryRelativePath)(options.selectedArtifactPath).normalizedPath;
    const conflicting = explicit ?? selected;
    if (conflicting !== null && conflicting !== capsule.artifactPath) {
        return {
            ...base,
            state: 'conflict',
            staleReason: null,
            completedActionIds: [],
            conflict: {
                code: 'RESUME_SELECTION_CONFLICT',
                explicitArtifactPath: conflicting,
                capsuleArtifactPath: capsule.artifactPath,
                noWrite: true,
            },
        };
    }
    const staleReason = freshness(options.repositoryRoot, capsule, options.now ?? new Date());
    if (staleReason !== null)
        return { ...base, state: 'stale', staleReason, conflict: null, completedActionIds: [] };
    return {
        ...base,
        state: 'fresh',
        staleReason: null,
        conflict: null,
        completedActionIds: [...capsule.completedActionIds],
    };
}
exports.resumeFromCapsule = inspectResumeCapsule;
function clearResumeCapsule(options) {
    return mutateRemoval(options, false);
}
function cleanupResumeCapsule(options) {
    return mutateRemoval(options, true);
}
function resolveResumeCapsuleStatePath(repositoryRoot, env = process.env) {
    return node_path_1.default.join((0, path_resolution_1.resolveDayShiftUserStateDirectory)({ env }).stateDirectory, 'resume-capsules', `${workspaceKey(repositoryRoot)}.json`);
}
function mutateRemoval(options, staleOnly) {
    const session = normalizeSession(options.session);
    const statePath = resolveResumeCapsuleStatePath(options.repositoryRoot, options.env);
    return withLock(statePath, () => {
        const store = readStore(statePath, options.repositoryRoot);
        const capsule = store.sessions[session];
        if (capsule === undefined)
            return {
                changed: false,
                reason: 'capsule-absent',
                session,
                storeRevision: store.revision,
                workspaceKey: store.workspaceKey,
                storageScope: 'user-local',
            };
        if (staleOnly && freshness(options.repositoryRoot, capsule, options.now ?? new Date()) === null) {
            return {
                changed: false,
                reason: 'capsule-fresh',
                session,
                storeRevision: store.revision,
                workspaceKey: store.workspaceKey,
                storageScope: 'user-local',
            };
        }
        assertPrecondition(store, session, options.ifMatch);
        delete store.sessions[session];
        store.revision += 1;
        writeStore(statePath, store);
        return {
            changed: true,
            reason: staleOnly ? 'stale-cleared' : 'capsule-cleared',
            session,
            storeRevision: store.revision,
            workspaceKey: store.workspaceKey,
            storageScope: 'user-local',
        };
    });
}
function freshness(root, capsule, now) {
    if (now.getTime() >= Date.parse(capsule.expiresAt))
        return 'expired';
    const artifact = readArtifact(root, capsule.artifactPath, false);
    if (!artifact.exists)
        return 'artifact-missing';
    if (artifact.artifactType !== capsule.artifactType)
        return 'artifact-identity-changed';
    if (artifact.hash !== capsule.artifactHash)
        return 'artifact-content-changed';
    for (const item of capsule.evidence) {
        const current = readArtifact(root, item.path, false);
        if (!current.exists)
            return 'evidence-missing';
        if (current.hash !== item.hash)
            return 'evidence-changed';
    }
    for (const item of capsule.postconditions) {
        const current = readArtifact(root, item.path, false);
        if (!current.exists)
            return 'postcondition-missing';
        if (current.hash !== item.hash)
            return 'postcondition-changed';
    }
    return null;
}
function capturePaths(root, values) {
    if (values.length > MAX_ITEMS)
        throw (0, errors_1.createUsageError)(`Resume evidence lists may contain at most ${MAX_ITEMS} paths.`);
    return [...new Set(values)].sort().map((value) => {
        const current = readArtifact(root, value, true);
        return { path: current.path, hash: current.hash };
    });
}
function readArtifact(root, value, required) {
    const parsed = (0, paths_1.parseRepositoryRelativePath)(value);
    const absolute = (0, paths_1.resolveRepositoryRelativePath)(root, parsed);
    if (!node_fs_1.default.existsSync(absolute) || !node_fs_1.default.statSync(absolute).isFile()) {
        if (required)
            throw (0, errors_1.createUsageError)(`Resume evidence '${parsed.normalizedPath}' does not exist.`);
        return { path: parsed.normalizedPath, exists: false, hash: '', artifactType: null };
    }
    const raw = node_fs_1.default.readFileSync(absolute, 'utf8');
    return {
        path: parsed.normalizedPath,
        exists: true,
        hash: `sha256:${(0, node_crypto_1.createHash)('sha256').update(raw).digest('hex')}`,
        artifactType: raw.match(/^artifact_type:\s*["']?([^"'\r\n]+)["']?\s*$/m)?.[1].trim() ?? null,
    };
}
function normalizeStrings(values, label) {
    if (values.length > MAX_ITEMS)
        throw (0, errors_1.createUsageError)(`Resume ${label} may contain at most ${MAX_ITEMS} entries.`);
    return [...new Set(values.map((value) => value.trim()))]
        .filter(Boolean)
        .sort()
        .map((value) => {
        if (value.length > 512 || hasUnsafeControlCharacters(value))
            throw (0, errors_1.createUsageError)(`Resume ${label} contain an invalid or oversized entry.`);
        return value;
    });
}
function validateAction(action) {
    if (typeof action !== 'object' ||
        action === null ||
        typeof action.id !== 'string' ||
        typeof action.callable !== 'boolean') {
        throw (0, errors_1.createUsageError)('Resume next action must use the executable workflow-action contract.');
    }
    if (action.callable && (!Array.isArray(action.argv) || action.argv.length === 0))
        throw (0, errors_1.createUsageError)('Callable resume actions require exact argv.');
    if (!action.callable && action.argv !== null)
        throw (0, errors_1.createUsageError)('Non-callable resume guidance must not carry argv.');
}
function normalizeSession(value) {
    const session = value?.trim() || exports.DEFAULT_RESUME_SESSION;
    if (!SESSION_PATTERN.test(session))
        throw (0, errors_1.createUsageError)('Resume session must start with a lowercase letter or digit and contain at most 64 lowercase letters, digits, dots, underscores, or hyphens.');
    return session;
}
function readStore(statePath, root) {
    const key = workspaceKey(root);
    if (!node_fs_1.default.existsSync(statePath))
        return { schemaVersion: 1, workspaceKey: key, revision: 0, sessions: {} };
    let value;
    try {
        value = JSON.parse(node_fs_1.default.readFileSync(statePath, 'utf8'));
    }
    catch {
        throw (0, errors_1.createInvalidConfigError)('Resume capsule state is malformed JSON.');
    }
    if (!isRecord(value) ||
        value.schemaVersion !== 1 ||
        value.workspaceKey !== key ||
        !Number.isInteger(value.revision) ||
        typeof value.sessions !== 'object' ||
        value.sessions === null) {
        throw (0, errors_1.createInvalidConfigError)('Resume capsule state has an unsupported or mismatched shape.');
    }
    return value;
}
function isRecord(value) {
    return typeof value === 'object' && value !== null;
}
function writeStore(statePath, store) {
    node_fs_1.default.mkdirSync(node_path_1.default.dirname(statePath), { recursive: true });
    const temporary = `${statePath}.${process.pid}.tmp`;
    node_fs_1.default.writeFileSync(temporary, `${JSON.stringify(store, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
    node_fs_1.default.renameSync(temporary, statePath);
}
function token(store, session) {
    const capsule = store.sessions[session] ?? null;
    const payload = JSON.stringify({
        v: 1,
        w: store.workspaceKey,
        s: session,
        sr: store.revision,
        rr: capsule?.revision ?? null,
        h: capsule?.artifactHash ?? null,
    });
    const encoded = Buffer.from(payload).toString('base64url');
    return `rc1.${encoded}.${(0, node_crypto_1.createHash)('sha256').update(payload).digest('base64url').slice(0, 22)}`;
}
function assertPrecondition(store, session, ifMatch) {
    if (store.sessions[session] === undefined && ifMatch === undefined)
        return;
    if (ifMatch !== token(store, session)) {
        throw new errors_1.RuntimeError({
            code: 'RESUME_PRECONDITION_FAILED',
            exitCode: errors_1.RuntimeExitCode.CONCURRENCY_CONFLICT,
            message: 'Resume capsule precondition does not match current session state.',
            details: [
                `Session: ${session}`,
                `Current store revision: ${store.revision}`,
                'Inspect the capsule for a fresh token. No resume state was changed.',
            ],
        });
    }
}
function withLock(statePath, operation) {
    node_fs_1.default.mkdirSync(node_path_1.default.dirname(statePath), { recursive: true });
    const lockPath = `${statePath}.lock`;
    let descriptor;
    try {
        descriptor = node_fs_1.default.openSync(lockPath, 'wx', 0o600);
    }
    catch {
        throw new errors_1.RuntimeError({
            code: 'RESUME_STATE_LOCKED',
            exitCode: errors_1.RuntimeExitCode.CONCURRENCY_CONFLICT,
            message: 'Resume capsule state is busy with another mutation.',
            details: ['Retry after inspecting current capsule evidence. No resume state was changed.'],
        });
    }
    try {
        return operation();
    }
    finally {
        node_fs_1.default.closeSync(descriptor);
        try {
            node_fs_1.default.unlinkSync(lockPath);
        }
        catch (error) {
            if (!isMissingPathError(error)) {
                // Best effort lock cleanup; keep mutation outcome authoritative.
            }
        }
    }
}
function isMissingPathError(error) {
    return (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT');
}
function workspaceKey(root) {
    const canonical = node_fs_1.default.realpathSync(root);
    const marker = node_fs_1.default.existsSync(node_path_1.default.join(canonical, '.day-shift', 'config.toml')) ? '.day-shift/config.toml' : '.git';
    return `ws-${(0, node_crypto_1.createHash)('sha256').update(`${canonical}\0${marker}`).digest('hex').slice(0, 24)}`;
}
