/**
 * LLM Client for the AI Avatar
 * -------------------------------
 * Sends the built prompt + messages to a real LLM and returns the response.
 *
 * Priority order:
 *   1. Anthropic (Claude) — if ANTHROPIC_API_KEY is set
 *   2. OpenAI (GPT)       — if OPENAI_API_KEY is set
 *   3. Mock fallback      — if neither key is set
 *
 * Both APIs are called with plain fetch() — no SDK needed.
 * This keeps dependencies minimal and the code simple.
 */

import { log } from "@/lib/logger";
import { trackAiUsage } from "@/lib/services/ai-usage-tracker";

// ─── Types ───────────────────────────────────────────────────────────────────

interface LlmMessage {
  role: "system" | "user" | "assistant";
  content: string;
}

interface LlmResult {
  reply: string;
  provider: "anthropic" | "openai" | "mock";
  model: string;
  tokensUsed?: number;
}

export interface LlmOptions {
  /** Max tokens to generate. Default 1024. Use 6000-8000 for artifact generation. */
  maxTokens?: number;
}

// ─── Provider detection ──────────────────────────────────────────────────────

function getProvider(): "anthropic" | "openai" | "mock" {
  // Trim guards against env vars set as " sk-ant-..." with stray whitespace
  // — Vercel's UI sometimes preserves trailing newlines on paste.
  const anthropicKey = process.env.ANTHROPIC_API_KEY?.trim();
  const openaiKey = process.env.OPENAI_API_KEY?.trim();
  if (anthropicKey) return "anthropic";
  if (openaiKey) return "openai";
  return "mock";
}

// ─── Anthropic (Claude) ──────────────────────────────────────────────────────

/**
 * Thrown when Anthropic returns 529 Overloaded on both the initial attempt
 * and the 3s retry. Used as a tagged signal so callLlm can distinguish
 * "transient capacity issue — try OpenAI instead" from real errors
 * (auth, malformed request, etc.) without string-matching on error.message.
 */
class AnthropicOverloadedError extends Error {
  readonly status = 529;
  constructor(message: string) {
    super(message);
    this.name = "AnthropicOverloadedError";
  }
}

async function callAnthropic(messages: LlmMessage[], maxTokens: number): Promise<LlmResult> {
  const apiKey = process.env.ANTHROPIC_API_KEY!.trim();
  const model = process.env.AI_MODEL_ID || "claude-sonnet-4-20250514";

  // Anthropic API separates system prompt from messages
  const systemMsg = messages.find(m => m.role === "system");
  const chatMessages = messages
    .filter(m => m.role !== "system")
    .map(m => ({ role: m.role, content: m.content }));

  const requestBody = JSON.stringify({
    model,
    max_tokens: maxTokens,
    system: systemMsg?.content ?? "",
    messages: chatMessages,
  });

  async function send() {
    return fetch("https://api.anthropic.com/v1/messages", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-api-key": apiKey,
        "anthropic-version": "2023-06-01",
      },
      body: requestBody,
    });
  }

  let res = await send();

  // 529 = Overloaded. Brief single retry after 3s catches transient
  // capacity blips without doubling up on real outages. Only retry once —
  // if Anthropic is still overloaded, surface the error to the caller.
  if (res.status === 529) {
    console.info("[LLM-DEBUG] Anthropic 529 Overloaded — retrying in 3s");
    await new Promise((r) => setTimeout(r, 3000));
    res = await send();
  }

  if (!res.ok) {
    const errBody = await res.text();
    // Try to surface Anthropic's structured error message when available so
    // the catch block + UI can show "invalid x-api-key" / "rate_limit_error"
    // rather than a generic 4xx blob.
    let msg = errBody;
    try {
      const parsed = JSON.parse(errBody);
      if (parsed?.error?.message) msg = parsed.error.message;
      else if (parsed?.error?.type) msg = parsed.error.type;
    } catch { /* keep raw text */ }
    console.error("[LLM-DEBUG] Anthropic API error", { status: res.status, message: msg });
    // Tag 529s so callLlm can recognise them and fall through to OpenAI
    // when that provider is configured.
    if (res.status === 529) {
      throw new AnthropicOverloadedError(`Anthropic API error (529): ${msg}`);
    }
    throw new Error(`Anthropic API error (${res.status}): ${msg}`);
  }

  const data = await res.json();
  const reply = data.content?.[0]?.text ?? "";
  const tokensUsed = (data.usage?.input_tokens ?? 0) + (data.usage?.output_tokens ?? 0);

  return { reply, provider: "anthropic", model, tokensUsed };
}

// ─── OpenAI (GPT) ────────────────────────────────────────────────────────────

/**
 * Extra system-prompt preamble shown only to GPT models. Empirical finding:
 * when the strategist artifact path falls through from Anthropic to OpenAI,
 * GPT-4o generates prose + empty section headers instead of filling in
 * Recharts components. Spelling out the exact import list + a concrete
 * ComposedChart template up front recovers chart fidelity on the fallback
 * path without adding noise to the Claude system prompt (Claude doesn't
 * need the hand-holding).
 */
const OPENAI_CHART_PREAMBLE = `IMPORTANT: You MUST generate working Recharts components. Import from "recharts": AreaChart, Area, ComposedChart, Bar, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend.

Every dashboard MUST include these actual chart components — not just text headers. Example:
<ResponsiveContainer width="100%" height={300}>
  <ComposedChart data={data}>
    <CartesianGrid strokeDasharray="3 3" stroke="#1E293B" />
    <XAxis dataKey="date" tick={{fill:"#94A3B8",fontSize:11}} />
    <YAxis tick={{fill:"#94A3B8",fontSize:11}} />
    <Bar dataKey="clicks" fill="#3B82F6" barSize={8} radius={[3,3,0,0]} />
    <Line type="monotone" dataKey="impressions" stroke="#F59E0B" strokeWidth={2} yAxisId="right" />
  </ComposedChart>
</ResponsiveContainer>

Generate at least 2 charts with real data arrays. Do NOT leave chart sections empty.

`;

async function callOpenAI(messages: LlmMessage[], maxTokens: number): Promise<LlmResult> {
  const apiKey = process.env.OPENAI_API_KEY!.trim();
  // Default bumped from gpt-4o-mini → gpt-4o. Mini consistently produced
  // text-only stubs when asked for a strategist artifact, and the cost
  // delta is small relative to the UX hit of empty dashboards. Set
  // OPENAI_MODEL_ID explicitly to override.
  const model = process.env.OPENAI_MODEL_ID || "gpt-4o";

  // Strengthen the system prompt for GPT — it needs an explicit
  // "these are the imports, here's an example, do not leave it empty"
  // nudge that Claude does not. The preamble is prepended to the
  // existing system message if one is present, or inserted as a new
  // system message otherwise.
  const augmented: LlmMessage[] = messages.map((m) => {
    if (m.role !== "system") return m;
    return { ...m, content: OPENAI_CHART_PREAMBLE + m.content };
  });
  if (!augmented.some((m) => m.role === "system")) {
    augmented.unshift({ role: "system", content: OPENAI_CHART_PREAMBLE });
  }

  // GPT-4o is verbose in JSX and truncates more easily at the Anthropic-
  // sized budget, so bump the floor to 8192 for the fallback path.
  // Respect caller overrides that are already higher.
  const effectiveMaxTokens = Math.max(maxTokens, 8192);

  const res = await fetch("https://api.openai.com/v1/chat/completions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${apiKey}`,
    },
    body: JSON.stringify({
      model,
      max_tokens: effectiveMaxTokens,
      messages: augmented.map(m => ({ role: m.role, content: m.content })),
    }),
  });

  if (!res.ok) {
    const err = await res.text();
    throw new Error(`OpenAI API error (${res.status}): ${err}`);
  }

  const data = await res.json();
  const reply = data.choices?.[0]?.message?.content ?? "";
  const tokensUsed = data.usage?.total_tokens ?? 0;

  return { reply, provider: "openai", model, tokensUsed };
}

// ─── Public API ──────────────────────────────────────────────────────────────

/**
 * Send messages to the best available LLM and return the response.
 * Falls back to mock if no API key is configured.
 *
 * @param messages - The full message array (system + history + user)
 * @param mockFallback - Function that generates a mock response
 */
export async function callLlm(
  messages: LlmMessage[],
  mockFallback: () => string,
  options: LlmOptions = {},
): Promise<LlmResult> {
  // Diagnostic log: confirms whether the API key is reaching the runtime at
  // all. Vercel preview-only scope, missing redeploy, or a typo in the env
  // var name would all surface here as `hasAnthropicKey: false`. Logs the
  // first 10 chars of the key so we can spot truncation / wrong-account
  // copy-paste without exposing the secret.
  const rawAnthropic = process.env.ANTHROPIC_API_KEY;
  const rawOpenai = process.env.OPENAI_API_KEY;
  console.log("[LLM-DEBUG] env state", {
    hasAnthropicKey: !!rawAnthropic?.trim(),
    anthropicKeyPrefix: rawAnthropic?.trim().substring(0, 10) ?? null,
    hasOpenAIKey: !!rawOpenai?.trim(),
    openaiKeyPrefix: rawOpenai?.trim().substring(0, 7) ?? null,
    nodeEnv: process.env.NODE_ENV,
    vercelEnv: process.env.VERCEL_ENV ?? null,
  });

  const provider = getProvider();
  const maxTokens = options.maxTokens ?? 1024;

  if (provider === "mock") {
    log.warn("avatar", "[AUDIT] No LLM API key configured — MOCK MODE. Artifacts will NOT be generated.", {
      metadata: { provider: "mock", hasAnthropicKey: !!rawAnthropic?.trim(), hasOpenAIKey: !!rawOpenai?.trim(), vercelEnv: process.env.VERCEL_ENV ?? null },
    });
    return {
      reply: mockFallback() + "\n\n---\n*This is a mock response. Add ANTHROPIC_API_KEY or OPENAI_API_KEY to .env.local for real AI responses.*",
      provider: "mock",
      model: "mock",
    };
  }

  try {
    log.info("avatar", `[AUDIT] Calling ${provider} LLM: model=${provider === "anthropic" ? (process.env.AI_MODEL_ID || "claude-sonnet-4-20250514") : (process.env.OPENAI_MODEL_ID || "gpt-4o-mini")}, maxTokens=${maxTokens}, messages=${messages.length}`, {
      metadata: { provider, messageCount: messages.length, maxTokens },
    });

    let result: LlmResult;
    try {
      result = provider === "anthropic"
        ? await callAnthropic(messages, maxTokens)
        : await callOpenAI(messages, maxTokens);
    } catch (inner) {
      // Provider-level fallback: if Anthropic is Overloaded (529 after a
      // 3s retry) and OpenAI is configured, quietly run the same request
      // through OpenAI rather than failing the user. Any other error from
      // Anthropic (auth, rate limit, malformed input) still bubbles.
      if (inner instanceof AnthropicOverloadedError && !!rawOpenai?.trim()) {
        console.info("[LLM] Anthropic 529, falling back to OpenAI");
        result = await callOpenAI(messages, maxTokens);
      } else {
        throw inner;
      }
    }

    log.info("avatar", `LLM response received (${result.tokensUsed ?? "?"} tokens)`, {
      metadata: { provider: result.provider, model: result.model, tokens: result.tokensUsed },
    });

    // Fire-and-forget usage tracking
    trackAiUsage({
      provider: result.provider, model: result.model, feature: "general",
      inputTokens: Math.round((result.tokensUsed ?? 0) * 0.6),
      outputTokens: Math.round((result.tokensUsed ?? 0) * 0.4),
    });

    return result;
  } catch (err) {
    const errMsg = err instanceof Error ? err.message : "LLM call failed";
    console.error("[LLM-DEBUG] LLM call threw", { provider, errMsg });
    log.error("avatar", `LLM error: ${errMsg}`, {
      metadata: { provider, error: errMsg },
    });

    // Surface the actual error to the user instead of silently masquerading
    // as a "cached response". This keeps the UX honest — invalid API key,
    // rate limit, or model deprecation now show up in the chat reply
    // itself, not just in server logs.
    return {
      reply: mockFallback() + `\n\n---\n*AI service error (${provider}): ${errMsg}*`,
      provider: "mock",
      model: "mock-fallback",
    };
  }
}

/**
 * Check which LLM provider is available.
 * Useful for the frontend to show which AI is active.
 */
export function getLlmStatus(): { provider: string; configured: boolean } {
  const provider = getProvider();
  return { provider, configured: provider !== "mock" };
}
