import Anthropic from "@anthropic-ai/sdk";
import prisma from "../config/database";
import { config } from "../config/env";

/** The single model this assistant uses (Phase 1). */
export const AI_MODEL = "claude-sonnet-4-6";

/** Per-token USD pricing. Sonnet 4.6 = $3 / $15 per 1M (input / output). */
const PRICING: Record<string, { input: number; output: number }> = {
  "claude-sonnet-4-6": { input: 3 / 1_000_000, output: 15 / 1_000_000 },
};

const DEFAULT_BUDGET_USD = 50;

export function isConfigured(): boolean {
  return !!config.anthropic.apiKey;
}

/** Lazily build the SDK client; throws a typed result upstream if unconfigured. */
function client(): Anthropic {
  return new Anthropic({ apiKey: config.anthropic.apiKey });
}

export function computeCostUsd(
  model: string,
  inputTokens: number,
  outputTokens: number,
): number {
  const p = PRICING[model] ?? PRICING[AI_MODEL];
  return inputTokens * p.input + outputTokens * p.output;
}

export async function recordUsage(args: {
  userId: number | null;
  kind: string;
  model: string;
  inputTokens: number;
  outputTokens: number;
}): Promise<void> {
  await prisma.ai_usage.create({
    data: {
      user_id: args.userId,
      kind: args.kind,
      model: args.model,
      input_tokens: args.inputTokens,
      output_tokens: args.outputTokens,
      cost_usd: computeCostUsd(args.model, args.inputTokens, args.outputTokens),
    },
  });
}

// Budget window edge uses the UTC month boundary (ai_usage.created_at is a UTC
// @db.Timestamp). At month turnover this is offset from Prague local time by the
// UTC offset for ~1-2h — acceptable for a soft monthly budget, and stable.
function startOfMonthUtc(): Date {
  const d = new Date();
  return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1));
}

export async function getMonthSpendUsd(): Promise<number> {
  const agg = await prisma.ai_usage.aggregate({
    _sum: { cost_usd: true },
    where: { created_at: { gte: startOfMonthUtc() } },
  });
  return Number(agg._sum.cost_usd ?? 0);
}

export async function getBudgetUsd(): Promise<number> {
  const settings = await prisma.company_settings.findFirst({
    select: { ai_monthly_budget_usd: true },
  });
  const v = settings?.ai_monthly_budget_usd;
  return v == null ? DEFAULT_BUDGET_USD : Number(v);
}

export async function setBudgetUsd(value: number): Promise<void> {
  const existing = await prisma.company_settings.findFirst({
    select: { id: true },
  });
  if (existing) {
    await prisma.company_settings.update({
      where: { id: existing.id },
      data: { ai_monthly_budget_usd: value },
    });
  } else {
    await prisma.company_settings.create({
      data: { company_name: "", ai_monthly_budget_usd: value },
    });
  }
}

/** Returns { error, status: 402 } when this month's spend has reached the budget. */
export async function assertBudgetAvailable(): Promise<{
  error: string;
  status: number;
} | null> {
  const [spend, budget] = await Promise.all([
    getMonthSpendUsd(),
    getBudgetUsd(),
  ]);
  if (spend >= budget) {
    return { error: "Měsíční rozpočet AI byl vyčerpán", status: 402 };
  }
  return null;
}

// ── Conversations (server-side, per user) ─────────────────────────────────
export interface StoredChatMessage {
  role: string;
  content: string;
  created_at: Date;
}
export interface ConversationSummary {
  id: number;
  title: string;
  updated_at: Date;
}

const DEFAULT_CONVERSATION_TITLE = "Nová konverzace";
const MESSAGE_LIMIT = 200; // cap a single thread read

export async function listConversations(
  userId: number,
): Promise<ConversationSummary[]> {
  return prisma.ai_conversations.findMany({
    where: { user_id: userId },
    orderBy: { id: "asc" }, // stable tab order
    select: { id: true, title: true, updated_at: true },
  });
}

export async function createConversation(
  userId: number,
  title?: string,
): Promise<ConversationSummary> {
  return prisma.ai_conversations.create({
    data: {
      user_id: userId,
      title: title?.trim() || DEFAULT_CONVERSATION_TITLE,
    },
    select: { id: true, title: true, updated_at: true },
  });
}

/** Ownership-checked fetch; null when not found / not owned. */
async function ownConversation(userId: number, convId: number) {
  return prisma.ai_conversations.findFirst({
    where: { id: convId, user_id: userId },
    select: { id: true, title: true },
  });
}

export async function getConversationMessages(
  userId: number,
  convId: number,
): Promise<{ data: StoredChatMessage[] } | { error: string; status: number }> {
  if (!(await ownConversation(userId, convId)))
    return { error: "Konverzace nenalezena", status: 404 };
  const rows = await prisma.ai_chat_messages.findMany({
    where: { conversation_id: convId },
    orderBy: { id: "desc" },
    take: MESSAGE_LIMIT,
    select: { role: true, content: true, created_at: true },
  });
  return { data: rows.reverse() };
}

export async function appendConversationMessages(
  userId: number,
  convId: number,
  messages: { role: string; content: string }[],
): Promise<{ data: { ok: true } } | { error: string; status: number }> {
  const conv = await ownConversation(userId, convId);
  if (!conv) return { error: "Konverzace nenalezena", status: 404 };
  // The route validates messages with min(1), so this is normally non-empty;
  // the guard is defensive (and still bumps updated_at below for an empty call).
  if (messages.length > 0) {
    await prisma.ai_chat_messages.createMany({
      data: messages.map((m) => ({
        user_id: userId,
        conversation_id: convId,
        role: m.role,
        content: m.content,
      })),
    });
  }
  // Auto-title from the first user message (only while still the default), and
  // always bump updated_at.
  const firstUser = messages.find((m) => m.role === "user");
  const data: { updated_at: Date; title?: string } = { updated_at: new Date() };
  if (conv.title === DEFAULT_CONVERSATION_TITLE && firstUser) {
    const t = firstUser.content.replace(/\s+/g, " ").trim().slice(0, 60);
    if (t) data.title = t;
  }
  await prisma.ai_conversations.update({ where: { id: convId }, data });
  return { data: { ok: true } };
}

export async function renameConversation(
  userId: number,
  convId: number,
  title: string,
): Promise<{ data: ConversationSummary } | { error: string; status: number }> {
  if (!(await ownConversation(userId, convId)))
    return { error: "Konverzace nenalezena", status: 404 };
  const updated = await prisma.ai_conversations.update({
    where: { id: convId },
    data: { title: title.trim() || DEFAULT_CONVERSATION_TITLE },
    select: { id: true, title: true, updated_at: true },
  });
  return { data: updated };
}

export async function deleteConversation(
  userId: number,
  convId: number,
): Promise<{ data: { ok: true } } | { error: string; status: number }> {
  if (!(await ownConversation(userId, convId)))
    return { error: "Konverzace nenalezena", status: 404 };
  await prisma.ai_conversations.delete({ where: { id: convId } }); // cascade
  return { data: { ok: true } };
}

export interface ChatMessage {
  role: "user" | "assistant";
  content: string;
}

const SYSTEM_PROMPT =
  "Jsi asistent v interním firemním systému (česká firma). Odpovídej česky, stručně a věcně. " +
  "Nemáš přístup k datům systému; pomáháš s obecnými dotazy a se čtením přiložených faktur.";

/** Plain chat turn. Records usage. Caller must check the budget first. */
export async function chat(
  messages: ChatMessage[],
  userId: number | null,
): Promise<{ reply: string }> {
  const res = await client().messages.create({
    model: AI_MODEL,
    max_tokens: 2048,
    system: SYSTEM_PROMPT,
    messages: messages.map((m) => ({ role: m.role, content: m.content })),
  });
  // Best-effort usage logging — a ledger-write blip must not fail the user's
  // call or vanish silently (CLAUDE.md: never swallow non-fatal failures).
  try {
    await recordUsage({
      userId,
      kind: "chat",
      model: AI_MODEL,
      inputTokens: res.usage.input_tokens,
      outputTokens: res.usage.output_tokens,
    });
  } catch (e) {
    console.error("[ai.service] recordUsage failed (chat)", e);
  }
  const reply = res.content
    .filter((b): b is Anthropic.TextBlock => b.type === "text")
    .map((b) => b.text)
    .join("\n");
  return { reply };
}

export interface ExtractedInvoice {
  supplier_name: string;
  invoice_number: string | null;
  amount: number;
  currency: string;
  vat_rate: number;
  issue_date: string | null;
  due_date: string | null;
  description: string | null;
}

// JSON schema for the structured extraction. Typed as the SDK's mutable
// index-signature shape (`Record<string, unknown>` leaves), NOT `as const` —
// a deeply-readonly literal won't assign to JSONOutputFormat.schema.
const INVOICE_SCHEMA: Record<string, unknown> = {
  type: "object",
  properties: {
    supplier_name: { type: "string" },
    invoice_number: { type: ["string", "null"] },
    amount: {
      type: "number",
      description:
        "Celková částka k úhradě VČETNĚ DPH (gross total), NE základ bez DPH.",
    },
    currency: { type: "string" },
    vat_rate: {
      type: "number",
      description: "Sazba DPH v procentech; 0 pokud faktura nemá DPH.",
    },
    issue_date: { type: ["string", "null"] },
    due_date: { type: ["string", "null"] },
    description: { type: ["string", "null"] },
  },
  required: ["supplier_name", "amount", "currency", "vat_rate"],
  additionalProperties: false,
};

/** Vision-extract the received-invoice fields from a PDF. Records usage. */
export async function extractInvoice(
  pdfBuffer: Buffer,
  userId: number | null,
): Promise<ExtractedInvoice> {
  const res = await client().messages.create({
    model: AI_MODEL,
    max_tokens: 1024,
    output_config: {
      format: { type: "json_schema", schema: INVOICE_SCHEMA },
    },
    messages: [
      {
        role: "user",
        content: [
          {
            type: "document",
            source: {
              type: "base64",
              media_type: "application/pdf",
              data: pdfBuffer.toString("base64"),
            },
          },
          {
            type: "text",
            text:
              "Vyčti z této přijaté faktury tato pole: dodavatele, číslo faktury, " +
              "celkovou částku k úhradě VČETNĚ DPH (tj. konečný součet, NE základ bez DPH), " +
              "měnu (ISO kód), sazbu DPH v procentech, datum vystavení a splatnosti (YYYY-MM-DD) a krátký popis. " +
              "Pokud faktura nemá DPH, vrať sazbu 0. Pokud pole chybí, vrať null.",
          },
        ],
      },
    ],
  });
  try {
    await recordUsage({
      userId,
      kind: "extract",
      model: AI_MODEL,
      inputTokens: res.usage.input_tokens,
      outputTokens: res.usage.output_tokens,
    });
  } catch (e) {
    console.error("[ai.service] recordUsage failed (extract)", e);
  }
  const text = res.content
    .filter((b): b is Anthropic.TextBlock => b.type === "text")
    .map((b) => b.text)
    .join("");
  // The json_schema output format makes a valid-JSON reply the norm, but a
  // truncated/non-JSON reply must not surface as a raw SyntaxError — throw a
  // clear, typed error the route's catch can turn into a friendly message.
  try {
    return JSON.parse(text) as ExtractedInvoice;
  } catch (e) {
    console.error("[ai.service] extractInvoice: model reply was not JSON", e);
    throw new Error("Model nevrátil platná data faktury (neplatný JSON)");
  }
}
