import { OAuthSequelizeAccessTokenRepository } from '@app/o-auth/access-token';
import { OAuthSequelizeCredentialRepository } from '@app/o-auth/credential';
import { OAuthSequelizeRefreshTokenRepository } from '@app/o-auth/refresh-token';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Cron } from '@nestjs/schedule';
import { InjectConnection } from '@nestjs/sequelize';
import { Op, QueryTypes, Sequelize } from 'sequelize';

const DAY_MS = 24 * 60 * 60 * 1000;
const DEFAULT_ACCESS_RETENTION_DAYS = 7;
const DEFAULT_REFRESH_RETENTION_DAYS = 7;
const DEFAULT_CREDENTIAL_RETENTION_DAYS = 90;

/**
 * PostgreSQL advisory lock ID reserved exclusively for TokenRetentionService.
 * If more distributed crons are added in the future, centralize them in a
 * `@aurora/modules/scheduling/advisory-locks.ts` registry.
 */
const TOKEN_RETENTION_LOCK_ID = 982374871;

/**
 * Daily scheduled task (03:00 UTC) that purges expired tokens and old credential audit
 * rows from the database. Retention windows are configurable via env vars:
 *   - OAUTH_ACCESS_TOKEN_RETENTION_DAYS (default 7), measured from `expiresAt`
 *   - OAUTH_REFRESH_TOKEN_RETENTION_DAYS (default 7), measured from `expiresAt`
 *   - OAUTH_CREDENTIAL_RETENTION_DAYS (default 90), measured from `createdAt`
 *
 * Runs regardless of OAUTH_STRATEGY: even consumer-mode deploys benefit from purging
 * their AuroraHubConsumerAdapter's cache-derived access tokens (though those rows are
 * created upstream, not here — so the cron is mostly a no-op in pure consumer mode).
 *
 * Multi-instance safe: wraps the purge in a `sequelize.transaction()` and acquires a
 * `pg_try_advisory_xact_lock` on entry. Only one instance per tick obtains the lock;
 * the rest skip silently. PostgreSQL releases the lock automatically on COMMIT or
 * ROLLBACK — and on connection close if the process dies mid-purge — so no manual
 * unlock or TTL cleanup is needed. Bonus: the 3 DELETEs are now atomic; if one fails,
 * all roll back and the next tick (24h later) re-attempts cleanly.
 */
@Injectable()
export class TokenRetentionService {
  private readonly logger = new Logger(TokenRetentionService.name);
  private readonly accessRetentionDays: number;
  private readonly refreshRetentionDays: number;
  private readonly credentialRetentionDays: number;

  constructor(
    configService: ConfigService,
    @InjectConnection() private readonly sequelize: Sequelize,
    private readonly accessTokenRepo: OAuthSequelizeAccessTokenRepository,
    private readonly refreshTokenRepo: OAuthSequelizeRefreshTokenRepository,
    private readonly credentialRepo: OAuthSequelizeCredentialRepository,
  ) {
    this.accessRetentionDays = Number(
      configService.get<string | number>(
        'OAUTH_ACCESS_TOKEN_RETENTION_DAYS',
        DEFAULT_ACCESS_RETENTION_DAYS,
      ),
    );
    this.refreshRetentionDays = Number(
      configService.get<string | number>(
        'OAUTH_REFRESH_TOKEN_RETENTION_DAYS',
        DEFAULT_REFRESH_RETENTION_DAYS,
      ),
    );
    this.credentialRetentionDays = Number(
      configService.get<string | number>(
        'OAUTH_CREDENTIAL_RETENTION_DAYS',
        DEFAULT_CREDENTIAL_RETENTION_DAYS,
      ),
    );
  }

  @Cron('0 3 * * *', { name: 'token-retention', timeZone: 'UTC' })
  async purge(): Promise<{ access: number; refresh: number; credential: number } | null> {
    return this.sequelize.transaction(async (transaction) => {
      const [{ acquired }] = await this.sequelize.query<{ acquired: boolean }>(
        'SELECT pg_try_advisory_xact_lock(:lockId) AS acquired',
        {
          type: QueryTypes.SELECT,
          replacements: { lockId: TOKEN_RETENTION_LOCK_ID },
          transaction,
        },
      );

      if (!acquired) {
        this.logger.debug('Token retention skipped: another instance holds the advisory lock.');
        return null;
      }

      const now = Date.now();
      const accessCutoff = new Date(now - this.accessRetentionDays * DAY_MS);
      const refreshCutoff = new Date(now - this.refreshRetentionDays * DAY_MS);
      const credentialCutoff = new Date(now - this.credentialRetentionDays * DAY_MS);

      const access = await this.accessTokenRepo.repository.destroy({
        where: { expiresAt: { [Op.lt]: accessCutoff } },
        force: true,
        transaction,
      });
      const refresh = await this.refreshTokenRepo.repository.destroy({
        where: { expiresAt: { [Op.lt]: refreshCutoff } },
        force: true,
        transaction,
      });
      const credential = await this.credentialRepo.repository.destroy({
        where: { createdAt: { [Op.lt]: credentialCutoff } },
        force: true,
        transaction,
      });

      this.logger.log(
        `Token retention purge: ${access} access, ${refresh} refresh, ${credential} credential rows deleted.`,
      );

      return { access, refresh, credential };
    });
  }
}
