/**
 * The database driver interface and the `Db` client built on top of it.
 *
 * Adapters (SQLite now; Postgres/MySQL/Mongo later) implement `Driver`. The
 * client adds ergonomics: `all`/`one` optionally map rows through a model's
 * `.parse` (so results match your schema), `tx` wraps work in a transaction, and
 * `createTable` runs a model's DDL. Every query is parameterized.
 */

import { createTableSql, type Dialect } from "./sql.ts";
import type { Model } from "./schema.ts";

export type Row = Record<string, unknown>;

export interface ExecResult {
  changes: number;
  lastInsertId?: number;
}

/** The minimal query surface — the driver itself and each transaction expose it. */
export interface TxHandle {
  /** Run a query returning rows (SELECT). Params are positional. */
  query(sql: string, params?: unknown[]): Promise<Row[]>;
  /** Run a statement (INSERT/UPDATE/DELETE/DDL). */
  exec(sql: string, params?: unknown[]): Promise<ExecResult>;
}

export interface Driver extends TxHandle {
  dialect: Dialect;
  /**
   * Run `fn` inside a transaction on a single reserved connection, committing
   * on success and rolling back on throw. (Pooled drivers must reserve one
   * connection so BEGIN/…/COMMIT don't span connections.)
   */
  transaction<T>(fn: (tx: TxHandle) => Promise<T>): Promise<T>;
  close(): void | Promise<void>;
}

export interface Db {
  driver: Driver;
  /** All matching rows, mapped through `model.parse` when a model is given. */
  all<T = Row>(sql: string, params?: unknown[], model?: Model<T>): Promise<T[]>;
  /** The first matching row (or null), mapped through `model.parse`. */
  one<T = Row>(sql: string, params?: unknown[], model?: Model<T>): Promise<T | null>;
  exec(sql: string, params?: unknown[]): Promise<ExecResult>;
  /** Run `fn` inside a transaction; rolls back if it throws. */
  tx<T>(fn: (db: Db) => Promise<T>): Promise<T>;
  /** Create a table from its model (`CREATE TABLE IF NOT EXISTS`). */
  createTable(model: Model): Promise<void>;
  close(): void | Promise<void>;
}

interface DbLifecycle {
  closing: boolean;
  active: Set<Promise<unknown>>;
  closePromise?: Promise<void>;
}

/** Build a `Db` over a query runner (the driver at top level, or a transaction). */
function dbOver(
  runner: TxHandle,
  driver: Driver,
  lifecycle: DbLifecycle,
  transactionScope = false,
): Db {
  function run<T>(operation: () => Promise<T>): Promise<T> {
    if (lifecycle.closing && !transactionScope) {
      return Promise.reject(new Error("WRN-DB-CLOSED: database is closing or closed"));
    }
    const promise = Promise.resolve().then(operation);
    lifecycle.active.add(promise);
    void promise.finally(() => lifecycle.active.delete(promise)).catch(() => {});
    return promise;
  }
  const db: Db = {
    driver,
    async all(sql, params = [], model) {
      const rows = await run(() => runner.query(sql, params));
      return (model ? rows.map((r) => model.parse(r)) : rows) as never;
    },
    async one(sql, params = [], model) {
      const rows = await db.all(sql, params, model as never);
      return (rows[0] ?? null) as never;
    },
    exec(sql, params = []) {
      return run(() => runner.exec(sql, params));
    },
    async tx(fn) {
      // Top level opens a real transaction; inside one, reuse the current tx.
      if (runner === driver)
        return run(() => driver.transaction((tx) => fn(dbOver(tx, driver, lifecycle, true))));
      return fn(db);
    },
    async createTable(model) {
      await run(() => runner.exec(createTableSql(model, driver.dialect)));
    },
    close() {
      if (lifecycle.closePromise) return lifecycle.closePromise;
      lifecycle.closing = true;
      lifecycle.closePromise = (async () => {
        await Promise.allSettled([...lifecycle.active]);
        await driver.close();
      })();
      return lifecycle.closePromise;
    },
  };
  return db;
}

/** Build a `Db` client from a driver. */
export function createDb(driver: Driver): Db {
  return dbOver(driver, driver, { closing: false, active: new Set() });
}
