import {
  Injectable,
  NotFoundException,
  ConflictException,
  BadRequestException,
} from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { CreateDepartmentDto } from './dto/create-department.dto';
import { UpdateDepartmentDto } from './dto/update-department.dto';
import { QueryDepartmentsDto } from './dto/query-departments.dto';
import { Prisma } from '@prisma/client';

export interface DepartmentWithChildren {
  id: string;
  name: string;
  code: string | null;
  parentId: string | null;
  managerId: string | null;
  isActive: boolean;
  createdAt: Date;
  updatedAt: Date;
  children?: DepartmentWithChildren[];
  manager?: {
    id: string;
    firstName: string;
    lastName: string;
    email: string;
  } | null;
  _count?: {
    users: number;
    children: number;
  };
}

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

  /**
   * Create a new department
   */
  async create(createDto: CreateDepartmentDto) {
    // Check for duplicate code
    if (createDto.code) {
      const existingCode = await this.prisma.department.findUnique({
        where: { code: createDto.code },
      });

      if (existingCode) {
        throw new ConflictException(
          `Department with code ${createDto.code} already exists`,
        );
      }
    }

    // Validate parent exists if provided
    if (createDto.parentId) {
      const parent = await this.prisma.department.findUnique({
        where: { id: createDto.parentId },
      });

      if (!parent) {
        throw new NotFoundException(
          `Parent department with ID ${createDto.parentId} not found`,
        );
      }
    }

    // Validate manager exists if provided
    if (createDto.managerId) {
      const manager = await this.prisma.user.findUnique({
        where: { id: createDto.managerId },
      });

      if (!manager) {
        throw new NotFoundException(
          `Manager with ID ${createDto.managerId} not found`,
        );
      }
    }

    return this.prisma.department.create({
      data: {
        name: createDto.name,
        code: createDto.code,
        parentId: createDto.parentId,
        managerId: createDto.managerId,
        isActive: createDto.isActive ?? true,
      },
      include: {
        parent: true,
        manager: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            email: true,
          },
        },
        _count: {
          select: {
            users: true,
            children: true,
          },
        },
      },
    });
  }

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

    const where: Prisma.DepartmentWhereInput = {
      ...(search && {
        OR: [
          { name: { contains: search, mode: 'insensitive' } },
          { code: { contains: search, mode: 'insensitive' } },
        ],
      }),
      ...(parentId === 'null' ? { parentId: null } : parentId && { parentId }),
      ...(isActive !== undefined && { isActive }),
    };

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

    const [departments, total] = await Promise.all([
      this.prisma.department.findMany({
        where,
        skip,
        take: limit,
        orderBy,
        include: {
          parent: true,
          manager: {
            select: {
              id: true,
              firstName: true,
              lastName: true,
              email: true,
            },
          },
          _count: {
            select: {
              users: true,
              children: true,
            },
          },
        },
      }),
      this.prisma.department.count({ where }),
    ]);

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

  /**
   * Find a single department by ID
   */
  async findOne(id: string) {
    const department = await this.prisma.department.findUnique({
      where: { id },
      include: {
        parent: true,
        children: true,
        manager: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            email: true,
          },
        },
        _count: {
          select: {
            users: true,
            children: true,
          },
        },
      },
    });

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

    return department;
  }

  /**
   * Update a department
   */
  async update(id: string, updateDto: UpdateDepartmentDto) {
    await this.findOne(id);

    // Check for duplicate code if updating
    if (updateDto.code) {
      const existingCode = await this.prisma.department.findFirst({
        where: {
          code: updateDto.code,
          id: { not: id },
        },
      });

      if (existingCode) {
        throw new ConflictException(
          `Department with code ${updateDto.code} already exists`,
        );
      }
    }

    // Prevent circular parent reference
    if (updateDto.parentId) {
      if (updateDto.parentId === id) {
        throw new BadRequestException('Department cannot be its own parent');
      }

      // Check if the new parent is a child of this department
      const isCircular = await this.isDescendant(updateDto.parentId, id);
      if (isCircular) {
        throw new BadRequestException(
          'Cannot set a descendant department as parent',
        );
      }

      const parent = await this.prisma.department.findUnique({
        where: { id: updateDto.parentId },
      });

      if (!parent) {
        throw new NotFoundException(
          `Parent department with ID ${updateDto.parentId} not found`,
        );
      }
    }

    // Validate manager exists if provided
    if (updateDto.managerId) {
      const manager = await this.prisma.user.findUnique({
        where: { id: updateDto.managerId },
      });

      if (!manager) {
        throw new NotFoundException(
          `Manager with ID ${updateDto.managerId} not found`,
        );
      }
    }

    return this.prisma.department.update({
      where: { id },
      data: updateDto,
      include: {
        parent: true,
        manager: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            email: true,
          },
        },
        _count: {
          select: {
            users: true,
            children: true,
          },
        },
      },
    });
  }

  /**
   * Delete a department (soft delete by deactivating)
   */
  async remove(id: string) {
    const department = await this.findOne(id);

    // Check if department has active users
    const hasActiveUsers = await this.prisma.user.count({
      where: { departmentId: id, isActive: true },
    });

    if (hasActiveUsers > 0) {
      throw new BadRequestException(
        `Cannot deactivate department with ${hasActiveUsers} active users`,
      );
    }

    // Check if department has active children
    const hasActiveChildren = await this.prisma.department.count({
      where: { parentId: id, isActive: true },
    });

    if (hasActiveChildren > 0) {
      throw new BadRequestException(
        `Cannot deactivate department with ${hasActiveChildren} active sub-departments`,
      );
    }

    return this.prisma.department.update({
      where: { id },
      data: { isActive: false },
      include: {
        parent: true,
        manager: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            email: true,
          },
        },
        _count: {
          select: {
            users: true,
            children: true,
          },
        },
      },
    });
  }

  /**
   * Get employees of a department
   */
  async getEmployees(
    departmentId: string,
    pagination: { page?: number; limit?: number } = {},
  ) {
    await this.findOne(departmentId);

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

    const [users, total] = await Promise.all([
      this.prisma.user.findMany({
        where: { departmentId, isActive: true },
        skip,
        take: limit,
        orderBy: { lastName: 'asc' },
        include: {
          employee: true,
          roles: {
            include: {
              role: true,
            },
          },
        },
      }),
      this.prisma.user.count({
        where: { departmentId, isActive: true },
      }),
    ]);

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

  /**
   * Get hierarchical department tree
   */
  async getTree(): Promise<DepartmentWithChildren[]> {
    const allDepartments = await this.prisma.department.findMany({
      where: { isActive: true },
      orderBy: { name: 'asc' },
      include: {
        manager: {
          select: {
            id: true,
            firstName: true,
            lastName: true,
            email: true,
          },
        },
        _count: {
          select: {
            users: true,
            children: true,
          },
        },
      },
    });

    return this.buildTree(allDepartments, null);
  }

  /**
   * Build tree structure from flat list
   */
  private buildTree(
    departments: DepartmentWithChildren[],
    parentId: string | null,
  ): DepartmentWithChildren[] {
    return departments
      .filter((dept) => dept.parentId === parentId)
      .map((dept) => ({
        ...dept,
        children: this.buildTree(departments, dept.id),
      }));
  }

  /**
   * Check if potentialDescendant is a descendant of ancestorId
   */
  private async isDescendant(
    potentialDescendantId: string,
    ancestorId: string,
  ): Promise<boolean> {
    const descendants = await this.getAllDescendants(ancestorId);
    return descendants.includes(potentialDescendantId);
  }

  /**
   * Get all descendant IDs of a department
   */
  private async getAllDescendants(departmentId: string): Promise<string[]> {
    const children = await this.prisma.department.findMany({
      where: { parentId: departmentId },
      select: { id: true },
    });

    if (children.length === 0) {
      return [];
    }

    const childIds = children.map((c) => c.id);
    const grandchildIds = await Promise.all(
      childIds.map((id) => this.getAllDescendants(id)),
    );

    return [...childIds, ...grandchildIds.flat()];
  }
}
