/**
 * @wrnexus/authz — authorization: role-based (RBAC), policy-based (PBAC), and
 * attribute-based (ABAC). Compose freely; all three reduce to a boolean check
 * plus an `authorize()` guard middleware.
 *
 *   const rbac = defineRbac({ admin: ["*"], editor: ["post:read", "post:write"] });
 *   rbac.can(user, "post:write");
 *
 *   // PBAC/ABAC: a policy is a predicate over subject + resource + attributes
 *   const ownsPost: Policy<User, Post> = (u, post) => u.id === post.authorId;
 *   authorize((ctx) => ownsPost(ctx.user, resource))  // middleware
 */

import type { Context, Middleware } from "@wrnexus/core";

export interface Subject {
  id?: string;
  roles?: string[];
  [attribute: string]: unknown;
}

// --- RBAC ------------------------------------------------------------------

export interface Rbac {
  /** True if any of the subject's roles grants `permission` (supports "*" and "ns:*"). */
  can(subject: Subject | undefined, permission: string): boolean;
  /** All permissions granted to a set of roles. */
  permissionsFor(roles: string[]): Set<string>;
}

/** Build an RBAC checker from a role → permissions map. */
export function defineRbac(roles: Record<string, string[]>): Rbac {
  const grants = (role: string, seen = new Set<string>()): string[] => {
    if (seen.has(role)) return [];
    seen.add(role);
    const out: string[] = [];
    for (const p of roles[role] ?? []) {
      // A permission that names another role (prefixed "role:") inherits it.
      if (p.startsWith("role:")) out.push(...grants(p.slice(5), seen));
      else out.push(p);
    }
    return out;
  };
  const permissionsFor = (subjectRoles: string[]): Set<string> => {
    const set = new Set<string>();
    for (const r of subjectRoles) for (const p of grants(r)) set.add(p);
    return set;
  };
  return {
    permissionsFor,
    can(subject, permission) {
      if (!subject?.roles?.length) return false;
      const perms = permissionsFor(subject.roles);
      if (perms.has("*") || perms.has(permission)) return true;
      // Namespace wildcards at every depth: "post:*" and "post:comment:*" both
      // grant "post:comment:delete".
      for (let at = permission.indexOf(":"); at !== -1; at = permission.indexOf(":", at + 1)) {
        if (perms.has(`${permission.slice(0, at)}:*`)) return true;
      }
      return false;
    },
  };
}

/** True if the subject has ALL of the given roles. */
export function hasRole(subject: Subject | undefined, ...required: string[]): boolean {
  const roles = new Set(subject?.roles ?? []);
  return required.every((r) => roles.has(r));
}

// --- PBAC / ABAC -----------------------------------------------------------

/** A policy predicate: subject (+ optional resource/attributes) → allowed. */
export type Policy<S = Subject, R = unknown> = (
  subject: S,
  resource?: R,
) => boolean | Promise<boolean>;

/** Combine policies: allow if ANY passes (OR). */
export function any<S, R>(...policies: Policy<S, R>[]): Policy<S, R> {
  return async (s, r) => {
    for (const p of policies) if (await p(s, r)) return true;
    return false;
  };
}

/** Combine policies: allow only if ALL pass (AND). */
export function all<S, R>(...policies: Policy<S, R>[]): Policy<S, R> {
  return async (s, r) => {
    for (const p of policies) if (!(await p(s, r))) return false;
    return true;
  };
}

/** ABAC helper: allow when an attribute matches (equality or predicate). */
export function attr<S extends Subject>(
  name: string,
  match: unknown | ((value: unknown) => boolean),
): Policy<S> {
  return (subject) => {
    const value = subject?.[name];
    return typeof match === "function"
      ? (match as (v: unknown) => boolean)(value)
      : value === match;
  };
}

// --- Guards (middleware) ---------------------------------------------------

function forbidden(): Response {
  return Response.json({ ok: false, error: "Forbidden" }, { status: 403 });
}

/** Guard a route with a policy over `ctx` (reads `ctx.user` as the subject). */
export function authorize(policy: (ctx: Context) => boolean | Promise<boolean>): Middleware {
  return async (ctx, next) => ((await policy(ctx)) ? next() : forbidden());
}

/** Guard requiring one of the given roles. */
export function requireRole(...roles: string[]): Middleware {
  return authorize((ctx) => {
    const subject = ctx.user as Subject | undefined;
    const have = new Set(subject?.roles ?? []);
    return roles.some((r) => have.has(r));
  });
}

/** Guard requiring an RBAC permission. */
export function requirePermission(rbac: Rbac, permission: string): Middleware {
  return authorize((ctx) => rbac.can(ctx.user as Subject | undefined, permission));
}
export {
  allow,
  deny,
  decision,
  owner,
  anyDecision,
  allDecisions,
  authorizeDecision,
  filterAuthorized,
} from "./advanced.ts";
export type { AuthorizationDecision, DecisionPolicy } from "./advanced.ts";
export { defineAuthz } from "./registry.ts";
export { mergeCatalogs, emptyCatalog } from "./catalog.ts";
export type { CatalogSource } from "./catalog.ts";
export { setAuthzCatalog, getAuthzCatalog, hasAuthzCatalog } from "./client.ts";
export { memoryPermissionStore, cachedPermissionStore, scopeKey } from "./store.ts";
export type { PermissionStore, CachedPermissionStore, CacheOptions, GrantEffect } from "./store.ts";
export { memoryAuditSink, consoleAuditSink, safeRecord } from "./audit.ts";
export type { AuthzAuditEvent, AuthzAuditSink, MemoryAuditSink } from "./audit.ts";
export { createAuthzResolver, expandRoles, permissionMatches, deniedBy } from "./engine.ts";
export type { AuthzResolver, AuthzResolverOptions, DecideInput } from "./engine.ts";
export {
  authzMiddleware,
  can,
  decideFor,
  guardPermission,
  filterCan,
  AUTHZ_LOCALS_KEY,
} from "./middleware.ts";
export type { GuardOptions } from "./middleware.ts";
export type {
  AuthzScope,
  AuthzCatalog,
  AuthzModule,
  AttributeMeta,
  PermissionMeta,
  SubjectAssignments,
} from "./types.ts";
export type { AuthorizeDecisionOptions } from "./advanced.ts";
export { generatePermissionTypes } from "./codegen.ts";
