/**
 * GitHub OAuth Client
 *
 * Implements GitHub Device Flow (RFC 8628) for CLI authentication.
 */

import {
  GITHUB_CLIENT_ID,
  GITHUB_ENDPOINTS,
  DEFAULT_SCOPES,
  DeviceCodeResponseSchema,
  TokenResponseSchema,
  OAuthErrorSchema,
  type AuthConfig,
  type DeviceCodeResponse,
  type TokenResponse,
  type OAuthErrorCode,
  type GitHubUser,
} from './types.js';
import { getTokenManager, type TokenManager } from './manager.js';
import { createLogger, type Logger } from '../utils/logger.js';

/**
 * GitHub OAuth Client Options
 */
export interface GitHubAuthOptions {
  /** OAuth App Client ID */
  clientId?: string;
  /** Token manager instance */
  tokenManager?: TokenManager;
  /** Logger instance */
  logger?: Logger;
}

/**
 * GitHub OAuth Client
 *
 * Handles the OAuth Device Flow:
 * 1. Request device code from GitHub
 * 2. User enters code at verification URL
 * 3. Poll for access token
 * 4. Fetch user info and store credentials
 */
export class GitHubAuthClient {
  private clientId: string;
  private tokenManager: TokenManager;
  private logger: Logger;

  // Active device codes for polling
  private activeDeviceCodes: Map<string, DeviceCodeResponse> = new Map();

  constructor(options?: GitHubAuthOptions) {
    this.clientId = options?.clientId ?? GITHUB_CLIENT_ID;
    this.tokenManager = options?.tokenManager ?? getTokenManager();
    this.logger = options?.logger ?? createLogger({ name: 'github-auth' });
  }

  /**
   * Request a device code from GitHub
   */
  async requestDeviceCode(scopes: string[] = DEFAULT_SCOPES): Promise<DeviceCodeResponse> {
    const params = new URLSearchParams({
      client_id: this.clientId,
      scope: scopes.join(' '),
    });

    const response = await fetch(GITHUB_ENDPOINTS.deviceCode, {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: params.toString(),
    });

    if (!response.ok) {
      const text = await response.text();
      throw new Error(`Failed to request device code: ${response.status} ${text}`);
    }

    const data = await response.json();
    const deviceCode = DeviceCodeResponseSchema.parse(data);

    // Store for later polling
    this.activeDeviceCodes.set(deviceCode.user_code, deviceCode);

    this.logger.info(
      { userCode: deviceCode.user_code, verificationUri: deviceCode.verification_uri },
      'Device code requested'
    );

    return deviceCode;
  }

  /**
   * Poll for access token
   *
   * Returns the token response or null if still pending/expired
   */
  async pollForToken(
    deviceCode: string,
    interval: number = 5
  ): Promise<{ status: 'pending' | 'success' | 'expired' | 'error'; config?: AuthConfig; error?: string }> {
    const params = new URLSearchParams({
      client_id: this.clientId,
      device_code: deviceCode,
      grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
    });

    try {
      const response = await fetch(GITHUB_ENDPOINTS.accessToken, {
        method: 'POST',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: params.toString(),
      });

      const data = await response.json();

      // Check for error response
      if (data.error) {
        const errorCode = data.error as OAuthErrorCode;

        switch (errorCode) {
          case 'authorization_pending':
            return { status: 'pending' };

          case 'slow_down':
            // GitHub wants us to slow down, increase interval
            this.logger.debug('Received slow_down, increasing poll interval');
            return { status: 'pending' };

          case 'expired_token':
            this.logger.warn('Device code expired');
            return { status: 'expired' };

          case 'access_denied':
            return { status: 'error', error: 'User denied authorization' };

          case 'access_suspended':
            return { status: 'error', error: 'User access suspended' };

          default:
            return { status: 'error', error: data.error_description || data.error };
        }
      }

      // Success - parse token response
      const tokenResponse = TokenResponseSchema.parse(data);

      // Fetch user info
      const user = await this.fetchUser(tokenResponse.access_token);

      // Build auth config
      const config: AuthConfig = {
        access_token: tokenResponse.access_token,
        token_type: tokenResponse.token_type,
        scope: tokenResponse.scope,
        username: user?.login,
      };

      // Save to storage
      await this.tokenManager.saveConfig(config);

      this.logger.info({ username: config.username }, 'Authentication successful');

      return { status: 'success', config };
    } catch (error) {
      this.logger.error({ error }, 'Poll for token failed');
      return { status: 'error', error: error instanceof Error ? error.message : 'Unknown error' };
    }
  }

  /**
   * Poll for token using user code (for API endpoint)
   */
  async pollByUserCode(
    userCode: string
  ): Promise<{ status: 'pending' | 'success' | 'expired' | 'error'; config?: AuthConfig; error?: string }> {
    const deviceCodeResponse = this.activeDeviceCodes.get(userCode);

    if (!deviceCodeResponse) {
      return { status: 'error', error: 'Unknown user code' };
    }

    // Check if expired
    const now = Date.now();
    const startTime = this.getDeviceCodeStartTime(userCode);
    if (startTime && now - startTime > deviceCodeResponse.expires_in * 1000) {
      this.activeDeviceCodes.delete(userCode);
      return { status: 'expired' };
    }

    const result = await this.pollForToken(deviceCodeResponse.device_code, deviceCodeResponse.interval);

    // Clean up on success or expiry
    if (result.status === 'success' || result.status === 'expired') {
      this.activeDeviceCodes.delete(userCode);
    }

    return result;
  }

  /**
   * Get start time for a device code (for expiry tracking)
   */
  private deviceCodeStartTimes: Map<string, number> = new Map();

  private getDeviceCodeStartTime(userCode: string): number | undefined {
    if (!this.deviceCodeStartTimes.has(userCode)) {
      this.deviceCodeStartTimes.set(userCode, Date.now());
    }
    return this.deviceCodeStartTimes.get(userCode);
  }

  /**
   * Fetch GitHub user info
   */
  async fetchUser(token: string): Promise<GitHubUser | null> {
    try {
      const response = await fetch(GITHUB_ENDPOINTS.user, {
        headers: {
          'Accept': 'application/json',
          'Authorization': `Bearer ${token}`,
        },
      });

      if (!response.ok) {
        this.logger.warn({ status: response.status }, 'Failed to fetch user info');
        return null;
      }

      const data = await response.json();
      return {
        login: data.login,
        id: data.id,
        name: data.name,
        email: data.email,
      };
    } catch (error) {
      this.logger.warn({ error }, 'Failed to fetch user info');
      return null;
    }
  }

  /**
   * Complete login flow (blocking, for CLI use)
   *
   * Opens browser for user and polls until authorized or timeout.
   */
  async login(
    scopes: string[] = DEFAULT_SCOPES,
    options?: { openBrowser?: boolean; timeout?: number }
  ): Promise<AuthConfig | null> {
    const { openBrowser = true, timeout = 900 } = options ?? {};

    // Request device code
    const deviceCode = await this.requestDeviceCode(scopes);

    // Open browser if requested
    if (openBrowser) {
      const { default: open } = await import('open');
      await open(deviceCode.verification_uri);
    }

    console.log(`\nPlease visit: ${deviceCode.verification_uri}`);
    console.log(`Enter code: ${deviceCode.user_code}\n`);

    // Poll for token
    const startTime = Date.now();
    let interval = deviceCode.interval * 1000;

    while (Date.now() - startTime < timeout * 1000) {
      await this.sleep(interval);

      const result = await this.pollForToken(deviceCode.device_code, deviceCode.interval);

      switch (result.status) {
        case 'success':
          return result.config!;
        case 'expired':
        case 'error':
          console.error(`Login failed: ${result.error ?? 'expired'}`);
          return null;
        case 'pending':
          // Continue polling
          break;
      }
    }

    console.error('Login timed out');
    return null;
  }

  /**
   * Logout - clear stored credentials
   */
  async logout(): Promise<boolean> {
    const deleted = await this.tokenManager.deleteConfig();
    if (deleted) {
      this.logger.info('Logged out');
    }
    return deleted;
  }

  /**
   * Sleep helper
   */
  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
}

// Singleton instance
let githubAuthInstance: GitHubAuthClient | null = null;

/**
 * Get the GitHub auth client singleton
 */
export function getGitHubAuth(): GitHubAuthClient {
  if (!githubAuthInstance) {
    githubAuthInstance = new GitHubAuthClient();
  }
  return githubAuthInstance;
}

/**
 * Create a new GitHub auth client
 */
export function createGitHubAuth(options?: GitHubAuthOptions): GitHubAuthClient {
  return new GitHubAuthClient(options);
}
