import { randomUUID } from 'crypto';
import { FaltaMapeoError } from '../contabilidad/services/falta-mapeo.error';

/**
 * Integrador "sombra": corre el integrador contable real de un documento sin
 * escribir nada, y devuelve el asiento que generaría hoy.
 *
 * Es lo que permite comparar el asiento guardado con el que saldría con el
 * mapeo y el código actuales (ver RecontabilizacionService) sin duplicar la
 * lógica de cada `integrar*`: se usa el mismo método, sobre otro Prisma.
 *
 * El Prisma sombra:
 *  - deja pasar las lecturas a la base real;
 *  - hace como si el documento no tuviera asiento (cont_documentos vacío), para
 *    saltear la idempotencia de los integradores, y devuelve un id inventado
 *    cuando lo crean;
 *  - corre `$transaction(fn)` sobre sí mismo;
 *  - rechaza cualquier otra escritura: si un integrador empezara a escribir en
 *    otra tabla, la simulación falla en vez de modificar datos.
 *
 * El asiento se captura reemplazando `asientos.crearConfirmado` por un colector
 * que hace las mismas cuentas (conversión a guaraníes y partida doble) que el real.
 */

const ESCRITURAS = new Set([
  'create',
  'createMany',
  'createManyAndReturn',
  'update',
  'updateMany',
  'updateManyAndReturn',
  'upsert',
  'delete',
  'deleteMany',
]);

export interface LineaSimulada {
  cuenta_id: string;
  debe_pyg: number;
  haber_pyg: number;
  descripcion?: string | null;
  /** Traza de la línea (cómo salió la cuenta), como se grabaría en cont_asientos_det. */
  mapeo_concepto?: string | null;
  mapeo_regla_id?: string | null;
  mapeo_nivel?: string | null;
}

export interface AsientoSimulado {
  fecha: string;
  glosa?: string | null;
  lineas: LineaSimulada[];
}

/** Lo que el colector necesita del AsientosService y del TipoCambioService reales. */
export interface DependenciasColector {
  convertirAPYG: (empresaId: string, monto: number, moneda: string, fecha: Date) => Promise<number>;
  validarPartidaDoble: (debe: number, haber: number) => void;
  /** Cuentas activas, imputables y centros de costo (lee el plan de cuentas). */
  validarLineas?: (empresaId: string, lineas: Array<Record<string, unknown>>) => Promise<void>;
}

const documentosVacios = {
  findFirst: async () => null,
  findUnique: async () => null,
  findMany: async () => [],
  count: async () => 0,
  create: async ({ data }: { data: Record<string, unknown> }) => ({ id: randomUUID(), ...data }),
  upsert: async ({ create }: { create: Record<string, unknown> }) => ({ id: randomUUID(), ...create }),
  update: async ({ where, data }: { where: { id?: string }; data: Record<string, unknown> }) => ({
    id: where.id ?? randomUUID(),
    ...data,
  }),
  updateMany: async () => ({ count: 0 }),
};

export function crearPrismaSombra<T extends object>(real: T): T {
  const sombra: T = new Proxy(real, {
    get(target, prop, receiver) {
      if (prop === '$transaction') {
        return async (arg: unknown) =>
          typeof arg === 'function' ? (arg as (tx: T) => unknown)(sombra) : Promise.all(arg as Promise<unknown>[]);
      }
      if (prop === '$executeRaw' || prop === '$executeRawUnsafe') {
        return () => {
          throw new Error('Simulación: escritura SQL bloqueada');
        };
      }
      if (prop === 'cont_documentos') return documentosVacios;

      const valor = Reflect.get(target, prop, receiver);
      if (valor && typeof valor === 'object' && typeof prop === 'string' && !prop.startsWith('$')) {
        // Delegado de un modelo: lecturas pasan, escrituras se rechazan.
        return new Proxy(valor as object, {
          get(delegado, metodo) {
            if (typeof metodo === 'string' && ESCRITURAS.has(metodo)) {
              return () => {
                throw new Error(`Simulación: escritura bloqueada en ${prop}.${metodo}`);
              };
            }
            const fn = Reflect.get(delegado, metodo);
            return typeof fn === 'function' ? fn.bind(delegado) : fn;
          },
        });
      }
      return typeof valor === 'function' ? valor.bind(target) : valor;
    },
  });
  return sombra;
}

/**
 * Colector que reemplaza a `AsientosService.crearConfirmado`: en vez de grabar,
 * guarda el asiento convertido a guaraníes. Valida la partida doble igual que el
 * real, así un asiento que no cuadraría aparece como error de la simulación.
 */
export function crearColectorAsientos(deps: DependenciasColector) {
  const capturados: AsientoSimulado[] = [];
  const colector = {
    capturados,
    async crearConfirmado(
      empresaId: string,
      _usuarioId: string | null,
      _documentoId: string,
      dto: {
        fecha: string;
        glosa?: string | null;
        moneda_origen?: string;
        lineas: Array<{
          cuenta_id: string;
          debe_moneda: number;
          haber_moneda: number;
          descripcion?: string | null;
          mapeo_concepto?: string | null;
          mapeo_regla_id?: string | null;
          mapeo_nivel?: string | null;
        }>;
      },
    ) {
      const moneda = dto.moneda_origen ?? 'PYG';
      const fecha = new Date(dto.fecha);
      const conPYG: Array<(typeof dto.lineas)[number] & { debe_pyg: number; haber_pyg: number }> = [];
      for (const l of dto.lineas) {
        conPYG.push({
          ...l,
          debe_pyg: await deps.convertirAPYG(empresaId, Number(l.debe_moneda ?? 0), moneda, fecha),
          haber_pyg: await deps.convertirAPYG(empresaId, Number(l.haber_moneda ?? 0), moneda, fecha),
        });
      }
      deps.validarPartidaDoble(
        conPYG.reduce((s, l) => s + l.debe_pyg, 0),
        conPYG.reduce((s, l) => s + l.haber_pyg, 0),
      );
      await deps.validarLineas?.(empresaId, conPYG);
      const lineas: LineaSimulada[] = conPYG.map((l) => ({
        cuenta_id: l.cuenta_id,
        debe_pyg: l.debe_pyg,
        haber_pyg: l.haber_pyg,
        descripcion: l.descripcion ?? null,
        mapeo_concepto: l.mapeo_concepto ?? null,
        mapeo_regla_id: l.mapeo_regla_id ?? null,
        mapeo_nivel: l.mapeo_nivel ?? null,
      }));
      capturados.push({ fecha: dto.fecha, glosa: dto.glosa ?? null, lineas });
      return { id: randomUUID() };
    },
  };
  return colector;
}

/**
 * Copia de un service integrador (ContabilidadIntegracionService o
 * RendicionViaticosContabilidadService) que usa el Prisma sombra y el colector.
 * Las demás dependencias (mapeo, períodos, tipo de cambio) solo leen y quedan
 * las reales. `opcionesMapeo` (vista previa de reglas: incluir borradores) va
 * solo en la copia; el integrador real nunca lo tiene.
 */
export function crearIntegradorSombra<S extends object>(
  real: S,
  prismaSombra: unknown,
  colector: ReturnType<typeof crearColectorAsientos>,
  opciones: { opcionesMapeo?: { incluirBorradores?: boolean } } = {},
): S {
  const sombra = Object.create(Object.getPrototypeOf(real)) as S;
  Object.assign(sombra, real, {
    opcionesMapeo: opciones.opcionesMapeo,
    prisma: prismaSombra,
    asientos: colector,
    logger: { log: () => undefined, warn: () => undefined, error: () => undefined, debug: () => undefined },
    audit: { log: async () => undefined },
    // La falta de mapeo tiene que verse como error (ERROR, no SOBRA) y no dejar
    // constancia ni alertas: la simulación no escribe.
    avisosMapeo: { registrarFaltaMapeo: async () => undefined },
    conAvisoDeMapeo: relanzarFaltaDeMapeo(null),
  });
  return sombra;
}

/**
 * El integrador real, pero con la falta de mapeo visible como error.
 *
 * Desde 2026-10-13 (spec "mapeo sin decisiones internas") los `integrar*` se
 * tragan `FaltaMapeoError` (dejan constancia y avisan) para no frenar la
 * operación del usuario. Quien rehace o regenera asientos necesita verla: si no,
 * "no generó nada" se confunde con "al documento ya no le corresponde asiento" y
 * se quitaría el asiento vigente. Con `registrar`, además deja la constancia y
 * el aviso como el integrador normal.
 */
export function conFaltaDeMapeoComoError<S extends object>(servicio: S, opciones: { registrar?: boolean } = {}): S {
  const estricto = Object.create(servicio) as S;
  Object.defineProperty(estricto, 'conAvisoDeMapeo', {
    value: relanzarFaltaDeMapeo(opciones.registrar ? servicio : null),
  });
  return estricto;
}

type ConAviso = (origenTipo: string, origenId: string, fn: () => Promise<void>) => Promise<void>;

function relanzarFaltaDeMapeo(registrarCon: object | null): ConAviso {
  return async (origenTipo, origenId, fn) => {
    try {
      await fn();
    } catch (e) {
      const original = registrarCon ? (registrarCon as { conAvisoDeMapeo?: ConAviso }).conAvisoDeMapeo : undefined;
      if (e instanceof FaltaMapeoError && original) {
        // El envoltorio normal registra y se la traga; acá se relanza igual.
        await original.call(registrarCon, origenTipo, origenId, () => Promise.reject(e));
      }
      throw e;
    }
  };
}
