import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { AuditService } from '../../audit/audit.service';
import { PrismaService } from '../../prisma/prisma.service';
import { PeriodosService } from './periodos.service';
import { TipoCambioService } from './tipo-cambio.service';

// Los DTOs ahora son clases con class-validator (ver ../dto/crear-asiento.dto).
// Se importan para uso local (tipos) y se re-exportan para no romper los imports
// existentes que apuntaban a este módulo.
import { CrearAsientoDto, EditarAsientoDto, LineaAsientoDto } from '../dto/crear-asiento.dto';
export { CrearAsientoDto, EditarAsientoDto, LineaAsientoDto };

@Injectable()
export class AsientosService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly periodosService: PeriodosService,
    private readonly tipoCambioService: TipoCambioService,
    private readonly audit: AuditService,
  ) {}

  async listar(empresaId: string, filters: { periodoId?: string; estado?: string; desde?: string; hasta?: string; skip?: number; take?: number }) {
    const { periodoId, estado, desde, hasta, skip = 0, take = 50 } = filters;
    const where: any = {
      empresa_id: empresaId,
      ...(periodoId ? { periodo_id: periodoId } : {}),
      ...(estado ? { estado } : {}),
      ...(desde || hasta
        ? { fecha: { ...(desde ? { gte: new Date(desde) } : {}), ...(hasta ? { lte: new Date(hasta) } : {}) } }
        : {}),
    };

    const [total, data] = await Promise.all([
      this.prisma.cont_asientos.count({ where }),
      this.prisma.cont_asientos.findMany({
        where,
        include: {
          lineas: { include: { cuenta: { select: { codigo: true, descripcion: true } }, centro_costo: { select: { codigo: true, descripcion: true } } } },
          periodo: { select: { mes: true, anio: true } },
          usuario: { select: { nombres: true } },
          documento: { select: { tipo: true, origen_tipo: true, numero_documento: true } },
        },
        orderBy: [{ fecha: 'desc' }, { numero: 'desc' }],
        skip,
        take,
      }),
    ]);

    return { total, data };
  }

  async getById(id: string, empresaId: string) {
    const asiento = await this.prisma.cont_asientos.findFirst({
      where: { id, empresa_id: empresaId },
      include: {
        lineas: { include: { cuenta: { select: { codigo: true, descripcion: true, tipo: true, naturaleza: true } }, centro_costo: true } },
        periodo: { select: { mes: true, anio: true, estado: true } },
        usuario: { select: { nombres: true } },
        documento: true,
      },
    });
    if (!asiento) throw new NotFoundException('Asiento no encontrado');
    return asiento;
  }

  async crear(empresaId: string, usuarioId: string, dto: CrearAsientoDto, esAdmin = false) {
    // 1. Validar período
    await this.periodosService.validarPeriodoAbierto(dto.periodo_id, empresaId, esAdmin);

    const moneda = dto.moneda_origen ?? 'PYG';
    const fecha = new Date(dto.fecha);

    // 2. Calcular totales y convertir a PYG
    let totalDebePYG = 0;
    let totalHaberPYG = 0;

    const lineasConPYG = await Promise.all(
      dto.lineas.map(async (l) => {
        const debePYG = await this.tipoCambioService.convertirAPYG(empresaId, l.debe_moneda, moneda, fecha);
        const haberPYG = await this.tipoCambioService.convertirAPYG(empresaId, l.haber_moneda, moneda, fecha);
        totalDebePYG += debePYG;
        totalHaberPYG += haberPYG;
        return { ...l, debe_pyg: debePYG, haber_pyg: haberPYG };
      }),
    );

    // 3. Validar partida doble
    this.validarPartidaDoble(totalDebePYG, totalHaberPYG);

    // 4. Validar que las cuentas acepten movimientos y centros de costo
    await this.validarLineas(empresaId, lineasConPYG);

    // 5. Obtener tipo de cambio ID si aplica
    let tipoCambioId: string | null = null;
    if (moneda !== 'PYG') {
      const tc = await this.prisma.cont_tipo_cambio.findFirst({
        where: { empresa_id: empresaId, moneda, fecha: { lte: fecha } },
        orderBy: { fecha: 'desc' },
        select: { id: true },
      });
      tipoCambioId = tc?.id ?? null;
    }

    // 6-7. Siguiente número + creación con reintento ante colisión concurrente (P2002):
    // dos creaciones simultáneas pueden leer el mismo max y chocar en el índice único
    // (empresa_id, periodo_id, numero). Recomputamos el número y reintentamos.
    const MAX_INTENTOS = 5;
    let asiento;
    for (let intento = 1; ; intento++) {
      const ultimoAsiento = await this.prisma.cont_asientos.findFirst({
        where: { empresa_id: empresaId, periodo_id: dto.periodo_id },
        orderBy: { numero: 'desc' },
        select: { numero: true },
      });
      const numero = (ultimoAsiento?.numero ?? 0) + 1;

      try {
        asiento = await this.prisma.cont_asientos.create({
          data: {
            empresa_id: empresaId,
            periodo_id: dto.periodo_id,
            numero,
            fecha,
            glosa: dto.glosa ?? null,
            estado: 'BORRADOR',
            moneda_origen: moneda,
            tipo_cambio_id: tipoCambioId,
            total_debe_pyg: totalDebePYG,
            total_haber_pyg: totalHaberPYG,
            usuario_id: usuarioId,
            lineas: {
              create: lineasConPYG.map((l, i) => ({
                cuenta_id: l.cuenta_id,
                centro_costo_id: l.centro_costo_id ?? null,
                descripcion: l.descripcion ?? null,
                debe_moneda: l.debe_moneda,
                haber_moneda: l.haber_moneda,
                debe_pyg: l.debe_pyg,
                haber_pyg: l.haber_pyg,
                orden: i + 1,
              })),
            },
          },
          include: { lineas: { include: { cuenta: { select: { codigo: true, descripcion: true } } } } },
        });
        break;
      } catch (e) {
        if (intento < MAX_INTENTOS && e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
          continue; // colisión de número: recomputa y reintenta
        }
        throw e;
      }
    }
    this.audit.log({ empresa_id: empresaId, user_id: usuarioId, action: 'CREATE', entity_type: 'cont_asientos', entity_id: asiento.id, descripcion: `Asiento N° ${asiento.numero} creado en borrador` });
    return asiento;
  }

  async eliminar(id: string, empresaId: string, usuarioId: string) {
    const asiento = await this.prisma.cont_asientos.findFirst({
      where: { id, empresa_id: empresaId },
      select: { id: true, numero: true, estado: true, documento_id: true },
    });
    if (!asiento) throw new NotFoundException('Asiento no encontrado');
    if (asiento.estado !== 'BORRADOR') {
      throw new BadRequestException(
        `Solo se pueden eliminar asientos en estado BORRADOR. Este asiento está ${asiento.estado}. ` +
        (asiento.estado === 'CONFIRMADO' ? 'Para anularlo, use la opción Revertir.' : ''),
      );
    }

    await this.prisma.$transaction(async (tx) => {
      await tx.cont_asientos_det.deleteMany({ where: { asiento_id: id } });
      await tx.cont_asientos.delete({ where: { id } });
      if (asiento.documento_id) {
        await tx.cont_documentos.delete({ where: { id: asiento.documento_id } }).catch(() => null);
      }
    });

    this.audit.log({ empresa_id: empresaId, user_id: usuarioId, action: 'DELETE', entity_type: 'cont_asientos', entity_id: id, descripcion: `Asiento N° ${asiento.numero} eliminado` });
    return { ok: true };
  }

  async corregir(id: string, empresaId: string, usuarioId: string) {
    // Carga el asiento ANTES de revertirlo para devolver sus datos originales
    const original = await this.prisma.cont_asientos.findFirst({
      where: { id, empresa_id: empresaId },
      include: {
        lineas: { orderBy: { orden: 'asc' } },
      },
    });
    if (!original) throw new NotFoundException('Asiento no encontrado');

    // Idempotencia: si ya fue revertido recientemente (en los últimos 30s), devolver datos del original
    if (original.estado === 'REVERTIDO') {
      return {
        reversion_numero: null,
        idempotente: true,
        datos_originales: {
          glosa: original.glosa ?? '',
          moneda_origen: original.moneda_origen ?? 'PYG',
          lineas: original.lineas.map((l) => ({
            cuenta_id: l.cuenta_id,
            descripcion: l.descripcion ?? '',
            debe_moneda: Number(l.debe_moneda),
            haber_moneda: Number(l.haber_moneda),
            centro_costo_id: l.centro_costo_id ?? null,
          })),
        },
      };
    }

    if (original.estado !== 'CONFIRMADO') {
      throw new BadRequestException(
        `Solo se pueden corregir asientos CONFIRMADOS. Este asiento está ${original.estado}.`,
      );
    }

    // Revertir el asiento original (contiene todas las validaciones de período abierto)
    const reversion = await this.revertir(id, empresaId, usuarioId);

    this.audit.log({
      empresa_id: empresaId,
      user_id: usuarioId,
      action: 'CORRECT',
      entity_type: 'cont_asientos',
      entity_id: id,
      descripcion: `Corrección de asiento N° ${original.numero}: revertido como #${reversion.numero}, usuario abrirá nuevo asiento`,
    });

    return {
      reversion_numero: reversion.numero,
      datos_originales: {
        glosa: original.glosa ?? '',
        moneda_origen: original.moneda_origen ?? 'PYG',
        lineas: original.lineas.map((l) => ({
          cuenta_id: l.cuenta_id,
          descripcion: l.descripcion ?? '',
          debe_moneda: Number(l.debe_moneda),
          haber_moneda: Number(l.haber_moneda),
          centro_costo_id: l.centro_costo_id ?? null,
        })),
      },
    };
  }

  async confirmar(id: string, empresaId: string, esAdmin = false) {
    const asiento = await this.prisma.cont_asientos.findFirst({
      where: { id, empresa_id: empresaId },
      include: { lineas: true },
    });
    if (!asiento) throw new NotFoundException('Asiento no encontrado');
    if (asiento.estado !== 'BORRADOR') throw new BadRequestException(`El asiento ya está en estado ${asiento.estado}`);

    // Re-validar período (puede haber cambiado)
    await this.periodosService.validarPeriodoAbierto(asiento.periodo_id, empresaId, esAdmin);

    // Re-validar partida doble
    this.validarPartidaDoble(Number(asiento.total_debe_pyg), Number(asiento.total_haber_pyg));

    const confirmado = await this.prisma.cont_asientos.update({
      where: { id },
      data: { estado: 'CONFIRMADO', updated_at: new Date() },
    });
    this.audit.log({ empresa_id: empresaId, action: 'CONFIRM', entity_type: 'cont_asientos', entity_id: id, descripcion: `Asiento N° ${asiento.numero} confirmado` });
    return confirmado;
  }

  async revertir(id: string, empresaId: string, usuarioId: string | null) {
    const asiento = await this.prisma.cont_asientos.findFirst({
      where: { id, empresa_id: empresaId },
      include: {
        lineas: true,
        documento: true,
      },
    });
    if (!asiento) throw new NotFoundException('Asiento no encontrado');
    if (asiento.estado !== 'CONFIRMADO') throw new BadRequestException('Solo se pueden revertir asientos confirmados');

    // Un período CERRADO/BLOQUEADO no se puede tocar: no se permite revertir asientos
    // de un período ya cerrado. La reversión siempre cae en el MISMO período del asiento
    // original, con su FECHA original (así no distorsiona ni el mes original ni el actual).
    // Si el usuario necesita corregir algo de un período cerrado, debe reabrir ese período
    // (acción controlada y auditada) o registrar un ajuste manual en el período actual.
    const periodoOriginal = await this.prisma.cont_periodos.findUnique({
      where: { id: asiento.periodo_id },
      select: { estado: true, mes: true, anio: true },
    });
    if (periodoOriginal?.estado !== 'ABIERTO') {
      const etiqueta =
        periodoOriginal ? `${String(periodoOriginal.mes).padStart(2, '0')}/${periodoOriginal.anio}` : 'del asiento';
      throw new BadRequestException(
        `No se puede revertir: el período ${etiqueta} del asiento está ${periodoOriginal?.estado ?? 'no disponible'}. ` +
        'Reabra ese período para revertirlo o registre un ajuste en el período actual.',
      );
    }

    const periodoReversionId = asiento.periodo_id;
    const fechaReversion = asiento.fecha;

    // Numeración dentro de la transacción + reintento ante colisión concurrente (P2002):
    // el número se recomputa en cada intento (dos reversiones simultáneas podrían chocar
    // en el índice único (empresa_id, periodo_id, numero)).
    const MAX_INTENTOS = 5;
    let reversionResult;
    for (let intento = 1; ; intento++) {
      try {
        reversionResult = await this.prisma.$transaction(async (tx) => {
          const ultimoAsiento = await tx.cont_asientos.findFirst({
            where: { empresa_id: empresaId, periodo_id: periodoReversionId },
            orderBy: { numero: 'desc' },
            select: { numero: true },
          });
          const numero = (ultimoAsiento?.numero ?? 0) + 1;

          // Crear documento de reversión si el asiento tenía documento
          let docReversionId: string | null = null;
          if (asiento.documento_id && asiento.documento) {
            const docRev = await tx.cont_documentos.create({
              data: {
                empresa_id: empresaId,
                tipo: 'REVERSION',
                origen_tipo: asiento.documento.origen_tipo,
                origen_id: null,
                numero_documento: `REV-${asiento.documento.numero_documento ?? asiento.numero}`,
                estado: 'CONFIRMADO',
                reversion_de_id: asiento.documento_id,
              },
              select: { id: true },
            });
            docReversionId = docRev.id;

            // Marcar documento original como REVERTIDO
            await tx.cont_documentos.update({
              where: { id: asiento.documento_id },
              data: { estado: 'REVERTIDO', updated_at: new Date() },
            });
          }

          // Crear asiento espejo (DEBE↔HABER invertidos)
          const asientoReversion = await tx.cont_asientos.create({
            data: {
              empresa_id: empresaId,
              periodo_id: periodoReversionId,
              documento_id: docReversionId,
              numero,
              fecha: fechaReversion,
              glosa: `REVERSIÓN: ${asiento.glosa ?? `Asiento N° ${asiento.numero}`}`,
              estado: 'CONFIRMADO',
              moneda_origen: asiento.moneda_origen,
              tipo_cambio_id: asiento.tipo_cambio_id,
              total_debe_pyg: asiento.total_haber_pyg,   // invertido
              total_haber_pyg: asiento.total_debe_pyg,   // invertido
              usuario_id: usuarioId,
              lineas: {
                create: asiento.lineas.map((l, i) => ({
                  cuenta_id: l.cuenta_id,
                  centro_costo_id: l.centro_costo_id,
                  descripcion: l.descripcion,
                  debe_moneda: l.haber_moneda,   // invertido
                  haber_moneda: l.debe_moneda,   // invertido
                  debe_pyg: l.haber_pyg,         // invertido
                  haber_pyg: l.debe_pyg,         // invertido
                  orden: i + 1,
                })),
              },
            },
            include: { lineas: { include: { cuenta: { select: { codigo: true, descripcion: true } } } } },
          });

          // Marcar asiento original como REVERTIDO
          await tx.cont_asientos.update({
            where: { id },
            data: { estado: 'REVERTIDO', updated_at: new Date() },
          });

          return asientoReversion;
        });
        break;
      } catch (e) {
        if (intento < MAX_INTENTOS && e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
          continue; // colisión de número: recomputa y reintenta la transacción completa
        }
        throw e;
      }
    }
    this.audit.log({ empresa_id: empresaId, user_id: usuarioId, action: 'REVERT', entity_type: 'cont_asientos', entity_id: id, descripcion: `Reversión de asiento N° ${asiento.numero}` });
    return reversionResult;
  }

  // ── Método interno usado por ContabilidadIntegracionService ───────────────

  async crearConfirmado(
    empresaId: string,
    usuarioId: string | null,
    documentoId: string,
    dto: CrearAsientoDto,
    tx?: Prisma.TransactionClient,
  ) {
    const db = tx ?? this.prisma;
    const moneda = dto.moneda_origen ?? 'PYG';
    const fecha = new Date(dto.fecha);

    let totalDebePYG = 0;
    let totalHaberPYG = 0;

    const lineasConPYG = await Promise.all(
      dto.lineas.map(async (l) => {
        const debePYG = await this.tipoCambioService.convertirAPYG(empresaId, l.debe_moneda, moneda, fecha);
        const haberPYG = await this.tipoCambioService.convertirAPYG(empresaId, l.haber_moneda, moneda, fecha);
        totalDebePYG += debePYG;
        totalHaberPYG += haberPYG;
        return { ...l, debe_pyg: debePYG, haber_pyg: haberPYG };
      }),
    );

    this.validarPartidaDoble(totalDebePYG, totalHaberPYG);

    let tipoCambioId: string | null = null;
    if (moneda !== 'PYG') {
      const tc = await db.cont_tipo_cambio.findFirst({
        where: { empresa_id: empresaId, moneda, fecha: { lte: fecha } },
        orderBy: { fecha: 'desc' },
        select: { id: true },
      });
      tipoCambioId = tc?.id ?? null;
    }

    // Numeración correlativa por período con reintento ante colisión concurrente.
    // Dos contabilizaciones simultáneas pueden leer el mismo max y chocar en el índice
    // único (empresa_id, periodo_id, numero) → P2002. Recomputamos el número y reintentamos.
    // (Estos callers no abren transacción externa, así que el retry es efectivo.)
    const MAX_INTENTOS = 5;
    for (let intento = 1; ; intento++) {
      const ultimoAsiento = await db.cont_asientos.findFirst({
        where: { empresa_id: empresaId, periodo_id: dto.periodo_id },
        orderBy: { numero: 'desc' },
        select: { numero: true },
      });
      const numero = (ultimoAsiento?.numero ?? 0) + 1;

      try {
        return await db.cont_asientos.create({
          data: {
            empresa_id: empresaId,
            periodo_id: dto.periodo_id,
            documento_id: documentoId,
            numero,
            fecha,
            glosa: dto.glosa ?? null,
            estado: 'CONFIRMADO',
            moneda_origen: moneda,
            tipo_cambio_id: tipoCambioId,
            total_debe_pyg: totalDebePYG,
            total_haber_pyg: totalHaberPYG,
            usuario_id: usuarioId,
            lineas: {
              create: lineasConPYG.map((l, i) => ({
                cuenta_id: l.cuenta_id,
                centro_costo_id: l.centro_costo_id ?? null,
                descripcion: l.descripcion ?? null,
                debe_moneda: l.debe_moneda,
                haber_moneda: l.haber_moneda,
                debe_pyg: l.debe_pyg,
                haber_pyg: l.haber_pyg,
                orden: i + 1,
              })),
            },
          },
          select: { id: true, numero: true },
        });
      } catch (e) {
        if (intento < MAX_INTENTOS && e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
          continue; // colisión de número: recomputa y reintenta
        }
        throw e;
      }
    }
  }

  // ── Validaciones privadas ─────────────────────────────────────────────────

  async editar(id: string, empresaId: string, usuarioId: string, dto: { glosa?: string; fecha?: string; lineas: LineaAsientoDto[] }) {
    const asiento = await this.prisma.cont_asientos.findFirst({ where: { id, empresa_id: empresaId } });
    if (!asiento) throw new NotFoundException('Asiento no encontrado');
    if (asiento.estado !== 'BORRADOR') throw new BadRequestException('Solo se pueden editar asientos en estado BORRADOR');

    await this.periodosService.validarPeriodoAbierto(asiento.periodo_id, empresaId);

    const moneda = asiento.moneda_origen;
    const fecha = dto.fecha ? new Date(dto.fecha) : asiento.fecha;

    let totalDebePYG = 0;
    let totalHaberPYG = 0;
    const lineasConPYG = await Promise.all(
      dto.lineas.map(async (l) => {
        const debePYG = await this.tipoCambioService.convertirAPYG(empresaId, l.debe_moneda, moneda, fecha);
        const haberPYG = await this.tipoCambioService.convertirAPYG(empresaId, l.haber_moneda, moneda, fecha);
        totalDebePYG += debePYG;
        totalHaberPYG += haberPYG;
        return { ...l, debe_pyg: debePYG, haber_pyg: haberPYG };
      }),
    );

    this.validarPartidaDoble(totalDebePYG, totalHaberPYG);
    await this.validarLineas(empresaId, lineasConPYG);

    await this.prisma.$transaction(async (tx) => {
      await tx.cont_asientos_det.deleteMany({ where: { asiento_id: id } });
      await tx.cont_asientos.update({
        where: { id },
        data: {
          fecha,
          glosa: dto.glosa ?? asiento.glosa,
          total_debe_pyg: totalDebePYG,
          total_haber_pyg: totalHaberPYG,
          updated_at: new Date(),
        },
      });
      await tx.cont_asientos_det.createMany({
        data: lineasConPYG.map((l, i) => ({
          asiento_id: id,
          cuenta_id: l.cuenta_id,
          centro_costo_id: l.centro_costo_id ?? null,
          descripcion: l.descripcion ?? null,
          debe_moneda: l.debe_moneda,
          haber_moneda: l.haber_moneda,
          debe_pyg: l.debe_pyg,
          haber_pyg: l.haber_pyg,
          orden: i + 1,
        })),
      });
    });

    this.audit.log({ empresa_id: empresaId, user_id: usuarioId, action: 'UPDATE', entity_type: 'cont_asientos', entity_id: id, descripcion: `Asiento N° ${asiento.numero} editado` });
    return this.getById(id, empresaId);
  }

  private validarPartidaDoble(debe: number, haber: number) {
    if (Math.abs(debe - haber) > 1) {
      // Tolerancia de 1 PYG por redondeos
      throw new BadRequestException(
        `El asiento no está balanceado: DEBE=${debe.toFixed(0)} ≠ HABER=${haber.toFixed(0)}`,
      );
    }
  }

  private async validarLineas(empresaId: string, lineas: (LineaAsientoDto & { debe_pyg: number; haber_pyg: number })[]) {
    for (const linea of lineas) {
      if (linea.debe_moneda === 0 && linea.haber_moneda === 0) {
        throw new BadRequestException('Una línea del asiento tiene DEBE y HABER en 0');
      }
      if (linea.debe_moneda > 0 && linea.haber_moneda > 0) {
        throw new BadRequestException('Una línea no puede tener DEBE y HABER simultáneamente');
      }

      const cuenta = await this.prisma.cont_plan_cuentas.findFirst({
        where: { id: linea.cuenta_id, OR: [{ empresa_id: empresaId }, { empresa_id: null }], active: true },
      });
      if (!cuenta) throw new BadRequestException(`Cuenta ${linea.cuenta_id} no encontrada o inactiva`);
      if (!cuenta.acepta_movimientos) throw new BadRequestException(`La cuenta "${cuenta.descripcion}" (${cuenta.codigo}) no acepta movimientos directos`);
      if (cuenta.acepta_cc && !linea.centro_costo_id) {
        throw new BadRequestException(`La cuenta "${cuenta.descripcion}" (${cuenta.codigo}) requiere un centro de costo`);
      }
    }
  }
}
