/**
 * IamBootstrapSeeder — creates the minimum IAM records for local-provider
 * password-grant login to work out of the box.
 *
 * Self-healing + idempotent: every step is safe to re-run on every boot.
 * Missing records are created; existing ones are skipped. Permission-role
 * attachment is unconditional and inserts only the missing rows so partial
 * first-boot states are healed on the next boot.
 *
 * First-boot strategy: SELF-HEALING across boots, not single-boot guaranteed.
 * The IamSeeder/OAuthSeeder seed the `permission` table via fire-and-forget
 * (void seeder.seedAll(...)) so on a true cold first boot the permission rows
 * may not be committed yet when this seeder runs. On the second boot all rows
 * exist and the admin ends fully permissioned. This avoids re-introducing
 * cross-BC coupling or heavy ordering hacks. A one-time log is emitted when
 * the first boot finds zero permissions so the operator knows to restart once.
 *
 * Gate: only runs when BOOTSTRAP_SEED=true OR NODE_ENV !== 'production'.
 *
 * Registration: registered directly as a provider in IamModule. Because it is
 * a provider OF IamModule, it injects IamModule's own handlers/services via
 * standard intra-module DI — no exports needed.
 *
 * Cross-BC: this seeder has ZERO @app/<other-bc> imports. Permissions are
 * queried from the iam `permission` table (both iam and oauth permissions are
 * seeded into it by their respective IamSeeder / OAuthSeeder). Gate is
 * imported from @aurora/ (BC-neutral).
 */

import { IamAccountTypeEnum } from '@api/graphql';
import {
  IamCountAccountService,
  IamCreateAccountService,
  IamFindAccountService,
  IamUpdateAccountByIdService,
} from '@app/iam/account';
import { IamGetPermissionsService } from '@app/iam/permission';
import {
  IamCreatePermissionsRolesService,
  IamGetPermissionsRolesService,
} from '@app/iam/permission-role';
import { IamCreateRoleService, IamFindRoleService, IamGetRolesService } from '@app/iam/role';
import { IamCreateRoleAccountService, IamFindRoleAccountByIdService } from '@app/iam/role-account';
import { iamCreatePermissionsFromRoles } from '@app/iam/shared';
import { IamCreateUserHandler } from '@app/iam/user';
import { bootstrapSeederIsEnabled } from '@aurora/services/bootstrap-gate';
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';

// ---------------------------------------------------------------------------
// Shared bootstrap constants — must match OAuthBootstrapSeeder exactly.
// Override with env vars before first production deployment.
// ---------------------------------------------------------------------------
export const BOOTSTRAP_ADMIN_ACCOUNT_ID =
  process.env.BOOTSTRAP_ADMIN_ACCOUNT_ID ?? 'a0000000-0000-4000-8000-000000000001';
const BOOTSTRAP_ADMIN_USER_ID =
  process.env.BOOTSTRAP_ADMIN_USER_ID ?? 'a0000000-0000-4000-8000-000000000002';
const BOOTSTRAP_ADMIN_ROLE_ID =
  process.env.BOOTSTRAP_ADMIN_ROLE_ID ?? 'a0000000-0000-4000-8000-000000000003';
const BOOTSTRAP_ADMIN_EMAIL = process.env.BOOTSTRAP_ADMIN_EMAIL ?? 'admin@aurora.dev';
const BOOTSTRAP_ADMIN_PASSWORD = process.env.BOOTSTRAP_ADMIN_PASSWORD ?? 'admin1234';

/**
 * The OAuthClient UUID that the admin account is linked to.
 * Must match the client created by OAuthBootstrapSeeder.
 */
const BOOTSTRAP_OAUTH_CLIENT_ID =
  process.env.BOOTSTRAP_OAUTH_CLIENT_ID ?? 'a0000000-0000-4000-8000-000000000011';

/**
 * The OAuthApplication code used as dApplicationCodes for the admin account.
 * Must match the application created by OAuthBootstrapSeeder.
 */
const BOOTSTRAP_OAUTH_APP_CODE = process.env.BOOTSTRAP_OAUTH_APP_CODE ?? 'aurora';

@Injectable()
export class IamBootstrapSeeder implements OnApplicationBootstrap {
  private readonly logger = new Logger(IamBootstrapSeeder.name);

  constructor(
    private readonly findAccountService: IamFindAccountService,
    private readonly countAccountService: IamCountAccountService,
    private readonly createAccountService: IamCreateAccountService,
    private readonly updateAccountByIdService: IamUpdateAccountByIdService,
    private readonly createUserHandler: IamCreateUserHandler,
    private readonly findRoleService: IamFindRoleService,
    private readonly createRoleService: IamCreateRoleService,
    private readonly getPermissionsService: IamGetPermissionsService,
    private readonly getPermissionsRolesService: IamGetPermissionsRolesService,
    private readonly createPermissionsRolesService: IamCreatePermissionsRolesService,
    private readonly findRoleAccountByIdService: IamFindRoleAccountByIdService,
    private readonly createRoleAccountService: IamCreateRoleAccountService,
    private readonly getRolesService: IamGetRolesService,
  ) {}

  async onApplicationBootstrap(): Promise<void> {
    if (!bootstrapSeederIsEnabled()) {
      this.logger.log(
        'IamBootstrapSeeder disabled (NODE_ENV=production and BOOTSTRAP_SEED not set)',
      );
      return;
    }

    this.logger.log('IamBootstrapSeeder starting...');

    try {
      // ── Run-once guard (empty-system gate) ──────────────────────────────
      // Check whether the bootstrap admin account still exists by its canonical
      // ID. If it is absent, inspect the total account count to decide whether
      // this is a fresh system (safe to seed) or an initialized system where
      // the admin was deliberately removed (must NOT be resurrected).
      const bootstrapAccount = await this.findAccountService.main({
        where: { id: BOOTSTRAP_ADMIN_ACCOUNT_ID },
      });

      if (!bootstrapAccount) {
        const totalAccounts = await this.countAccountService.main();

        if (totalAccounts > 0) {
          // System already has accounts but the bootstrap admin is gone.
          // This is an intentional deletion — do not recreate it.
          this.logger.log('system already initialized — not recreating the bootstrap admin');
          return;
        }

        // No accounts at all → fresh/uninitialized system. Proceed to create.
        await this.seedRole();
        await this.seedAccountAndUser();
        await this.seedPermissionRoles();
        await this.seedRoleAccount();
        await this.denormalizeAccountPermissions();
      } else {
        // Bootstrap admin exists → skip creation, only run reconciliation steps.
        await this.seedRole();
        await this.seedPermissionRoles();
        await this.seedRoleAccount();
        await this.denormalizeAccountPermissions();
      }

      this.logger.log(
        `IamBootstrapSeeder done — admin login ready: ${BOOTSTRAP_ADMIN_EMAIL} / ${BOOTSTRAP_ADMIN_PASSWORD}`,
      );
    } catch (err) {
      // Non-fatal: a seeder failure must not prevent the application from starting.
      this.logger.error(
        'IamBootstrapSeeder failed: ' + (err instanceof Error ? err.message : String(err)),
        err instanceof Error ? err.stack : undefined,
      );
    }
  }

  private async seedRole(): Promise<void> {
    const existing = await this.findRoleService.main({ where: { id: BOOTSTRAP_ADMIN_ROLE_ID } });
    if (existing) {
      this.logger.debug('IamRole already exists, skipping');
      return;
    }

    this.logger.log('Creating IamRole (Administrator)...');
    await this.createRoleService.main(
      {
        id: BOOTSTRAP_ADMIN_ROLE_ID,
        name: 'Administrator',
        isMaster: true,
        defaultRedirection: '/dashboard',
        hasHiddenVerticalNavigation: false,
        permissionIds: [],
        accountIds: [],
      },
      { timezone: process.env.TZ, repositoryOptions: { auditing: null } },
    );
  }

  /**
   * Creates IamAccount + IamUser.
   *
   * IamAccount is persisted directly via IamCreateAccountService to avoid
   * the complex validation chain in IamCreateAccountHandler (uniqueness check,
   * clientReader lookup, SUDO escalation guard). Our own idempotency guard
   * (email lookup) ensures we don't duplicate.
   *
   * IamUser is created via IamCreateUserHandler.main() so that
   * @ApplySchema(IamUserFieldSchema) fires and the PasswordTypeHandler bcrypt-hashes
   * the password. Calling IamCreateUserService directly would store plaintext.
   *
   * dPermissions is initially empty ({}) — it is populated properly after the
   * role-account pivot is created (see denormalizeAccountPermissions).
   */
  private async seedAccountAndUser(): Promise<void> {
    const existing = await this.findAccountService.main({
      where: { email: BOOTSTRAP_ADMIN_EMAIL },
    });
    if (existing) {
      this.logger.debug('IamAccount already exists, skipping');
      return;
    }

    this.logger.log('Creating IamAccount...');
    await this.createAccountService.main(
      {
        id: BOOTSTRAP_ADMIN_ACCOUNT_ID,
        type: IamAccountTypeEnum.USER,
        code: null,
        email: BOOTSTRAP_ADMIN_EMAIL,
        username: BOOTSTRAP_ADMIN_EMAIL,
        isActive: true,
        clientId: BOOTSTRAP_OAUTH_CLIENT_ID,
        // dApplicationCodes reflects the OAuthApplication.code values linked to
        // this client. We set it manually to the single application code so the
        // JWT carries the right audience — matches what IamCreateAccountHandler
        // would compute via clientReader.findByIdWithApplications.
        dApplicationCodes: [BOOTSTRAP_OAUTH_APP_CODE],
        dPermissions: {},
        dTenants: [],
        tags: null,
        scopes: null,
        meta: null,
        roleIds: [],
        tenantIds: [],
      },
      { timezone: process.env.TZ, repositoryOptions: { auditing: null } },
    );

    this.logger.log('Creating IamUser — password hashed via @ApplySchema(IamUserFieldSchema)...');
    await this.createUserHandler.main(
      {
        id: BOOTSTRAP_ADMIN_USER_ID,
        accountId: BOOTSTRAP_ADMIN_ACCOUNT_ID,
        name: 'Admin',
        surname: null,
        avatar: null,
        mobile: null,
        langId: null,
        password: BOOTSTRAP_ADMIN_PASSWORD,
        isTwoFactorAuthenticationEnabled: false,
        twoFactorAuthenticationSecret: null,
        rememberToken: null,
        meta: null,
      },
      { timezone: process.env.TZ, repositoryOptions: { auditing: null } },
    );
  }

  /**
   * Attaches ALL seeded permissions (iam + oauth) to the admin role.
   *
   * Runs UNCONDITIONALLY every boot (no roleIsNew gate) so partial first-boot
   * states self-heal. Strategy:
   *   1. Query the iam `permission` table for all rows (both iam and oauth
   *      permissions are seeded into it by their respective IamSeeder /
   *      OAuthSeeder). No cross-BC static import needed.
   *   2. Query the existing permission_role rows for this role.
   *   3. Insert only the MISSING rows — fully idempotent.
   *
   * If the permission table is empty (true cold first boot before seeders
   * have committed), this step is a no-op and a warning is logged. The next
   * boot will find the rows and attach them.
   */
  private async seedPermissionRoles(): Promise<void> {
    const allPermissions = await this.getPermissionsService.main();
    if (allPermissions.length === 0) {
      this.logger.warn(
        'No permissions in the iam permission table yet (first boot race?). ' +
          'Skipping permission-role attachment — will self-heal on next boot.',
      );
      return;
    }

    // Fetch existing permission-role rows for this role so we insert only missing ones.
    const existingPrs = await this.getPermissionsRolesService.main({
      where: { roleId: BOOTSTRAP_ADMIN_ROLE_ID },
    });
    const existingPermissionIds = new Set(existingPrs.map((pr) => pr.permissionId));

    const missingPrs = allPermissions
      .filter((p) => !existingPermissionIds.has(p.id))
      .map((p) => ({ permissionId: p.id, roleId: BOOTSTRAP_ADMIN_ROLE_ID }));

    if (missingPrs.length === 0) {
      this.logger.debug(
        `All ${allPermissions.length} permissions already attached to the Administrator role`,
      );
      return;
    }

    this.logger.log(
      `Attaching ${missingPrs.length} missing permissions to the Administrator role ` +
        `(${existingPrs.length} already present, ${allPermissions.length} total)...`,
    );

    try {
      await this.createPermissionsRolesService.main(missingPrs);
    } catch {
      this.logger.warn(
        'Bulk permission-role insert failed (duplicates?), inserting individually...',
      );
      for (const pr of missingPrs) {
        try {
          await this.createPermissionsRolesService.main([pr]);
        } catch {
          // Already exists — ignore.
        }
      }
    }
  }

  private async seedRoleAccount(): Promise<void> {
    const existing = await this.findRoleAccountByIdService.main(
      BOOTSTRAP_ADMIN_ROLE_ID,
      BOOTSTRAP_ADMIN_ACCOUNT_ID,
      {},
    );
    if (existing) {
      this.logger.debug('IamRoleAccount already exists, skipping');
      return;
    }

    this.logger.log('Creating IamRoleAccount...');
    await this.createRoleAccountService.main(
      {
        roleId: BOOTSTRAP_ADMIN_ROLE_ID,
        accountId: BOOTSTRAP_ADMIN_ACCOUNT_ID,
      },
      { timezone: process.env.TZ, repositoryOptions: { auditing: null } },
    );
  }

  /**
   * Populates account.dPermissions by loading the admin role (with permissions)
   * and running iamCreatePermissionsFromRoles. This matches what the production
   * IamCreateAccountHandler computes before persisting.
   *
   * Runs EVERY BOOT and writes if the computed dPermissions differs from what
   * is stored (i.e. the count changed). This ensures the account is healed even
   * when new permissions are added after the initial seed.
   *
   * Without this step, the account.dPermissions stays {} and any @Auth-protected
   * endpoint returns 403 — the AuthorizationPermissionsGuard reads dPermissions.all.
   */
  private async denormalizeAccountPermissions(): Promise<void> {
    const account = await this.findAccountService.main({
      where: { id: BOOTSTRAP_ADMIN_ACCOUNT_ID },
    });
    if (!account) {
      this.logger.debug('Account not found — skipping dPermissions denormalization');
      return;
    }

    const roles = await this.getRolesService.main(
      { where: { id: [BOOTSTRAP_ADMIN_ROLE_ID] } },
      { include: [{ association: 'permissions' }] },
    );

    if (roles.length === 0) {
      this.logger.warn('Admin role not found for dPermissions denormalization — skipping');
      return;
    }

    const dPermissions = iamCreatePermissionsFromRoles(roles);

    const existingAll = (account.dPermissions as { all?: string[] } | null)?.all ?? [];
    if (existingAll.length === dPermissions.all.length) {
      this.logger.debug(
        `Account dPermissions already up to date (${existingAll.length} permissions) — skipping update`,
      );
      return;
    }

    this.logger.log(
      `Updating account dPermissions: ${existingAll.length} → ${dPermissions.all.length} permissions...`,
    );
    await this.updateAccountByIdService.main(
      {
        id: BOOTSTRAP_ADMIN_ACCOUNT_ID,
        dPermissions,
      },
      undefined,
      { timezone: process.env.TZ, repositoryOptions: { auditing: null } },
    );
  }
}
