import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { hashPassword } from "@tria-plc/api-common/utils/argon";
import { EUserStatus } from "@tria-plc/api-common/utils/enums/user.enum";
import { DataSource, EntityManager, In, IsNull, Repository } from "typeorm";

import { Employee, Organization, UserCredential } from "@tria-plc/iamapi-common";
import { Role } from "@tria-plc/iamapi-common/entities/iam/user/role.entity";
import { UserRole } from "@tria-plc/iamapi-common/entities/iam/user/user-role.entity";
import { User } from "@tria-plc/iamapi-common/entities/iam/user/user.entity";

import { CreateOrganizationUserDto } from "./dto/create-organization-user.dto";

const RESERVED_ROLE_KEYS = new Set([
  "super_admin",
  "organization_admin",
  "unit_admin",
]);
const DEFAULT_USER_PASSWORD = "12345678";
const ORGANIZATION_ADMIN_ROLE_KEY = "organization_admin";
const EDR_ORG_MANAGER_ROLE_KEY = "edr_org_manager";

@Injectable()
export class BackofficeService {
  constructor(
    @InjectRepository(Employee)
    private readonly employeeRepository: Repository<Employee>,
    @InjectRepository(Organization)
    private readonly organizationRepository: Repository<Organization>,
    @InjectRepository(Role)
    private readonly roleRepository: Repository<Role>,
    @InjectRepository(UserRole)
    private readonly userRoleRepository: Repository<UserRole>,
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
    private readonly dataSource: DataSource,
  ) {}

  async createOrganizationUser(
    organizationId: string,
    dto: CreateOrganizationUserDto,
  ) {
    const organizationExists = await this.organizationRepository.exists({
      where: { id: organizationId },
    });

    if (!organizationExists) {
      throw new NotFoundException("organization_not_found");
    }

    const email = dto.email.trim().toLowerCase();
    const username = dto.username.trim().toLowerCase();
    const phoneNumber = dto.phoneNumber?.trim() || undefined;
    const assignOrganizationAdmin = dto.assignOrganizationAdmin === true;
    const name = {
      en: dto.name.en.trim(),
      ...(dto.name.am?.trim() ? { am: dto.name.am.trim() } : {}),
    };

    const existingUsers = await this.userRepository.find({
      where: [{ email }, { username }],
      select: { id: true, email: true, username: true },
    });

    const emailUser = existingUsers.find((user) => user.email === email);
    const usernameUser = existingUsers.find((user) => user.username === username);

    if (emailUser && usernameUser && emailUser.id !== usernameUser.id) {
      throw new BadRequestException("email_or_username_already_in_use");
    }

    const existingUser = emailUser ?? usernameUser;
    const hashedPassword = await hashPassword(DEFAULT_USER_PASSWORD);

    return this.dataSource.transaction(async (manager) => {
      let user = existingUser;

      if (!user) {
        user = await manager.getRepository(User).save(
          manager.getRepository(User).create({
            email,
            username,
            phoneNumber,
            name,
            isActive: true,
            hasSetPassword: true,
            status: EUserStatus.ACCEPTED,
          }),
        );
      } else {
        await manager.getRepository(User).update(
          { id: user.id },
          {
            email,
            username,
            phoneNumber,
            name,
            isActive: true,
            hasSetPassword: true,
            status: EUserStatus.ACCEPTED,
          },
        );
      }

      const activeCredentialExists = await manager.getRepository(UserCredential).exists({
        where: {
          userId: user.id,
          isActive: true,
        },
      });

      if (!activeCredentialExists) {
        await manager.getRepository(UserCredential).insert({
          userId: user.id,
          password: hashedPassword,
          isActive: true,
        });
      }

      let employee = await manager.getRepository(Employee).findOne({
        where: {
          userId: user.id,
          organizationId,
          isCurrent: true,
        },
        relations: {
          user: true,
          employeePositions: {
            position: true,
          },
        },
      });

      if (!employee) {
        const insertResult = await manager.getRepository(Employee).insert({
          userId: user.id,
          organizationId,
          isCurrent: true,
          name,
        });

        employee = await manager.getRepository(Employee).findOne({
          where: { id: insertResult.identifiers[0]?.id as string },
          relations: {
            user: true,
            employeePositions: {
              position: true,
            },
          },
        });
      } else {
        await manager.getRepository(Employee).update(
          { id: employee.id },
          { name },
        );

        employee = await manager.getRepository(Employee).findOne({
          where: { id: employee.id },
          relations: {
            user: true,
            employeePositions: {
              position: true,
            },
          },
        });
      }

      if (!employee) {
        throw new NotFoundException("employee_create_failed");
      }

      const userId = user.id;

      if (!userId) {
        throw new NotFoundException("user_create_failed");
      }

      if (assignOrganizationAdmin) {
        await this.ensureOrganizationAdminAccess(manager, organizationId, userId);
      }

      return employee;
    });
  }

  async getEmployeeUserRoles(organizationId: string, userId: string) {
    await this.assertUserBelongsToOrganization(organizationId, userId);

    const userRoles = await this.userRoleRepository.find({
      where: {
        userId,
        organizationId,
        unitId: IsNull(),
      },
      relations: {
        role: true,
      },
      order: {
        role: {
          key: "ASC",
        },
      },
    });

    return userRoles
      .map((userRole) => userRole.role)
      .filter((role): role is Role => Boolean(role))
      .map((role) => ({
        id: role.id,
        key: role.key,
        name: role.name,
      }));
  }

  async getOrganizationEmployees(
    organizationId: string,
    query: { skip?: string; take?: string },
  ) {
    const organizationExists = await this.organizationRepository.exists({
      where: { id: organizationId },
    });

    if (!organizationExists) {
      throw new NotFoundException("organization_not_found");
    }

    const take = Number.parseInt(query.take ?? "1000", 10);
    const skip = Number.parseInt(query.skip ?? "0", 10);

    const employees = await this.employeeRepository.find({
      where: {
        organizationId,
        isCurrent: true,
      },
      relations: {
        user: true,
        employeePositions: {
          position: true,
        },
      },
      order: {
        createdAt: "DESC",
      },
    });

    const deduplicated = this.mergeEmployeesByUser(employees);

    return {
      count: deduplicated.length,
      items: deduplicated.slice(skip, skip + take),
    };
  }

  async replaceEmployeeUserRoles(
    organizationId: string,
    userId: string,
    roleIds: string[],
  ) {
    await this.assertUserBelongsToOrganization(organizationId, userId);

    const uniqueRoleIds = [...new Set(roleIds)];
    const roles = uniqueRoleIds.length
      ? await this.roleRepository.find({
          where: {
            id: In(uniqueRoleIds),
          },
        })
      : [];

    if (roles.length !== uniqueRoleIds.length) {
      throw new NotFoundException("one_or_more_roles_not_found");
    }

    const reservedRoles = roles.filter((role) => RESERVED_ROLE_KEYS.has(role.key));
    if (reservedRoles.length) {
      throw new BadRequestException("reserved_roles_must_use_admin_actions");
    }

    await this.dataSource.transaction(async (manager) => {
      await manager.getRepository(UserRole).delete({
        userId,
        organizationId,
        unitId: IsNull(),
      });

      if (!roles.length) {
        return;
      }

      await manager.getRepository(UserRole).insert(
        roles.map((role) => ({
          userId,
          roleId: role.id,
          organizationId,
        })),
      );
    });

    return this.getEmployeeUserRoles(organizationId, userId);
  }

  private async assertUserBelongsToOrganization(
    organizationId: string,
    userId: string,
  ) {
    const exists = await this.userRepository
      .createQueryBuilder("user")
      .innerJoin(
        "user.employee",
        "employee",
        "employee.organizationId = :organizationId AND employee.isCurrent = true",
        { organizationId },
      )
      .where("user.id = :userId", { userId })
      .getExists();

    if (!exists) {
      throw new NotFoundException("user_not_found_in_organization");
    }
  }

  private mergeEmployeesByUser(employees: Employee[]) {
    const employeesByUserId = new Map<string, Employee>();

    for (const employee of employees) {
      const userId = employee.userId;
      const employeeId = employee.id;

      if (!userId) {
        if (employeeId) {
          employeesByUserId.set(employeeId, employee);
        }
        continue;
      }

      const existing = employeesByUserId.get(userId);

      if (!existing) {
        employeesByUserId.set(userId, employee);
        continue;
      }

      const existingPositions = existing.employeePositions ?? [];
      const nextPositions = employee.employeePositions ?? [];
      const mergedEmployeePositions = Array.from(
        new Map(
          [...existingPositions, ...nextPositions].map((employeePosition) => [
            employeePosition.id,
            employeePosition,
          ]),
        ).values(),
      );

      employeesByUserId.set(userId, {
        ...existing,
        ...employee,
        id: existing.id,
        user: existing.user ?? employee.user,
        userId,
        name: existing.name ?? employee.name,
        status: existing.status ?? employee.status,
        employeePositions: mergedEmployeePositions,
      });
    }

    return [...employeesByUserId.values()];
  }

  private async ensureOrganizationAdminAccess(
    manager: EntityManager,
    organizationId: string,
    userId: string,
  ) {
    const roles = await manager.getRepository(Role).find({
      where: [
        { key: ORGANIZATION_ADMIN_ROLE_KEY },
        { key: EDR_ORG_MANAGER_ROLE_KEY },
      ],
      select: { id: true, key: true },
    });

    const requiredRoles = [ORGANIZATION_ADMIN_ROLE_KEY, EDR_ORG_MANAGER_ROLE_KEY].map((key) => {
      const role = roles.find((item) => item.key === key);

      if (!role?.id) {
        throw new NotFoundException(`required_role_not_seeded:${key}`);
      }

      return {
        id: role.id,
        key: role.key,
      };
    });

    const existingRoleIds = new Set(
      (
        await manager.getRepository(UserRole).find({
          where: {
            userId,
            organizationId,
          },
          select: { roleId: true },
        })
      ).map((userRole) => userRole.roleId),
    );

    const rolesToInsert = requiredRoles
      .filter((role) => !existingRoleIds.has(role.id))
      .map((role) => ({
        userId,
        roleId: role.id,
        organizationId,
      }));

    if (!rolesToInsert.length) {
      return;
    }

    await manager.getRepository(UserRole).insert(rolesToInsert);
  }
}
