import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { AuditService } from '../../audit/audit.service';
import { PrismaService } from '../../prisma/prisma.service';

@Injectable()
export class EjerciciosService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly audit: AuditService,
  ) {}

  async listar(empresaId: string) {
    return this.prisma.cont_ejercicios.findMany({
      where: { empresa_id: empresaId },
      include: { periodos: { orderBy: { numero: 'asc' } } },
      orderBy: { anio: 'desc' },
    });
  }

  async crear(empresaId: string, data: { anio: number; generarPeriodosAuto: boolean }, usuarioId?: string) {
    const existente = await this.prisma.cont_ejercicios.findFirst({
      where: { empresa_id: empresaId, anio: data.anio },
    });
    if (existente) throw new BadRequestException(`Ya existe un ejercicio para el año ${data.anio}`);

    const fechaInicio = new Date(data.anio, 0, 1);  // 01/01/año
    const fechaFin = new Date(data.anio, 11, 31);   // 31/12/año

    const ejercicio = await this.prisma.cont_ejercicios.create({
      data: {
        empresa_id: empresaId,
        anio: data.anio,
        fecha_inicio: fechaInicio,
        fecha_fin: fechaFin,
        estado: 'ABIERTO',
      },
    });

    if (data.generarPeriodosAuto) {
      await this.generarPeriodos(ejercicio.id, empresaId, data.anio);
    }

    const result = await this.prisma.cont_ejercicios.findUnique({
      where: { id: ejercicio.id },
      include: { periodos: { orderBy: { numero: 'asc' } } },
    });
    this.audit.log({ empresa_id: empresaId, user_id: usuarioId, action: 'CREATE', entity_type: 'cont_ejercicios', entity_id: ejercicio.id, descripcion: `Ejercicio ${data.anio} creado` });
    return result;
  }

  async cerrar(ejercicioId: string, empresaId: string, usuarioId?: string) {
    const ejercicio = await this.prisma.cont_ejercicios.findFirst({
      where: { id: ejercicioId, empresa_id: empresaId },
      include: { periodos: { orderBy: { numero: 'desc' } } },
    });
    if (!ejercicio) throw new NotFoundException('Ejercicio no encontrado');
    if (ejercicio.estado === 'CERRADO') throw new BadRequestException('El ejercicio ya está cerrado');

    const periodosAbiertos = ejercicio.periodos.filter((p) => p.estado === 'ABIERTO' || p.estado === 'AJUSTE');
    if (periodosAbiertos.length > 0) {
      throw new BadRequestException(`Hay ${periodosAbiertos.length} período(s) aún abierto(s). Cerrá todos los períodos antes de cerrar el ejercicio`);
    }

    // ── Asiento de cierre contable ───────────────────────────────────────────
    // Al cerrar el ejercicio se genera automáticamente un asiento que zeroa las
    // cuentas de Ingresos/Costos/Gastos y transfiere el resultado neto a Patrimonio
    // (cuenta 3.2.1.01 "Resultado del Ejercicio").
    await this._generarAsientoCierre(ejercicio, empresaId, usuarioId);

    const result = await this.prisma.cont_ejercicios.update({
      where: { id: ejercicioId },
      data: { estado: 'CERRADO', updated_at: new Date() },
    });
    this.audit.log({ empresa_id: empresaId, user_id: usuarioId, action: 'CLOSE', entity_type: 'cont_ejercicios', entity_id: ejercicioId, descripcion: `Ejercicio ${ejercicio.anio} cerrado` });
    return result;
  }

  private async _generarAsientoCierre(
    ejercicio: { id: string; anio: number; periodos: { id: string; numero: number; fecha_fin: Date }[] },
    empresaId: string,
    usuarioId?: string,
  ) {
    const periodoIds = ejercicio.periodos.map((p) => p.id);

    // Obtener sumas de debe/haber por cuenta para el ejercicio completo
    const grupos = await this.prisma.cont_asientos_det.groupBy({
      by: ['cuenta_id'],
      where: { asiento: { empresa_id: empresaId, estado: 'CONFIRMADO', periodo_id: { in: periodoIds } } },
      _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 }, tipo: { in: ['INGRESO', 'COSTO', 'GASTO'] } },
      select: { id: true, codigo: true, descripcion: true, tipo: true, naturaleza: true },
    });
    if (cuentas.length === 0) return; // Sin cuentas de resultado, no hay nada que cerrar

    const cuentaMap = new Map(cuentas.map((c) => [c.id, c]));

    // Calcular saldo de cada cuenta de resultado (solo las que tienen movimientos)
    const lineasResultado = grupos
      .filter((g) => cuentaMap.has(g.cuenta_id))
      .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);
        // Saldo normal: INGRESO (acreedora) = haber - debe | COSTO/GASTO (deudora) = debe - haber
        const saldo = cuenta.naturaleza === 'DEUDORA' ? debe - haber : haber - debe;
        return { cuenta, saldo };
      })
      .filter((r) => Math.abs(r.saldo) > 0);

    if (lineasResultado.length === 0) return;

    // Cuenta 3.2.1.01 — Resultado del Ejercicio (destino en Patrimonio)
    const cuentaResultado = await this.prisma.cont_plan_cuentas.findFirst({
      where: {
        OR: [{ empresa_id: empresaId }, { empresa_id: null }],
        codigo: '3.2.1.01',
        acepta_movimientos: true,
      },
    });
    if (!cuentaResultado) {
      throw new BadRequestException(
        'No se encontró la cuenta "Resultado del Ejercicio" (3.2.1.01) en el plan de cuentas. Agregála antes de cerrar el ejercicio.',
      );
    }

    const totalIngresos = lineasResultado.filter((r) => r.cuenta.tipo === 'INGRESO').reduce((s, r) => s + r.saldo, 0);
    const totalCostosGastos = lineasResultado.filter((r) => r.cuenta.tipo !== 'INGRESO').reduce((s, r) => s + r.saldo, 0);
    const utilidadNeta = totalIngresos - totalCostosGastos;

    // Construir líneas del asiento:
    // · INGRESO (naturaleza acreedora, saldo en haber) → DEBE para zerear
    // · COSTO/GASTO (naturaleza deudora, saldo en debe) → HABER para zerear
    // · 3.2.1.01 → recibe el resultado neto (HABER si ganancia, DEBE si pérdida)
    const lineas: {
      cuenta_id: string;
      descripcion: string;
      debe_moneda: number;
      haber_moneda: number;
      debe_pyg: number;
      haber_pyg: number;
      orden: number;
    }[] = [];
    let orden = 1;

    for (const r of lineasResultado.filter((rl) => rl.cuenta.tipo === 'INGRESO')) {
      lineas.push({ cuenta_id: r.cuenta.id, descripcion: `Cierre: ${r.cuenta.descripcion}`, debe_moneda: r.saldo, haber_moneda: 0, debe_pyg: r.saldo, haber_pyg: 0, orden: orden++ });
    }
    for (const r of lineasResultado.filter((rl) => rl.cuenta.tipo !== 'INGRESO')) {
      lineas.push({ cuenta_id: r.cuenta.id, descripcion: `Cierre: ${r.cuenta.descripcion}`, debe_moneda: 0, haber_moneda: r.saldo, debe_pyg: 0, haber_pyg: r.saldo, orden: orden++ });
    }
    if (Math.abs(utilidadNeta) > 0) {
      const esGanancia = utilidadNeta > 0;
      lineas.push({
        cuenta_id: cuentaResultado.id,
        descripcion: `Resultado del Ejercicio ${ejercicio.anio}${esGanancia ? '' : ' (Pérdida)'}`,
        debe_moneda: esGanancia ? 0 : Math.abs(utilidadNeta),
        haber_moneda: esGanancia ? utilidadNeta : 0,
        debe_pyg: esGanancia ? 0 : Math.abs(utilidadNeta),
        haber_pyg: esGanancia ? utilidadNeta : 0,
        orden: orden++,
      });
    }

    const totalDebe = lineas.reduce((s, l) => s + l.debe_pyg, 0);
    const totalHaber = lineas.reduce((s, l) => s + l.haber_pyg, 0);

    // Número correlativo del próximo asiento
    const lastAsiento = await this.prisma.cont_asientos.findFirst({
      where: { empresa_id: empresaId },
      orderBy: { numero: 'desc' },
      select: { numero: true },
    });
    const nextNumero = (lastAsiento?.numero ?? 0) + 1;

    // Usar el último período del ejercicio como período del asiento de cierre
    const ultimoPeriodo = ejercicio.periodos[0]; // ya ordenado desc por numero

    await this.prisma.cont_asientos.create({
      data: {
        empresa_id: empresaId,
        periodo_id: ultimoPeriodo.id,
        numero: nextNumero,
        fecha: ultimoPeriodo.fecha_fin,
        glosa: `Asiento de cierre — Ejercicio ${ejercicio.anio}`,
        estado: 'CONFIRMADO',
        total_debe_pyg: totalDebe,
        total_haber_pyg: totalHaber,
        usuario_id: usuarioId ?? null,
        lineas: { create: lineas },
      },
    });

    this.audit.log({
      empresa_id: empresaId,
      user_id: usuarioId,
      action: 'CREATE',
      entity_type: 'cont_asientos',
      entity_id: ejercicio.id,
      descripcion: `Asiento de cierre ejercicio ${ejercicio.anio} — Utilidad Neta: ${utilidadNeta.toLocaleString('es-PY')} Gs.`,
    });
  }

  private async generarPeriodos(ejercicioId: string, empresaId: string, anio: number) {
    const MESES = [
      { numero: 1, nombre: 'Enero', dias: 31 },
      { numero: 2, nombre: 'Febrero', dias: anio % 4 === 0 ? 29 : 28 },
      { numero: 3, nombre: 'Marzo', dias: 31 },
      { numero: 4, nombre: 'Abril', dias: 30 },
      { numero: 5, nombre: 'Mayo', dias: 31 },
      { numero: 6, nombre: 'Junio', dias: 30 },
      { numero: 7, nombre: 'Julio', dias: 31 },
      { numero: 8, nombre: 'Agosto', dias: 31 },
      { numero: 9, nombre: 'Septiembre', dias: 30 },
      { numero: 10, nombre: 'Octubre', dias: 31 },
      { numero: 11, nombre: 'Noviembre', dias: 30 },
      { numero: 12, nombre: 'Diciembre', dias: 31 },
    ];

    await this.prisma.cont_periodos.createMany({
      data: MESES.map((m) => ({
        ejercicio_id: ejercicioId,
        empresa_id: empresaId,
        numero: m.numero,
        mes: m.numero,
        anio,
        fecha_inicio: new Date(anio, m.numero - 1, 1),
        fecha_fin: new Date(anio, m.numero - 1, m.dias),
        estado: 'ABIERTO',
      })),
    });
  }

  async crearPeriodoManual(ejercicioId: string, empresaId: string, data: { numero: number; mes: number; fecha_inicio: string; fecha_fin: string }) {
    const ejercicio = await this.prisma.cont_ejercicios.findFirst({
      where: { id: ejercicioId, empresa_id: empresaId, estado: 'ABIERTO' },
    });
    if (!ejercicio) throw new NotFoundException('Ejercicio no encontrado o cerrado');

    const existente = await this.prisma.cont_periodos.findFirst({
      where: { ejercicio_id: ejercicioId, numero: data.numero },
    });
    if (existente) throw new BadRequestException(`El período ${data.numero} ya existe en este ejercicio`);

    return this.prisma.cont_periodos.create({
      data: {
        ejercicio_id: ejercicioId,
        empresa_id: empresaId,
        numero: data.numero,
        mes: data.mes,
        anio: ejercicio.anio,
        fecha_inicio: new Date(data.fecha_inicio),
        fecha_fin: new Date(data.fecha_fin),
        estado: 'ABIERTO',
      },
    });
  }
}
