import fs from "node:fs";
import path from "node:path";
import { createRequire } from "node:module";
import { formatArchMismatchRemediation, throwNativeSqliteLoadError } from "../../../core/native-sqlite-diagnostics.js";
const requireBetterSqlite3 = createRequire(import.meta.url);
function loadBetterSqlite3Constructor() {
    try {
        return requireBetterSqlite3("better-sqlite3");
    }
    catch (error) {
        throwNativeSqliteLoadError(error);
    }
}
import { prepareKitSqliteDatabase, TASK_ENGINE_DEPENDENCIES_TABLE, TASK_ENGINE_MUTATION_LOG_TABLE, TASK_ENGINE_TASKS_TABLE, TASK_ENGINE_TRANSITION_LOG_TABLE, kitSqliteHasRelationalTaskDdl } from "../../../core/state/kit-sqlite/planning-sqlite-kernel.js";
import { TaskEngineError } from "../transitions.js";
import { normalizeTaskStoreDocumentFromUnknown } from "./task-store-migration.js";
import { stripAgentRoutingFromTask } from "../agent-task-routing-projection.js";
import { relationalCompatibilityTaskBlob, rowToTaskEntity, taskEntityToRow } from "./sqlite-task-row-mapping.js";
import { syncWorkspaceKitStatusFromYamlIfNeeded } from "./workspace-status-yaml-import.js";
import { featureRegistryActiveOnConnection, loadTaskFeatureLinkMap, replaceAllTaskFeatureLinks, replaceTaskFeatureLinksForTasks } from "./feature-registry-queries.js";
import { classifyNativeSqliteErrorMessage, formatNodeRuntimeIdentity } from "../../../core/native-sqlite-diagnostics.js";
function emptyTaskStoreDocument() {
    return {
        schemaVersion: 1,
        tasks: [],
        transitionLog: [],
        mutationLog: [],
        lastUpdated: new Date().toISOString()
    };
}
function emptyWishlistDocument() {
    return {
        schemaVersion: 1,
        items: [],
        lastUpdated: new Date().toISOString()
    };
}
function detectTableShape(db) {
    const rows = db.prepare("PRAGMA table_info(workspace_planning_state)").all();
    if (rows.some((r) => r.name === "wishlist_store_json")) {
        return "legacy-dual";
    }
    return "task-only";
}
function planningStateColumnSet(db) {
    const rows = db.prepare("PRAGMA table_info(workspace_planning_state)").all();
    return new Set(rows.map((r) => r.name));
}
function dependencyTableAvailable(db) {
    return tableAvailable(db, TASK_ENGINE_DEPENDENCIES_TABLE);
}
function tableAvailable(db, tableName) {
    const row = db
        .prepare("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?")
        .get(tableName);
    return Boolean(row?.ok);
}
function evidenceTablesAvailable(db) {
    return tableAvailable(db, TASK_ENGINE_TRANSITION_LOG_TABLE) && tableAvailable(db, TASK_ENGINE_MUTATION_LOG_TABLE);
}
function parseJsonArrayField(raw, label) {
    try {
        const parsed = JSON.parse(raw);
        if (!Array.isArray(parsed)) {
            throw new TaskEngineError("storage-read-error", `${label} must be a JSON array`);
        }
        return parsed;
    }
    catch (e) {
        if (e instanceof TaskEngineError) {
            throw e;
        }
        throw new TaskEngineError("storage-read-error", `Failed to parse ${label}: ${e.message}`);
    }
}
function parseJsonObjectField(raw) {
    if (raw === null) {
        return undefined;
    }
    const parsed = JSON.parse(raw);
    return parsed && typeof parsed === "object" && !Array.isArray(parsed)
        ? parsed
        : undefined;
}
function dependencyProjection(tasks) {
    const dependsOnByTask = new Map();
    const unblocksByTask = new Map();
    for (const task of tasks) {
        const deps = [...new Set(task.dependsOn ?? [])].sort();
        dependsOnByTask.set(task.id, deps);
        for (const dep of deps) {
            const list = unblocksByTask.get(dep) ?? [];
            list.push(task.id);
            unblocksByTask.set(dep, list);
        }
    }
    for (const [taskId, list] of unblocksByTask) {
        unblocksByTask.set(taskId, [...new Set(list)].sort());
    }
    return { dependsOnByTask, unblocksByTask };
}
function readRelationalFlagFromRow(row) {
    if (!row) {
        return false;
    }
    const v = row.relational_tasks;
    if (typeof v === "number") {
        return v === 1;
    }
    if (typeof v === "bigint") {
        return Number(v) === 1;
    }
    return false;
}
function readDbFileIdentity(dbPath) {
    try {
        const stat = fs.statSync(dbPath, { bigint: true });
        return {
            dev: stat.dev.toString(),
            ino: stat.ino.toString()
        };
    }
    catch (err) {
        if (err.code === "ENOENT") {
            return null;
        }
        throw err;
    }
}
function sameDbFileIdentity(a, b) {
    if (a === null || b === null) {
        return a === b;
    }
    return a.dev === b.dev && a.ino === b.ino;
}
/** Single-file SQLite backing for task JSON document; legacy rows may include wishlist_store_json until collapsed on store open. */
export class SqliteDualPlanningStore {
    db = null;
    _dbFileIdentity = null;
    _reloadOnNextRead = false;
    dbPath;
    _workspaceRoot;
    _taskDoc;
    _wishlistDoc;
    _tableShape = "task-only";
    /** When true, load/save tasks via task_engine_tasks + envelope log columns. */
    _relationalTasks = false;
    /** Monotonic optimistic-lock counter for unified planning SQLite row (tasks + wishlist + logs). */
    _planningGeneration = 0;
    constructor(workspacePath, databaseRelativePath) {
        this._workspaceRoot = path.resolve(workspacePath);
        this.dbPath = path.resolve(workspacePath, databaseRelativePath);
        this._taskDoc = emptyTaskStoreDocument();
        this._wishlistDoc = emptyWishlistDocument();
    }
    get taskDocument() {
        this.reloadIfCachedStateStale();
        return this._taskDoc;
    }
    get wishlistDocument() {
        this.reloadIfCachedStateStale();
        return this._wishlistDoc;
    }
    get tableShape() {
        return this._tableShape;
    }
    /** True when this DB uses relational task rows (not blob-only for task bodies). */
    get relationalTasksEnabled() {
        return this._relationalTasks;
    }
    /** Current planning generation (SQLite `workspace_planning_state.planning_generation`). */
    getPlanningGeneration() {
        this.reloadIfCachedStateStale();
        return this._planningGeneration;
    }
    getDisplayPath() {
        return this.dbPath;
    }
    /** Opened SQLite handle (creates file + migrations as needed). For read-only helpers (feature registry). */
    getDatabase() {
        return this.ensureDb();
    }
    /**
     * Close the SQLite handle if open. Use when another code path must reopen the same `dbPath`
     * in-process (avoids multi-handle races on the same file).
     */
    closeDatabase() {
        if (this.db) {
            try {
                this.db.close();
            }
            catch {
                /* best-effort */
            }
            this.db = null;
            this._dbFileIdentity = null;
        }
    }
    closeReplacedDatabaseHandle() {
        if (!this.db) {
            return;
        }
        const preservedPath = `${this.dbPath}.replaced-${process.pid}-${Date.now()}.tmp`;
        let preservedCurrent = false;
        try {
            if (fs.existsSync(this.dbPath)) {
                fs.renameSync(this.dbPath, preservedPath);
                preservedCurrent = true;
            }
        }
        catch {
            preservedCurrent = false;
        }
        try {
            this.db.close();
        }
        catch {
            /* best-effort */
        }
        finally {
            this.db = null;
            this._dbFileIdentity = null;
            try {
                fs.rmSync(this.dbPath, { force: true });
                fs.rmSync(`${this.dbPath}-wal`, { force: true });
                fs.rmSync(`${this.dbPath}-shm`, { force: true });
                if (preservedCurrent) {
                    fs.renameSync(preservedPath, this.dbPath);
                }
            }
            catch {
                /* best-effort */
            }
        }
    }
    currentDbFileIdentityChanged() {
        if (!this.db) {
            return false;
        }
        const current = readDbFileIdentity(this.dbPath);
        return !sameDbFileIdentity(this._dbFileIdentity, current);
    }
    readPlanningGenFromOpenDb(db) {
        const cols = planningStateColumnSet(db);
        if (!cols.has("planning_generation")) {
            return 0;
        }
        const row = db
            .prepare("SELECT planning_generation AS g FROM workspace_planning_state WHERE id = 1")
            .get();
        return row !== undefined ? Number(row.g) || 0 : 0;
    }
    reloadIfCachedStateStale() {
        if (!this.db) {
            return;
        }
        if (this._reloadOnNextRead) {
            this._reloadOnNextRead = false;
            this.closeReplacedDatabaseHandle();
            this.loadFromDisk();
            return;
        }
        if (this.currentDbFileIdentityChanged()) {
            this.closeReplacedDatabaseHandle();
            this.loadFromDisk();
            return;
        }
        if (this.readPlanningGenFromOpenDb(this.db) !== this._planningGeneration) {
            this.loadFromDisk();
        }
    }
    reloadBeforeReuseIfNeeded() {
        if (!this.db) {
            return;
        }
        if (this._reloadOnNextRead) {
            this._reloadOnNextRead = false;
            this.closeReplacedDatabaseHandle();
            this.loadFromDisk();
            return;
        }
        if (this.currentDbFileIdentityChanged()) {
            this.closeReplacedDatabaseHandle();
            this.loadFromDisk();
            return;
        }
        if (this.readPlanningGenFromOpenDb(this.db) !== this._planningGeneration) {
            this.loadFromDisk();
        }
    }
    openDatabase() {
        const dir = path.dirname(this.dbPath);
        fs.mkdirSync(dir, { recursive: true });
        try {
            const DatabaseCtor = loadBetterSqlite3Constructor();
            this.db = new DatabaseCtor(this.dbPath);
        }
        catch (e) {
            const msg = e instanceof Error ? e.message : String(e);
            const classification = classifyNativeSqliteErrorMessage(msg);
            const errCode = e instanceof Error ? e.code : undefined;
            if (classification.kind === "architecture-mismatch" || errCode === "runtime-host-arch-mismatch") {
                const remediation = formatArchMismatchRemediation(e);
                throw new TaskEngineError("native-binding-arch-mismatch", `${remediation.message} Remediation: ${remediation.remediationCommand} Runtime: ${formatNodeRuntimeIdentity()}`);
            }
            throw new TaskEngineError("storage-read-error", `Failed to open SQLite planning DB: ${msg}`);
        }
        prepareKitSqliteDatabase(this.db);
        this._dbFileIdentity = readDbFileIdentity(this.dbPath);
        this._tableShape = detectTableShape(this.db);
        return this.db;
    }
    ensureDb(options) {
        if (this.db) {
            if (this.currentDbFileIdentityChanged()) {
                this.closeReplacedDatabaseHandle();
                if (options?.reloadOnReplacement !== false) {
                    this.loadFromDisk();
                    if (this.db) {
                        return this.db;
                    }
                }
            }
            else {
                if (options?.reloadOnReplacement !== false) {
                    this.reloadBeforeReuseIfNeeded();
                    if (this.db) {
                        return this.db;
                    }
                }
                return this.db;
            }
        }
        return this.openDatabase();
    }
    refreshPlanningGenFromOpenDb(db) {
        this._planningGeneration = this.readPlanningGenFromOpenDb(db);
    }
    loadRelationalTasks(db) {
        const rows = db.prepare(`SELECT * FROM ${TASK_ENGINE_TASKS_TABLE} ORDER BY id ASC`).all();
        const linkMap = loadTaskFeatureLinkMap(db);
        const tasks = rows.map((r) => rowToTaskEntity(r, { taskFeatureLinkMap: linkMap }));
        if (dependencyTableAvailable(db)) {
            const depRows = db
                .prepare(`SELECT task_id AS taskId, depends_on_task_id AS dependsOnTaskId FROM ${TASK_ENGINE_DEPENDENCIES_TABLE} ORDER BY task_id ASC, depends_on_task_id ASC`)
                .all();
            const dependsOnByTask = new Map();
            const unblocksByTask = new Map();
            for (const row of depRows) {
                const deps = dependsOnByTask.get(row.taskId) ?? [];
                deps.push(row.dependsOnTaskId);
                dependsOnByTask.set(row.taskId, deps);
                const unblocks = unblocksByTask.get(row.dependsOnTaskId) ?? [];
                unblocks.push(row.taskId);
                unblocksByTask.set(row.dependsOnTaskId, unblocks);
            }
            for (const task of tasks) {
                const deps = [...new Set(dependsOnByTask.get(task.id) ?? [])].sort();
                const unblocks = [...new Set(unblocksByTask.get(task.id) ?? [])].sort();
                if (deps.length > 0) {
                    task.dependsOn = deps;
                }
                else {
                    delete task.dependsOn;
                }
                if (unblocks.length > 0) {
                    task.unblocks = unblocks;
                }
                else {
                    delete task.unblocks;
                }
            }
        }
        this._taskDoc.tasks = tasks;
    }
    parseLogs(transitionJson, mutationJson) {
        try {
            const tl = JSON.parse(transitionJson);
            if (!Array.isArray(tl)) {
                throw new TaskEngineError("storage-read-error", "transition_log_json must be a JSON array");
            }
            this._taskDoc.transitionLog = tl;
        }
        catch (e) {
            if (e instanceof TaskEngineError) {
                throw e;
            }
            throw new TaskEngineError("storage-read-error", `Failed to parse transition_log_json: ${e.message}`);
        }
        try {
            const ml = JSON.parse(mutationJson);
            if (!Array.isArray(ml)) {
                throw new TaskEngineError("storage-read-error", "mutation_log_json must be a JSON array");
            }
            this._taskDoc.mutationLog = ml;
        }
        catch (e) {
            if (e instanceof TaskEngineError) {
                throw e;
            }
            throw new TaskEngineError("storage-read-error", `Failed to parse mutation_log_json: ${e.message}`);
        }
    }
    loadEvidenceLogs(db) {
        if (!evidenceTablesAvailable(db)) {
            return false;
        }
        const transitionRows = db
            .prepare(`SELECT transition_id, task_id, from_state, to_state, action, timestamp, actor,
                client_mutation_id, payload_digest, guard_results_json, dependents_unblocked_json
         FROM ${TASK_ENGINE_TRANSITION_LOG_TABLE}
         ORDER BY timestamp ASC, transition_id ASC`)
            .all();
        const mutationRows = db
            .prepare(`SELECT mutation_id, mutation_type, task_id, timestamp, actor, details_json
         FROM ${TASK_ENGINE_MUTATION_LOG_TABLE}
         ORDER BY timestamp ASC, mutation_id ASC`)
            .all();
        if (transitionRows.length === 0 && mutationRows.length === 0) {
            return false;
        }
        this._taskDoc.transitionLog = transitionRows.map((row) => {
            const evidence = {
                transitionId: row.transition_id,
                taskId: row.task_id,
                fromState: row.from_state,
                toState: row.to_state,
                action: row.action,
                guardResults: parseJsonArrayField(row.guard_results_json, "guard_results_json"),
                dependentsUnblocked: parseJsonArrayField(row.dependents_unblocked_json, "dependents_unblocked_json"),
                timestamp: row.timestamp
            };
            if (row.actor !== null)
                evidence.actor = row.actor;
            if (row.client_mutation_id !== null)
                evidence.clientMutationId = row.client_mutation_id;
            if (row.payload_digest !== null)
                evidence.payloadDigest = row.payload_digest;
            return evidence;
        });
        this._taskDoc.mutationLog = mutationRows.map((row) => {
            const evidence = {
                mutationId: row.mutation_id,
                mutationType: row.mutation_type,
                taskId: row.task_id,
                timestamp: row.timestamp
            };
            if (row.actor !== null)
                evidence.actor = row.actor;
            const details = parseJsonObjectField(row.details_json);
            if (details !== undefined)
                evidence.details = details;
            return evidence;
        });
        return true;
    }
    /** Load documents from an existing database file; otherwise start empty (no file created). */
    loadFromDisk() {
        if (!fs.existsSync(this.dbPath)) {
            this._taskDoc = emptyTaskStoreDocument();
            this._wishlistDoc = emptyWishlistDocument();
            this._tableShape = "task-only";
            this._relationalTasks = false;
            this._planningGeneration = 0;
            return;
        }
        const db = this.ensureDb({ reloadOnReplacement: false });
        syncWorkspaceKitStatusFromYamlIfNeeded(this._workspaceRoot, db);
        try {
            this._tableShape = detectTableShape(db);
            const hasRel = kitSqliteHasRelationalTaskDdl(db);
            if (this._tableShape === "legacy-dual") {
                const pcols = planningStateColumnSet(db);
                const hasEnvelope = pcols.has("relational_tasks") && pcols.has("transition_log_json");
                const row = hasEnvelope
                    ? db
                        .prepare("SELECT task_store_json, wishlist_store_json, transition_log_json, mutation_log_json, relational_tasks FROM workspace_planning_state WHERE id = 1")
                        .get()
                    : db
                        .prepare("SELECT task_store_json, wishlist_store_json FROM workspace_planning_state WHERE id = 1")
                        .get();
                if (!row) {
                    this._taskDoc = emptyTaskStoreDocument();
                    this._wishlistDoc = emptyWishlistDocument();
                    this._relationalTasks = false;
                    return;
                }
                const useRel = hasRel && hasEnvelope && readRelationalFlagFromRow(row);
                this._relationalTasks = useRel;
                try {
                    if (useRel) {
                        this._taskDoc = emptyTaskStoreDocument();
                        this.loadRelationalTasks(db);
                        const tr = row.transition_log_json;
                        const mj = row.mutation_log_json;
                        if (typeof tr !== "string" || typeof mj !== "string") {
                            throw new TaskEngineError("storage-read-error", "envelope log columns missing");
                        }
                        if (!this.loadEvidenceLogs(db)) {
                            this.parseLogs(tr, mj);
                        }
                    }
                    else {
                        const taskParsed = normalizeTaskStoreDocumentFromUnknown(JSON.parse(row.task_store_json));
                        const wishParsed = JSON.parse(row.wishlist_store_json);
                        if (wishParsed.schemaVersion !== 1) {
                            throw new TaskEngineError("storage-read-error", `Unsupported wishlist schema in SQLite: ${wishParsed.schemaVersion}`);
                        }
                        if (!Array.isArray(wishParsed.items)) {
                            throw new TaskEngineError("storage-read-error", "Wishlist items missing in SQLite row");
                        }
                        this._taskDoc = taskParsed;
                        this._wishlistDoc = wishParsed;
                    }
                }
                catch (err) {
                    if (err instanceof TaskEngineError) {
                        throw err;
                    }
                    throw new TaskEngineError("storage-read-error", `Failed to parse SQLite planning row: ${err.message}`);
                }
                return;
            }
            const pcols = planningStateColumnSet(db);
            const hasEnvelope = hasRel && pcols.has("relational_tasks") && pcols.has("transition_log_json");
            const cols = hasEnvelope
                ? "task_store_json, transition_log_json, mutation_log_json, relational_tasks"
                : "task_store_json";
            const row = db.prepare(`SELECT ${cols} FROM workspace_planning_state WHERE id = 1`).get();
            if (!row) {
                this._taskDoc = emptyTaskStoreDocument();
                this._wishlistDoc = emptyWishlistDocument();
                this._relationalTasks = false;
                return;
            }
            try {
                const useRel = hasEnvelope && readRelationalFlagFromRow(row);
                this._relationalTasks = useRel;
                if (useRel) {
                    this._taskDoc = emptyTaskStoreDocument();
                    this.loadRelationalTasks(db);
                    if (!this.loadEvidenceLogs(db)) {
                        this.parseLogs(row.transition_log_json, row.mutation_log_json);
                    }
                }
                else {
                    this._taskDoc = normalizeTaskStoreDocumentFromUnknown(JSON.parse(row.task_store_json));
                    this._wishlistDoc = emptyWishlistDocument();
                }
            }
            catch (err) {
                if (err instanceof TaskEngineError) {
                    throw err;
                }
                throw new TaskEngineError("storage-read-error", `Failed to parse SQLite planning row: ${err.message}`);
            }
        }
        finally {
            this.refreshPlanningGenFromOpenDb(db);
            this._dbFileIdentity = readDbFileIdentity(this.dbPath);
        }
    }
    /** Replace in-memory documents (used by migrate). */
    seedFromDocuments(taskDoc, wishlistDoc) {
        this._taskDoc = taskDoc;
        this._wishlistDoc = wishlistDoc;
    }
    /** Switch to relational persistence and flush current in-memory task document + wishlist (transactional). */
    enableRelationalPersistenceAndPersist() {
        if (this._relationalTasks) {
            return;
        }
        this._relationalTasks = true;
        this.persistSync();
    }
    runPersistMutation(db, options, work) {
        const pcols = planningStateColumnSet(db);
        if (!pcols.has("planning_generation")) {
            throw new TaskEngineError("storage-write-error", "workspace_planning_state missing planning_generation column; upgrade workspace-kit and reopen the database");
        }
        let currentGen = 0;
        const gRow = db
            .prepare("SELECT planning_generation AS g FROM workspace_planning_state WHERE id = 1")
            .get();
        if (gRow !== undefined) {
            currentGen = Number(gRow.g) || 0;
        }
        if (options.expectedPlanningGeneration !== undefined &&
            options.expectedPlanningGeneration !== currentGen) {
            this._reloadOnNextRead = true;
            throw new TaskEngineError("planning-generation-mismatch", `expectedPlanningGeneration ${options.expectedPlanningGeneration} does not match current planning generation ${currentGen}`, {
                currentPlanningGeneration: currentGen,
                passedExpectedPlanningGeneration: options.expectedPlanningGeneration,
                retryAfterRead: true,
                readCommandSuggestion: {
                    command: "list-tasks",
                    args: {}
                }
            });
        }
        if (work) {
            work();
        }
        this._taskDoc.lastUpdated = new Date().toISOString();
        this._wishlistDoc.lastUpdated = new Date().toISOString();
        const nextGen = currentGen + 1;
        if (this._relationalTasks && kitSqliteHasRelationalTaskDdl(db)) {
            this.persistRelational(db, nextGen, options);
        }
        else {
            this.persistBlobOnly(db, nextGen);
        }
        this._planningGeneration = nextGen;
        this._dbFileIdentity = readDbFileIdentity(this.dbPath);
    }
    persistRelational(db, nextPlanningGeneration, options) {
        const dirtyTaskIds = [...new Set(options.dirtyTaskIds ?? [])].filter((id) => id.trim().length > 0);
        const canPersistIncrementally = options.persistScope === "incremental" &&
            dirtyTaskIds.length > 0 &&
            this._tableShape !== "legacy-dual" &&
            dependencyTableAvailable(db) &&
            evidenceTablesAvailable(db);
        if (canPersistIncrementally) {
            this.persistRelationalIncremental(db, nextPlanningGeneration, {
                ...options,
                dirtyTaskIds
            });
            return;
        }
        this.persistRelationalFull(db, nextPlanningGeneration);
    }
    taskInsertStatement(db) {
        const insertSql = `
      INSERT OR REPLACE INTO ${TASK_ENGINE_TASKS_TABLE} (
        id, status, type, title, created_at, updated_at, archived, archived_at,
        priority, phase, phase_key, ownership, approach,
        depends_on_json, unblocks_json, technical_scope_json, acceptance_criteria_json,
        summary, description, risk, queue_namespace, evidence_key, evidence_kind, metadata_json, features_json,
        routing_category, routing_confidence_tier, routing_blocked_reason_category, routing_tags_json
      ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
    `;
        return db.prepare(insertSql);
    }
    /** Incremental path only — UPDATE-in-place avoids DELETE that trips depends_on RESTRICT FKs. */
    taskUpsertStatement(db) {
        const upsertSql = `
      INSERT INTO ${TASK_ENGINE_TASKS_TABLE} (
        id, status, type, title, created_at, updated_at, archived, archived_at,
        priority, phase, phase_key, ownership, approach,
        depends_on_json, unblocks_json, technical_scope_json, acceptance_criteria_json,
        summary, description, risk, queue_namespace, evidence_key, evidence_kind, metadata_json, features_json,
        routing_category, routing_confidence_tier, routing_blocked_reason_category, routing_tags_json
      ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
      ON CONFLICT(id) DO UPDATE SET
        status = excluded.status,
        type = excluded.type,
        title = excluded.title,
        updated_at = excluded.updated_at,
        archived = excluded.archived,
        archived_at = excluded.archived_at,
        priority = excluded.priority,
        phase = excluded.phase,
        phase_key = excluded.phase_key,
        ownership = excluded.ownership,
        approach = excluded.approach,
        depends_on_json = excluded.depends_on_json,
        unblocks_json = excluded.unblocks_json,
        technical_scope_json = excluded.technical_scope_json,
        acceptance_criteria_json = excluded.acceptance_criteria_json,
        summary = excluded.summary,
        description = excluded.description,
        risk = excluded.risk,
        queue_namespace = excluded.queue_namespace,
        evidence_key = excluded.evidence_key,
        evidence_kind = excluded.evidence_kind,
        metadata_json = excluded.metadata_json,
        features_json = excluded.features_json,
        routing_category = excluded.routing_category,
        routing_confidence_tier = excluded.routing_confidence_tier,
        routing_blocked_reason_category = excluded.routing_blocked_reason_category,
        routing_tags_json = excluded.routing_tags_json
    `;
        return db.prepare(upsertSql);
    }
    insertTaskRow(insert, task, projection, registry) {
        const compatTask = { ...task, unblocks: projection.unblocksByTask.get(task.id) ?? [] };
        const r = taskEntityToRow(compatTask, registry ? { omitFeaturesJson: true } : undefined);
        insert.run(r.id, r.status, r.type, r.title, r.created_at, r.updated_at, r.archived, r.archived_at, r.priority, r.phase, r.phase_key, r.ownership, r.approach, r.depends_on_json, r.unblocks_json, r.technical_scope_json, r.acceptance_criteria_json, r.summary, r.description, r.risk, r.queue_namespace, r.evidence_key, r.evidence_kind, r.metadata_json, r.features_json ?? "[]", r.routing_category ?? null, r.routing_confidence_tier ?? null, r.routing_blocked_reason_category ?? null, r.routing_tags_json ?? null);
    }
    insertTransitionEvidence(db, evidenceRows) {
        if (evidenceRows.length === 0) {
            return;
        }
        const insertTransition = db.prepare(`INSERT OR IGNORE INTO ${TASK_ENGINE_TRANSITION_LOG_TABLE} (
        transition_id, task_id, from_state, to_state, action, timestamp, actor,
        client_mutation_id, payload_digest, guard_results_json, dependents_unblocked_json
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
        for (const evidence of evidenceRows) {
            insertTransition.run(evidence.transitionId, evidence.taskId, evidence.fromState, evidence.toState, evidence.action, evidence.timestamp, evidence.actor ?? null, evidence.clientMutationId ?? null, evidence.payloadDigest ?? null, JSON.stringify(evidence.guardResults ?? []), JSON.stringify(evidence.dependentsUnblocked ?? []));
        }
    }
    insertMutationEvidence(db, evidenceRows) {
        if (evidenceRows.length === 0) {
            return;
        }
        const insertMutation = db.prepare(`INSERT OR IGNORE INTO ${TASK_ENGINE_MUTATION_LOG_TABLE} (
        mutation_id, mutation_type, task_id, timestamp, actor, details_json
      ) VALUES (?, ?, ?, ?, ?, ?)`);
        for (const evidence of evidenceRows) {
            insertMutation.run(evidence.mutationId, evidence.mutationType, evidence.taskId, evidence.timestamp, evidence.actor ?? null, evidence.details !== undefined ? JSON.stringify(evidence.details) : null);
        }
    }
    persistRelationalIncremental(db, nextPlanningGeneration, options) {
        const dirtySet = new Set(options.dirtyTaskIds);
        const dirtyTasks = this._taskDoc.tasks.filter((t) => dirtySet.has(t.id));
        const projection = dependencyProjection(this._taskDoc.tasks);
        const registry = featureRegistryActiveOnConnection(db);
        const upsert = this.taskUpsertStatement(db);
        for (const t of dirtyTasks) {
            this.insertTaskRow(upsert, t, projection, registry);
        }
        const deleteDependency = db.prepare(`DELETE FROM ${TASK_ENGINE_DEPENDENCIES_TABLE} WHERE task_id = ?`);
        const insertDependency = db.prepare(`INSERT OR IGNORE INTO ${TASK_ENGINE_DEPENDENCIES_TABLE} (task_id, depends_on_task_id, created_at, source) VALUES (?, ?, ?, 'dependsOn')`);
        const now = new Date().toISOString();
        for (const taskId of dirtySet) {
            deleteDependency.run(taskId);
            for (const depId of projection.dependsOnByTask.get(taskId) ?? []) {
                insertDependency.run(taskId, depId, now);
            }
        }
        if (registry) {
            replaceTaskFeatureLinksForTasks(db, dirtyTasks);
        }
        const transitionIds = new Set(options.newTransitionIds ?? []);
        const mutationIds = new Set(options.newMutationIds ?? []);
        this.insertTransitionEvidence(db, this._taskDoc.transitionLog.filter((e) => transitionIds.has(e.transitionId)));
        this.insertMutationEvidence(db, (this._taskDoc.mutationLog ?? []).filter((e) => mutationIds.has(e.mutationId)));
        const blobJson = JSON.stringify(relationalCompatibilityTaskBlob(this._taskDoc.lastUpdated));
        const exists = db.prepare("SELECT 1 AS ok FROM workspace_planning_state WHERE id = 1").get();
        if (exists) {
            db.prepare(`UPDATE workspace_planning_state SET task_store_json = ?, transition_log_json = '[]', mutation_log_json = '[]', relational_tasks = 1, planning_generation = ? WHERE id = 1`).run(blobJson, nextPlanningGeneration);
        }
        else {
            db.prepare(`INSERT INTO workspace_planning_state (id, task_store_json, transition_log_json, mutation_log_json, relational_tasks, planning_generation) VALUES (1, ?, '[]', '[]', 1, ?)`).run(blobJson, nextPlanningGeneration);
        }
    }
    persistRelationalFull(db, nextPlanningGeneration) {
        const projection = dependencyProjection(this._taskDoc.tasks);
        const blobJson = JSON.stringify(relationalCompatibilityTaskBlob(this._taskDoc.lastUpdated));
        const evidenceRelational = evidenceTablesAvailable(db);
        const tr = evidenceRelational ? "[]" : JSON.stringify(this._taskDoc.transitionLog);
        const ml = evidenceRelational ? "[]" : JSON.stringify(this._taskDoc.mutationLog ?? []);
        const insert = this.taskInsertStatement(db);
        const registry = featureRegistryActiveOnConnection(db);
        if (dependencyTableAvailable(db)) {
            db.prepare(`DELETE FROM ${TASK_ENGINE_DEPENDENCIES_TABLE}`).run();
        }
        db.prepare(`DELETE FROM ${TASK_ENGINE_TASKS_TABLE}`).run();
        for (const t of this._taskDoc.tasks) {
            this.insertTaskRow(insert, t, projection, registry);
        }
        if (dependencyTableAvailable(db)) {
            const insertDependency = db.prepare(`INSERT OR IGNORE INTO ${TASK_ENGINE_DEPENDENCIES_TABLE} (task_id, depends_on_task_id, created_at, source) VALUES (?, ?, ?, 'dependsOn')`);
            const now = new Date().toISOString();
            for (const [taskId, deps] of projection.dependsOnByTask) {
                for (const depId of deps) {
                    insertDependency.run(taskId, depId, now);
                }
            }
        }
        if (registry) {
            replaceAllTaskFeatureLinks(db, this._taskDoc.tasks);
        }
        if (evidenceTablesAvailable(db)) {
            db.prepare(`DELETE FROM ${TASK_ENGINE_TRANSITION_LOG_TABLE}`).run();
            db.prepare(`DELETE FROM ${TASK_ENGINE_MUTATION_LOG_TABLE}`).run();
            const insertTransition = db.prepare(`INSERT OR IGNORE INTO ${TASK_ENGINE_TRANSITION_LOG_TABLE} (
          transition_id, task_id, from_state, to_state, action, timestamp, actor,
          client_mutation_id, payload_digest, guard_results_json, dependents_unblocked_json
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
            for (const evidence of this._taskDoc.transitionLog) {
                insertTransition.run(evidence.transitionId, evidence.taskId, evidence.fromState, evidence.toState, evidence.action, evidence.timestamp, evidence.actor ?? null, evidence.clientMutationId ?? null, evidence.payloadDigest ?? null, JSON.stringify(evidence.guardResults ?? []), JSON.stringify(evidence.dependentsUnblocked ?? []));
            }
            const insertMutation = db.prepare(`INSERT OR IGNORE INTO ${TASK_ENGINE_MUTATION_LOG_TABLE} (
          mutation_id, mutation_type, task_id, timestamp, actor, details_json
        ) VALUES (?, ?, ?, ?, ?, ?)`);
            for (const evidence of this._taskDoc.mutationLog ?? []) {
                insertMutation.run(evidence.mutationId, evidence.mutationType, evidence.taskId, evidence.timestamp, evidence.actor ?? null, evidence.details !== undefined ? JSON.stringify(evidence.details) : null);
            }
        }
        if (this._tableShape === "legacy-dual") {
            const w = JSON.stringify(this._wishlistDoc);
            const exists = db.prepare("SELECT 1 AS ok FROM workspace_planning_state WHERE id = 1").get();
            if (exists) {
                db.prepare(`UPDATE workspace_planning_state SET task_store_json = ?, wishlist_store_json = ?, transition_log_json = ?, mutation_log_json = ?, relational_tasks = 1, planning_generation = ? WHERE id = 1`).run(blobJson, w, tr, ml, nextPlanningGeneration);
            }
            else {
                db.prepare(`INSERT INTO workspace_planning_state (id, task_store_json, wishlist_store_json, transition_log_json, mutation_log_json, relational_tasks, planning_generation) VALUES (1, ?, ?, ?, ?, 1, ?)`).run(blobJson, w, tr, ml, nextPlanningGeneration);
            }
            return;
        }
        const exists = db.prepare("SELECT 1 AS ok FROM workspace_planning_state WHERE id = 1").get();
        if (exists) {
            db.prepare(`UPDATE workspace_planning_state SET task_store_json = ?, transition_log_json = ?, mutation_log_json = ?, relational_tasks = 1, planning_generation = ? WHERE id = 1`).run(blobJson, tr, ml, nextPlanningGeneration);
        }
        else {
            db.prepare(`INSERT INTO workspace_planning_state (id, task_store_json, transition_log_json, mutation_log_json, relational_tasks, planning_generation) VALUES (1, ?, ?, ?, 1, ?)`).run(blobJson, tr, ml, nextPlanningGeneration);
        }
    }
    persistBlobOnly(db, nextPlanningGeneration) {
        const docForBlob = {
            ...this._taskDoc,
            tasks: this._taskDoc.tasks.map((x) => stripAgentRoutingFromTask(x))
        };
        const t = JSON.stringify(docForBlob);
        if (this._tableShape === "legacy-dual") {
            const w = JSON.stringify(this._wishlistDoc);
            const exists = db.prepare("SELECT 1 AS ok FROM workspace_planning_state WHERE id = 1").get();
            if (exists) {
                db.prepare("UPDATE workspace_planning_state SET task_store_json = ?, wishlist_store_json = ?, planning_generation = ? WHERE id = 1").run(t, w, nextPlanningGeneration);
            }
            else {
                db.prepare("INSERT INTO workspace_planning_state (id, task_store_json, wishlist_store_json, planning_generation) VALUES (1, ?, ?, ?)").run(t, w, nextPlanningGeneration);
            }
            return;
        }
        const exists = db.prepare("SELECT 1 AS ok FROM workspace_planning_state WHERE id = 1").get();
        if (exists) {
            db.prepare("UPDATE workspace_planning_state SET task_store_json = ?, planning_generation = ? WHERE id = 1").run(t, nextPlanningGeneration);
        }
        else {
            db.prepare("INSERT INTO workspace_planning_state (id, task_store_json, planning_generation) VALUES (1, ?, ?)").run(t, nextPlanningGeneration);
        }
    }
    persistSync(options) {
        const db = this.ensureDb();
        this._tableShape = detectTableShape(db);
        db.transaction(() => this.runPersistMutation(db, options ?? {})).immediate();
    }
    /**
     * Run synchronous work inside one SQLite transaction, then flush with planning generation bump.
     * Pass `expectedPlanningGeneration` when using optimistic concurrency (must match row before work runs).
     */
    withTransaction(work, options) {
        const db = this.ensureDb();
        this._tableShape = detectTableShape(db);
        db.transaction(() => this.runPersistMutation(db, options ?? {}, work)).immediate();
    }
    /**
     * Drop legacy `wishlist_store_json` column by recreating the table (task JSON only).
     * Caller must persist an empty wishlist document first if migrating off legacy data.
     */
    migrateToTaskOnlyTableSchema() {
        const db = this.ensureDb();
        if (detectTableShape(db) !== "legacy-dual") {
            this._tableShape = "task-only";
            return;
        }
        const hasRel = kitSqliteHasRelationalTaskDdl(db);
        const oldTableCols = planningStateColumnSet(db);
        let preservedPlanningGen = 0;
        if (oldTableCols.has("planning_generation")) {
            const gr = db
                .prepare("SELECT planning_generation AS g FROM workspace_planning_state WHERE id = 1")
                .get();
            preservedPlanningGen = gr !== undefined ? Number(gr.g) || 0 : 0;
        }
        const oldRow = hasRel
            ? db
                .prepare("SELECT task_store_json, transition_log_json, mutation_log_json, relational_tasks FROM workspace_planning_state WHERE id = 1")
                .get()
            : undefined;
        db.exec(`
      CREATE TABLE IF NOT EXISTS workspace_planning_state_new (
        id INTEGER PRIMARY KEY CHECK (id = 1),
        task_store_json TEXT NOT NULL
      );
    `);
        if (hasRel) {
            db.exec("ALTER TABLE workspace_planning_state_new ADD COLUMN transition_log_json TEXT NOT NULL DEFAULT '[]'");
            db.exec("ALTER TABLE workspace_planning_state_new ADD COLUMN mutation_log_json TEXT NOT NULL DEFAULT '[]'");
            db.exec("ALTER TABLE workspace_planning_state_new ADD COLUMN relational_tasks INTEGER NOT NULL DEFAULT 0");
        }
        db.exec("ALTER TABLE workspace_planning_state_new ADD COLUMN planning_generation INTEGER NOT NULL DEFAULT 0");
        const row = db
            .prepare("SELECT task_store_json FROM workspace_planning_state WHERE id = 1")
            .get();
        const taskJson = row?.task_store_json ?? JSON.stringify(emptyTaskStoreDocument());
        if (hasRel) {
            db.prepare("INSERT OR REPLACE INTO workspace_planning_state_new (id, task_store_json, transition_log_json, mutation_log_json, relational_tasks, planning_generation) VALUES (1, ?, ?, ?, ?, ?)").run(taskJson, oldRow?.transition_log_json ?? "[]", oldRow?.mutation_log_json ?? "[]", oldRow?.relational_tasks ?? 0, preservedPlanningGen);
        }
        else {
            db.prepare("INSERT OR REPLACE INTO workspace_planning_state_new (id, task_store_json, planning_generation) VALUES (1, ?, ?)").run(taskJson, preservedPlanningGen);
        }
        db.exec("DROP TABLE workspace_planning_state");
        db.exec("ALTER TABLE workspace_planning_state_new RENAME TO workspace_planning_state");
        this._tableShape = "task-only";
        this._wishlistDoc = emptyWishlistDocument();
    }
}
