"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WORK_SELECTION_SCHEMA_VERSION = exports.DEFAULT_WORK_SELECTION_SESSION = void 0;
exports.selectWorkArtifact = selectWorkArtifact;
exports.inspectWorkSelection = inspectWorkSelection;
exports.clearWorkSelection = clearWorkSelection;
exports.cleanupStaleWorkSelection = cleanupStaleWorkSelection;
exports.resolveWorkSelection = resolveWorkSelection;
exports.resolveWorkSelectionStatePath = resolveWorkSelectionStatePath;
const node_crypto_1 = require("node:crypto");
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const errors_1 = require("../errors");
const paths_1 = require("../paths");
const path_resolution_1 = require("../runtime/path-resolution");
const work_discovery_1 = require("../planning/work-discovery");
exports.DEFAULT_WORK_SELECTION_SESSION = 'default';
exports.WORK_SELECTION_SCHEMA_VERSION = 1;
const MAX_INTENT_LENGTH = 256;
const SESSION_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
function hasControlCharacters(value) {
    for (let index = 0; index < value.length; index += 1) {
        const codePoint = value.charCodeAt(index);
        if ((codePoint >= 0 && codePoint <= 31) || codePoint === 127) {
            return true;
        }
    }
    return false;
}
const PLANNING_ARTIFACT_TYPES = new Set([
    'work_overview',
    'slice_overview',
    'phase_overview',
    'milestone_overview',
    'task_definition',
    'implementation_summary',
    'reconciliation',
]);
function selectWorkArtifact(options) {
    const session = normalizeSession(options.session);
    const intent = normalizeIntent(options.intent);
    const artifact = readPlanningArtifactIdentity(options.repositoryRoot, options.artifactPath);
    const statePath = resolveWorkSelectionStatePath(options.repositoryRoot, options.env);
    withSelectionLock(statePath, () => {
        const store = readWorkSelectionStore(statePath, options.repositoryRoot);
        assertMutationPrecondition(store, session, options.ifMatch, {
            command: 'work select',
            argv: [
                'day-shift',
                'work',
                'select',
                '--artifact',
                artifact.artifactPath,
                '--session',
                session,
                ...(intent === null ? [] : ['--intent', intent]),
            ],
            selectedArtifactPath: artifact.artifactPath,
        });
        const currentArtifact = readPlanningArtifactIdentity(options.repositoryRoot, artifact.artifactPath);
        const nextRevision = store.revision + 1;
        store.revision = nextRevision;
        store.sessions[session] = {
            artifactPath: currentArtifact.artifactPath,
            artifactType: currentArtifact.artifactType,
            selectedAt: (options.now ?? new Date()).toISOString(),
            source: 'explicit-selection',
            intent,
            contentHash: currentArtifact.contentHash,
            revision: nextRevision,
        };
        writeWorkSelectionStore(statePath, store);
    });
    return inspectWorkSelection({ ...options, session });
}
function inspectWorkSelection(options) {
    const session = normalizeSession(options.session);
    const statePath = resolveWorkSelectionStatePath(options.repositoryRoot, options.env);
    const store = readWorkSelectionStore(statePath, options.repositoryRoot);
    const selection = store.sessions[session] ?? null;
    const base = {
        session,
        selection,
        workspaceKey: store.workspaceKey,
        storeRevision: store.revision,
        storageScope: 'user-local',
        preconditionToken: createPreconditionToken(store, session),
        workEvidence: null,
    };
    if (selection === null) {
        return { ...base, state: 'none', staleReason: null };
    }
    const absolutePath = (0, paths_1.resolveRepositoryRelativePath)(options.repositoryRoot, (0, paths_1.parseRepositoryRelativePath)(selection.artifactPath));
    if (!node_fs_1.default.existsSync(absolutePath)) {
        return { ...base, state: 'stale', staleReason: 'artifact-missing' };
    }
    const rawText = node_fs_1.default.readFileSync(absolutePath, 'utf8');
    const artifactType = parseArtifactType(rawText);
    if (artifactType !== selection.artifactType) {
        return { ...base, state: 'stale', staleReason: 'artifact-identity-changed' };
    }
    if (hashContent(rawText) !== selection.contentHash) {
        return { ...base, state: 'stale', staleReason: 'artifact-content-changed' };
    }
    return {
        ...base,
        state: 'fresh',
        staleReason: null,
        workEvidence: (0, work_discovery_1.projectCanonicalWorkCandidate)({
            repositoryRoot: options.repositoryRoot,
            artifactPath: selection.artifactPath,
        }),
    };
}
function clearWorkSelection(options) {
    const session = normalizeSession(options.session);
    const statePath = resolveWorkSelectionStatePath(options.repositoryRoot, options.env);
    return withSelectionLock(statePath, () => {
        const store = readWorkSelectionStore(statePath, options.repositoryRoot);
        if (store.sessions[session] === undefined) {
            if (options.ifMatch !== undefined)
                assertMutationPrecondition(store, session, options.ifMatch, {
                    command: 'work clear',
                    argv: ['day-shift', 'work', 'clear', '--session', session],
                });
            return {
                changed: false,
                session,
                storeRevision: store.revision,
                workspaceKey: store.workspaceKey,
                storageScope: 'user-local',
            };
        }
        assertMutationPrecondition(store, session, options.ifMatch, {
            command: 'work clear',
            argv: ['day-shift', 'work', 'clear', '--session', session],
        });
        delete store.sessions[session];
        store.revision += 1;
        writeWorkSelectionStore(statePath, store);
        return {
            changed: true,
            session,
            storeRevision: store.revision,
            workspaceKey: store.workspaceKey,
            storageScope: 'user-local',
        };
    });
}
function cleanupStaleWorkSelection(options) {
    const session = normalizeSession(options.session);
    const statePath = resolveWorkSelectionStatePath(options.repositoryRoot, options.env);
    return withSelectionLock(statePath, () => {
        const store = readWorkSelectionStore(statePath, options.repositoryRoot);
        const selection = store.sessions[session];
        if (selection === undefined) {
            if (options.ifMatch !== undefined)
                assertMutationPrecondition(store, session, options.ifMatch, {
                    command: 'work cleanup',
                    argv: ['day-shift', 'work', 'cleanup', '--session', session],
                });
            return {
                changed: false,
                reason: 'selection-absent',
                session,
                storeRevision: store.revision,
                workspaceKey: store.workspaceKey,
                storageScope: 'user-local',
            };
        }
        const freshness = inspectRecordFreshness(options.repositoryRoot, selection);
        if (freshness.state === 'fresh') {
            return {
                changed: false,
                reason: 'selection-fresh',
                session,
                storeRevision: store.revision,
                workspaceKey: store.workspaceKey,
                storageScope: 'user-local',
            };
        }
        assertMutationPrecondition(store, session, options.ifMatch, {
            command: 'work cleanup',
            argv: ['day-shift', 'work', 'cleanup', '--session', session],
        });
        delete store.sessions[session];
        store.revision += 1;
        writeWorkSelectionStore(statePath, store);
        return {
            changed: true,
            reason: 'stale-cleared',
            session,
            storeRevision: store.revision,
            workspaceKey: store.workspaceKey,
            storageScope: 'user-local',
        };
    });
}
function resolveWorkSelection(options) {
    const session = normalizeSession(options.session);
    const explicit = options.explicitArtifactPath === undefined
        ? null
        : readPlanningArtifactIdentity(options.repositoryRoot, options.explicitArtifactPath, options.allowSpecificationArtifact === true);
    if (explicit !== null) {
        return {
            status: 'resolved',
            source: 'explicit',
            session,
            artifactPath: explicit.artifactPath,
            artifactType: explicit.artifactType,
            freshness: 'fresh',
            staleReason: null,
            preconditionToken: null,
            workEvidence: explicit.artifactType === 'specification'
                ? null
                : (0, work_discovery_1.projectCanonicalWorkCandidate)({
                    repositoryRoot: options.repositoryRoot,
                    artifactPath: explicit.artifactPath,
                }),
            conflict: null,
        };
    }
    const inspection = inspectWorkSelection({ ...options, session });
    if (inspection.selection === null) {
        return {
            status: 'none',
            source: 'none',
            session,
            artifactPath: null,
            artifactType: null,
            freshness: 'none',
            staleReason: null,
            preconditionToken: inspection.preconditionToken,
            workEvidence: null,
            conflict: null,
        };
    }
    return {
        status: inspection.state === 'stale' ? 'stale' : 'resolved',
        source: 'stored',
        session,
        artifactPath: inspection.selection.artifactPath,
        artifactType: inspection.selection.artifactType,
        freshness: inspection.state,
        staleReason: inspection.staleReason,
        preconditionToken: inspection.preconditionToken,
        workEvidence: inspection.state === 'fresh' ? inspection.workEvidence : null,
        conflict: null,
    };
}
function resolveWorkSelectionStatePath(repositoryRoot, env = process.env) {
    const stateRoot = (0, path_resolution_1.resolveDayShiftUserStateDirectory)({ env }).stateDirectory;
    return node_path_1.default.join(stateRoot, 'work-selection', `${workspaceKey(repositoryRoot)}.json`);
}
function readPlanningArtifactIdentity(repositoryRoot, artifactPath, allowSpecificationArtifact = false) {
    const parsedPath = (0, paths_1.parseRepositoryRelativePath)(artifactPath);
    const absolutePath = (0, paths_1.resolveRepositoryRelativePath)(repositoryRoot, parsedPath);
    if (!node_fs_1.default.existsSync(absolutePath) || !node_fs_1.default.statSync(absolutePath).isFile()) {
        throw (0, errors_1.createUsageError)(`Work selection artifact '${parsedPath.normalizedPath}' does not exist.`, [
            'Select an existing repository-relative planning artifact.',
        ]);
    }
    const rawText = node_fs_1.default.readFileSync(absolutePath, 'utf8');
    const artifactType = parseArtifactType(rawText);
    if (artifactType === null ||
        (!PLANNING_ARTIFACT_TYPES.has(artifactType) && !(allowSpecificationArtifact && artifactType === 'specification'))) {
        throw (0, errors_1.createUsageError)(allowSpecificationArtifact
            ? `Artifact '${parsedPath.normalizedPath}' is not a supported planning or specification artifact.`
            : `Work selection artifact '${parsedPath.normalizedPath}' is not a supported planning artifact.`);
    }
    return { artifactPath: parsedPath.normalizedPath, artifactType, contentHash: hashContent(rawText) };
}
function parseArtifactType(rawText) {
    const match = rawText.match(/^artifact_type:\s*["']?([^"'\r\n]+)["']?\s*$/m);
    return match?.[1].trim() ?? null;
}
function normalizeSession(session) {
    const normalized = session?.trim() || exports.DEFAULT_WORK_SELECTION_SESSION;
    if (!SESSION_PATTERN.test(normalized)) {
        throw (0, errors_1.createUsageError)('Work selection session must start with a lowercase letter or digit and contain at most 64 lowercase letters, digits, dots, underscores, or hyphens.');
    }
    return normalized;
}
function normalizeIntent(intent) {
    if (intent === undefined || intent.trim() === '')
        return null;
    const normalized = intent.trim();
    if (normalized.length > MAX_INTENT_LENGTH || hasControlCharacters(normalized)) {
        throw (0, errors_1.createUsageError)(`Work selection intent must be at most ${MAX_INTENT_LENGTH} printable characters.`);
    }
    return normalized;
}
function readWorkSelectionStore(statePath, repositoryRoot) {
    const key = workspaceKey(repositoryRoot);
    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)('Work selection state is malformed JSON.', [
            'Clear or restore the user-local work selection state before retrying.',
        ]);
    }
    if (!isWorkSelectionStore(value) || value.workspaceKey !== key) {
        throw (0, errors_1.createInvalidConfigError)('Work selection state has an unsupported or mismatched shape.', [
            'Clear or restore the user-local work selection state before retrying.',
        ]);
    }
    return value;
}
function isWorkSelectionStore(value) {
    if (typeof value !== 'object' || value === null)
        return false;
    const candidate = value;
    return (candidate.schemaVersion === 1 &&
        typeof candidate.workspaceKey === 'string' &&
        Number.isInteger(candidate.revision) &&
        candidate.revision >= 0 &&
        typeof candidate.sessions === 'object' &&
        candidate.sessions !== null);
}
function writeWorkSelectionStore(statePath, store) {
    node_fs_1.default.mkdirSync(node_path_1.default.dirname(statePath), { recursive: true });
    const temporaryPath = `${statePath}.${process.pid}.tmp`;
    node_fs_1.default.writeFileSync(temporaryPath, `${JSON.stringify(store, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
    node_fs_1.default.renameSync(temporaryPath, statePath);
}
function createPreconditionToken(store, session) {
    const selection = store.sessions[session] ?? null;
    const payload = JSON.stringify({
        v: 1,
        w: store.workspaceKey,
        s: session,
        sr: store.revision,
        rr: selection?.revision ?? null,
        a: selection === null ? null : (0, node_crypto_1.createHash)('sha256').update(selection.artifactPath).digest('hex').slice(0, 16),
        c: selection?.contentHash ?? null,
    });
    const encoded = Buffer.from(payload, 'utf8').toString('base64url');
    const digest = (0, node_crypto_1.createHash)('sha256').update(payload).digest('base64url').slice(0, 22);
    return `ws1.${encoded}.${digest}`;
}
function assertMutationPrecondition(store, session, ifMatch, retry) {
    const currentToken = createPreconditionToken(store, session);
    const selectionExists = store.sessions[session] !== undefined;
    if (!selectionExists && ifMatch === undefined)
        return;
    if (ifMatch === undefined || !isWellFormedPreconditionToken(ifMatch) || ifMatch !== currentToken) {
        throw new errors_1.RuntimeError({
            code: 'WORK_SELECTION_PRECONDITION_FAILED',
            exitCode: errors_1.RuntimeExitCode.USAGE,
            message: 'Work selection precondition does not match current session state.',
            details: [
                `Session: ${session}`,
                `Current store revision: ${store.revision}`,
                `Refresh argv: ${JSON.stringify(['day-shift', 'work', 'current', '--session', session, '--format', 'json'])}`,
                'No selection state was changed.',
            ],
            recoveryActions: [
                (0, errors_1.createDescriptorBackedRecoveryAction)({
                    command: 'work current',
                    argv: ['day-shift', 'work', 'current', '--session', session, '--format', 'json'],
                    explanation: 'Refresh the current session evidence and precondition token without changing selection state.',
                }),
                (0, errors_1.createDescriptorBackedRecoveryAction)({
                    command: retry.command,
                    argv: [...retry.argv, '--if-match', currentToken, '--format', 'json'],
                    explanation: 'Retry the intended mutation against the current session precondition token.',
                    selectedArtifactPath: retry.selectedArtifactPath,
                }),
            ],
        });
    }
}
function isWellFormedPreconditionToken(value) {
    const match = value.match(/^ws1\.([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]{22})$/);
    if (match === null)
        return false;
    try {
        const payload = Buffer.from(match[1], 'base64url').toString('utf8');
        const expected = (0, node_crypto_1.createHash)('sha256').update(payload).digest('base64url').slice(0, 22);
        const parsed = JSON.parse(payload);
        return expected === match[2] && parsed?.v === 1;
    }
    catch {
        return false;
    }
}
function withSelectionLock(statePath, operation) {
    node_fs_1.default.mkdirSync(node_path_1.default.dirname(statePath), { recursive: true });
    const lockPath = `${statePath}.lock`;
    const deadline = Date.now() + 1000;
    let descriptor = null;
    while (descriptor === null) {
        try {
            descriptor = node_fs_1.default.openSync(lockPath, 'wx', 0o600);
            node_fs_1.default.writeFileSync(descriptor, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }));
        }
        catch (error) {
            if (!isAlreadyExistsError(error))
                throw error;
            if (recoverStaleLock(lockPath))
                continue;
            if (Date.now() >= deadline) {
                throw new errors_1.RuntimeError({
                    code: 'WORK_SELECTION_LOCKED',
                    exitCode: errors_1.RuntimeExitCode.USAGE,
                    message: 'Work selection state is busy with another mutation.',
                    details: ['Retry after refreshing current selection evidence. No selection state was changed.'],
                });
            }
            Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
        }
    }
    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 recoverStaleLock(lockPath) {
    try {
        const stats = node_fs_1.default.statSync(lockPath);
        if (Date.now() - stats.mtimeMs <= 120_000)
            return false;
        node_fs_1.default.unlinkSync(lockPath);
        return true;
    }
    catch (error) {
        if (isMissingPathError(error))
            return true;
        return false;
    }
}
function inspectRecordFreshness(repositoryRoot, selection) {
    const absolutePath = (0, paths_1.resolveRepositoryRelativePath)(repositoryRoot, (0, paths_1.parseRepositoryRelativePath)(selection.artifactPath));
    if (!node_fs_1.default.existsSync(absolutePath))
        return { state: 'stale', staleReason: 'artifact-missing' };
    const rawText = node_fs_1.default.readFileSync(absolutePath, 'utf8');
    if (parseArtifactType(rawText) !== selection.artifactType)
        return { state: 'stale', staleReason: 'artifact-identity-changed' };
    if (hashContent(rawText) !== selection.contentHash)
        return { state: 'stale', staleReason: 'artifact-content-changed' };
    return { state: 'fresh', staleReason: null };
}
function isAlreadyExistsError(error) {
    return (typeof error === 'object' && error !== null && 'code' in error && error.code === 'EEXIST');
}
function isMissingPathError(error) {
    return (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT');
}
function workspaceKey(repositoryRoot) {
    const canonicalRoot = node_fs_1.default.realpathSync(repositoryRoot);
    const marker = node_fs_1.default.existsSync(node_path_1.default.join(canonicalRoot, '.day-shift', 'config.toml'))
        ? '.day-shift/config.toml'
        : '.git';
    return `ws-${(0, node_crypto_1.createHash)('sha256').update(`${canonicalRoot}\0${marker}`).digest('hex').slice(0, 24)}`;
}
function hashContent(rawText) {
    return `sha256:${(0, node_crypto_1.createHash)('sha256').update(rawText).digest('hex')}`;
}
