import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import { CreateEmpleadoDto } from '../dto/create-empleado.dto';
import { QueryEmpleadosDto } from '../dto/query-empleados.dto';
import { UpdateEmpleadoDto } from '../dto/update-empleado.dto';

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

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

    const where: Prisma.rrhh_empleadosWhereInput = { empresa_id: empresaId };

    if (query.estado) where.estado = query.estado;
    if (query.sucursal_id) where.sucursal_id = query.sucursal_id;
    if (query.departamento_id) where.departamento_id = query.departamento_id;
    if (query.cargo_id) where.cargo_id = query.cargo_id;
    if (query.tipo_empleado_id) where.tipo_empleado_id = query.tipo_empleado_id;

    if (query.search) {
      const term = query.search.trim();
      if (term) {
        where.OR = [
          { numero_empleado: { contains: term, mode: 'insensitive' } },
          { nombres: { contains: term, mode: 'insensitive' } },
          { apellidos: { contains: term, mode: 'insensitive' } },
          { cedula_identidad: { contains: term, mode: 'insensitive' } },
          { email: { contains: term, mode: 'insensitive' } },
        ];
      }
    }

    const [data, total] = await this.prisma.$transaction([
      this.prisma.rrhh_empleados.findMany({
        where,
        skip,
        take: limit,
        orderBy: [{ apellidos: 'asc' }, { nombres: 'asc' }],
        include: {
          cargo: { select: { id: true, codigo: true, nombre: true } },
          departamento: { select: { id: true, codigo: true, nombre: true } },
          centro_costo: { select: { id: true, codigo: true, descripcion: true } },
          tipo_empleado: { select: { id: true, codigo: true, nombre: true } },
          sucursal: { select: { id: true, descripcion: true } },
          banco: { select: { id: true, codigo: true, descripcion: true } },
          ciudad: { select: { id: true, descripcion: true } },
        },
      }),
      this.prisma.rrhh_empleados.count({ where }),
    ]);

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

  async obtener(id: string, empresaId: string) {
    const empleado = await this.prisma.rrhh_empleados.findFirst({
      where: { id, empresa_id: empresaId },
      include: {
        cargo: true,
        departamento: true,
        centro_costo: true,
        tipo_empleado: true,
        sucursal: { select: { id: true, descripcion: true } },
        banco: true,
        ciudad: { select: { id: true, descripcion: true } },
      },
    });
    if (!empleado) throw new NotFoundException('Empleado no encontrado');
    return empleado;
  }

  private async validarReferencias(
    empresaId: string,
    refs: {
      sucursal_id: string;
      departamento_id: string;
      cargo_id: string;
      centro_costo_id: string;
      tipo_empleado_id: string;
    },
  ) {
    const [sucursal, departamento, cargo, centroCosto, tipoEmpleado] = await Promise.all([
      this.prisma.empresas_sucursales.findFirst({ where: { id: refs.sucursal_id, empresa_id: empresaId } }),
      this.prisma.rrhh_departamentos.findFirst({ where: { id: refs.departamento_id, empresa_id: empresaId } }),
      this.prisma.rrhh_cargos.findFirst({ where: { id: refs.cargo_id, empresa_id: empresaId } }),
      this.prisma.cont_centros_costo.findFirst({ where: { id: refs.centro_costo_id, empresa_id: empresaId } }),
      this.prisma.rrhh_tipos_empleado.findFirst({ where: { id: refs.tipo_empleado_id, empresa_id: empresaId } }),
    ]);

    if (!sucursal) throw new BadRequestException('Sucursal inválida para esta empresa');
    if (!departamento) throw new BadRequestException('Departamento inválido para esta empresa');
    if (!cargo) throw new BadRequestException('Cargo inválido para esta empresa');
    if (!centroCosto) throw new BadRequestException('Centro de costo inválido para esta empresa');
    if (!tipoEmpleado) throw new BadRequestException('Tipo de empleado inválido para esta empresa');
  }

  /**
   * Devuelve el siguiente `numero_empleado` secuencial para la empresa.
   * Considera sólo valores numéricos existentes y devuelve `max + 1` (o `1` si no hay).
   */
  private async siguienteNumeroEmpleado(empresaId: string): Promise<string> {
    const rows = await this.prisma.$queryRaw<Array<{ next: number | null }>>`
      SELECT COALESCE(MAX(CAST(numero_empleado AS INTEGER)), 0) + 1 AS next
      FROM rrhh_empleados
      WHERE empresa_id = ${empresaId}::uuid
        AND numero_empleado ~ '^[0-9]+$'
    `;
    const next = Number(rows?.[0]?.next ?? 1);
    return String(Number.isFinite(next) && next > 0 ? next : 1);
  }

  async crear(empresaId: string, dto: CreateEmpleadoDto, userId?: string) {
    await this.validarReferencias(empresaId, {
      sucursal_id: dto.sucursal_id,
      departamento_id: dto.departamento_id,
      cargo_id: dto.cargo_id,
      centro_costo_id: dto.centro_costo_id,
      tipo_empleado_id: dto.tipo_empleado_id,
    });

    // Si el cliente envía numero_empleado, validamos duplicado de número y cédula
    // en un solo round-trip. Si no, sólo nos importa la cédula (el número lo
    // generamos nosotros con reintento en caso de race).
    if (dto.numero_empleado) {
      const duplicado = await this.prisma.rrhh_empleados.findFirst({
        where: {
          empresa_id: empresaId,
          OR: [
            { numero_empleado: dto.numero_empleado },
            { cedula_identidad: dto.cedula_identidad },
          ],
        },
        select: { id: true, numero_empleado: true, cedula_identidad: true },
      });
      if (duplicado) {
        const motivo =
          duplicado.numero_empleado === dto.numero_empleado
            ? `Ya existe un empleado con número ${dto.numero_empleado}`
            : `Ya existe un empleado con cédula ${dto.cedula_identidad}`;
        throw new BadRequestException(motivo);
      }
    } else {
      const dupCedula = await this.prisma.rrhh_empleados.findFirst({
        where: { empresa_id: empresaId, cedula_identidad: dto.cedula_identidad },
        select: { id: true },
      });
      if (dupCedula) {
        throw new BadRequestException(`Ya existe un empleado con cédula ${dto.cedula_identidad}`);
      }
    }

    // Inserta con retry hasta 5 veces si dos altas concurrentes colisionan en
    // el unique (empresa_id, numero_empleado). Sólo aplica cuando autogeneramos.
    const intentarCrear = async (numero: string) =>
      this.prisma.rrhh_empleados.create({
        data: {
          empresa_id: empresaId,
          numero_empleado: numero,
          nombres: dto.nombres,
          apellidos: dto.apellidos,
          cedula_identidad: dto.cedula_identidad,
          ruc: dto.ruc,
          fecha_nacimiento: new Date(dto.fecha_nacimiento),
          sexo: dto.sexo,
          estado_civil: dto.estado_civil,
          cantidad_hijos: dto.cantidad_hijos ?? 0,
          email: dto.email,
          telefono: dto.telefono,
          direccion: dto.direccion,
          ciudad_id: dto.ciudad_id,
          sucursal_id: dto.sucursal_id,
          departamento_id: dto.departamento_id,
          cargo_id: dto.cargo_id,
          centro_costo_id: dto.centro_costo_id,
          tipo_empleado_id: dto.tipo_empleado_id,
          fecha_ingreso: new Date(dto.fecha_ingreso),
          fecha_egreso: dto.fecha_egreso ? new Date(dto.fecha_egreso) : null,
          salario_base: dto.salario_base,
          tipo_salario: dto.tipo_salario,
          banco_id: dto.banco_id,
          cuenta_bancaria: dto.cuenta_bancaria,
          tipo_cuenta: dto.tipo_cuenta,
          numero_ips: dto.numero_ips,
          // aporta_ips es DERIVADO del régimen: única fuente de verdad es regimen_ips.
          // 'EXENTO' ⇒ no aporta. Se ignora dto.aporta_ips a propósito.
          regimen_ips: dto.regimen_ips ?? 'GENERAL',
          aporta_ips: (dto.regimen_ips ?? 'GENERAL') !== 'EXENTO',
          observaciones: dto.observaciones,
          foto_url: dto.foto_url,
          creado_por: userId,
        },
      });

    if (dto.numero_empleado) {
      return intentarCrear(dto.numero_empleado);
    }

    let intento = 0;
    // eslint-disable-next-line no-constant-condition
    while (true) {
      intento++;
      const numero = await this.siguienteNumeroEmpleado(empresaId);
      try {
        return await intentarCrear(numero);
      } catch (err) {
        const isUnique =
          err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002';
        if (!isUnique || intento >= 5) throw err;
        // Otro proceso tomó el mismo número: reintentar tras una pausa mínima.
        await new Promise((r) => setTimeout(r, 25 * intento));
      }
    }
  }

  async actualizar(id: string, empresaId: string, dto: UpdateEmpleadoDto) {
    const existente = await this.prisma.rrhh_empleados.findFirst({ where: { id, empresa_id: empresaId } });
    if (!existente) throw new NotFoundException('Empleado no encontrado');

    if (
      dto.sucursal_id ||
      dto.departamento_id ||
      dto.cargo_id ||
      dto.centro_costo_id ||
      dto.tipo_empleado_id
    ) {
      await this.validarReferencias(empresaId, {
        sucursal_id: dto.sucursal_id ?? existente.sucursal_id,
        departamento_id: dto.departamento_id ?? existente.departamento_id,
        cargo_id: dto.cargo_id ?? existente.cargo_id,
        centro_costo_id: dto.centro_costo_id ?? existente.centro_costo_id,
        tipo_empleado_id: dto.tipo_empleado_id ?? existente.tipo_empleado_id,
      });
    }

    if (dto.numero_empleado && dto.numero_empleado !== existente.numero_empleado) {
      const dup = await this.prisma.rrhh_empleados.findFirst({
        where: { empresa_id: empresaId, numero_empleado: dto.numero_empleado, NOT: { id } },
      });
      if (dup) throw new BadRequestException(`Ya existe un empleado con número ${dto.numero_empleado}`);
    }
    if (dto.cedula_identidad && dto.cedula_identidad !== existente.cedula_identidad) {
      const dup = await this.prisma.rrhh_empleados.findFirst({
        where: { empresa_id: empresaId, cedula_identidad: dto.cedula_identidad, NOT: { id } },
      });
      if (dup) throw new BadRequestException(`Ya existe un empleado con cédula ${dto.cedula_identidad}`);
    }

    // aporta_ips es DERIVADO del régimen (única fuente de verdad). Si el régimen
    // cambia, recalculamos; si no viene en el dto, lo dejamos como está.
    const regimenEfectivo = dto.regimen_ips ?? existente.regimen_ips;
    return this.prisma.rrhh_empleados.update({
      where: { id },
      data: {
        ...dto,
        regimen_ips: regimenEfectivo,
        aporta_ips: regimenEfectivo !== 'EXENTO',
        fecha_nacimiento: dto.fecha_nacimiento ? new Date(dto.fecha_nacimiento) : undefined,
        fecha_ingreso: dto.fecha_ingreso ? new Date(dto.fecha_ingreso) : undefined,
        fecha_egreso: dto.fecha_egreso ? new Date(dto.fecha_egreso) : undefined,
        updated_at: new Date(),
      },
    });
  }

  async eliminar(id: string, empresaId: string) {
    const empleado = await this.prisma.rrhh_empleados.findFirst({ where: { id, empresa_id: empresaId } });
    if (!empleado) throw new NotFoundException('Empleado no encontrado');

    const esResponsable = await this.prisma.rrhh_departamentos.findFirst({
      where: { empresa_id: empresaId, responsable_empleado_id: id },
    });
    if (esResponsable) {
      throw new BadRequestException(
        'El empleado figura como responsable de un departamento. Reasigne antes de eliminar.',
      );
    }

    return this.prisma.rrhh_empleados.update({
      where: { id },
      data: { estado: 'EGRESADO', fecha_egreso: empleado.fecha_egreso ?? new Date(), updated_at: new Date() },
    });
  }
}
