import { Injectable } from '@nestjs/common';
import * as XLSX from 'xlsx';
import { PrismaService } from '../../prisma/prisma.service';
import {
  CuentaCobrarEstado,
  TesChequeEstado,
  TesMovimientoEstado,
  TesMovimientoTipo,
  TesOrdenPagoEstado,
  TesOrigenTipo,
} from '../tes.enums';

@Injectable()
export class TesReportesService {
  constructor(private readonly prisma: PrismaService) {}

  // ── Posición de caja ────────────────────────────────────────────────────────
  // Saldo actual de todas las cuentas activas, agrupadas por moneda y tipo

  async posicionCaja(empresaId: string) {
    const cuentas = await this.prisma.tes_cuentas.findMany({
      where: { empresa_id: empresaId, activo: true },
      orderBy: [{ moneda: 'asc' }, { tipo: 'asc' }, { nombre: 'asc' }],
    });

    // Lookup banco names by id (no Prisma relation defined on tes_cuentas)
    const bancoIds = [...new Set(cuentas.map(c => c.banco_id).filter(Boolean))] as string[];
    const bancosMap: Record<string, string> = {};
    if (bancoIds.length) {
      const bancos = await this.prisma.bancos.findMany({ where: { id: { in: bancoIds } }, select: { id: true, descripcion: true } });
      for (const b of bancos) bancosMap[b.id] = b.descripcion || '';
    }

    const porMoneda: Record<string, { moneda: string; total: number; cuentas: any[] }> = {};
    for (const c of cuentas) {
      if (!porMoneda[c.moneda]) porMoneda[c.moneda] = { moneda: c.moneda, total: 0, cuentas: [] };
      porMoneda[c.moneda].cuentas.push({ ...c, banco: c.banco_id ? { nombre: bancosMap[c.banco_id] || '—' } : null });
      porMoneda[c.moneda].total += Number(c.saldo_actual);
    }

    return {
      fecha: new Date().toISOString().substring(0, 10),
      resumen: Object.values(porMoneda),
      totalCuentas: cuentas.length,
    };
  }

  // ── Cash flow real ──────────────────────────────────────────────────────────
  // Ingresos y egresos confirmados agrupados por categoría, en un rango de fechas

  async cashFlowReal(
    empresaId: string,
    filters: { fecha_desde: string; fecha_hasta: string; cuenta_id?: string; moneda?: string },
  ) {
    // lte fecha_hasta + 1 día (lt) para cubrir todo el día en cualquier zona horaria
    const fechaDesde = new Date(filters.fecha_desde + 'T00:00:00.000Z');
    const fechaHasta = new Date(new Date(filters.fecha_hasta + 'T00:00:00.000Z').getTime() + 86_400_000);
    const where: any = {
      empresa_id: empresaId,
      estado: TesMovimientoEstado.CONFIRMADO,
      fecha: { gte: fechaDesde, lt: fechaHasta },
      // NULL en origen_tipo = movimiento normal. OR explícito porque NOT IN excluye NULLs en SQL.
      OR: [
        { origen_tipo: null },
        { origen_tipo: { notIn: [TesOrigenTipo.TRANSFERENCIA_ORIGEN, TesOrigenTipo.TRANSFERENCIA_DESTINO] } },
      ],
    };
    if (filters.cuenta_id) where.cuenta_id = filters.cuenta_id;
    if (filters.moneda) where.cuenta = { moneda: filters.moneda };

    const movimientos = await this.prisma.tes_movimientos.findMany({
      where,
      include: {
        cuenta: { select: { nombre: true, moneda: true } },
        detalles: { include: { categoria: { select: { nombre: true, tipo: true } } } },
      },
      orderBy: { fecha: 'asc' },
    });

    // Agrupar por categoría
    const categorias: Record<string, { nombre: string; tipo: string; total: number; cantidad: number }> = {};
    let totalIngresos = 0;
    let totalEgresos = 0;

    for (const mov of movimientos) {
      const monto = Number(mov.monto);
      if ((mov.tipo as TesMovimientoTipo) === TesMovimientoTipo.INGRESO) totalIngresos += monto;
      else totalEgresos += monto;

      const dets = mov.detalles?.length ? mov.detalles : [{ categoria: { nombre: 'Sin categoría', tipo: mov.tipo } }];
      for (const det of dets) {
        const key = det.categoria?.nombre || 'Sin categoría';
        if (!categorias[key]) categorias[key] = { nombre: key, tipo: det.categoria?.tipo || mov.tipo, total: 0, cantidad: 0 };
        categorias[key].total += monto / (mov.detalles?.length || 1);
        categorias[key].cantidad++;
      }
    }

    return {
      fecha_desde: filters.fecha_desde,
      fecha_hasta: filters.fecha_hasta,
      totalIngresos,
      totalEgresos,
      resultado: totalIngresos - totalEgresos,
      porCategoria: Object.values(categorias).sort((a, b) => b.total - a.total),
      movimientos: movimientos.map(m => ({
        id: m.id,
        fecha: m.fecha,
        cuenta: m.cuenta?.nombre,
        moneda: m.cuenta?.moneda,
        tipo: m.tipo,
        monto: Number(m.monto),
        descripcion: m.descripcion,
        categorias: m.detalles?.map(d => d.categoria?.nombre).filter(Boolean) || [],
      })),
    };
  }

  // ── Cash flow proyectado ────────────────────────────────────────────────────
  // Real confirmado + CxC pendientes (cobros futuros) + CxP / órdenes de pago (pagos futuros)

  async cashFlowProyectado(
    empresaId: string,
    filters: { fecha_desde: string; fecha_hasta: string },
  ) {
    const pDesde = new Date(filters.fecha_desde + 'T00:00:00.000Z');
    const pHasta = new Date(new Date(filters.fecha_hasta + 'T00:00:00.000Z').getTime() + 86_400_000);
    const [realData, cobros, pagos] = await Promise.all([
      this.cashFlowReal(empresaId, filters),
      this.prisma.cuentas_cobrar.findMany({
        where: {
          empresa_id: empresaId,
          estado: { in: [CuentaCobrarEstado.PENDIENTE, CuentaCobrarEstado.PARCIAL] },
          fecha_vencimiento: { gte: pDesde, lt: pHasta },
        },
      }).then(r => r as any[]),
      this.prisma.orden_pago_proveedor_cab.findMany({
        where: {
          empresa_id: empresaId,
          estado: TesOrdenPagoEstado.BORRADOR,
          fecha_pago: { gte: pDesde, lt: pHasta },
        },
      }).then(r => r as any[]).catch(() => [] as any[]),
    ]);

    // Lookup nombres: clientes y proveedores almacenan nombre en tabla personas
    const clienteIds = [...new Set((cobros as any[]).map((c: any) => c.cliente_id).filter(Boolean))] as string[];
    const proveedorIds = [...new Set((pagos as any[]).map((p: any) => p.proveedor_id).filter(Boolean))] as string[];

    const clientesConPersona = clienteIds.length
      ? await this.prisma.clientes.findMany({ where: { id: { in: clienteIds } }, select: { id: true, persona_id: true } })
      : [];
    const proveedoresConPersona = proveedorIds.length
      ? await this.prisma.proveedores.findMany({ where: { id: { in: proveedorIds } }, select: { id: true, persona_id: true } }).catch(() => [] as any[])
      : [];

    const personaIds = [
      ...clientesConPersona.map((c: any) => c.persona_id),
      ...(proveedoresConPersona as any[]).map((p: any) => p.persona_id),
    ].filter(Boolean) as string[];

    const personas = personaIds.length
      ? await this.prisma.personas.findMany({ where: { id: { in: personaIds } }, select: { id: true, razon_social: true } })
      : [];
    const personasMap: Record<string, string> = Object.fromEntries(personas.map((p: any) => [p.id, p.razon_social]));

    const clientesMap: Record<string, string> = Object.fromEntries(
      clientesConPersona.map((c: any) => [c.id, personasMap[c.persona_id] || '—']),
    );
    const proveedoresMap: Record<string, string> = Object.fromEntries(
      (proveedoresConPersona as any[]).map((p: any) => [p.id, personasMap[p.persona_id] || '—']),
    );

    const cobrosProyectados = (cobros as any[]).reduce((s: number, c: any) => s + Number(c.saldo_pendiente || 0), 0);
    const pagosProyectados = (pagos as any[]).reduce((s: number, p: any) => s + Number(p.monto_total || 0), 0);

    return {
      ...realData,
      cobrosProyectados,
      pagosProyectados,
      resultadoProyectado: realData.resultado + cobrosProyectados - pagosProyectados,
      detalleCobros: (cobros as any[]).map((c: any) => ({
        cliente: clientesMap[c.cliente_id] || '—',
        monto: Number(c.saldo_pendiente),
        vencimiento: c.fecha_vencimiento,
      })),
      detallePagos: (pagos as any[]).map((p: any) => ({
        proveedor: proveedoresMap[p.proveedor_id] || '—',
        monto: Number(p.monto_total || 0),
        fecha: p.fecha_pago,
      })),
    };
  }

  // ── Extracto de cuenta ──────────────────────────────────────────────────────
  // Movimientos de una cuenta con saldo acumulado línea a línea

  async extractoCuenta(
    empresaId: string,
    filters: { cuenta_id: string; fecha_desde: string; fecha_hasta: string },
  ) {
    const cuenta = await this.prisma.tes_cuentas.findFirst({
      where: { id: filters.cuenta_id, empresa_id: empresaId },
    });
    const bancoNombre = cuenta?.banco_id
      ? (await this.prisma.bancos.findFirst({ where: { id: cuenta.banco_id }, select: { descripcion: true } }))?.descripcion || null
      : null;

    const exDesde = new Date(filters.fecha_desde + 'T00:00:00.000Z');
    const exHasta = new Date(new Date(filters.fecha_hasta + 'T00:00:00.000Z').getTime() + 86_400_000);

    const movimientos = await this.prisma.tes_movimientos.findMany({
      where: {
        empresa_id: empresaId,
        cuenta_id: filters.cuenta_id,
        estado: TesMovimientoEstado.CONFIRMADO,
        fecha: { gte: exDesde, lt: exHasta },
      },
      include: { detalles: { include: { categoria: { select: { nombre: true } } } } },
      // `fecha` es DATE: sin desempate, el saldo acumulado que se calcula abajo
      // línea a línea puede salir en distinto orden en cada corrida.
      orderBy: [{ fecha: 'asc' }, { created_at: 'asc' }, { id: 'asc' }],
    });

    // Calcular saldo anterior sumando ingresos y restando egresos antes del período
    const movsPrevios = await this.prisma.tes_movimientos.findMany({
      where: {
        empresa_id: empresaId,
        cuenta_id: filters.cuenta_id,
        estado: TesMovimientoEstado.CONFIRMADO,
        fecha: { lt: exDesde },
      },
      select: { tipo: true, monto: true },
    });

    const saldoInicial = Number(cuenta?.saldo_inicial || 0);
    const saldoAnterior = movsPrevios.reduce((s, m) => {
      return (m.tipo as TesMovimientoTipo) === TesMovimientoTipo.INGRESO ? s + Number(m.monto) : s - Number(m.monto);
    }, saldoInicial);

    let saldoAcumulado = saldoAnterior;
    const lineas = movimientos.map(m => {
      const tipo = m.tipo as TesMovimientoTipo;
      const monto = Number(m.monto);
      if (tipo === TesMovimientoTipo.INGRESO) saldoAcumulado += monto;
      else saldoAcumulado -= monto;
      return {
        fecha: m.fecha,
        descripcion: m.descripcion,
        categorias: m.detalles?.map(d => d.categoria?.nombre).filter(Boolean) || [],
        tipo,
        ingreso: tipo === TesMovimientoTipo.INGRESO ? monto : 0,
        egreso: tipo === TesMovimientoTipo.EGRESO ? monto : 0,
        saldo: saldoAcumulado,
      };
    });

    return {
      cuenta: {
        id: cuenta?.id,
        nombre: cuenta?.nombre,
        tipo: cuenta?.tipo,
        moneda: cuenta?.moneda,
        banco: bancoNombre,
      },
      fecha_desde: filters.fecha_desde,
      fecha_hasta: filters.fecha_hasta,
      saldoAnterior,
      saldoFinal: saldoAcumulado,
      lineas,
    };
  }

  // ── Cartera de cheques ──────────────────────────────────────────────────────

  async carteraCheques(
    empresaId: string,
    filters: { tipo_cheque?: string; estado?: string; cuenta_id?: string; fecha_desde?: string; fecha_hasta?: string },
  ) {
    const where: any = { empresa_id: empresaId };
    if (filters.tipo_cheque) where.tipo = filters.tipo_cheque;
    if (filters.estado) where.estado = filters.estado;
    if (filters.cuenta_id) where.cuenta_id = filters.cuenta_id;
    if (filters.fecha_desde || filters.fecha_hasta) {
      where.fecha_vencimiento = {};
      if (filters.fecha_desde) where.fecha_vencimiento.gte = new Date(filters.fecha_desde);
      if (filters.fecha_hasta) where.fecha_vencimiento.lte = new Date(filters.fecha_hasta);
    }

    const cheques = await this.prisma.tes_cheques.findMany({
      where,
      include: { cuenta: { select: { nombre: true } } },
      orderBy: { fecha_vencimiento: 'asc' },
    });

    const hoy = new Date();
    const resumen = {
      enCartera: cheques.filter(c => (c.estado as TesChequeEstado) === TesChequeEstado.EN_CARTERA).length,
      depositados: cheques.filter(c => (c.estado as TesChequeEstado) === TesChequeEstado.DEPOSITADO).length,
      vencenEn7Dias: cheques.filter(c => {
        if ((c.estado as TesChequeEstado) !== TesChequeEstado.EN_CARTERA || !c.fecha_vencimiento) return false;
        const dias = Math.ceil((new Date(c.fecha_vencimiento).getTime() - hoy.getTime()) / 86400000);
        return dias >= 0 && dias <= 7;
      }).length,
      totalCartera: cheques.filter(c => (c.estado as TesChequeEstado) === TesChequeEstado.EN_CARTERA).reduce((s, c) => s + Number(c.monto), 0),
    };

    return {
      resumen,
      cheques: cheques.map(c => ({
        id: c.id,
        numero: c.numero_cheque,
        tipo: c.tipo,
        banco: c.banco_emisor,
        titular: c.titular,
        monto: Number(c.monto),
        moneda: c.moneda,
        emision: c.fecha_emision,
        vencimiento: c.fecha_vencimiento,
        recepcion: c.created_at,
        estado: c.estado,
        cuenta: c.cuenta?.nombre,
      })),
    };
  }

  // ── Excel helpers ───────────────────────────────────────────────────────────

  private fmtDate(d: Date | string | null | undefined) {
    if (!d) return '';
    return new Date(d).toLocaleDateString('es-PY');
  }

  private makeBuffer(wb: XLSX.WorkBook): Buffer {
    return XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }) as Buffer;
  }

  async posicionCajaExcel(empresaId: string): Promise<Buffer> {
    const data = await this.posicionCaja(empresaId);
    const rows: Record<string, unknown>[] = [];
    for (const grupo of data.resumen) {
      for (const c of grupo.cuentas) {
        rows.push({
          Cuenta: c.nombre,
          Tipo: c.tipo,
          Moneda: c.moneda,
          Banco: c.banco?.nombre || '—',
          'N° Cuenta': c.numero_cuenta || '—',
          'Saldo Actual': Number(c.saldo_actual),
        });
      }
      rows.push({ Cuenta: `TOTAL ${grupo.moneda}`, 'Saldo Actual': grupo.total });
      rows.push({});
    }
    const ws = XLSX.utils.json_to_sheet(rows);
    const wb = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(wb, ws, 'Posición de Caja');
    return this.makeBuffer(wb);
  }

  async cashFlowRealExcel(empresaId: string, filters: { fecha_desde: string; fecha_hasta: string; cuenta_id?: string; moneda?: string }): Promise<Buffer> {
    const data = await this.cashFlowReal(empresaId, filters);
    const wb = XLSX.utils.book_new();

    // Hoja Resumen
    XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet([
      { Concepto: 'Período', Valor: `${filters.fecha_desde} → ${filters.fecha_hasta}` },
      { Concepto: 'Ingresos', Valor: data.totalIngresos },
      { Concepto: 'Egresos', Valor: data.totalEgresos },
      { Concepto: 'Resultado', Valor: data.resultado },
    ]), 'Resumen');

    // Hoja Por Categoría
    XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(
      data.porCategoria.map(c => ({ Categoría: c.nombre, Tipo: c.tipo, Total: c.total, Cantidad: c.cantidad })),
    ), 'Por Categoría');

    // Hoja Movimientos
    XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(
      data.movimientos.map(m => ({
        Fecha: this.fmtDate(m.fecha),
        Cuenta: m.cuenta,
        Moneda: m.moneda,
        Tipo: m.tipo,
        Descripción: m.descripcion,
        Categorías: m.categorias.join(', '),
        Monto: m.monto,
      })),
    ), 'Movimientos');

    return this.makeBuffer(wb);
  }

  async cashFlowProyectadoExcel(empresaId: string, filters: { fecha_desde: string; fecha_hasta: string }): Promise<Buffer> {
    const data = await this.cashFlowProyectado(empresaId, filters);
    const wb = XLSX.utils.book_new();

    XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet([
      { Concepto: 'Ingresos reales', Valor: data.totalIngresos },
      { Concepto: 'Egresos reales', Valor: data.totalEgresos },
      { Concepto: 'Resultado real', Valor: data.resultado },
      { Concepto: 'Cobros proyectados (CxC)', Valor: data.cobrosProyectados },
      { Concepto: 'Pagos proyectados (CxP)', Valor: data.pagosProyectados },
      { Concepto: 'Resultado proyectado', Valor: data.resultadoProyectado },
    ]), 'Resumen Proyectado');

    if (data.detalleCobros?.length) {
      XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(
        data.detalleCobros.map((c: any) => ({ Cliente: c.cliente, Vencimiento: this.fmtDate(c.vencimiento), Monto: c.monto })),
      ), 'CxC Proyectados');
    }
    if (data.detallePagos?.length) {
      XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(
        data.detallePagos.map((p: any) => ({ Proveedor: p.proveedor, Fecha: this.fmtDate(p.fecha), Monto: p.monto })),
      ), 'CxP Proyectados');
    }

    return this.makeBuffer(wb);
  }

  async extractoCuentaExcel(empresaId: string, filters: { cuenta_id: string; fecha_desde: string; fecha_hasta: string }): Promise<Buffer> {
    const data = await this.extractoCuenta(empresaId, filters);
    const wb = XLSX.utils.book_new();

    XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet([
      { Concepto: 'Cuenta', Valor: data.cuenta?.nombre },
      { Concepto: 'Período', Valor: `${filters.fecha_desde} → ${filters.fecha_hasta}` },
      { Concepto: 'Saldo anterior', Valor: data.saldoAnterior },
      { Concepto: 'Saldo final', Valor: data.saldoFinal },
    ]), 'Resumen');

    XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(
      data.lineas.map(l => ({
        Fecha: this.fmtDate(l.fecha),
        Descripción: l.descripcion,
        Categorías: l.categorias.join(', '),
        Tipo: l.tipo,
        Ingreso: l.ingreso || '',
        Egreso: l.egreso || '',
        Saldo: l.saldo,
      })),
    ), 'Extracto');

    return this.makeBuffer(wb);
  }

  async carteraChequesExcel(empresaId: string, filters: { tipo_cheque?: string; estado?: string; cuenta_id?: string; fecha_desde?: string; fecha_hasta?: string }): Promise<Buffer> {
    const data = await this.carteraCheques(empresaId, filters);
    const wb = XLSX.utils.book_new();

    XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet([
      { Concepto: 'En Cartera', Valor: data.resumen.enCartera },
      { Concepto: 'Depositados', Valor: data.resumen.depositados },
      { Concepto: 'Vencen en 7 días', Valor: data.resumen.vencenEn7Dias },
      { Concepto: 'Total en cartera', Valor: data.resumen.totalCartera },
    ]), 'Resumen');

    XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(
      data.cheques.map(c => ({
        Número: c.numero,
        Tipo: c.tipo,
        Banco: c.banco || '—',
        Titular: c.titular || '—',
        Moneda: c.moneda,
        Emisión: this.fmtDate(c.emision),
        Vencimiento: this.fmtDate(c.vencimiento),
        Recepción: this.fmtDate(c.recepcion),
        Estado: c.estado,
        Cuenta: c.cuenta || '—',
        Monto: c.monto,
      })),
    ), 'Cheques');

    return this.makeBuffer(wb);
  }
}
