import {
  IamAccount,
  IamAccountTypeEnum,
  IamCreateAccountInput,
  IamCreateUserInput,
} from '@api/graphql';
import {
  IamCreateAccountService,
  IamFindAccountByIdService,
  IamGetAccountsService,
} from '@app/iam/account';
import { IamAccountFieldSchema } from '@app/iam/account/domain';
import { IamGetRolesService } from '@app/iam/role';
import { iamCreatePermissionsFromRoles, IamPermissions } from '@app/iam/shared';
import { IamGetTenantsService } from '@app/iam/tenant';
import { IamCreateUserService } from '@app/iam/user';
import { ApplySchema, EmitEvent, HandlerMeta } from '@aurorajs.dev/core-back';
import { Arr, Obj, Operator, uuid } from '@aurorajs.dev/core-common';
import { IAM_CLIENT_READER, IIamClientReader } from '@bridges/iam/client/ports';
import { BadRequestException, ConflictException, Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';

@Injectable()
export class IamCreateAccountHandler {
  constructor(
    private readonly eventEmitter: EventEmitter2,
    private readonly createService: IamCreateAccountService,
    private readonly findByIdService: IamFindAccountByIdService,
    private readonly getAccountsService: IamGetAccountsService,
    private readonly getRolesService: IamGetRolesService,
    private readonly getTenantsService: IamGetTenantsService,
    @Inject(IAM_CLIENT_READER)
    private readonly clientReader: IIamClientReader,
    private readonly userCreateService: IamCreateUserService,
  ) {}

  @EmitEvent('iam.account.created')
  @ApplySchema(IamAccountFieldSchema)
  async main(
    currentAccount: IamAccount,
    payload: IamCreateAccountInput,
    handlerMeta?: HandlerMeta,
  ): Promise<IamAccount> {
    // #region pre-validate USER block
    if (payload.type === IamAccountTypeEnum.USER && !payload.user) {
      throw new BadRequestException({
        message: 'User block is required for accounts of type USER',
        translation: 'iam.error.userBlockRequired',
      });
    }
    // #endregion

    // #region uniqueness validation
    const orWhere: Record<string, string> = { username: payload.username };
    if (payload.email) orWhere.email = payload.email;
    if (payload.code) orWhere.code = payload.code;

    const existingAccounts = await this.getAccountsService.main({
      where: { [Operator.or]: orWhere },
    });

    if (existingAccounts.length > 0) {
      if (payload.email && existingAccounts.some((existing) => existing.email === payload.email)) {
        throw new ConflictException({
          message: `The email ${payload.email} already exists`,
          translation: 'iam.error.uniqueEmail',
          placeholders: { email: payload.email },
        });
      }
      if (payload.code && existingAccounts.some((existing) => existing.code === payload.code)) {
        throw new ConflictException({
          message: `The code ${payload.code} already exists`,
          translation: 'iam.error.uniqueCode',
          placeholders: { code: payload.code },
        });
      }
      if (existingAccounts.some((existing) => existing.username === payload.username)) {
        throw new ConflictException({
          message: `The username ${payload.username} already exists`,
          translation: 'iam.error.uniqueUsername',
          placeholders: { username: payload.username },
        });
      }
    }
    // #endregion

    // #region fetch related entities
    const clientPortResult = await this.clientReader.findByIdWithApplications(payload.clientId);

    const roleIds = (payload.roleIds ?? []).filter((id): id is string => Boolean(id));
    const roles =
      roleIds.length > 0
        ? await this.getRolesService.main({
            where: { id: roleIds },
            include: [{ association: 'permissions' }],
          })
        : [];
    // #endregion

    // #region compute derived fields
    const computedPermissions = iamCreatePermissionsFromRoles(roles);
    const computedApplicationCodes = clientPortResult.applicationCodes;
    // #endregion

    // #region permission escalation check
    const callerPermissions = (currentAccount?.dPermissions as { all?: string[] })?.all ?? [];
    if (
      !callerPermissions.includes(IamPermissions.SUDO) &&
      !Arr.contained(computedPermissions.all, callerPermissions)
    ) {
      throw new ConflictException({
        statusCode: 401,
        message: 'Insufficient permissions to create account with these roles',
        translation: 'iam.error.insufficientPermissions',
      });
    }
    // #endregion

    // #region expand child tenants
    const incomingTenantIds = (payload.tenantIds ?? []).filter((id): id is string => Boolean(id));
    let resolvedTenantIds = incomingTenantIds;
    if (payload.hasAddChildTenants && incomingTenantIds.length > 0) {
      const tenants = await this.getTenantsService.main();
      const tenantTreeNodes = tenants
        .filter((tenant): tenant is typeof tenant & { id: string } => Boolean(tenant?.id))
        .map((tenant) => ({ ...tenant, parentId: tenant.parentId ?? '' }));
      const expanded = new Set<string>();
      for (const tenantId of incomingTenantIds) {
        expanded.add(tenantId);
        for (const descendant of Obj.getNestedObjectsFromParentId(tenantTreeNodes, tenantId)) {
          expanded.add(descendant.id);
        }
      }
      resolvedTenantIds = [...expanded];
    }
    // #endregion

    // #region create account
    const operationId = uuid();
    const enrichedPayload: IamCreateAccountInput = {
      ...payload,
      tenantIds: resolvedTenantIds,
      dApplicationCodes: computedApplicationCodes,
      dPermissions: computedPermissions,
      dTenants: resolvedTenantIds,
    };

    const accountHandlerMeta: HandlerMeta = {
      ...handlerMeta,
      repositoryOptions: {
        ...handlerMeta?.repositoryOptions,
        auditing: {
          ...handlerMeta?.repositoryOptions?.auditing,
          operationId,
          operationSort: 1,
        },
      },
    };

    await this.createService.main(enrichedPayload, accountHandlerMeta);
    // #endregion

    // #region create user (if USER)
    if (payload.type === IamAccountTypeEnum.USER && payload.user) {
      const userPayload: IamCreateUserInput = {
        id: payload.user.id ?? uuid(),
        accountId: payload.user.accountId ?? payload.id,
        name: payload.user.name,
        surname: payload.user.surname,
        avatar: payload.user.avatar,
        mobile: payload.user.mobile,
        langId: payload.user.langId,
        password: payload.user.password,
        isTwoFactorAuthenticationEnabled: false,
        twoFactorAuthenticationSecret: null,
        rememberToken: null,
        meta: null,
      };
      const userHandlerMeta: HandlerMeta = {
        ...handlerMeta,
        repositoryOptions: {
          ...handlerMeta?.repositoryOptions,
          auditing: {
            ...handlerMeta?.repositoryOptions?.auditing,
            operationId,
            operationSort: 2,
          },
        },
      };
      await this.userCreateService.main(userPayload, userHandlerMeta);
    }
    // #endregion

    return await this.findByIdService.main(payload.id, {}, handlerMeta);
  }
}
