import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { CATALOGO_ASIENTOS } from './catalogo-asientos';
import { MapeoMediosPagoService } from './mapeo-medios-pago.service';

@Injectable()
export class MapeoCuentasService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly mediosPago: MapeoMediosPagoService,
  ) {}

  async listar(empresaId: string) {
    return this.prisma.cont_mapeo_cuentas.findMany({
      where: { empresa_id: empresaId },
      include: { cuenta: { select: { codigo: true, descripcion: true, tipo: true } } },
      orderBy: { concepto: 'asc' },
    });
  }

  /**
   * Cómo quedaría cada asiento automático con las cuentas que la empresa mapeó.
   * Es lo que dibuja la pantalla de Mapeo: el contador ve el debe y el haber antes
   * de que exista el primer documento. Ver `catalogo-asientos.ts`.
   */
  async previsualizacionAsientos(empresaId: string) {
    const mapeos = await this.prisma.cont_mapeo_cuentas.findMany({
      where: { empresa_id: empresaId },
      select: { concepto: true, cuenta: { select: { codigo: true, descripcion: true } } },
    });
    const porConcepto = new Map(mapeos.map((m) => [m.concepto, m.cuenta]));

    // Tesorería no usa el mapeo: la cuenta sale de la caja/banco del movimiento y
    // de la categoría. Se mandan las reales de la empresa para que el contador vea
    // sus propias cuentas, y cuáles todavía no tienen cuenta contable.
    // Ojo: `tes_cuentas` y `tes_categorias` guardan `cuenta_contable_id` suelto, sin
    // relación Prisma al plan, así que las cuentas se buscan aparte.
    const [cuentasTes, categoriasTes, mapeosCategoria, mediosPago] = await Promise.all([
      this.prisma.tes_cuentas.findMany({
        where: { empresa_id: empresaId, activo: true },
        select: { id: true, nombre: true, moneda: true, cuenta_contable_id: true },
        orderBy: { nombre: 'asc' },
      }),
      this.prisma.tes_categorias.findMany({
        where: { OR: [{ empresa_id: empresaId }, { empresa_id: null }], activo: true },
        select: { id: true, nombre: true, tipo: true, cuenta_contable_id: true },
        orderBy: { nombre: 'asc' },
      }),
      this.prisma.tes_categoria_cuenta_contable.findMany({
        where: { empresa_id: empresaId },
        select: { categoria_id: true, cuenta_contable_id: true },
      }),
      // Con qué se cobró / de dónde salió la plata: la cuenta la elige la empresa
      // por medio de pago. Se mandan los medios con su cuenta para que el panel
      // muestre el asiento de cada uno, no un texto genérico.
      this.mediosPago.listar(empresaId),
    ]);

    const cuentaPorCategoria = new Map(mapeosCategoria.map((m) => [m.categoria_id, m.cuenta_contable_id]));
    const idsPlan = [
      ...cuentasTes.map((c) => c.cuenta_contable_id),
      ...categoriasTes.map((c) => cuentaPorCategoria.get(c.id) ?? c.cuenta_contable_id),
    ].filter((id): id is string => !!id);

    const planTes = idsPlan.length
      ? await this.prisma.cont_plan_cuentas.findMany({
          where: { id: { in: [...new Set(idsPlan)] } },
          select: { id: true, codigo: true, descripcion: true },
        })
      : [];
    const planPorId = new Map(planTes.map((c) => [c.id, { codigo: c.codigo, descripcion: c.descripcion }]));
    // Copia nueva por fila: dos categorías que comparten cuenta devolvían la MISMA
    // referencia y el serializador de la API mandaba "[Circular]" en la segunda
    // (10 de 36 categorías de DOBA en producción quedaban sin cuenta en pantalla).
    const cuentaDe = (id?: string | null) => {
      const c = id ? planPorId.get(id) : null;
      return c ? { codigo: c.codigo, descripcion: c.descripcion } : null;
    };

    const tesoreria = {
      cuentas: cuentasTes.map((c) => ({
        id: c.id,
        nombre: `${c.nombre}${c.moneda && c.moneda !== 'PYG' ? ` (${c.moneda})` : ''}`,
        cuenta: cuentaDe(c.cuenta_contable_id),
      })),
      categorias: categoriasTes.map((c) => {
        // El mapeo por empresa gana sobre la cuenta propia de la categoría (mismo
        // orden que usa `integrarMovimientoTesoreria`).
        const cuentaId = cuentaPorCategoria.get(c.id) ?? c.cuenta_contable_id;
        return {
          id: c.id,
          nombre: c.nombre,
          tipo: c.tipo,
          cuenta: cuentaDe(cuentaId),
        };
      }),
    };

    const situaciones = CATALOGO_ASIENTOS.map((situacion) => {
      const lineas = situacion.lineas.map((linea) => {
        const cuenta = linea.concepto ? porConcepto.get(linea.concepto) ?? null : null;
        return {
          lado: linea.lado,
          etiqueta: linea.etiqueta,
          monto: linea.monto,
          condicion: linea.condicion ?? null,
          concepto: linea.concepto ?? null,
          cuenta_dinamica: linea.cuentaDinamica ?? null,
          fuente: linea.fuente ?? null,
          cuenta: cuenta ? { codigo: cuenta.codigo, descripcion: cuenta.descripcion } : null,
          // Una cuenta que no sale del mapeo (la del proveedor, la del banco) nunca
          // es una falta: no hay nada que el contador pueda mapear acá.
          sin_mapear: !!linea.concepto && !cuenta,
        };
      });

      const sumar = (lado: 'DEBE' | 'HABER') =>
        lineas.filter((l) => l.lado === lado).reduce((acc, l) => acc + l.monto, 0);

      return {
        clave: situacion.clave,
        grupo: situacion.grupo,
        titulo: situacion.titulo,
        cuando: situacion.cuando,
        documento: situacion.documento,
        nota: situacion.nota ?? null,
        lineas,
        total_debe: sumar('DEBE'),
        total_haber: sumar('HABER'),
        conceptos_sin_mapear: [...new Set(lineas.filter((l) => l.sin_mapear).map((l) => l.concepto as string))],
      };
    });

    return {
      situaciones,
      tesoreria,
      medios_pago: mediosPago.medios,
      grupos: [...new Set(situaciones.map((s) => s.grupo))],
      total_sin_mapear: [...new Set(situaciones.flatMap((s) => s.conceptos_sin_mapear))].length,
    };
  }

  async actualizarBulk(empresaId: string, items: Array<{ concepto: string; cuenta_id: string }>) {
    const resultados: any[] = [];

    for (const item of items) {
      const cuenta = await this.prisma.cont_plan_cuentas.findFirst({
        where: { id: item.cuenta_id, OR: [{ empresa_id: empresaId }, { empresa_id: null }] },
      });
      if (!cuenta) throw new NotFoundException(`Cuenta ${item.cuenta_id} no encontrada`);

      const resultado = await this.prisma.cont_mapeo_cuentas.upsert({
        where: { empresa_id_concepto: { empresa_id: empresaId, concepto: item.concepto } },
        update: { cuenta_id: item.cuenta_id, updated_at: new Date() },
        create: { empresa_id: empresaId, concepto: item.concepto, cuenta_id: item.cuenta_id },
      });
      resultados.push(resultado);
    }

    return resultados;
  }

  async getCuentaPorConcepto(empresaId: string, concepto: string): Promise<string> {
    const mapeo = await this.prisma.cont_mapeo_cuentas.findUnique({
      where: { empresa_id_concepto: { empresa_id: empresaId, concepto } },
      select: {
        cuenta_id: true,
        cuenta: { select: { codigo: true, active: true, acepta_movimientos: true } },
      },
    });
    if (!mapeo) throw new NotFoundException(`No hay cuenta mapeada para el concepto "${concepto}". Configurá el mapeo en Contabilidad → Configuración`);

    // Un mapeo puede quedar apuntando a una cuenta dada de baja (típico tras migrar
    // el plan de cuentas): antes se contabilizaba igual contra la cuenta muerta y el
    // asiento no aparecía en ningún reporte, sin ninguna advertencia.
    const cuenta = mapeo.cuenta;
    if (cuenta && !cuenta.active) {
      throw new BadRequestException(
        `El concepto "${concepto}" está mapeado a la cuenta ${cuenta.codigo}, que está desactivada. ` +
          'Actualizá el mapeo en Contabilidad → Configuración.',
      );
    }
    if (cuenta && !cuenta.acepta_movimientos) {
      throw new BadRequestException(
        `El concepto "${concepto}" está mapeado a la cuenta ${cuenta.codigo}, que no acepta movimientos ` +
          '(es una cuenta de agrupación). Mapealo a una cuenta imputable en Contabilidad → Configuración.',
      );
    }

    return mapeo.cuenta_id;
  }
}
