import { type OAuthApplication } from '@api/graphql';
import type { QueryStatement } from '@aurorajs.dev/core-common';
import { InvalidClientError } from '../../flows/domain/errors';
import { oAuthSafeStringCompare } from './o-auth-safe-string-compare.function';

/**
 * Minimal port over the Cliter-generated `OAuthFindApplicationService`. The service's
 * `main(queryStatement?, constraint?, handlerMeta?)` is structurally assignable to this.
 */
export interface OAuthFindApplicationPort {
  main(queryStatement?: QueryStatement): Promise<OAuthApplication>;
}

// 60-char constant matching a bcrypt-length secret, used purely to equalize timing on the
// application-not-found path (the real secrets are arbitrary length, but a fixed dummy is
// enough to avoid an obvious early-return timing signal).
const DUMMY_SECRET = '0'.repeat(60);

/**
 * Phase 1 of OAuth client authentication (RFC 6749 §2.2/§2.3.1 + Aurora's Application model):
 * 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 (no code enumeration). A dummy comparison runs on the not-found path so the timing
 * does not betray whether the code existed.
 *
 * The application is returned with its `clients` association eager-loaded, so callers that
 * also need phase 2 (grant→client resolution, see `oAuthResolveApplicationClient`) reuse the
 * same query. Callers that only need authentication (e.g., token revocation, RFC 7009 §2.1)
 * ignore `clients`.
 */
export const oAuthAuthenticateApplication = async (
  findApplication: OAuthFindApplicationPort,
  args: { clientId: string; clientSecret: string },
): Promise<OAuthApplication> => {
  const application = await tryFindApplicationByCode(findApplication, args.clientId);

  if (!application) {
    oAuthSafeStringCompare(DUMMY_SECRET, args.clientSecret);
    throw new InvalidClientError();
  }

  if (!oAuthSafeStringCompare(application.secret, args.clientSecret)) {
    throw new InvalidClientError();
  }

  return application;
};

const tryFindApplicationByCode = async (
  findApplication: OAuthFindApplicationPort,
  code: string,
): Promise<OAuthApplication | null> => {
  try {
    const application = await findApplication.main({
      where: { code },
      include: [{ association: 'clients' }],
    });
    return application ?? null;
  } catch {
    return null;
  }
};
