import type { Context, Middleware } from "@wrnexus/core";
import type { Policy, Subject } from "./index.ts";

export interface AuthorizationDecision {
  allowed: boolean;
  reason?: string;
  policy?: string;
  metadata?: Record<string, unknown>;
}
export type DecisionPolicy<S = Subject, R = unknown> = (
  subject: S,
  resource?: R,
) => AuthorizationDecision | Promise<AuthorizationDecision>;

export function allow(reason?: string, metadata?: Record<string, unknown>): AuthorizationDecision {
  return { allowed: true, reason, metadata };
}
export function deny(
  reason = "Forbidden",
  metadata?: Record<string, unknown>,
): AuthorizationDecision {
  return { allowed: false, reason, metadata };
}
export function decision<S, R>(
  name: string,
  policy: Policy<S, R>,
  denial = "Policy denied access",
): DecisionPolicy<S, R> {
  return async (subject, resource) => {
    const allowed = await policy(subject, resource);
    return {
      allowed,
      reason: allowed ? undefined : denial,
      policy: name,
    };
  };
}
export function owner<SubjectType extends Subject, Resource extends Record<string, unknown>>(
  subjectKey: keyof SubjectType = "id",
  resourceKey: keyof Resource | string = "userId",
): DecisionPolicy<SubjectType, Resource> {
  return (subject, resource) => {
    const subjectValue = subject?.[subjectKey];
    const resourceValue = resource?.[resourceKey as keyof Resource];
    // An absent id on either side must never satisfy ownership.
    if (subjectValue === undefined || subjectValue === null)
      return deny("resource ownership required");
    if (resourceValue === undefined || resourceValue === null)
      return deny("resource ownership required");
    return Object.is(subjectValue, resourceValue)
      ? allow("resource owner")
      : deny("resource ownership required");
  };
}
export function anyDecision<S, R>(...policies: DecisionPolicy<S, R>[]): DecisionPolicy<S, R> {
  return async (subject, resource) => {
    const denied: AuthorizationDecision[] = [];
    for (const policy of policies) {
      const result = await policy(subject, resource);
      if (result.allowed) return result;
      denied.push(result);
    }
    return deny(
      denied
        .map((item) => item.reason)
        .filter(Boolean)
        .join("; ") || "No policy allowed access",
    );
  };
}
export function allDecisions<S, R>(...policies: DecisionPolicy<S, R>[]): DecisionPolicy<S, R> {
  return async (subject, resource) => {
    for (const policy of policies) {
      const result = await policy(subject, resource);
      if (!result.allowed) return result;
    }
    return allow("all policies passed");
  };
}
export interface AuthorizeDecisionOptions {
  /**
   * Include `reason` and `policy` in the 403 body. Off by default: policy
   * names describe internal authorization structure and should not reach an
   * unauthenticated caller.
   */
  exposeReason?: boolean;
}

export function authorizeDecision(
  evaluate: (ctx: Context) => AuthorizationDecision | Promise<AuthorizationDecision>,
  options: AuthorizeDecisionOptions = {},
): Middleware {
  return async (ctx, next) => {
    const result = await evaluate(ctx);
    if (result.allowed) return next();
    return Response.json(
      options.exposeReason
        ? { ok: false, error: "Forbidden", reason: result.reason, policy: result.policy }
        : { ok: false, error: "Forbidden" },
      { status: 403 },
    );
  };
}
export function filterAuthorized<S, R>(
  subject: S,
  values: readonly R[],
  policy: Policy<S, R>,
): Promise<R[]> {
  return Promise.all(
    values.map(async (value) => ({ value, allowed: await policy(subject, value) })),
  ).then((results) => results.filter((result) => result.allowed).map((result) => result.value));
}
