/**
 * `wrnexus db <cmd> [--db=<name>]` — database migrations & tooling.
 *
 *   wrnexus db new <name> [--from-models]   scaffold a migration (from TS models)
 *   wrnexus db migrate                      apply all pending migrations
 *   wrnexus db rollback                     revert the last applied migration
 *   wrnexus db status                       list applied / pending migrations
 *   wrnexus db generate                     regenerate typed queries
 *   wrnexus db seed                         run the seed script
 *   wrnexus db studio [table]               inspect tables
 *
 * Without `--db`, commands target the DEFAULT database (`db` in wrnexus.config.ts),
 * with files under `app/db/`. With `--db=<name>`, they target the named database
 * (`databases.<name>`), with files under `app/db/<name>/`.
 */

import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { loadAppConfig, type AppConfig } from "@wrnexus/styles";
import {
  generateQueriesFile,
  analyzeMigrations,
  loadMigrations,
  migrate,
  parseQueries,
  rollback,
  scaffoldMigration,
  status,
  type Dialect,
  type Model,
  type ModelRef,
} from "@wrnexus/db";
import { connectFromConfig } from "@wrnexus/db/connect";

function dialectOf(driver: string | undefined): Dialect {
  return driver === "postgres" || driver === "mysql" ? driver : "sqlite";
}

/** The directory holding a database's schema/migrations/queries. */
function dbBaseOf(appDir: string, dbName: string | null): string {
  return dbName ? join(appDir, "db", dbName) : join(appDir, "db");
}

/** List user tables for the connected database (dialect-aware introspection). */
async function listTables(db: import("@wrnexus/db").Db): Promise<string[]> {
  const dialect = db.driver.dialect;
  const sql =
    dialect === "postgres"
      ? "SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename"
      : dialect === "mysql"
        ? "SELECT table_name AS name FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY table_name"
        : "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name";
  const rows = await db.all<{ name: string }>(sql);
  return rows.map((r) => r.name).filter((n) => n !== "_wire_migrations");
}

function isModel(value: unknown): value is Model {
  const m = value as Partial<Model> | null;
  return (
    !!m &&
    typeof m === "object" &&
    typeof m.name === "string" &&
    typeof m.parse === "function" &&
    typeof m.describe === "function" &&
    !!m.columns
  );
}

/** Load model refs from a database's `schema.ts` (dbBase is app/db or app/db/<name>). */
async function loadModelRefs(dbBase: string): Promise<ModelRef[]> {
  const schemaFile = join(dbBase, "schema.ts");
  if (!existsSync(schemaFile)) return [];
  const mod = (await import(pathToFileURL(schemaFile).href)) as Record<string, unknown>;
  return Object.entries(mod)
    .filter(([, value]) => isModel(value))
    .map(([varName, model]) => ({ varName, model: model as Model }));
}

async function loadModels(dbBase: string): Promise<Model[]> {
  return (await loadModelRefs(dbBase)).map((r) => r.model);
}

/**
 * Regenerate one database's `queries.gen.ts` from its `queries/*.sql`. Returns the
 * number of queries generated, or -1 if there is no queries directory. `dbName`
 * selects a named database (files under app/db/<name>/); null = the default.
 */
export async function regenerateQueries(
  appDir: string,
  driver: string | undefined,
  dbName: string | null = null,
): Promise<number> {
  const dbBase = dbBaseOf(appDir, dbName);
  const queriesDir = join(dbBase, "queries");
  if (!existsSync(queriesDir)) return -1;
  const queries = readdirSync(queriesDir)
    .filter((f) => f.endsWith(".sql"))
    .flatMap((f) => parseQueries(readFileSync(join(queriesDir, f), "utf8")));
  const refs = await loadModelRefs(dbBase);
  const code = generateQueriesFile(queries, refs, dialectOf(driver));
  writeFileSync(join(dbBase, "queries.gen.ts"), code, "utf8");
  return queries.length;
}

/** Regenerate typed queries for the default database and every named one. */
export async function regenerateAllQueries(appDir: string, config: AppConfig): Promise<void> {
  await regenerateQueries(appDir, config.db?.driver, null);
  for (const [name, cfg] of Object.entries(config.databases ?? {})) {
    await regenerateQueries(appDir, cfg.driver, name);
  }
}

export async function runDbCommand(
  appRoot: string,
  sub: string | undefined,
  args: string[],
): Promise<void> {
  const root = resolve(appRoot);
  const appDir = join(root, "app");
  const config = await loadAppConfig(root);

  // --db=<name> targets a named database + its app/db/<name>/ folder.
  const dbFlag = args.find((a) => a.startsWith("--db="));
  const dbName = dbFlag ? (dbFlag.split("=")[1] ?? "") : null;
  const dbConfig = dbName ? config.databases?.[dbName] : config.db;
  const dbBase = dbBaseOf(appDir, dbName);
  const migrationsDir = join(dbBase, "migrations");
  const label = dbName ? ` (db: ${dbName})` : "";
  const safetyIssues = analyzeMigrations(loadMigrations(migrationsDir));

  if (sub === "check") {
    if (!safetyIssues.length) console.log(`✓ Migration rollout safety check passed${label}.`);
    for (const issue of safetyIssues) {
      console[issue.severity === "error" ? "error" : "warn"](
        `${issue.code} ${issue.migration}: ${issue.statement}\n  ${issue.recommendation}`,
      );
    }
    if (safetyIssues.some((issue) => issue.severity === "error")) process.exitCode = 1;
    return;
  }

  if (dbName && !config.databases?.[dbName]) {
    console.error(
      `No database named '${dbName}' in wrnexus.config.ts. ` +
        `Add it under databases: { ${dbName}: { driver, url } }.`,
    );
    process.exit(1);
  }

  if (sub === "new") {
    const name = args.find((a) => !a.startsWith("--")) ?? "migration";
    const fromModels = args.includes("--from-models");
    const models = fromModels ? await loadModels(dbBase) : [];
    if (fromModels && models.length === 0) {
      console.warn(`[wrnexus] no models found in ${join(dbBase, "schema.ts")}`);
    }
    console.log(
      `✓ Created ${scaffoldMigration(migrationsDir, name, dialectOf(dbConfig?.driver), models)}`,
    );
    return;
  }

  if (sub === "generate") {
    const count = await regenerateQueries(appDir, dbConfig?.driver, dbName);
    if (count < 0) console.warn(`[wrnexus] no ${join(dbBase, "queries")} directory`);
    else console.log(`✓ Generated ${join(dbBase, "queries.gen.ts")} (${count} queries)`);
    return;
  }

  if (!dbConfig) {
    console.error(
      "No `db` config in wrnexus.config.ts. Add: db: { driver: 'sqlite', url: 'file:./dev.db' }",
    );
    process.exit(1);
  }

  const db = connectFromConfig(dbConfig, root);
  try {
    switch (sub) {
      case "seed": {
        const seedFile = join(dbBase, "seed.ts");
        if (!existsSync(seedFile)) {
          console.warn(`[wrnexus] no ${seedFile}`);
          break;
        }
        const mod = (await import(pathToFileURL(seedFile).href)) as {
          default?: (db: unknown) => Promise<void>;
          seed?: (db: unknown) => Promise<void>;
        };
        const fn = mod.default ?? mod.seed;
        if (typeof fn !== "function") {
          console.error(`${seedFile} must export a default async function(db).`);
          process.exit(1);
        }
        await fn(db);
        console.log(`✓ Seeded${label}`);
        break;
      }
      case "studio": {
        const target = args.find((a) => !a.startsWith("--"));
        const tables = await listTables(db);
        if (target) {
          if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(target)) {
            console.error(`Invalid table name: ${target}`);
            process.exit(1);
          }
          if (!tables.includes(target)) {
            console.error(`No such table: ${target}. Available: ${tables.join(", ") || "(none)"}`);
            process.exit(1);
          }
          const rows = await db.all(`SELECT * FROM ${target} LIMIT 50`);
          console.log(`\n${target}${label} — first ${rows.length} row(s):`);
          console.table(rows);
        } else if (tables.length === 0) {
          console.log(
            `No tables found${label}. Run \`wrnexus db migrate${dbFlag ? " " + dbFlag : ""}\` first.`,
          );
        } else {
          console.log(`\nTables${label}:`);
          for (const t of tables) {
            const count = await db.one<{ n: number }>(`SELECT COUNT(*) AS n FROM ${t}`);
            console.log(`  ${t.padEnd(24)} ${Number(count?.n ?? 0)} rows`);
          }
          console.log("\nInspect one with: wrnexus db studio <table>");
        }
        break;
      }
      case "migrate": {
        const pending = new Set(
          (await status(db, migrationsDir))
            .filter((migration) => !migration.applied)
            .map((migration) => migration.name),
        );
        const pendingIssues = safetyIssues.filter((issue) => pending.has(issue.migration));
        const blockers = pendingIssues.filter((issue) => issue.severity === "error");
        for (const issue of pendingIssues.filter((item) => item.severity === "warning")) {
          console.warn(`${issue.code} ${issue.migration}: ${issue.recommendation}`);
        }
        if (blockers.length && !args.includes("--allow-breaking")) {
          throw new Error(
            `WRN-DB-UNSAFE-MIGRATION: ${blockers.length} breaking rollout operation(s) found. Run 'wrnexus db check' and use an expand/contract migration; --allow-breaking explicitly overrides this gate.`,
          );
        }
        const applied = await migrate(db, migrationsDir);
        console.log(
          applied.length
            ? `✓ Applied ${applied.length}${label}:\n  ${applied.join("\n  ")}`
            : `Already up to date${label}.`,
        );
        break;
      }
      case "rollback": {
        const name = await rollback(db, migrationsDir);
        console.log(name ? `✓ Rolled back ${name}${label}` : `Nothing to roll back${label}.`);
        break;
      }
      case "status": {
        const rows = await status(db, migrationsDir);
        if (rows.length === 0) console.log(`No migrations found in ${migrationsDir}.`);
        else for (const r of rows) console.log(`  [${r.applied ? "x" : " "}] ${r.name}`);
        break;
      }
      default:
        console.error(
          "Usage: wrnexus db <check|migrate|rollback|status|generate|seed|studio [table]|new [name] [--from-models]> [--db=<name>]",
        );
        process.exit(1);
    }
  } finally {
    await db.close();
  }
}
