/**
 * `wrnexus authz <cmd>` — authorization catalog tooling.
 *
 *   wrnexus authz list       print every registered permission, role, and policy
 *   wrnexus authz generate   write app/authz/permissions.gen.ts type unions
 *   wrnexus authz init [--dialect=sqlite|postgres|mysql]
 *                             scaffold the assignment-table migration
 */

import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { buildRouter } from "@wrnexus/router";
import {
  generatePermissionTypes,
  mergeCatalogs,
  type AuthzCatalog,
  type AuthzModule,
  type CatalogSource,
} from "@wrnexus/authz";
import { authzMigrationSql } from "@wrnexus/authz/db";
import type { Dialect } from "@wrnexus/db";

const USAGE = "usage: wrnexus authz <list|generate|init>";
const DIALECTS = ["sqlite", "postgres", "mysql"] as const;

/** Import every app/authz declaration and merge it into one catalog. */
export async function loadAuthzCatalog(appDir: string): Promise<AuthzCatalog> {
  const router = buildRouter(appDir);
  const sources: CatalogSource[] = [];
  for (const entry of router.authz) {
    const imported = (await import(pathToFileURL(entry.file).href)) as {
      default?: AuthzModule;
    };
    if (!imported.default) {
      console.warn(`[wrnexus] ${entry.file} has no default export; skipping`);
      continue;
    }
    sources.push({ source: entry.file, module: imported.default });
  }
  return mergeCatalogs(sources);
}

/** Print a message and exit non-zero, matching db.ts's convention for user-facing
 *  CLI errors: never throw, so index.ts's generic `main().catch` handler (which
 *  prints the raw error, stack and all) is never reached for an expected failure. */
function fail(message: string): never {
  console.error(message);
  process.exit(1);
}

// Same leading-digit extraction as db/migrate.ts's `nextNumber`: a fixed
// `slice(0, 4)` would undercount once a migration number grows past 9999.
function nextMigrationNumber(dir: string): string {
  if (!existsSync(dir)) return "0001";
  let max = 0;
  for (const name of readdirSync(dir)) {
    const match = /^(\d+)/.exec(name);
    if (match) max = Math.max(max, Number(match[1]));
  }
  return String(max + 1).padStart(4, "0");
}

/** Parse `--dialect=<value>` from CLI args. Defaults to sqlite; rejects unknown values. */
function resolveDialect(args: string[]): Dialect {
  const flag = args.find((arg) => arg.startsWith("--dialect="));
  if (!flag) return "sqlite";
  const value = flag.split("=")[1] ?? "";
  if ((DIALECTS as readonly string[]).includes(value)) return value as Dialect;
  return fail(`Unrecognised --dialect='${value}'. Use one of: ${DIALECTS.join(", ")}.`);
}

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

  switch (sub) {
    case "list": {
      const catalog = await loadAuthzCatalog(appDir);
      console.log(`Permissions (${catalog.permissions.size}):`);
      for (const [id, meta] of [...catalog.permissions].sort()) {
        const tags = [meta.risk && `risk=${meta.risk}`, meta.public && "public"]
          .filter(Boolean)
          .join(" ");
        console.log(`  ${id}${meta.title ? ` — ${meta.title}` : ""}${tags ? `  [${tags}]` : ""}`);
      }
      console.log(`\nRoles (${catalog.roles.size}):`);
      for (const [name, grants] of [...catalog.roles].sort()) {
        console.log(`  ${name} → ${grants.join(", ") || "(nothing)"}`);
      }
      console.log(`\nPolicies (${catalog.policies.size}):`);
      for (const name of [...catalog.policies.keys()].sort()) {
        const bound = [...catalog.bindings]
          .filter(([, names]) => names.includes(name))
          .map(([permission]) => permission);
        console.log(`  ${name}${bound.length ? ` → ${bound.join(", ")}` : "  (unbound)"}`);
      }
      return;
    }

    case "generate": {
      const catalog = await loadAuthzCatalog(appDir);
      const target = join(appDir, "authz", "permissions.gen.ts");
      mkdirSync(join(appDir, "authz"), { recursive: true });
      writeFileSync(target, generatePermissionTypes(catalog), "utf8");
      console.log(
        `Wrote ${target} (${catalog.permissions.size} permissions, ${catalog.roles.size} roles)`,
      );
      return;
    }

    case "init": {
      const dialect = resolveDialect(args);
      const dir = join(appDir, "db", "migrations");
      mkdirSync(dir, { recursive: true });
      const { up, down } = authzMigrationSql(dialect);
      const file = join(dir, `${nextMigrationNumber(dir)}_authz_tables.sql`);
      // up/down are statement LISTS; interpolating the arrays directly would
      // comma-join them into one unparseable statement.
      const block = (statements: string[]) => statements.map((s) => `${s};`).join("\n\n");
      writeFileSync(file, `-- +up\n${block(up)}\n\n-- +down\n${block(down)}\n`, "utf8");
      console.log(`Wrote ${file}`);
      console.log("Run `wrnexus db migrate` to apply it.");
      return;
    }

    default:
      fail(USAGE);
  }
}
