import {
  BadRequestException,
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import {
  AsignarTurnoDto,
  CreateTurnoDto,
  QueryAsignacionesDto,
  QueryTurnosDto,
  TurnoAsignacionTipo,
  TurnoBloqueDto,
  TurnoTipo,
  UpdateTurnoDto,
} from '../dto/turnos.dto';

/**
 * Convierte "HH:MM" o "HH:MM:SS" a Date 1970-01-01T<hh>:<mm>:<ss>Z para Postgres TIME.
 */
function parseHora(value: string): Date {
  const [hh, mm, ss = '0'] = value.split(':');
  const d = new Date('1970-01-01T00:00:00Z');
  d.setUTCHours(Number(hh) || 0, Number(mm) || 0, Number(ss) || 0, 0);
  return d;
}

function horaToString(value: Date | null | undefined): string | null {
  if (!value) return null;
  const hh = String(value.getUTCHours()).padStart(2, '0');
  const mm = String(value.getUTCMinutes()).padStart(2, '0');
  return `${hh}:${mm}`;
}

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

  // ═════════════════════════════════════════════════════════════════
  // TURNOS — CRUD + bloques
  // ═════════════════════════════════════════════════════════════════

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

    const where: Prisma.rrhh_turnosWhereInput = { empresa_id: empresaId };
    if (query.tipo) where.tipo = query.tipo as any;
    if (typeof query.activo === 'boolean') where.activo = query.activo;
    if (query.q) {
      where.nombre = { contains: query.q, mode: 'insensitive' };
    }

    const [data, total] = await this.prisma.$transaction([
      this.prisma.rrhh_turnos.findMany({
        where,
        skip,
        take: limit,
        orderBy: [{ activo: 'desc' }, { nombre: 'asc' }],
        include: {
          bloques: { orderBy: { orden: 'asc' } },
          _count: { select: { asignaciones: true } },
        },
      }),
      this.prisma.rrhh_turnos.count({ where }),
    ]);

    return {
      data: data.map((t) => this.serializarTurno(t)),
      total,
      page,
      limit,
    };
  }

  async obtener(id: string, empresaId: string) {
    const turno = await this.prisma.rrhh_turnos.findFirst({
      where: { id, empresa_id: empresaId },
      include: {
        bloques: { orderBy: { orden: 'asc' } },
        _count: { select: { asignaciones: true } },
      },
    });
    if (!turno) throw new NotFoundException('Turno no encontrado');
    return this.serializarTurno(turno);
  }

  async crear(empresaId: string, dto: CreateTurnoDto, userId?: string) {
    this.validarBloques(dto.tipo, dto.bloques);
    this.validarDiasSemana(dto);

    return this.prisma.$transaction(async (tx) => {
      const turno = await tx.rrhh_turnos.create({
        data: {
          empresa_id: empresaId,
          nombre: dto.nombre,
          descripcion: dto.descripcion ?? null,
          tipo: dto.tipo as any,
          lunes: dto.lunes ?? false,
          martes: dto.martes ?? false,
          miercoles: dto.miercoles ?? false,
          jueves: dto.jueves ?? false,
          viernes: dto.viernes ?? false,
          sabado: dto.sabado ?? false,
          domingo: dto.domingo ?? false,
          activo: dto.activo ?? true,
          creado_por: userId ?? null,
        },
      });
      await tx.rrhh_turnos_bloques.createMany({
        data: dto.bloques.map((b) => ({
          turno_id: turno.id,
          orden: b.orden,
          hora_entrada: parseHora(b.hora_entrada),
          hora_salida: parseHora(b.hora_salida),
        })),
      });
      const completo = await tx.rrhh_turnos.findUnique({
        where: { id: turno.id },
        include: {
          bloques: { orderBy: { orden: 'asc' } },
          _count: { select: { asignaciones: true } },
        },
      });
      return this.serializarTurno(completo!);
    });
  }

  async actualizar(id: string, empresaId: string, dto: UpdateTurnoDto) {
    const turno = await this.prisma.rrhh_turnos.findFirst({
      where: { id, empresa_id: empresaId },
    });
    if (!turno) throw new NotFoundException('Turno no encontrado');

    const tipoFinal = (dto.tipo ?? turno.tipo) as TurnoTipo;
    if (dto.bloques) {
      this.validarBloques(tipoFinal, dto.bloques);
    }

    // Si el caller manda dias_semana parciales, validamos con el merge.
    if (
      dto.lunes !== undefined ||
      dto.martes !== undefined ||
      dto.miercoles !== undefined ||
      dto.jueves !== undefined ||
      dto.viernes !== undefined ||
      dto.sabado !== undefined ||
      dto.domingo !== undefined
    ) {
      this.validarDiasSemana({
        lunes: dto.lunes ?? turno.lunes,
        martes: dto.martes ?? turno.martes,
        miercoles: dto.miercoles ?? turno.miercoles,
        jueves: dto.jueves ?? turno.jueves,
        viernes: dto.viernes ?? turno.viernes,
        sabado: dto.sabado ?? turno.sabado,
        domingo: dto.domingo ?? turno.domingo,
      });
    }

    return this.prisma.$transaction(async (tx) => {
      await tx.rrhh_turnos.update({
        where: { id },
        data: {
          nombre: dto.nombre ?? undefined,
          descripcion: dto.descripcion ?? undefined,
          tipo: dto.tipo ? (dto.tipo as any) : undefined,
          lunes: dto.lunes ?? undefined,
          martes: dto.martes ?? undefined,
          miercoles: dto.miercoles ?? undefined,
          jueves: dto.jueves ?? undefined,
          viernes: dto.viernes ?? undefined,
          sabado: dto.sabado ?? undefined,
          domingo: dto.domingo ?? undefined,
          activo: dto.activo ?? undefined,
          updated_at: new Date(),
        },
      });

      if (dto.bloques) {
        await tx.rrhh_turnos_bloques.deleteMany({ where: { turno_id: id } });
        await tx.rrhh_turnos_bloques.createMany({
          data: dto.bloques.map((b) => ({
            turno_id: id,
            orden: b.orden,
            hora_entrada: parseHora(b.hora_entrada),
            hora_salida: parseHora(b.hora_salida),
          })),
        });
      }

      const completo = await tx.rrhh_turnos.findUnique({
        where: { id },
        include: {
          bloques: { orderBy: { orden: 'asc' } },
          _count: { select: { asignaciones: true } },
        },
      });
      return this.serializarTurno(completo!);
    });
  }

  async eliminar(id: string, empresaId: string) {
    const turno = await this.prisma.rrhh_turnos.findFirst({
      where: { id, empresa_id: empresaId },
      include: { _count: { select: { asignaciones: true, novedades: true } } },
    });
    if (!turno) throw new NotFoundException('Turno no encontrado');

    if (turno._count.asignaciones > 0 || turno._count.novedades > 0) {
      // No se borra: se desactiva para conservar trazabilidad histórica.
      const desactivado = await this.prisma.rrhh_turnos.update({
        where: { id },
        data: { activo: false, updated_at: new Date() },
        include: {
          bloques: { orderBy: { orden: 'asc' } },
          _count: { select: { asignaciones: true } },
        },
      });
      return { eliminado: false, desactivado: true, turno: this.serializarTurno(desactivado) };
    }

    await this.prisma.rrhh_turnos.delete({ where: { id } });
    return { eliminado: true, desactivado: false };
  }

  // ═════════════════════════════════════════════════════════════════
  // CATÁLOGO DE EMPLEADOS PARA PRESENTISMO
  // ═════════════════════════════════════════════════════════════════

  async listarEmpleadosCatalogo(
    empresaId: string,
    query: { page?: number; limit?: number; search?: string; estado?: string },
  ) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 200;
    const skip = (page - 1) * limit;

    const where: Prisma.rrhh_empleadosWhereInput = { empresa_id: empresaId };
    if (query.estado) where.estado = query.estado;
    if (query.search?.trim()) {
      const term = query.search.trim();
      where.OR = [
        { numero_empleado: { contains: term, mode: 'insensitive' } },
        { nombres: { contains: term, mode: 'insensitive' } },
        { apellidos: { contains: term, mode: 'insensitive' } },
        { cedula_identidad: { 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' }],
        select: {
          id: true,
          numero_empleado: true,
          cedula_identidad: true,
          nombres: true,
          apellidos: true,
          estado: true,
          sucursal_id: true,
          departamento_id: true,
        },
      }),
      this.prisma.rrhh_empleados.count({ where }),
    ]);

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

  // ═════════════════════════════════════════════════════════════════
  // ASIGNACIONES — empleados ↔ turnos
  // ═════════════════════════════════════════════════════════════════

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

    const where: Prisma.rrhh_empleados_turnosWhereInput = {
      empleado_id: empleadoId,
      empresa_id: empresaId,
    };
    if (query.tipo_asignacion) where.tipo_asignacion = query.tipo_asignacion as any;
    if (query.fecha) {
      const fecha = new Date(query.fecha);
      where.AND = [
        {
          OR: [
            { fecha_inicio: null },
            { fecha_inicio: { lte: fecha } },
          ],
        },
        {
          OR: [
            { fecha_fin: null },
            { fecha_fin: { gte: fecha } },
          ],
        },
      ];
    }

    const data = await this.prisma.rrhh_empleados_turnos.findMany({
      where,
      orderBy: [
        { tipo_asignacion: 'asc' },
        { prioridad: 'asc' },
        { fecha_inicio: 'desc' },
      ],
      include: {
        turno: { select: { id: true, nombre: true, tipo: true, activo: true } },
      },
    });
    return data;
  }

  async asignar(empleadoId: string, empresaId: string, dto: AsignarTurnoDto, userId?: string) {
    const empleado = await this.prisma.rrhh_empleados.findFirst({
      where: { id: empleadoId, empresa_id: empresaId },
      select: { id: true, estado: true },
    });
    if (!empleado) throw new NotFoundException('Empleado no encontrado');
    if (empleado.estado !== 'ACTIVO') {
      throw new BadRequestException('Sólo se puede asignar turnos a empleados ACTIVOS');
    }

    const turno = await this.prisma.rrhh_turnos.findFirst({
      where: { id: dto.turno_id, empresa_id: empresaId },
      select: { id: true, activo: true },
    });
    if (!turno) throw new BadRequestException('Turno inválido para esta empresa');
    if (!turno.activo) throw new BadRequestException('No se puede asignar un turno inactivo');

    if (dto.tipo_asignacion === TurnoAsignacionTipo.OCASIONAL) {
      if (!dto.fecha_inicio || !dto.fecha_fin) {
        throw new BadRequestException(
          'Las asignaciones OCASIONALES requieren fecha_inicio y fecha_fin',
        );
      }
      if (new Date(dto.fecha_inicio) > new Date(dto.fecha_fin)) {
        throw new BadRequestException('fecha_inicio debe ser anterior o igual a fecha_fin');
      }
      await this.validarSinConflictoOcasional(empleadoId, empresaId, dto);
    } else {
      // PERPETUA: si existe otra perpetua vigente, la cerramos en su fecha_fin actual
      // o la convertimos en histórica al setear fecha_fin = (dto.fecha_inicio - 1).
      if (dto.fecha_fin) {
        throw new BadRequestException(
          'Las asignaciones PERPETUAS no llevan fecha_fin (queda abierta indefinidamente)',
        );
      }
    }

    return this.prisma.rrhh_empleados_turnos.create({
      data: {
        empresa_id: empresaId,
        empleado_id: empleadoId,
        turno_id: dto.turno_id,
        tipo_asignacion: dto.tipo_asignacion as any,
        fecha_inicio: dto.fecha_inicio ? new Date(dto.fecha_inicio) : null,
        fecha_fin: dto.fecha_fin ? new Date(dto.fecha_fin) : null,
        prioridad: dto.prioridad ?? 100,
        observacion: dto.observacion ?? null,
        creado_por: userId ?? null,
      },
      include: {
        turno: { select: { id: true, nombre: true, tipo: true } },
      },
    });
  }

  async eliminarAsignacion(asignacionId: string, empleadoId: string, empresaId: string) {
    const asign = await this.prisma.rrhh_empleados_turnos.findFirst({
      where: { id: asignacionId, empleado_id: empleadoId, empresa_id: empresaId },
    });
    if (!asign) throw new NotFoundException('Asignación no encontrada');
    await this.prisma.rrhh_empleados_turnos.delete({ where: { id: asignacionId } });
    return { eliminado: true };
  }

  // ═════════════════════════════════════════════════════════════════
  // Helpers privados
  // ═════════════════════════════════════════════════════════════════

  private validarBloques(tipo: TurnoTipo, bloques: TurnoBloqueDto[]) {
    if (!bloques.length) {
      throw new BadRequestException('Debe especificar al menos un bloque horario');
    }

    if (tipo === TurnoTipo.SIMPLE && bloques.length !== 1) {
      throw new BadRequestException('Un turno SIMPLE debe tener exactamente un bloque horario');
    }
    if (tipo === TurnoTipo.CORTADO && bloques.length < 2) {
      throw new BadRequestException('Un turno CORTADO debe tener al menos dos bloques horarios');
    }

    // Validar órdenes correlativos 1..N únicos
    const ordenes = bloques.map((b) => b.orden);
    const setOrdenes = new Set(ordenes);
    if (setOrdenes.size !== ordenes.length) {
      throw new BadRequestException('Los órdenes de los bloques deben ser únicos');
    }
    const ordenado = [...bloques].sort((a, b) => a.orden - b.orden);
    for (let i = 0; i < ordenado.length; i++) {
      if (ordenado[i].orden !== i + 1) {
        throw new BadRequestException(
          'Los órdenes de los bloques deben ser correlativos comenzando en 1',
        );
      }
    }

    // Validar horario de cada bloque y no solapamiento
    for (const b of ordenado) {
      const entrada = parseHora(b.hora_entrada);
      const salida = parseHora(b.hora_salida);
      if (entrada >= salida) {
        throw new BadRequestException(
          `Bloque ${b.orden}: hora_entrada (${b.hora_entrada}) debe ser anterior a hora_salida (${b.hora_salida})`,
        );
      }
    }
    for (let i = 1; i < ordenado.length; i++) {
      const prev = ordenado[i - 1];
      const curr = ordenado[i];
      const prevSalida = parseHora(prev.hora_salida);
      const currEntrada = parseHora(curr.hora_entrada);
      if (currEntrada <= prevSalida) {
        throw new BadRequestException(
          `Bloque ${curr.orden}: hora_entrada (${curr.hora_entrada}) debe ser estrictamente posterior a hora_salida del bloque ${prev.orden} (${prev.hora_salida})`,
        );
      }
    }
  }

  private validarDiasSemana(dto: {
    lunes?: boolean;
    martes?: boolean;
    miercoles?: boolean;
    jueves?: boolean;
    viernes?: boolean;
    sabado?: boolean;
    domingo?: boolean;
  }) {
    const algun =
      dto.lunes || dto.martes || dto.miercoles || dto.jueves || dto.viernes || dto.sabado || dto.domingo;
    if (!algun) {
      throw new BadRequestException('Debe marcar al menos un día de la semana en el que aplica el turno');
    }
  }

  /**
   * Regla 1 del plan: dos asignaciones OCASIONAL solapadas con la misma
   * prioridad para el mismo empleado son inválidas (HTTP 409).
   */
  private async validarSinConflictoOcasional(
    empleadoId: string,
    empresaId: string,
    dto: AsignarTurnoDto,
  ) {
    const desde = new Date(dto.fecha_inicio!);
    const hasta = new Date(dto.fecha_fin!);
    const prioridad = dto.prioridad ?? 100;

    const solapadas = await this.prisma.rrhh_empleados_turnos.findMany({
      where: {
        empresa_id: empresaId,
        empleado_id: empleadoId,
        tipo_asignacion: 'OCASIONAL',
        prioridad,
        // Rangos que se intersectan con [desde, hasta]
        OR: [
          {
            AND: [
              { fecha_inicio: { lte: hasta } },
              { fecha_fin: { gte: desde } },
            ],
          },
        ],
      },
      select: { id: true, fecha_inicio: true, fecha_fin: true },
    });
    if (solapadas.length > 0) {
      throw new ConflictException(
        `Ya existe una asignación OCASIONAL con prioridad ${prioridad} solapada al rango pedido. ` +
          `Cambiá la prioridad (menor número = mayor precedencia) o ajustá las fechas.`,
      );
    }
  }

  private serializarTurno(turno: any) {
    return {
      id: turno.id,
      empresa_id: turno.empresa_id,
      nombre: turno.nombre,
      descripcion: turno.descripcion,
      tipo: turno.tipo,
      lunes: turno.lunes,
      martes: turno.martes,
      miercoles: turno.miercoles,
      jueves: turno.jueves,
      viernes: turno.viernes,
      sabado: turno.sabado,
      domingo: turno.domingo,
      activo: turno.activo,
      creado_por: turno.creado_por,
      created_at: turno.created_at,
      updated_at: turno.updated_at,
      bloques: (turno.bloques ?? []).map((b: any) => ({
        id: b.id,
        orden: b.orden,
        hora_entrada: horaToString(b.hora_entrada),
        hora_salida: horaToString(b.hora_salida),
      })),
      asignaciones_count: turno._count?.asignaciones ?? 0,
    };
  }
}
