/**
 * Task ID sequence management core module.
 * @task T4538
 * @epic T4454
 */
import { existsSync, readFileSync, renameSync } from 'node:fs';
import { join } from 'node:path';
import { ExitCode } from '@cleocode/contracts';
import { CleoError } from '../errors.js';
import { createDataAccessor } from '../store/data-accessor.js';
import { setMetaValue } from '../store/sqlite-data-accessor.js';
import { schemaMeta } from '../store/tasks-schema.js';
const SEQUENCE_META_KEY = 'task_id_sequence';
function getLegacySequenceJsonPath(cwd) {
    return join(cwd ?? process.cwd(), '.cleo', '.sequence.json');
}
function getLegacySequencePath(cwd) {
    return join(cwd ?? process.cwd(), '.cleo', '.sequence');
}
function isValidSequenceState(value) {
    if (!value || typeof value !== 'object')
        return false;
    const seq = value;
    return (typeof seq.counter === 'number' &&
        typeof seq.lastId === 'string' &&
        typeof seq.checksum === 'string');
}
function isSeedSequence(value) {
    return value.counter === 0 && value.lastId === 'T000' && value.checksum === 'seed';
}
function readLegacySequenceFile(path) {
    if (!existsSync(path))
        return null;
    try {
        const parsed = JSON.parse(readFileSync(path, 'utf-8'));
        return isValidSequenceState(parsed) ? parsed : null;
    }
    catch {
        return null;
    }
}
function renameLegacyFile(path) {
    if (!existsSync(path))
        return;
    const migratedPath = `${path}.migrated`;
    try {
        if (!existsSync(migratedPath)) {
            renameSync(path, migratedPath);
            return;
        }
        renameSync(path, `${migratedPath}.${Date.now()}`);
    }
    catch {
        // Non-fatal; sequence data has already been persisted in SQLite.
    }
}
async function readSequenceFromDb(cwd, accessor) {
    if (accessor) {
        const value = await accessor.getMetaValue(SEQUENCE_META_KEY);
        return isValidSequenceState(value) ? value : null;
    }
    const { getDb } = await import('../store/sqlite.js');
    const { eq } = await import('drizzle-orm');
    const db = await getDb(cwd);
    const rows = await db
        .select()
        .from(schemaMeta)
        .where(eq(schemaMeta.key, SEQUENCE_META_KEY))
        .all();
    const raw = rows[0]?.value;
    if (!raw)
        return null;
    try {
        const parsed = JSON.parse(raw);
        return isValidSequenceState(parsed) ? parsed : null;
    }
    catch {
        return null;
    }
}
async function writeSequenceToDb(state, cwd, accessor) {
    if (accessor) {
        await accessor.setMetaValue(SEQUENCE_META_KEY, state);
        return;
    }
    await setMetaValue(cwd, SEQUENCE_META_KEY, state);
}
async function maybeMigrateLegacySequence(dbState, cwd, accessor) {
    const legacyJsonPath = getLegacySequenceJsonPath(cwd);
    const legacyPath = getLegacySequencePath(cwd);
    const candidates = [
        readLegacySequenceFile(legacyJsonPath),
        readLegacySequenceFile(legacyPath),
    ].filter((value) => value !== null);
    if (candidates.length === 0) {
        return dbState;
    }
    const preferredLegacy = candidates.reduce((best, current) => current.counter > best.counter ? current : best);
    const shouldMigrate = !dbState || isSeedSequence(dbState) || preferredLegacy.counter > dbState.counter;
    if (shouldMigrate) {
        await writeSequenceToDb(preferredLegacy, cwd, accessor);
    }
    // Legacy files are obsolete once validated. Rename to prevent drift.
    renameLegacyFile(legacyJsonPath);
    renameLegacyFile(legacyPath);
    return shouldMigrate ? preferredLegacy : dbState;
}
async function readSequence(cwd, accessor) {
    const fromDb = await readSequenceFromDb(cwd, accessor);
    return maybeMigrateLegacySequence(fromDb, cwd, accessor);
}
function getMaxIdFromTasks(tasks) {
    let max = 0;
    for (const t of tasks) {
        const match = t.id.match(/^T(\d+)$/);
        if (match) {
            const num = parseInt(match[1], 10);
            if (num > max)
                max = num;
        }
    }
    return max;
}
/** Show current sequence state. */
export async function showSequence(cwd) {
    const seq = await readSequence(cwd);
    if (!seq) {
        throw new CleoError(ExitCode.NOT_FOUND, 'Sequence state not found in SQLite schema_meta');
    }
    return {
        counter: seq.counter,
        lastId: seq.lastId,
        checksum: seq.checksum,
        nextId: `T${String(seq.counter + 1).padStart(3, '0')}`,
    };
}
async function loadAllTasks(cwd, accessor) {
    let localAccessor = null;
    const activeAccessor = accessor ?? (await createDataAccessor(undefined, cwd));
    if (!accessor) {
        localAccessor = activeAccessor;
    }
    try {
        const { tasks: activeTasks } = await activeAccessor.queryTasks({});
        const archiveData = await activeAccessor.loadArchive();
        return [...(activeTasks ?? []), ...(archiveData?.archivedTasks ?? [])];
    }
    finally {
        if (localAccessor) {
            await localAccessor.close();
        }
    }
}
/** Check sequence integrity. */
export async function checkSequence(cwd, accessor) {
    const seq = await readSequence(cwd, accessor);
    if (!seq) {
        // File missing — return invalid state so callers trigger auto-repair instead of crashing.
        // repairSequence() handles null state and will initialize the counter from task data.
        return { valid: false, counter: 0, maxIdInData: 0, missing: true };
    }
    const allTasks = await loadAllTasks(cwd, accessor);
    const maxId = getMaxIdFromTasks(allTasks);
    const valid = seq.counter >= maxId;
    return {
        counter: seq.counter,
        maxIdInData: maxId,
        valid,
        ...(valid
            ? {}
            : {
                issue: `Counter (${seq.counter}) is behind max ID (T${maxId})`,
                fix: 'Run cleo sequence repair',
            }),
    };
}
/** Repair sequence if behind. */
export async function repairSequence(cwd, accessor) {
    const seq = await readSequence(cwd, accessor);
    const allTasks = await loadAllTasks(cwd, accessor);
    const maxId = getMaxIdFromTasks(allTasks);
    const oldCounter = seq?.counter ?? 0;
    if (!seq) {
        const initialized = {
            counter: maxId,
            lastId: `T${maxId}`,
            checksum: `seed-${Date.now()}`,
        };
        await writeSequenceToDb(initialized, cwd, accessor);
        return {
            repaired: true,
            counter: initialized.counter,
            oldCounter,
            newCounter: initialized.counter,
            message: `Sequence initialized at ${initialized.counter}`,
        };
    }
    if (oldCounter >= maxId) {
        return {
            repaired: false,
            message: 'Sequence already valid',
            counter: oldCounter,
            oldCounter,
            newCounter: oldCounter,
        };
    }
    const newCounter = maxId;
    const newSeq = {
        counter: newCounter,
        lastId: `T${newCounter}`,
        checksum: `repair-${Date.now()}`,
    };
    await writeSequenceToDb(newSeq, cwd, accessor);
    return {
        repaired: true,
        counter: newCounter,
        oldCounter,
        newCounter,
        message: `Sequence repaired: ${oldCounter} -> ${newCounter}`,
    };
}
/** Maximum retries for allocateNextTaskId when collision detected. */
const MAX_ALLOC_RETRIES = 3;
/**
 * Atomically allocate the next task ID via SQLite.
 *
 * Uses BEGIN IMMEDIATE to guarantee no two concurrent callers
 * receive the same ID, even across processes (WAL mode).
 *
 * Falls back to repair+retry if the sequence counter is behind
 * the actual max task ID (e.g., stale counter from installations
 * that never incremented it).
 *
 * @task T5184
 */
export async function allocateNextTaskId(cwd, retryCount = 0) {
    // Ensure DB is initialized (triggers migrations, seeds sequence counter)
    const { getDb, getNativeDb } = await import('../store/sqlite.js');
    await getDb(cwd);
    const nativeDb = getNativeDb();
    if (!nativeDb) {
        throw new CleoError(ExitCode.FILE_ERROR, 'Native database not available for atomic ID allocation');
    }
    // Atomic transaction: increment counter and read new value
    nativeDb.prepare('BEGIN IMMEDIATE').run();
    try {
        // Increment counter atomically
        nativeDb
            .prepare(`
      UPDATE schema_meta
      SET value = json_set(value,
        '$.counter', json_extract(value, '$.counter') + 1,
        '$.lastId', 'T' || printf('%03d', json_extract(value, '$.counter') + 1),
        '$.checksum', 'alloc-' || strftime('%s','now')
      )
      WHERE key = 'task_id_sequence'
    `)
            .run();
        // Read new counter value
        const row = nativeDb
            .prepare(`
      SELECT json_extract(value, '$.counter') AS counter
      FROM schema_meta WHERE key = 'task_id_sequence'
    `)
            .get();
        if (!row) {
            throw new CleoError(ExitCode.FILE_ERROR, 'Sequence counter not found after increment');
        }
        const newId = `T${String(row.counter).padStart(3, '0')}`;
        // Collision check: verify no existing task with this ID
        const existing = nativeDb.prepare('SELECT id FROM tasks WHERE id = ?').get(newId);
        if (existing) {
            // Counter was behind actual data — rollback, repair, and retry
            nativeDb.prepare('ROLLBACK').run();
            if (retryCount >= MAX_ALLOC_RETRIES) {
                throw new CleoError(ExitCode.ID_COLLISION, `Failed to allocate unique task ID after ${MAX_ALLOC_RETRIES} retries (last attempted: ${newId})`);
            }
            await repairSequence(cwd);
            return allocateNextTaskId(cwd, retryCount + 1);
        }
        nativeDb.prepare('COMMIT').run();
        return newId;
    }
    catch (err) {
        // Ensure transaction is rolled back on any error
        try {
            nativeDb.prepare('ROLLBACK').run();
        }
        catch {
            /* ignore rollback errors */
        }
        throw err;
    }
}
//# sourceMappingURL=index.js.map