/**
 * OAuth Token Manager — discovers Keycloak endpoints and manages client_credentials tokens.
 *
 * Discovery follows RFC 9728:
 *   MCP_URL → /.well-known/oauth-protected-resource → authorization_servers[0]
 *           → /.well-known/oauth-authorization-server → token_endpoint
 */

const log = (msg) => process.stderr.write(`[mcp-proxy] ${msg}\n`);

export class TokenManager {
  #clientId;
  #clientSecret;
  #tokenEndpoint = null;
  #accessToken = null;
  #expiresAt = 0;
  #refreshBufferSec;
  #refreshPromise = null;

  /**
   * @param {object} opts
   * @param {string} opts.clientId
   * @param {string} opts.clientSecret
   * @param {number} [opts.refreshBufferSec=60] — seconds before expiry to trigger refresh
   */
  constructor({ clientId, clientSecret, refreshBufferSec = 60 }) {
    this.#clientId = clientId;
    this.#clientSecret = clientSecret;
    this.#refreshBufferSec = refreshBufferSec;
  }

  /**
   * Discovers the token endpoint from the MCP server's OAuth metadata.
   * @param {string} mcpUrl — e.g. "https://mcp.ccmqa.com/mcp"
   */
  async discover(mcpUrl) {
    const base = mcpUrl.replace(/\/mcp\/?$/, '');

    log(`Discovering OAuth metadata from ${base}...`);
    const prmRes = await fetch(`${base}/.well-known/oauth-protected-resource`);
    if (!prmRes.ok) {
      throw new Error(`Failed to fetch protected resource metadata: ${prmRes.status} ${prmRes.statusText}`);
    }
    const prm = await prmRes.json();

    const authServer = prm.authorization_servers?.[0];
    if (!authServer) {
      throw new Error('No authorization_servers in protected resource metadata');
    }
    log(`Authorization server: ${authServer}`);

    const asMeta = await fetch(`${authServer}/.well-known/oauth-authorization-server`);
    if (!asMeta.ok) {
      throw new Error(`Failed to fetch authorization server metadata: ${asMeta.status} ${asMeta.statusText}`);
    }
    const asData = await asMeta.json();

    this.#tokenEndpoint = asData.token_endpoint;
    if (!this.#tokenEndpoint) {
      throw new Error('No token_endpoint in authorization server metadata');
    }
    log(`Token endpoint: ${this.#tokenEndpoint}`);
  }

  /**
   * Returns a valid access token, refreshing if needed.
   * @returns {Promise<string>}
   */
  async getToken() {
    const nowSec = Date.now() / 1000;
    if (this.#accessToken && nowSec < this.#expiresAt - this.#refreshBufferSec) {
      return this.#accessToken;
    }
    if (this.#refreshPromise) {
      return this.#refreshPromise;
    }
    this.#refreshPromise = this.#fetchToken().finally(() => { this.#refreshPromise = null; });
    return this.#refreshPromise;
  }

  /**
   * Forces a fresh token fetch (e.g. after a 401).
   * @returns {Promise<string>}
   */
  async forceRefresh() {
    return this.#fetchToken();
  }

  async #fetchToken() {
    if (!this.#tokenEndpoint) {
      throw new Error('Token endpoint not discovered — call discover() first');
    }

    log('Fetching token...');
    const body = new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: this.#clientId,
      client_secret: this.#clientSecret,
    });

    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 30_000);
    let res;
    try {
      res = await fetch(this.#tokenEndpoint, {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body,
        signal: controller.signal,
      });
    } catch (err) {
      throw new Error(err.name === 'AbortError' ? 'Token request timed out after 30s' : `Token request failed: ${err.message}`);
    } finally {
      clearTimeout(timeout);
    }

    if (!res.ok) {
      const text = await res.text().catch(() => '');
      throw new Error(`Token request failed: ${res.status} ${res.statusText} — ${text}`);
    }

    let data;
    try {
      data = await res.json();
    } catch {
      throw new Error('Token response is not valid JSON');
    }
    if (!data.access_token) {
      throw new Error('No access_token in token response');
    }

    this.#accessToken = data.access_token;
    this.#expiresAt = this.#parseExp(data.access_token) ?? (Date.now() / 1000 + (data.expires_in || 300));

    const remainingSec = Math.round(this.#expiresAt - Date.now() / 1000);
    log(`Token acquired (expires in ${remainingSec}s)`);

    return this.#accessToken;
  }

  /** Decodes JWT payload to extract `exp` claim. Returns epoch seconds or null. */
  #parseExp(jwt) {
    try {
      const parts = jwt.split('.');
      if (parts.length < 2) return null;
      const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
      return typeof payload.exp === 'number' ? payload.exp : null;
    } catch {
      return null;
    }
  }
}
