import { generateObject, generateText } from "ai";
import { z } from "zod";
import type { ProviderApiKeys } from "./keys";
import type { MatrixRow, ResearchMap } from "@/lib/types/research";
import { withAgentSystem } from "./agent";
import { buildPersonaPromptBlock } from "./persona";
import { getModel } from "./provider";
import { extractJsonFromText } from "./coerce-research-map";
import {
  getOpenCodeGenerationSettings,
  prefersOpenCodeTextFirst,
} from "./opencode-go-settings";
import {
  coerceMatrixRaw,
  formatMatrixError,
  matrixLooseSchema,
} from "./coerce-matrix";
import { HASHTAG_USER_REMINDER, HASHTAG_WRITING_RULES } from "./hashtag-rules";
import { sanitizePromptText, THREADS_MAX_CHARS } from "@/lib/utils";

const matrixSchema = z.object({
  rows: z.array(
    z.object({
      sortOrder: z.number().int().min(1),
      searchTag: z.string(),
      angle: z.string(),
      hook: z.string(),
      text: z.string().max(THREADS_MAX_CHARS),
      referenceNotes: z.string(),
      sourcePermalinks: z.array(z.string()),
      rationale: z.string(),
    })
  ),
});

const TEXT_FIRST_MODELS = new Set(["grok-3", "grok-3-fast"]);

const BATCH_COUNT = 3;

export interface GenerateMatrixInput {
  topicLabel: string;
  query: string;
  brief?: string | null;
  persona?: string | null;
  researchMap?: ResearchMap | null;
  aiProvider: string;
  aiModel: string;
  apiKeys?: ProviderApiKeys;
  count: number;
  posts: Array<{
    text: string;
    authorName?: string | null;
    permalink?: string | null;
    searchTag?: string | null;
    likeCount?: number | null;
    replyCount?: number | null;
    qualityReason?: string | null;
    replies?: Array<{ text: string; authorName?: string | null; likeCount?: number | null }>;
  }>;
}

function distributePosts<T>(posts: T[], n: number): T[][] {
  const batches: T[][] = Array.from({ length: n }, () => []);
  posts.forEach((post, i) => batches[i % n].push(post));
  return batches;
}

function buildMaterialsBlock(posts: GenerateMatrixInput["posts"], defaultQuery: string): string {
  return posts
    .map((post, i) => {
      const repliesBlock =
        post.replies && post.replies.length > 0
          ? `\n  留言：${post.replies
              .slice(0, 3)
              .map(
                (r) =>
                  `@${sanitizePromptText(r.authorName) || "匿名"}：${sanitizePromptText(r.text).slice(0, 80)}`
              )
              .join(" | ")}`
          : "";

      return `${i + 1}. [${sanitizePromptText(post.searchTag) || sanitizePromptText(defaultQuery)}] @${sanitizePromptText(post.authorName) || "匿名"}（${post.likeCount ?? 0}讚）
  ${sanitizePromptText(post.text).slice(0, 300)}
  連結：${sanitizePromptText(post.permalink) || "無"}${repliesBlock}${post.qualityReason ? `\n  品質說明：${sanitizePromptText(post.qualityReason)}` : ""}`;
    })
    .join("\n\n");
}

function buildResearchBlock(researchMap?: ResearchMap | null): string {
  if (!researchMap) return "";

  const similarAccounts = (researchMap.similarAccounts ?? [])
    .slice(0, 10)
    .map((a) => `  @${sanitizePromptText(a.username)}${a.reason ? `（${sanitizePromptText(a.reason)}）` : ""}`)
    .join("\n");

  return `
受眾：${sanitizePromptText(researchMap.audienceSummary)}
內容目標：${sanitizePromptText(researchMap.contentGoal)}
想回答的問題：${researchMap.questions.map(sanitizePromptText).join("、")}
內容支柱：${researchMap.pillars.map(sanitizePromptText).join("、")}
${similarAccounts ? `同領域參考帳號：\n${similarAccounts}` : ""}
`;
}

function buildSystemPrompt(persona?: string | null) {
  return withAgentSystem(`你是 Threads 內容企劃師。根據參考素材（含優質與中等品質），產出「內容矩陣」——一週可發的貼文企劃表。

${buildPersonaPromptBlock(persona)}

規則：
- 每篇必須原創，不可抄襲參考貼文
- hook 是一句吸引人的開頭（可獨立於正文），風格符合人設開場習慣
- text 是完整 Threads 貼文（≤500字，含 #標籤），語感要像創作者本人親筆
${HASHTAG_WRITING_RULES}
- referenceNotes 是從參考素材摘出的 2-3 個重點（簡短條列感，用換行分隔）
- 每篇對應一個 searchTag（主題標籤）
- sortOrder 從 1 開始遞增
- 繁體中文台灣用語
- 知識型貼文：不寫未查證的醫療／數據斷言，rationale 標註需網路查證的關鍵句
- 部分素材附有［品質說明］：品質較低的仍有參考價值（角度、切點可用），產出時不要直接沿用其內容`);
}

function buildBatchPrompt(
  input: GenerateMatrixInput,
  researchBlock: string,
  materials: string,
  batchIndex: number,
  totalBatches: number,
  thisBatchCount: number
) {
  return `主題：${sanitizePromptText(input.topicLabel)}
種子關鍵字：${sanitizePromptText(input.query)}
${input.brief ? `Brief：${sanitizePromptText(input.brief)}` : ""}
${researchBlock}

【批次 ${batchIndex + 1}/${totalBatches}】
以下為第 ${batchIndex + 1} 批素材（共 ${materials.split("\n\n").length} 篇），請產出 ${thisBatchCount} 篇不同切角，注意與其他批次做出區隔，避免重複。

參考素材：
${materials}

寫作提醒：矩陣裡的 text 必須像創作者親筆，有代表句範例時語感要向範例靠攏。${HASHTAG_USER_REMINDER}`;
}

function buildJsonPromptSuffix(count: number) {
  return `

請只回傳 JSON（不要 markdown），格式：
{
  "rows": [
    {
      "sortOrder": 1,
      "searchTag": "標籤",
      "angle": "切角說明",
      "hook": "開頭一句",
      "text": "完整貼文（結尾含 1～3 個 #話題標籤）",
      "referenceNotes": "參考重點（多行以換行分隔）",
      "sourcePermalinks": ["https://..."],
      "rationale": "為什麼這篇有效"
    }
  ]
}
請產出 ${count} 篇，sortOrder 從 1 遞增，每篇 text 不超過 ${THREADS_MAX_CHARS} 字。`;
}

function normalizeRows(rows: MatrixRow[]): MatrixRow[] {
  return rows
    .sort((a, b) => a.sortOrder - b.sortOrder)
    .map((row) => ({
      ...row,
      text: row.text.slice(0, THREADS_MAX_CHARS),
    }));
}

function mergeBatches(batches: MatrixRow[][]): MatrixRow[] {
  const all = batches.flat();
  const seen = new Set<string>();
  const merged: MatrixRow[] = [];

  for (const row of all.sort((a, b) => a.sortOrder - b.sortOrder)) {
    const key = row.angle.slice(0, 30).toLowerCase().replace(/\s+/g, "");
    if (!seen.has(key)) {
      seen.add(key);
      merged.push(row);
    }
  }

  return merged.map((row, i) => ({ ...row, sortOrder: i + 1 }));
}

async function generateWithText(
  model: ReturnType<typeof getModel>,
  system: string,
  prompt: string,
  fallback: { query: string; count: number },
  provider: string,
  modelId: string
) {
  const settings = getOpenCodeGenerationSettings(provider, modelId);
  const { text } = await generateText({
    model,
    system: sanitizePromptText(`${system}${buildJsonPromptSuffix(fallback.count)}`),
    prompt: sanitizePromptText(prompt),
    ...settings,
  });
  const parsed = extractJsonFromText(text);
  matrixLooseSchema.parse(parsed);
  return coerceMatrixRaw(parsed, fallback);
}

async function generateWithObject(
  model: ReturnType<typeof getModel>,
  system: string,
  prompt: string,
  fallback: { query: string; count: number },
  provider: string,
  modelId: string
) {
  const settings = getOpenCodeGenerationSettings(provider, modelId);
  const { object } = await generateObject({
    model,
    schema: matrixSchema,
    system: sanitizePromptText(system),
    prompt: sanitizePromptText(`${prompt}${buildJsonPromptSuffix(fallback.count)}`),
    ...settings,
  });
  return normalizeRows(object.rows);
}

async function attemptBatch(
  model: ReturnType<typeof getModel>,
  system: string,
  prompt: string,
  fallback: { query: string; count: number },
  provider: string,
  modelId: string,
  preferText: boolean
): Promise<MatrixRow[]> {
  const attempts: Array<() => Promise<MatrixRow[]>> = preferText
    ? [
        () => generateWithText(model, system, prompt, fallback, provider, modelId),
        () => generateWithObject(model, system, prompt, fallback, provider, modelId),
        () =>
          generateWithText(
            model,
            system,
            `${prompt}\n\n上次格式不完整，請務必補齊 ${fallback.count} 篇 rows。`,
            fallback,
            provider,
            modelId
          ),
      ]
    : [
        () => generateWithObject(model, system, prompt, fallback, provider, modelId),
        () => generateWithText(model, system, prompt, fallback, provider, modelId),
        () =>
          generateWithText(
            model,
            system,
            `${prompt}\n\n上次格式不完整，請務必補齊 ${fallback.count} 篇 rows。`,
            fallback,
            provider,
            modelId
          ),
      ];

  let rows: MatrixRow[] | null = null;
  let lastError: unknown;

  for (const attempt of attempts) {
    try {
      rows = await attempt();
      break;
    } catch (error) {
      lastError = error;
    }
  }

  if (!rows) {
    throw new Error(formatMatrixError(lastError));
  }

  return normalizeRows(rows);
}

export async function generateContentMatrix(input: GenerateMatrixInput): Promise<MatrixRow[]> {
  if (input.posts.length === 0) {
    throw new Error("此海巡沒有素材，請重新海巡");
  }

  const model = getModel(input.aiProvider, input.aiModel, input.apiKeys ?? {});
  const researchBlock = buildResearchBlock(input.researchMap);
  const system = buildSystemPrompt(input.persona);
  const preferText =
    prefersOpenCodeTextFirst(input.aiProvider, input.aiModel) ||
    TEXT_FIRST_MODELS.has(input.aiModel);

  const batches = distributePosts(input.posts, BATCH_COUNT);
  const perBatch = Math.ceil(input.count / BATCH_COUNT);

  const results = await Promise.all(
    batches.map((batchPosts, batchIndex) => {
      if (batchPosts.length === 0) return Promise.resolve([] as MatrixRow[]);
      const materials = buildMaterialsBlock(batchPosts, input.query);
      const prompt = buildBatchPrompt(input, researchBlock, materials, batchIndex, BATCH_COUNT, perBatch);
      const fallback = { query: input.query, count: perBatch };
      return attemptBatch(model, system, prompt, fallback, input.aiProvider, input.aiModel, preferText);
    })
  );

  const merged = mergeBatches(results);
  return normalizeRows(merged).slice(0, input.count);
}
