/**
 * Claude Service Module
 *
 * Reusable service wrapping the official @anthropic-ai/sdk for Claude API interactions.
 * Provides four main capabilities:
 *   1. callClaudeSDK()  — Single-turn completions (drop-in replacement for callClaude())
 *   2. streamClaude()   — Streaming responses via Server-Sent Events
 *   3. chatClaude()     — Multi-turn conversations
 *   4. countClaudeTokens() — Token counting for prompt planning
 *
 * Integration with the existing aiProvider.ts:
 *   - callClaude() in aiProvider.ts delegates to callClaudeSDK() here
 *   - Returns the same AIResult interface so all 25+ pipeline callers are unaffected
 *   - Error objects carry a .status property so isRetryableError() and isKeySpecificError() work
 *
 * Security:
 *   - API keys are never logged in full (only last 4 chars via maskApiKey)
 *   - No SDK client is stored at module level — created per-call to support key rotation
 *   - maxRetries is set to 0 because retry/key-rotation is handled by tryKeysForProvider()
 */

import Anthropic from '@anthropic-ai/sdk';

// ============================================
// TYPE IMPORTS — reuse existing interfaces
// ============================================

// AIResult is the canonical return type across all providers.
// We import it from aiProvider to avoid duplication and stay in sync.
import { AIResult, maskApiKey } from '../utils/aiProvider';

// APIKeyEntry describes a single key with optional per-key model/url overrides.
// Used by tryKeysForProvider() to pass specific keys through the call chain.
import type { APIKeyEntry } from '../utils/aiProvider';

// ============================================
// CONFIGURATION
// ============================================

/** Default model used when no override is specified */
const DEFAULT_MODEL = 'claude-sonnet-4-6';

/** Maximum output tokens when not specified by caller */
const DEFAULT_MAX_TOKENS = 4096;

/** Allowed Claude model IDs for validation in route handlers */
export const ALLOWED_CLAUDE_MODELS = [
  'claude-sonnet-4-6',
  'claude-sonnet-4-5',
  'claude-sonnet-4-20250514',
  'claude-opus-4-8',
  'claude-opus-4-5',
  'claude-haiku-4-5',
  'claude-3-5-sonnet-20241022',
  'claude-3-5-haiku-20241022',
  'claude-3-opus-20240229',
] as const;

/** Static model metadata for the /models endpoint */
export const CLAUDE_MODELS_INFO = [
  { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', contextWindow: 200000, maxOutput: 64000, description: 'Best balance of speed and intelligence' },
  { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5', contextWindow: 200000, maxOutput: 16000, description: 'Previous generation Sonnet' },
  { id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4 (May 2025)', contextWindow: 200000, maxOutput: 16000, description: 'Snapshot of Claude Sonnet 4' },
  { id: 'claude-opus-4-8', name: 'Claude Opus 4.8', contextWindow: 1000000, maxOutput: 128000, description: 'Most capable model for complex tasks' },
  { id: 'claude-opus-4-5', name: 'Claude Opus 4.5', contextWindow: 200000, maxOutput: 32000, description: 'Previous generation Opus' },
  { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5', contextWindow: 200000, maxOutput: 64000, description: 'Fastest model for lightweight tasks' },
  { id: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', contextWindow: 200000, maxOutput: 8192, description: 'Legacy 3.5 Sonnet' },
  { id: 'claude-3-5-haiku-20241022', name: 'Claude 3.5 Haiku', contextWindow: 200000, maxOutput: 8192, description: 'Legacy 3.5 Haiku' },
  { id: 'claude-3-opus-20240229', name: 'Claude 3 Opus', contextWindow: 200000, maxOutput: 4096, description: 'Legacy Claude 3 Opus' },
];

// ============================================
// SDK CLIENT FACTORY
// ============================================

/**
 * Create an Anthropic SDK client instance for a given API key.
 *
 * We create a new client per call rather than using a module-level singleton
 * because tryKeysForProvider() rotates through multiple keys, and each key
 * may need a different client. Setting maxRetries=0 ensures our own retry
 * logic in aiProvider.ts is the sole retry mechanism.
 *
 * @param apiKey - The Anthropic API key (sk-ant-...)
 * @returns Configured Anthropic client
 */
function createClient(apiKey: string): Anthropic {
  return new Anthropic({
    apiKey,
    maxRetries: 0, // We handle retries in tryKeysForProvider()
  });
}

/**
 * Resolve the API key to use for a Claude call.
 * Priority: keyOverride.key > env CLAUDE_API_KEY
 * Throws if no key is available.
 */
async function resolveApiKey(keyOverride?: APIKeyEntry, userId?: string): Promise<string> {
  // keyOverride comes from tryKeysForProvider() — it's the specific key being tried
  if (keyOverride?.key) {
    return keyOverride.key;
  }

  // Fall back to environment variable
  if (process.env.CLAUDE_API_KEY) {
    return process.env.CLAUDE_API_KEY;
  }

  // Try DB config as last resort
  const { getAIConfig } = await import('../utils/aiProvider');
  const config = await getAIConfig(userId);
  if (config.CLAUDE_API_KEY) {
    return config.CLAUDE_API_KEY;
  }

  throw new Error('Claude API key not configured. Set CLAUDE_API_KEY in .env or add a key in Super Admin settings.');
}

/**
 * Resolve the model to use, with cascading priority:
 *   1. keyOverride.model (per-key model override from Super Admin)
 *   2. CLAUDE_MODEL env var
 *   3. Default (claude-sonnet-4-6)
 */
async function resolveModel(keyOverride?: APIKeyEntry): Promise<string> {
  if (keyOverride?.model) {
    return keyOverride.model;
  }

  if (process.env.CLAUDE_MODEL) {
    return process.env.CLAUDE_MODEL;
  }

  return DEFAULT_MODEL;
}

// ============================================
// JSON FORMAT HELPER
// ============================================

/**
 * Enhance the system prompt with JSON formatting instructions.
 * This mirrors the existing behavior in aiProvider.ts callClaude().
 */
function enhanceSystemPromptForJson(systemPrompt: string, format: string): string {
  if (format === 'json') {
    return `${systemPrompt}\n\nCRITICAL: You MUST respond with ONLY a valid JSON object. Do NOT respond with a bare array. Do NOT include any text before or after the JSON. Do NOT use markdown code fences. The entire response must be a single JSON object like {"key": "value"}.`;
  }
  return systemPrompt;
}

// ============================================
// ERROR MAPPING
// ============================================

/**
 * Map an Anthropic SDK error to a plain Error with a .status property.
 *
 * This is critical for compatibility with aiProvider.ts:
 *   - isKeySpecificError() checks for 401/403 status codes
 *   - isRetryableError() checks for 429/529/5xx status codes
 *
 * By assigning .status to the error object, the existing string-matching
 * checks AND the new numeric checks both work.
 */
function mapSDKError(err: unknown): never {
  if (err instanceof Anthropic.AuthenticationError) {
    // 401 — invalid API key
    const e = new Error(`Claude API error (401): ${err.message}`);
    (e as any).status = 401;
    throw e;
  }

  if (err instanceof Anthropic.PermissionDeniedError) {
    // 403 — key doesn't have access
    const e = new Error(`Claude API error (403): ${err.message}`);
    (e as any).status = 403;
    throw e;
  }

  if (err instanceof Anthropic.BadRequestError) {
    // 400 — malformed request, not retryable
    const e = new Error(`Claude API error (400): ${err.message}`);
    (e as any).status = 400;
    throw e;
  }

  if (err instanceof Anthropic.NotFoundError) {
    // 404 — model not found
    const e = new Error(`Claude API error (404): ${err.message}`);
    (e as any).status = 404;
    throw e;
  }

  if (err instanceof Anthropic.RateLimitError) {
    // 429 — rate limited, retryable
    const e = new Error(`Claude API error (429): ${err.message}`);
    (e as any).status = 429;
    throw e;
  }

  if (err instanceof Anthropic.InternalServerError) {
    // 500 — server error, retryable
    const e = new Error(`Claude API error (500): ${err.message}`);
    (e as any).status = 500;
    throw e;
  }

  if (err instanceof Anthropic.APIError) {
    // Generic SDK error — check for overloaded (529) and other status codes
    const status = err.status || 500;
    // 529 (overloaded) is a special case — retryable
    const e = new Error(`Claude API error (${status}): ${err.message}`);
    (e as any).status = status;
    throw e;
  }

  // Non-SDK error — re-throw as-is (could be AbortError, network error, etc.)
  throw err;
}

// ============================================
// MAIN EXPORTS
// ============================================

/**
 * Generate a single-turn completion using the Claude API.
 *
 * This is the primary integration point — callClaude() in aiProvider.ts
 * delegates to this function. It maintains the exact same function signature
 * and return type (AIResult) for seamless compatibility.
 *
 * @param prompt - The user message content
 * @param systemPrompt - System instructions for the model
 * @param maxTokens - Maximum tokens in the response
 * @param temperature - Sampling temperature (0-1), undefined uses model default
 * @param format - 'text' or 'json' (json adds formatting instructions to system prompt)
 * @param keyOverride - Specific API key to use (from tryKeysForProvider rotation)
 * @returns AIResult with content, model, provider, tokenUsage, latencyMs, keyUsed
 */
export async function callClaudeSDK(
  prompt: string,
  systemPrompt: string,
  maxTokens: number,
  temperature?: number,
  format: string = 'text',
  keyOverride?: APIKeyEntry,
  timeoutOverride?: number,
  userId?: string
): Promise<AIResult> {
  const callStartTime = Date.now();

  const apiKey = await resolveApiKey(keyOverride, userId);
  const model = await resolveModel(keyOverride);
  const client = createClient(apiKey);

  // Enhance system prompt for JSON format
  const enhancedSystemPrompt = enhanceSystemPromptForJson(systemPrompt, format);

  try {
    const response = await client.messages.create({
      model,
      max_tokens: maxTokens,
      messages: [{ role: 'user', content: prompt }],
      system: enhancedSystemPrompt,
      ...(temperature !== undefined && { temperature }),
    });

    const content = response.content
      .filter((block): block is Anthropic.TextBlock => block.type === 'text')
      .map(block => block.text)
      .join('');

    if (!content) {
      throw new Error('Claude returned empty content');
    }

    return {
      content,
      model: response.model,
      provider: 'claude',
      tokenUsage: {
        inputTokens: response.usage.input_tokens,
        outputTokens: response.usage.output_tokens,
        totalTokens: response.usage.input_tokens + response.usage.output_tokens,
      },
      latencyMs: Date.now() - callStartTime,
      finishReason: response.stop_reason || undefined,
      keyUsed: maskApiKey(apiKey),
    };
  } catch (err) {
    // If it's an SDK error, map it to a plain Error with .status
    if (err instanceof Anthropic.APIError || err instanceof Anthropic.AuthenticationError) {
      mapSDKError(err);
    }
    // Non-SDK errors (network, abort, etc.) — re-throw as-is
    throw err;
  }
}

/**
 * Stream a Claude response, calling onChunk for each text delta.
 *
 * Designed for Server-Sent Events (SSE) — the route handler passes a
 * callback that writes each chunk to the response stream.
 *
 * @param prompt - The user message content
 * @param systemPrompt - System instructions
 * @param maxTokens - Maximum response tokens
 * @param onChunk - Callback invoked for each text delta
 * @param temperature - Sampling temperature (0-1)
 * @param format - 'text' or 'json'
 * @param keyOverride - Specific API key to use
 * @returns AIResult with the full assembled content and usage stats
 */
export async function streamClaude(
  prompt: string,
  systemPrompt: string,
  maxTokens: number,
  onChunk: (text: string) => void,
  temperature?: number,
  format: string = 'text',
  keyOverride?: APIKeyEntry,
  userId?: string
): Promise<AIResult> {
  const callStartTime = Date.now();

  const apiKey = await resolveApiKey(keyOverride, userId);
  const model = await resolveModel(keyOverride);
  const client = createClient(apiKey);

  const enhancedSystemPrompt = enhanceSystemPromptForJson(systemPrompt, format);

  let fullContent = '';
  let inputTokens = 0;
  let outputTokens = 0;
  let stopReason: string | undefined;

  try {
    const stream = client.messages.stream({
      model,
      max_tokens: maxTokens,
      messages: [{ role: 'user', content: prompt }],
      system: enhancedSystemPrompt,
      ...(temperature !== undefined && { temperature }),
    });

    // Process each event from the stream
    for await (const event of stream) {
      if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
        const text = event.delta.text;
        fullContent += text;
        onChunk(text);
      }

      if (event.type === 'message_start') {
        inputTokens = event.message.usage.input_tokens;
      }

      if (event.type === 'message_delta') {
        outputTokens = event.usage.output_tokens;
        stopReason = event.delta.stop_reason ?? undefined;
      }
    }

    if (!fullContent) {
      throw new Error('Claude returned empty content in stream');
    }

    return {
      content: fullContent,
      model,
      provider: 'claude',
      tokenUsage: {
        inputTokens,
        outputTokens,
        totalTokens: inputTokens + outputTokens,
      },
      latencyMs: Date.now() - callStartTime,
      finishReason: stopReason || undefined,
      keyUsed: maskApiKey(apiKey),
    };
  } catch (err) {
    if (err instanceof Anthropic.APIError || err instanceof Anthropic.AuthenticationError) {
      mapSDKError(err);
    }
    throw err;
  }
}

/**
 * Multi-turn conversation with Claude.
 *
 * Accepts an array of message objects (role + content) to support
 * back-and-forth chat. The messages are passed directly to the SDK
 * in Anthropic MessageParam format.
 *
 * @param messages - Array of {role: 'user'|'assistant', content: string} messages
 * @param systemPrompt - System instructions
 * @param maxTokens - Maximum response tokens
 * @param temperature - Sampling temperature (0-1)
 * @param keyOverride - Specific API key to use
 * @returns AIResult with the assistant's response
 */
export async function chatClaude(
  messages: Array<{ role: 'user' | 'assistant'; content: string }>,
  systemPrompt?: string,
  maxTokens: number = DEFAULT_MAX_TOKENS,
  temperature?: number,
  keyOverride?: APIKeyEntry
): Promise<AIResult> {
  const callStartTime = Date.now();

  const apiKey = await resolveApiKey(keyOverride);
  const model = await resolveModel(keyOverride);
  const client = createClient(apiKey);

  try {
    const params: Anthropic.MessageCreateParams = {
      model,
      max_tokens: maxTokens,
      messages: messages.map(m => ({
        role: m.role,
        content: m.content,
      })),
      ...(systemPrompt && { system: systemPrompt }),
      ...(temperature !== undefined && { temperature }),
    };

    const response = await client.messages.create(params);

    const content = response.content
      .filter((block): block is Anthropic.TextBlock => block.type === 'text')
      .map(block => block.text)
      .join('');

    if (!content) {
      throw new Error('Claude returned empty content');
    }

    return {
      content,
      model: response.model,
      provider: 'claude',
      tokenUsage: {
        inputTokens: response.usage.input_tokens,
        outputTokens: response.usage.output_tokens,
        totalTokens: response.usage.input_tokens + response.usage.output_tokens,
      },
      latencyMs: Date.now() - callStartTime,
      finishReason: response.stop_reason || undefined,
      keyUsed: maskApiKey(apiKey),
    };
  } catch (err) {
    if (err instanceof Anthropic.APIError || err instanceof Anthropic.AuthenticationError) {
      mapSDKError(err);
    }
    throw err;
  }
}

/**
 * Count tokens for a set of messages without generating a response.
 *
 * Useful for estimating costs and checking if messages fit within
 * the model's context window before making a generation call.
 *
 * @param messages - Array of {role, content} messages to count tokens for
 * @param systemPrompt - Optional system prompt (adds to token count)
 * @param model - Model to count tokens for (defaults to claude-sonnet-4-6)
 * @param keyOverride - Specific API key to use
 * @returns Object with inputTokens count
 */
export async function countClaudeTokens(
  messages: Array<{ role: 'user' | 'assistant'; content: string }>,
  systemPrompt?: string,
  model?: string,
  keyOverride?: APIKeyEntry
): Promise<{ inputTokens: number; model: string }> {
  const apiKey = await resolveApiKey(keyOverride);
  const resolvedModel = model || await resolveModel(keyOverride);
  const client = createClient(apiKey);

  try {
    const result = await client.messages.countTokens({
      model: resolvedModel,
      messages: messages.map(m => ({
        role: m.role as 'user' | 'assistant',
        content: m.content,
      })),
      ...(systemPrompt && { system: systemPrompt }),
    });

    return {
      inputTokens: result.input_tokens,
      model: resolvedModel,
    };
  } catch (err) {
    if (err instanceof Anthropic.APIError || err instanceof Anthropic.AuthenticationError) {
      mapSDKError(err);
    }
    throw err;
  }
}