import bcrypt from "bcryptjs";
import { Role, RiskStatus, User, Prisma } from "@prisma/client";

import {
  userRepository,
  UserListParams,
  UserListResult,
} from "./users.repository";
import {
  createPasswordResetToken,
  generateRawToken,
} from "../../lib/passwordReset";
import { sendInvitationEmail } from "../../lib/email";
import prisma from "../../prismaClient";

export interface CreateUserData {
  firstName: string;
  lastName: string;
  email: string;
  password?: string;
  title?: string;
  imageUrl?: string;
  riskStatus?: RiskStatus;
  role?: Role;
  managerId?: string;
}

export interface UpdateUserData {
  firstName?: string;
  lastName?: string;
  email?: string;
  title?: string;
  riskStatus?: RiskStatus;
  imageUrl?: string;
  isLocked?: boolean;
  role?: Role;
  managerId?: string;
}

export interface LoginLogItem {
  id: string;
  name: string;
  email: string;
  lastLoginAt: Date;
  activity: string;
  status?: string;
  failureReason?: string | null;
}

export class UserService {
  /**
   * Build where clause for user queries
   */
  private buildWhereClause(params: UserListParams): any {
    const where: any = {};

    where.role =
      params.roleType === "admin"
        ? { in: [Role.ADMIN, Role.MANAGER] }
        : Role.USER;

    if (params.search?.trim()) {
      const tokens = params.search
        .split(/\s+/)
        .map((t) => t.trim())
        .filter(Boolean);

      const searchableFields = ["firstName", "lastName", "email", "title"];

      const andClauses = tokens.map((tok) => ({
        OR: searchableFields.map((field) => ({
          [field]: { contains: tok, mode: "insensitive" },
        })),
      }));

      // If only one token, AND with one item behaves like OR
      where.AND = [...(where.AND || []), ...andClauses];
    }

    if (params.risks?.length) {
      where.riskStatus = { in: params.risks };
    }

    return where;
  }

  /**
   * Get paginated list of users
   */
  async getUsers(params: UserListParams): Promise<UserListResult> {
    const where = this.buildWhereClause(params);
    const total = await userRepository.countUsers(where);

    let items;
    if (params.sortBy === "risk") {
      // Special handling for risk sort
      items = await userRepository.findUsersWithRiskSort(
        where,
        params.search,
        params.sortDir,
        params.page,
        params.limit
      );
    } else {
      // Default Prisma ordering
      let orderBy: any = undefined;
      if (params.sortBy) {
        if (params.sortBy === "name") {
          orderBy = { firstName: params.sortDir };
        } else if (params.sortBy === "email") {
          orderBy = { email: params.sortDir };
        } else if (params.sortBy === "title") {
          orderBy = { title: params.sortDir };
        } else if (params.sortBy === "role") {
          orderBy = { role: params.sortDir };
        }
      }

      items = await userRepository.findManyUsers(
        where,
        orderBy,
        (params.page - 1) * params.limit,
        params.limit
      );
    }

    return { items, total };
  }

  /**
   * Get user login activity logs
   */
  async getUserLogs(
    page: number,
    limit: number,
    search: string
  ): Promise<{ items: LoginLogItem[]; total: number }> {
    let where: any = {};
    if (search && search.trim()) {
      const tokens = search
        .split(/\s+/)
        .map((t) => t.trim())
        .filter((t) => t.length >= 2)
        .slice(0, 3);
      const tokenAndClauses = tokens.map((tok) => ({
        OR: [
          { activity: { contains: tok, mode: "insensitive" } },
          { user: { firstName: { contains: tok, mode: "insensitive" } } },
          { user: { lastName: { contains: tok, mode: "insensitive" } } },
          { user: { email: { contains: tok, mode: "insensitive" } } },
        ],
      }));
      if (tokenAndClauses.length > 0) where = { AND: tokenAndClauses };
    }

    const [total, rows] = await Promise.all([
      prisma.loginActivity.count({ where }),
      prisma.loginActivity.findMany({
        where,
        select: {
          id: true,
          userId: true,
          createdAt: true,
          activity: true,
          status: true,
          failureReason: true,
          user: { select: { firstName: true, lastName: true, email: true } },
        },
        orderBy: { createdAt: "desc" },
        skip: (page - 1) * limit,
        take: limit,
      }),
    ]);

    const items: LoginLogItem[] = (rows as any[]).map((row: any) => ({
      id: row.id,
      name: `${row.user.firstName} ${row.user.lastName}`,
      email: row.user.email,
      lastLoginAt: row.createdAt,
      activity: row.activity,
      status: row.status,
      failureReason: row.failureReason ?? null,
    }));

    return { items, total };
  }

  /**
   * Get login activities for a specific user
   */
  async getUserLoginActivities(
    userId: string,
    page: number,
    limit: number
  ): Promise<{ items: LoginLogItem[]; total: number }> {
    const where: any = { userId };
    const [total, rows] = await Promise.all([
      prisma.loginActivity.count({ where }),
      prisma.loginActivity.findMany({
        where,
        select: {
          id: true,
          userId: true,
          createdAt: true,
          activity: true,
          status: true,
          failureReason: true,
          user: { select: { firstName: true, lastName: true, email: true } },
        },
        orderBy: { createdAt: "desc" },
        skip: (page - 1) * limit,
        take: limit,
      }),
    ]);

    const items: LoginLogItem[] = (rows as any[]).map((row: any) => ({
      id: row.id,
      name: `${row.user.firstName} ${row.user.lastName}`,
      email: row.user.email,
      lastLoginAt: row.createdAt,
      activity: row.activity,
      status: row.status,
      failureReason: row.failureReason ?? null,
    }));

    return { items, total };
  }

  /**
   * Resend invitation email to a user
   */
  async resendInvitation(userId: string): Promise<void> {
    const user = await userRepository.findUserById(userId);
    if (!user) {
      throw new Error("User not found");
    }
    if (user.role === Role.USER) {
      throw new Error("Invitations are not applicable for end-user accounts");
    }
    if (user.isActivated) {
      throw new Error("User is already activated");
    }
    const { link } = await createPasswordResetToken(user.id);
    await sendInvitationEmail(user.email, link);
  }

  /**
   * Get a single user by ID
   */
  async getUserById(userId: string) {
    const user = await userRepository.findUserById(userId);
    if (!user) {
      throw new Error("User not found");
    }
    return user;
  }

  /**
   * Create a new user with role-based validation
   */
  async createUser(
    data: CreateUserData,
    requesterRole?: Role,
    requesterId?: string
  ): Promise<User> {
    const {
      firstName,
      lastName,
      email,
      password,
      title,
      imageUrl,
      riskStatus = RiskStatus.LOW_RISK,
      role = Role.USER,
      managerId,
    } = data;

    // Validation
    if (!firstName || !lastName || !email) {
      throw new Error("Missing required fields");
    }

    const existing = await userRepository.findUserByEmail(email);
    if (existing) {
      throw new Error("Email already exists");
    }

    // Role-based permissions
    if (requesterRole === Role.ADMIN && role !== Role.USER) {
      throw new Error("Admins may only create end-user accounts");
    }

    // Password and invitation logic
    let passwordToSet = password;
    let shouldInvite = false;

    if (role === Role.USER) {
      if (!passwordToSet) {
        passwordToSet = generateRawToken(16);
      }
    } else {
      if (!passwordToSet) {
        passwordToSet = generateRawToken(16);
        shouldInvite = true;
      }
    }

    // Manager assignment for ADMINs
    let managerIdToSet: string | undefined = undefined;
    if (role === Role.ADMIN) {
      if (requesterRole !== Role.MANAGER) {
        throw new Error("Only Managers can create Admin accounts");
      }

      let targetManagerId = managerId || requesterId;
      if (!targetManagerId) {
        throw new Error("Could not determine manager for the new Admin");
      }

      const targetManager = await userRepository.findUserForValidation(
        targetManagerId
      );
      if (!targetManager || targetManager.role !== Role.MANAGER) {
        throw new Error("managerId must reference an existing Manager");
      }
      managerIdToSet = targetManagerId;
    }

    const hash = await bcrypt.hash(passwordToSet, 10);
    const userData: Prisma.UserCreateInput = {
      firstName,
      lastName,
      email,
      password: hash,
      title: title || "",
      imageUrl,
      riskStatus,
      role,
      isActivated: role === Role.USER ? true : shouldInvite ? false : true,
    };

    // Only set managerId if it's defined
    if (managerIdToSet) {
      userData.manager = { connect: { id: managerIdToSet } };
    }

    const user = await userRepository.createUser(userData);

    // Send invitation email if needed
    if (shouldInvite) {
      try {
        const { link } = await createPasswordResetToken(user.id);
        await sendInvitationEmail(email, link);
      } catch (e) {
        console.warn("Failed to send invitation email:", e);
      }
    }

    return user;
  }

  /**
   * Update a user with role-based validation
   */
  async updateUser(
    userId: string,
    data: UpdateUserData,
    requesterRole?: Role,
    requesterId?: string
  ): Promise<User> {
    const target = await userRepository.findUserById(userId);
    if (!target) {
      throw new Error("User not found");
    }

    // Role-based permission checks
    if (requesterRole === Role.ADMIN) {
      if (data.role && data.role !== Role.USER) {
        throw new Error("Admins may not change role to Admin/Manager");
      }
      if (typeof data.managerId !== "undefined") {
        throw new Error("Admins may not assign or reassign Admins");
      }
    }

    // Manager assignment validation
    let updateData: Prisma.UserUpdateInput = {
      firstName: data.firstName,
      lastName: data.lastName,
      email: data.email,
      title: data.title,
      riskStatus: data.riskStatus,
      imageUrl: data.imageUrl,
      isLocked: data.isLocked,
      role: data.role,
    };

    if (typeof data.managerId !== "undefined") {
      if (requesterRole !== Role.MANAGER) {
        throw new Error("Only Managers can assign/reassign Admins");
      }

      const effectiveRole = data.role ?? target.role;
      if (effectiveRole !== Role.ADMIN) {
        throw new Error("managerId may only be set for Admin accounts");
      }

      const mgrId = data.managerId || requesterId;
      if (!mgrId) {
        throw new Error("Could not determine manager for the Admin");
      }

      const mgr = await userRepository.findUserForValidation(mgrId);
      if (!mgr || mgr.role !== Role.MANAGER) {
        throw new Error("managerId must reference an existing Manager");
      }

      updateData.manager = { connect: { id: mgrId } };
    }

    return await userRepository.updateUser(userId, updateData);
  }

  /**
   * Toggle user lock status
   */
  async toggleUserLock(userId: string): Promise<User> {
    const user = await userRepository.findUserById(userId);
    if (!user) {
      throw new Error("User not found");
    }

    return await userRepository.updateUser(userId, {
      isLocked: !user.isLocked,
    });
  }

  /**
   * Delete a single user with role-based validation
   */
  async deleteUser(userId: string, requesterRole?: Role): Promise<void> {
    const target = await userRepository.findUserById(userId);
    if (!target) {
      throw new Error("User not found");
    }

    if (requesterRole === Role.ADMIN && target.role !== Role.USER) {
      throw new Error("Admins may only delete regular users");
    }

    await userRepository.deleteTodosByCreator(userId);
    await userRepository.deleteUser(userId);
  }

  /**
   * Bulk delete users with role-based validation
   */
  async deleteUsers(userIds: string[], requesterRole?: Role): Promise<void> {
    if (!Array.isArray(userIds) || userIds.length === 0) {
      throw new Error("No user IDs provided");
    }

    // Validate targets for admin constraints
    if (requesterRole === Role.ADMIN) {
      const targets = await userRepository.findUsersByIds(userIds);
      const forbidden = targets
        .filter((t) => t.role !== Role.USER)
        .map((t) => t.id);
      if (forbidden.length > 0) {
        throw new Error("Admins may only delete regular users");
      }
    }

    await userRepository.deleteTodosByCreators(userIds);
    await userRepository.deleteUsers(userIds);
  }
}

export const userService = new UserService();
