// Clase para mapear errores de Dominio/Aplicación/Infraestructura → ApiError (Problem+JSON).
// - Inmutable (register() devuelve una NUEVA instancia).
// - Extensible por reglas con prioridad.
// - Sin dependencias de vendors (p.ej. Sequelize).
//
// ─ Convenciones de carpetas sugeridas:
// domain/errors/* → errores semánticos (DDD)
// application/errors/* → errores de aplicación (servicios, lógica de negocio)
// infrastructure/errors/* → errores técnicos (DB, red, timeouts)
// infrastructure/express/errors/* → ApiError (RFC7807) y familia
//
// Nota: Todos los nombres de tipos/clases/archivos en inglés; comentarios en castellano.

import {
  DomainValidationError,
  ValidationErrorCollection,
  isDomainValidationError,
  isValidationErrorCollection,
} from "@repo/rdx-ddd";

import {
  DuplicateEntityError,
  EntityNotFoundError,
  isDuplicateEntityError,
  isEntityNotFoundError,
} from "../../domain";
import {
  InfrastructureRepositoryError,
  InfrastructureUnavailableError,
  isInfrastructureRepositoryError,
  isInfrastructureUnavailableError,
} from "../errors";
import {
  ApiError,
  ConflictApiError,
  ForbiddenApiError,
  InternalApiError,
  NotFoundApiError,
  UnauthorizedApiError,
  UnavailableApiError,
  ValidationApiError,
} from "./errors";

// ────────────────────────────────────────────────────────────────────────────────
// Contexto opcional para enriquecer Problem+JSON (útil en middleware Express)
// ────────────────────────────────────────────────────────────────────────────────
export interface ApiErrorContext {
  instance?: string; // p.ej. req.originalUrl
  correlationId?: string; // p.ej. header 'x-correlation-id'
  method?: string; // GET/POST/PUT/DELETE
  userId?: string;
  tenantId?: string;
}

// ────────────────────────────────────────────────────────────────────────────────
// Regla de mapeo: cómo reconocer y construir un ApiError
// ────────────────────────────────────────────────────────────────────────────────
export interface ErrorToApiRule {
  priority?: number; // mayor valor ⇒ se evalúa antes (default 0)
  matches: (e: unknown) => boolean;
  build: (e: unknown, ctx?: ApiErrorContext) => ApiError;
}

// ────────────────────────────────────────────────────────────────────────────────
// ApiErrorMapper (inmutable)
// ────────────────────────────────────────────────────────────────────────────────
export class ApiErrorMapper {
  private readonly rules: ReadonlyArray<ErrorToApiRule>;
  private readonly fallback: (e: unknown, ctx?: ApiErrorContext) => ApiError;

  private constructor(
    rules: ReadonlyArray<ErrorToApiRule>,
    fallback: (e: unknown, ctx?: ApiErrorContext) => ApiError
  ) {
    this.rules = [...rules].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
    this.fallback = fallback;
    Object.freeze(this);
  }

  // Crea un mapper con reglas por defecto (cubren casos comunes)
  static default(): ApiErrorMapper {
    return new ApiErrorMapper(defaultRules, defaultFallback);
  }

  // Registra una regla adicional devolviendo una NUEVA instancia
  register(rule: ErrorToApiRule): ApiErrorMapper {
    return new ApiErrorMapper([...this.rules, rule], this.fallback);
  }

  // Mapea un error a un ApiError evaluando reglas por prioridad
  map(error: unknown, ctx?: ApiErrorContext): ApiError {
    for (const rule of this.rules) {
      try {
        if (rule.matches(error)) {
          return rule.build(error, ctx);
        }
      } catch {
        // ⚠️ Una regla no debe tumbar el mapper; continuamos con la siguiente.
        // continue
      }
    }
    return this.fallback(error, ctx);
  }

  // Útil en tests / introspección
  getRules(): ReadonlyArray<ErrorToApiRule> {
    return this.rules;
  }
}

// ────────────────────────────────────────────────────────────────────────────────
// Reglas por defecto (prioridad alta a más específicas)
// ────────────────────────────────────────────────────────────────────────────────
const defaultRules: ReadonlyArray<ErrorToApiRule> = [
  // 1) Validación múltiple (colección)
  {
    priority: 100,
    matches: (e) => isValidationErrorCollection(e),
    build: (e) =>
      new ValidationApiError(
        (e as ValidationErrorCollection).message,
        (e as ValidationErrorCollection).details
      ),
  },

  // 2) Validación de dominio unitaria
  {
    priority: 90,
    matches: (e) => isDomainValidationError(e),
    build: (e) =>
      new ValidationApiError((e as DomainValidationError).detail, [
        { path: (e as DomainValidationError).field, message: (e as DomainValidationError).detail },
      ]),
  },

  // 3) Duplicados / conflictos de unicidad
  {
    priority: 80,
    matches: (e) => isDuplicateEntityError(e),
    build: (e) => new ConflictApiError((e as DuplicateEntityError).message),
  },

  // 4) No encontrado
  {
    priority: 70,
    matches: (e) => isEntityNotFoundError(e),
    build: (e) => new NotFoundApiError((e as EntityNotFoundError).message),
  },

  // 5) Infra transitoria (DB/servicio caído, timeouts)
  {
    priority: 60,
    matches: (e) => isInfrastructureUnavailableError(e),
    build: (e) =>
      new UnavailableApiError(
        (e as InfrastructureUnavailableError).message,
        "Service temporarily unavailable."
      ),
  },

  // 6) Infra no transitoria (errores de repositorio inesperados)
  {
    priority: 50,
    matches: (e) => isInfrastructureRepositoryError(e),
    build: (e) =>
      new InternalApiError(
        (e as InfrastructureRepositoryError).message,
        "Unexpected repository error"
      ),
  },

  // 7) Autenticación/autorización por nombre (si no tienes clases dedicadas)
  {
    priority: 40,
    matches: (e): e is Error => e instanceof Error && e.name === "UnauthorizedError",
    build: (e) => new UnauthorizedApiError((e as Error).message || "Unauthorized"),
  },
  {
    priority: 40,
    matches: (e): e is Error => e instanceof Error && e.name === "ForbiddenError",
    build: (e) => new ForbiddenApiError((e as Error).message || "Forbidden"),
  },
];

// Fallback genérico (500)
function defaultFallback(e: unknown): ApiError {
  if (e instanceof ApiError) {
    return e; // ya es un ApiError
  }

  const message = typeof (e as any)?.message === "string" ? (e as any).message : "";
  const detail = typeof (e as any)?.detail === "string" ? (e as any).detail : "";

  return new InternalApiError(`${message} ${detail}`);
}

// ────────────────────────────────────────────────────────────────────────────────
// Serializador opcional a Problem+JSON
// ────────────────────────────────────────────────────────────────────────────────
export function toProblemJson(apiError: ApiError, ctx?: ApiErrorContext) {
  const maybeErrors = (apiError as any).errors ? { errors: (apiError as any).errors } : {};
  return {
    type: apiError.type,
    title: apiError.title,
    status: apiError.status,
    detail: apiError.detail,
    ...(ctx?.userId ? { userId: ctx.userId } : {}),
    ...(ctx?.tenantId ? { tenantId: ctx.tenantId } : {}),
    ...(ctx?.instance ? { instance: ctx.instance } : {}),
    ...(ctx?.correlationId ? { correlationId: ctx.correlationId } : {}),
    ...(ctx?.method ? { method: ctx.method } : {}),
    ...maybeErrors,
  };
}
