import { Logger } from "@nestjs/common";
import Anthropic from "@anthropic-ai/sdk";
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";

/**
 * Static list of Claude models.
 * Claude API doesn't provide a list models endpoint, so we maintain this manually.
 */
const CLAUDE_MODELS = [
  "claude-opus-4-20250514",
  "claude-sonnet-4-20250514",
  "claude-3-5-sonnet-20241022",
  "claude-3-5-haiku-20241022",
  "claude-3-opus-20240229",
  "claude-3-sonnet-20240229",
  "claude-3-haiku-20240307",
];

/**
 * Configuration for Claude (Anthropic) LLM provider.
 * Extends base LlmProviderConfig with Claude-specific options.
 *
 * @example
 * ```typescript
 * const config: ClaudeProviderConfig = {
 *   endpoint: "https://api.anthropic.com",
 *   apiKey: "sk-ant-...",
 *   timeout: 30000
 * };
 * ```
 */
export interface ClaudeProviderConfig extends LlmProviderConfig {
  /**
   * Claude API endpoint URL
   * @default "https://api.anthropic.com"
   */
  endpoint: string;

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

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

/**
 * Claude (Anthropic) LLM provider implementation.
 * Provides integration with Anthropic's Claude models (Opus, Sonnet, Haiku).
 *
 * @example
 * ```typescript
 * const provider = new ClaudeProvider({
 *   endpoint: "https://api.anthropic.com",
 *   apiKey: "sk-ant-...",
 *   timeout: 30000
 * });
 *
 * await provider.initialize();
 *
 * const response = await provider.chat({
 *   model: "claude-opus-4-20250514",
 *   messages: [{ role: "user", content: "Hello" }]
 * });
 * ```
 */
export class ClaudeProvider implements LlmProviderInterface {
  readonly name = "Claude";
  readonly type = "claude" as const;

  private readonly logger = new Logger(ClaudeProvider.name);
  private readonly client: Anthropic;
  private readonly config: ClaudeProviderConfig;

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

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

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

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

  /**
   * Check if the Claude API is healthy and reachable.
   * Since Claude doesn't have a dedicated health check endpoint,
   * we perform a minimal API call to verify connectivity.
   *
   * @returns Health status with available models if healthy
   */
  async checkHealth(): Promise<LlmProviderHealthStatus> {
    try {
      // Test the API with a minimal request
      await this.client.messages.create({
        model: "claude-3-haiku-20240307",
        max_tokens: 1,
        messages: [{ role: "user", content: "test" }],
      });

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

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

  /**
   * List all available Claude models.
   * Returns a static list as Claude doesn't provide a list models API.
   *
   * @returns Array of model names
   */
  listModels(): Promise<string[]> {
    return Promise.resolve(CLAUDE_MODELS);
  }

  /**
   * 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: "claude", operation: "chat" })
  async chat(request: ChatRequestDto): Promise<ChatResponseDto> {
    try {
      const { systemPrompt, messages } = this.extractSystemPrompt(request);
      const options = this.buildChatOptions(request);

      const response = await this.client.messages.create({
        model: request.model,
        max_tokens: request.maxTokens ?? 1024,
        messages: messages.map((m) => ({
          role: m.role as "user" | "assistant",
          content: m.content,
        })),
        ...(systemPrompt ? { system: systemPrompt } : {}),
        ...options,
      });

      // Extract text content from response
      const textContent = response.content
        .filter((block) => block.type === "text")
        .map((block) => ("text" in block ? block.text : ""))
        .join("");

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

      // Add usage information
      result.promptEvalCount = response.usage.input_tokens;
      result.evalCount = response.usage.output_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 Claude 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("claude", "chat.stream", request.model);

    try {
      const { systemPrompt, messages } = this.extractSystemPrompt(request);
      const options = this.buildChatOptions(request);

      const streamGenerator = this.client.messages.stream({
        model: request.model,
        max_tokens: request.maxTokens ?? 1024,
        messages: messages.map((m) => ({
          role: m.role as "user" | "assistant",
          content: m.content,
        })),
        ...(systemPrompt ? { system: systemPrompt } : {}),
        ...options,
      });

      for await (const event of streamGenerator) {
        if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
          yield {
            model: request.model,
            message: {
              role: "assistant",
              content: event.delta.text,
            },
            done: false,
          };
        } else if (event.type === "message_stop") {
          yield {
            model: request.model,
            message: {
              role: "assistant",
              content: "",
            },
            done: true,
          };
        }
      }

      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.
   * Claude does not support embeddings - this method throws an error.
   *
   * @param _request - Embedding request (unused)
   * @throws {Error} Always throws as Claude doesn't support embeddings
   */
  @TraceLlmCall({ system: "claude", operation: "embed" })
  embed(_request: EmbedRequestDto): Promise<EmbedResponseDto> {
    throw new Error(
      "Claude provider does not support embeddings. Use Ollama or OpenAI for embeddings."
    );
  }

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

  /**
   * Extract system prompt from messages or systemPrompt field.
   * Claude requires system prompts to be separate from messages.
   *
   * @param request - Chat request
   * @returns Object with system prompt and filtered messages
   */
  private extractSystemPrompt(request: ChatRequestDto): {
    systemPrompt: string | undefined;
    messages: { role: string; content: string }[];
  } {
    let systemPrompt = request.systemPrompt;
    const messages = [];

    // Extract system message from messages array if present
    for (const message of request.messages) {
      if (message.role === "system") {
        systemPrompt = message.content;
      } else {
        messages.push(message);
      }
    }

    return { systemPrompt, messages };
  }

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

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

    return options;
  }
}
