"use client";

import type { A4ParagraphV1, A4TextToken, TuViA4ComposedSectionV1 } from "@/lib/tuvi-a4-overview-interpretation";
import { paragraphToPlainText, stringToA4Tokens } from "@/lib/tuvi-a4-overview-interpretation";
import type { TuViPremiumTeaserV1 } from "@/lib/tuvi-premium-teaser.v1";
import type { TuViPremiumDeepReportV1 } from "@/lib/tuvi-premium-report.v1";
import { Fragment, useCallback, useRef } from "react";
import {
  NarrativeContainer,
  NarrativeHeading,
  NarrativeParagraph,
  NarrativeSection,
  splitPlainTextWithNarrativeEntities,
} from "./narrative";
import { TuViPalaceFocusHeatmap } from "./TuViPalaceFocusHeatmap";
import { TuViStrategicSignalBars } from "./TuViStrategicSignalBars";
import { TuViTimingMomentumStrip } from "./TuViTimingMomentumStrip";
import { StarNameTooltip } from "./StarNameTooltip";
import { buildSoftLaunchOverviewSectionKey, SoftLaunchOverviewSectionChrome } from "./SoftLaunchOverviewSectionChrome";
import { TuViA4PremiumConversion } from "./TuViA4PremiumConversion";
import { isSoftLaunchClientEnabled } from "@/lib/soft-launch-client";
import { TUVI_FUNNEL_EVENT, queueTuViFunnelTelemetry } from "@/lib/tuvi-funnel-telemetry.v1";

type Props = {
  /** Khối từ backend `chartOverviewOnePage` — humanization + semantic layer; ưu tiên khi có. */
  serverOverview?: {
    readonly body: string;
    readonly paragraphRoles: readonly string[];
    readonly layeredSections?: readonly {
      readonly id: string;
      readonly title: string;
      readonly paragraphs: readonly string[];
      readonly canonicalKey?: string;
      readonly signalHints?: readonly string[];
    }[];
    readonly interpretationSignals?: readonly {
      readonly key: string;
      readonly label: string;
      readonly value: number;
      readonly confidence: "low" | "medium" | "high";
      readonly drivers: readonly string[];
      readonly cautions?: readonly string[];
    }[];
    readonly strategicRecommendationsA4?: {
      readonly title: string;
      readonly bullets: readonly string[];
    };
    readonly strategicRecommendationsBlockPlain?: string;
  } | null;
  /** Thứ tự 12 cung trên lá — dùng lưới trọng tâm đọc (không bắt buộc). */
  overviewPalaces?: readonly { readonly key: string; readonly name: string }[] | null;
  /** Khi có — hiển thị trực tiếp từ builder (không ghép chunk + nối từ cứng). */
  composedSections?: readonly TuViA4ComposedSectionV1[] | null;
  composedFooter?: string | null;
  /** Legacy: một khối `\n\n` từ builder cũ. */
  body?: string | null;
  fallbackMessage: string;
  className?: string;
  /** Khối chuyển tiếp + CTA (chỉ UI); không đụng pipeline luận giải. */
  onContinueToDetailedAnalysis?: () => void;
  /** Debug pipeline (dev): semantic layer / nguồn overview. */
  overviewPipelineDebug?: TuViOverviewPipelineDebugUi | null;
  /** Chế độ soft launch: telemetry + phản hồi theo mục (chỉ khi `NEXT_PUBLIC_SOFT_LAUNCH_MODE=1`). */
  softLaunchReading?: { readonly chartId: string; readonly chartFingerprint?: string } | null;
  /** Teaser luận chuyên sâu (deterministic, không thanh toán) — ẩn khi null. */
  premiumTeaser?: TuViPremiumTeaserV1 | null;
  /** Phase 32 — mục lục xem trước luận sâu (kèm teaser). */
  premiumDeepReport?: TuViPremiumDeepReportV1 | null;
  /** Optional: saved account reading id for interest lead (ownership enforced server-side). */
  premiumSavedReadingId?: string | null;
  /** Tu Vi funnel telemetry (soft launch): chart id + viewing year — no PII. */
  funnelTelemetry?: { readonly chartId: string; readonly viewingYearSolar: number } | null;
};

export type TuViOverviewPipelineDebugUi = {
  overviewSource: "server" | "client";
  semanticLayerApplied: boolean;
  semanticReplacementCount: number;
  semanticReplacementsSample: readonly { semanticKey: string; matched: string; rendered: string }[];
};

function renderNarrativeTextSlices(text: string, keyBase: string) {
  return splitPlainTextWithNarrativeEntities(text).map((sl, i) =>
    sl.entity ?
      <span key={`${keyBase}-e-${i}`} className="ctkp-narrative-entity">
        {sl.text}
      </span>
    : <Fragment key={`${keyBase}-t-${i}`}>{sl.text}</Fragment>,
  );
}

function A4ParagraphInline({ para }: { para: A4ParagraphV1 }) {
  if (typeof para === "string") return <>{renderNarrativeTextSlices(para, "s")}</>;
  return (
    <span className="inline leading-[inherit]">
      {para.map((tok: A4TextToken, i: number) =>
        tok.type === "text" ?
          <span key={i} className="inline">
            {renderNarrativeTextSlices(tok.text, `t${i}`)}
          </span>
        : <StarNameTooltip key={`${tok.starKey}-${i}`} starKey={tok.starKey} label={tok.label} tone="manuscript" />,
      )}
    </span>
  );
}

function norm(s: string): string {
  return s.replace(/\s+/g, " ").trim();
}

/** Khi tiêu đề mục và nhãn bucket quá giống — ẩn bucket (metadata), tránh hai dòng tiêu đề. */
function titlesRoughlyMatch(sectionTitle: string, bucketLabel: string): boolean {
  const a = norm(sectionTitle).toLowerCase();
  const b = norm(bucketLabel).toLowerCase();
  if (!a || !b) return false;
  if (a === b) return true;
  if (a.length >= 14 && b.length >= 14 && (a.includes(b) || b.includes(a))) return true;
  const wa = new Set(a.split(/\s+/).filter((w) => w.length > 2));
  const wb = new Set(b.split(/\s+/).filter((w) => w.length > 2));
  let inter = 0;
  for (const w of wa) if (wb.has(w)) inter += 1;
  const union = wa.size + wb.size - inter;
  return union > 0 && inter / union >= 0.55;
}

/** Tách câu đầy đủ — không cắt giữa từ; giữ dấu … trong lookbehind. */
const FULL_SENTENCE_SPLIT = /(?<=[.!?…])\s+/;

function filterFullSentences(parts: readonly string[]): string[] {
  return parts.map(norm).filter((s) => s.length >= 10);
}

/**
 * Gỡ mảnh vỡ / câu quá ngắn; chỉ giữ câu trọn vẹn trước khi humanize.
 * Không slice theo độ dài ký tự trên toàn chuỗi.
 */
function ensureChunkSentenceIntegrity(chunk: string): string {
  const t = norm(chunk);
  if (!t) return "";
  const sents = filterFullSentences(t.split(FULL_SENTENCE_SPLIT));
  if (sents.length) return sents.join(" ");
  return t.length >= 10 ? t : "";
}

function ensureBodySentenceIntegrity(body: string): string {
  return body
    .split(/\n\n+/)
    .map((c) => ensureChunkSentenceIntegrity(c.trim()))
    .filter(Boolean)
    .join("\n\n");
}

/** Gỡ cụm nội bộ / meta — chỉ tầng hiển thị, thứ tự cố định. */
function stripInternalPhrases(raw: string): string {
  let t = norm(raw);
  const patterns: RegExp[] = [
    /\bBối cảnh cung[^.!?…]{0,160}[.!?…]?\s*/gi,
    /\bNhập\s+[Mm]ệnh\b[^.!?…]{0,40}?[.!?…]?\s*/gi,
    /\bNhập\s+[Tt]hân\b[^.!?…]{0,40}?[.!?…]?\s*/gi,
    /\bHiện cung này (chưa|chưa có|hiện chưa|đang)[^.!?…]{0,220}[.!?…]?\s*/gi,
    /\btrục chính tinh[^.!?…]{0,180}[.!?…]?\s*/gi,
    /\btrục chính[^.!?…]{0,100}?tinh[^.!?…]{0,60}[.!?…]?\s*/gi,
  ];
  for (const re of patterns) t = t.replace(re, " ");
  return norm(t).replace(/\s*([,.;:])\s*/g, "$1 ").replace(/\s+/g, " ");
}

/** Ngắt mệnh đề → câu riêng (cụm nối cố định, không thêm ý mới). */
function applyStructuralBreaks(t: string): string {
  let s = norm(t);
  const pairs: [RegExp, string][] = [
    [/, nên /gi, ". "],
    [/; nên /gi, ". "],
    [/, vì vậy /gi, ". "],
    [/, do đó /gi, ". Do đó, "],
    [/, nhưng /gi, ". Nhưng "],
    [/, tuy nhiên,\s*/gi, ". Tuy nhiên, "],
    [/, song /gi, ". Song "],
    [/, mà /gi, ". Mà "],
  ];
  for (const [re, rep] of pairs) s = s.replace(re, rep);
  return norm(s);
}

function capitalizeFirstVi(s: string): string {
  const t = s.trim();
  if (!t) return t;
  const c0 = t.charAt(0);
  const up = c0.toLocaleUpperCase("vi-VN");
  return up + t.slice(1);
}

function hasTerminalPunct(s: string): boolean {
  return /[.!?…]\s*$/.test(s.trim());
}

function ensureTerminalPunct(s: string): string {
  const t = s.trim();
  if (!t) return t;
  return hasTerminalPunct(t) ? t : `${t}.`;
}

/** Không chèn nối kiểu “AI therapist” — giữ thứ tự câu gốc (sparsity cadence). */
function insertSoftConnectors(sentences: readonly string[]): string[] {
  return [...sentences];
}

function trySplitLongClause(s: string, maxLen: number): [string, string] | null {
  if (s.length <= maxLen) return null;
  const minLeft = 36;
  const minRight = 26;
  const upper = Math.min(maxLen, s.length - minRight);

  const tryComma = (sep: string): [string, string] | null => {
    for (let i = minLeft; i <= upper; i++) {
      if (s.slice(i, i + sep.length) === sep) {
        const left = s.slice(0, i).trim();
        const right = s.slice(i + sep.length).trim();
        if (left.length >= minLeft - 8 && right.length >= minRight) return [left, right];
      }
    }
    return null;
  };

  const c1 = tryComma(", ");
  if (c1) return c1;
  const c2 = tryComma("; ");
  if (c2) return c2;

  const mid = tryMidConjunctionSplit(s, minLeft, minRight);
  if (mid) return mid;
  return null;
}

function formatMidSplitRight(match: string, right: string): string {
  const r = right.trim();
  if (/\bnhưng\b/i.test(match)) return `Nhưng ${capitalizeFirstVi(r)}`;
  if (/\bsong\b/i.test(match)) return `Song ${capitalizeFirstVi(r)}`;
  if (/\btuy nhiên\b/i.test(match)) return `Tuy nhiên, ${capitalizeFirstVi(r)}`;
  if (/\bmà\b/i.test(match)) return `Mà ${capitalizeFirstVi(r)}`;
  if (/\bvà\b/i.test(match)) return `Và ${capitalizeFirstVi(r)}`;
  return capitalizeFirstVi(r);
}

function tryMidConjunctionSplit(s: string, minLeft: number, minRight: number): [string, string] | null {
  const patterns = [/\s+nhưng\s+/gi, /\s+song\s+/gi, /\s+tuy nhiên,\s+/gi, /\s+mà\s+/gi, /\s+và\s+/gi];
  for (const re of patterns) {
    const g = new RegExp(re.source, "gi");
    let m: RegExpExecArray | null;
    while ((m = g.exec(s)) !== null) {
      if (m.index < minLeft || s.length - m.index - m[0].length < minRight) continue;
      const left = s.slice(0, m.index).trim();
      const rightRaw = s.slice(m.index + m[0].length).trim();
      if (left.length < 20 || rightRaw.length < minRight) continue;
      return [left, formatMidSplitRight(m[0], rightRaw)];
    }
  }
  return null;
}

function breakLongSentence(sentence: string, maxLen = 84): string[] {
  const acc: string[] = [];
  let c = norm(sentence);
  if (!c || c.length < 10) return [];
  while (c.length > maxLen) {
    const sp = trySplitLongClause(c, maxLen);
    if (!sp) {
      acc.push(ensureTerminalPunct(c));
      return acc;
    }
    const [L, R] = sp;
    acc.push(ensureTerminalPunct(L));
    c = capitalizeFirstVi(norm(R));
  }
  acc.push(ensureTerminalPunct(c));
  return acc.map(norm).filter((s) => s.length >= 10);
}

function splitRawSentences(text: string): string[] {
  const t = norm(text);
  if (!t) return [];
  return filterFullSentences(t.split(FULL_SENTENCE_SPLIT));
}

function dedupeConsecutiveSentences(sentences: readonly string[]): string[] {
  const out: string[] = [];
  const normKey = (x: string) => norm(x).toLowerCase();
  for (const s of sentences) {
    const k = normKey(s);
    if (!k) continue;
    const prev = out[out.length - 1];
    if (prev && normKey(prev) === k) continue;
    if (prev) {
      const pk = normKey(prev);
      const L = 48;
      if (k.length >= L && pk.length >= L && k.slice(0, L) === pk.slice(0, L)) continue;
      if (pk.length >= 18 && k.length >= 18) {
        if (pk.includes(k) && k.length <= pk.length * 0.88) continue;
        if (k.includes(pk) && pk.length <= k.length * 0.88) {
          out[out.length - 1] = norm(s);
          continue;
        }
      }
    }
    out.push(norm(s));
  }
  return out;
}

function softenPhaseTone(t: string): string {
  let s = norm(t);
  s = s.replace(/\bchắc chắn là\b/gi, "có thể là");
  s = s.replace(/\bchắc chắn\b/gi, "thường");
  s = s.replace(/\bchắc hẳn\b/gi, "nhiều khi");
  s = s.replace(/\bnhất định\b/gi, "có thể");
  s = s.replace(/\btất nhiên\b/gi, "nhiều khi");
  return norm(s);
}

type HumanizeChunkOpts = { phase?: boolean };

function humanizeChunk(raw: string, opts: HumanizeChunkOpts): string {
  let t = stripInternalPhrases(raw);
  t = applyStructuralBreaks(t);
  if (opts.phase) t = softenPhaseTone(t);
  const rawSents = splitRawSentences(t);
  const expanded = rawSents.flatMap((s) => breakLongSentence(s, 84));
  const deduped = dedupeConsecutiveSentences(expanded);
  const bridged = opts.phase ? deduped : insertSoftConnectors(deduped);
  let out = norm(bridged.join(" "));
  out = stripTherapistCadenceParagraphUi(out);
  const s2 = splitRawSentences(out).map(stripTherapistCadenceParagraphUi).filter(Boolean);
  return norm(trimChunkSentenceBudgetUi(s2).join(" "));
}

/**
 * Chuỗi xử lý hiển thị: ngắt câu, gỡ ngôn ngữ nội bộ, nối mềm cố định, giữ thứ tụ khối `\n\n` (chunk 0–7).
 * Hoàn toàn deterministic — không gọi model, không đổi logic luận backend.
 */
export function humanizeOverviewText(text: string): string {
  const stabilized = ensureBodySentenceIntegrity(text);
  const rawChunks = stabilized.split(/\n\n+/).map((c) => c.trim()).filter(Boolean);
  return rawChunks.map((chunk, i) => humanizeChunk(chunk, { phase: i === 6 })).join("\n\n");
}

function splitSentences(text: string): string[] {
  const t = norm(text);
  if (!t) return [];
  return filterFullSentences(t.split(FULL_SENTENCE_SPLIT));
}

/**
 * Góp các câu trọn vẹn vào đoạn: thêm câu khi chưa vượt ngân sách ký tự;
 * không bao giờ cắt giữa câu. Câu đơn dài vẫn giữ nguyên một đoạn.
 */
function packSentencesIntoParagraphs(
  sentences: readonly string[],
  maxParagraphs: number,
  maxCharsPerParagraph: number,
): string[] {
  if (!sentences.length) return [];
  const out: string[] = [];
  let idx = 0;
  for (let p = 0; p < maxParagraphs && idx < sentences.length; p++) {
    const isLastSlot = p === maxParagraphs - 1;
    if (isLastSlot) {
      out.push(norm(sentences.slice(idx).join(" ")));
      break;
    }
    const first = sentences[idx]!;
    if (first.length > maxCharsPerParagraph) {
      out.push(first);
      idx += 1;
      continue;
    }
    let acc = first;
    idx += 1;
    while (idx < sentences.length) {
      const s = sentences[idx]!;
      const candidate = `${acc} ${s}`;
      if (candidate.length > maxCharsPerParagraph) break;
      acc = candidate;
      idx += 1;
    }
    out.push(norm(acc));
  }
  return out.filter(Boolean);
}

/** Đoạn văn chỉ gồm câu trọn vẹn; đoạn cuối gom phần còn lại. */
function toShortParagraphs(block: string, maxParagraphs = 4, maxCharsPerParagraph = 380): string[] {
  const sents = splitSentences(block);
  const b = norm(block);
  if (!sents.length) return b.length >= 10 ? [b] : [];
  return packSentencesIntoParagraphs(sents, maxParagraphs, maxCharsPerParagraph);
}

type SectionSpec = { title: string; chunkIndices: readonly number[]; highlight?: boolean };

const SECTION_SPECS: readonly SectionSpec[] = [
  { title: "Nhận diện bản thân", chunkIndices: [0, 1] },
  { title: "Sự nghiệp & tài chính", chunkIndices: [2, 3] },
  { title: "Tình cảm & gia đình", chunkIndices: [4] },
  { title: "Sức khỏe & áp lực", chunkIndices: [5] },
  { title: "Giai đoạn hiện tại", chunkIndices: [6], highlight: true },
] as const;

const FOOTER_CHUNK_INDEX = 7;

function mergeChunks(chunks: readonly string[], indices: readonly number[]): string {
  const parts = indices.map((i) => chunks[i]).filter((p): p is string => Boolean(p && norm(p)));
  return norm(parts.join(" "));
}

const THERAPIST_CADENCE_STRIP_UI: readonly RegExp[] = [
  /\bKhông phải lúc nào cũng thế,?\s*(nhưng\s+)?/gi,
  /\bThường thì,?\s*/gi,
  /\bĐiều này khiến\s+/gi,
  /\bVì vậy,?\s+/gi,
];

function stripTherapistCadenceParagraphUi(t: string): string {
  let s = norm(t);
  for (const re of THERAPIST_CADENCE_STRIP_UI) s = norm(s.replace(re, " "));
  return norm(s.replace(/\s*,\s*,/g, ",").replace(/^\s*,\s*/g, ""));
}

function trimChunkSentenceBudgetUi(sents: readonly string[]): string[] {
  if (sents.length <= 3) return [...sents];
  const ok = (x: string) => x.split(/\s+/).filter(Boolean).length >= 7 && norm(x).length >= 22;
  if (ok(sents[0]!) && ok(sents[1]!)) return [sents[0]!, sents[1]!];
  return sents.slice(0, 3);
}

function stabilizeServerOverviewParagraph(raw: string): string {
  let t = stripInternalPhrases(raw);
  t = applyStructuralBreaks(t);
  t = stripTherapistCadenceParagraphUi(t);
  const sents = splitRawSentences(t).map(stripTherapistCadenceParagraphUi).filter(Boolean);
  return norm(trimChunkSentenceBudgetUi(sents).join(" "));
}

type UiSectionBucket = "identity" | "career" | "relationship" | "health" | "timing" | "strategy" | "phase" | "closing";

function uiBucketForRole(role: string): UiSectionBucket {
  if (role === "opening" || role === "identity") return "identity";
  if (role === "career") return "career";
  if (role === "relationship") return "relationship";
  if (role === "health") return "health";
  if (role === "timing") return "timing";
  if (role === "strategy") return "strategy";
  if (role === "phase") return "phase";
  return "closing";
}

const BUCKET_TITLE: Record<UiSectionBucket, string> = {
  identity: "Nhận diện bản thân",
  career: "Sự nghiệp & tài chính",
  relationship: "Tình cảm & gia đình",
  health: "Sức khỏe & áp lực",
  timing: "Vận hạn & thời điểm",
  strategy: "Chiến lược & cải mệnh",
  phase: "Giai đoạn hiện tại",
  closing: "Chốt & lời kết",
};

const LAYERED_CANONICAL_TO_BUCKET: Record<string, UiSectionBucket> = {
  core_identity: "identity",
  career_finance: "career",
  relationships: "relationship",
  inner_health: "health",
  major_period: "timing",
  annual_period: "timing",
  timing_windows: "timing",
  strategic_summary: "strategy",
  closing: "closing",
};

const LAYERED_ID_TO_BUCKET: Record<string, UiSectionBucket> = {
  chan_dung_ban_menh: "identity",
  su_nghiep_tai_chinh: "career",
  gia_dao_nhan_duyen: "relationship",
  noi_tam_suc_khoe: "health",
  dai_van_hien_tai: "timing",
  van_nam_hien_tai: "timing",
  giai_doan_dang_chu_y: "timing",
  tong_ket_chien_luoc: "strategy",
  loi_ket: "closing",
};

function uiBucketForLayeredSection(sec: { readonly id: string; readonly canonicalKey?: string }): UiSectionBucket {
  const ck = sec.canonicalKey?.trim();
  if (ck && LAYERED_CANONICAL_TO_BUCKET[ck]) return LAYERED_CANONICAL_TO_BUCKET[ck]!;
  return LAYERED_ID_TO_BUCKET[sec.id] ?? "closing";
}

/** Chín mục phễu từ backend `layeredSections` — tiêu đề do engine đặt. */
function buildStructuredSectionsFromLayered(
  layers: readonly {
    readonly id: string;
    readonly title: string;
    readonly paragraphs: readonly string[];
    readonly canonicalKey?: string;
  }[],
): {
  sections: {
    title: string;
    roleBucketLabel: string;
    paragraphs: A4ParagraphV1[];
    highlight: boolean;
    timingVariant: "none" | "dai" | "annual" | "windows" | "strategy";
    sectionId?: string;
    canonicalKey?: string;
  }[];
  footer: string | null;
} {
  const sections: {
    title: string;
    roleBucketLabel: string;
    paragraphs: A4ParagraphV1[];
    highlight: boolean;
    timingVariant: "none" | "dai" | "annual" | "windows" | "strategy";
    sectionId?: string;
    canonicalKey?: string;
  }[] = [];
  for (const sec of layers) {
    if (!sec.paragraphs.length) continue;
    const bucket = uiBucketForLayeredSection(sec);
    const roleBucketLabel = BUCKET_TITLE[bucket];
    const ck = sec.canonicalKey?.trim();
    const isTimingWindows = sec.id === "giai_doan_dang_chu_y" || ck === "timing_windows";
    const isStrategy = sec.id === "tong_ket_chien_luoc" || ck === "strategic_summary";
    const maxChars = isTimingWindows ? 300 : isStrategy ? 320 : 360;
    const maxParas = isTimingWindows ? 3 : isStrategy ? 4 : 4;
    const paragraphs: A4ParagraphV1[] = [];
    for (const tx of sec.paragraphs) {
      const stab = stabilizeServerOverviewParagraph(tx);
      for (const chunk of toShortParagraphs(stab, maxParas, maxChars)) {
        paragraphs.push(stringToA4Tokens(chunk));
      }
    }
    if (!paragraphs.length) continue;
    const highlight = sec.id === "van_nam_hien_tai" || sec.id === "dai_van_hien_tai";
    const timingVariant =
      sec.id === "dai_van_hien_tai" || ck === "major_period" ? "dai"
      : sec.id === "van_nam_hien_tai" || ck === "annual_period" ? "annual"
      : isTimingWindows ? "windows"
      : isStrategy ? "strategy"
      : "none";
    const canonicalKey = ck || undefined;
    sections.push({
      title: sec.title,
      roleBucketLabel,
      paragraphs,
      highlight,
      timingVariant,
      sectionId: sec.id,
      canonicalKey,
    });
  }
  const last = sections[sections.length - 1];
  let footer: string | null = null;
  if (last?.title === "Lời kết") {
    const plain = last.paragraphs.map(paragraphToPlainText).join(" ");
    footer = plain.length >= 12 ? plain : null;
    sections.pop();
  }
  return { sections, footer };
}

/** Ghép mục A4 theo role backend — không dùng layout 8 chunk legacy. */
function buildStructuredSectionsFromServerRoles(
  body: string,
  roles: readonly string[],
): { sections: { title: string; roleBucketLabel: string; paragraphs: A4ParagraphV1[]; highlight: boolean; timingVariant: "none"; sectionId?: string; canonicalKey?: string }[]; footer: string | null } {
  const paras = body
    .split(/\n\n+/)
    .map((p) => norm(p))
    .filter(Boolean);
  if (paras.length === 0) return { sections: [], footer: null };
  if (paras.length !== roles.length) {
    return buildStructuredSections(body);
  }

  const order: UiSectionBucket[] = ["identity", "career", "relationship", "health", "timing", "strategy", "phase"];
  const sectionsMap = new Map<UiSectionBucket, string[]>();
  for (const b of order) sectionsMap.set(b, []);
  const footerParts: string[] = [];

  for (let i = 0; i < paras.length; i++) {
    const role = roles[i] ?? "closing";
    const bucket = uiBucketForRole(role);
    if (bucket === "closing") {
      footerParts.push(paras[i]!);
      continue;
    }
    sectionsMap.get(bucket)!.push(paras[i]!);
  }

  const sections: { title: string; roleBucketLabel: string; paragraphs: A4ParagraphV1[]; highlight: boolean; timingVariant: "none"; sectionId?: string; canonicalKey?: string }[] = [];
  for (const b of order) {
    const texts = sectionsMap.get(b)!;
    if (!texts.length) continue;
    const paragraphs = texts.map((tx) => stringToA4Tokens(stabilizeServerOverviewParagraph(tx)));
    sections.push({
      title: BUCKET_TITLE[b],
      roleBucketLabel: BUCKET_TITLE[b],
      paragraphs,
      highlight: b === "phase" || b === "timing",
      timingVariant: "none",
    });
  }

  const footerJoined = footerParts.length ? norm(footerParts.join(" ")) : null;
  const footer = footerJoined && footerJoined.length >= 12 ? footerJoined : null;
  return { sections, footer };
}

function buildStructuredSections(body: string): {
  sections: { title: string; roleBucketLabel: string; paragraphs: A4ParagraphV1[]; highlight: boolean; timingVariant: "none"; sectionId?: string; canonicalKey?: string }[];
  footer: string | null;
} {
  const layered = humanizeOverviewText(body);
  const chunks = layered
    .split(/\n\n+/)
    .map((p) => norm(p))
    .filter(Boolean);

  const sections = SECTION_SPECS.map((spec) => {
    const merged = mergeChunks(chunks, spec.chunkIndices);
    const paragraphs =
      merged.length > 0 ? toShortParagraphs(merged, 4, 380).map((p) => stringToA4Tokens(p)) : [];
    return {
      title: spec.title,
      roleBucketLabel: spec.title,
      paragraphs,
      highlight: Boolean(spec.highlight),
      timingVariant: "none" as const,
    };
  }).filter((s) => s.paragraphs.length > 0);

  const footerRaw = chunks[FOOTER_CHUNK_INDEX];
  const footer = footerRaw && norm(footerRaw).length >= 16 ? norm(footerRaw) : null;

  return { sections, footer };
}

function SectionBlock({
  title,
  roleBucketLabel,
  paragraphs,
  timingVariant,
}: {
  title: string;
  roleBucketLabel?: string;
  paragraphs: readonly A4ParagraphV1[];
  timingVariant?: "none" | "dai" | "annual" | "windows" | "strategy";
}) {
  const borderAccent =
    timingVariant === "windows" ? "border-l border-l-[color:rgba(120,52,48,0.38)] pl-4"
    : timingVariant === "strategy" ? "border-l border-l-[color:rgba(90,72,52,0.32)] pl-4"
    : "";
  return (
    <NarrativeSection className={borderAccent}>
      <NarrativeHeading variant="section" subtitle={roleBucketLabel}>
        {title}
      </NarrativeHeading>
      <div className="space-y-7 text-pretty md:space-y-8">
        {paragraphs.map((para, i) => {
          const plain = paragraphToPlainText(para);
          return (
            <NarrativeParagraph key={i} title={plain.length > 280 ? plain : undefined}>
              <A4ParagraphInline para={para} />
            </NarrativeParagraph>
          );
        })}
      </div>
    </NarrativeSection>
  );
}

function A4OverviewConversionContinuation({ onCta }: { onCta: () => void }) {
  return (
    <div
      className="mt-12 border-t border-[color:rgba(90,72,52,0.22)] pt-10 md:mt-14 md:pt-12"
      aria-label="Chuyển sang phân tích chi tiết"
    >
      <h3 className="text-center font-ctkp-serif text-[1.05rem] font-semibold tracking-[0.08em] text-[color:var(--ctkp-narrative-ink-strong)] sm:text-[1.1rem]">
        Bạn đang thấy đúng… nhưng chưa đủ.
      </h3>
      <p className="mx-auto mt-5 max-w-prose text-center font-ctkp-serif text-[16px] leading-[1.88] text-[color:var(--ctkp-narrative-ink)] text-pretty md:text-[17px] md:leading-[1.95]">
        Phần trên chỉ phản ánh bức tranh tổng quan. Những gì thực sự quan trọng nằm ở từng giai đoạn cụ thể — khi nào
        nên tiến, khi nào nên dừng, và điều gì đang tác động ở thời điểm hiện tại, trong các lớp vận hạn và lựa chọn
        cụ thể.
      </p>
      <p className="mx-auto mt-4 max-w-prose text-center font-ctkp-serif text-[14px] leading-[1.85] text-[color:var(--ctkp-narrative-caption)] text-pretty opacity-90 md:text-[15px]">
        Trong 1–3 tháng gần đây, có thể bạn đã bắt đầu cảm nhận rõ sự thay đổi này.
      </p>
      <div className="mt-8 flex justify-center">
        <button
          type="button"
          onClick={onCta}
          className="rounded-sm border border-[color:rgba(90,72,52,0.42)] bg-[rgba(255,252,245,0.55)] px-8 py-2.5 font-ctkp-serif text-[12px] font-medium tracking-[0.12em] text-[color:var(--ctkp-narrative-ink-strong)] transition-[background-color,border-color] duration-200 hover:border-[color:rgba(90,72,52,0.55)] hover:bg-[rgba(232,220,198,0.65)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:rgba(90,72,52,0.45)] active:translate-y-px sm:text-[13px]"
        >
          Xem phân tích chi tiết
        </button>
      </div>
    </div>
  );
}

function PhaseHighlightCard({
  title,
  paragraphs,
  badgeLabel,
}: {
  title: string;
  paragraphs: readonly A4ParagraphV1[];
  badgeLabel?: string;
}) {
  const showBadge = Boolean(badgeLabel?.trim());
  return (
    <div className="ctkp-narrative-phase-card px-5 py-6 sm:px-6 sm:py-7" role="region" aria-label={title}>
      {showBadge ?
        <>
          <h3 className="sr-only">{title}</h3>
          <div className="mb-5 flex flex-wrap items-center gap-2">
            <span className="ctkp-narrative-rubric inline-flex items-center rounded-sm border border-[color:rgba(120,52,48,0.32)] bg-[rgba(255,250,242,0.65)] px-3 py-1 font-ctkp-serif text-[10px] font-semibold uppercase tracking-[0.22em] text-[color:var(--ctkp-narrative-caption)] sm:text-[11px]">
              {badgeLabel}
            </span>
          </div>
        </>
      : <NarrativeHeading variant="section" className="mb-5">{title}</NarrativeHeading>}
      <div className="space-y-7 text-pretty md:space-y-8">
        {paragraphs.map((para, i) => {
          const plain = paragraphToPlainText(para);
          return (
            <NarrativeParagraph key={i} title={plain.length > 280 ? plain : undefined}>
              <A4ParagraphInline para={para} />
            </NarrativeParagraph>
          );
        })}
      </div>
    </div>
  );
}

type ServerOverviewShape = NonNullable<Props["serverOverview"]>;

function TuViStrategicRecommendationsCompact({
  block,
  blockPlain,
  funnelTelemetry,
}: {
  block: NonNullable<ServerOverviewShape["strategicRecommendationsA4"]>;
  blockPlain: string | undefined;
  funnelTelemetry: Props["funnelTelemetry"];
}) {
  const expandedOnce = useRef(false);
  const bullets = block.bullets;
  const n = bullets.length;
  const onToggle = useCallback(
    (e: React.ToggleEvent<HTMLDetailsElement>) => {
      if (!e.currentTarget.open || expandedOnce.current) return;
      if (!isSoftLaunchClientEnabled()) {
        expandedOnce.current = true;
        return;
      }
      expandedOnce.current = true;
      const cid = funnelTelemetry?.chartId?.trim();
      queueTuViFunnelTelemetry({
        eventType: TUVI_FUNNEL_EVENT.recommendation_expand,
        chartId: cid || undefined,
        payload: {
          source: "tu_vi_a4_strategic",
          viewingYearSolar: funnelTelemetry?.viewingYearSolar,
          strategicRecBulletCount: n,
        },
      });
    },
    [funnelTelemetry?.chartId, funnelTelemetry?.viewingYearSolar, n],
  );

  const onCopy = useCallback(async () => {
    const text =
      blockPlain?.trim().length ?
        blockPlain
      : `${block.title}\n${bullets.map((b) => `• ${b.endsWith(".") ? b : `${b}.`}`).join("\n")}`;
    if (isSoftLaunchClientEnabled()) {
      const cid = funnelTelemetry?.chartId?.trim();
      queueTuViFunnelTelemetry({
        eventType: TUVI_FUNNEL_EVENT.recommendation_copy,
        chartId: cid || undefined,
        payload: {
          source: "tu_vi_a4_strategic",
          viewingYearSolar: funnelTelemetry?.viewingYearSolar,
          strategicRecBulletCount: n,
        },
      });
    }
    try {
      await navigator.clipboard.writeText(text.trim());
    } catch {
      /* ignore */
    }
  }, [block.title, blockPlain, bullets, funnelTelemetry?.chartId, funnelTelemetry?.viewingYearSolar, n]);

  return (
    <div className="rounded-lg border border-[color:rgba(90,72,52,0.22)] bg-[rgba(8,10,18,0.28)] px-4 py-5 md:px-6">
      <details className="group" onToggle={onToggle}>
        <summary className="cursor-pointer list-none font-ctkp-serif text-[0.95rem] font-semibold tracking-[0.06em] text-[color:var(--ctkp-narrative-ink-strong)] marker:content-none [&::-webkit-details-marker]:hidden">
          <span className="inline-flex w-full items-start justify-between gap-3">
            <span className="text-pretty">{block.title}</span>
            <span className="shrink-0 pt-0.5 font-sans text-[10px] font-medium uppercase tracking-[0.18em] text-[color:var(--ctkp-narrative-caption)] group-open:hidden">
              Mở rộng
            </span>
            <span className="hidden shrink-0 pt-0.5 font-sans text-[10px] font-medium uppercase tracking-[0.18em] text-[color:var(--ctkp-narrative-caption)] group-open:inline">
              Thu gọn
            </span>
          </span>
        </summary>
        <ul className="mt-4 list-disc space-y-2.5 pl-5 font-ctkp-serif text-[14px] leading-[1.8] text-[color:var(--ctkp-narrative-ink)] text-pretty sm:text-[15px]">
          {bullets.map((line, i) => (
            <li key={`sr-${i}`}>{line.endsWith(".") ? line : `${line}.`}</li>
          ))}
        </ul>
        <div className="mt-4 flex flex-wrap gap-2 border-t border-[color:rgba(90,72,52,0.15)] pt-4">
          <button
            type="button"
            onClick={() => void onCopy()}
            className="rounded-md border border-[color:rgba(90,72,52,0.35)] bg-[rgba(14,16,24,0.5)] px-3 py-1.5 font-sans text-[11px] font-medium uppercase tracking-[0.14em] text-[color:var(--ctkp-narrative-ink)] transition hover:border-[color:rgba(90,72,52,0.55)]"
          >
            Sao chép khối
          </button>
        </div>
      </details>
    </div>
  );
}

export function TuViA4OverviewSection({
  serverOverview,
  overviewPalaces,
  composedSections,
  composedFooter,
  body,
  fallbackMessage,
  className,
  onContinueToDetailedAnalysis,
  overviewPipelineDebug,
  softLaunchReading,
  premiumTeaser,
  premiumDeepReport = null,
  premiumSavedReadingId,
  funnelTelemetry,
}: Props) {
  const fromLayered =
    serverOverview?.layeredSections && serverOverview.layeredSections.length > 0 ?
      buildStructuredSectionsFromLayered(serverOverview.layeredSections)
    : null;

  const fromServer =
    !fromLayered && serverOverview && norm(serverOverview.body).length >= 12 ?
      buildStructuredSectionsFromServerRoles(serverOverview.body, serverOverview.paragraphRoles)
    : null;

  const fromComposed =
    composedSections && composedSections.length > 0 ?
      {
        sections: composedSections.map((s) => ({
          title: s.title,
          roleBucketLabel: "",
          paragraphs: [...s.paragraphs],
          highlight: Boolean(s.highlight),
          timingVariant: "none" as const,
          sectionId: undefined,
          canonicalKey: undefined,
        })),
        footer: composedFooter && norm(composedFooter).length >= 12 ? norm(composedFooter) : null,
      }
    : null;

  const structured = !fromComposed && !fromLayered && !fromServer && body ? buildStructuredSections(body) : null;
  const resolved = fromComposed ?? fromLayered ?? fromServer ?? structured;
  const hasContent = resolved && resolved.sections.length > 0;
  const layeredForVisuals = serverOverview?.layeredSections;

  return (
    <section aria-labelledby="tuvi-khai-quat-heading" className={`motion-safe:ctkp-fade-in-slow ${className ?? ""}`}>
      <NarrativeContainer>
        <NarrativeHeading variant="masthead" id="tuvi-khai-quat-heading">
          Luận giải khái quát
        </NarrativeHeading>

        <div className="mt-10 md:mt-12">
          {hasContent ?
            <div className="flex flex-col gap-14 md:gap-16">
              {resolved!.sections.map((sec) => {
                const metaLabel =
                  sec.roleBucketLabel && !titlesRoughlyMatch(sec.title, sec.roleBucketLabel) ?
                    sec.roleBucketLabel
                  : undefined;
                const sectionKey = buildSoftLaunchOverviewSectionKey(sec);
                const sectionBody = (
                  <>
                    {sec.sectionId === "giai_doan_dang_chu_y" && layeredForVisuals?.length ?
                      <TuViTimingMomentumStrip layeredSections={layeredForVisuals} className="mb-4" />
                    : null}
                    {sec.sectionId === "tong_ket_chien_luoc" && overviewPalaces?.length ?
                      <TuViPalaceFocusHeatmap
                        palaceRing={overviewPalaces}
                        layeredSections={layeredForVisuals}
                        interpretationSignals={serverOverview?.interpretationSignals}
                        className="mb-4"
                      />
                    : null}
                    {sec.highlight ?
                      <PhaseHighlightCard title={sec.title} badgeLabel={metaLabel} paragraphs={sec.paragraphs} />
                    : <SectionBlock
                        title={sec.title}
                        roleBucketLabel={metaLabel}
                        timingVariant={sec.timingVariant}
                        paragraphs={sec.paragraphs}
                      />}
                    {sec.sectionId === "chan_dung_ban_menh" && serverOverview?.interpretationSignals?.length ?
                      <TuViStrategicSignalBars
                        signals={serverOverview.interpretationSignals}
                        className="mt-4 print:mt-3"
                      />
                    : null}
                  </>
                );
                return (
                  <Fragment key={`${sec.sectionId ?? sec.title}-${sec.timingVariant}`}>
                    {softLaunchReading ?
                      <SoftLaunchOverviewSectionChrome
                        sectionKey={sectionKey}
                        chartId={softLaunchReading.chartId}
                        chartFingerprint={softLaunchReading.chartFingerprint}
                      >
                        {sectionBody}
                      </SoftLaunchOverviewSectionChrome>
                    : sectionBody}
                  </Fragment>
                );
              })}
              {serverOverview?.strategicRecommendationsA4?.bullets?.length ?
                <TuViStrategicRecommendationsCompact
                  block={serverOverview.strategicRecommendationsA4}
                  blockPlain={serverOverview.strategicRecommendationsBlockPlain}
                  funnelTelemetry={funnelTelemetry}
                />
              : null}
              {resolved!.footer ?
                <p className="border-t border-[color:rgba(90,72,52,0.18)] pt-6 text-center font-ctkp-serif text-[14px] italic leading-[1.85] text-[color:var(--ctkp-narrative-caption)] md:text-[15px]">
                  {resolved!.footer}
                </p>
              : null}
              {premiumTeaser ?
                <TuViA4PremiumConversion
                  teaser={premiumTeaser}
                  premiumSavedReadingId={premiumSavedReadingId}
                  funnelChartId={funnelTelemetry?.chartId ?? null}
                  funnelViewingYearSolar={funnelTelemetry?.viewingYearSolar ?? null}
                  strategicRecommendationBulletCount={
                    serverOverview?.strategicRecommendationsA4?.bullets.length ?? 0
                  }
                  premiumDeepReport={premiumDeepReport}
                  className="mt-10 md:mt-12"
                />
              : null}
              {onContinueToDetailedAnalysis ?
                <A4OverviewConversionContinuation onCta={onContinueToDetailedAnalysis} />
              : null}
            </div>
          : <p className="text-center font-ctkp-serif text-[16px] leading-[1.85] text-[color:var(--ctkp-narrative-caption)] md:text-[17px]">
              {fallbackMessage}
            </p>}
        </div>
      </NarrativeContainer>

      {process.env.NODE_ENV === "development" && overviewPipelineDebug ?
        <details className="mt-6 rounded-lg border border-cyan-900/35 bg-[#050a12]/80 px-3 py-2 font-mono text-[10px] leading-relaxed text-cyan-100/88">
          <summary className="cursor-pointer select-none font-sans text-[11px] text-cyan-200/95">
            Overview pipeline (dev)
          </summary>
          <dl className="mt-2 space-y-1">
            <div>
              <dt className="inline text-cyan-300/90">overviewSource </dt>
              <dd className="inline">{overviewPipelineDebug.overviewSource}</dd>
            </div>
            <div>
              <dt className="inline text-cyan-300/90">semanticLayerApplied </dt>
              <dd className="inline">{String(overviewPipelineDebug.semanticLayerApplied)}</dd>
            </div>
            <div>
              <dt className="inline text-cyan-300/90">semanticReplacementCount </dt>
              <dd className="inline">{overviewPipelineDebug.semanticReplacementCount}</dd>
            </div>
          </dl>
          {overviewPipelineDebug.semanticReplacementsSample.length ?
            <ol className="mt-2 list-decimal pl-4">
              {overviewPipelineDebug.semanticReplacementsSample.slice(0, 10).map((r, i) => (
                <li key={i} className="mt-1">
                  <span className="text-cyan-400/90">{r.semanticKey}</span>: «{r.matched}» → «{r.rendered}»
                </li>
              ))}
            </ol>
          : null}
        </details>
      : null}

      {process.env.NODE_ENV === "development" && serverOverview?.interpretationSignals?.length ?
        <details className="mt-4 rounded-lg border border-violet-900/40 bg-[#0a0518]/88 px-3 py-2 font-mono text-[10px] leading-relaxed text-violet-100/90">
          <summary className="cursor-pointer select-none font-sans text-[11px] text-violet-200/95">
            Interpretation signals (dev)
          </summary>
          <table className="mt-2 w-full border-collapse text-left">
            <thead>
              <tr className="border-b border-violet-800/50 text-violet-300/95">
                <th className="py-1 pr-2 font-normal">Nhãn</th>
                <th className="py-1 pr-2 font-normal">Giá trị</th>
                <th className="py-1 pr-2 font-normal">Độ tin</th>
                <th className="py-1 font-normal">Drivers (top)</th>
              </tr>
            </thead>
            <tbody>
              {serverOverview.interpretationSignals.map((s) => (
                <tr key={s.key} className="border-b border-violet-900/30 align-top">
                  <td className="py-1 pr-2">{s.label}</td>
                  <td className="py-1 pr-2">{s.value}</td>
                  <td className="py-1 pr-2">{s.confidence}</td>
                  <td className="py-1 text-violet-200/85">{s.drivers.slice(0, 2).join(" · ")}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </details>
      : null}
    </section>
  );
}
