/**
 * OAuthBootstrapSeeder — creates the minimum OAuth records for local-provider
 * password-grant login to work out of the box.
 *
 * Idempotent: each entity is queried before creation. Re-running on every
 * boot is a no-op when the data already exists.
 *
 * Gate: only runs when BOOTSTRAP_SEED=true OR NODE_ENV !== 'production'.
 *
 * Registration: registered directly as a provider in OAuthModule. Because it is
 * a provider OF OAuthModule, it injects OAuthModule's own handlers/services via
 * standard intra-module DI — no exports needed.
 *
 * Ordering note: this seeder creates OAuthClient BEFORE IamBootstrapSeeder (which
 * runs in IamModule) creates the IamAccount that references it. OnApplicationBootstrap
 * order across modules is not guaranteed; if the account seeder runs first it will
 * succeed (the FK is set from a constant, not a live lookup) and the DB constraint
 * is safe because the OAuthClient FK is declared with `constraints: false` in the
 * Sequelize association — the DB allows the insert regardless of insertion order.
 */

import { OAuthClientGrantTypeEnum } from '@api/graphql';
import {
  OAuthCreateApplicationHandler,
  OAuthFindApplicationService,
  OAuthUpdateApplicationByIdService,
} from '@app/o-auth/application';
import {
  OAuthCreateApplicationClientService,
  OAuthFindApplicationClientByIdService,
} from '@app/o-auth/application-client';
import { OAuthCreateClientHandler, OAuthFindClientService } from '@app/o-auth/client';
import { bootstrapSeederIsEnabled } from '@aurora/services/bootstrap-gate';
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';

// ---------------------------------------------------------------------------
// Shared bootstrap constants — must match IamBootstrapSeeder exactly.
// Override with env vars before first production deployment.
// ---------------------------------------------------------------------------
export const BOOTSTRAP_OAUTH_APP_ID =
  process.env.BOOTSTRAP_OAUTH_APP_ID ?? 'a0000000-0000-4000-8000-000000000010';
export const BOOTSTRAP_OAUTH_APP_CODE = process.env.BOOTSTRAP_OAUTH_APP_CODE ?? 'aurora';
// Must match the frontend's `environment.*.ts` oAuth.applicationSecret EXACTLY: the backend
// compares the application secret literally (oAuthAuthenticateApplication), so any mismatch
// yields `invalid_client`. This is the value the frontend ships across all environments.
export const BOOTSTRAP_OAUTH_APP_SECRET =
  process.env.BOOTSTRAP_OAUTH_APP_SECRET ??
  '$2y$10$EOA/SKEwKRgHPw64kO4LZ.6oy5b8kl6JzW/mCROMfSq6S38/IiywG';
export const BOOTSTRAP_OAUTH_CLIENT_ID =
  process.env.BOOTSTRAP_OAUTH_CLIENT_ID ?? 'a0000000-0000-4000-8000-000000000011';

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

  constructor(
    private readonly findApplicationService: OAuthFindApplicationService,
    private readonly createApplicationHandler: OAuthCreateApplicationHandler,
    private readonly updateApplicationByIdService: OAuthUpdateApplicationByIdService,
    private readonly findClientService: OAuthFindClientService,
    private readonly createClientHandler: OAuthCreateClientHandler,
    private readonly findApplicationClientByIdService: OAuthFindApplicationClientByIdService,
    private readonly createApplicationClientService: OAuthCreateApplicationClientService,
  ) {}

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

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

    try {
      await this.seedOAuthApplication();
      await this.seedOAuthClient();
      await this.seedOAuthApplicationClient();
      this.logger.log('OAuthBootstrapSeeder done.');
    } catch (err) {
      // Non-fatal: a seeder failure must not prevent the application from starting.
      this.logger.error(
        'OAuthBootstrapSeeder failed: ' + (err instanceof Error ? err.message : String(err)),
      );
    }
  }

  private async seedOAuthApplication(): Promise<void> {
    const existing = await this.findApplicationService.main({
      where: { code: BOOTSTRAP_OAUTH_APP_CODE },
    });
    if (existing) {
      // Reconcile the secret: heal an install seeded with an out-of-sync secret. The backend
      // compares it literally against what the frontend sends, so a drift = invalid_client.
      if (existing.secret !== BOOTSTRAP_OAUTH_APP_SECRET) {
        this.logger.log('OAuthApplication secret drift — updating to the frontend-aligned secret');
        await this.updateApplicationByIdService.main({
          id: existing.id,
          secret: BOOTSTRAP_OAUTH_APP_SECRET,
        });
      } else {
        this.logger.debug('OAuthApplication already exists with the correct secret, skipping');
      }
      return;
    }

    this.logger.log('Creating OAuthApplication...');
    await this.createApplicationHandler.main({
      id: BOOTSTRAP_OAUTH_APP_ID,
      code: BOOTSTRAP_OAUTH_APP_CODE,
      name: 'Aurora',
      secret: BOOTSTRAP_OAUTH_APP_SECRET,
      isMaster: true,
      clientIds: [],
    });
  }

  private async seedOAuthClient(): Promise<void> {
    const exists = await this.safeFind(() =>
      this.findClientService.main({ where: { id: BOOTSTRAP_OAUTH_CLIENT_ID } }),
    );
    if (exists) {
      this.logger.debug('OAuthClient already exists, skipping');
      return;
    }

    this.logger.log('Creating OAuthClient (PASSWORD grant)...');
    await this.createClientHandler.main({
      id: BOOTSTRAP_OAUTH_CLIENT_ID,
      grantType: OAuthClientGrantTypeEnum.PASSWORD,
      name: 'Aurora Password Client',
      secret: BOOTSTRAP_OAUTH_APP_SECRET,
      isMaster: true,
      isActive: true,
      scopeOptions: null,
      applicationIds: [],
      authUrl: null,
      redirect: null,
      accessTokenLifetimeSeconds: null,
      refreshTokenLifetimeSeconds: null,
    });
  }

  private async seedOAuthApplicationClient(): Promise<void> {
    const exists = await this.safeFind(() =>
      this.findApplicationClientByIdService.main(
        BOOTSTRAP_OAUTH_APP_ID,
        BOOTSTRAP_OAUTH_CLIENT_ID,
        {},
      ),
    );
    if (exists) {
      this.logger.debug('OAuthApplicationClient pivot already exists, skipping');
      return;
    }

    this.logger.log('Creating OAuthApplicationClient pivot...');
    await this.createApplicationClientService.main({
      applicationId: BOOTSTRAP_OAUTH_APP_ID,
      clientId: BOOTSTRAP_OAUTH_CLIENT_ID,
    });
  }

  // ─── Helpers ───────────────────────────────────────────────────────────────

  /**
   * Runs a find query and returns true if the entity exists.
   * Aurora repositories return null when not found — !!result handles that.
   * Genuine infrastructure errors are intentionally allowed to propagate so
   * they surface as real failures instead of being silently swallowed.
   */
  private async safeFind(fn: () => Promise<unknown>): Promise<boolean> {
    const result = await fn();
    return !!result;
  }
}
