/**
 * The highest schema version this build knows how to apply. Bump this in lock
 * step whenever a new `if (db.schemaVersion() < N)` step is added below.
 *
 * It is also the downgrade tripwire: because migrations are forward-only, a DB
 * written by a NEWER build (schemaVersion > MAX) has a physical shape this
 * build does not understand. Running against it would silently no-op every
 * gate and then read/write a future schema with old code — exactly the kind of
 * silent corruption the launcher's atomic-update story must rule out. So we
 * fail closed instead.
 */
export const MAX_SCHEMA_VERSION = 22;
export class SchemaDowngradeError extends Error {
    persistedVersion;
    supportedVersion;
    constructor(persistedVersion, supportedVersion) {
        super(`Local store schema version ${persistedVersion} is newer than this build supports (${supportedVersion}). ` +
            "This database was written by a newer PatchTrail version. " +
            "Re-install the matching version (e.g. `patchtrail update`) — downgrading is not supported.");
        this.name = "SchemaDowngradeError";
        this.persistedVersion = persistedVersion;
        this.supportedVersion = supportedVersion;
    }
}
/**
 * Applies the versioned schema migrations for a local store against the given
 * connection. Each step is gated on {@link SqliteConnection.schemaVersion} and
 * recorded in schema_migrations, so migrate() is idempotent and forward-only.
 *
 * The v7 step backfills permission_activity_events from the existing request
 * and decision rows; that work is injected via {@link backfillPermissionActivityEvents}
 * and is invoked *before* the version-7 marker is written, preserving the
 * original ordering.
 *
 * If the persisted schema is NEWER than {@link MAX_SCHEMA_VERSION}, migrate()
 * throws {@link SchemaDowngradeError} (fail-closed) rather than operating on a
 * schema it does not understand.
 */
export function migrate(db, backfillPermissionActivityEvents) {
    db.exec("PRAGMA journal_mode = WAL");
    db.exec("PRAGMA foreign_keys = ON");
    db.exec(`
    CREATE TABLE IF NOT EXISTS schema_migrations (
      version INTEGER PRIMARY KEY,
      applied_at TEXT NOT NULL
    );
  `);
    const persistedVersion = db.schemaVersion();
    if (persistedVersion > MAX_SCHEMA_VERSION) {
        throw new SchemaDowngradeError(persistedVersion, MAX_SCHEMA_VERSION);
    }
    if (db.schemaVersion() < 1) {
        db.exec(`
      CREATE TABLE IF NOT EXISTS workspaces (
        id TEXT PRIMARY KEY,
        root_path TEXT NOT NULL,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL
      );

      CREATE TABLE IF NOT EXISTS runs (
        id TEXT PRIMARY KEY,
        workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
        provider TEXT NOT NULL,
        status TEXT NOT NULL,
        title TEXT,
        runtime_session_id TEXT,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL,
        last_error TEXT
      );

      CREATE TABLE IF NOT EXISTS outbox_events (
        id TEXT PRIMARY KEY,
        type TEXT NOT NULL,
        aggregate_id TEXT NOT NULL,
        payload_json TEXT NOT NULL,
        status TEXT NOT NULL,
        attempts INTEGER NOT NULL DEFAULT 0,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL
      );

      CREATE INDEX IF NOT EXISTS idx_runs_workspace_id ON runs(workspace_id);
      CREATE INDEX IF NOT EXISTS idx_outbox_status_created_at ON outbox_events(status, created_at);
    `);
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (1, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 2) {
        db.addColumnIfMissing("runs", "title", "TEXT");
        db.addColumnIfMissing("runs", "runtime_session_id", "TEXT");
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (2, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 3) {
        db.exec(`
      CREATE TABLE IF NOT EXISTS runtime_events (
        id TEXT PRIMARY KEY,
        run_id TEXT REFERENCES runs(id) ON DELETE CASCADE,
        runtime_session_id TEXT,
        source TEXT NOT NULL,
        type TEXT NOT NULL,
        occurred_at TEXT NOT NULL,
        title TEXT NOT NULL,
        description TEXT NOT NULL,
        tone TEXT NOT NULL,
        metadata_json TEXT NOT NULL,
        created_at TEXT NOT NULL
      );

      CREATE INDEX IF NOT EXISTS idx_runs_runtime_session_id ON runs(runtime_session_id);
      CREATE INDEX IF NOT EXISTS idx_runtime_events_run_occurred_at ON runtime_events(run_id, occurred_at, id);
    `);
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (3, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 4) {
        db.exec(`
      CREATE TABLE IF NOT EXISTS permission_requests (
        id TEXT PRIMARY KEY,
        run_id TEXT REFERENCES runs(id) ON DELETE CASCADE,
        workspace_id TEXT NOT NULL,
        runtime_session_id TEXT NOT NULL,
        provider TEXT NOT NULL,
        provider_request_id TEXT,
        provider_permission_id TEXT,
        source_runtime_event_id TEXT REFERENCES runtime_events(id),
        request_json TEXT NOT NULL,
        evaluation_json TEXT NOT NULL,
        status TEXT NOT NULL,
        selected_option TEXT,
        opencode_reply TEXT,
        decision_audit_id TEXT,
        last_error TEXT,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL,
        answered_at TEXT
      );

      CREATE INDEX IF NOT EXISTS idx_permission_requests_status_created_at ON permission_requests(status, created_at, id);
      CREATE INDEX IF NOT EXISTS idx_permission_requests_run_created_at ON permission_requests(run_id, created_at, id);
      CREATE UNIQUE INDEX IF NOT EXISTS idx_permission_requests_provider_permission_id ON permission_requests(runtime_session_id, provider_permission_id) WHERE provider_permission_id IS NOT NULL;
      CREATE UNIQUE INDEX IF NOT EXISTS idx_permission_requests_provider_request_id ON permission_requests(runtime_session_id, provider_request_id) WHERE provider_request_id IS NOT NULL;
    `);
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (4, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 5) {
        db.exec(`
      CREATE TABLE IF NOT EXISTS permission_decisions (
        id TEXT PRIMARY KEY,
        workspace_id TEXT NOT NULL,
        run_id TEXT,
        runtime_session_id TEXT,
        scope TEXT NOT NULL,
        effect TEXT NOT NULL,
        option TEXT NOT NULL,
        capabilities_json TEXT NOT NULL,
        pattern TEXT NOT NULL,
        tool_name TEXT NOT NULL,
        tool_title TEXT NOT NULL,
        tool_risk TEXT NOT NULL,
        input_preview TEXT NOT NULL,
        affected_paths_json TEXT NOT NULL,
        source_permission_request_id TEXT,
        audit_id TEXT NOT NULL,
        status TEXT NOT NULL,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL,
        revoked_at TEXT,
        revoke_audit_id TEXT
      );

      CREATE INDEX IF NOT EXISTS idx_permission_decisions_workspace_status_created_at ON permission_decisions(workspace_id, status, created_at, id);
      CREATE INDEX IF NOT EXISTS idx_permission_decisions_active ON permission_decisions(status) WHERE status = 'active';
    `);
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (5, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 6) {
        db.addColumnIfMissing("permission_decisions", "revoke_reason", "TEXT");
        db.addColumnIfMissing("permission_decisions", "revoke_source", "TEXT");
        db.exec(`
      CREATE INDEX IF NOT EXISTS idx_permission_decisions_session_allow_history
        ON permission_decisions(workspace_id, scope, effect, option, status, created_at, id);
    `);
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (6, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 7) {
        db.exec(`
      CREATE TABLE IF NOT EXISTS permission_activity_events (
        id TEXT PRIMARY KEY,
        kind TEXT NOT NULL,
        activity_at TEXT NOT NULL,
        source TEXT NOT NULL,
        workspace_id TEXT NOT NULL,
        run_id TEXT,
        runtime_session_id TEXT,
        request_id TEXT,
        decision_id TEXT,
        status TEXT,
        selected_option TEXT,
        opencode_reply TEXT,
        tool_name TEXT NOT NULL,
        tool_title TEXT NOT NULL,
        tool_risk TEXT NOT NULL,
        input_preview TEXT NOT NULL,
        affected_paths_json TEXT NOT NULL,
        external_service_mutation INTEGER NOT NULL DEFAULT 0,
        deny_only_diagnostics_json TEXT,
        error TEXT,
        revoke_reason TEXT,
        revoke_source TEXT,
        created_at TEXT NOT NULL
      );

      CREATE INDEX IF NOT EXISTS idx_permission_activity_workspace_activity
        ON permission_activity_events(workspace_id, activity_at, id);
      CREATE INDEX IF NOT EXISTS idx_permission_activity_workspace_run_activity
        ON permission_activity_events(workspace_id, run_id, activity_at, id);
      CREATE INDEX IF NOT EXISTS idx_permission_activity_request
        ON permission_activity_events(request_id);
      CREATE INDEX IF NOT EXISTS idx_permission_activity_decision
        ON permission_activity_events(decision_id);
    `);
        backfillPermissionActivityEvents();
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (7, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 8) {
        db.exec(`
      CREATE TABLE IF NOT EXISTS debug_reveal_events (
        id TEXT PRIMARY KEY,
        workspace_id TEXT NOT NULL,
        reveal_session_id TEXT NOT NULL,
        kind TEXT NOT NULL,
        event_at TEXT NOT NULL,
        expires_at TEXT,
        ttl_seconds INTEGER,
        reason_label TEXT,
        surfaces_json TEXT NOT NULL,
        created_at TEXT NOT NULL
      );

      CREATE INDEX IF NOT EXISTS idx_debug_reveal_workspace_event
        ON debug_reveal_events(workspace_id, event_at, id);
      CREATE INDEX IF NOT EXISTS idx_debug_reveal_workspace_session_event
        ON debug_reveal_events(workspace_id, reveal_session_id, event_at, id);
    `);
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (8, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 9) {
        db.addColumnIfMissing("runs", "source_work_item_id", "TEXT");
        db.addColumnIfMissing("runs", "source_repository_id", "TEXT");
        db.addColumnIfMissing("runs", "source_preparation_id", "TEXT");
        db.addColumnIfMissing("runs", "source_run_intent_id", "TEXT");
        db.addColumnIfMissing("runs", "branch_plan_id", "TEXT");
        db.addColumnIfMissing("runs", "branch_name", "TEXT");
        db.addColumnIfMissing("runs", "base_branch", "TEXT");
        db.exec(`
      CREATE INDEX IF NOT EXISTS idx_runs_source_binding
        ON runs(workspace_id, source_repository_id, source_work_item_id, created_at, id);
    `);
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (9, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 10) {
        db.exec(`
      CREATE TABLE IF NOT EXISTS run_proof_evidence (
        run_id TEXT NOT NULL PRIMARY KEY REFERENCES runs(id) ON DELETE CASCADE,
        changed_files_json TEXT NOT NULL,
        validations_json TEXT NOT NULL,
        warnings_json TEXT NOT NULL,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL
      );
    `);
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (10, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 11) {
        db.addColumnIfMissing("runs", "cap_policy_json", "TEXT");
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (11, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 12) {
        db.exec(`
      CREATE TABLE IF NOT EXISTS repository_bindings (
        id TEXT NOT NULL PRIMARY KEY,
        workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
        display_label TEXT NOT NULL,
        provider TEXT NOT NULL,
        status TEXT NOT NULL,
        last_sync_label TEXT,
        last_error_code TEXT,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL
      );
      CREATE INDEX IF NOT EXISTS idx_repository_bindings_workspace
        ON repository_bindings(workspace_id);

      CREATE TABLE IF NOT EXISTS onboarding_gates (
        id TEXT NOT NULL PRIMARY KEY,
        workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
        gate TEXT NOT NULL,
        status TEXT NOT NULL,
        tone TEXT NOT NULL,
        display_label TEXT NOT NULL,
        last_error_code TEXT,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL,
        UNIQUE(workspace_id, gate)
      );
      CREATE INDEX IF NOT EXISTS idx_onboarding_gates_workspace
        ON onboarding_gates(workspace_id);

      CREATE TABLE IF NOT EXISTS settings_readiness (
        id TEXT NOT NULL PRIMARY KEY,
        workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
        area TEXT NOT NULL,
        status TEXT NOT NULL,
        tone TEXT NOT NULL,
        display_label TEXT NOT NULL,
        last_error_code TEXT,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL,
        UNIQUE(workspace_id, area)
      );
      CREATE INDEX IF NOT EXISTS idx_settings_readiness_workspace
        ON settings_readiness(workspace_id);
    `);
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (12, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 13) {
        db.exec(`
      CREATE TABLE IF NOT EXISTS review_queue (
        id TEXT NOT NULL PRIMARY KEY,
        workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
        run_id TEXT,
        status TEXT NOT NULL,
        risk_tone TEXT NOT NULL,
        headline TEXT NOT NULL,
        summary_label TEXT,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL
      );
      CREATE INDEX IF NOT EXISTS idx_review_queue_workspace
        ON review_queue(workspace_id);

      CREATE TABLE IF NOT EXISTS review_decisions (
        id TEXT NOT NULL PRIMARY KEY,
        workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
        review_id TEXT NOT NULL REFERENCES review_queue(id) ON DELETE CASCADE,
        decision TEXT NOT NULL,
        reason_code TEXT,
        note_label TEXT,
        decided_at TEXT NOT NULL,
        created_at TEXT NOT NULL
      );
      CREATE INDEX IF NOT EXISTS idx_review_decisions_workspace
        ON review_decisions(workspace_id);
      CREATE INDEX IF NOT EXISTS idx_review_decisions_review
        ON review_decisions(review_id);
    `);
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (13, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 14) {
        db.exec(`
      CREATE TABLE IF NOT EXISTS sync_state (
        domain TEXT NOT NULL,
        idempotency_key TEXT NOT NULL,
        workspace_id TEXT NOT NULL,
        row_id TEXT NOT NULL,
        last_attempt_at TEXT,
        last_success_at TEXT,
        retry_count INTEGER NOT NULL DEFAULT 0,
        last_failure_reason TEXT,
        PRIMARY KEY (domain, idempotency_key)
      );
      CREATE INDEX IF NOT EXISTS idx_sync_state_domain
        ON sync_state(domain);
    `);
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (14, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 15) {
        db.exec(`
      CREATE TABLE IF NOT EXISTS audit_events (
        seq INTEGER PRIMARY KEY AUTOINCREMENT,
        id TEXT NOT NULL UNIQUE,
        type TEXT NOT NULL,
        severity TEXT NOT NULL,
        at TEXT NOT NULL,
        message TEXT NOT NULL,
        metadata_json TEXT
      );
      CREATE INDEX IF NOT EXISTS idx_audit_events_at
        ON audit_events(at, seq);
    `);
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (15, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 16) {
        db.addColumnIfMissing("runs", "attempt_count", "INTEGER NOT NULL DEFAULT 1");
        db.addColumnIfMissing("runs", "previous_run_id", "TEXT");
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (16, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 17) {
        db.addColumnIfMissing("run_proof_evidence", "patch_files_json", "TEXT");
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (17, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 18) {
        db.exec(`
      CREATE TABLE IF NOT EXISTS devices (
        id TEXT NOT NULL PRIMARY KEY,
        label TEXT NOT NULL,
        workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
        registered_at TEXT NOT NULL,
        updated_at TEXT NOT NULL
      );
      CREATE INDEX IF NOT EXISTS idx_devices_workspace
        ON devices(workspace_id);

      CREATE TABLE IF NOT EXISTS command_inbox (
        command_id TEXT NOT NULL PRIMARY KEY,
        command_type TEXT NOT NULL,
        payload_json TEXT NOT NULL,
        status TEXT NOT NULL DEFAULT 'received',
        received_at TEXT NOT NULL,
        processed_at TEXT,
        failure_reason TEXT
      );
      CREATE INDEX IF NOT EXISTS idx_command_inbox_status
        ON command_inbox(status);
    `);
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (18, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 19) {
        // Layer 3 device pairing: bind the device's own public key and the trusted
        // (browser-held) issuer public key the bridge verifies remote commands
        // against. Both are public keys — safe at rest in the local store.
        db.addColumnIfMissing("devices", "device_public_key_spki", "TEXT");
        db.addColumnIfMissing("devices", "paired_issuer_public_key_spki", "TEXT");
        db.addColumnIfMissing("devices", "paired_at", "TEXT");
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (19, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 20) {
        // Layer 3 Remote Control: bridge-side, authoritative default-deny policy.
        // A category is denied unless an explicit row enables it (absence ⇒ denied).
        // risk_mode 'confirm' requires local confirmation before dispatch even when
        // enabled; 'silent' executes silently (high-risk categories may only be set
        // silent by explicit user opt-in).
        db.exec(`
      CREATE TABLE IF NOT EXISTS remote_control_policy (
        workspace_id TEXT NOT NULL,
        category TEXT NOT NULL,
        enabled INTEGER NOT NULL DEFAULT 0,
        risk_mode TEXT NOT NULL DEFAULT 'confirm',
        updated_at TEXT NOT NULL,
        PRIMARY KEY (workspace_id, category)
      );
    `);
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (20, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 21) {
        // Layer 3 replay defense: the inbox primary key is the TRANSPORT command id,
        // which the untrusted backend relay chooses freely — so it cannot dedup a
        // captured valid RCA re-enqueued under a fresh transport id. This ledger
        // makes the issuer-signed rca.commandId single-use: the first inbox row to
        // act on it claims it, and any later transport delivery of the same signed
        // command id is rejected as a replay (security model §3b exactly-once / §4d).
        db.exec(`
      CREATE TABLE IF NOT EXISTS remote_command_claims (
        rca_command_id TEXT NOT NULL PRIMARY KEY,
        inbox_command_id TEXT NOT NULL,
        claimed_at TEXT NOT NULL
      );
    `);
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (21, ?)").run(new Date().toISOString());
    }
    if (db.schemaVersion() < 22) {
        // Repository linking: a binding gains a real remote identity (remote URL,
        // owner/repo/default branch), a local working-copy path, and an explicit
        // link + clone lifecycle. local_path is durable at rest but is NEVER
        // surfaced to the browser — the public projection strips it (a local path
        // is a forbidden public surface per the trust boundary). link_state and
        // clone_state default so pre-existing seed rows read back as 'unlinked' /
        // 'idle' without a data backfill.
        db.addColumnIfMissing("repository_bindings", "remote_url", "TEXT");
        db.addColumnIfMissing("repository_bindings", "owner", "TEXT");
        db.addColumnIfMissing("repository_bindings", "repo", "TEXT");
        db.addColumnIfMissing("repository_bindings", "default_branch", "TEXT");
        db.addColumnIfMissing("repository_bindings", "local_path", "TEXT");
        db.addColumnIfMissing("repository_bindings", "link_state", "TEXT NOT NULL DEFAULT 'unlinked'");
        db.addColumnIfMissing("repository_bindings", "clone_state", "TEXT NOT NULL DEFAULT 'idle'");
        db.addColumnIfMissing("repository_bindings", "last_clone_error_code", "TEXT");
        db.prepare("INSERT OR IGNORE INTO schema_migrations (version, applied_at) VALUES (22, ?)").run(new Date().toISOString());
    }
}
