import crypto from 'crypto';
import fetch from 'node-fetch';

export interface TokenInfo {
  accessToken: string;
  appId?: string;
  appSecret?: string;
  expiresAt?: number; // Unix timestamp
  isValid: boolean;
  scopes?: string[];
  userId?: string;
}

export class AuthManager {
  private accessToken: string;
  private appId?: string;
  private appSecret?: string;
  private tokenInfo?: TokenInfo;
  private validationPromise?: Promise<TokenInfo>;

  constructor(accessToken: string, appId?: string, appSecret?: string) {
    if (!accessToken) {
      throw new Error('META_ACCESS_TOKEN is required');
    }
    this.accessToken = accessToken;
    this.appId = appId;
    this.appSecret = appSecret;
  }

  /**
   * Get the current access token
   */
  getAccessToken(): string {
    return this.accessToken;
  }

  /**
   * Generate appsecret_proof for secure API calls
   * HMAC-SHA256 hash of access token using app secret as key
   */
  generateAppSecretProof(): string | undefined {
    if (!this.appSecret) {
      return undefined;
    }
    const hmac = crypto.createHmac('sha256', this.appSecret);
    hmac.update(this.accessToken);
    return hmac.digest('hex');
  }

  /**
   * Validate the access token via Meta's debug_token endpoint
   */
  async validateToken(): Promise<TokenInfo> {
    // Return cached validation if available
    if (this.tokenInfo && this.tokenInfo.isValid) {
      // Check expiry if available
      if (this.tokenInfo.expiresAt && this.tokenInfo.expiresAt > Date.now()) {
        return this.tokenInfo;
      }
    }

    // Prevent concurrent validation requests
    if (this.validationPromise) {
      return this.validationPromise;
    }

    this.validationPromise = this._performValidation();
    
    try {
      this.tokenInfo = await this.validationPromise;
      return this.tokenInfo;
    } finally {
      this.validationPromise = undefined;
    }
  }

  private async _performValidation(): Promise<TokenInfo> {
    // Use input_token to debug the access token
    // If we have app token (appId|appSecret), use that for validation
    // Otherwise, use the access token to validate itself
    let debugTokenUrl: string;

    if (this.appId && this.appSecret) {
      const appToken = `${this.appId}|${this.appSecret}`;
      debugTokenUrl = `https://graph.facebook.com/v21.0/debug_token?input_token=${this.accessToken}&access_token=${appToken}`;
    } else {
      // Self-validation (less secure, but works for user tokens)
      debugTokenUrl = `https://graph.facebook.com/v21.0/debug_token?input_token=${this.accessToken}&access_token=${this.accessToken}`;
    }

    try {
      const response = await fetch(debugTokenUrl);
      const data = await response.json() as { 
        data?: {
          is_valid: boolean;
          expires_at?: number;
          data_access_expires_at?: number;
          scopes?: string[];
          user_id?: string;
          app_id?: string;
          error?: { message: string; code: number };
        };
        error?: { message: string; code: number };
      };

      if (!response.ok || data.error) {
        throw new Error(`Token validation failed: ${data.error?.message || 'Unknown error'}`);
      }

      if (!data.data) {
        throw new Error('Invalid response from debug_token endpoint');
      }

      const tokenData = data.data;

      if (!tokenData.is_valid) {
        throw new Error('Access token is not valid');
      }

      // Calculate expiry timestamp (prefer data_access_expires_at for newer tokens)
      const expiresAt = tokenData.data_access_expires_at || tokenData.expires_at;

      return {
        accessToken: this.accessToken,
        appId: this.appId,
        appSecret: this.appSecret,
        expiresAt: expiresAt ? expiresAt * 1000 : undefined, // Convert to milliseconds
        isValid: true,
        scopes: tokenData.scopes,
        userId: tokenData.user_id,
      };
    } catch (error) {
      if (error instanceof Error) {
        throw new Error(`Token validation error: ${error.message}`);
      }
      throw error;
    }
  }

  /**
   * Check if token is expired or close to expiry
   */
  isTokenExpired(bufferMinutes: number = 10): boolean {
    if (!this.tokenInfo?.expiresAt) {
      return false; // No expiry info, assume valid
    }
    const bufferMs = bufferMinutes * 60 * 1000;
    return this.tokenInfo.expiresAt - bufferMs < Date.now();
  }

  /**
   * Exchange short-lived token for long-lived token
   * Note: Requires appId and appSecret
   */
  async refreshToLongLivedToken(): Promise<string> {
    if (!this.appId || !this.appSecret) {
      throw new Error('App ID and App Secret required for token refresh');
    }

    const url = new URL('https://graph.facebook.com/v21.0/oauth/access_token');
    url.searchParams.set('grant_type', 'fb_exchange_token');
    url.searchParams.set('client_id', this.appId);
    url.searchParams.set('client_secret', this.appSecret);
    url.searchParams.set('fb_exchange_token', this.accessToken);

    try {
      const response = await fetch(url.toString());
      const data = await response.json() as {
        access_token?: string;
        token_type?: string;
        expires_in?: number;
        error?: { message: string; code: number };
      };

      if (!response.ok || data.error) {
        throw new Error(`Token refresh failed: ${data.error?.message || 'Unknown error'}`);
      }

      if (!data.access_token) {
        throw new Error('No access token returned from refresh');
      }

      // Update the access token
      this.accessToken = data.access_token;
      
      // Update expiry if provided
      if (data.expires_in) {
        this.tokenInfo = {
          ...this.tokenInfo,
          accessToken: data.access_token,
          expiresAt: Date.now() + data.expires_in * 1000,
          isValid: true,
        };
      }

      return data.access_token;
    } catch (error) {
      if (error instanceof Error) {
        throw new Error(`Token refresh error: ${error.message}`);
      }
      throw error;
    }
  }

  /**
   * Get token info with auto-validation
   */
  async getTokenInfo(): Promise<TokenInfo> {
    if (!this.tokenInfo) {
      await this.validateToken();
    }
    return this.tokenInfo!;
  }
}
