type MaybePromise<T> = T | Promise<T>;

/**
 * A policy decision that allows the requested ability.
 */
export type GateAllowedDecision = {
  allowed: true;
};

/**
 * A policy decision that denies the requested ability.
 *
 * Use `reason`, `code`, and `details` to preserve structured denial context for
 * errors, audit logs, and tests.
 */
export type GateDeniedDecision = {
  allowed: false;
  reason?: string;
  code?: string;
  details?: unknown;
};

/**
 * Normalized authorization decision returned by gate inspection.
 */
export type GateDecision = GateAllowedDecision | GateDeniedDecision;

/**
 * Value a policy resolver may return.
 *
 * Returning `true`/`false` is convenient for simple policies. Return
 * `allow()`/`deny(...)` when the caller needs a denial reason, code, or
 * structured details.
 */
export type GatePolicyResult = boolean | GateDecision;

/**
 * Function that decides whether a context can perform an ability.
 *
 * The first argument is always the application context. Policies that operate
 * on a record receive that record as their second argument.
 */
export type PolicyResolver = (
  ...args: never[]
) => MaybePromise<GatePolicyResult>;

/**
 * Typed collection of ability resolvers created by `definePolicy(...)`.
 */
export type PolicyDefinition<
  TPolicies extends Record<string, PolicyResolver> = Record<
    string,
    PolicyResolver
  >,
> = {
  policies: TPolicies;
};

/**
 * Infer the application context type from a policy resolver.
 */
export type PolicyContext<TResolver> = TResolver extends (
  ctx: infer Ctx,
  ...args: never[]
) => MaybePromise<GatePolicyResult>
  ? Ctx
  : never;

/**
 * Infer whether an ability needs a subject argument.
 */
export type PolicySubjectArgs<TResolver> = TResolver extends (
  ...args: infer TArgs
) => MaybePromise<GatePolicyResult>
  ? TArgs extends [unknown, infer Subject]
    ? [subject: Subject]
    : []
  : [];

/**
 * One authorization check accepted by batch gate APIs.
 */
export type PolicyBatchCheck<TPolicies extends readonly PolicyDefinition[]> = {
  [TAbility in keyof PolicyMapFromDefinitions<TPolicies> & string]: readonly [
    ability: TAbility,
    ...subject: PolicySubjectArgs<
      PolicyMapFromDefinitions<TPolicies>[TAbility]
    >,
  ];
}[keyof PolicyMapFromDefinitions<TPolicies> & string];

/**
 * Keyed authorization checks accepted by `inspectMany(...)` and `canMany(...)`.
 */
export type PolicyBatch<TPolicies extends readonly PolicyDefinition[]> = Record<
  string,
  PolicyBatchCheck<TPolicies>
>;

/**
 * Full decision map returned by `inspectMany(...)`.
 */
export type PolicyBatchDecisionMap<
  TBatch extends PolicyBatch<readonly PolicyDefinition[]>,
> = {
  [TKey in keyof TBatch]: GateDecision;
};

/**
 * Boolean decision map returned by `canMany(...)`.
 */
export type PolicyBatchBooleanMap<
  TBatch extends PolicyBatch<readonly PolicyDefinition[]>,
> = {
  [TKey in keyof TBatch]: boolean;
};

/**
 * Gate method that produced a policy decision observation.
 */
export type GateDecisionSource =
  | "can"
  | "inspect"
  | "authorize"
  | "canMany"
  | "inspectMany";

type UnionToIntersection<T> = (
  T extends unknown
    ? (value: T) => void
    : never
) extends (value: infer U) => void
  ? U
  : never;

/**
 * Merge the ability maps from multiple policy definitions.
 */
export type PolicyMapFromDefinitions<
  TPolicies extends readonly PolicyDefinition[],
> = UnionToIntersection<
  TPolicies[number] extends PolicyDefinition<infer TPolicyMap>
    ? TPolicyMap
    : never
>;

/**
 * Infer the application context type shared by a list of policy definitions.
 */
export type PolicyContextFromDefinitions<
  TPolicies extends readonly PolicyDefinition[],
> = PolicyContext<
  PolicyMapFromDefinitions<TPolicies>[keyof PolicyMapFromDefinitions<TPolicies>]
>;

/**
 * Gate bound to a specific application context.
 *
 * Apps commonly attach this to request context as `ctx.gate` so use cases can
 * call `ctx.gate.authorize("posts.update", post)` without passing `ctx` back
 * into every authorization call.
 */
export type BoundGate<TPolicies extends readonly PolicyDefinition[]> = {
  /**
   * Type-only marker that keeps `TPolicies` inferable from a bound gate.
   * Runtime bound gates never implement this method. Method syntax keeps the
   * marker bivariant so readonly and mutable policy tuples stay compatible.
   */
  __policies?(policies: TPolicies): void;
  /**
   * Return only whether the ability is allowed.
   */
  can<TAbility extends keyof PolicyMapFromDefinitions<TPolicies> & string>(
    ability: TAbility,
    ...subject: PolicySubjectArgs<PolicyMapFromDefinitions<TPolicies>[TAbility]>
  ): Promise<boolean>;
  /**
   * Return keyed boolean decisions for several abilities.
   */
  canMany<const TBatch extends PolicyBatch<TPolicies>>(
    checks: TBatch,
  ): Promise<PolicyBatchBooleanMap<TBatch>>;
  /**
   * Return the full allow/deny decision without throwing.
   */
  inspect<TAbility extends keyof PolicyMapFromDefinitions<TPolicies> & string>(
    ability: TAbility,
    ...subject: PolicySubjectArgs<PolicyMapFromDefinitions<TPolicies>[TAbility]>
  ): Promise<GateDecision>;
  /**
   * Return keyed allow/deny decisions for several abilities.
   */
  inspectMany<const TBatch extends PolicyBatch<TPolicies>>(
    checks: TBatch,
  ): Promise<PolicyBatchDecisionMap<TBatch>>;
  /**
   * Return an allowed decision or throw for denied abilities.
   */
  authorize<
    TAbility extends keyof PolicyMapFromDefinitions<TPolicies> & string,
  >(
    ability: TAbility,
    ...subject: PolicySubjectArgs<PolicyMapFromDefinitions<TPolicies>[TAbility]>
  ): Promise<GateAllowedDecision>;
};

/**
 * Context shape contributed by `gate.attach(...)`.
 */
export type GateContext<
  TPolicies extends readonly PolicyDefinition[] = readonly PolicyDefinition[],
> = {
  /**
   * Gate bound to the context that carries it.
   */
  gate: BoundGate<TPolicies>;
};

/**
 * App-facing authorization gate.
 *
 * The gate evaluates app-owned policies. It is not an authentication provider:
 * authenticate at the HTTP boundary first, then pass the resulting actor/user
 * data into policy context.
 */
export type GatePort<
  TContext,
  TPolicies extends readonly PolicyDefinition[] = readonly PolicyDefinition[],
> = {
  /**
   * Bind this gate to a fixed context snapshot.
   *
   * This is the low-level primitive: the returned gate keeps evaluating
   * against the exact object it was bound to. Prefer `attach(...)` for app
   * context assembly so identity changes can never go stale.
   */
  bind(ctx: TContext): BoundGate<TPolicies>;
  /**
   * Attach a live `gate` property to a context object.
   *
   * The gate is exposed through a getter that re-binds against the receiving
   * object on every access, so in-place updates to fields such as `actor` or
   * `tenant` are always observed. The property is non-enumerable on purpose:
   * spreading the context (`{ ...ctx }`) drops the gate instead of silently
   * carrying a stale identity, and the next `ctx.gate` access fails loudly.
   */
  attach<C extends TContext & object>(ctx: C): C & GateContext<TPolicies>;
  /**
   * Return only whether the ability is allowed for a context.
   */
  can<TAbility extends keyof PolicyMapFromDefinitions<TPolicies> & string>(
    ctx: TContext,
    ability: TAbility,
    ...subject: PolicySubjectArgs<PolicyMapFromDefinitions<TPolicies>[TAbility]>
  ): Promise<boolean>;
  /**
   * Return keyed boolean decisions for several abilities.
   */
  canMany<const TBatch extends PolicyBatch<TPolicies>>(
    ctx: TContext,
    checks: TBatch,
  ): Promise<PolicyBatchBooleanMap<TBatch>>;
  /**
   * Return the full allow/deny decision for a context without throwing.
   */
  inspect<TAbility extends keyof PolicyMapFromDefinitions<TPolicies> & string>(
    ctx: TContext,
    ability: TAbility,
    ...subject: PolicySubjectArgs<PolicyMapFromDefinitions<TPolicies>[TAbility]>
  ): Promise<GateDecision>;
  /**
   * Return keyed allow/deny decisions for several abilities.
   */
  inspectMany<const TBatch extends PolicyBatch<TPolicies>>(
    ctx: TContext,
    checks: TBatch,
  ): Promise<PolicyBatchDecisionMap<TBatch>>;
  /**
   * Return an allowed decision or throw for denied abilities.
   */
  authorize<
    TAbility extends keyof PolicyMapFromDefinitions<TPolicies> & string,
  >(
    ctx: TContext,
    ability: TAbility,
    ...subject: PolicySubjectArgs<PolicyMapFromDefinitions<TPolicies>[TAbility]>
  ): Promise<GateAllowedDecision>;
};

/**
 * Hook used to convert a denied decision into an application-specific error.
 */
export type GateDenyHandler<TContext> = (
  decision: GateDeniedDecision,
  params: {
    ctx: TContext;
    ability: string;
    subject?: unknown;
  },
) => MaybePromise<Error | undefined>;

/**
 * Best-effort policy decision observation emitted by a gate.
 *
 * Observers are for diagnostics and audit-style integrations only. They do not
 * participate in authorization control flow and thrown/rejected observer errors
 * are ignored.
 */
export type GateDecisionObservation<TContext> = {
  /**
   * Gate method that produced the decision.
   */
  source: GateDecisionSource;
  /**
   * Key from a batch input object, when the decision came from a batch call.
   */
  batchKey?: string;
  /**
   * Application context used for policy evaluation.
   */
  ctx: TContext;
  /**
   * Ability being evaluated.
   */
  ability: string;
  /**
   * Optional subject passed to the policy.
   */
  subject?: unknown;
  /**
   * Normalized decision, when policy evaluation returned normally.
   */
  decision?: GateDecision;
  /**
   * Error thrown by the policy resolver, when evaluation failed.
   */
  error?: unknown;
  /**
   * Policy evaluation duration, excluding observer work.
   */
  durationMs: number;
  /**
   * Correlation fields copied from the context when present.
   */
  requestId?: string;
  traceId?: string;
  spanId?: string;
  parentSpanId?: string;
  traceparent?: string;
};

/**
 * Best-effort observer called after each gate decision or resolver error.
 */
export type GateDecisionObserver<TContext> = (
  observation: GateDecisionObservation<TContext>,
) => MaybePromise<void>;

/**
 * Options for `createGate(...)`.
 */
export type CreateGateOptions<
  TContext,
  TPolicies extends readonly PolicyDefinition[],
> = {
  /**
   * Policy definitions to register.
   */
  policies: TPolicies;
  /**
   * Optional mapper for denied authorization decisions.
   */
  onDeny?: GateDenyHandler<TContext>;
  /**
   * Optional best-effort observer for policy decisions.
   *
   * This hook is diagnostic only: it cannot change decisions or thrown errors.
   */
  onDecision?: GateDecisionObserver<TContext>;
};

/**
 * Default error thrown by `authorize(...)` when a policy denies access.
 */
export class GateAuthorizationError extends Error {
  readonly code: string;
  readonly status = 403;
  readonly details?: unknown;

  constructor(decision: GateDeniedDecision = deny()) {
    super(decision.reason ?? "Forbidden");
    this.name = "GateAuthorizationError";
    this.code = decision.code ?? "FORBIDDEN";
    this.details = decision.details;
  }
}

/**
 * Create an explicit allow decision.
 *
 * @returns A normalized gate decision with `allowed: true`.
 */
export function allow(): GateAllowedDecision {
  return { allowed: true };
}

/**
 * Create an explicit deny decision.
 *
 * @example
 * ```ts
 * return deny("Only owners can edit this post");
 * ```
 *
 * @param reasonOrDecision - Optional reason string or structured denial data.
 * @returns A normalized gate decision with `allowed: false`.
 */
export function deny(
  reasonOrDecision?: string | Omit<GateDeniedDecision, "allowed">,
): GateDeniedDecision {
  if (typeof reasonOrDecision === "string") {
    return { allowed: false, reason: reasonOrDecision };
  }

  return {
    allowed: false,
    ...reasonOrDecision,
  };
}

/**
 * Define a typed group of authorization policies.
 *
 * Keep policy definitions near the feature that owns the business rule. The
 * returned definition is registered with `createGate(...)`.
 *
 * @example
 * ```ts
 * export const postPolicy = definePolicy({
 *   "posts.update": (ctx, post: Post) => post.authorId === ctx.actor.id,
 * });
 * ```
 *
 * @param policies - Ability resolver map keyed by stable ability names.
 * @returns A typed policy definition for registration with `createGate(...)`.
 */
export function definePolicy<
  const TPolicies extends Record<string, PolicyResolver>,
>(policies: TPolicies): PolicyDefinition<TPolicies> {
  return { policies };
}

/**
 * Create an authorization gate from app-owned policy definitions.
 *
 * Register the gate as a port, then let the server context blueprint attach
 * it: `context: { gate: (ports) => ports.gate, ... }`. Use cases can then call
 * `ctx.gate.authorize(...)` for business authorization.
 *
 * @param options - Policy definitions and optional denial mapper.
 * @returns A gate port that can evaluate registered abilities.
 */
export function createGate<
  TContext,
  const TPolicies extends readonly PolicyDefinition[],
>(
  options: CreateGateOptions<TContext, TPolicies>,
): GatePort<TContext, TPolicies> {
  const registry = new Map<string, PolicyResolver>();
  const observeDecision = createDecisionObserver(options.onDecision);

  for (const definition of options.policies) {
    for (const [ability, resolver] of Object.entries(definition.policies)) {
      if (registry.has(ability)) {
        throw new Error(`Policy ability "${ability}" is already registered.`);
      }

      registry.set(ability, resolver);
    }
  }

  async function evaluate(
    ctx: TContext,
    ability: string,
    subject?: unknown,
    source: GateDecisionSource = "inspect",
    batchKey?: string,
  ): Promise<GateDecision> {
    const startedAt = policyNow();
    const resolver = registry.get(ability);
    if (!resolver) {
      const decision = deny({
        reason: `No policy registered for "${ability}".`,
        code: "POLICY_NOT_FOUND",
      });

      observeDecision({
        source,
        batchKey,
        ctx,
        ability,
        subject,
        decision,
        durationMs: policyNow() - startedAt,
      });

      return decision;
    }

    try {
      const result = await resolver(ctx as never, subject as never);
      const decision = normalizeDecision(result);

      observeDecision({
        source,
        batchKey,
        ctx,
        ability,
        subject,
        decision,
        durationMs: policyNow() - startedAt,
      });

      return decision;
    } catch (error) {
      observeDecision({
        source,
        batchKey,
        ctx,
        ability,
        subject,
        error,
        durationMs: policyNow() - startedAt,
      });

      throw error;
    }
  }

  async function authorize(
    ctx: TContext,
    ability: string,
    subject?: unknown,
  ): Promise<GateAllowedDecision> {
    const decision = await evaluate(ctx, ability, subject, "authorize");
    if (decision.allowed) return decision;

    const thrown = await options.onDeny?.(decision, { ctx, ability, subject });
    throw thrown ?? new GateAuthorizationError(decision);
  }

  async function inspectMany<const TBatch extends PolicyBatch<TPolicies>>(
    ctx: TContext,
    checks: TBatch,
    source: "canMany" | "inspectMany",
  ): Promise<PolicyBatchDecisionMap<TBatch>> {
    const decisions: Record<string, GateDecision> = {};

    for (const [batchKey, check] of Object.entries(checks)) {
      const [ability, subject] = check as unknown as readonly [
        string,
        unknown?,
      ];
      decisions[batchKey] = await evaluate(
        ctx,
        ability,
        subject,
        source,
        batchKey,
      );
    }

    return decisions as PolicyBatchDecisionMap<TBatch>;
  }

  function bind(ctx: TContext): BoundGate<TPolicies> {
    return {
      can: async (ability, ...subject) => gate.can(ctx, ability, ...subject),
      canMany: async (checks) => gate.canMany(ctx, checks),
      inspect: async (ability, ...subject) =>
        gate.inspect(ctx, ability, ...subject),
      inspectMany: async (checks) => gate.inspectMany(ctx, checks),
      authorize: async (ability, ...subject) =>
        gate.authorize(ctx, ability, ...subject),
    };
  }

  function attach<C extends TContext & object>(
    ctx: C,
  ): C & GateContext<TPolicies> {
    Object.defineProperty(ctx, "gate", {
      configurable: true,
      enumerable: false,
      get(this: TContext) {
        return bind(this);
      },
    });

    return ctx as C & GateContext<TPolicies>;
  }

  const gate: GatePort<TContext, TPolicies> = {
    bind,
    attach,
    async can(ctx, ability, ...subject) {
      return (await evaluate(ctx, ability, firstSubject(subject), "can"))
        .allowed;
    },
    async canMany(ctx, checks) {
      const decisions = await inspectMany(ctx, checks, "canMany");
      const result: Record<string, boolean> = {};

      for (const [key, decision] of Object.entries(decisions)) {
        result[key] = decision.allowed;
      }

      return result as PolicyBatchBooleanMap<typeof checks>;
    },
    inspect: async (ctx, ability, ...subject) =>
      evaluate(ctx, ability, firstSubject(subject), "inspect"),
    inspectMany: async (ctx, checks) => inspectMany(ctx, checks, "inspectMany"),
    authorize: async (ctx, ability, ...subject) =>
      authorize(ctx, ability, firstSubject(subject)),
  };

  return gate;
}

function normalizeDecision(result: GatePolicyResult): GateDecision {
  if (typeof result === "boolean") {
    return result ? allow() : deny();
  }

  return result;
}

function firstSubject(subject: readonly unknown[]): unknown {
  return subject[0];
}

function policyNow(): number {
  return typeof performance !== "undefined" ? performance.now() : Date.now();
}

function contextStringField(ctx: unknown, field: string): string | undefined {
  if (!ctx || typeof ctx !== "object") return undefined;
  const value = (ctx as Record<string, unknown>)[field];
  return typeof value === "string" ? value : undefined;
}

function enrichObservation<TContext>(
  observation: GateDecisionObservation<TContext>,
): GateDecisionObservation<TContext> {
  return {
    ...observation,
    requestId: contextStringField(observation.ctx, "requestId"),
    traceId: contextStringField(observation.ctx, "traceId"),
    spanId: contextStringField(observation.ctx, "spanId"),
    parentSpanId: contextStringField(observation.ctx, "parentSpanId"),
    traceparent: contextStringField(observation.ctx, "traceparent"),
  };
}

function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
  return (
    value !== null &&
    (typeof value === "object" || typeof value === "function") &&
    typeof (value as { then?: unknown }).then === "function"
  );
}

function createDecisionObserver<TContext>(
  observer: GateDecisionObserver<TContext> | undefined,
): (observation: GateDecisionObservation<TContext>) => void {
  return (observation) => {
    if (!observer) return;

    try {
      const result = observer(enrichObservation(observation));
      if (isPromiseLike(result)) {
        Promise.resolve(result).catch(() => undefined);
      }
    } catch {
      // Observers are diagnostic only and must not affect authorization.
    }
  };
}
