import {
  Injectable,
  ConflictException,
  NotFoundException,
  BadRequestException,
  ForbiddenException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ClsService } from 'nestjs-cls';
import * as bcrypt from 'bcrypt';
import { UserRepository } from './repositories/user.repository';
import { RoleEntity } from '../../entities/role.entity';
import { UserEntity } from '../../entities/user.entity';
import { CreateUserDto, UpdateUserDto, ResetPasswordDto } from './dto';
import { CLS_USER_ID } from '../../common/cls/cls.constants';
import { serviceMessages } from '../../common/constants/messages';
import { LOOKUP_MAX } from '../../common/constants/lookup-limits';
import { RoleHierarchyService } from '../role/role-hierarchy.service';

const MSG = serviceMessages('User');

@Injectable()
export class UserService {
  constructor(
    private readonly userRepo: UserRepository,
    @InjectRepository(RoleEntity)
    private readonly roleRepo: Repository<RoleEntity>,
    private readonly cls: ClsService,
    private readonly roleHierarchy: RoleHierarchyService,
  ) {}

  /**
   * Get current user ID from CLS and throw if operating on self.
   */
  private preventSelfAction(targetUserId: string, action: string): void {
    const currentUserId = this.cls.get<string>(CLS_USER_ID);
    if (currentUserId === targetUserId) {
      throw new ForbiddenException(`You cannot ${action} your own account`);
    }
  }

  async findAll(query?: {
    page?: number;
    limit?: number;
    search?: string;
    reports_to?: string;
  }) {
    const result = await this.userRepo.findPaginatedForAdmin({
      page: query?.page,
      limit: query?.limit,
      search: query?.search,
      reportsTo: query?.reports_to,
    });
    return {
      items: result.items.map((u) => this.toListResponse(u)),
      meta: result.meta,
    };
  }

  async findAllForLookup() {
    const items = await this.userRepo.find({
      where: { is_active: true },
      select: {
        id: true,
        name: true,
        email: true,
        role_id: true,
      },
      relations: {
        role: true,
      },
      order: { name: 'ASC' },
      take: LOOKUP_MAX,
    });
    return items.map((u) => ({
      id: u.id,
      name: u.name,
      email: u.email,
      role_id: u.role_id,
      role: u.role ? { id: u.role.id, name: u.role.name } : null,
    }));
  }

  async findById(id: string) {
    const user = await this.userRepo.findByIdWithRole(id);
    if (!user) throw new NotFoundException(MSG.NOT_FOUND);
    return this.toDetailResponse(user);
  }

  async create(dto: CreateUserDto) {
    const existing = await this.userRepo.findByEmail(dto.email);
    if (existing) throw new ConflictException(MSG.ALREADY_EXISTS('email'));

    const role = await this.roleRepo.findOneBy({ id: dto.role_id });
    if (!role) throw new BadRequestException('Invalid role ID');

    if (dto.parent_user_id) {
      await this.validateParentUserAssignment(undefined, dto.parent_user_id, dto.role_id);
    }

    const passwordHash = await bcrypt.hash(dto.password, 10);

    const user = await this.userRepo.save({
      name: dto.name,
      email: dto.email,
      phone: dto.phone || null,
      password_hash: passwordHash,
      role_id: dto.role_id,
      is_active: dto.is_active !== undefined ? dto.is_active : true,
      parent_user_id: dto.parent_user_id ?? null,
    });

    return { id: user.id, message: MSG.CREATED };
  }

  async update(id: string, dto: UpdateUserDto) {
    // Prevent admin from changing their own role
    if (dto.role_id) {
      this.preventSelfAction(id, 'change your own role');
    }

    const user = await this.userRepo.findByIdWithRole(id);
    if (!user) throw new NotFoundException(MSG.NOT_FOUND);

    if (dto.email && dto.email !== user.email) {
      const existing = await this.userRepo.findByEmail(dto.email);
      if (existing) throw new ConflictException(MSG.ALREADY_EXISTS('email'));
    }

    if (dto.role_id) {
      const role = await this.roleRepo.findOneBy({ id: dto.role_id });
      if (!role) throw new BadRequestException('Invalid role ID');
    }

    const finalRoleId = dto.role_id ?? user.role_id;
    const finalParentId =
      dto.parent_user_id !== undefined ? dto.parent_user_id : user.parent_user_id;
    if (finalParentId) {
      await this.validateParentUserAssignment(id, finalParentId, finalRoleId);
    }

    const updateData: Partial<UserEntity> = {};
    if (dto.name !== undefined) updateData.name = dto.name;
    if (dto.email !== undefined) updateData.email = dto.email;
    if (dto.phone !== undefined) updateData.phone = dto.phone;
    if (dto.role_id !== undefined) updateData.role_id = dto.role_id;
    if (dto.is_active !== undefined) updateData.is_active = dto.is_active;
    if (dto.parent_user_id !== undefined) updateData.parent_user_id = dto.parent_user_id;

    await this.userRepo.update(id, updateData);
    return { id, message: MSG.UPDATED };
  }

  /**
   * Update the current user's own workspace preferences. Separate
   * from `update()` which is admin-facing and requires SETTINGS:EDIT;
   * this runs as a Lookup endpoint so every user can self-manage.
   * Currently only `active_destination_id` is editable.
   */
  async updatePreferences(id: string, prefs: { active_destination_id: string | null }) {
    const user = await this.userRepo.findById(id);
    if (!user) throw new NotFoundException(MSG.NOT_FOUND);
    await this.userRepo.update(id, { active_destination_id: prefs.active_destination_id });
    return { active_destination_id: prefs.active_destination_id };
  }

  async resetPassword(id: string, dto: ResetPasswordDto) {
    const user = await this.userRepo.findById(id);
    if (!user) throw new NotFoundException(MSG.NOT_FOUND);

    const passwordHash = await bcrypt.hash(dto.new_password, 10);
    await this.userRepo.update(id, { password_hash: passwordHash });
    return { message: 'Password reset successfully' };
  }

  async toggleStatus(id: string) {
    this.preventSelfAction(id, 'deactivate yourself');

    const user = await this.userRepo.findById(id);
    if (!user) throw new NotFoundException(MSG.NOT_FOUND);

    await this.userRepo.update(id, { is_active: !user.is_active });
    return {
      id,
      is_active: !user.is_active,
      message: `User ${!user.is_active ? 'activated' : 'deactivated'} successfully`,
    };
  }

  async remove(id: string) {
    this.preventSelfAction(id, 'delete yourself');

    const user = await this.userRepo.findById(id);
    if (!user) throw new NotFoundException(MSG.NOT_FOUND);

    await this.userRepo.softDelete(id);
    return { message: MSG.DELETED };
  }

  /**
   * Validate optional parent_user_id: active tenant user, not self,
   * no cycles (cannot report to own descendant or ancestor loop).
   */
  private async validateParentUserAssignment(
    userId: string | undefined,
    parentUserId: string | null,
    userRoleId: string,
  ): Promise<void> {
    if (parentUserId === null || parentUserId === undefined || parentUserId === '') return;
    if (userId && parentUserId === userId) {
      throw new BadRequestException('A user cannot report to themselves');
    }

    const ancestorRoleIds = await this.roleHierarchy.getAncestorRoleIds(userRoleId);
    if (ancestorRoleIds.length === 0) {
      throw new BadRequestException(
        'This role has no parent role in the hierarchy — Reports to must be empty',
      );
    }

    const parent = await this.userRepo.findById(parentUserId);
    if (!parent || !parent.is_active) {
      throw new BadRequestException('Reports to must be an active user');
    }
    if (!ancestorRoleIds.includes(parent.role_id)) {
      throw new BadRequestException(
        'Reports to must be a user in a role above the selected role in the hierarchy',
      );
    }

    if (userId) {
      const descendants = await this.userRepo.findDescendantUserIds(userId);
      if (descendants.includes(parentUserId)) {
        throw new BadRequestException('Reports to cannot be someone in your reporting chain');
      }
    }

    let cursor: string | null = parentUserId;
    const seen = new Set<string>();
    while (cursor) {
      if (userId && cursor === userId) {
        throw new BadRequestException('Reports to would create a reporting cycle');
      }
      if (seen.has(cursor)) {
        throw new BadRequestException('Reports to would create a reporting cycle');
      }
      seen.add(cursor);
      const row = await this.userRepo.findById(cursor);
      cursor = row?.parent_user_id ?? null;
    }
  }

  private toListResponse(user: UserEntity) {
    return {
      id: user.id,
      name: user.name,
      email: user.email,
      phone: user.phone,
      role: user.role ? { id: user.role.id, name: user.role.name } : null,
      reports_to: user.parent_user
        ? { id: user.parent_user.id, name: user.parent_user.name }
        : null,
      is_active: user.is_active,
      last_login_at: user.last_login_at,
      created_at: user.created_at,
      updated_at: user.updated_at,
    };
  }

  private toDetailResponse(user: UserEntity) {
    return {
      id: user.id,
      name: user.name,
      email: user.email,
      phone: user.phone,
      reports_to: user.parent_user
        ? { id: user.parent_user.id, name: user.parent_user.name }
        : null,
      is_active: user.is_active,
      last_login_at: user.last_login_at,
      created_at: user.created_at,
      updated_at: user.updated_at,
      created_by: user.created_by,
      updated_by: user.updated_by,
      role: user.role
        ? {
            id: user.role.id,
            name: user.role.name,
            description: user.role.description,
            is_system: user.role.is_system,
            permissions:
              user.role.permissions?.map((p) => ({
                module: p.module,
                access_level: p.access_level,
              })) || [],
          }
        : null,
    };
  }
}
