/**
 * MigrationRunner — idempotent create-on-empty schema migration for AirCoding session DB.
 *
 * Implements DD §4.2. Creates all tables and indexes from db-schema-v1.md §2–§18.
 * V1.0.0 Alpha target schema_version = 1.
 *
 * @module packages/runtime/src/storage/MigrationRunner
 */

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/**
 * Minimal database handle abstraction. Matches the surface that DatabaseManager
 * (DD §4.1) will expose — Bun's `Database` satisfies this contract directly.
 */
export interface DatabaseHandle {
  exec(sql: string): void;
  prepare(sql: string): StatementHandle;
  query<T = Record<string, unknown>>(sql: string, ...params: unknown[]): T[];
}

export interface StatementHandle {
  run(...params: unknown[]): { changes: number };
  get<T = Record<string, unknown>>(...params: unknown[]): T | undefined;
  all<T = Record<string, unknown>>(...params: unknown[]): T[];
}

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

/** V1.0.0 Alpha target schema version. */
const TARGET_VERSION = 1;

/** Version string written into schema_meta. */
const AIRCODING_VERSION = '1.0.0-alpha.0';

// ---------------------------------------------------------------------------
// MigrationRunner
// ---------------------------------------------------------------------------

/**
 * Creates and manages the session DB schema. V1 only supports create-on-empty;
 * future versions will add incremental migration logic.
 *
 * Usage:
 * ```ts
 * const runner = new MigrationRunner();
 * await runner.migrate(db);
 * ```
 */
export class MigrationRunner {
  // -----------------------------------------------------------------------
  // Public API
  // -----------------------------------------------------------------------

  /**
   * Run all pending migrations. Idempotent — safe to call on an already-migrated DB.
   *
   * If the `schema_meta` table does not exist, the full V1 schema is created and
   * seeded. If it exists, only `aircoding_version_last_opened` is updated.
   */
  async migrate(db: DatabaseHandle): Promise<void> {
    const current = this.currentVersion(db);

    if (current === 0) {
      // Fresh database — create everything.
      this.applyV1(db);
      return;
    }

    // Already migrated — just update the last-opened version stamp.
    this.updateLastOpened(db);

    // Future: if (current < this.targetVersion()) { ... apply incremental ... }
  }

  /**
   * Returns the current schema version stored in `schema_meta`, or `0` if the
   * table does not exist yet (i.e. fresh / empty database).
   */
  currentVersion(db: DatabaseHandle): number {
    // Check whether schema_meta table exists at all.
    const tables = db.query<Record<string, unknown>>(
      "SELECT name FROM sqlite_master WHERE type='table' AND name='schema_meta'",
    );
    if (tables.length === 0) return 0;

    const rows = db.query<Record<string, unknown>>(
      "SELECT value FROM schema_meta WHERE key = 'schema_version'",
    );
    if (rows.length === 0) return 0;

    const parsed = Number(rows[0]!.value);
    return Number.isFinite(parsed) ? parsed : 0;
  }

  /** V1.0.0 Alpha target version. */
  targetVersion(): number {
    return TARGET_VERSION;
  }

  // -----------------------------------------------------------------------
  // V1 schema creation
  // -----------------------------------------------------------------------

  /**
   * Apply the complete V1 schema (db-schema-v1 §2–§18) and seed `schema_meta`.
   * Runs inside a transaction so the DB is never left in a partial state.
   */
  private applyV1(db: DatabaseHandle): void {
    db.exec('BEGIN TRANSACTION;');
    try {
      this.createTables(db);
      this.createIndexes(db);
      this.seedSchemaMeta(db);
      db.exec('COMMIT;');
    } catch (err) {
      db.exec('ROLLBACK;');
      throw err;
    }
  }

  // -----------------------------------------------------------------------
  // Table DDL (db-schema-v1 §2–§18)
  // -----------------------------------------------------------------------

  private createTables(db: DatabaseHandle): void {
    // §2 schema_meta
    db.exec(`
      CREATE TABLE IF NOT EXISTS schema_meta (
        key TEXT PRIMARY KEY,
        value TEXT NOT NULL
      );
    `);

    // §3 sessions
    db.exec(`
      CREATE TABLE IF NOT EXISTS sessions (
        id TEXT PRIMARY KEY,
        project_id TEXT NOT NULL,
        project_root TEXT NOT NULL,
        title TEXT,
        status TEXT NOT NULL,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL,
        exited_at TEXT,
        model_provider_id TEXT,
        model_id TEXT,
        metadata_json TEXT
      );
    `);

    // §4 messages
    db.exec(`
      CREATE TABLE IF NOT EXISTS messages (
        id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL,
        role TEXT NOT NULL,
        canonical_format TEXT NOT NULL,
        content_json TEXT NOT NULL,
        parent_message_id TEXT,
        route_json TEXT,
        created_at TEXT NOT NULL,
        token_estimate INTEGER,
        metadata_json TEXT
      );
    `);

    // §5 message_drafts
    db.exec(`
      CREATE TABLE IF NOT EXISTS message_drafts (
        message_id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL,
        role TEXT NOT NULL,
        canonical_format TEXT NOT NULL,
        partial_content_json TEXT NOT NULL,
        status TEXT NOT NULL,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL,
        metadata_json TEXT
      );
    `);

    // §6 events
    db.exec(`
      CREATE TABLE IF NOT EXISTS events (
        id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL,
        type TEXT NOT NULL,
        version INTEGER NOT NULL,
        timestamp TEXT NOT NULL,

        source_kind TEXT NOT NULL,
        source_id TEXT,
        agent_type TEXT,

        task_id TEXT,
        agent_id TEXT,
        tool_run_id TEXT,
        command_run_id TEXT,

        route_json TEXT NOT NULL,
        route_text TEXT NOT NULL,

        payload_json TEXT NOT NULL
      );
    `);

    // §7 tasks
    db.exec(`
      CREATE TABLE IF NOT EXISTS tasks (
        id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL,
        type TEXT NOT NULL,
        status TEXT NOT NULL,
        title TEXT NOT NULL,

        task_spec_json TEXT NOT NULL,
        worker_result_json TEXT,

        assigned_agent_id TEXT,
        workspace_id TEXT,

        retry_count INTEGER NOT NULL DEFAULT 0,

        created_at TEXT NOT NULL,
        started_at TEXT,
        completed_at TEXT,
        heartbeat_at TEXT,

        metadata_json TEXT
      );
    `);

    // §8 task_dependencies
    db.exec(`
      CREATE TABLE IF NOT EXISTS task_dependencies (
        id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL,
        task_id TEXT NOT NULL,
        depends_on_task_id TEXT NOT NULL,
        dependency_type TEXT NOT NULL,
        reason TEXT,
        created_at TEXT NOT NULL
      );
    `);

    // §9 task_attempts
    db.exec(`
      CREATE TABLE IF NOT EXISTS task_attempts (
        id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL,
        task_id TEXT NOT NULL,
        attempt_index INTEGER NOT NULL,

        agent_id TEXT,
        status TEXT NOT NULL,

        failure_signature TEXT,
        failure_summary TEXT,

        started_at TEXT NOT NULL,
        completed_at TEXT,

        worker_result_json TEXT,
        metadata_json TEXT
      );
    `);

    // §10 agents
    db.exec(`
      CREATE TABLE IF NOT EXISTS agents (
        id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL,
        type TEXT NOT NULL,
        status TEXT NOT NULL,

        pid INTEGER,
        task_id TEXT,

        model_provider_id TEXT,
        model_id TEXT,

        started_at TEXT NOT NULL,
        completed_at TEXT,
        last_heartbeat_at TEXT,

        metadata_json TEXT
      );
    `);

    // §11 tool_runs
    db.exec(`
      CREATE TABLE IF NOT EXISTS tool_runs (
        id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL,

        task_id TEXT,
        agent_id TEXT,
        origin_message_id TEXT,

        tool_name TEXT NOT NULL,
        status TEXT NOT NULL,

        input_json TEXT NOT NULL,
        output_json TEXT,
        error_json TEXT,

        started_at TEXT NOT NULL,
        completed_at TEXT,
        duration_ms INTEGER,

        artifacts_json TEXT,
        evidence_refs_json TEXT,
        metadata_json TEXT
      );
    `);

    // §12 command_runs
    db.exec(`
      CREATE TABLE IF NOT EXISTS command_runs (
        id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL,

        task_id TEXT,
        agent_id TEXT,
        origin_message_id TEXT,
        tool_run_id TEXT,

        command TEXT NOT NULL,
        cwd TEXT NOT NULL,

        exit_code INTEGER,

        stdout_artifact_id TEXT,
        stderr_artifact_id TEXT,
        combined_artifact_id TEXT,

        started_at TEXT NOT NULL,
        completed_at TEXT,
        duration_ms INTEGER,

        parsed_diagnostics_json TEXT,
        metadata_json TEXT
      );
    `);

    // §13 artifacts
    db.exec(`
      CREATE TABLE IF NOT EXISTS artifacts (
        id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL,
        type TEXT NOT NULL,

        uri TEXT NOT NULL,
        path TEXT NOT NULL,
        original_name TEXT,

        size_bytes INTEGER,
        sha256 TEXT,

        task_id TEXT,
        agent_id TEXT,
        tool_run_id TEXT,
        command_run_id TEXT,

        associated_entity_type TEXT,
        associated_entity_id TEXT,

        created_at TEXT NOT NULL,
        metadata_json TEXT
      );
    `);

    // §14 diagnostics
    db.exec(`
      CREATE TABLE IF NOT EXISTS diagnostics (
        id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL,

        task_id TEXT,
        agent_id TEXT,
        command_run_id TEXT,
        artifact_id TEXT,

        language TEXT,
        toolchain TEXT,
        severity TEXT,

        file TEXT,
        line INTEGER,
        column INTEGER,
        code TEXT,

        message TEXT NOT NULL,
        semantic_signature TEXT NOT NULL,

        created_at TEXT NOT NULL,
        metadata_json TEXT
      );
    `);

    // §15 evidence_refs
    db.exec(`
      CREATE TABLE IF NOT EXISTS evidence_refs (
        id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL,

        task_id TEXT,
        agent_id TEXT,
        tool_run_id TEXT,
        command_run_id TEXT,
        artifact_id TEXT,
        diagnostic_id TEXT,
        message_id TEXT,

        kind TEXT NOT NULL,
        ref TEXT NOT NULL,
        location_json TEXT,
        claim TEXT NOT NULL,

        created_at TEXT NOT NULL
      );
    `);

    // §16 workspaces
    db.exec(`
      CREATE TABLE IF NOT EXISTS workspaces (
        id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL,

        task_id TEXT,
        agent_id TEXT,

        path TEXT NOT NULL,
        strategy TEXT NOT NULL,
        status TEXT NOT NULL,

        base_ref TEXT,
        branch_name TEXT,

        created_at TEXT NOT NULL,
        merged_at TEXT,

        metadata_json TEXT
      );
    `);

    // §17 summaries
    db.exec(`
      CREATE TABLE IF NOT EXISTS summaries (
        id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL,

        type TEXT NOT NULL,
        range_start_message_id TEXT,
        range_end_message_id TEXT,

        content_json TEXT NOT NULL,

        created_at TEXT NOT NULL,
        metadata_json TEXT
      );
    `);

    // §18 ui_state
    db.exec(`
      CREATE TABLE IF NOT EXISTS ui_state (
        id TEXT PRIMARY KEY,
        session_id TEXT NOT NULL,
        scope TEXT NOT NULL,
        key TEXT NOT NULL,
        value_json TEXT NOT NULL,
        updated_at TEXT NOT NULL
      );
    `);
  }

  // -----------------------------------------------------------------------
  // Index DDL (db-schema-v1 §4–§18)
  // -----------------------------------------------------------------------

  private createIndexes(db: DatabaseHandle): void {
    // §4 messages
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_messages_session_created
      ON messages(session_id, created_at);
    `);

    // §5 message_drafts
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_drafts_session_status
      ON message_drafts(session_id, status);
    `);

    // §6 events
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_events_session_type_time
      ON events(session_id, type, timestamp);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_events_task_time
      ON events(task_id, timestamp);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_events_agent_time
      ON events(agent_id, timestamp);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_events_route_text
      ON events(route_text);
    `);

    // §7 tasks
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_tasks_session_status
      ON tasks(session_id, status);
    `);

    // §8 task_dependencies
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_task_deps_task
      ON task_dependencies(task_id);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_task_deps_depends_on
      ON task_dependencies(depends_on_task_id);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_task_deps_session_type
      ON task_dependencies(session_id, dependency_type);
    `);

    // §9 task_attempts
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_task_attempts_task
      ON task_attempts(task_id, attempt_index);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_task_attempts_failure_signature
      ON task_attempts(failure_signature);
    `);

    // §10 agents
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_agents_session_status
      ON agents(session_id, status);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_agents_task
      ON agents(task_id);
    `);

    // §11 tool_runs
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_tool_runs_origin_message
      ON tool_runs(origin_message_id);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_tool_runs_session_time
      ON tool_runs(session_id, started_at);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_tool_runs_task_time
      ON tool_runs(task_id, started_at);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_tool_runs_agent_time
      ON tool_runs(agent_id, started_at);
    `);

    // §12 command_runs
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_command_runs_origin_message
      ON command_runs(origin_message_id);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_command_runs_session_time
      ON command_runs(session_id, started_at);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_command_runs_task_time
      ON command_runs(task_id, started_at);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_command_runs_agent_time
      ON command_runs(agent_id, started_at);
    `);

    // §13 artifacts
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_artifacts_session_type_time
      ON artifacts(session_id, type, created_at);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_artifacts_task
      ON artifacts(task_id);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_artifacts_agent
      ON artifacts(agent_id);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_artifacts_tool_run
      ON artifacts(tool_run_id);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_artifacts_command_run
      ON artifacts(command_run_id);
    `);

    // §14 diagnostics
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_diagnostics_signature
      ON diagnostics(semantic_signature);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_diagnostics_file
      ON diagnostics(file);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_diagnostics_command
      ON diagnostics(command_run_id);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_diagnostics_task
      ON diagnostics(task_id);
    `);

    // §15 evidence_refs
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_evidence_task
      ON evidence_refs(task_id);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_evidence_artifact
      ON evidence_refs(artifact_id);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_evidence_diagnostic
      ON evidence_refs(diagnostic_id);
    `);

    // §16 workspaces
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_workspaces_task
      ON workspaces(task_id);
    `);
    db.exec(`
      CREATE INDEX IF NOT EXISTS idx_workspaces_status
      ON workspaces(session_id, status);
    `);

    // §18 ui_state (unique index)
    db.exec(`
      CREATE UNIQUE INDEX IF NOT EXISTS idx_ui_state_session_scope_key
      ON ui_state(session_id, scope, key);
    `);
  }

  // -----------------------------------------------------------------------
  // Seeding & version stamps
  // -----------------------------------------------------------------------

  /**
   * Seed `schema_meta` with initial keys per db-schema §2.
   */
  private seedSchemaMeta(db: DatabaseHandle): void {
    const now = new Date().toISOString();

    const insert = db.prepare(
      'INSERT INTO schema_meta (key, value) VALUES (?, ?)',
    );

    insert.run('schema_version', String(TARGET_VERSION));
    insert.run('created_by', 'aircoding');
    insert.run('created_at', now);
    insert.run('aircoding_version_created', AIRCODING_VERSION);
    insert.run('aircoding_version_last_opened', AIRCODING_VERSION);
  }

  /**
   * Update `aircoding_version_last_opened` on every open (DD §4.2).
   */
  private updateLastOpened(db: DatabaseHandle): void {
    const stmt = db.prepare(
      "UPDATE schema_meta SET value = ? WHERE key = 'aircoding_version_last_opened'",
    );
    stmt.run(AIRCODING_VERSION);
  }
}