import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import type { Cache } from 'cache-manager';
import type { Request } from 'express';
import { readFileSync } from 'node:fs';
import { ExtractJwt, type StrategyOptionsWithRequest } from 'passport-jwt';
import {
  ACCOUNT_LOADER,
  type AuthenticatedAccount,
  type IAccountLoader,
  type IAuthenticationStrategy,
} from '../../../domain/ports';
import { resolveAuthenticationMode } from '../../../domain/resolve-authentication-mode';

const ISSUER = 'Aurora Catalyst';
const DEFAULT_PUBLIC_KEY_PATH = './.keys/oauth-public.key';
const CACHE_TTL_MS = 300_000; // 5 min — mirrors AuroraHubConsumerAdapter

@Injectable()
export class LocalProviderAdapter implements IAuthenticationStrategy {
  private readonly publicKey: string;

  constructor(
    configService: ConfigService,
    @Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
    @Inject(ACCOUNT_LOADER) private readonly accountLoader: IAccountLoader,
  ) {
    const mode = resolveAuthenticationMode(configService.get<string>('OAUTH_STRATEGY'));

    if (mode !== 'local-provider') {
      // In none or aurora-hub mode: skip readFileSync. Local RSA key material is only
      // required for local-provider mode. In aurora-hub mode the hub owns the signing
      // key and this adapter's validate()/getPassportOptions() are never invoked
      // (the factory returns AuroraHubConsumerAdapter instead). In none mode the guard
      // short-circuits before passport runs. An empty key is safe in both dormant modes.
      this.publicKey = '';
      return;
    }

    const publicKeyPath = configService.get<string>(
      'OAUTH_PUBLIC_KEY_PATH',
      DEFAULT_PUBLIC_KEY_PATH,
    );
    // Fail-fast at construction: if the public key file is missing or unreadable,
    // boot crashes here rather than the first request.
    this.publicKey = readFileSync(publicKeyPath, 'utf-8');
  }

  getPassportOptions(): StrategyOptionsWithRequest {
    return {
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      passReqToCallback: true,
      secretOrKey: this.publicKey,
      algorithms: ['RS256'],
      issuer: ISSUER,
    };
  }

  async validate(_req: Request, payload: Record<string, unknown>): Promise<AuthenticatedAccount> {
    const accountId = typeof payload.aci === 'string' ? payload.aci : null;
    if (!accountId) {
      throw new UnauthorizedException({
        statusCode: 401,
        message: 'OAuthAccessToken not allowed',
      });
    }

    const cacheKey = `jwt-account:${accountId}`;
    const cached = await this.cacheManager.get<AuthenticatedAccount>(cacheKey);
    if (cached) return cached;

    const account = await this.accountLoader.loadById(accountId);
    if (!account) {
      throw new UnauthorizedException({
        statusCode: 401,
        message: 'OAuthAccessToken not allowed',
      });
    }

    await this.cacheManager.set(cacheKey, account, CACHE_TTL_MS);
    return account;
  }
}
