/**
 * Gemini LlmProvider — wraps @google/genai 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 plan's pricePerMillionTokens table).
 */

import { GoogleGenAI } from '@google/genai';
import type { LlmProvider, NormalizedTextResult, TextRequest, ImageRequest } from './types';
import {
  ProviderRateLimitError,
  ProviderQuotaExhaustedError,
  ProviderAuthError,
  ProviderFatalError,
  ProviderTransientError,
} from './types';
import type { AiCredentials } from '@/server/ai/credentials';

const DEFAULT_TIMEOUT_MS = 60_000;

interface CallOpts {
  timeoutMs?: number;
}

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

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

  if (status === 429) {
    if (/RESOURCE_EXHAUSTED|quota/i.test(msg)) throw new ProviderQuotaExhaustedError(msg);
    throw new ProviderRateLimitError(msg);
  }
  if (status === 401 || status === 403) throw new ProviderAuthError(msg);
  if (status === 400 || status === 404) throw new ProviderFatalError(msg);
  if (
    code === 'ECONNRESET' ||
    code === 'ETIMEDOUT' ||
    code === 'ENOTFOUND' ||
    (status !== undefined && status >= 500)
  ) {
    throw new ProviderTransientError(msg);
  }
  throw new ProviderTransientError(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 {
  // Pricing as of 2026-05; update as Google publishes new tiers.
  if (/gemini-2\.5-flash/.test(model)) return { in: 0.3, out: 2.5 };
  if (/gemini-2\.5-pro/.test(model)) return { in: 1.25, out: 10.0 };
  if (/gemini-1\.5-flash/.test(model)) return { in: 0.075, out: 0.3 };
  if (/gemini-3/.test(model)) return { in: 0.075, out: 0.3 }; // placeholder same tier as 1.5-flash
  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 normalize(model: string, raw: unknown): NormalizedTextResult {
  const r = raw as {
    text?: string;
    usageMetadata?: {
      promptTokenCount?: number;
      candidatesTokenCount?: number;
      totalTokenCount?: number;
    };
  };
  const promptTokens = r.usageMetadata?.promptTokenCount ?? 0;
  const completionTokens = r.usageMetadata?.candidatesTokenCount ?? 0;
  const totalTokens = r.usageMetadata?.totalTokenCount ?? promptTokens + completionTokens;

  return {
    text: r.text?.trim() ?? '',
    model,
    usage: { promptTokens, completionTokens, totalTokens },
    costUsd: computeCostUsd(model, promptTokens, completionTokens),
    rawResponse: raw,
  };
}

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

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 GoogleGenAI({ apiKey: creds.apiKey });
  try {
    const raw = await withTimeout(
      client.models.generateContent({ model: req.model, contents: req.prompt }),
      opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
    );
    return normalize(req.model, raw);
  } catch (err) {
    if (err instanceof ProviderTransientError) throw err;
    mapError(err);
  }
}

async function callImage(
  creds: AiCredentials,
  req: ImageRequest,
  opts: CallOpts = {},
): Promise<NormalizedTextResult> {
  const client = new GoogleGenAI({ apiKey: creds.apiKey });
  const b64 = arrayBufferToBase64(req.imageBytes);
  try {
    const raw = await withTimeout(
      client.models.generateContent({
        model: req.model,
        contents: [{ text: req.prompt }, { inlineData: { mimeType: req.mime, data: b64 } }],
      }),
      opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
    );
    return normalize(req.model, raw);
  } catch (err) {
    if (err instanceof ProviderTransientError) throw err;
    mapError(err);
  }
}

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

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