import { BadGatewayException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import * as XLSX from 'xlsx';
import { envs } from '../../config/envs';
import { PrismaService } from '../../prisma/prisma.service';

@Injectable()
export class ReportesService {
  private readonly logger = new Logger(ReportesService.name);
  private readonly kudeUrl = envs.apiGeneradorPDF;

  constructor(private readonly prisma: PrismaService) {}

  // ── Helpers privados ──────────────────────────────────────────────────────

  private async getEmpresaHeader(empresaId: string) {
    const e = await this.prisma.empresas.findUnique({
      where: { id: empresaId },
      select: { razon_social: true, nombre_fantasia: true, ruc: true, logo: true },
    });
    // nombre_fantasia: el PDF lo pone arriba y la razón social debajo (mismo criterio que la factura).
    return {
      razon_social: e?.razon_social ?? '',
      nombre_fantasia: e?.nombre_fantasia ?? null,
      ruc: e?.ruc ?? '',
      logo_url: e?.logo ?? null,
    };
  }

  private async callMsvKude(endpoint: string, payload: Record<string, unknown>): Promise<Buffer> {
    const url = `${this.kudeUrl}/api/contabilidad/${endpoint}`;
    try {
      const response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ ...payload, tipo: 'view' }),
      });
      if (!response.ok) {
        const err = await response.text();
        this.logger.error(`msv-kude ${url} → ${response.status}: ${err}`);
        throw new BadGatewayException('Error generando PDF en msv-kude');
      }
      const buf = await response.arrayBuffer();
      return Buffer.from(buf);
    } catch (e) {
      if (e instanceof BadGatewayException) throw e;
      throw new BadGatewayException(`No se pudo conectar a msv-kude: ${e instanceof Error ? e.message : 'unknown'}`);
    }
  }

  // ── Libro Diario ──────────────────────────────────────────────────────────

  async libroDiario(empresaId: string, filtros: { periodoId?: string; desde?: string; hasta?: string; centroCostoId?: string }) {
    const where: any = {
      empresa_id: empresaId,
      estado: 'CONFIRMADO',
    };
    if (filtros.periodoId) where.periodo_id = filtros.periodoId;
    if (filtros.desde || filtros.hasta) {
      where.fecha = {};
      if (filtros.desde) where.fecha.gte = new Date(filtros.desde);
      if (filtros.hasta) where.fecha.lte = new Date(filtros.hasta);
    }
    // Filtro por centro de costo: solo asientos que tengan al menos una línea con ese CC
    if (filtros.centroCostoId) {
      where.lineas = { some: { centro_costo_id: filtros.centroCostoId } };
    }

    const asientos = await this.prisma.cont_asientos.findMany({
      where,
      include: {
        lineas: {
          include: {
            cuenta: { select: { codigo: true, descripcion: true } },
            centro_costo: { select: { codigo: true, descripcion: true } },
          },
          orderBy: { orden: 'asc' },
        },
        periodo: { select: { mes: true, anio: true } },
        documento: { select: { tipo: true, numero_documento: true } },
      },
      orderBy: [{ fecha: 'asc' }, { numero: 'asc' }],
    });

    return asientos.map((a) => ({
      id: a.id,
      numero: a.numero,
      fecha: a.fecha,
      glosa: a.glosa,
      moneda_origen: a.moneda_origen,
      total_debe_pyg: Number(a.total_debe_pyg),
      total_haber_pyg: Number(a.total_haber_pyg),
      periodo: a.periodo,
      documento: a.documento,
      lineas: a.lineas.map((l) => ({
        orden: l.orden,
        cuenta_codigo: l.cuenta?.codigo,
        cuenta_descripcion: l.cuenta?.descripcion,
        centro_costo: l.centro_costo ? `${l.centro_costo.codigo} — ${l.centro_costo.descripcion}` : null,
        descripcion: l.descripcion,
        debe_moneda: Number(l.debe_moneda),
        haber_moneda: Number(l.haber_moneda),
        debe_pyg: Number(l.debe_pyg),
        haber_pyg: Number(l.haber_pyg),
      })),
    }));
  }

  // ── Libro Mayor ───────────────────────────────────────────────────────────

  async libroMayor(empresaId: string, cuentaId: string, filtros: { periodoId?: string; desde?: string; hasta?: string; centroCostoId?: string }) {
    const cuenta = await this.prisma.cont_plan_cuentas.findFirst({
      where: { id: cuentaId, OR: [{ empresa_id: empresaId }, { empresa_id: null }] },
    });
    if (!cuenta) throw new NotFoundException('Cuenta no encontrada');

    const whereAsiento: any = { empresa_id: empresaId, estado: 'CONFIRMADO' };
    if (filtros.periodoId) whereAsiento.periodo_id = filtros.periodoId;
    if (filtros.desde || filtros.hasta) {
      whereAsiento.fecha = {};
      if (filtros.desde) whereAsiento.fecha.gte = new Date(filtros.desde);
      if (filtros.hasta) whereAsiento.fecha.lte = new Date(filtros.hasta);
    }

    const whereDet: any = { cuenta_id: cuentaId, asiento: whereAsiento };
    if (filtros.centroCostoId) whereDet.centro_costo_id = filtros.centroCostoId;

    const lineas = await this.prisma.cont_asientos_det.findMany({
      where: whereDet,
      include: {
        asiento: { select: { numero: true, fecha: true, glosa: true, documento: { select: { numero_documento: true } } } },
      },
      orderBy: [{ asiento: { fecha: 'asc' } }, { asiento: { numero: 'asc' } }, { orden: 'asc' }],
    });

    let saldoAcumulado = 0;
    const movimientos = lineas.map((l) => {
      const debe = Number(l.debe_pyg);
      const haber = Number(l.haber_pyg);
      saldoAcumulado += cuenta.naturaleza === 'DEUDORA' ? debe - haber : haber - debe;
      return {
        fecha: l.asiento.fecha,
        numero_asiento: l.asiento.numero,
        glosa: l.asiento.glosa,
        numero_documento: l.asiento.documento?.numero_documento ?? null,
        descripcion: l.descripcion,
        debe_pyg: debe,
        haber_pyg: haber,
        saldo_acumulado: saldoAcumulado,
      };
    });

    return {
      cuenta: { id: cuenta.id, codigo: cuenta.codigo, descripcion: cuenta.descripcion, naturaleza: cuenta.naturaleza },
      movimientos,
      totales: {
        total_debe: movimientos.reduce((s, m) => s + m.debe_pyg, 0),
        total_haber: movimientos.reduce((s, m) => s + m.haber_pyg, 0),
        saldo_final: saldoAcumulado,
      },
    };
  }

  // ── Balance de Comprobación ───────────────────────────────────────────────

  async balanceComprobacion(empresaId: string, filtros: { periodoId?: string; ejercicioId?: string; desde?: string; hasta?: string }) {
    const whereAsiento: any = { empresa_id: empresaId, estado: 'CONFIRMADO' };
    if (filtros.periodoId) whereAsiento.periodo_id = filtros.periodoId;
    if (filtros.ejercicioId) {
      const periodos = await this.prisma.cont_periodos.findMany({
        where: { empresa_id: empresaId, ejercicio_id: filtros.ejercicioId },
        select: { id: true },
      });
      whereAsiento.periodo_id = { in: periodos.map((p) => p.id) };
    }
    if (filtros.desde || filtros.hasta) {
      whereAsiento.fecha = {};
      if (filtros.desde) whereAsiento.fecha.gte = new Date(filtros.desde);
      if (filtros.hasta) whereAsiento.fecha.lte = new Date(filtros.hasta);
    }

    // Agrupamos por cuenta_id con sumas
    const grupos = await this.prisma.cont_asientos_det.groupBy({
      by: ['cuenta_id'],
      where: { asiento: whereAsiento },
      _sum: { debe_pyg: true, haber_pyg: true },
    });

    if (grupos.length === 0) return { filas: [], totales: { total_debe: 0, total_haber: 0, saldo_deudor: 0, saldo_acreedor: 0 } };

    const cuentaIds = grupos.map((g) => g.cuenta_id);
    const cuentas = await this.prisma.cont_plan_cuentas.findMany({
      where: { id: { in: cuentaIds } },
      select: { id: true, codigo: true, descripcion: true, tipo: true, naturaleza: true },
    });
    const cuentaMap = new Map(cuentas.map((c) => [c.id, c]));

    const filas = grupos
      .map((g) => {
        const cuenta = cuentaMap.get(g.cuenta_id);
        const debe = Number(g._sum.debe_pyg ?? 0);
        const haber = Number(g._sum.haber_pyg ?? 0);
        const saldo_deudor = debe > haber ? debe - haber : 0;
        const saldo_acreedor = haber > debe ? haber - debe : 0;
        return {
          cuenta_id: g.cuenta_id,
          codigo: cuenta?.codigo ?? '',
          descripcion: cuenta?.descripcion ?? '',
          tipo: cuenta?.tipo,
          naturaleza: cuenta?.naturaleza,
          total_debe: debe,
          total_haber: haber,
          saldo_deudor,
          saldo_acreedor,
        };
      })
      .sort((a, b) => a.codigo.localeCompare(b.codigo));

    const totales = filas.reduce(
      (acc, f) => ({
        total_debe: acc.total_debe + f.total_debe,
        total_haber: acc.total_haber + f.total_haber,
        saldo_deudor: acc.saldo_deudor + f.saldo_deudor,
        saldo_acreedor: acc.saldo_acreedor + f.saldo_acreedor,
      }),
      { total_debe: 0, total_haber: 0, saldo_deudor: 0, saldo_acreedor: 0 },
    );

    return { filas, totales };
  }

  // ── Estado de Resultados ─────────────────────────────────────────────────

  async estadoResultados(empresaId: string, filtros: { ejercicioId?: string; periodoId?: string; centroCostoId?: string }) {
    const saldosPorTipo = await this.getSaldosPorTipo(empresaId, filtros, true);

    const ingresos = saldosPorTipo.filter((s) => s.tipo === 'INGRESO');
    const costos = saldosPorTipo.filter((s) => s.tipo === 'COSTO');
    const gastos = saldosPorTipo.filter((s) => s.tipo === 'GASTO');

    const totalIngresos = ingresos.reduce((s, c) => s + c.saldo, 0);
    const totalCostos = costos.reduce((s, c) => s + c.saldo, 0);
    const totalGastos = gastos.reduce((s, c) => s + c.saldo, 0);
    const utilidadBruta = totalIngresos - totalCostos;
    const utilidadNeta = utilidadBruta - totalGastos;

    return {
      ingresos: { cuentas: ingresos, total: totalIngresos },
      costos: { cuentas: costos, total: totalCostos },
      gastos: { cuentas: gastos, total: totalGastos },
      utilidad_bruta: utilidadBruta,
      utilidad_neta: utilidadNeta,
    };
  }

  // ── Balance General ───────────────────────────────────────────────────────

  async balanceGeneral(empresaId: string, filtros: { ejercicioId?: string; periodoId?: string }) {
    // getSaldosPorTipo devuelve TODOS los tipos incluyendo INGRESO/COSTO/GASTO
    const todos = await this.getSaldosPorTipo(empresaId, filtros);

    const activos = todos.filter((s) => s.tipo === 'ACTIVO');
    const pasivos = todos.filter((s) => s.tipo === 'PASIVO');
    const patrimonio = todos.filter((s) => s.tipo === 'PATRIMONIO');

    // Calcular utilidad del período a partir de los saldos de resultado
    // (las cuentas de Ingresos/Costos/Gastos NO aparecen en el Balance General,
    //  pero su resultado neto debe mostrarse en Patrimonio como "Resultado del Ejercicio")
    const totalIngresos = todos.filter((s) => s.tipo === 'INGRESO').reduce((s, c) => s + c.saldo, 0);
    const totalCostos = todos.filter((s) => s.tipo === 'COSTO').reduce((s, c) => s + c.saldo, 0);
    const totalGastos = todos.filter((s) => s.tipo === 'GASTO').reduce((s, c) => s + c.saldo, 0);
    const utilidadNeta = totalIngresos - totalCostos - totalGastos;

    // En ejercicios CERRADOS el asiento de cierre ya registró la utilidad en la
    // cuenta 3.2.1.01 (PATRIMONIO), así que no la duplicamos.
    // En ejercicios ABIERTOS la agregamos dinámicamente para que el balance cuadre.
    let ejercicioCerrado = false;
    if (filtros.ejercicioId) {
      const ej = await this.prisma.cont_ejercicios.findUnique({
        where: { id: filtros.ejercicioId },
        select: { estado: true },
      });
      ejercicioCerrado = ej?.estado === 'CERRADO';
    }

    const patrimonioFinal = [...patrimonio];
    if (!ejercicioCerrado && Math.abs(utilidadNeta) > 0) {
      patrimonioFinal.push({
        cuenta_id: '__resultado_calculado__',
        codigo: '(calc)',
        descripcion: utilidadNeta >= 0 ? 'Resultado del Ejercicio (Utilidad)' : 'Resultado del Ejercicio (Pérdida)',
        tipo: 'PATRIMONIO',
        naturaleza: 'ACREEDORA',
        debe: 0,
        haber: 0,
        saldo: utilidadNeta,
      });
    }

    const totalActivos = activos.reduce((s, c) => s + c.saldo, 0);
    const totalPasivos = pasivos.reduce((s, c) => s + c.saldo, 0);
    const totalPatrimonio = patrimonioFinal.reduce((s, c) => s + c.saldo, 0);

    return {
      activos: { cuentas: activos, total: totalActivos },
      pasivos: { cuentas: pasivos, total: totalPasivos },
      patrimonio: { cuentas: patrimonioFinal, total: totalPatrimonio },
      utilidad_neta: utilidadNeta,
      ejercicio_cerrado: ejercicioCerrado,
      total_pasivo_patrimonio: totalPasivos + totalPatrimonio,
      cuadra: Math.abs(totalActivos - (totalPasivos + totalPatrimonio)) <= 1,
    };
  }

  // ── Excel exports ─────────────────────────────────────────────────────────

  async libroDiarioExcel(empresaId: string, filtros: { periodoId?: string; desde?: string; hasta?: string; centroCostoId?: string }): Promise<Buffer> {
    const data = await this.libroDiario(empresaId, filtros);

    const rows: any[] = [];
    for (const asiento of data) {
      for (const linea of asiento.lineas) {
        rows.push({
          Fecha: new Date(asiento.fecha).toLocaleDateString('es-PY'),
          'N° Asiento': asiento.numero,
          Glosa: asiento.glosa ?? '',
          'Cód. Cuenta': linea.cuenta_codigo ?? '',
          Cuenta: linea.cuenta_descripcion ?? '',
          'Centro de Costo': linea.centro_costo ?? '',
          Descripción: linea.descripcion ?? '',
          'Debe (Gs)': linea.debe_pyg,
          'Haber (Gs)': linea.haber_pyg,
        });
      }
    }

    const ws = XLSX.utils.json_to_sheet(rows);
    const wb = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(wb, ws, 'Libro Diario');
    return XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }) as Buffer;
  }

  async balanceComprobacionExcel(empresaId: string, filtros: { periodoId?: string; ejercicioId?: string }): Promise<Buffer> {
    const { filas, totales } = await this.balanceComprobacion(empresaId, filtros);

    const rows = [
      ...filas.map((f) => ({
        Código: f.codigo,
        Cuenta: f.descripcion,
        Tipo: f.tipo,
        'Total Debe (Gs)': f.total_debe,
        'Total Haber (Gs)': f.total_haber,
        'Saldo Deudor (Gs)': f.saldo_deudor,
        'Saldo Acreedor (Gs)': f.saldo_acreedor,
      })),
      {
        Código: 'TOTALES',
        Cuenta: '',
        Tipo: '',
        'Total Debe (Gs)': totales.total_debe,
        'Total Haber (Gs)': totales.total_haber,
        'Saldo Deudor (Gs)': totales.saldo_deudor,
        'Saldo Acreedor (Gs)': totales.saldo_acreedor,
      },
    ];

    const ws = XLSX.utils.json_to_sheet(rows);
    const wb = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(wb, ws, 'Balance Comprobación');
    return XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }) as Buffer;
  }

  // ── PDF via msv-kude ─────────────────────────────────────────────────────

  async libroDiarioPdf(empresaId: string, filtros: { periodoId?: string; desde?: string; hasta?: string; titulo?: string; periodo?: string; centroCostoId?: string }): Promise<Buffer> {
    const [empresa, data] = await Promise.all([
      this.getEmpresaHeader(empresaId),
      this.libroDiario(empresaId, filtros),
    ]);
    const totales = data.reduce((acc, a) => ({
      total_debe: acc.total_debe + a.total_debe_pyg,
      total_haber: acc.total_haber + a.total_haber_pyg,
    }), { total_debe: 0, total_haber: 0 });
    return this.callMsvKude('libro-diario', {
      empresa,
      titulo: filtros.titulo ?? 'Libro Diario',
      periodo: filtros.periodo ?? '',
      filename: 'libro-diario.pdf',
      asientos: data.map((a) => ({
        numero: a.numero,
        fecha: a.fecha,
        glosa: a.glosa,
        lineas: a.lineas,
        total_debe: a.total_debe_pyg,
        total_haber: a.total_haber_pyg,
      })),
      totales,
    });
  }

  async libroMayorPdf(empresaId: string, cuentaId: string, filtros: { periodoId?: string; desde?: string; hasta?: string; centroCostoId?: string }): Promise<Buffer> {
    const [empresa, data] = await Promise.all([
      this.getEmpresaHeader(empresaId),
      this.libroMayor(empresaId, cuentaId, filtros),
    ]);
    return this.callMsvKude('libro-mayor', {
      empresa,
      titulo: `Libro Mayor — ${data.cuenta.codigo} ${data.cuenta.descripcion}`,
      periodo: '',
      filename: `libro-mayor-${data.cuenta.codigo}.pdf`,
      ...data,
    });
  }

  async balanceComprobacionPdf(empresaId: string, filtros: { periodoId?: string; ejercicioId?: string; desde?: string; hasta?: string; titulo?: string; periodo?: string }): Promise<Buffer> {
    const [empresa, data] = await Promise.all([
      this.getEmpresaHeader(empresaId),
      this.balanceComprobacion(empresaId, filtros),
    ]);
    return this.callMsvKude('balance-comprobacion', {
      empresa,
      titulo: filtros.titulo ?? 'Balance de Comprobación',
      periodo: filtros.periodo ?? '',
      filename: 'balance-comprobacion.pdf',
      ...data,
    });
  }

  async estadoResultadosPdf(empresaId: string, filtros: { ejercicioId?: string; periodoId?: string; titulo?: string; periodo?: string; centroCostoId?: string }): Promise<Buffer> {
    const [empresa, data] = await Promise.all([
      this.getEmpresaHeader(empresaId),
      this.estadoResultados(empresaId, filtros),
    ]);
    return this.callMsvKude('estado-resultados', {
      empresa,
      titulo: filtros.titulo ?? 'Estado de Resultados',
      periodo: filtros.periodo ?? '',
      filename: 'estado-resultados.pdf',
      ...data,
    });
  }

  async balanceGeneralPdf(empresaId: string, filtros: { ejercicioId?: string; periodoId?: string; titulo?: string; periodo?: string }): Promise<Buffer> {
    const [empresa, data] = await Promise.all([
      this.getEmpresaHeader(empresaId),
      this.balanceGeneral(empresaId, filtros),
    ]);
    return this.callMsvKude('balance-general', {
      empresa,
      titulo: filtros.titulo ?? 'Balance General',
      periodo: filtros.periodo ?? '',
      filename: 'balance-general.pdf',
      ...data,
    });
  }

  // ── Helpers privados ──────────────────────────────────────────────────────

  private async getSaldosPorTipo(empresaId: string, filtros: { ejercicioId?: string; periodoId?: string; centroCostoId?: string }, acumularHastaPeriodo = false) {
    const whereAsiento: any = { empresa_id: empresaId, estado: 'CONFIRMADO' };
    if (filtros.periodoId && acumularHastaPeriodo) {
      // Acumula todos los períodos del ejercicio hasta el período seleccionado (inclusive)
      const periodo = await this.prisma.cont_periodos.findUnique({ where: { id: filtros.periodoId }, select: { ejercicio_id: true, numero: true } });
      if (periodo) {
        const periodosAcumulados = await this.prisma.cont_periodos.findMany({
          where: { empresa_id: empresaId, ejercicio_id: periodo.ejercicio_id, numero: { lte: periodo.numero } },
          select: { id: true },
        });
        whereAsiento.periodo_id = { in: periodosAcumulados.map((p) => p.id) };
      }
    } else if (filtros.periodoId) {
      whereAsiento.periodo_id = filtros.periodoId;
    } else if (filtros.ejercicioId) {
      const periodos = await this.prisma.cont_periodos.findMany({
        where: { empresa_id: empresaId, ejercicio_id: filtros.ejercicioId },
        select: { id: true },
      });
      whereAsiento.periodo_id = { in: periodos.map((p) => p.id) };
    }

    const whereDet: any = { asiento: whereAsiento };
    if (filtros.centroCostoId) whereDet.centro_costo_id = filtros.centroCostoId;

    const grupos = await this.prisma.cont_asientos_det.groupBy({
      by: ['cuenta_id'],
      where: whereDet,
      _sum: { debe_pyg: true, haber_pyg: true },
    });

    if (grupos.length === 0) return [];

    const cuentaIds = grupos.map((g) => g.cuenta_id);
    const cuentas = await this.prisma.cont_plan_cuentas.findMany({
      where: { id: { in: cuentaIds } },
      select: { id: true, codigo: true, descripcion: true, tipo: true, naturaleza: true },
    });
    const cuentaMap = new Map(cuentas.map((c) => [c.id, c]));

    return grupos
      .map((g) => {
        const cuenta = cuentaMap.get(g.cuenta_id);
        const debe = Number(g._sum.debe_pyg ?? 0);
        const haber = Number(g._sum.haber_pyg ?? 0);
        const saldo = cuenta?.naturaleza === 'DEUDORA' ? debe - haber : haber - debe;
        // Cuentas contra-activo (tipo ACTIVO, naturaleza ACREEDORA, ej: Depreciación Acumulada):
        // su saldo se devuelve negativo para que reste del total de activos en el Balance General.
        const saldoFinal = (cuenta?.tipo === 'ACTIVO' && cuenta?.naturaleza === 'ACREEDORA') ? -saldo : saldo;
        return {
          cuenta_id: g.cuenta_id,
          codigo: cuenta?.codigo ?? '',
          descripcion: cuenta?.descripcion ?? '',
          tipo: cuenta?.tipo ?? '',
          naturaleza: cuenta?.naturaleza ?? '',
          debe,
          haber,
          saldo: saldoFinal,
        };
      })
      .sort((a, b) => a.codigo.localeCompare(b.codigo));
  }
}
