import {
  Injectable,
  NotFoundException,
  ConflictException,
  BadRequestException,
} from '@nestjs/common';
import { PrismaService } from '../../../prisma/prisma.service';
import { EncryptionService } from '../../../common/services/encryption.service';
import { CreateEmployeeDto, BankAccountDto } from './dto/create-employee.dto';
import { UpdateEmployeeDto } from './dto/update-employee.dto';
import { QueryEmployeesDto } from './dto/query-employees.dto';
import { Prisma, Employee } from '@prisma/client';

export interface EmployeeWithUser extends Employee {
  user: {
    id: string;
    email: string;
    firstName: string;
    lastName: string;
    avatar: string | null;
    department: {
      id: string;
      name: string;
      code: string | null;
    } | null;
  };
}

export interface OrgChartNode {
  id: string;
  employeeNumber: string;
  position: string;
  user: {
    id: string;
    email: string;
    firstName: string;
    lastName: string;
    avatar: string | null;
    department: {
      id: string;
      name: string;
      code: string | null;
    } | null;
  };
  managerId: string | null;
  subordinates: OrgChartNode[];
}

@Injectable()
export class EmployeesService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly encryptionService: EncryptionService,
  ) {}

  /**
   * Create a new employee record
   */
  async create(createDto: CreateEmployeeDto): Promise<EmployeeWithUser> {
    // Check if user exists
    const user = await this.prisma.user.findUnique({
      where: { id: createDto.userId },
    });

    if (!user) {
      throw new NotFoundException(`User with ID ${createDto.userId} not found`);
    }

    // Check if user already has an employee record
    const existingEmployee = await this.prisma.employee.findUnique({
      where: { userId: createDto.userId },
    });

    if (existingEmployee) {
      throw new ConflictException(
        `User ${createDto.userId} already has an employee record`,
      );
    }

    // Check for duplicate employee number
    const existingNumber = await this.prisma.employee.findUnique({
      where: { employeeNumber: createDto.employeeNumber },
    });

    if (existingNumber) {
      throw new ConflictException(
        `Employee number ${createDto.employeeNumber} already exists`,
      );
    }

    // Encrypt sensitive data
    const encryptedBankAccount = createDto.bankAccount
      ? this.encryptionService.encryptObject(createDto.bankAccount)
      : null;

    // Encrypt salary as string for secure storage
    const encryptedSalary = createDto.salary
      ? this.encryptionService.encrypt(createDto.salary.toString())
      : null;

    const employee = await this.prisma.employee.create({
      data: {
        userId: createDto.userId,
        employeeNumber: createDto.employeeNumber,
        position: createDto.position,
        entryDate: createDto.entryDate,
        exitDate: createDto.exitDate,
        contractType: createDto.contractType,
        workingHours: createDto.workingHours ?? 40,
        salary: encryptedSalary,
        taxClass: createDto.taxClass,
        bankAccount: encryptedBankAccount as any,
        isActive: createDto.isActive ?? true,
      },
      include: {
        user: {
          select: {
            id: true,
            email: true,
            firstName: true,
            lastName: true,
            avatar: true,
            department: {
              select: {
                id: true,
                name: true,
                code: true,
              },
            },
          },
        },
      },
    });

    return this.sanitizeEmployee(employee as EmployeeWithUser);
  }

  /**
   * Find all employees with filtering and pagination
   */
  async findAll(query: QueryEmployeesDto) {
    const {
      page = 1,
      limit = 20,
      search,
      departmentId,
      contractType,
      isActive,
      sortBy,
      sortOrder,
    } = query;
    const skip = (page - 1) * limit;

    const where: Prisma.EmployeeWhereInput = {
      ...(search && {
        OR: [
          { employeeNumber: { contains: search, mode: 'insensitive' } },
          { position: { contains: search, mode: 'insensitive' } },
          { user: { firstName: { contains: search, mode: 'insensitive' } } },
          { user: { lastName: { contains: search, mode: 'insensitive' } } },
          { user: { email: { contains: search, mode: 'insensitive' } } },
        ],
      }),
      ...(departmentId && {
        user: { departmentId },
      }),
      ...(contractType && { contractType }),
      ...(isActive !== undefined && { isActive }),
    };

    const orderBy: Prisma.EmployeeOrderByWithRelationInput = sortBy
      ? { [sortBy]: sortOrder || 'asc' }
      : { employeeNumber: 'asc' };

    const [employees, total] = await Promise.all([
      this.prisma.employee.findMany({
        where,
        skip,
        take: limit,
        orderBy,
        include: {
          user: {
            select: {
              id: true,
              email: true,
              firstName: true,
              lastName: true,
              avatar: true,
              department: {
                select: {
                  id: true,
                  name: true,
                  code: true,
                },
              },
            },
          },
        },
      }),
      this.prisma.employee.count({ where }),
    ]);

    return {
      data: employees.map((emp) =>
        this.sanitizeEmployee(emp as EmployeeWithUser),
      ),
      meta: {
        total,
        page,
        limit,
        totalPages: Math.ceil(total / limit),
      },
    };
  }

  /**
   * Find a single employee by ID
   */
  async findOne(id: string, includeSensitive = false): Promise<EmployeeWithUser> {
    const employee = await this.prisma.employee.findUnique({
      where: { id },
      include: {
        user: {
          select: {
            id: true,
            email: true,
            firstName: true,
            lastName: true,
            avatar: true,
            department: {
              select: {
                id: true,
                name: true,
                code: true,
              },
            },
          },
        },
      },
    });

    if (!employee) {
      throw new NotFoundException(`Employee with ID ${id} not found`);
    }

    if (includeSensitive) {
      return this.decryptEmployee(employee as EmployeeWithUser);
    }

    return this.sanitizeEmployee(employee as EmployeeWithUser);
  }

  /**
   * Find employee by user ID
   */
  async findByUserId(userId: string): Promise<EmployeeWithUser | null> {
    const employee = await this.prisma.employee.findUnique({
      where: { userId },
      include: {
        user: {
          select: {
            id: true,
            email: true,
            firstName: true,
            lastName: true,
            avatar: true,
            department: {
              select: {
                id: true,
                name: true,
                code: true,
              },
            },
          },
        },
      },
    });

    if (!employee) {
      return null;
    }

    return this.sanitizeEmployee(employee as EmployeeWithUser);
  }

  /**
   * Update an employee
   */
  async update(
    id: string,
    updateDto: UpdateEmployeeDto,
  ): Promise<EmployeeWithUser> {
    await this.findOne(id);

    // Check for duplicate employee number if updating
    if (updateDto.employeeNumber) {
      const existingNumber = await this.prisma.employee.findFirst({
        where: {
          employeeNumber: updateDto.employeeNumber,
          id: { not: id },
        },
      });

      if (existingNumber) {
        throw new ConflictException(
          `Employee number ${updateDto.employeeNumber} already exists`,
        );
      }
    }

    // Prepare update data with encryption
    const updateData: Prisma.EmployeeUpdateInput = {
      ...(updateDto.employeeNumber && {
        employeeNumber: updateDto.employeeNumber,
      }),
      ...(updateDto.position && { position: updateDto.position }),
      ...(updateDto.entryDate && { entryDate: updateDto.entryDate }),
      ...(updateDto.exitDate !== undefined && { exitDate: updateDto.exitDate }),
      ...(updateDto.contractType && { contractType: updateDto.contractType }),
      ...(updateDto.workingHours !== undefined && {
        workingHours: updateDto.workingHours,
      }),
      ...(updateDto.taxClass !== undefined && { taxClass: updateDto.taxClass }),
      ...(updateDto.isActive !== undefined && { isActive: updateDto.isActive }),
    };

    // Encrypt salary if provided
    if (updateDto.salary !== undefined) {
      updateData.salary = updateDto.salary
        ? this.encryptionService.encrypt(updateDto.salary.toString())
        : null;
    }

    // Encrypt bank account if provided
    if (updateDto.bankAccount !== undefined) {
      updateData.bankAccount = updateDto.bankAccount
        ? (this.encryptionService.encryptObject(
            updateDto.bankAccount,
          ) as any)
        : Prisma.JsonNull;
    }

    const employee = await this.prisma.employee.update({
      where: { id },
      data: updateData,
      include: {
        user: {
          select: {
            id: true,
            email: true,
            firstName: true,
            lastName: true,
            avatar: true,
            department: {
              select: {
                id: true,
                name: true,
                code: true,
              },
            },
          },
        },
      },
    });

    return this.sanitizeEmployee(employee as EmployeeWithUser);
  }

  /**
   * Soft delete an employee (deactivate)
   */
  async remove(id: string): Promise<EmployeeWithUser> {
    const employee = await this.findOne(id);

    if (!employee.isActive) {
      throw new BadRequestException(
        `Employee ${id} is already deactivated`,
      );
    }

    const updated = await this.prisma.employee.update({
      where: { id },
      data: {
        isActive: false,
        exitDate: new Date(),
      },
      include: {
        user: {
          select: {
            id: true,
            email: true,
            firstName: true,
            lastName: true,
            avatar: true,
            department: {
              select: {
                id: true,
                name: true,
                code: true,
              },
            },
          },
        },
      },
    });

    return this.sanitizeEmployee(updated as EmployeeWithUser);
  }

  /**
   * Get time entries for an employee
   */
  async getTimeEntries(
    employeeId: string,
    pagination: { page?: number; limit?: number; startDate?: Date; endDate?: Date } = {},
  ) {
    await this.findOne(employeeId);

    const { page = 1, limit = 20, startDate, endDate } = pagination;
    const skip = (page - 1) * limit;

    const where: Prisma.TimeEntryWhereInput = {
      employeeId,
      ...(startDate || endDate
        ? {
            date: {
              ...(startDate && { gte: startDate }),
              ...(endDate && { lte: endDate }),
            },
          }
        : {}),
    };

    const [entries, total] = await Promise.all([
      this.prisma.timeEntry.findMany({
        where,
        skip,
        take: limit,
        orderBy: { date: 'desc' },
        include: {
          correctedBy: {
            select: {
              id: true,
              firstName: true,
              lastName: true,
            },
          },
        },
      }),
      this.prisma.timeEntry.count({ where }),
    ]);

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

  /**
   * Get organizational chart data (hierarchical)
   */
  async getOrgChart(): Promise<OrgChartNode[]> {
    // Get all active employees with their department info
    const employees = await this.prisma.employee.findMany({
      where: { isActive: true },
      include: {
        user: {
          select: {
            id: true,
            email: true,
            firstName: true,
            lastName: true,
            avatar: true,
            department: {
              select: {
                id: true,
                name: true,
                code: true,
                managerId: true,
              },
            },
          },
        },
      },
    });

    // Build a map of user ID to employee for quick lookup
    const userToEmployee = new Map<string, typeof employees[0]>();
    employees.forEach((emp) => {
      userToEmployee.set(emp.user.id, emp);
    });

    // Build manager relationships based on department managers
    const employeeNodes: Map<string, OrgChartNode> = new Map();

    // First pass: create all nodes
    employees.forEach((emp) => {
      const managerId = emp.user.department?.managerId ?? null;

      employeeNodes.set(emp.id, {
        id: emp.id,
        employeeNumber: emp.employeeNumber,
        position: emp.position,
        user: {
          id: emp.user.id,
          email: emp.user.email,
          firstName: emp.user.firstName,
          lastName: emp.user.lastName,
          avatar: emp.user.avatar,
          department: emp.user.department
            ? {
                id: emp.user.department.id,
                name: emp.user.department.name,
                code: emp.user.department.code,
              }
            : null,
        },
        managerId:
          managerId && managerId !== emp.user.id
            ? (userToEmployee.get(managerId)?.id ?? null)
            : null,
        subordinates: [],
      });
    });

    // Second pass: build hierarchy
    const rootNodes: OrgChartNode[] = [];

    employeeNodes.forEach((node) => {
      if (node.managerId && employeeNodes.has(node.managerId)) {
        const manager = employeeNodes.get(node.managerId)!;
        manager.subordinates.push(node);
      } else {
        rootNodes.push(node);
      }
    });

    // Sort subordinates by name at each level
    const sortSubordinates = (nodes: OrgChartNode[]) => {
      nodes.sort((a, b) =>
        `${a.user.lastName} ${a.user.firstName}`.localeCompare(
          `${b.user.lastName} ${b.user.firstName}`,
        ),
      );
      nodes.forEach((node) => sortSubordinates(node.subordinates));
    };

    sortSubordinates(rootNodes);

    return rootNodes;
  }

  /**
   * Remove sensitive fields from employee data
   */
  private sanitizeEmployee(employee: EmployeeWithUser): EmployeeWithUser {
    return {
      ...employee,
      salary: null,
      bankAccount: null,
    };
  }

  /**
   * Decrypt sensitive employee data
   */
  private decryptEmployee(employee: EmployeeWithUser): EmployeeWithUser {
    let decryptedBankAccount: BankAccountDto | null = null;
    let decryptedSalary: number | null = null;

    // Decrypt bank account
    if (employee.bankAccount && typeof employee.bankAccount === 'string') {
      try {
        decryptedBankAccount = this.encryptionService.decryptObject<BankAccountDto>(
          employee.bankAccount,
        );
      } catch (error) {
        // If decryption fails, return null
        decryptedBankAccount = null;
      }
    }

    // Decrypt salary (stored as encrypted string in DB)
    if (employee.salary) {
      try {
        const decryptedSalaryStr = this.encryptionService.decrypt(employee.salary);
        decryptedSalary = parseFloat(decryptedSalaryStr);
      } catch (error) {
        // If decryption fails, return null
        decryptedSalary = null;
      }
    }

    return {
      ...employee,
      // Return decrypted salary as a string representation of the number for API response
      salary: decryptedSalary !== null ? String(decryptedSalary) : null,
      bankAccount: decryptedBankAccount as unknown as Prisma.JsonValue,
    };
  }
}
