/**
 * OIDCTokenExchangeController — handles GitHub Actions OIDC token exchange.
 *
 * This endpoint allows GitHub Actions workflows to exchange a GitHub-issued
 * OIDC JWT for a SentryAgent.ai access token, without requiring any long-lived
 * API credentials stored in GitHub Secrets.
 *
 * Flow:
 *   1. Workflow calls `core.getIDToken()` → receives a GitHub-signed JWT.
 *   2. Workflow POSTs `{ provider, token, agentId? }` to POST /oidc/token.
 *   3. This controller decodes the JWT (MVP: no signature verification — TODO),
 *      verifies `iss` is the GitHub Actions OIDC issuer, enforces trust policies
 *      when agentId is provided, and issues a SentryAgent.ai access token.
 *
 * MVP NOTE: GitHub OIDC JWTs are signed by GitHub's JWKS endpoint at
 *   https://token.actions.githubusercontent.com/.well-known/jwks
 * Full JWKS-based signature verification is deferred to a follow-up spec.
 * For now, we decode without verification and validate the `iss` claim.
 */

import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { OIDCTrustPolicyService, TrustPolicyViolationError } from '../services/OIDCTrustPolicyService.js';
import { ValidationError, SentryAgentError } from '../utils/errors.js';
import { signToken, getTokenExpiresIn } from '../utils/jwt.js';
import { v4 as uuidv4 } from 'uuid';

/** Expected `iss` claim for all GitHub Actions OIDC tokens. */
const GITHUB_OIDC_ISSUER = 'https://token.actions.githubusercontent.com';

/**
 * Decoded claims subset from a GitHub Actions OIDC JWT.
 * Only the claims used for trust-policy enforcement are typed here.
 */
interface IGitHubOIDCPayload {
  /** Token issuer — must equal GITHUB_OIDC_ISSUER. */
  iss: string;
  /** GitHub repository in "org/repo" format (e.g. "acme/my-repo"). */
  repository: string;
  /** Full Git ref, e.g. "refs/heads/main". Optional in some workflow contexts. */
  ref?: string;
}

/** Shape of the POST /oidc/token request body. */
interface IOIDCTokenExchangeRequest {
  /** OIDC provider. Only "github" is supported. */
  provider: string;
  /** The raw GitHub OIDC JWT obtained via `core.getIDToken()`. */
  token: string;
  /**
   * UUID of the agent for which to issue a token.
   * When omitted, a short-lived bootstrap token with `agents:write` scope is
   * returned — this enables the register-agent action to create a new agent.
   * When provided, trust-policy enforcement is applied.
   */
  agentId?: string;
}

/**
 * Controller for the unauthenticated OIDC token exchange endpoint.
 * The GitHub OIDC token IS the authentication credential.
 */
export class OIDCTokenExchangeController {
  /**
   * @param trustPolicyService - Service for enforcing OIDC trust policies.
   * @param privateKey - PEM-encoded RSA private key used to sign access tokens.
   */
  constructor(
    private readonly trustPolicyService: OIDCTrustPolicyService,
    private readonly privateKey: string,
  ) {}

  // ─────────────────────────────────────────────────────────────────────────
  // Public handlers
  // ─────────────────────────────────────────────────────────────────────────

  /**
   * Exchanges a GitHub OIDC JWT for a SentryAgent.ai access token.
   *
   * When `agentId` is provided, the endpoint enforces registered trust policies
   * and returns a token scoped to that agent.
   *
   * When `agentId` is omitted, the endpoint verifies the GitHub OIDC issuer
   * claim and returns a short-lived bootstrap token with `agents:write` scope,
   * enabling the workflow to register a new agent.
   *
   * @param req - Express request. Body must conform to IOIDCTokenExchangeRequest.
   * @param res - Express response.
   * @param next - Express next — forwards errors to the global error handler.
   */
  async exchangeToken(req: Request, res: Response, next: NextFunction): Promise<void> {
    try {
      const body = req.body as Partial<IOIDCTokenExchangeRequest>;

      // ── Input validation ────────────────────────────────────────────────
      if (!body.provider || typeof body.provider !== 'string') {
        throw new ValidationError('provider is required.', { received: body.provider });
      }
      if (body.provider !== 'github') {
        throw new ValidationError(
          'Only "github" is supported as an OIDC provider at this time.',
          { provider: body.provider },
        );
      }
      if (!body.token || typeof body.token !== 'string') {
        throw new ValidationError('token is required.', { received: typeof body.token });
      }

      // ── Decode GitHub OIDC JWT ──────────────────────────────────────────
      // TODO: Verify JWT signature against GitHub's JWKS endpoint at
      //   https://token.actions.githubusercontent.com/.well-known/jwks
      // For the MVP we decode without verification and rely on the `iss` claim.
      let githubClaims: IGitHubOIDCPayload;
      try {
        const decoded = jwt.decode(body.token, { complete: false });
        if (!decoded || typeof decoded === 'string') {
          throw new ValidationError('The provided token could not be decoded as a JWT.');
        }
        githubClaims = decoded as IGitHubOIDCPayload;
      } catch (err) {
        if (err instanceof SentryAgentError) {
          throw err;
        }
        throw new ValidationError('The provided token is not a valid JWT.');
      }

      // ── Issuer verification ─────────────────────────────────────────────
      if (githubClaims.iss !== GITHUB_OIDC_ISSUER) {
        throw new SentryAgentError(
          `Invalid token issuer. Expected "${GITHUB_OIDC_ISSUER}", got "${githubClaims.iss}".`,
          'INVALID_OIDC_ISSUER',
          403,
          { expected: GITHUB_OIDC_ISSUER, received: githubClaims.iss },
        );
      }

      // ── Repository claim ────────────────────────────────────────────────
      if (!githubClaims.repository || typeof githubClaims.repository !== 'string') {
        throw new SentryAgentError(
          'GitHub OIDC token is missing the required "repository" claim.',
          'INVALID_OIDC_CLAIMS',
          422,
          { missingClaim: 'repository' },
        );
      }

      const { repository } = githubClaims;
      const ref = githubClaims.ref;

      // ── Trust-policy enforcement or bootstrap mode ───────────────────────
      if (body.agentId) {
        // Enforce trust policy for the specific agent
        await this.trustPolicyService.enforceTrustPolicy('github', repository, ref, body.agentId);

        const accessToken = this.issueAccessToken(body.agentId, 'agents:read agents:write');

        res.status(200).json({
          access_token: accessToken,
          token_type: 'Bearer',
          expires_in: getTokenExpiresIn(),
          scope: 'agents:read agents:write',
        });
      } else {
        // Bootstrap mode: no agentId — the caller is about to register a new agent.
        // Return a short-lived token with agents:write scope.
        // The caller's identity is the GitHub repo (repository claim).
        const bootstrapSubject = `github:${repository}`;
        const accessToken = this.issueAccessToken(bootstrapSubject, 'agents:write');

        res.status(200).json({
          access_token: accessToken,
          token_type: 'Bearer',
          expires_in: getTokenExpiresIn(),
          scope: 'agents:write',
        });
      }
    } catch (err) {
      if (err instanceof TrustPolicyViolationError) {
        next(err);
        return;
      }
      next(err);
    }
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Private helpers
  // ─────────────────────────────────────────────────────────────────────────

  /**
   * Issues a signed RS256 access token for the given subject and scope.
   *
   * Uses the same `signToken` utility as the standard OAuth2 flow so all
   * issued tokens are compatible with the existing `verifyToken` and
   * introspection/revocation machinery.
   *
   * @param sub - The token subject (agentId or "github:<repo>" for bootstrap).
   * @param scope - Space-separated OAuth 2.0 scopes to embed in the token.
   * @returns Signed JWT string.
   */
  private issueAccessToken(sub: string, scope: string): string {
    return signToken(
      {
        sub,
        client_id: sub,
        scope,
        jti: uuidv4(),
        organization_id: 'org_system',
      },
      this.privateKey,
    );
  }
}
