/**
 * issue-token GitHub Action script.
 *
 * Flow:
 *   1. Request a GitHub OIDC token via @actions/core.getIDToken()
 *   2. Exchange the OIDC token for a SentryAgent.ai access token via POST /oidc/token
 *   3. Set outputs: access-token (masked) and expires-at (ISO 8601)
 *
 * The access token is immediately registered with core.setSecret() so it never
 * appears in plaintext in workflow logs.
 *
 * Error handling:
 *   - OIDC exchange failures emit a clear message with a link to the trust policy setup docs
 */

'use strict';

const core = require('@actions/core');
const { HttpClient } = require('@actions/http-client');

/**
 * Exchanges a GitHub OIDC JWT for a SentryAgent.ai access token for a specific agent.
 *
 * @param {string} apiUrl - Base URL of the SentryAgent.ai AgentIdP API.
 * @param {string} oidcToken - GitHub OIDC JWT obtained from core.getIDToken().
 * @param {string} agentId - UUID of the agent for which to issue a token.
 * @returns {Promise<{ accessToken: string; expiresIn: number }>} The access token and its TTL in seconds.
 * @throws {Error} If the exchange fails, with a message including trust policy setup instructions.
 */
async function exchangeOIDCToken(apiUrl, oidcToken, agentId) {
  const client = new HttpClient('sentryagent-issue-token/1.0');
  const url = `${apiUrl}/api/v1/oidc/token`;

  const body = JSON.stringify({
    provider: 'github',
    token: oidcToken,
    agentId,
  });

  let response;
  try {
    response = await client.post(url, body, {
      'Content-Type': 'application/json',
      Accept: 'application/json',
    });
  } catch (err) {
    throw new Error(
      `Failed to reach the SentryAgent.ai OIDC token endpoint at ${url}. ` +
        `Check that the api-url input is correct and the API is reachable.\n` +
        `Underlying error: ${err instanceof Error ? err.message : String(err)}`,
    );
  }

  const rawBody = await response.readBody();
  const statusCode = response.message.statusCode ?? 0;

  if (statusCode === 403) {
    throw new Error(
      'GitHub OIDC token exchange was rejected with HTTP 403 (Forbidden). ' +
        'This usually means no trust policy has been registered for this repository.\n\n' +
        'To fix this, register a trust policy by calling:\n' +
        `  POST ${apiUrl}/oidc/trust-policies\n` +
        '  Body: { "provider": "github", "repository": "org/repo", "agentId": "<agent-id>" }\n\n' +
        'For full setup instructions, visit: https://docs.sentryagent.ai/github-actions#trust-policy',
    );
  }

  if (statusCode < 200 || statusCode >= 300) {
    let detail = rawBody;
    try {
      const parsed = JSON.parse(rawBody);
      detail = parsed.message ?? parsed.error_description ?? rawBody;
    } catch {
      // use rawBody as-is
    }
    throw new Error(
      `OIDC token exchange failed with HTTP ${statusCode}: ${detail}\n` +
        'For trust policy setup instructions, visit: https://docs.sentryagent.ai/github-actions#trust-policy',
    );
  }

  let tokenData;
  try {
    tokenData = JSON.parse(rawBody);
  } catch {
    throw new Error(`OIDC token exchange returned non-JSON response: ${rawBody}`);
  }

  if (typeof tokenData.access_token !== 'string' || tokenData.access_token.length === 0) {
    throw new Error('OIDC token exchange response did not include an access_token.');
  }

  const expiresIn = typeof tokenData.expires_in === 'number' ? tokenData.expires_in : 3600;

  return { accessToken: tokenData.access_token, expiresIn };
}

/**
 * Computes an ISO 8601 expiry timestamp from a TTL in seconds.
 *
 * @param {number} expiresInSeconds - Number of seconds until the token expires.
 * @returns {string} ISO 8601 timestamp string.
 */
function computeExpiresAt(expiresInSeconds) {
  return new Date(Date.now() + expiresInSeconds * 1000).toISOString();
}

/**
 * Main entry point for the issue-token GitHub Action.
 *
 * @returns {Promise<void>}
 */
async function run() {
  try {
    // Read inputs
    const apiUrl = core.getInput('api-url', { required: true }).replace(/\/$/, '');
    const agentId = core.getInput('agent-id', { required: true });

    core.info(`Requesting GitHub OIDC token for audience: ${apiUrl}`);
    let oidcToken;
    try {
      oidcToken = await core.getIDToken(apiUrl);
    } catch (err) {
      throw new Error(
        'Failed to obtain a GitHub OIDC token. ' +
          "Ensure the workflow has 'id-token: write' permission in its permissions block.\n\n" +
          'Example:\n' +
          'permissions:\n' +
          '  id-token: write\n' +
          '  contents: read\n\n' +
          `Underlying error: ${err instanceof Error ? err.message : String(err)}\n` +
          'For setup instructions, visit: https://docs.sentryagent.ai/github-actions#trust-policy',
      );
    }

    core.info(`Exchanging GitHub OIDC token for SentryAgent.ai access token (agent: ${agentId})...`);
    const { accessToken, expiresIn } = await exchangeOIDCToken(apiUrl, oidcToken, agentId);

    // Mask the token immediately — must happen before any logging or output
    core.setSecret(accessToken);

    const expiresAt = computeExpiresAt(expiresIn);

    core.setOutput('access-token', accessToken);
    core.setOutput('expires-at', expiresAt);

    core.info(`Access token issued successfully. Expires at: ${expiresAt}`);
  } catch (err) {
    core.setFailed(err instanceof Error ? err.message : String(err));
  }
}

run();
