import { EventEmitter } from 'events';
import pRetry from 'p-retry';
import pThrottle from 'p-throttle';

import {
  CompletionsRequestOptions,
  NeuraResponse,
  NeuraSDKConfig,
  NeuraStream,
  ProcessingError,
  StreamChunk,
} from '../types';
import { MemoryCache } from '../utils/cache';

const DEFAULT_CONFIG: Partial<NeuraSDKConfig> = {
  maxRetries: 3,
  retryDelay: 1000,
  requestsPerMinute: 60,
  enableCache: true,
  cacheTTL: 5 * 60 * 1000, // 5 minutes
};

export class NeuraSDK {
  private baseUrl: string;
  private apiKey: string;
  private maxRetries: number;
  private retryDelay: number;
  private cache: MemoryCache | null;
  private throttle: <T>(fn: (...args: any[]) => Promise<T>) => (...args: any[]) => Promise<T>;

  constructor(config: NeuraSDKConfig) {
    this.baseUrl = config.baseUrl.endsWith('/') ? config.baseUrl.slice(0, -1) : config.baseUrl;
    this.apiKey = config.apiKey;
    this.maxRetries = config.maxRetries ?? DEFAULT_CONFIG.maxRetries!;
    this.retryDelay = config.retryDelay ?? DEFAULT_CONFIG.retryDelay!;
    
    // Initialize cache if enabled
    this.cache = config.enableCache ?? DEFAULT_CONFIG.enableCache 
      ? new MemoryCache(config.cacheTTL ?? DEFAULT_CONFIG.cacheTTL)
      : null;
    
    // Initialize rate limiter
    const requestsPerMinute = config.requestsPerMinute ?? DEFAULT_CONFIG.requestsPerMinute!;
    this.throttle = pThrottle({
      limit: requestsPerMinute,
      interval: 60 * 1000, // 1 minute in milliseconds
    });
  }

  /**
   * Create a chat completion
   * @param options Request options
   * @returns Promise with the completion response
   */
  async createCompletion(options: CompletionsRequestOptions): Promise<NeuraResponse> {
    // Check cache first if enabled and not streaming
    if (this.cache && !options.stream) {
      const cacheKey = this.getCacheKey(options);
      const cachedResponse = this.cache.get<NeuraResponse>(cacheKey);
      
      if (cachedResponse) {
        return cachedResponse;
      }
    }
    
    // Apply throttling and retry logic
    const throttledRequest = this.throttle(async () => {
      return await pRetry(
        async () => {
          const response = await this.makeRequest(options);
          return response;
        },
        {
          retries: this.maxRetries,
          minTimeout: this.retryDelay,
          onFailedAttempt: (error) => {
            console.warn(
              `Request failed (attempt ${error.attemptNumber}/${error.retriesLeft + error.attemptNumber}): ${error.message}`
            );
          },
        }
      );
    });
    
    const response = await throttledRequest();
    
    // Cache the response if caching is enabled and it's not an error
    if (this.cache && !options.stream && !('error' in response)) {
      const cacheKey = this.getCacheKey(options);
      this.cache.set(cacheKey, response);
    }
    
    return response;
  }

  /**
   * Create a streaming chat completion
   * @param options Request options
   * @returns EventEmitter that emits chunks, done, and error events
   */
  createCompletionStream(options: CompletionsRequestOptions): NeuraStream {
    const eventEmitter = new EventEmitter() as NeuraStream;
    
    // Force stream to true
    const streamOptions = { ...options, stream: true };
    
    // Apply throttling
    const throttledRequest = this.throttle(async () => {
      try {
        await this.processStream(streamOptions, eventEmitter);
      } catch (error) {
        eventEmitter.emit('error', error instanceof Error 
          ? error 
          : new Error(String(error)));
      }
    });
    
    // Start the stream processing
    throttledRequest().catch((error) => {
      eventEmitter.emit('error', error instanceof Error 
        ? error 
        : new Error(String(error)));
    });
    
    return eventEmitter;
  }

  /**
   * Clear the cache
   */
  clearCache(): void {
    this.cache?.clear();
  }

  /**
   * Make a request to the API
   * @param options Request options
   * @returns Promise with the response
   * @private
   */
  private async makeRequest(options: CompletionsRequestOptions): Promise<NeuraResponse> {
    const endpoint = `${this.baseUrl}/v1/chat/completions`;
    
    // Prepare the request body
    const body: Record<string, any> = {
      messages: options.messages,
    };

    if (options.sessionId) body.session_id = options.sessionId;
    if (options.userId) body.user_id = options.userId;
    if (options.stream !== undefined) body.stream = options.stream;
    if (options.reasoningFormat) body.reasoning_format = options.reasoningFormat;
    
    // Handle file data (convert Buffer to base64 if needed)
    if (options.fileData) {
      if (Buffer.isBuffer(options.fileData)) {
        body.file_data = options.fileData.toString('base64');
      } else {
        body.file_data = options.fileData;
      }
    }

    try {
      const response = await fetch(endpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${this.apiKey}`,
        },
        body: JSON.stringify(body),
      });

      // Check for non-2xx responses
      if (!response.ok) {
        const errorData = await response.json();
        return errorData as ProcessingError;
      }

      // Parse the response
      const data = await response.json();
      return data as NeuraResponse;
    } catch (error) {
      throw new Error(`Failed to create completion: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  /**
   * Process a streaming response
   * @param options Request options
   * @param emitter EventEmitter to emit events
   * @private
   */
  private async processStream(
    options: CompletionsRequestOptions, 
    emitter: NeuraStream
  ): Promise<void> {
    const endpoint = `${this.baseUrl}/v1/chat/completions`;
    
    // Prepare the request body
    const body: Record<string, any> = {
      messages: options.messages,
      stream: true,
    };

    if (options.sessionId) body.session_id = options.sessionId;
    if (options.userId) body.user_id = options.userId;
    if (options.reasoningFormat) body.reasoning_format = options.reasoningFormat;
    
    // Handle file data (convert Buffer to base64 if needed)
    if (options.fileData) {
      if (Buffer.isBuffer(options.fileData)) {
        body.file_data = options.fileData.toString('base64');
      } else {
        body.file_data = options.fileData;
      }
    }

    // Make the request with retry logic
    await pRetry(
      async () => {
        const response = await fetch(endpoint, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${this.apiKey}`,
          },
          body: JSON.stringify(body),
        });

        // Check for non-2xx responses
        if (!response.ok) {
          const errorData = await response.json();
          emitter.emit('error', errorData as ProcessingError);
          throw new Error(`API returned ${response.status}: ${JSON.stringify(errorData)}`);
        }

        // Process the SSE stream
        const reader = response.body?.getReader();
        if (!reader) {
          const error = new Error('Response body is null');
          emitter.emit('error', error);
          throw error;
        }

        const decoder = new TextDecoder();
        let buffer = '';

        while (true) {
          const { done, value } = await reader.read();
          
          if (done) {
            break;
          }
          
          // Decode the chunk and add it to our buffer
          buffer += decoder.decode(value, { stream: true });
          
          // Process complete SSE events in the buffer
          const events = buffer.split('\n\n');
          buffer = events.pop() || ''; // Keep the last incomplete event in the buffer
          
          for (const event of events) {
            if (!event.trim()) continue;
            
            // Parse the SSE data
            const lines = event.split('\n');
            for (const line of lines) {
              if (line.startsWith('data: ')) {
                const data = line.slice(6);
                
                // Check if it's the [DONE] marker
                if (data.includes('[DONE]')) {
                  emitter.emit('done');
                  continue;
                }
                
                try {
                  const parsed = JSON.parse(data);
                  emitter.emit('chunk', parsed as StreamChunk);
                } catch (e) {
                  console.error('Failed to parse SSE data:', data);
                }
              }
            }
          }
        }
        
        // Emit any remaining data
        if (buffer.trim()) {
          const lines = buffer.split('\n');
          for (const line of lines) {
            if (line.startsWith('data: ')) {
              try {
                const data = JSON.parse(line.slice(6));
                emitter.emit('chunk', data as StreamChunk);
              } catch (e) {
                console.error('Failed to parse remaining SSE data:', line);
              }
            }
          }
        }
        
        emitter.emit('done');
      },
      {
        retries: this.maxRetries,
        minTimeout: this.retryDelay,
        onFailedAttempt: (error) => {
          console.warn(
            `Stream request failed (attempt ${error.attemptNumber}/${error.retriesLeft + error.attemptNumber}): ${error.message}`
          );
        },
      }
    );
  }

  /**
   * Generate a cache key for the request
   * @param options Request options
   * @returns Cache key
   * @private
   */
  private getCacheKey(options: CompletionsRequestOptions): string {
    // Create a deterministic cache key from the request options
    const keyParts = [
      options.messages,
      options.sessionId || '',
      options.userId || '',
      options.reasoningFormat || '',
    ];
    
    // We don't include file data in the cache key as it could be large
    // Instead, we use a hash of the file data if present
    if (options.fileData) {
      if (Buffer.isBuffer(options.fileData)) {
        // Simple hash for the buffer
        const hash = Buffer.from(options.fileData).reduce(
          (hash, byte) => (hash * 31 + byte) & 0xFFFFFFFF, 0
        ).toString(16);
        keyParts.push(hash);
      } else {
        // For base64 strings, use the first 20 chars as a fingerprint
        keyParts.push(options.fileData.substring(0, 20));
      }
    }
    
    return keyParts.join('::');
  }
}