import type { Db, Dialect } from "@wrnexus/db";
import { authzMigrationSql } from "./migrations.ts";
import { scopeKey, type PermissionStore } from "./store.ts";
import type { AuthzScope, SubjectAssignments } from "./types.ts";

// Re-exported so `@wrnexus/authz/db` is the single entry point for everything
// database-related, including the DDL the CLI scaffolds.
export { authzMigrationSql } from "./migrations.ts";

/**
 * Create the tables if absent. Production apps should use a real migration.
 *
 * The UNIQUE constraints in this DDL are load-bearing beyond deduplication:
 * `grant`/`assignRole` below use ON CONFLICT / ON DUPLICATE KEY, which infers
 * its conflict target from them. A hand-rolled migration that recreates these
 * tables without `_wrn_authz_grant_unique` (or the assignment equivalent)
 * will make those methods reject outright, where the old delete-then-insert
 * approach would have silently worked without the constraint.
 */
export async function ensureAuthzTables(
  db: Db,
  dialect: Dialect = db.driver.dialect,
): Promise<void> {
  for (const statement of authzMigrationSql(dialect).up) await db.exec(statement);
}

/** Positional placeholder for the dialect: postgres numbers them, others use "?". */
function ph(dialect: Dialect, index: number): string {
  return dialect === "postgres" ? `$${index}` : "?";
}

export function dbPermissionStore(db: Db): PermissionStore {
  const dialect = db.driver.dialect;
  const p = (n: number) => ph(dialect, n);
  // Single-statement upserts. A transaction here would be worse than useless:
  // the drivers run BEGIN on one shared connection, so an open transaction
  // swallows any concurrent write from another method and discards it on
  // rollback - a revoke would resolve successfully while the role survived.
  const onConflict = (columns: string, update: string) =>
    dialect === "mysql"
      ? ` ON DUPLICATE KEY UPDATE ${update}`
      : ` ON CONFLICT (${columns}) DO UPDATE SET ${update}`;
  const onConflictIgnore = (columns: string) =>
    dialect === "mysql"
      ? " ON DUPLICATE KEY UPDATE id = id"
      : ` ON CONFLICT (${columns}) DO NOTHING`;

  return {
    async assignmentsFor(subjectId: string, scope?: AuthzScope): Promise<SubjectAssignments> {
      const key = scopeKey(scope);
      // A request inside a tenant sees global rows plus that tenant's rows.
      const roleRows = await db.all<{ role: string }>(
        `SELECT role FROM _wrn_authz_assignment WHERE subject_id = ${p(1)} AND (scope = '' OR scope = ${p(2)})`,
        [subjectId, key],
      );
      const grantRows = await db.all<{ permission: string; effect: string }>(
        `SELECT permission, effect FROM _wrn_authz_grant WHERE subject_id = ${p(1)} AND (scope = '' OR scope = ${p(2)})`,
        [subjectId, key],
      );
      return {
        roles: roleRows.map((row) => row.role),
        grants: grantRows.filter((r) => r.effect === "allow").map((r) => r.permission),
        // Anything that is not literally "allow" counts as a deny, so a
        // corrupted or mis-cased effect fails closed rather than vanishing.
        denies: grantRows.filter((r) => r.effect !== "allow").map((r) => r.permission),
      };
    },

    async assignRole(subjectId, role, scope) {
      await db.exec(
        `INSERT INTO _wrn_authz_assignment (subject_id, scope, role) VALUES (${p(1)}, ${p(2)}, ${p(3)})` +
          onConflictIgnore("subject_id, scope, role"),
        [subjectId, scopeKey(scope), role],
      );
    },

    async revokeRole(subjectId, role, scope) {
      await db.exec(
        `DELETE FROM _wrn_authz_assignment WHERE subject_id = ${p(1)} AND scope = ${p(2)} AND role = ${p(3)}`,
        [subjectId, scopeKey(scope), role],
      );
    },

    async grant(subjectId, permission, effect, scope) {
      // `VALUES(effect)` is deprecated as of MySQL 8.0.20 in favour of the
      // row-alias form (`... VALUES (...) AS new ON DUPLICATE KEY UPDATE
      // effect = new.effect`). Noted here rather than migrated because there
      // is no MySQL server in CI to catch its eventual removal.
      await db.exec(
        `INSERT INTO _wrn_authz_grant (subject_id, scope, permission, effect) VALUES (${p(1)}, ${p(2)}, ${p(3)}, ${p(4)})` +
          onConflict(
            "subject_id, scope, permission",
            "effect = " + (dialect === "mysql" ? "VALUES(effect)" : "excluded.effect"),
          ),
        [subjectId, scopeKey(scope), permission, effect],
      );
    },

    async revokeGrant(subjectId, permission, scope) {
      await db.exec(
        `DELETE FROM _wrn_authz_grant WHERE subject_id = ${p(1)} AND scope = ${p(2)} AND permission = ${p(3)}`,
        [subjectId, scopeKey(scope), permission],
      );
    },

    async listSubjects(scope) {
      const key = scopeKey(scope);
      const rows = await db.all<{ subject_id: string }>(
        `SELECT subject_id FROM _wrn_authz_assignment WHERE scope = ${p(1)} ` +
          `UNION SELECT subject_id FROM _wrn_authz_grant WHERE scope = ${p(2)}`,
        [key, key],
      );
      return [...new Set(rows.map((row) => row.subject_id))];
    },
  };
}
