/**
 * IDTokenService — builds and signs OIDC ID tokens for agent identities.
 * ID tokens are signed RS256 or ES256 JWTs using the current OIDC signing key.
 * They conform to OpenID Connect Core 1.0 §2 and contain agent-specific claims.
 *
 * Security constraints:
 * - `alg: none` is explicitly rejected in verifyIDToken().
 * - Only RS256 and ES256 algorithms are accepted for verification.
 * - Signature is verified against the JWKS from OIDCKeyService.
 */

import jwt from 'jsonwebtoken';
import { createPublicKey, JsonWebKey } from 'crypto';

import { OIDCKeyService } from './OIDCKeyService.js';
import { IIDTokenClaims, IJWKSKey } from '../types/oidc.js';
import { IAgent } from '../types/index.js';
import { SentryAgentError } from '../utils/errors.js';

/** 401 — ID token failed verification. */
class IDTokenVerificationError extends SentryAgentError {
  constructor(reason: string) {
    super(`ID token verification failed: ${reason}`, 'ID_TOKEN_INVALID', 401);
  }
}

/**
 * Converts a JWK object to a PEM public key string using Node.js crypto.
 *
 * @param jwk - The IJWKSKey to convert.
 * @returns A PEM-encoded public key string.
 */
function jwkToPem(jwk: IJWKSKey): string {
  // createPublicKey accepts JsonWebKeyInput format
  const keyObj = createPublicKey({ key: jwk as unknown as JsonWebKey, format: 'jwk' });
  return keyObj.export({ type: 'spki', format: 'pem' }) as string;
}

/**
 * Service that builds, signs, and verifies OIDC ID tokens.
 * Delegates key management to OIDCKeyService.
 */
export class IDTokenService {
  /**
   * @param oidcKeyService - Service that manages OIDC signing key pairs.
   */
  constructor(private readonly oidcKeyService: OIDCKeyService) {}

  // ─────────────────────────────────────────────────────────────────────────────
  // Public API
  // ─────────────────────────────────────────────────────────────────────────────

  /**
   * Builds the full ID token claims payload for an agent.
   * Follows OpenID Connect Core 1.0 §2 claim requirements.
   *
   * @param agent - The agent identity record.
   * @param clientId - The OAuth 2.0 client_id that requested the token (becomes `aud`).
   * @param scope - The granted OAuth 2.0 scope string.
   * @param nonce - Optional nonce for replay protection (echoed from the token request).
   * @returns The fully populated IIDTokenClaims object.
   */
  async buildIDTokenClaims(
    agent: IAgent,
    clientId: string,
    scope: string,
    nonce?: string,
  ): Promise<IIDTokenClaims> {
    const issuer = process.env['OIDC_ISSUER'] ?? 'https://idp.sentryagent.ai';
    const ttlSeconds = parseInt(process.env['OIDC_ID_TOKEN_TTL_SECONDS'] ?? '3600', 10);
    const now = Math.floor(Date.now() / 1000);

    const claims: IIDTokenClaims = {
      iss: issuer,
      sub: agent.agentId,
      aud: clientId,
      iat: now,
      exp: now + ttlSeconds,
      agent_type: agent.agentType,
      deployment_env: agent.deploymentEnv,
      organization_id: agent.organizationId,
    };

    if (nonce !== undefined) {
      claims.nonce = nonce;
    }

    if (agent.did !== undefined) {
      claims.did = agent.did;
    }

    // scope is captured in the access token; included here for downstream consumers
    void scope;

    return claims;
  }

  /**
   * Signs an ID token claims payload using the current OIDC signing key.
   * The `kid` of the signing key is included in the JWT header for JWKS lookup.
   *
   * @param claims - The IIDTokenClaims payload to sign.
   * @returns The signed JWT ID token string.
   * @throws OIDCKeyNotFoundError if no current signing key is configured.
   */
  async signIDToken(claims: IIDTokenClaims): Promise<string> {
    const currentKey = await this.oidcKeyService.getCurrentKey();
    const privateKeyPem = await this.oidcKeyService.getPrivateKeyPem(
      currentKey.kid,
      currentKey.vault_key_path,
    );

    const algorithm = currentKey.algorithm as 'RS256' | 'ES256';

    return jwt.sign(claims, privateKeyPem, {
      algorithm,
      header: {
        alg: algorithm,
        kid: currentKey.kid,
        typ: 'JWT',
      },
    });
  }

  /**
   * Verifies an ID token JWT.
   * Fetches the JWKS from OIDCKeyService and selects the key matching the `kid` header.
   * Explicitly rejects tokens with `alg: none`.
   *
   * @param token - The JWT ID token string to verify.
   * @returns The verified IIDTokenClaims payload.
   * @throws IDTokenVerificationError if verification fails for any reason.
   */
  async verifyIDToken(token: string): Promise<IIDTokenClaims> {
    // Decode header without verification to extract kid and alg
    const decoded = jwt.decode(token, { complete: true });
    if (!decoded || typeof decoded === 'string') {
      throw new IDTokenVerificationError('token is malformed');
    }

    const header = decoded.header;

    // Explicitly reject alg: none
    if (!header.alg || header.alg.toLowerCase() === 'none') {
      throw new IDTokenVerificationError('alg:none tokens are not accepted');
    }

    const alg = header.alg;
    if (alg !== 'RS256' && alg !== 'ES256') {
      throw new IDTokenVerificationError(
        `unsupported algorithm "${alg}"; only RS256 and ES256 are accepted`,
      );
    }

    const kid = header.kid;
    if (!kid) {
      throw new IDTokenVerificationError('missing kid header');
    }

    // Fetch JWKS and find the matching key
    const jwks = await this.oidcKeyService.getPublicJWKS();
    const jwkKey = jwks.keys.find((k) => k.kid === kid);
    if (!jwkKey) {
      throw new IDTokenVerificationError(
        `no matching key found in JWKS for kid "${kid}"`,
      );
    }

    // Convert JWK to PEM for jsonwebtoken verification
    const publicKeyPem = jwkToPem(jwkKey);

    try {
      const verifiedPayload = jwt.verify(token, publicKeyPem, {
        algorithms: ['RS256', 'ES256'],
      });

      if (typeof verifiedPayload === 'string') {
        throw new IDTokenVerificationError('unexpected string payload');
      }

      return verifiedPayload as IIDTokenClaims;
    } catch (err) {
      if (err instanceof IDTokenVerificationError) {
        throw err;
      }
      const message = err instanceof Error ? err.message : 'unknown error';
      throw new IDTokenVerificationError(message);
    }
  }
}
