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

@Injectable()
export class IamUpdateAccountByIdHandler {
  constructor(
    private readonly eventEmitter: EventEmitter2,
    private readonly updateByIdService: IamUpdateAccountByIdService,
    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 userUpdateByIdService: IamUpdateUserByIdService,
  ) {}

  @EmitEvent('iam.account.updated')
  @ApplySchema(IamAccountFieldSchema)
  async main(
    currentAccount: IamAccount,
    payload: IamUpdateAccountByIdInput,
    constraint?: QueryStatement,
    handlerMeta?: HandlerMeta,
  ): Promise<IamAccount> {
    // #region fetch existing account with user relation
    const existing = await this.findByIdService.main(
      payload.id,
      { include: [{ association: 'user' }] },
      handlerMeta,
    );

    if (!existing) {
      throw new NotFoundException(`IamAccount with id: ${payload.id}, not found`);
    }
    // #endregion

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

    if (Object.keys(orWhere).length > 0) {
      const collisions = await this.getAccountsService.main({
        where: { [Operator.or]: orWhere },
      });

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

    // #region recompute derived fields on delta
    let computedApplicationCodes: string[] | undefined;
    if (payload.clientId && payload.clientId !== existing.clientId) {
      const newClient = await this.clientReader.findByIdWithApplications(payload.clientId);
      computedApplicationCodes = newClient.applicationCodes;
    }

    let computedPermissions: AccountPermissions | undefined;
    if (Array.isArray(payload.roleIds)) {
      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' }],
            })
          : [];
      computedPermissions = iamCreatePermissionsFromRoles(roles);
    }
    // #endregion

    // #region permission escalation check (only when roles change)
    if (computedPermissions) {
      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 update account with these roles',
          translation: 'iam.error.insufficientPermissions',
        });
      }
    }
    // #endregion

    // #region expand child tenants
    let resolvedTenantIds: string[] | undefined;
    if (Array.isArray(payload.tenantIds)) {
      const incomingTenantIds = payload.tenantIds.filter((id): id is string => Boolean(id));
      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 update account (with diff to skip unchanged fields)
    const enrichedPayload: IamUpdateAccountByIdInput = { ...payload };
    if (computedApplicationCodes !== undefined) {
      enrichedPayload.dApplicationCodes = computedApplicationCodes;
    }
    if (computedPermissions !== undefined) {
      enrichedPayload.dPermissions = computedPermissions;
    }
    if (resolvedTenantIds !== undefined) {
      enrichedPayload.tenantIds = resolvedTenantIds;
      enrichedPayload.dTenants = resolvedTenantIds;
    }

    const dataToUpdate = Obj.diff(enrichedPayload, existing);

    await this.updateByIdService.main({ ...dataToUpdate, id: payload.id }, constraint, handlerMeta);
    // #endregion

    // #region update user (if USER and payload.user provided)
    const existingUser = (existing as IamAccount & { user?: { id?: string } }).user;
    if (existing.type === IamAccountTypeEnum.USER && payload.user && existingUser?.id) {
      const userPayload: Record<string, unknown> = {
        ...payload.user,
        id: existingUser.id,
      };
      if (
        userPayload.password === '' ||
        userPayload.password === null ||
        userPayload.password === undefined
      ) {
        delete userPayload.password;
      }
      await this.userUpdateByIdService.main(userPayload, {}, handlerMeta);
    }
    // #endregion

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