import { HttpErrorResponse } from '@angular/common/http';
import {
  ApiError,
  ApiErrorCode,
  ApiErrorDetail,
  ApiErrorEnvelope,
  fallbackCodeForStatus,
} from './models/api-error.model';

/**
 * Turn any HttpErrorResponse into a normalized ApiError.
 * Contract: BACKEND-API-REFERENCE.md §5.
 *
 * Deliberately total: every input produces an ApiError. A backend that answers
 * with HTML, an empty body, or a differently-shaped JSON object still yields a
 * usable error rather than throwing inside the error path — a throw there
 * replaces the real failure with a less informative one.
 */
export function toApiError(response: HttpErrorResponse): ApiError {
  const status = response.status ?? 0;
  const envelope = readEnvelope(response.error);
  const retryAfterSeconds = readRetryAfter(response, envelope);

  if (envelope) {
    return {
      code: envelope.code,
      message: envelope.message,
      // Trust the transport status over a self-reported one: a proxy or
      // gateway can produce a status the application never set.
      status: status || envelope.status,
      requestId: envelope.requestId,
      details: envelope.details ?? [],
      hasEnvelope: true,
      retryAfterSeconds,
      url: response.url ?? undefined,
    };
  }

  return {
    code: fallbackCodeForStatus(status),
    message: fallbackMessage(status, response),
    status,
    details: [],
    hasEnvelope: false,
    retryAfterSeconds,
    url: response.url ?? undefined,
  };
}

function readEnvelope(body: unknown): ApiErrorEnvelope | null {
  const wrapper = asRecord(body);
  if (!wrapper) {
    return null;
  }

  // Accept both { error: {...} } and a bare {...}: some gateways unwrap the
  // envelope, and rejecting that shape would lose a code we actually have.
  const candidate = asRecord(wrapper['error']) ?? wrapper;

  const code = candidate['code'];
  const status = candidate['status'];
  if (typeof code !== 'string' || code.length === 0) {
    return null;
  }

  return {
    code,
    message: typeof candidate['message'] === 'string' ? candidate['message'] : code,
    status: typeof status === 'number' ? status : 0,
    requestId: typeof candidate['requestId'] === 'string' ? candidate['requestId'] : undefined,
    details: readDetails(candidate['details']),
  };
}

function readDetails(value: unknown): readonly ApiErrorDetail[] | undefined {
  if (!Array.isArray(value)) {
    return undefined;
  }
  const details = value
    .map(asRecord)
    .filter((entry): entry is Record<string, unknown> => entry !== null)
    .filter(entry => typeof entry['field'] === 'string')
    .map(entry => ({
      field: entry['field'] as string,
      code: typeof entry['code'] === 'string' ? entry['code'] : ApiErrorCode.VALIDATION_FAILED,
      message: typeof entry['message'] === 'string' ? entry['message'] : '',
    }));
  return details.length > 0 ? details : undefined;
}

/**
 * Retry-After per RFC 9110: either delta-seconds or an HTTP-date. The envelope
 * may also carry retryAfterSeconds (Track S §5); the header wins when both are
 * present, because it is what the edge actually enforces.
 */
function readRetryAfter(response: HttpErrorResponse, envelope: ApiErrorEnvelope | null): number | undefined {
  const header = response.headers?.get('Retry-After');
  if (header) {
    const seconds = Number(header);
    if (Number.isFinite(seconds) && seconds >= 0) {
      return Math.ceil(seconds);
    }
    const date = Date.parse(header);
    if (!Number.isNaN(date)) {
      return Math.max(0, Math.ceil((date - Date.now()) / 1000));
    }
  }

  const fromBody = asRecord(asRecord(response.error)?.['error'] ?? response.error)?.['retryAfterSeconds'];
  if (typeof fromBody === 'number' && Number.isFinite(fromBody) && fromBody >= 0) {
    return Math.ceil(fromBody);
  }

  // A 429 with no hint still needs a delay, or callers busy-loop the endpoint
  // that just asked them to stop.
  return envelope?.code === ApiErrorCode.RATE_LIMITED || response.status === 429 ? 5 : undefined;
}

function fallbackMessage(status: number, response: HttpErrorResponse): string {
  if (status === 0) {
    return 'Network unavailable.';
  }
  return response.message || `Request failed with status ${status}.`;
}

function asRecord(value: unknown): Record<string, unknown> | null {
  return typeof value === 'object' && value !== null && !Array.isArray(value)
    ? (value as Record<string, unknown>)
    : null;
}
