/**
 * EmployeeService - Servicio para gestión de empleados
 *
 * CRUD de empleados con gestión de estado y asignaciones a obras.
 *
 * @module HR
 */

import { DataSource, Repository, FindOptionsWhere } from 'typeorm';
import { Employee, EstadoEmpleado, Genero } from '../entities/employee.entity';
import { EmployeeFraccionamiento } from '../entities/employee-fraccionamiento.entity';

interface ServiceContext {
  tenantId: string;
  userId?: string;
}

interface PaginatedResult<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

export { Genero };

export interface CreateEmployeeDto {
  codigo: string;
  nombre: string;
  apellidoPaterno: string;
  apellidoMaterno?: string;
  curp?: string;
  rfc?: string;
  nss?: string;
  fechaNacimiento?: Date;
  genero?: Genero;
  email?: string;
  telefono?: string;
  direccion?: string;
  fechaIngreso: Date;
  puestoId?: string;
  departamento?: string;
  tipoContrato?: string;
  salarioDiario?: number;
  fotoUrl?: string;
}

export interface UpdateEmployeeDto {
  nombre?: string;
  apellidoPaterno?: string;
  apellidoMaterno?: string;
  curp?: string;
  rfc?: string;
  nss?: string;
  fechaNacimiento?: Date;
  genero?: Genero;
  email?: string;
  telefono?: string;
  direccion?: string;
  puestoId?: string;
  departamento?: string;
  tipoContrato?: string;
  salarioDiario?: number;
  fotoUrl?: string;
}

export interface AssignFraccionamientoDto {
  fraccionamientoId: string;
  fechaInicio: Date;
  fechaFin?: Date;
  rol?: string;
}

export interface EmployeeFilters {
  estado?: EstadoEmpleado;
  puestoId?: string;
  departamento?: string;
  fraccionamientoId?: string;
  search?: string;
}

export interface EmployeeStats {
  total: number;
  activos: number;
  inactivos: number;
  bajas: number;
  porDepartamento: { departamento: string; count: number }[];
}

export class EmployeeService {
  private employeeRepository: Repository<Employee>;
  private asignacionRepository: Repository<EmployeeFraccionamiento>;

  constructor(dataSource: DataSource) {
    this.employeeRepository = dataSource.getRepository(Employee);
    this.asignacionRepository = dataSource.getRepository(EmployeeFraccionamiento);
  }

  async findWithFilters(
    ctx: ServiceContext,
    filters: EmployeeFilters = {},
    page: number = 1,
    limit: number = 20
  ): Promise<PaginatedResult<Employee>> {
    const skip = (page - 1) * limit;

    const queryBuilder = this.employeeRepository
      .createQueryBuilder('employee')
      .leftJoinAndSelect('employee.puesto', 'puesto')
      .where('employee.tenant_id = :tenantId', { tenantId: ctx.tenantId });

    if (filters.estado) {
      queryBuilder.andWhere('employee.estado = :estado', { estado: filters.estado });
    }

    if (filters.puestoId) {
      queryBuilder.andWhere('employee.puesto_id = :puestoId', { puestoId: filters.puestoId });
    }

    if (filters.departamento) {
      queryBuilder.andWhere('employee.departamento = :departamento', { departamento: filters.departamento });
    }

    if (filters.fraccionamientoId) {
      queryBuilder
        .innerJoin('employee.asignaciones', 'asig')
        .andWhere('asig.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId: filters.fraccionamientoId })
        .andWhere('asig.activo = true');
    }

    if (filters.search) {
      queryBuilder.andWhere(
        '(employee.codigo ILIKE :search OR employee.nombre ILIKE :search OR employee.apellido_paterno ILIKE :search OR employee.curp ILIKE :search)',
        { search: `%${filters.search}%` }
      );
    }

    queryBuilder
      .orderBy('employee.apellido_paterno', 'ASC')
      .addOrderBy('employee.nombre', 'ASC')
      .skip(skip)
      .take(limit);

    const [data, total] = await queryBuilder.getManyAndCount();

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  async findById(ctx: ServiceContext, id: string): Promise<Employee | null> {
    return this.employeeRepository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
      } as FindOptionsWhere<Employee>,
      relations: ['puesto', 'asignaciones', 'asignaciones.fraccionamiento'],
    });
  }

  async findByCodigo(ctx: ServiceContext, codigo: string): Promise<Employee | null> {
    return this.employeeRepository.findOne({
      where: {
        codigo,
        tenantId: ctx.tenantId,
      } as FindOptionsWhere<Employee>,
    });
  }

  async findByCurp(ctx: ServiceContext, curp: string): Promise<Employee | null> {
    return this.employeeRepository.findOne({
      where: {
        curp,
        tenantId: ctx.tenantId,
      } as FindOptionsWhere<Employee>,
    });
  }

  async create(ctx: ServiceContext, dto: CreateEmployeeDto): Promise<Employee> {
    // Validate unique codigo
    const existingCodigo = await this.findByCodigo(ctx, dto.codigo);
    if (existingCodigo) {
      throw new Error(`Employee with codigo ${dto.codigo} already exists`);
    }

    // Validate unique CURP if provided
    if (dto.curp) {
      const existingCurp = await this.findByCurp(ctx, dto.curp);
      if (existingCurp) {
        throw new Error(`Employee with CURP ${dto.curp} already exists`);
      }
    }

    const employee = this.employeeRepository.create({
      tenantId: ctx.tenantId,
      createdById: ctx.userId ?? null,
      estado: 'activo',
      codigo: dto.codigo,
      nombre: dto.nombre,
      apellidoPaterno: dto.apellidoPaterno,
      apellidoMaterno: dto.apellidoMaterno,
      curp: dto.curp,
      rfc: dto.rfc,
      nss: dto.nss,
      fechaNacimiento: dto.fechaNacimiento,
      genero: dto.genero,
      email: dto.email,
      telefono: dto.telefono,
      direccion: dto.direccion,
      fechaIngreso: dto.fechaIngreso,
      puestoId: dto.puestoId,
      departamento: dto.departamento,
      tipoContrato: dto.tipoContrato,
      salarioDiario: dto.salarioDiario,
      fotoUrl: dto.fotoUrl,
    } as Partial<Employee>);

    const saved = await this.employeeRepository.save(employee);
    return Array.isArray(saved) ? saved[0] : saved;
  }

  async update(ctx: ServiceContext, id: string, dto: UpdateEmployeeDto): Promise<Employee | null> {
    const existing = await this.findById(ctx, id);
    if (!existing) {
      return null;
    }

    // Validate unique CURP if changed
    if (dto.curp && dto.curp !== existing.curp) {
      const existingCurp = await this.findByCurp(ctx, dto.curp);
      if (existingCurp) {
        throw new Error(`Employee with CURP ${dto.curp} already exists`);
      }
    }

    const updated = this.employeeRepository.merge(existing, dto);
    return this.employeeRepository.save(updated);
  }

  async changeStatus(ctx: ServiceContext, id: string, estado: EstadoEmpleado, fechaBaja?: Date): Promise<Employee | null> {
    const existing = await this.findById(ctx, id);
    if (!existing) {
      return null;
    }

    existing.estado = estado;
    if (estado === 'baja' && fechaBaja) {
      existing.fechaBaja = fechaBaja;
    }

    return this.employeeRepository.save(existing);
  }

  async assignToFraccionamiento(
    ctx: ServiceContext,
    employeeId: string,
    dto: AssignFraccionamientoDto
  ): Promise<EmployeeFraccionamiento> {
    const employee = await this.findById(ctx, employeeId);
    if (!employee) {
      throw new Error('Employee not found');
    }

    // Deactivate previous active assignment to same fraccionamiento
    await this.asignacionRepository.update(
      {
        tenantId: ctx.tenantId,
        employeeId,
        fraccionamientoId: dto.fraccionamientoId,
        activo: true,
      } as FindOptionsWhere<EmployeeFraccionamiento>,
      { activo: false, fechaFin: new Date() }
    );

    const asignacion = this.asignacionRepository.create({
      tenantId: ctx.tenantId,
      employeeId,
      fraccionamientoId: dto.fraccionamientoId,
      fechaInicio: dto.fechaInicio,
      fechaFin: dto.fechaFin,
      rol: dto.rol,
      activo: true,
    });

    return this.asignacionRepository.save(asignacion);
  }

  async removeFromFraccionamiento(
    ctx: ServiceContext,
    employeeId: string,
    fraccionamientoId: string
  ): Promise<boolean> {
    const result = await this.asignacionRepository.update(
      {
        tenantId: ctx.tenantId,
        employeeId,
        fraccionamientoId,
        activo: true,
      } as FindOptionsWhere<EmployeeFraccionamiento>,
      { activo: false, fechaFin: new Date() }
    );

    return (result.affected ?? 0) > 0;
  }

  async getEmployeesByFraccionamiento(
    ctx: ServiceContext,
    fraccionamientoId: string
  ): Promise<Employee[]> {
    const asignaciones = await this.asignacionRepository.find({
      where: {
        tenantId: ctx.tenantId,
        fraccionamientoId,
        activo: true,
      } as FindOptionsWhere<EmployeeFraccionamiento>,
      relations: ['employee', 'employee.puesto'],
    });

    return asignaciones.map(a => a.employee);
  }

  async getStats(ctx: ServiceContext): Promise<EmployeeStats> {
    const [total, activos, inactivos, bajas] = await Promise.all([
      this.employeeRepository.count({
        where: { tenantId: ctx.tenantId } as FindOptionsWhere<Employee>,
      }),
      this.employeeRepository.count({
        where: { tenantId: ctx.tenantId, estado: 'activo' } as FindOptionsWhere<Employee>,
      }),
      this.employeeRepository.count({
        where: { tenantId: ctx.tenantId, estado: 'inactivo' } as FindOptionsWhere<Employee>,
      }),
      this.employeeRepository.count({
        where: { tenantId: ctx.tenantId, estado: 'baja' } as FindOptionsWhere<Employee>,
      }),
    ]);

    const porDepartamento = await this.employeeRepository
      .createQueryBuilder('employee')
      .select('employee.departamento', 'departamento')
      .addSelect('COUNT(*)', 'count')
      .where('employee.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('employee.estado = :estado', { estado: 'activo' })
      .groupBy('employee.departamento')
      .getRawMany();

    return {
      total,
      activos,
      inactivos,
      bajas,
      porDepartamento: porDepartamento.map(p => ({
        departamento: p.departamento || 'Sin departamento',
        count: parseInt(p.count, 10),
      })),
    };
  }
}
