/**
 * Pluggable LLM rewrite client.
 *
 * Provider interface so we can swap Anthropic ↔ Ollama ↔ OpenRouter without
 * rewriting the orchestrator. Default provider is "anthropic" using
 * claude-opus-4-7 with prompt caching on the system block.
 */

import type { Persona } from "./personas";
import { buildSystemPrompt, BANNED_AI_TELLS, STYLE_GUIDE } from "./persona-prompts";

export interface RewriteRequest {
  persona: Persona;
  rawTitle: string;
  rawContent: string;
}

export interface RewriteResult {
  title: string;
  excerpt: string;
  body: string;
  category: string;
  tags: string[];
  model: string;
  inputTokens: number;
  outputTokens: number;
  cacheReadTokens: number;
  cacheCreationTokens: number;
}

export interface RewriteProvider {
  name: string;
  rewrite(req: RewriteRequest): Promise<RewriteResult>;
}

const ANTHROPIC_API = "https://api.anthropic.com/v1/messages";
const DEFAULT_MODEL = process.env.BB_REWRITE_MODEL || "claude-opus-4-7";
const MAX_OUTPUT_TOKENS = 4096;

function parseJsonFromText(text: string): any {
  const trimmed = text.trim();
  const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
  const candidate = fenced ? fenced[1] : trimmed;
  return JSON.parse(candidate);
}

export const anthropicProvider: RewriteProvider = {
  name: "anthropic",

  async rewrite(req: RewriteRequest): Promise<RewriteResult> {
    const apiKey = process.env.ANTHROPIC_API_KEY;
    if (!apiKey) throw new Error("ANTHROPIC_API_KEY not set in env");

    const systemBlock = buildSystemPrompt(req.persona);

    const userPrompt = [
      `Below is a raw press release submitted by an industry contact. Rewrite it as a journalism piece in the voice of ${req.persona.name} (${req.persona.beat}).`,
      "",
      "Output STRICTLY as a JSON object with these keys:",
      `  "title"     (string — your rewritten headline, NOT the original press-release headline)`,
      `  "excerpt"   (string — 1-2 sentence dek, max 240 chars)`,
      `  "body"      (string — HTML <p>, <h2>, <ul>, <strong>, <em>, <a> only; no <script>; no <style>; 400-1200 words for a typical PR)`,
      `  "category"  (string — one of: News, Live Production, Streaming, Post Production, Sports, Business, Technology)`,
      `  "tags"      (array of 3-7 short string tags)`,
      "",
      "Hard rules:",
      `  - Do NOT use any of these AI-tell words: ${BANNED_AI_TELLS.slice(0, 30).join(", ")} … (full list in your system prompt).`,
      "  - Do not invent quotes that are not in the source. Paraphrase real quotes; use direct quotes only when they appear verbatim in the source.",
      "  - Do not invent dates, dollar figures, or specs not in the source.",
      "  - If the source is thin, write a SHORT article (300-450 words). Do not pad.",
      "  - Lead with the news, not the company. Inverted pyramid.",
      "  - Subhead the article every 200-300 words with <h2>.",
      "",
      "RAW PRESS RELEASE BEGINS:",
      `Title: ${req.rawTitle}`,
      "",
      req.rawContent,
      "RAW PRESS RELEASE ENDS.",
      "",
      "Now produce the JSON.",
    ].join("\n");

    const body = {
      model: DEFAULT_MODEL,
      max_tokens: MAX_OUTPUT_TOKENS,
      system: [
        {
          type: "text",
          text: systemBlock,
          cache_control: { type: "ephemeral" },
        },
        {
          type: "text",
          text: STYLE_GUIDE,
          cache_control: { type: "ephemeral" },
        },
      ],
      messages: [{ role: "user", content: userPrompt }],
    };

    const res = await fetch(ANTHROPIC_API, {
      method: "POST",
      headers: {
        "x-api-key": apiKey,
        "anthropic-version": "2023-06-01",
        "content-type": "application/json",
      },
      body: JSON.stringify(body),
    });

    if (!res.ok) {
      const errText = await res.text();
      throw new Error(`Anthropic API ${res.status}: ${errText.slice(0, 500)}`);
    }

    const data: any = await res.json();
    const text = (data.content?.[0]?.text || "").toString();
    let parsed: any;
    try {
      parsed = parseJsonFromText(text);
    } catch (e: any) {
      throw new Error(`Could not parse model output as JSON: ${e.message}; head=${text.slice(0, 300)}`);
    }

    if (!parsed.title || !parsed.body) {
      throw new Error("Model output missing required 'title' or 'body'");
    }

    return {
      title: String(parsed.title).trim(),
      excerpt: String(parsed.excerpt || "").trim(),
      body: String(parsed.body).trim(),
      category: String(parsed.category || "News").trim(),
      tags: Array.isArray(parsed.tags) ? parsed.tags.map((t: any) => String(t)) : [],
      model: data.model || DEFAULT_MODEL,
      inputTokens: data.usage?.input_tokens ?? 0,
      outputTokens: data.usage?.output_tokens ?? 0,
      cacheReadTokens: data.usage?.cache_read_input_tokens ?? 0,
      cacheCreationTokens: data.usage?.cache_creation_input_tokens ?? 0,
    };
  },
};

// ─── Ollama provider ──────────────────────────────────────────────────────────
//
// Talks to a local Ollama instance via the OpenAI-compatible
// /v1/chat/completions endpoint. Env:
//   OLLAMA_ENDPOINT          e.g. http://10.10.0.66:11434  (NO trailing /v1)
//   OLLAMA_MODEL_DEFAULT     e.g. llama3.1:70b
//   OLLAMA_API_KEY           optional bearer
//   BB_REWRITE_MODEL         overrides the model just for the rewriter
export const ollamaProvider: RewriteProvider = {
  name: "ollama",

  async rewrite(req: RewriteRequest): Promise<RewriteResult> {
    const primary = (process.env.OLLAMA_ENDPOINT || "").replace(/\/$/, "");
    if (!primary) throw new Error("OLLAMA_ENDPOINT not set in env");
    const fallback = (process.env.OLLAMA_FALLBACK_ENDPOINT || "").replace(/\/$/, "");
    const endpoints = [primary, fallback].filter(Boolean);
    const model =
      process.env.BB_REWRITE_MODEL ||
      process.env.OLLAMA_MODEL_DEFAULT ||
      "llama3.1:70b";

    const systemBlock =
      buildSystemPrompt(req.persona) + "\n\n=== STYLE GUIDE ===\n" + STYLE_GUIDE;

    const userPrompt = [
      `Below is a raw press release submitted by an industry contact. Rewrite it as a journalism piece in the voice of ${req.persona.name} (${req.persona.beat}).`,
      "",
      "Output STRICTLY as a JSON object with these keys (no markdown fences, no commentary):",
      `  "title"     (string — your rewritten headline, NOT the original press-release headline)`,
      `  "excerpt"   (string — 1-2 sentence dek, max 240 chars)`,
      `  "body"      (string — HTML <p>, <h2>, <ul>, <strong>, <em>, <a> only; no <script>; no <style>; 400-1200 words for a typical PR)`,
      `  "category"  (string — one of: News, Live Production, Streaming, Post Production, Sports, Business, Technology)`,
      `  "tags"      (array of 3-7 short string tags)`,
      "",
      "Hard rules:",
      `  - Do NOT use any of these AI-tell words: ${BANNED_AI_TELLS.slice(0, 30).join(", ")} … (full list in your system prompt).`,
      "  - Do not invent quotes that are not in the source. Paraphrase real quotes; use direct quotes only when they appear verbatim in the source.",
      "  - Do not invent dates, dollar figures, or specs not in the source.",
      "  - If the source is thin, write a SHORT article (300-450 words). Do not pad.",
      "  - Lead with the news, not the company. Inverted pyramid.",
      "  - Subhead the article every 200-300 words with <h2>.",
      "",
      "RAW PRESS RELEASE BEGINS:",
      `Title: ${req.rawTitle}`,
      "",
      req.rawContent,
      "RAW PRESS RELEASE ENDS.",
      "",
      "Now produce the JSON.",
    ].join("\n");

    const headers: Record<string, string> = {
      "content-type": "application/json",
    };
    const apiKey = process.env.OLLAMA_API_KEY;
    if (apiKey) headers["authorization"] = `Bearer ${apiKey}`;

    // Retry-with-backoff + multi-endpoint failover. We try primary first;
    // if it returns 503/busy/timeout, we immediately try the fallback before
    // burning a backoff sleep. Each round trip through all endpoints counts
    // as one "attempt" against RETRY_DELAYS_MS. Net effect: when AI_002 is
    // saturated, AI_001 picks up the slack with no delay.
    const RETRY_DELAYS_MS = [0, 8000, 30000, 90000]; // between rounds
    let res: Response | null = null;
    let lastErr = "";
    outer: for (let i = 0; i < RETRY_DELAYS_MS.length; i++) {
      if (RETRY_DELAYS_MS[i] > 0) await new Promise((r) => setTimeout(r, RETRY_DELAYS_MS[i]));
      for (const endpoint of endpoints) {
        try {
          res = await fetch(`${endpoint}/v1/chat/completions`, {
            method: "POST",
            headers,
            body: JSON.stringify({
              model,
              messages: [
                { role: "system", content: systemBlock },
                { role: "user", content: userPrompt },
              ],
              stream: false,
              response_format: { type: "json_object" },
              temperature: 0.6,
              max_tokens: MAX_OUTPUT_TOKENS,
            }),
            signal: AbortSignal.timeout(180_000),
          });
          if (res.ok) break outer;
          const txt = await res.text().catch(() => "");
          lastErr = `Ollama API ${res.status} @ ${endpoint}: ${txt.slice(0, 400)}`;
          // 4xx other than 429 is a permanent error — no point hitting fallback
          if (res.status >= 400 && res.status < 500 && res.status !== 429) {
            throw new Error(lastErr);
          }
          // Retryable — fall through to next endpoint within the same round
        } catch (e: any) {
          lastErr = `${e?.message || String(e)} @ ${endpoint}`;
        }
      }
      if (i === RETRY_DELAYS_MS.length - 1) throw new Error(lastErr || "Ollama request failed after retries across all endpoints");
    }
    if (!res || !res.ok) throw new Error(lastErr || "Ollama request failed");

    const data: any = await res.json();
    const text = (data?.choices?.[0]?.message?.content || "").toString();
    let parsed: any;
    try {
      parsed = parseJsonFromText(text);
    } catch (e: any) {
      throw new Error(
        `Could not parse Ollama output as JSON: ${e.message}; head=${text.slice(0, 300)}`
      );
    }

    if (!parsed.title || !parsed.body) {
      throw new Error("Ollama output missing required 'title' or 'body'");
    }

    return {
      title: String(parsed.title).trim(),
      excerpt: String(parsed.excerpt || "").trim(),
      body: String(parsed.body).trim(),
      category: String(parsed.category || "News").trim(),
      tags: Array.isArray(parsed.tags) ? parsed.tags.map((t: any) => String(t)) : [],
      model: data?.model || model,
      inputTokens: data?.usage?.prompt_tokens ?? 0,
      outputTokens: data?.usage?.completion_tokens ?? 0,
      // Ollama doesn't surface a cache-token concept; report zeros.
      cacheReadTokens: 0,
      cacheCreationTokens: 0,
    };
  },
};

export const openrouterProvider: RewriteProvider = {
  name: "openrouter",
  async rewrite(): Promise<RewriteResult> {
    throw new Error("openrouter provider not implemented yet — set BB_REWRITE_PROVIDER=anthropic");
  },
};

// Default provider is Ollama. Anthropic stays available for explicit opt-in
// via BB_REWRITE_PROVIDER=anthropic, but we no longer route the daily jobs
// at it because the billing account is empty. See ai_rewrite_jobs.failure_reason
// — every attempt since 2026-01-26 was a 400 "credit balance too low".
export function getProvider(name?: string): RewriteProvider {
  const want = (name || process.env.BB_REWRITE_PROVIDER || "ollama").toLowerCase();
  if (want === "anthropic") return anthropicProvider;
  if (want === "ollama") return ollamaProvider;
  if (want === "openrouter") return openrouterProvider;
  throw new Error(`Unknown rewrite provider: ${want}`);
}
