import { eq } from "drizzle-orm"
import type OpenAI from "openai"
import { db } from "@/lib/db"
import { app_settings } from "@/lib/db/schema"
import { openai } from "@/lib/ai/client"
import { getAnthropic } from "@/lib/ai/anthropic"

// ============================================================================
// AI provider abstraction — one call site for every AI feature, backed by
// EITHER OpenAI or Anthropic (Claude). The active provider is chosen by an admin
// in Settings → System (persisted in app_settings), falling back to whichever
// provider actually has an API key configured on the server.
// ============================================================================

export type AiProvider = "openai" | "anthropic"
export type AiRole = "system" | "user" | "assistant"
export type AiMessage = { role: AiRole; content: string }

export const AI_PROVIDER_KEY = "ai_provider"

// Models are env-overridable. Both default to each provider's cheapest tier to
// keep token spend low: OpenAI gpt-4o-mini, Anthropic Claude Haiku 4.5 ($1/$5
// per 1M). Pin a stronger model via OPENAI_MODEL / ANTHROPIC_MODEL if desired.
export const OPENAI_MODEL = process.env.OPENAI_MODEL ?? "gpt-4o-mini"
export const ANTHROPIC_MODEL = process.env.ANTHROPIC_MODEL ?? "claude-haiku-4-5"

export function openaiConfigured(): boolean {
  return Boolean(process.env.OPENAI_API_KEY)
}
export function anthropicConfigured(): boolean {
  return Boolean(process.env.ANTHROPIC_API_KEY)
}

function isProvider(v: unknown): v is AiProvider {
  return v === "openai" || v === "anthropic"
}

/** The admin-selected provider (defaults to openai). Fails safe to openai. */
export async function getAiProvider(): Promise<AiProvider> {
  try {
    const row = await db.query.app_settings.findFirst({
      where: eq(app_settings.key, AI_PROVIDER_KEY),
    })
    const v = (row?.value as { provider?: string } | null)?.provider
    return isProvider(v) ? v : "openai"
  } catch {
    return "openai"
  }
}

/** Persist the admin's provider choice. Admin-gated by the calling action. */
export async function setAiProvider(provider: AiProvider): Promise<void> {
  await db
    .insert(app_settings)
    .values({ key: AI_PROVIDER_KEY, value: { provider } })
    .onConflictDoUpdate({
      target: app_settings.key,
      set: { value: { provider }, updated_at: new Date().toISOString() },
    })
}

/**
 * The provider actually used for a request: the selected one, unless it has no
 * API key on the server and the other provider does — then we fall back so AI
 * features keep working after a provider switch even if the key isn't set yet.
 */
function resolveEffective(selected: AiProvider): AiProvider {
  if (selected === "anthropic" && !anthropicConfigured() && openaiConfigured()) return "openai"
  if (selected === "openai" && !openaiConfigured() && anthropicConfigured()) return "anthropic"
  return selected
}

/** Everything the admin UI needs to render the provider picker. */
export async function aiProviderStatus() {
  const selected = await getAiProvider()
  return {
    selected,
    effective: resolveEffective(selected),
    openaiConfigured: openaiConfigured(),
    anthropicConfigured: anthropicConfigured(),
    openaiModel: OPENAI_MODEL,
    anthropicModel: ANTHROPIC_MODEL,
  }
}

/** Strip a ```json fenced code block, if the model wrapped its JSON in one. */
function stripFences(s: string): string {
  return s
    .replace(/^\s*```(?:json)?\s*/i, "")
    .replace(/```\s*$/i, "")
    .trim()
}

/**
 * Provider-agnostic single-shot completion. Returns the model's text output.
 *
 * `json: true` asks for a JSON object (OpenAI uses response_format; both
 * providers rely on the prompt saying "JSON only") and strips any code fences
 * so the caller can `JSON.parse` the result directly.
 */
export async function aiComplete(opts: {
  messages: AiMessage[]
  maxTokens?: number
  json?: boolean
}): Promise<string> {
  const provider = resolveEffective(await getAiProvider())
  const maxTokens = opts.maxTokens ?? 1024

  let text: string
  if (provider === "anthropic") {
    // Anthropic takes a top-level `system`; the rest are user/assistant turns.
    const system = opts.messages
      .filter((m) => m.role === "system")
      .map((m) => m.content)
      .join("\n\n")
    const convo = opts.messages
      .filter((m) => m.role !== "system")
      .map((m) => ({ role: (m.role === "assistant" ? "assistant" : "user") as "assistant" | "user", content: m.content }))
    if (convo.length === 0) convo.push({ role: "user", content: system || "Continue." })

    const res = await getAnthropic().messages.create({
      model: ANTHROPIC_MODEL,
      max_tokens: maxTokens,
      ...(system ? { system } : {}),
      messages: convo,
    })
    text = res.content.map((b) => (b.type === "text" ? b.text : "")).join("")
  } else {
    const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = opts.messages.map((m) =>
      m.role === "system"
        ? { role: "system", content: m.content }
        : m.role === "assistant"
          ? { role: "assistant", content: m.content }
          : { role: "user", content: m.content }
    )
    const res = await openai.chat.completions.create({
      model: OPENAI_MODEL,
      max_tokens: maxTokens,
      messages,
      ...(opts.json ? { response_format: { type: "json_object" as const } } : {}),
    })
    text = res.choices[0]?.message?.content ?? ""
  }

  return opts.json ? stripFences(text) : text
}
