import type { InferOutput, StandardSchema } from "../../contracts/index.js";
import { AuthUnauthorizedError } from "../../ports/index.js";
import type { HttpRequestLike, RouteHook } from "../types.js";

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

/**
 * Arguments passed to auth route-hook callbacks.
 */
export type AuthHookArgs<
  Ctx,
  HeadersSchema extends StandardSchema | undefined = undefined,
> = {
  /**
   * Framework-neutral request.
   */
  req: HttpRequestLike;
  /**
   * Current route handler context.
   */
  ctx: Ctx;
  /**
   * Matched contract metadata and schemas.
   */
  contract: {
    metadata?: Record<string, unknown>;
  };
  /**
   * Parsed path parameters.
   */
  path: unknown;
  /**
   * Parsed query parameters.
   */
  query: unknown;
  /**
   * Hook-owned request headers.
   *
   * When the auth hooks declare a `headers` schema, this is that schema's
   * output parsed from the raw lowercase request header record. Without a
   * schema it is the raw lowercase header record itself, so auth resolution
   * never depends on each route's contract header schema.
   */
  headers: HeadersSchema extends StandardSchema
    ? InferOutput<HeadersSchema>
    : Record<string, string>;
  /**
   * Parsed request body.
   */
  body: unknown;
};

/**
 * Options for route-scoped auth hooks.
 *
 * Auth additions must not include `gate`: the server re-attaches the gate
 * declared by the context blueprint after every hook, so elevated identities
 * are authorized against the updated context automatically.
 */
export type AuthHooksOptions<
  Ctx,
  AddedCtx extends object & { gate?: never },
  HeadersSchema extends StandardSchema | undefined = undefined,
> = {
  /**
   * Hook name prefix used in diagnostics.
   */
  name?: string;
  /**
   * Optional Standard Schema for the credential headers this hook reads.
   *
   * The schema is validated against the raw lowercase request header record
   * before `resolve` runs. On `required()` hooks a schema failure rejects the
   * request with a framework-owned 401; on `optional()` hooks a schema failure
   * skips auth resolution; `public()` hooks never parse headers.
   */
  headers?: HeadersSchema;
  /**
   * Resolve authenticated context additions for the current request.
   *
   * Return `null` when the request is unauthenticated. Required hooks will
   * reject that request; optional hooks will add no auth context.
   */
  resolve: (
    args: AuthHookArgs<Ctx, HeadersSchema>,
  ) => MaybePromise<AddedCtx | null>;
};

/**
 * Route-scoped auth hook set.
 */
export type AuthRouteHooks<Ctx, AddedCtx extends object & { gate?: never }> = {
  /**
   * Mark a route as intentionally public.
   */
  public: () => RouteHook<Ctx, Record<string, never>>;
  /**
   * Resolve auth when present and add optional auth fields to the handler ctx.
   */
  optional: () => RouteHook<Ctx, Partial<AddedCtx>>;
  /**
   * Require auth and add authenticated fields to the handler ctx.
   */
  required: () => RouteHook<Ctx, AddedCtx>;
};

function rawRequestHeaders(req: HttpRequestLike): Record<string, string> {
  const record: Record<string, string> = {};
  req.headers.forEach((value, key) => {
    record[key.toLowerCase()] = value;
  });
  return record;
}

type ParsedAuthHeaders = { ok: true; headers: unknown } | { ok: false };

async function parseAuthHeaders(
  schema: StandardSchema | undefined,
  req: HttpRequestLike,
): Promise<ParsedAuthHeaders> {
  const raw = rawRequestHeaders(req);
  if (!schema) {
    return { ok: true, headers: raw };
  }

  const result = await schema["~standard"].validate(raw);
  if (result.issues) {
    return { ok: false };
  }

  return { ok: true, headers: result.value };
}

/**
 * Create route-scoped authentication hooks.
 *
 * The outer call binds the app context; the inner call takes auth options and
 * infers the added context from `resolve`:
 *
 * ```ts
 * const auth = createAuthHooks<AppContext>()({
 *   resolve: ({ ctx }) => (ctx.auth ? { user: ctx.auth.user } : null),
 * });
 * ```
 *
 * Use `auth.required()` on routes that require an authenticated actor and
 * `auth.optional()` where handlers can use auth when present. The returned
 * route hooks enrich handler `ctx`; business authorization still belongs in
 * feature policies or use cases.
 *
 * Declare a `headers` schema when credentials live in request headers. The
 * hook validates the raw lowercase header record itself, so `resolve` receives
 * typed headers without contract casts and a `required()` hook rejects
 * missing or malformed credentials with a framework-owned 401.
 *
 * @returns A function that takes auth options and returns public, optional,
 * and required route-hook factories.
 */
export function createAuthHooks<Ctx>() {
  return <
    AddedCtx extends object & { gate?: never },
    HeadersSchema extends StandardSchema | undefined = undefined,
  >(
    options: AuthHooksOptions<Ctx, AddedCtx, HeadersSchema>,
  ): AuthRouteHooks<Ctx, AddedCtx> => {
    const name = options.name ?? "auth";

    const toAuthArgs = (
      args: Parameters<RouteHook<Ctx, object>["resolve"]>[0],
      headers: unknown,
    ): AuthHookArgs<Ctx, HeadersSchema> =>
      ({
        req: args.req,
        ctx: args.ctx,
        contract: args.contract,
        path: args.path,
        query: args.query,
        headers,
        body: args.body,
      }) as AuthHookArgs<Ctx, HeadersSchema>;

    return {
      public: () => ({
        name: `${name}.public`,
        resolve: () => undefined,
      }),
      optional: () => ({
        name: `${name}.optional`,
        resolve: async (args) => {
          const parsed = await parseAuthHeaders(options.headers, args.req);
          if (!parsed.ok) {
            return undefined;
          }

          const additions = await options.resolve(
            toAuthArgs(args, parsed.headers),
          );

          return additions ?? undefined;
        },
      }),
      required: () => ({
        name: `${name}.required`,
        resolve: async (args) => {
          const parsed = await parseAuthHeaders(options.headers, args.req);
          if (!parsed.ok) {
            throw new AuthUnauthorizedError();
          }

          const additions = await options.resolve(
            toAuthArgs(args, parsed.headers),
          );
          if (!additions) {
            throw new AuthUnauthorizedError();
          }

          return additions;
        },
      }),
    };
  };
}
