/**
 * Anthropic LlmProvider — wraps @anthropic-ai/sdk with uniform error classes.
 *
 * - 60s wall-clock timeout per call (provider-owned).
 * - Classifies SDK errors into provider error taxonomy.
 * - Cost math is inline (matches published Anthropic pricing tiers).
 */

import Anthropic from '@anthropic-ai/sdk';
import type { LlmProvider, NormalizedTextResult, TextRequest, ImageRequest } from './types';
import {
  ProviderRateLimitError,
  ProviderAuthError,
  ProviderFatalError,
  ProviderTransientError,
} from './types';
import type { AiCredentials } from '@/server/ai/credentials';

const DEFAULT_TIMEOUT_MS = 60_000;
const MAX_TOKENS_TEXT = 8192;
const MAX_TOKENS_IMAGE = 4096;

interface CallOpts {
  timeoutMs?: number;
}

// ─── Error mapper ─────────────────────────────────────────────────────────────

function mapError(err: unknown): never {
  const status = (err as { status?: number }).status;
  const msg = (err as { message?: string }).message ?? String(err);

  if (status === 429) throw new ProviderRateLimitError(msg);
  if (status === 529 || status === 500 || status === 503) throw new ProviderTransientError(msg);
  if (status === 401 || status === 403) throw new ProviderAuthError(msg);
  if (status === 400 || status === 404) throw new ProviderFatalError(msg);
  if (status !== undefined && status >= 500) throw new ProviderTransientError(msg);
  throw new ProviderFatalError(msg);
}

// ─── Timeout wrapper ──────────────────────────────────────────────────────────

function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
  return new Promise((resolve, reject) => {
    const t = setTimeout(() => reject(new ProviderTransientError(`timeout after ${ms}ms`)), ms);
    p.then(
      (v) => {
        clearTimeout(t);
        resolve(v);
      },
      (e) => {
        clearTimeout(t);
        reject(e);
      },
    );
  });
}

// ─── Cost math ────────────────────────────────────────────────────────────────

function pricePerMillionTokens(model: string): { in: number; out: number } | null {
  if (/claude-sonnet-4/.test(model)) return { in: 3.0, out: 15.0 };
  if (/claude-opus-4/.test(model)) return { in: 15.0, out: 75.0 };
  if (/claude-haiku/.test(model)) return { in: 0.8, out: 4.0 };
  if (/claude-3-5-sonnet/.test(model)) return { in: 3.0, out: 15.0 };
  if (/claude-3-5-haiku/.test(model)) return { in: 0.8, out: 4.0 };
  return null;
}

function computeCostUsd(
  model: string,
  promptTokens: number,
  completionTokens: number,
): number | null {
  const p = pricePerMillionTokens(model);
  if (!p) return null;
  return (promptTokens * p.in + completionTokens * p.out) / 1_000_000;
}

// ─── Response normalizer ──────────────────────────────────────────────────────

function extractText(raw: Anthropic.Message): string {
  const block = raw.content[0];
  if (block?.type === 'text') return block.text.trim();
  return '';
}

function normalize(model: string, raw: Anthropic.Message): NormalizedTextResult {
  const promptTokens = raw.usage.input_tokens ?? 0;
  const completionTokens = raw.usage.output_tokens ?? 0;
  const totalTokens = promptTokens + completionTokens;

  return {
    text: extractText(raw),
    model,
    usage: { promptTokens, completionTokens, totalTokens },
    costUsd: computeCostUsd(model, promptTokens, completionTokens),
    rawResponse: raw,
  };
}

// ─── ArrayBuffer → base64 ─────────────────────────────────────────────────────

type AnthropicImageMime = 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp';

function toAnthropicImageMime(mime: string): AnthropicImageMime {
  if (
    mime === 'image/jpeg' ||
    mime === 'image/png' ||
    mime === 'image/gif' ||
    mime === 'image/webp'
  ) {
    return mime;
  }
  return 'image/jpeg';
}

function arrayBufferToBase64(buf: ArrayBuffer): string {
  const bytes = new Uint8Array(buf);
  let binary = '';
  const chunkSize = 8192;
  for (let i = 0; i < bytes.length; i += chunkSize) {
    binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
  }
  return btoa(binary);
}

// ─── Call implementations ─────────────────────────────────────────────────────

async function callText(
  creds: AiCredentials,
  req: TextRequest,
  opts: CallOpts = {},
): Promise<NormalizedTextResult> {
  const client = new Anthropic({ apiKey: creds.apiKey });
  try {
    const raw = await withTimeout(
      client.messages.create({
        model: req.model,
        max_tokens: MAX_TOKENS_TEXT,
        stream: false,
        messages: [{ role: 'user', content: req.prompt }],
      }),
      opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
    );
    return normalize(req.model, raw);
  } catch (err) {
    if (
      err instanceof ProviderTransientError ||
      err instanceof ProviderRateLimitError ||
      err instanceof ProviderAuthError ||
      err instanceof ProviderFatalError
    ) {
      throw err;
    }
    mapError(err);
  }
}

async function callImage(
  creds: AiCredentials,
  req: ImageRequest,
  opts: CallOpts = {},
): Promise<NormalizedTextResult> {
  const client = new Anthropic({ apiKey: creds.apiKey });
  const b64 = arrayBufferToBase64(req.imageBytes);
  try {
    const raw = await withTimeout(
      client.messages.create({
        model: req.model,
        max_tokens: MAX_TOKENS_IMAGE,
        stream: false,
        messages: [
          {
            role: 'user',
            content: [
              {
                type: 'image',
                source: {
                  type: 'base64',
                  media_type: toAnthropicImageMime(req.mime),
                  data: b64,
                },
              },
              { type: 'text', text: req.prompt },
            ],
          },
        ],
      }),
      opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
    );
    return normalize(req.model, raw);
  } catch (err) {
    if (
      err instanceof ProviderTransientError ||
      err instanceof ProviderRateLimitError ||
      err instanceof ProviderAuthError ||
      err instanceof ProviderFatalError
    ) {
      throw err;
    }
    mapError(err);
  }
}

// ─── Provider export ──────────────────────────────────────────────────────────

export const anthropicProvider: LlmProvider = {
  id: 'anthropic',
  supportsImage: true,
  generateText: callText,
  generateTextWithImage: callImage,
};
