import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios from 'axios';
import { AppLoggerService } from 'src/common/services/logger.service';
import { handleKnownErrors } from 'src/common/utils/handle-error.util';
import { ERROR_CODES } from 'src/common/constants/error-string-constants';
import { ZOOM_ENV_KEYS, ZOOM_DEFAULTS } from 'src/common/constants/zoom.constants';
import { CachedToken } from '../interfaces/zoom-oauth.interface';

/**
 * Obtains and caches a Server-to-Server OAuth access token for Zoom.
 *
 * The serverless implementation minted a fresh token on every call; here the
 * token is cached in-memory until shortly before its expiry to cut auth load.
 */
@Injectable()
export class ZoomOAuthService {
  private cached: CachedToken | null = null;
  private inFlight: Promise<string> | null = null;

  constructor(
    private readonly configService: ConfigService,
    private readonly logger: AppLoggerService,
  ) {}

  /**
   * Returns a valid Zoom access token, reusing the cached one when possible.
   * Concurrent callers share a single in-flight request.
   */
  async getAccessToken(): Promise<string> {
    if (this.cached && this.cached.expiresAt > Date.now()) {
      return this.cached.accessToken;
    }
    if (this.inFlight) {
      return this.inFlight;
    }
    this.inFlight = this.requestToken().finally(() => {
      this.inFlight = null;
    });
    return this.inFlight;
  }

  /** Drops the cached token (e.g. after a 401 from the API). */
  invalidate(): void {
    this.cached = null;
  }

  private async requestToken(): Promise<string> {
    try {
      const accountId = this.configService.get<string>(ZOOM_ENV_KEYS.ACCOUNT_ID) ?? '';
      const clientId = this.configService.get<string>(ZOOM_ENV_KEYS.CLIENT_ID) ?? '';
      const clientSecret = this.configService.get<string>(ZOOM_ENV_KEYS.CLIENT_SECRET) ?? '';
      const authUrl =
        this.configService.get<string>(ZOOM_ENV_KEYS.AUTH_URL) ?? ZOOM_DEFAULTS.AUTH_URL;

      const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');

      const response = await axios.post(authUrl, null, {
        params: { grant_type: 'account_credentials', account_id: accountId },
        headers: { Authorization: `Basic ${basicAuth}` },
      });

      const accessToken: string = response.data?.access_token;
      // A 200 with no token (unexpected body) must not be cached — otherwise a
      // `Bearer undefined` is served for the whole TTL. Surface it as an auth
      // failure so the caller can retry a real refresh.
      if (!accessToken) {
        handleKnownErrors(
          ERROR_CODES.ZOOM_AUTH_FAILED,
          new Error('Zoom token response missing access_token'),
        );
      }
      const expiresIn: number = response.data?.expires_in ?? 3600;

      this.cached = {
        accessToken,
        expiresAt: Date.now() + (expiresIn - ZOOM_DEFAULTS.TOKEN_EXPIRY_SKEW_SECONDS) * 1000,
      };
      return accessToken;
    } catch (error: any) {
      this.logger.error('Failed to obtain Zoom access token', error?.stack, {
        error: error?.response?.data ?? error?.message,
      });
      handleKnownErrors(ERROR_CODES.ZOOM_AUTH_FAILED, error);
    }
  }
}
