import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { createPublicKey, randomBytes } from 'node:crypto';
import { readFileSync } from 'node:fs';
import type { IHubSessionManager, IJwtSigner, SignedToken } from '../../../domain/ports';
import { resolveAuthenticationMode } from '../../../domain/resolve-authentication-mode';

const DEFAULT_SESSION_TTL_SECONDS = 28800; // 8 hours

const ISSUER = 'Aurora Catalyst';
const DEFAULT_PRIVATE_KEY_PATH = './.keys/oauth-private.key';
const DEFAULT_ACCESS_TTL_SECONDS = 3600;
const DEFAULT_REFRESH_TTL_SECONDS = 2592000;
const REFRESH_TOKEN_BYTES = 48; // 48 bytes → 64 chars in base64url (no padding)

/**
 * Single adapter implementing both IJwtSigner (access/refresh token issuance) and
 * IHubSessionManager (hub_session sign/verify + access token verification).
 *
 * Both concerns use the same RS256 key pair, so a single adapter avoids duplicating
 * the key-loading logic. The two DI tokens (JWT_SIGNER and HUB_SESSION_MANAGER) both
 * point here via `useExisting` in their respective providers.
 */
@Injectable()
export class JwtSignerAdapter implements IJwtSigner, IHubSessionManager {
  private readonly mode: 'none' | 'aurora-hub' | 'local-provider';
  private readonly jwtService: JwtService | null;
  private readonly accessTtlSeconds: number;
  private readonly refreshTtlSeconds: number;
  private readonly sessionTtlSeconds: number;

  constructor(configService: ConfigService) {
    this.mode = resolveAuthenticationMode(configService.get<string>('OAUTH_STRATEGY'));

    if (this.mode !== 'local-provider') {
      // In none or aurora-hub mode: skip readFileSync entirely. Local RSA key material
      // is only required for local-provider mode. In aurora-hub mode the hub owns the
      // signing key; in none mode token issuance is disabled. signAccess/signRefresh/
      // signHubSession will throw a clear error if invoked in either dormant mode.
      this.jwtService = null;
      this.accessTtlSeconds = DEFAULT_ACCESS_TTL_SECONDS;
      this.refreshTtlSeconds = DEFAULT_REFRESH_TTL_SECONDS;
      this.sessionTtlSeconds = DEFAULT_SESSION_TTL_SECONDS;
      return;
    }

    const privateKeyPath = configService.get<string>(
      'OAUTH_PRIVATE_KEY_PATH',
      DEFAULT_PRIVATE_KEY_PATH,
    );
    // Read the private key synchronously at construction time. Fail-fast: if the file is missing
    // or unreadable, the application crashes at bootstrap (consistent with the OAUTH_STRATEGY
    // factory's fail-fast policy) rather than at the first token request.
    const privateKey = readFileSync(privateKeyPath, 'utf-8');

    // Derive the public key from the private key so verification is self-contained — no
    // separate public key file required. RS256 verify() requires publicKey; without it,
    // JwtService.verify() throws "secret or public key must be provided".
    const publicKey = createPublicKey({ key: privateKey, format: 'pem' })
      .export({ format: 'pem', type: 'spki' })
      .toString();

    this.accessTtlSeconds = Number(
      configService.get<string | number>('JWT_ACCESS_TTL_SECONDS', DEFAULT_ACCESS_TTL_SECONDS),
    );
    this.refreshTtlSeconds = Number(
      configService.get<string | number>('JWT_REFRESH_TTL_SECONDS', DEFAULT_REFRESH_TTL_SECONDS),
    );
    this.sessionTtlSeconds = Number(
      configService.get<string | number>('OAUTH_SESSION_COOKIE_TTL', DEFAULT_SESSION_TTL_SECONDS),
    );

    this.jwtService = new JwtService({
      privateKey,
      publicKey,
      signOptions: {
        algorithm: 'RS256',
        issuer: ISSUER,
      },
    });
  }

  async signAccess(payload: { aci: string }): Promise<SignedToken> {
    if (!this.jwtService) {
      throw new Error(`token issuance disabled while OAUTH_STRATEGY=${this.mode}`);
    }

    const token = await this.jwtService.signAsync(
      { aci: payload.aci },
      { expiresIn: this.accessTtlSeconds },
    );
    const expiresAt = new Date(Date.now() + this.accessTtlSeconds * 1000);
    return { token, expiresAt };
  }

  async signRefresh(): Promise<SignedToken> {
    if (!this.jwtService) {
      throw new Error(`token issuance disabled while OAUTH_STRATEGY=${this.mode}`);
    }

    // Refresh tokens are OPAQUE per RFC 6749 — not JWTs. Validation is DB-lookup only,
    // so signing is unnecessary and rotation of the JWT signing key does not invalidate
    // active refresh tokens.
    const token = randomBytes(REFRESH_TOKEN_BYTES).toString('base64url');
    const expiresAt = new Date(Date.now() + this.refreshTtlSeconds * 1000);
    return Promise.resolve({ token, expiresAt });
  }

  signHubSession(accountId: string): string {
    if (!this.jwtService) {
      throw new Error(`hub session signing disabled while OAUTH_STRATEGY=${this.mode}`);
    }
    return this.jwtService.sign(
      { sub: accountId, type: 'hub_session' },
      { expiresIn: this.sessionTtlSeconds },
    );
  }

  verifyHubSession(token: string): string | null {
    if (!this.jwtService) return null;
    try {
      const payload = this.jwtService.verify(token, { algorithms: ['RS256'], issuer: ISSUER });
      if (!payload || payload.type !== 'hub_session' || !payload.sub) return null;
      return payload.sub as string;
    } catch {
      return null;
    }
  }

  verifyAccessToken(token: string): string | null {
    if (!this.jwtService) return null;
    try {
      const payload = this.jwtService.verify(token, { algorithms: ['RS256'], issuer: ISSUER });
      if (!payload || !payload.aci) return null;
      return payload.aci as string;
    } catch {
      return null;
    }
  }
}
