import type { GraphQLFormattedError } from 'graphql';

import { CombinedGraphQLErrors } from '@apollo/client/errors';

import type { GraphQLOriginalError, GraphQLTranslationError } from './graphql.types';

const normalizeErrorMessage = (message: unknown): string => {
  if (typeof message === 'string') return message;
  if (Array.isArray(message)) return message.map(normalizeErrorMessage).join(', ');
  if (message && typeof message === 'object') {
    // Standard NestJS message objects carry a 'message' key. If the key is absent
    // (e.g. RFC 6749 OAuth bodies), JSON.stringify is the last-resort fallback.
    // This path is NOT normally reached for OAuth errors because they are filtered out
    // via skipCodes BEFORE translation; it exists only for non-OAuth callers.
    return ((message as Record<string, unknown>)['message'] as string) ?? JSON.stringify(message);
  }
  return String(message);
};

const hasOriginalError = (
  extensions: GraphQLFormattedError['extensions'],
): extensions is { originalError: GraphQLOriginalError } => {
  if (!extensions || typeof extensions !== 'object') return false;
  const ext = extensions as Record<string, unknown>;
  if (!ext['originalError'] || typeof ext['originalError'] !== 'object') return false;
  const original = ext['originalError'] as Record<string, unknown>;
  // statusCode must be present; message is optional (OAuth errors use `error` instead).
  return typeof original['statusCode'] === 'number';
};

/**
 * Extracts the RFC 6749 error code from a GraphQL error's `extensions.originalError`.
 *
 * Reads `originalError.error` directly — the shape produced by OAuth flow errors
 * after the backend includes `statusCode` in the body (post-finding-#1 fix).
 * Returns `undefined` when the path is missing or malformed.
 */
export const getOAuthErrorCode = (error: GraphQLFormattedError | undefined): string | undefined => {
  if (!error?.extensions) return undefined;
  const original = (error.extensions as Record<string, unknown>)['originalError'];
  if (!original || typeof original !== 'object') return undefined;
  const code = (original as Record<string, unknown>)['error'];
  return typeof code === 'string' ? code : undefined;
};

/**
 * Returns `true` when `err` is a `CombinedGraphQLErrors` instance that carries at
 * least one error with the given RFC 6749 error code in `originalError.error`.
 *
 * Encapsulates the Apollo internals so domain code (e.g. login-form.ts) stays
 * clean and does not need to import `CombinedGraphQLErrors` directly.
 */
export const isGraphqlErrorWithCode = (err: unknown, code: string): boolean => {
  if (!CombinedGraphQLErrors.is(err)) return false;
  return err.errors.some((e) => getOAuthErrorCode(e) === code);
};

/**
 * Translates GraphQL errors into displayable messages, optionally skipping errors
 * whose RFC 6749 code is listed in `skipCodes`.
 *
 * Skipping happens BEFORE translation so suppressed errors never waste translation
 * work and the returned array aligns 1:1 with the errors that were NOT skipped
 * (eliminating index-drift bugs when multi-error responses are filtered later).
 *
 * @param graphqlErrors   Raw errors from a CombinedGraphQLErrors instance.
 * @param translator      Transloco translate function (key, params) → string.
 * @param skipCodes       RFC 6749 error codes to exclude from the result.
 */
export const extractGraphqlErrorTranslations = (
  graphqlErrors: ReadonlyArray<GraphQLFormattedError>,
  translator: (translationKey: string, params?: Record<string, unknown>) => string,
  skipCodes?: readonly string[],
): GraphQLTranslationError[] => {
  return graphqlErrors
    .filter((error) => {
      if (!hasOriginalError(error.extensions)) return false;
      if (skipCodes && skipCodes.length > 0) {
        const code = getOAuthErrorCode(error);
        if (code !== undefined && skipCodes.includes(code)) return false;
      }
      return true;
    })
    .map((error) => {
      const { originalError } = error.extensions as { originalError: GraphQLOriginalError };
      return {
        statusCode: originalError.statusCode,
        translation: originalError.translation,
        message: originalError.translation
          ? translator(originalError.translation, originalError.params)
          : normalizeErrorMessage(originalError.message),
      };
    });
};
