/**
 * Database migration runner.
 * Tracks applied migrations in schema_meta table.
 *
 * @module src/store/migrations/runner
 */

import type { Database } from "bun:sqlite";

import type { FtsTokenizer } from "../../config/types";
import type { MigrationResult, StoreResult } from "../types";

import { err, ok } from "../types";

// ─────────────────────────────────────────────────────────────────────────────
// Types
// ─────────────────────────────────────────────────────────────────────────────

/** Migration definition */
export interface Migration {
  /** Version number (must be unique and sequential) */
  version: number;
  /** Human-readable name */
  name: string;
  /** Apply migration */
  up(db: Database, ftsTokenizer: FtsTokenizer): void;
  /** Rollback migration (optional) */
  down?(db: Database): void;
}

// ─────────────────────────────────────────────────────────────────────────────
// Schema Meta Queries
// ─────────────────────────────────────────────────────────────────────────────

const BOOTSTRAP_META_TABLE = `
  CREATE TABLE IF NOT EXISTS schema_meta (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL,
    updated_at TEXT NOT NULL DEFAULT (datetime('now'))
  )
`;

const GET_META = "SELECT value FROM schema_meta WHERE key = ?";

const SET_META = `
  INSERT INTO schema_meta (key, value, updated_at)
  VALUES (?, ?, datetime('now'))
  ON CONFLICT(key) DO UPDATE SET
    value = excluded.value,
    updated_at = datetime('now')
`;

const HAS_SCHEMA_META_TABLE = `
  SELECT name
  FROM sqlite_master
  WHERE type = 'table' AND name = 'schema_meta'
`;

// ─────────────────────────────────────────────────────────────────────────────
// Migration Runner
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Get current schema version from database.
 * Returns 0 if no version is set.
 */
export function getSchemaVersion(db: Database): number {
  try {
    const row = db.query<{ value: string }, [string]>(GET_META).get("version");
    return row ? Number.parseInt(row.value, 10) : 0;
  } catch {
    // Table doesn't exist yet
    return 0;
  }
}

/**
 * Get current FTS tokenizer from database.
 * Returns null if not set.
 */
export function getDbFtsTokenizer(db: Database): FtsTokenizer | null {
  try {
    const row = db
      .query<{ value: string }, [string]>(GET_META)
      .get("fts_tokenizer");
    return row ? (row.value as FtsTokenizer) : null;
  } catch {
    return null;
  }
}

/**
 * Set metadata value.
 */
function setMeta(db: Database, key: string, value: string): void {
  db.run(SET_META, [key, value]);
}

/**
 * Run pending migrations.
 *
 * @param db - Open database connection
 * @param migrations - Array of migrations to apply
 * @param ftsTokenizer - FTS tokenizer from config
 * @returns Migration result with applied versions
 */
export function runMigrations(
  db: Database,
  migrations: Migration[],
  ftsTokenizer: FtsTokenizer
): StoreResult<MigrationResult> {
  try {
    const hasSchemaMetaTable = db
      .query<{ name: string }, []>(HAS_SCHEMA_META_TABLE)
      .get();
    if (!hasSchemaMetaTable) {
      db.exec(BOOTSTRAP_META_TABLE);
    }

    // Get current version
    const currentVersion = getSchemaVersion(db);

    // Check FTS tokenizer consistency
    const dbTokenizer = getDbFtsTokenizer(db);
    if (dbTokenizer !== null && dbTokenizer !== ftsTokenizer) {
      // Tokenizer mismatch - this requires special handling
      // For now, we error out. EPIC 5 will handle FTS rebuild.
      return err(
        "MIGRATION_FAILED",
        `FTS tokenizer mismatch: DB has '${dbTokenizer}', config has '${ftsTokenizer}'. ` +
          "Run `gno index --rebuild-fts` to recreate FTS index with new tokenizer."
      );
    }

    // Sort migrations by version
    const sorted = [...migrations].sort((a, b) => a.version - b.version);

    // Validate sequential versions
    for (let i = 0; i < sorted.length; i++) {
      const migration = sorted[i];
      if (migration && migration.version !== i + 1) {
        return err(
          "MIGRATION_FAILED",
          `Migration versions must be sequential. Expected ${i + 1}, got ${migration.version}`
        );
      }
    }

    // Filter pending migrations
    const pending = sorted.filter((m) => m.version > currentVersion);

    if (pending.length === 0) {
      return ok({
        applied: [],
        currentVersion,
        ftsTokenizer: dbTokenizer ?? ftsTokenizer,
      });
    }

    // Apply migrations in a transaction
    const applied: number[] = [];
    const transaction = db.transaction(() => {
      for (const migration of pending) {
        migration.up(db, ftsTokenizer);
        setMeta(db, "version", migration.version.toString());
        applied.push(migration.version);
      }

      // Set FTS tokenizer if not already set
      if (dbTokenizer === null) {
        setMeta(db, "fts_tokenizer", ftsTokenizer);
        setMeta(db, "created_at", new Date().toISOString());
      }
    });

    transaction();

    return ok({
      applied,
      currentVersion: pending.at(-1)?.version ?? currentVersion,
      ftsTokenizer,
    });
  } catch (cause) {
    const message =
      cause instanceof Error ? cause.message : "Unknown migration error";
    return err("MIGRATION_FAILED", message, cause);
  }
}

/**
 * Check if FTS tokenizer change is needed.
 */
export function needsFtsRebuild(
  db: Database,
  configTokenizer: FtsTokenizer
): boolean {
  const dbTokenizer = getDbFtsTokenizer(db);
  return dbTokenizer !== null && dbTokenizer !== configTokenizer;
}
