/**
 * TokenManager — handles OAuth 2.0 token acquisition, caching, and refresh.
 * Tokens are re-issued automatically when expired or within 60 seconds of expiry.
 */

import { AgentIdPError } from './errors.js';
import { TokenResponse } from './types.js';

/** Seconds before expiry at which a token refresh is triggered. */
const REFRESH_BUFFER_SECONDS = 60;

interface CachedToken {
  accessToken: string;
  expiresAt: number; // Unix seconds
}

/**
 * Manages token acquisition and caching for the AgentIdP SDK.
 * Callers request a token via `getToken()` — the manager handles issuance and refresh transparently.
 */
export class TokenManager {
  private cached: CachedToken | null = null;

  /**
   * @param baseUrl - AgentIdP API base URL.
   * @param clientId - The agent's clientId (agentId UUID).
   * @param clientSecret - The agent's clientSecret.
   * @param scopes - Space-separated OAuth 2.0 scopes to request.
   */
  constructor(
    private readonly baseUrl: string,
    private readonly clientId: string,
    private readonly clientSecret: string,
    private readonly scopes: string,
  ) {}

  /**
   * Returns a valid access token.
   * Acquires a new token if none is cached or the cached token is near expiry.
   *
   * @returns A valid JWT access token string.
   * @throws AgentIdPError if token acquisition fails.
   */
  async getToken(): Promise<string> {
    const now = Math.floor(Date.now() / 1000);

    if (this.cached && this.cached.expiresAt - now > REFRESH_BUFFER_SECONDS) {
      return this.cached.accessToken;
    }

    const token = await this.issueToken();
    this.cached = {
      accessToken: token.access_token,
      expiresAt: now + token.expires_in,
    };

    return this.cached.accessToken;
  }

  /**
   * Calls POST /token to issue a new access token.
   *
   * @returns TokenResponse from the API.
   * @throws AgentIdPError on authentication failure or API error.
   */
  private async issueToken(): Promise<TokenResponse> {
    const body = new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: this.clientId,
      client_secret: this.clientSecret,
      scope: this.scopes,
    });

    const response = await fetch(`${this.baseUrl}/api/v1/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: body.toString(),
    });

    const data = (await response.json()) as Record<string, unknown>;

    if (!response.ok) {
      const errorBody = data as { error: string; error_description: string };
      throw AgentIdPError.fromOAuth2Error(
        { error: String(errorBody.error ?? 'unknown_error'), error_description: String(errorBody.error_description ?? 'Token request failed.') },
        response.status,
      );
    }

    return data as unknown as TokenResponse;
  }

  /** Clears the cached token, forcing re-acquisition on next getToken() call. */
  clearCache(): void {
    this.cached = null;
  }
}
