/**
 * Typed shape of the backend error envelope.
 * Contract: BACKEND-API-REFERENCE.md §5.
 *
 * Callers branch on `code`, never on `message` — the message is an English
 * fallback and is free to change without notice.
 */

/** Per-field validation issue. Present only on 422. */
export interface ApiErrorDetail {
  readonly field: string;
  readonly code: string;
  readonly message: string;
}

/** The envelope body as the backend sends it. */
export interface ApiErrorEnvelope {
  readonly code: string;
  readonly message: string;
  readonly status: number;
  readonly requestId?: string;
  readonly details?: readonly ApiErrorDetail[];
}

/**
 * Normalized error every HTTP caller sees.
 *
 * A response that carries no envelope still arrives as an ApiError, with
 * `code` synthesized from the HTTP status and `hasEnvelope: false`. That way
 * no caller needs a second code path for "backend answered in a shape we did
 * not expect" — the most common case in practice, and the one that silently
 * breaks error UIs when it is not modelled.
 */
export interface ApiError {
  readonly code: string;
  readonly message: string;
  readonly status: number;
  readonly requestId?: string;
  readonly details: readonly ApiErrorDetail[];
  /** False when the code was synthesized from the HTTP status. */
  readonly hasEnvelope: boolean;
  /** Seconds to wait before retrying. Only set for 429 and 503. */
  readonly retryAfterSeconds?: number;
  /** The URL that failed, for logging. Never rendered to a user. */
  readonly url?: string;
}

/** Codes the app branches on. Extend as the backend defines more. */
export const ApiErrorCode = {
  VALIDATION_FAILED: 'VALIDATION_FAILED',
  RATE_LIMITED: 'RATE_LIMITED',
  UNAUTHORIZED: 'UNAUTHORIZED',
  FORBIDDEN: 'FORBIDDEN',
  NOT_FOUND: 'NOT_FOUND',
  CONFLICT: 'CONFLICT',
  FX_SOURCE_UNAVAILABLE: 'FX_SOURCE_UNAVAILABLE',
  SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
  NETWORK_UNAVAILABLE: 'NETWORK_UNAVAILABLE',
  UNKNOWN: 'UNKNOWN',
} as const;

export type ApiErrorCodeValue = (typeof ApiErrorCode)[keyof typeof ApiErrorCode];

/** Status → code, for responses that arrive without an envelope. */
const STATUS_CODE_FALLBACK: Readonly<Record<number, string>> = {
  0: ApiErrorCode.NETWORK_UNAVAILABLE,
  401: ApiErrorCode.UNAUTHORIZED,
  403: ApiErrorCode.FORBIDDEN,
  404: ApiErrorCode.NOT_FOUND,
  409: ApiErrorCode.CONFLICT,
  422: ApiErrorCode.VALIDATION_FAILED,
  429: ApiErrorCode.RATE_LIMITED,
  503: ApiErrorCode.SERVICE_UNAVAILABLE,
};

export function fallbackCodeForStatus(status: number): string {
  return STATUS_CODE_FALLBACK[status] ?? ApiErrorCode.UNKNOWN;
}

export function isApiError(value: unknown): value is ApiError {
  return (
    typeof value === 'object' &&
    value !== null &&
    typeof (value as ApiError).code === 'string' &&
    typeof (value as ApiError).status === 'number' &&
    typeof (value as ApiError).hasEnvelope === 'boolean'
  );
}

/** True when retrying the identical request could plausibly succeed. */
export function isRetryable(error: ApiError): boolean {
  return (
    error.status === 429 ||
    error.status === 503 ||
    error.status === 0 ||
    error.status === 504
  );
}
