import { Logger } from "@nestjs/common";
import OpenAI from "openai";
import type { ChatCompletionMessageParam } from "openai/resources/chat";
import type {
  LlmProviderInterface,
  LlmProviderConfig,
  LlmProviderHealthStatus,
} from "./llm-provider.interface";
import type { ChatRequestDto, ChatResponseDto, EmbedRequestDto, EmbedResponseDto } from "../dto";
import { TraceLlmCall, createLlmSpan } from "../../telemetry";
import { SpanStatusCode } from "@opentelemetry/api";

/**
 * Configuration for OpenAI LLM provider.
 * Extends base LlmProviderConfig with OpenAI-specific options.
 *
 * @example
 * ```typescript
 * const config: OpenAiProviderConfig = {
 *   endpoint: "https://api.openai.com/v1",
 *   apiKey: "sk-...",
 *   organization: "org-...",
 *   timeout: 30000
 * };
 * ```
 */
export interface OpenAiProviderConfig extends LlmProviderConfig {
  /**
   * OpenAI API endpoint URL
   * @default "https://api.openai.com/v1"
   */
  endpoint: string;

  /**
   * OpenAI API key (required)
   */
  apiKey: string;

  /**
   * Optional OpenAI organization ID
   */
  organization?: string;

  /**
   * Request timeout in milliseconds
   * @default 30000
   */
  timeout?: number;
}

/**
 * OpenAI LLM provider implementation.
 * Provides integration with OpenAI's GPT models (GPT-4, GPT-3.5, etc.).
 *
 * @example
 * ```typescript
 * const provider = new OpenAiProvider({
 *   endpoint: "https://api.openai.com/v1",
 *   apiKey: "sk-...",
 *   timeout: 30000
 * });
 *
 * await provider.initialize();
 *
 * const response = await provider.chat({
 *   model: "gpt-4",
 *   messages: [{ role: "user", content: "Hello" }]
 * });
 * ```
 */
export class OpenAiProvider implements LlmProviderInterface {
  readonly name = "OpenAI";
  readonly type = "openai" as const;

  private readonly logger = new Logger(OpenAiProvider.name);
  private readonly client: OpenAI;
  private readonly config: OpenAiProviderConfig;

  /**
   * Creates a new OpenAI provider instance.
   *
   * @param config - OpenAI provider configuration
   */
  constructor(config: OpenAiProviderConfig) {
    this.config = {
      ...config,
      timeout: config.timeout ?? 30000,
    };

    this.client = new OpenAI({
      apiKey: this.config.apiKey,
      organization: this.config.organization,
      baseURL: this.config.endpoint,
      timeout: this.config.timeout,
    });

    this.logger.log(`OpenAI provider initialized with endpoint: ${this.config.endpoint}`);
  }

  /**
   * Initialize the OpenAI provider.
   * This is a no-op for OpenAI as the client is initialized in the constructor.
   */
  async initialize(): Promise<void> {
    // OpenAI client is initialized in constructor
    // No additional setup required
  }

  /**
   * Check if the OpenAI API is healthy and reachable.
   *
   * @returns Health status with available models if healthy
   */
  async checkHealth(): Promise<LlmProviderHealthStatus> {
    try {
      const response = await this.client.models.list();
      const models = response.data.map((m) => m.id);

      return {
        healthy: true,
        provider: "openai",
        endpoint: this.config.endpoint,
        models,
      };
    } catch (error: unknown) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      this.logger.warn(`OpenAI health check failed: ${errorMessage}`);

      return {
        healthy: false,
        provider: "openai",
        endpoint: this.config.endpoint,
        error: errorMessage,
      };
    }
  }

  /**
   * List all available models from the OpenAI API.
   *
   * @returns Array of model names
   * @throws {Error} If the request fails
   */
  async listModels(): Promise<string[]> {
    try {
      const response = await this.client.models.list();
      return response.data.map((m) => m.id);
    } catch (error: unknown) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      this.logger.error(`Failed to list models: ${errorMessage}`);
      throw new Error(`Failed to list models: ${errorMessage}`);
    }
  }

  /**
   * Perform a synchronous chat completion.
   *
   * @param request - Chat request with messages and configuration
   * @returns Complete chat response
   * @throws {Error} If the request fails
   */
  @TraceLlmCall({ system: "openai", operation: "chat" })
  async chat(request: ChatRequestDto): Promise<ChatResponseDto> {
    try {
      const messages = this.buildMessages(request);
      const options = this.buildChatOptions(request);

      const response = await this.client.chat.completions.create({
        model: request.model,
        messages,
        stream: false,
        ...options,
      });

      const choice = response.choices[0];
      if (!choice) {
        throw new Error("No completion choice returned from OpenAI");
      }

      const result: ChatResponseDto = {
        model: response.model,
        message: {
          role: choice.message.role as "assistant",
          content: choice.message.content ?? "",
        },
        done: true,
      };

      // Add optional properties only if they exist
      if (response.usage?.prompt_tokens !== undefined) {
        result.promptEvalCount = response.usage.prompt_tokens;
      }
      if (response.usage?.completion_tokens !== undefined) {
        result.evalCount = response.usage.completion_tokens;
      }

      return result;
    } catch (error: unknown) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      this.logger.error(`Chat completion failed: ${errorMessage}`);
      throw new Error(`Chat completion failed: ${errorMessage}`);
    }
  }

  /**
   * Perform a streaming chat completion.
   * Yields response chunks as they arrive from the OpenAI API.
   *
   * @param request - Chat request with messages and configuration
   * @yields Chat response chunks
   * @throws {Error} If the request fails
   */
  async *chatStream(request: ChatRequestDto): AsyncGenerator<ChatResponseDto> {
    const span = createLlmSpan("openai", "chat.stream", request.model);

    try {
      const messages = this.buildMessages(request);
      const options = this.buildChatOptions(request);

      const stream = await this.client.chat.completions.create({
        model: request.model,
        messages,
        stream: true,
        ...options,
      });

      for await (const chunk of stream) {
        const choice = chunk.choices[0];
        if (!choice) {
          continue;
        }

        const isDone = choice.finish_reason === "stop" || choice.finish_reason === "length";

        const role = choice.delta.role === "assistant" ? "assistant" : "assistant";

        yield {
          model: chunk.model,
          message: {
            role,
            content: choice.delta.content ?? "",
          },
          done: isDone,
        };
      }

      span.setStatus({ code: SpanStatusCode.OK });
    } catch (error: unknown) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      this.logger.error(`Streaming failed: ${errorMessage}`);

      span.recordException(error instanceof Error ? error : new Error(errorMessage));
      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: errorMessage,
      });

      throw new Error(`Streaming failed: ${errorMessage}`);
    } finally {
      span.end();
    }
  }

  /**
   * Generate embeddings for the given input texts.
   *
   * @param request - Embedding request with model and input texts
   * @returns Embeddings response with vector arrays
   * @throws {Error} If the request fails
   */
  @TraceLlmCall({ system: "openai", operation: "embed" })
  async embed(request: EmbedRequestDto): Promise<EmbedResponseDto> {
    try {
      const response = await this.client.embeddings.create({
        model: request.model,
        input: request.input,
      });

      return {
        model: response.model,
        embeddings: response.data.map((item) => item.embedding),
      };
    } catch (error: unknown) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      this.logger.error(`Embedding failed: ${errorMessage}`);
      throw new Error(`Embedding failed: ${errorMessage}`);
    }
  }

  /**
   * Get the current provider configuration.
   * Returns a copy to prevent external modification.
   *
   * @returns Provider configuration object
   */
  getConfig(): OpenAiProviderConfig {
    return { ...this.config };
  }

  /**
   * Build message array from chat request.
   * Prepends system prompt if provided and not already in messages.
   *
   * @param request - Chat request
   * @returns Array of messages for OpenAI
   */
  private buildMessages(request: ChatRequestDto): ChatCompletionMessageParam[] {
    const messages: ChatCompletionMessageParam[] = [];

    // Add system prompt if provided and not already in messages
    if (request.systemPrompt && !request.messages.some((m) => m.role === "system")) {
      messages.push({
        role: "system",
        content: request.systemPrompt,
      });
    }

    // Add all request messages
    for (const message of request.messages) {
      messages.push({
        role: message.role,
        content: message.content,
      });
    }

    return messages;
  }

  /**
   * Build OpenAI-specific chat options from request.
   *
   * @param request - Chat request
   * @returns OpenAI options object
   */
  private buildChatOptions(request: ChatRequestDto): {
    temperature?: number;
    max_tokens?: number;
  } {
    const options: { temperature?: number; max_tokens?: number } = {};

    if (request.temperature !== undefined) {
      options.temperature = request.temperature;
    }

    if (request.maxTokens !== undefined) {
      options.max_tokens = request.maxTokens;
    }

    return options;
  }
}
