import {
  BadRequestException,
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { randomUUID } from 'crypto';
import * as bcrypt from 'bcrypt';
import { UserRole, UserStatus } from '../common/constants/enums';
import { User, UserDocument } from './schemas/user.schema';
import { docId } from '../common/utils/mongo.util';
import {
  ManagerSummary,
  NormalizedCreateUserInput,
  resolveStoredRole,
  resolveUserDisplayName,
  toClientUser,
} from './user-api.mapper';

const REPORTING_MANAGER_ROLES: UserRole[] = [
  UserRole.MANAGER,
  UserRole.REPORTING_MANAGER,
  UserRole.HOD,
  UserRole.ADMIN,
  UserRole.SUPER_ADMIN,
];

@Injectable()
export class UsersService {
  constructor(@InjectModel(User.name) private readonly userModel: Model<UserDocument>) {}

  toPublic(user: UserDocument) {
    const obj = user.toObject();
    const { passwordHash: _, ...rest } = obj;
    return { ...rest, id: docId(user) };
  }

  async toClientUserWithManager(user: UserDocument) {
    let manager: ManagerSummary = null;
    if (user.reportingManagerId) {
      const doc = await this.findById(user.reportingManagerId);
      if (doc) {
        manager = {
          id: docId(doc),
          uid: doc.uid,
          fullName: resolveUserDisplayName(doc) ?? doc.fullName,
          name: doc.name,
          employeeCode: doc.employeeCode,
          role: doc.role,
        };
      }
    }
    return toClientUser({ ...user.toObject(), id: docId(user) }, manager);
  }

  private isActiveUser(status: unknown): boolean {
    return (
      status === UserStatus.ACTIVE ||
      String(status).toLowerCase() === 'active'
    );
  }

  private isReportingManagerRole(role: unknown): boolean {
    return REPORTING_MANAGER_ROLES.includes(resolveStoredRole(String(role)));
  }

  private async resolveReportingManagerRef(ref: string): Promise<string> {
    const trimmed = ref.trim();
    let manager: UserDocument | null = null;

    if (Types.ObjectId.isValid(trimmed)) {
      manager = await this.findById(trimmed);
    }
    if (!manager) {
      manager = await this.findByUid(trimmed);
    }
    if (!manager) {
      throw new BadRequestException('Reporting manager not found');
    }
    if (!this.isActiveUser(manager.status)) {
      throw new BadRequestException('Reporting manager is not active');
    }
    if (!this.isReportingManagerRole(manager.role)) {
      throw new BadRequestException(
        'Selected user cannot be assigned as reporting manager',
      );
    }
    return docId(manager);
  }

  async findById(id: string): Promise<UserDocument | null> {
    if (!Types.ObjectId.isValid(id)) return null;
    return this.userModel.findById(id).exec();
  }

  async findByUid(uid: string): Promise<UserDocument | null> {
    return this.userModel.findOne({ uid }).exec();
  }

  async syncUserRole(uid: string, role: UserRole): Promise<void> {
    await this.userModel.updateOne({ uid }, { role });
  }

  async ensureCanonicalRole(user: UserDocument): Promise<UserDocument> {
    const role = resolveStoredRole(user.role);
    if (role !== user.role) {
      await this.syncUserRole(user.uid, role);
      user.role = role;
    }
    return user;
  }

  async findByEmail(email: string): Promise<UserDocument | null> {
    return this.userModel.findOne({ email: email.toLowerCase().trim() }).exec();
  }

  async verifyPassword(user: UserDocument, password: string): Promise<boolean> {
    const withHash = await this.userModel
      .findById(user._id)
      .select('+passwordHash')
      .exec();
    if (!withHash) return false;
    return bcrypt.compare(password, withHash.passwordHash);
  }

  async createUser(data: NormalizedCreateUserInput): Promise<UserDocument> {
    if (!data.fullName) {
      throw new BadRequestException('name is required');
    }
    if (!data.mobileNumber) {
      throw new BadRequestException('mobile is required');
    }
    if (!/^\+?[0-9]{10,15}$/.test(data.mobileNumber)) {
      throw new BadRequestException(
        'mobile must be 10-15 digits, optional leading +',
      );
    }

    const email = data.email.toLowerCase().trim();
    const role = data.role ?? UserRole.EMPLOYEE;
    const mobileNumber = data.mobileNumber.trim();

    let reportingManagerId: string | undefined;
    if (role !== UserRole.SUPER_ADMIN) {
      const eligibleManagers = await this.userModel
        .find({})
        .select('role status')
        .lean()
        .exec();
      const managerCount = eligibleManagers.filter(
        (u) =>
          this.isActiveUser(u.status) && this.isReportingManagerRole(u.role),
      ).length;

      if (!data.reportingManagerRef?.trim()) {
        const canBootstrap =
          managerCount === 0 &&
          (role === UserRole.ADMIN || role === UserRole.MANAGER);
        if (!canBootstrap) {
          throw new BadRequestException('reportingManagerId is required');
        }
      } else {
        reportingManagerId = await this.resolveReportingManagerRef(
          data.reportingManagerRef,
        );
      }
    }

    const passwordHash = await bcrypt.hash(data.password, 12);
    try {
      return await this.userModel.create({
        uid: randomUUID(),
        email,
        passwordHash,
        fullName: data.fullName.trim(),
        employeeCode: data.employeeCode.trim(),
        role,
        mobileNumber,
        sittingLocation: data.sittingLocation?.trim(),
        reportingManagerId,
        deviceId: data.deviceId,
        profileImage: data.profileImage,
      });
    } catch (e: unknown) {
      if (e && typeof e === 'object' && 'code' in e && e.code === 11000) {
        const keyPattern =
          'keyPattern' in e &&
          e.keyPattern &&
          typeof e.keyPattern === 'object'
            ? (e.keyPattern as Record<string, unknown>)
            : {};
        if (keyPattern.mobileNumber) {
          throw new ConflictException('Mobile number already exists');
        }
        throw new ConflictException('Email or employee code already exists');
      }
      throw e;
    }
  }

  async updatePassword(uid: string, password: string): Promise<void> {
    const passwordHash = await bcrypt.hash(password, 12);
    await this.userModel.updateOne({ uid }, { passwordHash });
  }

  async updateSelf(
    uid: string,
    data: { fullName?: string; employeeCode?: string; fcmToken?: string },
  ) {
    const user = await this.userModel
      .findOneAndUpdate(
        { uid },
        {
          fullName: data.fullName?.trim(),
          employeeCode: data.employeeCode?.trim(),
          fcmToken: data.fcmToken,
        },
        { new: true },
      )
      .exec();
    if (!user) throw new NotFoundException('User not found');
    return this.toPublic(user);
  }

  async adminUpdate(
    uid: string,
    data: Partial<{
      fullName: string;
      employeeCode: string;
      email: string;
      mobileNumber: string;
      sittingLocation: string;
      reportingManagerId: string | null;
      reportingManagerRef: string;
      role: UserRole;
      status: UserStatus;
      deviceId: string;
      profileImage: string;
    }>,
  ) {
    const { reportingManagerRef, email, ...rest } = data;
    const patch: Record<string, unknown> = { ...rest };
    if (email) patch.email = email.toLowerCase().trim();
    if (reportingManagerRef) {
      patch.reportingManagerId = await this.resolveReportingManagerRef(
        reportingManagerRef,
      );
    }

    const user = await this.userModel
      .findOneAndUpdate({ uid }, patch, { new: true })
      .exec();
    if (!user) throw new NotFoundException('User not found');
    return this.toClientUserWithManager(user);
  }

  async list(params: {
    role?: UserRole;
    status?: UserStatus;
    search?: string;
    reportingManagerId?: string;
    cursor?: string;
    limit?: number;
  }) {
    const limit = params.limit ?? 20;
    const filter: Record<string, unknown> = {};
    if (params.role) filter.role = params.role;
    if (params.status) filter.status = params.status;
    if (params.reportingManagerId) {
      filter.reportingManagerId = params.reportingManagerId;
    }
    if (params.search?.trim()) {
      const q = params.search.trim();
      const re = new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
      filter.$or = [{ fullName: re }, { email: re }, { employeeCode: re }];
    }
    if (params.cursor && Types.ObjectId.isValid(params.cursor)) {
      filter._id = { $lt: new Types.ObjectId(params.cursor) };
    }

    const users = await this.userModel
      .find(filter)
      .sort({ createdAt: -1 })
      .limit(limit + 1)
      .lean()
      .exec();

    const hasMore = users.length > limit;
    const items = hasMore ? users.slice(0, limit) : users;

    const managerIds = [
      ...new Set(
        items.map((u) => u.reportingManagerId).filter(Boolean) as string[],
      ),
    ];
    const managers = managerIds.length
      ? await this.userModel
          .find({ _id: { $in: managerIds } })
          .select('uid fullName employeeCode role')
          .lean()
          .exec()
      : [];
    const managerMap = new Map(managers.map((m) => [m._id.toString(), m]));

    const clientItems = items.map((u) => {
      const managerDoc = u.reportingManagerId
        ? managerMap.get(u.reportingManagerId)
        : null;
      const manager: ManagerSummary = managerDoc
        ? {
            id: managerDoc._id.toString(),
            uid: managerDoc.uid,
            fullName: managerDoc.fullName,
            employeeCode: managerDoc.employeeCode,
            role: managerDoc.role,
          }
        : null;
      return toClientUser(
        { ...u, id: u._id.toString() },
        manager,
      );
    });

    return {
      items: clientItems,
      nextCursor: hasMore ? items[items.length - 1]._id.toString() : null,
    };
  }

  async deactivate(uid: string) {
    const user = await this.userModel
      .findOneAndUpdate({ uid }, { status: UserStatus.INACTIVE }, { new: true })
      .exec();
    if (!user) throw new NotFoundException('User not found');
    return this.toPublic(user);
  }

  async activate(uid: string) {
    const user = await this.userModel
      .findOneAndUpdate({ uid }, { status: UserStatus.ACTIVE }, { new: true })
      .exec();
    if (!user) throw new NotFoundException('User not found');
    return this.toPublic(user);
  }

  async bindDevice(uid: string, deviceId: string, isRooted = false) {
    return this.userModel
      .findOneAndUpdate({ uid }, { deviceId, isRooted }, { new: true })
      .exec();
  }

  async assertDeviceMatch(user: UserDocument, deviceId?: string): Promise<void> {
    if (!user.deviceId) return;
    if (deviceId && user.deviceId !== deviceId) {
      throw new ConflictException('Device not authorized for this account');
    }
  }

  async getDirectReportIds(managerId: string): Promise<string[]> {
    const reports = await this.userModel
      .find({ reportingManagerId: managerId })
      .select('_id')
      .lean()
      .exec();
    return reports.map((r) => r._id.toString());
  }

  async requireByUid(uid: string): Promise<UserDocument> {
    const user = await this.findByUid(uid);
    if (!user) throw new NotFoundException('User not found');
    return user;
  }

  async listReportingManagers() {
    const users = await this.userModel
      .find({})
      .select('uid fullName name employeeCode role mobileNumber status')
      .sort({ fullName: 1, name: 1 })
      .lean()
      .exec();

    return users
      .filter(
        (m) => this.isActiveUser(m.status) && this.isReportingManagerRole(m.role),
      )
      .map((m) => toClientUser({ ...m, id: m._id.toString() }, null));
  }

  async count(): Promise<number> {
    return this.userModel.countDocuments().exec();
  }
}
