/**
 * Tử Vi Số · AI provider abstraction
 *
 * Strategy: streaming chat completion with chart-aware context.
 *
 * Providers:
 *   - "mock"   : deterministic template responses for dev/CI (no API key)
 *   - "openai" : OpenAI Chat Completions API with stream=true (Node fetch)
 *
 * Selection happens via env:
 *   AI_PROVIDER=mock | openai (default: mock if no OPENAI_API_KEY)
 *   OPENAI_API_KEY=sk-...
 *   OPENAI_MODEL=gpt-4o-mini (default)
 *
 * The provider returns an async iterator of text chunks so callers can
 * pipe directly into a Web ReadableStream / SSE.
 */
import type { Chart } from "@/lib/tuvi/engine";

export type ChatMessage = {
  role: "system" | "user" | "assistant";
  content: string;
};

export type LuanGiaiInput = {
  chart: Chart;
  question: string;
  history?: ChatMessage[];
};

export interface AIProvider {
  name: "mock" | "openai" | "anthropic";
  /**
   * Stream a luận-giải response. Returns an async iterable of text chunks.
   * Caller is responsible for joining/encoding for SSE.
   */
  stream(input: LuanGiaiInput): AsyncIterable<string>;
  /**
   * Lower-level streaming: caller provides the full message list (system,
   * user, history). Used by /api/ai/insight for non-Tử-Vi tools that
   * have their own context shape.
   */
  streamMessages(messages: ChatMessage[]): AsyncIterable<string>;
}

/**
 * Build a Vietnamese system prompt with the chart packed in. Kept small —
 * we don't dump every star, just Mệnh / Thân / cục / chính tinh + a brief
 * pillar summary. Most luận giải requires only this much context.
 */
export function buildSystemPrompt(chart: Chart): string {
  const palaces = chart.palaces.map((p) => {
    const stars = [
      ...p.majorStars.map(
        (s) =>
          `${s.name}${s.brightness ? `(${s.brightness})` : ""}${s.mutagen ? ` Hóa ${s.mutagen}` : ""}`
      ),
      ...p.minorStars.map((s) => s.name),
    ];
    return `  - ${p.name} (${p.heavenlyStem} ${p.earthlyBranch})${
      p.isBodyPalace ? " [Thân]" : ""
    }: ${stars.join(", ") || "không sao"}`;
  });

  return `Bạn là một thầy tử vi đẩu số người Việt, có 30 năm kinh nghiệm. Bạn luận giải bằng tiếng Việt, văn phong điềm đạm, chính xác, dựa trên cổ thư.

Lá số tử vi của khách:
- Họ tên: ${chart.fullName || "(không cung cấp)"}
- Giới tính: ${chart.gender}
- Ngày sinh dương lịch: ${chart.solarDate}
- Lịch âm: ${chart.lunarDate}
- Trụ năm-tháng-ngày-giờ: ${chart.chineseDate}
- Cục mệnh: ${chart.fiveElements}
- Mệnh chủ: ${chart.soulStar} (cung ${chart.soulBranch})
- Thân chủ: ${chart.bodyStar} (cung ${chart.bodyBranch})
- Con giáp: ${chart.zodiac}
- Cung hoàng đạo Tây: ${chart.sign}

12 cung tử vi:
${palaces.join("\n")}

Quy tắc trả lời:
1. Luôn dựa trên lá số ở trên — đừng bịa thêm sao không có.
2. Khi luận, nêu rõ "vì cung X có sao Y" để khách hiểu.
3. Trung thực: nếu sao không tốt, nói thẳng là cần lưu ý gì.
4. KHÔNG đưa ra lời khuyên y tế / pháp luật / tài chính cụ thể (chỉ định hướng chung).
5. Khép lại với 1 câu lời khuyên ngắn (action item).
6. Tránh sáo ngữ "tốt nhất là...", "cẩn thận là tốt" — nói cụ thể.
7. Tối đa 350 từ trừ khi khách hỏi "phân tích chi tiết".`;
}

const TEMPLATE_CHAPTERS = [
  "tổng quan vận mệnh",
  "tình duyên",
  "sự nghiệp",
  "tài lộc",
  "sức khỏe",
  "gia đạo",
];

/** Pick a chapter heuristically by keywords in the question. */
function pickChapter(q: string): string {
  const lower = q.toLowerCase();
  // Money first — common keywords like "tiền", "kiếm" are unambiguous and
  // should win over love patterns where "hôn" appears inside "không".
  if (/(tiền|tài chính|tài lộc|đầu tư|kinh doanh|giàu|lộc|nợ|kiếm tiền|làm giàu)/.test(lower))
    return "tài lộc";
  // Career
  if (/(việc làm|nghề|sự nghiệp|công ty|sếp|đồng nghiệp|thăng tiến|công việc)/.test(lower))
    return "sự nghiệp";
  // Love — note: bare "hôn" excluded because it matches "không"; require
  // "hôn nhân" / "hôn lễ" for the marriage angle.
  if (
    /(yêu|tình duyên|tình cảm|chồng|vợ|hôn nhân|hôn lễ|cưới|duyên|người yêu|bạn gái|bạn trai)/.test(
      lower
    )
  )
    return "tình duyên";
  // Health — bare "y" excluded; require disease/wellness words.
  if (/(sức khỏe|bệnh|ốm|khỏe|y tế|y học|sinh lý|tật)/.test(lower)) return "sức khỏe";
  // Family
  if (/(gia đình|gia đạo|cha mẹ|bố mẹ|cha|mẹ|con cái|anh chị em|anh em|chị em)/.test(lower))
    return "gia đạo";
  return "tổng quan vận mệnh";
}

/**
 * Mock provider: builds a deterministic template response from the chart
 * and the user's question. Used when no OPENAI_API_KEY is configured so
 * dev / CI runs don't need network access.
 */
class MockProvider implements AIProvider {
  name = "mock" as const;

  async *stream(input: LuanGiaiInput): AsyncIterable<string> {
    const { chart, question } = input;
    const chapter = pickChapter(question);
    const menh = chart.palaces.find((p) => p.name === "Mệnh");
    const menhStars = menh?.majorStars.map((s) => s.name).join(", ") || "không có chính tinh";
    const target = (() => {
      switch (chapter) {
        case "tình duyên":
          return chart.palaces.find((p) => p.name === "Phu Thê");
        case "sự nghiệp":
          return chart.palaces.find((p) => p.name === "Quan Lộc");
        case "tài lộc":
          return chart.palaces.find((p) => p.name === "Tài Bạch");
        case "sức khỏe":
          return chart.palaces.find((p) => p.name === "Tật Ách");
        case "gia đạo":
          return chart.palaces.find((p) => p.name === "Phụ Mẫu");
        default:
          return menh;
      }
    })();

    const targetStars =
      target?.majorStars
        .map((s) => `${s.name}${s.brightness ? `(${s.brightness})` : ""}`)
        .join(", ") || "không có chính tinh";

    const para = (text: string) => text + "\n\n";
    const yieldChunked = async function* (text: string) {
      // Stream char-by-char with tiny delays so the UI shows real streaming
      const chunkSize = 4;
      for (let i = 0; i < text.length; i += chunkSize) {
        yield text.slice(i, i + chunkSize);
        // Cooperative yielding — keep latency under ~10ms
        if (i % 64 === 0) await new Promise((r) => setTimeout(r, 8));
      }
    };

    // Provide a clear "[mock]" marker so callers can tell this isn't real AI
    const text =
      para(
        `Chào ${chart.fullName || chart.gender === "Nam" ? "anh" : "chị"}, dựa trên lá số ${chart.solarDate} (${chart.fiveElements}), tôi luận về **${chapter}** như sau:`
      ) +
      para(
        `Cung Mệnh tại ${menh?.earthlyBranch ?? "?"} có ${menhStars} — đây là nền tảng cho cá tính và đường đi cuộc đời. Cung ${target?.name ?? menh?.name} có ${targetStars}, là nơi chính ảnh hưởng tới chủ đề bạn hỏi.`
      ) +
      para(
        `Câu hỏi của bạn: "${question}". Theo cách bố trí cung và sao trên, ${chapter} của bạn nghiêng về hướng cần kiên nhẫn và tích lũy hơn là kỳ vọng đột phá ngắn hạn. Sao Mệnh chủ ${chart.soulStar} thiên về phân tích, ưu tiên chiều sâu hơn chiều rộng.`
      ) +
      para(
        `Lời khuyên ngắn: trong 6 tháng tới, hãy đầu tư vào một kỹ năng cụ thể liên quan đến ${chapter}, đặt mục tiêu cuối quý có thể đo đếm được.`
      ) +
      `_[Phản hồi mock — chưa nối với OpenAI/Claude. Khi đặt OPENAI_API_KEY, hệ thống sẽ tự dùng mô hình thật.]_`;

    for await (const chunk of yieldChunked(text)) {
      yield chunk;
    }
  }

  async *streamMessages(messages: ChatMessage[]): AsyncIterable<string> {
    // Mock fallback for the generic /api/ai/insight endpoint.
    const userMsg = messages.findLast((m) => m.role === "user")?.content ?? "";
    const text =
      `[Phản hồi mock] Câu hỏi của bạn: "${userMsg.slice(0, 80)}${userMsg.length > 80 ? "..." : ""}".\n\n` +
      `Đây là phần luận giải mẫu. Để có phân tích thực sự, admin cần cấu hình OpenAI API key tại /admin/ai. ` +
      `Khi đó AI sẽ trả lời chi tiết dựa trên dữ liệu bạn cung cấp.\n\n` +
      `_Phản hồi mock — không sử dụng AI thật._`;
    const chunkSize = 4;
    for (let i = 0; i < text.length; i += chunkSize) {
      yield text.slice(i, i + chunkSize);
      if (i % 64 === 0) await new Promise((r) => setTimeout(r, 8));
    }
  }
}

/**
 * OpenAI-compatible provider. Works with the official OpenAI API and any
 * OpenAI-compatible endpoint (Azure, OpenRouter, Together, Groq, vLLM,
 * Ollama, LiteLLM) — just point baseURL at their /v1 endpoint.
 *
 * Activated when an API key is configured.
 */
class OpenAIProvider implements AIProvider {
  name = "openai" as const;
  private apiKey: string;
  private model: string;
  private baseURL: string;

  constructor(apiKey: string, model: string, baseURL: string) {
    this.apiKey = apiKey;
    this.model = model;
    // Strip trailing slash so we always join with "/chat/completions"
    this.baseURL = baseURL.replace(/\/+$/, "");
  }

  async *stream(input: LuanGiaiInput): AsyncIterable<string> {
    const messages: ChatMessage[] = [
      { role: "system", content: buildSystemPrompt(input.chart) },
      ...(input.history ?? []),
      { role: "user", content: input.question },
    ];
    yield* this.streamMessages(messages);
  }

  async *streamMessages(messages: ChatMessage[]): AsyncIterable<string> {
    const res = await fetch(`${this.baseURL}/chat/completions`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${this.apiKey}`,
      },
      body: JSON.stringify({
        model: this.model,
        messages,
        stream: true,
        temperature: 0.7,
      }),
    });

    if (!res.ok || !res.body) {
      const errText = await res.text().catch(() => "");
      throw new Error(`AI API ${res.status}: ${errText.slice(0, 200)}`);
    }

    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let buf = "";

    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      buf += decoder.decode(value, { stream: true });

      // OpenAI sends "data: {...}\n\n" lines
      const lines = buf.split("\n");
      buf = lines.pop() ?? "";
      for (const line of lines) {
        const trimmed = line.trim();
        if (!trimmed.startsWith("data:")) continue;
        const payload = trimmed.slice(5).trim();
        if (payload === "[DONE]") return;
        try {
          const parsed = JSON.parse(payload) as {
            choices?: { delta?: { content?: string } }[];
          };
          const piece = parsed.choices?.[0]?.delta?.content;
          if (piece) yield piece;
        } catch {
          // skip malformed line
        }
      }
    }
  }
}

/**
 * Anthropic provider — uses Claude's Messages API with stream=true.
 * Anthropic separates the "system" message into a top-level field and
 * accepts only user/assistant in messages[]. We split here.
 *
 * Activated when ai.provider=anthropic + ai.anthropic_api_key set.
 */
class AnthropicProvider implements AIProvider {
  name = "anthropic" as const;
  private apiKey: string;
  private model: string;
  private baseURL = "https://api.anthropic.com/v1";

  constructor(apiKey: string, model: string) {
    this.apiKey = apiKey;
    this.model = model;
  }

  async *stream(input: LuanGiaiInput): AsyncIterable<string> {
    const messages: ChatMessage[] = [
      { role: "system", content: buildSystemPrompt(input.chart) },
      ...(input.history ?? []),
      { role: "user", content: input.question },
    ];
    yield* this.streamMessages(messages);
  }

  async *streamMessages(messages: ChatMessage[]): AsyncIterable<string> {
    // Anthropic wants system as a separate top-level string. Concat all
    // system messages together (rare, but possible from /api/ai/insight).
    const systemParts = messages.filter((m) => m.role === "system").map((m) => m.content);
    const turn = messages.filter((m) => m.role !== "system");

    const res = await fetch(`${this.baseURL}/messages`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-api-key": this.apiKey,
        "anthropic-version": "2023-06-01",
      },
      body: JSON.stringify({
        model: this.model,
        max_tokens: 1024,
        temperature: 0.7,
        system: systemParts.join("\n\n"),
        messages: turn.map((m) => ({ role: m.role, content: m.content })),
        stream: true,
      }),
    });

    if (!res.ok || !res.body) {
      const errText = await res.text().catch(() => "");
      throw new Error(`Anthropic API ${res.status}: ${errText.slice(0, 200)}`);
    }

    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let buf = "";

    while (true) {
      const { value, done } = await reader.read();
      if (done) break;
      buf += decoder.decode(value, { stream: true });

      // Anthropic SSE: "event: <name>\ndata: {...}\n\n"
      const lines = buf.split("\n");
      buf = lines.pop() ?? "";
      for (const line of lines) {
        const trimmed = line.trim();
        if (!trimmed.startsWith("data:")) continue;
        const payload = trimmed.slice(5).trim();
        if (!payload || payload === "[DONE]") continue;
        try {
          const parsed = JSON.parse(payload) as {
            type?: string;
            delta?: { type?: string; text?: string };
          };
          if (parsed.type === "content_block_delta" && parsed.delta?.type === "text_delta") {
            const piece = parsed.delta.text;
            if (piece) yield piece;
          }
          if (parsed.type === "message_stop") return;
        } catch {
          // skip malformed line
        }
      }
    }
  }
}

/** Resolve provider from DB settings (fallback to env). Async because
 *  Setting reads hit the DB. Cached in lib/settings.ts (30s TTL).
 */
export async function getProvider(): Promise<AIProvider> {
  const { getSetting } = await import("@/lib/settings");
  const wanted = await getSetting("ai.provider");
  const openaiKey = await getSetting("ai.openai_api_key");
  const openaiModel = (await getSetting("ai.openai_model")) || "gpt-4o-mini";
  const baseURL = (await getSetting("ai.openai_base_url")) || "https://api.openai.com/v1";
  const anthropicKey = await getSetting("ai.anthropic_api_key");
  const anthropicModel = (await getSetting("ai.anthropic_model")) || "claude-3-5-sonnet-latest";

  if (wanted === "openai" && !openaiKey) {
    throw new Error("AI provider set to 'openai' but no API key configured");
  }
  if (wanted === "anthropic" && !anthropicKey) {
    throw new Error("AI provider set to 'anthropic' but no API key configured");
  }
  if (wanted === "mock") return new MockProvider();
  if (wanted === "anthropic") {
    return new AnthropicProvider(anthropicKey, anthropicModel);
  }
  if (wanted === "openai") {
    return new OpenAIProvider(openaiKey, openaiModel, baseURL);
  }
  // auto: prefer Anthropic if its key is set and OpenAI is not, else OpenAI,
  // else mock.
  if (wanted === "auto") {
    if (openaiKey) return new OpenAIProvider(openaiKey, openaiModel, baseURL);
    if (anthropicKey) return new AnthropicProvider(anthropicKey, anthropicModel);
  }
  return new MockProvider();
}

/** Test a provider configuration without persisting it. Returns the first
 *  chunk of streamed output, or throws with a useful error message.
 *  Used by /api/admin/ai/test to validate a key before saving.
 */
export async function testProvider(
  provider: "openai" | "anthropic",
  config: { apiKey: string; model: string; baseURL?: string }
): Promise<{ ok: true; sample: string; provider: string; model: string }> {
  if (!config.apiKey) {
    throw new Error("API key is required");
  }
  const instance: AIProvider =
    provider === "openai"
      ? new OpenAIProvider(
          config.apiKey,
          config.model,
          config.baseURL || "https://api.openai.com/v1"
        )
      : new AnthropicProvider(config.apiKey, config.model);

  const messages: ChatMessage[] = [
    { role: "system", content: "Bạn trả lời ngắn gọn 1 câu để xác nhận kết nối." },
    { role: "user", content: "Hello, just say 'OK' to confirm." },
  ];

  let sample = "";
  let chunks = 0;
  for await (const chunk of instance.streamMessages(messages)) {
    sample += chunk;
    chunks += 1;
    if (sample.length >= 80 || chunks >= 30) break;
  }
  return {
    ok: true,
    sample: sample.trim() || "(empty response)",
    provider: instance.name,
    model: config.model,
  };
}

/** Exposed for testing — picks the right provider class without env reads. */
export const _internal = {
  MockProvider,
  OpenAIProvider,
  AnthropicProvider,
  TEMPLATE_CHAPTERS,
  pickChapter,
};
