import { BadRequestException, Injectable, Logger, NotFoundException, Optional } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { ContabilidadIntegracionService } from '../../contabilidad/services/integracion.service';
import { MapeoCuentasService } from '../../contabilidad/services/mapeo-cuentas.service';
import {
  AccionAnticipoDto,
  CreateAnticipoDto,
  QueryAnticiposDto,
  UpdateAnticipoDto,
} from '../dto/anticipos.dto';

const TRANSICIONES_VALIDAS: Record<string, string[]> = {
  PENDIENTE: ['APROBADO', 'ANULADO'],
  APROBADO: ['DESCONTADO', 'ANULADO'],
  DESCONTADO: [],
  ANULADO: [],
};

const ORIGEN_MOVIMIENTO_TESORERIA = 'rrhh_anticipo_salario';

@Injectable()
export class AnticiposService {
  private readonly logger = new Logger(AnticiposService.name);
  constructor(
    private readonly prisma: PrismaService,
    @Optional() private readonly contabilidad?: ContabilidadIntegracionService,
    @Optional() private readonly mapeo?: MapeoCuentasService,
  ) {}

  private async tieneModuloTesoreria(empresaId: string): Promise<boolean> {
    const registro = await this.prisma.suscripcion_modulos.findFirst({
      where: {
        activo: true,
        modulos: { codigo: 'TESORERIA' },
        suscripciones: { empresa_id: empresaId },
      },
    });
    return !!registro;
  }

  /**
   * Registra el egreso del desembolso del anticipo (idempotente por origen). El asiento
   * lo genera la integración de tesorería → contabilidad a partir de la cuenta contable
   * del detalle: DEBE "Anticipo al Personal" / HABER Banco.
   */
  private async registrarMovimientoTesoreria(
    anticipo: { id: string; empresa_id: string; cuenta_tesoreria_id: string | null; monto: Prisma.Decimal },
    empleadoNombre: string,
    fechaPago: Date,
    userId?: string,
  ): Promise<void> {
    const empresaId = anticipo.empresa_id;
    const cuentaId = anticipo.cuenta_tesoreria_id;
    if (!cuentaId) {
      throw new BadRequestException(
        'Seleccioná la cuenta de tesorería desde la que se paga el anticipo para poder aprobarlo.',
      );
    }
    const cuenta = await this.prisma.tes_cuentas.findFirst({
      where: { id: cuentaId, empresa_id: empresaId, activo: true },
      select: { id: true, moneda: true },
    });
    if (!cuenta) throw new BadRequestException('Cuenta de tesorería inválida o inactiva.');
    if (cuenta.moneda !== 'PYG') {
      throw new BadRequestException(
        `El anticipo se paga en PYG. Elegí una cuenta en PYG (la seleccionada está en ${cuenta.moneda}).`,
      );
    }

    const existente = await this.prisma.tes_movimientos.findFirst({
      where: { empresa_id: empresaId, origen_tipo: ORIGEN_MOVIMIENTO_TESORERIA, origen_id: anticipo.id },
    });
    if (existente) return;

    const categoria = await this.prisma.tes_categorias.findFirst({
      where: { codigo_interno: 'ANTICIPO_PERSONAL_RRHH', is_sistema: true },
    });
    if (!categoria) {
      throw new BadRequestException(
        'Falta la categoría de tesorería "Anticipo al personal (RRHH)". Ejecutá las migraciones de tesorería.',
      );
    }

    // Cuenta a debitar. Va DESCUENTOS_VARIOS_RRHH primero, y no ANTICIPO_PERSONAL,
    // porque tiene que ser LA MISMA cuenta que la liquidación acredita al descontar
    // el anticipo del sueldo (ver integrarLiquidacionRrhh: "DESCUENTOS_VARIOS_RRHH =
    // Σ egresos − ips_obrero (anticipos, cuotas, ...)"). Si se debitara en otra, el
    // anticipo quedaría abierto para siempre en una cuenta y el descuento colgado en
    // otra, y el saldo del empleado nunca cerraría.
    // ANTICIPO_PERSONAL queda de respaldo: lo usa el módulo de Gastos/Viáticos y en
    // muchas empresas apunta a la misma cuenta.
    let cuentaContableId: string | null = null;
    for (const concepto of ['DESCUENTOS_VARIOS_RRHH', 'ANTICIPO_PERSONAL']) {
      try {
        cuentaContableId = (await this.mapeo?.getCuentaPorConcepto(empresaId, concepto)) ?? null;
        if (cuentaContableId) break;
      } catch {
        /* sin mapeo para ese concepto: se prueba el siguiente */
      }
    }
    cuentaContableId = cuentaContableId ?? categoria.cuenta_contable_id ?? null;

    const monto = Number(anticipo.monto);
    if (monto <= 0) return;

    const movimiento = await this.prisma.$transaction(async (tx) => {
      const mov = await tx.tes_movimientos.create({
        data: {
          empresa_id: empresaId,
          cuenta_id: cuentaId,
          tipo: 'EGRESO',
          estado: 'CONFIRMADO',
          // Fecha de valor = la del desembolso, no la de carga: una aprobación
          // registrada tarde tiene que caer igual en el mes en que salió la plata.
          fecha: fechaPago,
          monto,
          descripcion: `Anticipo de salario — ${empleadoNombre}`,
          origen_tipo: ORIGEN_MOVIMIENTO_TESORERIA,
          origen_id: anticipo.id,
          confirmado_por: userId ?? null,
          confirmado_en: new Date(),
          created_by: userId ?? null,
          detalles: {
            create: [
              {
                categoria_id: categoria.id,
                monto,
                descripcion: `Anticipo al personal — ${empleadoNombre}`,
                cuenta_contable_id: cuentaContableId,
              },
            ],
          },
        },
      });
      await tx.tes_cuentas.update({
        where: { id: cuentaId },
        data: { saldo_actual: { increment: -monto } },
      });
      return mov;
    });

    this.contabilidad?.integrarMovimientoTesoreria(movimiento.id).catch((e) =>
      this.logger.error(`Error al contabilizar el anticipo ${anticipo.id}: ${(e as Error).message}`),
    );
  }

  async listar(empresaId: string, query: QueryAnticiposDto) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 20;
    const skip = (page - 1) * limit;

    const where: Prisma.rrhh_anticipos_salarioWhereInput = { empresa_id: empresaId };
    if (query.estado) where.estado = query.estado;
    if (query.empleado_id) where.empleado_id = query.empleado_id;
    if (query.periodo_anio) where.periodo_anio = query.periodo_anio;
    if (query.periodo_mes) where.periodo_mes = query.periodo_mes;

    const [data, total] = await this.prisma.$transaction([
      this.prisma.rrhh_anticipos_salario.findMany({
        where,
        skip,
        take: limit,
        orderBy: [{ fecha_solicitud: 'desc' }, { created_at: 'desc' }],
        include: {
          empleado: {
            select: {
              id: true,
              numero_empleado: true,
              nombres: true,
              apellidos: true,
              cedula_identidad: true,
            },
          },
        },
      }),
      this.prisma.rrhh_anticipos_salario.count({ where }),
    ]);

    return { data, total, page, limit };
  }

  async obtener(id: string, empresaId: string) {
    const anticipo = await this.prisma.rrhh_anticipos_salario.findFirst({
      where: { id, empresa_id: empresaId },
      include: {
        empleado: {
          select: {
            id: true,
            numero_empleado: true,
            nombres: true,
            apellidos: true,
            cedula_identidad: true,
            salario_base: true,
          },
        },
      },
    });
    if (!anticipo) throw new NotFoundException('Anticipo no encontrado');
    return anticipo;
  }

  async crear(empresaId: string, dto: CreateAnticipoDto, userId?: string) {
    const empleado = await this.prisma.rrhh_empleados.findFirst({
      where: { id: dto.empleado_id, empresa_id: empresaId },
    });
    if (!empleado) throw new BadRequestException('Empleado inválido para esta empresa');
    if (empleado.estado === 'EGRESADO') {
      throw new BadRequestException('No se pueden crear anticipos para empleados egresados');
    }

    return this.prisma.rrhh_anticipos_salario.create({
      data: {
        empresa_id: empresaId,
        empleado_id: dto.empleado_id,
        fecha_solicitud: new Date(dto.fecha_solicitud),
        monto: dto.monto,
        periodo_anio: dto.periodo_anio,
        periodo_mes: dto.periodo_mes,
        observacion: dto.observacion,
        creado_por: userId,
      },
    });
  }

  async actualizar(id: string, empresaId: string, dto: UpdateAnticipoDto) {
    const anticipo = await this.prisma.rrhh_anticipos_salario.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!anticipo) throw new NotFoundException('Anticipo no encontrado');
    if (anticipo.estado !== 'PENDIENTE') {
      throw new BadRequestException(
        `Solo se puede editar un anticipo en estado PENDIENTE (actual: ${anticipo.estado})`,
      );
    }

    return this.prisma.rrhh_anticipos_salario.update({
      where: { id },
      data: {
        ...dto,
        fecha_solicitud: dto.fecha_solicitud ? new Date(dto.fecha_solicitud) : undefined,
        updated_at: new Date(),
      },
    });
  }

  private async transicionar(
    id: string,
    empresaId: string,
    nuevoEstado: string,
    userId?: string,
    comentario?: string,
    opciones?: { cuenta_tesoreria_id?: string; fecha_pago?: string },
  ) {
    const anticipo = await this.prisma.rrhh_anticipos_salario.findFirst({
      where: { id, empresa_id: empresaId },
      include: { empleado: { select: { nombres: true, apellidos: true } } },
    });
    if (!anticipo) throw new NotFoundException('Anticipo no encontrado');

    const permitidas = TRANSICIONES_VALIDAS[anticipo.estado] ?? [];
    if (!permitidas.includes(nuevoEstado)) {
      throw new BadRequestException(
        `Transición no permitida: ${anticipo.estado} → ${nuevoEstado}`,
      );
    }

    const data: Prisma.rrhh_anticipos_salarioUpdateInput = {
      estado: nuevoEstado,
      updated_at: new Date(),
    };
    if (comentario) {
      data.observacion = anticipo.observacion
        ? `${anticipo.observacion}\n[${nuevoEstado}] ${comentario}`
        : `[${nuevoEstado}] ${comentario}`;
    }
    if (nuevoEstado === 'APROBADO') {
      data.aprobado_por = userId;
      data.fecha_aprobacion = new Date();
      const fechaPago = opciones?.fecha_pago ? new Date(`${opciones.fecha_pago}T12:00:00`) : new Date();
      data.fecha_pago = fechaPago;

      const cuentaTesoreriaId = opciones?.cuenta_tesoreria_id ?? anticipo.cuenta_tesoreria_id ?? null;
      if (cuentaTesoreriaId) data.cuenta_tesoreria = { connect: { id: cuentaTesoreriaId } };

      // El egreso se registra ANTES de dejar el anticipo en APROBADO: si falla por
      // configuración (cuenta, categoría), la aprobación no queda a medias.
      if (await this.tieneModuloTesoreria(empresaId)) {
        const nombre = [anticipo.empleado?.nombres, anticipo.empleado?.apellidos]
          .filter(Boolean)
          .join(' ') || 'empleado';
        await this.registrarMovimientoTesoreria(
          { ...anticipo, cuenta_tesoreria_id: cuentaTesoreriaId },
          nombre,
          fechaPago,
          userId,
        );
      }
    }
    if (nuevoEstado === 'ANULADO') {
      data.anulado_por = userId;
      data.fecha_anulacion = new Date();
    }

    return this.prisma.rrhh_anticipos_salario.update({ where: { id }, data });
  }

  aprobar(id: string, empresaId: string, dto: AccionAnticipoDto, userId?: string) {
    return this.transicionar(id, empresaId, 'APROBADO', userId, dto.comentario, {
      cuenta_tesoreria_id: dto.cuenta_tesoreria_id,
      fecha_pago: dto.fecha_pago,
    });
  }

  anular(id: string, empresaId: string, dto: AccionAnticipoDto, userId?: string) {
    return this.transicionar(id, empresaId, 'ANULADO', userId, dto.comentario);
  }

  async eliminar(id: string, empresaId: string) {
    const anticipo = await this.prisma.rrhh_anticipos_salario.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!anticipo) throw new NotFoundException('Anticipo no encontrado');
    if (anticipo.estado !== 'PENDIENTE') {
      throw new BadRequestException(
        `Solo se puede eliminar un anticipo PENDIENTE (actual: ${anticipo.estado}). Usá «Anular» para los demás.`,
      );
    }
    return this.prisma.rrhh_anticipos_salario.delete({ where: { id } });
  }
}
