import { type OAuthClient, OAuthClientGrantTypeEnum } from '@api/graphql';
import { InternalServerErrorException, Logger } from '@nestjs/common';
import { InvalidRequestError, UnauthorizedClientError } from '../../flows/domain/errors';
import {
  oAuthAuthenticateApplication,
  type OAuthFindApplicationPort,
} from './o-auth-authenticate-application.function';

const logger = new Logger('oAuthResolveApplicationClient');

/**
 * Phase 2 of client resolution (shared between `oAuthResolveApplicationClient` and
 * `oAuthResolveApplicationClientWithoutSecret`): selects the unique OAuthClient whose
 * grantType matches the requested grant.
 *
 *   - 0 matches → UnauthorizedClientError.
 *   - >1 matches → server-side misconfiguration → InternalServerErrorException (never a
 *     heuristic pick).
 */
const selectClientByGrantType = (
  clients: (OAuthClient | null | undefined)[] | null | undefined,
  clientId: string,
  grantType: OAuthClientGrantTypeEnum,
): OAuthClient => {
  const matching = (clients ?? []).filter(
    (client): client is OAuthClient => !!client && client.grantType === grantType,
  );

  if (matching.length === 0) throw new UnauthorizedClientError();
  if (matching.length > 1) {
    logger.error(
      `application "${clientId}" has ${matching.length} clients for grant ${grantType}: ambiguous configuration`,
    );
    throw new InternalServerErrorException();
  }

  return matching[0];
};

/**
 * Finds an OAuthApplication by `code` (= public `client_id`) with clients eager-loaded.
 * Used exclusively by `oAuthResolveApplicationClientWithoutSecret` (the `/authorize` path).
 *
 * Throws `InvalidRequestError` (HTTP 400) when the application is not found.
 * Per RFC 6749 §4.1.2.1 + spec §4.1, an unknown `client_id` at the Authorization endpoint
 * is a bad request (400), NOT a client authentication failure (401). The 401 path belongs
 * exclusively to the `/token` endpoint where the client secret is verified.
 *
 * Unexpected errors (e.g. transient DB faults) are propagated as-is (→ 500),
 * so only a genuine "not found" result (null return) is converted to `InvalidRequestError`.
 */
const findApplicationByCode = async (findApplication: OAuthFindApplicationPort, code: string) => {
  const application = await findApplication.main({
    where: { code },
    include: [{ association: 'clients' }],
  });
  if (!application) throw new InvalidRequestError();
  return application;
};

/**
 * Two-phase client resolution (RFC 6749 §2.2/§2.3.1 + Aurora's Application/Client model):
 *
 *   1. AUTHENTICATE: delegate to `oAuthAuthenticateApplication` — find the `OAuthApplication`
 *      by `code` (the public client_id) and timing-safe compare its `secret`. Not-found and
 *      mismatch both raise `InvalidClientError`, indistinguishable to the caller.
 *   2. AUTHORIZE/RESOLVE: from the application's linked clients, select the one whose
 *      `grantType` matches the flow. `grantType` is single-valued, so this MUST resolve to
 *      exactly one client: zero → `UnauthorizedClientError`; more than one → ambiguous
 *      server-side misconfiguration → `InternalServerErrorException` (NEVER a heuristic pick).
 *
 * The returned `OAuthClient` is the technical client whose `id` flows into downstream
 * persistence (`OAuthAccessToken.clientId`, `OAuthCredential.clientId`).
 *
 * Use this function for flows that require client authentication at this step (e.g.
 * token exchange at `/token`). For the Authorization Code `/authorize` endpoint — where the
 * public client is identified by `client_id` but no secret is presented (per RFC 6749 §4.1.1,
 * the browser redirect is a public channel) — use `oAuthResolveApplicationClientWithoutSecret`.
 */
export const oAuthResolveApplicationClient = async (
  findApplication: OAuthFindApplicationPort,
  args: { clientId: string; clientSecret: string; grantType: OAuthClientGrantTypeEnum },
): Promise<OAuthClient> => {
  const application = await oAuthAuthenticateApplication(findApplication, args);
  return selectClientByGrantType(application.clients, args.clientId, args.grantType);
};

/**
 * Client resolution WITHOUT secret authentication (RFC 6749 §4.1.1 — Authorization Request).
 *
 * At the `/authorize` endpoint the browser issues a GET redirect — there is no secure channel
 * to carry a client secret. Per RFC 6749 §4.1.1, the Authorization Request only requires
 * `client_id`; the client secret is verified at the token-exchange step (§4.1.3 + §4.1.4).
 *
 * This function:
 *   1. Looks up the `OAuthApplication` by `code` (= `client_id`). Unknown code → throws
 *      `InvalidRequestError` (HTTP 400, per RFC 6749 §4.1.2.1 — an unknown client_id at the
 *      Authorization endpoint is a bad request, NOT a client authentication failure).
 *   2. Selects the unique OAuthClient for the requested grant type (shared phase-2 logic).
 *      Zero or ambiguous clients → thrown as above.
 *
 * Secret comparison is intentionally absent. Do NOT replace this with
 * `oAuthResolveApplicationClient` + `clientSecret: ''`: `oAuthSafeStringCompare(secret, '')`
 * always fails for any real application secret, breaking the Authorization Code flow (RFC
 * 6749 §4.1.1 correctness regression).
 */
export const oAuthResolveApplicationClientWithoutSecret = async (
  findApplication: OAuthFindApplicationPort,
  args: { clientId: string; grantType: OAuthClientGrantTypeEnum },
): Promise<OAuthClient> => {
  const application = await findApplicationByCode(findApplication, args.clientId);
  return selectClientByGrantType(application.clients, args.clientId, args.grantType);
};
