<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from "vue";

type Product = {
  product_id: string;
  product_version_id: string;
  product_code: string;
  name: string;
  summary: string;
  version_no?: string;
  plans: Array<{ id: string; code: string; name: string }>;
};

type AgentAction = {
  type:
    | "open_enrollment"
    | "open_orders"
    | "open_policies"
    | "open_surrenders"
    | "none";
  label: string;
  payload: Record<string, unknown>;
  confirmation_required: boolean;
};
type RuntimeEvent = {
  event:
    | "run.started"
    | "tool.completed"
    | "ui.ready"
    | "run.completed"
    | "run.failed";
  run_id: string;
  sequence: number;
  data: Record<string, unknown>;
};
type KnowledgeSource = {
  document_id: string;
  document_version: number;
  title: string;
  source_name: string;
  excerpt: string;
  excerpts?: string[];
  score: number;
};

function normalizeKnowledgeSources(sources: KnowledgeSource[]): KnowledgeSource[] {
  const grouped = new Map<string, KnowledgeSource>();
  for (const source of sources) {
    const key = `${source.document_id}:${source.document_version}`;
    const excerpts = source.excerpts?.length
      ? source.excerpts
      : [source.excerpt];
    const existing = grouped.get(key);
    if (!existing) {
      const normalizedExcerpts = [...new Set(excerpts.filter(Boolean))].slice(0, 2);
      grouped.set(key, {
        ...source,
        excerpt: normalizedExcerpts[0] ?? source.excerpt,
        excerpts: normalizedExcerpts,
      });
      continue;
    }
    const mergedExcerpts = [
      ...(existing.excerpts ?? [existing.excerpt]),
      ...excerpts,
    ];
    existing.excerpts = [...new Set(mergedExcerpts.filter(Boolean))].slice(0, 2);
    existing.excerpt = existing.excerpts[0] ?? existing.excerpt;
    existing.score = Math.max(existing.score, source.score);
  }
  return [...grouped.values()];
}

type ChatMessage = {
  role: "user" | "assistant";
  text: string;
  html?: string;
  products?: Product[];
  sources?: KnowledgeSource[];
  actions?: AgentAction[];
  enrollmentContext?: {
    age?: number;
    relationship?: Relationship;
  };
};
type AgentThreadSummary = {
  id: string;
  title: string;
  status: string;
  message_count: number;
  last_message: {
    role: "USER" | "ASSISTANT";
    content: string;
    created_at: string;
  } | null;
  created_at: string;
  updated_at: string;
};
type MainTab = "advisor" | "plans" | "coverage" | "services";
type Relationship = "SELF" | "PARENT" | "SPOUSE" | "CHILD";
type Quote = {
  quote_id: string;
  product_id: string;
  plan_id: string;
  premium_cents: number;
  currency: string;
  expires_at: string;
};
type Order = {
  order_id: string;
  order_no: string;
  status: string;
  amount_cents: number;
};
type Payment = {
  payment_id: string;
  payment_no: string;
  status: string;
};
type Policy = {
  policy_id: string;
  policy_no: string;
  order_id: string;
  order_no: string;
  product_name: string;
  plan_name: string;
  applicant: { name?: string; id_no?: string };
  insured: { name?: string; id_no?: string };
  status: string;
  premium_cents: number;
  coverage_start: string;
  coverage_end: string;
  issued_at: string;
};
type MaterialRecognition = {
  recognition_id: string;
  policy_id: string;
  material_type: string;
  source_name: string;
  fields: Record<string, string>;
  warnings: string[];
  status: string;
  created_at: string;
};
type ServiceRequestItem = {
  request_id: string;
  request_no: string;
  policy_id: string;
  request_type: string;
  recognition_id: string;
  confirmed_fields: Record<string, string>;
  status: string;
  submitted_at: string;
  reviewed_by?: string | null;
  review_note?: string | null;
  reviewed_at?: string | null;
};
type SurrenderRequestItem = {
  request_id: string;
  request_no: string;
  policy_id: string;
  reason: string;
  status: string;
  refund_amount_cents: number;
  currency: string;
  calculation?: {
    pre_effective?: boolean;
    cooling_off?: boolean;
    elapsed_days?: number;
    remaining_days?: number;
    gross_unearned_cents?: number;
    deduction_cents?: number;
  };
  risk_level?: string | null;
  risk_reasons?: string[];
  created_at: string;
  confirmed_at?: string | null;
  review_note?: string | null;
};
type SurrenderPreview = {
  policy_id: string;
  policy_no: string;
  eligible: boolean;
  reason_code: string;
  reason: string;
  refund_amount_cents: number;
  refund_basis?:
    | "PRE_EFFECTIVE_FULL_REFUND"
    | "COOLING_OFF_FULL_REFUND"
    | "UNEARNED_PREMIUM_WITH_DEDUCTION";
  refund_explanation?: string;
  currency: string;
  rule_version: string;
  calculation: {
    premium_cents: number;
    total_days: number;
    elapsed_days: number;
    remaining_days: number;
    gross_unearned_cents: number;
    deduction_cents: number;
    cooling_off: boolean;
    pre_effective: boolean;
  };
};

const apiBase =
  import.meta.env.VITE_API_BASE_URL ?? "http://127.0.0.1:8000/api/v1";
const mobile = ref("");
const code = ref("");
const imageCode = ref("");
const imageCodeInput = ref("");
const agreementAccepted = ref(false);
const codeCountdown = ref(0);
const codeRequested = ref(false);
let countdownTimer: ReturnType<typeof setInterval> | undefined;
const token = ref("");
const referralCode = new URLSearchParams(window.location.search)
  .get("ref")
  ?.trim()
  .toUpperCase() ?? "";
const contactMobile = ref("");
const products = ref<Product[]>([]);
const error = ref("");
const codeMessage = ref("");
const requestingCode = ref(false);
const loggingIn = ref(false);
const refreshing = ref(false);
const restoring = ref(true);
const agentInput = ref("我想给65岁的父亲买医疗险，常住成都，普通职业。");
const agentThreadId = ref("");
const agentMessages = ref<ChatMessage[]>([]);
const agentThreads = ref<AgentThreadSummary[]>([]);
const conversationHistoryOpen = ref(false);
const conversationHistoryLoading = ref(false);
const agentLoading = ref(false);
const agentProgress = ref("");
const serviceNo = ref("");
const activeTab = ref<MainTab>("advisor");
const coverageSection = ref<"policies" | "surrenders">("policies");
const selectedProduct = ref<Product | null>(null);
const quote = ref<Quote | null>(null);
const order = ref<Order | null>(null);
const payment = ref<Payment | null>(null);
const policy = ref<Policy | null>(null);
const policies = ref<Policy[]>([]);
const openedPolicy = ref<Policy | null>(null);
const serviceRequests = ref<ServiceRequestItem[]>([]);
const surrenderRequests = ref<SurrenderRequestItem[]>([]);
const surrenderApplicationOpen = ref(false);
const surrenderLoading = ref(false);
const surrenderError = ref("");
const selectedSurrenderPolicyId = ref("");
const surrenderPreview = ref<SurrenderPreview | null>(null);
const surrenderReason = ref("保障计划调整");
const surrenderReasonDetail = ref("");
const surrenderConfirmed = ref(false);
const serviceApplicationOpen = ref(false);
const serviceLoading = ref(false);
const serviceError = ref("");
const selectedServicePolicyId = ref("");
const serviceFile = ref<File | null>(null);
const serviceFilePreview = ref("");
const materialRecognition = ref<MaterialRecognition | null>(null);
const serviceConfirmed = ref(false);
const serviceFields = ref({
  invoice_no: "",
  hospital_name: "",
  visit_date: "",
  amount_yuan: "",
  contact_mobile: "",
});
const businessLoading = ref(false);
const quotingProductId = ref("");
const quoteFeedback = ref("");
const enrollmentOpen = ref(false);
const preQuoteConfirming = ref(false);
const enrollmentStep = ref(1);
const applicantName = ref("");
const applicantIdNo = ref("");
const insuredName = ref("");
const insuredIdNo = ref("");
const insuredAge = ref(65);
const relationship = ref<Relationship>("PARENT");
const noticeAccepted = ref(false);
const disclosureConfirmed = ref(false);
const canRequestCode = computed(
  () => !requestingCode.value && codeCountdown.value === 0,
);
const displayedProducts = computed(() => {
  if (!selectedProduct.value) return products.value;
  return [
    selectedProduct.value,
    ...products.value.filter(
      (product) => product.product_id !== selectedProduct.value?.product_id,
    ),
  ];
});
const selectedServicePolicy = computed(
  () =>
    policies.value.find(
      (item) => item.policy_id === selectedServicePolicyId.value,
    ) ?? null,
);
const selectedSurrenderPolicy = computed(
  () =>
    policies.value.find(
      (item) => item.policy_id === selectedSurrenderPolicyId.value,
    ) ?? null,
);

function money(cents: number) {
  return `¥${(cents / 100).toFixed(2)}`;
}

function escapeHtml(value: string) {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

function renderInlineMarkdown(value: string) {
  return escapeHtml(value)
    .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
    .replace(/`([^`]+)`/g, "<code>$1</code>");
}

function markdownCells(line: string) {
  return line
    .replace(/^\s*\|/, "")
    .replace(/\|\s*$/, "")
    .split("|")
    .map((cell) => cell.trim());
}

function renderAgentMarkdown(source: string) {
  const lines = source.replace(/\r\n?/g, "\n").split("\n");
  const output: string[] = [];
  let listItems: string[] = [];

  const flushList = () => {
    if (listItems.length === 0) return;
    output.push(`<ul>${listItems.map((item) => `<li>${item}</li>`).join("")}</ul>`);
    listItems = [];
  };

  for (let index = 0; index < lines.length; index += 1) {
    const line = lines[index].trim();
    if (!line || /^-{3,}$/.test(line)) {
      flushList();
      continue;
    }

    const nextLine = lines[index + 1]?.trim() ?? "";
    if (
      line.includes("|") &&
      /^\|?\s*:?-{3,}/.test(nextLine)
    ) {
      flushList();
      const headers = markdownCells(line);
      const rows: string[][] = [];
      index += 2;
      while (index < lines.length && lines[index].includes("|")) {
        rows.push(markdownCells(lines[index]));
        index += 1;
      }
      index -= 1;
      output.push(
        `<div class="response-table-wrap"><table><thead><tr>${headers
          .map((cell) => `<th>${renderInlineMarkdown(cell)}</th>`)
          .join("")}</tr></thead><tbody>${rows
          .map(
            (row) =>
              `<tr>${row
                .map((cell) => `<td>${renderInlineMarkdown(cell)}</td>`)
                .join("")}</tr>`,
          )
          .join("")}</tbody></table></div>`,
      );
      continue;
    }

    const heading = line.match(/^#{1,4}\s+(.+)$/);
    if (heading) {
      flushList();
      output.push(`<h4>${renderInlineMarkdown(heading[1])}</h4>`);
      continue;
    }

    const listItem = line.match(/^(?:[-*•]|\d+[.)])\s+(.+)$/);
    if (listItem) {
      listItems.push(renderInlineMarkdown(listItem[1]));
      continue;
    }

    const quoteLine = line.match(/^>\s*(.+)$/);
    if (quoteLine) {
      flushList();
      output.push(`<blockquote>${renderInlineMarkdown(quoteLine[1])}</blockquote>`);
      continue;
    }

    flushList();
    output.push(`<p>${renderInlineMarkdown(line)}</p>`);
  }

  flushList();
  return output.join("");
}

function dateTime(value: string) {
  return new Intl.DateTimeFormat("zh-CN", {
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    hour12: false,
  }).format(new Date(value));
}

function policyStatus(status: string) {
  return {
    ACTIVE: "保障中",
    PENDING: "待生效",
    EXPIRED: "已到期",
    SURRENDERING: "退保处理中",
    SURRENDERED: "已退保",
  }[status] ?? status;
}

function surrenderStatus(status: string) {
  return {
    PENDING_CONFIRMATION: "待确认",
    WAITING_APPROVAL: "等待审核",
    REJECTED: "审核未通过",
    REFUND_PENDING: "退款处理中",
    REFUND_RETRYING: "退款重试中",
    MANUAL_INTERVENTION: "人工处理中",
    REFUNDED: "退款完成",
    COMPENSATED: "已恢复保障",
  }[status] ?? status;
}

function surrenderStatusTone(status: string) {
  if (status === "REFUNDED" || status === "COMPENSATED") return "success";
  if (status === "REJECTED") return "danger";
  if (status === "MANUAL_INTERVENTION") return "warning";
  return "pending";
}

function serviceRequestStatus(status: string) {
  return {
    SUBMITTED: "待审核",
    ACCEPTED: "已受理",
    REJECTED: "未受理",
  }[status] ?? status;
}

function serviceRequestStatusTone(status: string) {
  if (status === "ACCEPTED") return "success";
  if (status === "REJECTED") return "danger";
  return "pending";
}

function policyById(policyId: string) {
  return policies.value.find((item) => item.policy_id === policyId);
}

function relationshipName(value: Relationship) {
  return {
    SELF: "本人",
    PARENT: "父母",
    SPOUSE: "配偶",
    CHILD: "子女",
  }[value];
}

function inferEnrollmentContext(text: string) {
  const ageMatch = text.match(/(\d{1,3})\s*岁/);
  const age = ageMatch ? Number(ageMatch[1]) : undefined;
  let inferredRelationship: Relationship | undefined;
  if (/(父亲|母亲|父母|爸爸|妈妈)/.test(text)) inferredRelationship = "PARENT";
  else if (/(爱人|配偶|丈夫|妻子|老公|老婆)/.test(text)) inferredRelationship = "SPOUSE";
  else if (/(孩子|子女|儿子|女儿)/.test(text)) inferredRelationship = "CHILD";
  else if (/(本人|自己|我想给我|为自己)/.test(text)) inferredRelationship = "SELF";
  return {
    age: age !== undefined && age >= 0 && age <= 120 ? age : undefined,
    relationship: inferredRelationship,
  };
}

function maskIdNo(value: string) {
  if (value.length < 10) return value;
  return `${value.slice(0, 6)}********${value.slice(-4)}`;
}

function createImageCode() {
  const alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
  const bytes = new Uint8Array(4);
  crypto.getRandomValues(bytes);
  imageCode.value = Array.from(
    bytes,
    (value) => alphabet[value % alphabet.length],
  ).join("");
  imageCodeInput.value = "";
}

function startCodeCountdown() {
  if (countdownTimer) clearInterval(countdownTimer);
  codeCountdown.value = 60;
  countdownTimer = setInterval(() => {
    codeCountdown.value -= 1;
    if (codeCountdown.value <= 0 && countdownTimer) {
      clearInterval(countdownTimer);
      countdownTimer = undefined;
    }
  }, 1000);
}

function validateMobile() {
  if (!/^1\d{10}$/.test(mobile.value)) {
    error.value = "请输入正确的11位手机号";
    return false;
  }
  return true;
}

async function apiFetch(path: string, init: RequestInit = {}) {
  return fetch(`${apiBase}${path}`, {
    ...init,
    credentials: "include",
  });
}

async function responseError(response: Response, fallback: string) {
  try {
    const body = await response.json();
    return body.error?.message ?? fallback;
  } catch {
    return fallback;
  }
}

async function authorizedFetch(path: string, init: RequestInit = {}, allowRefresh = true) {
  const headers = new Headers(init.headers);
  headers.set("Authorization", `Bearer ${token.value}`);
  const response = await apiFetch(path, { ...init, headers });
  if (response.status === 401 && allowRefresh && (await refreshAccessToken())) {
    return authorizedFetch(path, init, false);
  }
  return response;
}

async function requestCode() {
  if (requestingCode.value) return;
  error.value = "";
  codeMessage.value = "";
  if (!validateMobile()) return;
  if (
    imageCodeInput.value.trim().toUpperCase() !== imageCode.value.toUpperCase()
  ) {
    error.value = "图形验证码不正确，请重新输入";
    createImageCode();
    return;
  }
  requestingCode.value = true;
  try {
    const response = await apiFetch("/h5/auth/request-code", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ mobile: mobile.value }),
    });
    if (!response.ok) {
      throw new Error(await responseError(response, "验证码请求失败"));
    }
    const body = await response.json();
    const validMinutes = Math.max(1, Math.floor(body.data.expires_in / 60));
    codeRequested.value = true;
    codeMessage.value = `验证码已发送，${validMinutes}分钟内有效`;
    startCodeCountdown();
  } catch (reason) {
    error.value = reason instanceof Error ? reason.message : "请求失败";
  } finally {
    requestingCode.value = false;
  }
}

async function login() {
  if (loggingIn.value) return;
  error.value = "";
  if (!validateMobile()) return;
  if (!codeRequested.value) {
    error.value = "请先获取短信验证码";
    return;
  }
  if (!/^\d{6}$/.test(code.value)) {
    error.value = "请输入6位短信验证码";
    return;
  }
  if (!agreementAccepted.value) {
    error.value = "请阅读并同意服务协议与隐私政策";
    return;
  }
  loggingIn.value = true;
  try {
    const response = await apiFetch("/h5/auth/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        mobile: mobile.value,
        code: code.value,
        referral_code: referralCode || undefined,
      }),
    });
    if (!response.ok) {
      throw new Error(await responseError(response, "登录失败"));
    }
    const body = await response.json();
    token.value = body.data.tokens.access_token;
    mobile.value = body.data.user.mobile;
    contactMobile.value = body.data.user.mobile;
    code.value = "";
    codeMessage.value = "";
    await loadProducts();
    await loadBusiness();
    await loadAgentThreads();
  } catch (reason) {
    error.value = reason instanceof Error ? reason.message : "请求失败";
  } finally {
    loggingIn.value = false;
  }
}

async function refreshAccessToken() {
  const response = await apiFetch("/h5/auth/refresh", { method: "POST" });
  if (!response.ok) {
    token.value = "";
    products.value = [];
    return false;
  }
  const body = await response.json();
  token.value = body.data.tokens.access_token;
  mobile.value = body.data.user.mobile;
  contactMobile.value = body.data.user.mobile;
  return true;
}

async function loadProducts(allowRefresh = true) {
  const response = await apiFetch("/h5/products", {
    headers: { Authorization: `Bearer ${token.value}` },
  });
  if (response.status === 401 && allowRefresh && (await refreshAccessToken())) {
    return loadProducts(false);
  }
  if (!response.ok) {
    if (response.status === 401) token.value = "";
    throw new Error(await responseError(response, "产品加载失败"));
  }
  const body = await response.json();
  products.value = body.data.items;
}

async function loadAgentThreads() {
  if (!token.value) return;
  conversationHistoryLoading.value = true;
  try {
    const response = await authorizedFetch("/agent/threads?limit=30");
    if (!response.ok) {
      throw new Error(await responseError(response, "历史会话加载失败"));
    }
    agentThreads.value = (await response.json()).data.items;
  } finally {
    conversationHistoryLoading.value = false;
  }
}

async function openConversationHistory() {
  conversationHistoryOpen.value = true;
  error.value = "";
  try {
    await loadAgentThreads();
  } catch (reason) {
    error.value = reason instanceof Error ? reason.message : "历史会话加载失败";
  }
}

function startNewConversation() {
  agentThreadId.value = "";
  agentMessages.value = [];
  serviceNo.value = "";
  agentInput.value = "";
  conversationHistoryOpen.value = false;
  error.value = "";
}

async function restoreConversation(thread: AgentThreadSummary) {
  if (agentLoading.value) return;
  conversationHistoryLoading.value = true;
  error.value = "";
  try {
    const response = await authorizedFetch(
      `/agent/threads/${thread.id}/messages`,
    );
    if (!response.ok) {
      throw new Error(await responseError(response, "会话内容加载失败"));
    }
    const records = (await response.json()).data.items as Array<{
      role: "USER" | "ASSISTANT";
      content: string;
      metadata?: Record<string, any>;
    }>;
    let previousUserText = "";
    let restoredServiceNo = "";
    agentMessages.value = records.map((record) => {
      if (record.role === "USER") {
        previousUserText = record.content;
        return { role: "user", text: record.content };
      }
      const metadata = record.metadata ?? {};
      const cards = (metadata.cards ?? []) as Array<{
        type: string;
        items?: unknown[];
      }>;
      const products = cards
        .filter((card) => card.type === "product_recommendations")
        .flatMap((card) => card.items ?? []) as Product[];
      const sources = normalizeKnowledgeSources(
        cards
          .filter((card) => card.type === "knowledge_sources")
          .flatMap((card) => card.items ?? []) as KnowledgeSource[],
      );
      restoredServiceNo = String(metadata.service_no ?? restoredServiceNo);
      return {
        role: "assistant",
        text: record.content,
        html: renderAgentMarkdown(record.content),
        products: products.slice(0, 2),
        sources: sources.slice(0, 5),
        actions: (metadata.actions ?? []) as AgentAction[],
        enrollmentContext: inferEnrollmentContext(previousUserText),
      };
    });
    agentThreadId.value = thread.id;
    serviceNo.value = restoredServiceNo;
    conversationHistoryOpen.value = false;
    await nextTick();
    document
      .querySelector<HTMLElement>(".messages")
      ?.scrollTo({ top: 999999, behavior: "smooth" });
  } catch (reason) {
    error.value = reason instanceof Error ? reason.message : "会话内容加载失败";
  } finally {
    conversationHistoryLoading.value = false;
  }
}

async function sendAgentMessage() {
  const text = agentInput.value.trim();
  if (!text || agentLoading.value) return;
  agentLoading.value = true;
  agentProgress.value = "正在理解保障需求";
  error.value = "";
  const optimisticMessage = { role: "user" as const, text };
  agentMessages.value.push(optimisticMessage);
  agentInput.value = "";
  try {
    if (!agentThreadId.value) {
      const threadResponse = await authorizedFetch("/agent/threads", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ title: text.slice(0, 28) }),
      });
      if (!threadResponse.ok) {
        throw new Error(await responseError(threadResponse, "创建咨询会话失败"));
      }
      agentThreadId.value = (await threadResponse.json()).data.id;
    }
    const response = await authorizedFetch(
      `/agent/threads/${agentThreadId.value}/messages/stream`,
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          content: { type: "text", text },
          client_message_id: crypto.randomUUID(),
        }),
      },
    );
    if (!response.ok) {
      throw new Error(await responseError(response, "智能体响应失败"));
    }
    const data = await consumeAgentStream(response);
    const assistantText = data.assistant_message.text as string;
    const cards = (data.assistant_message.cards ?? []) as Array<{
      type: string;
      items?: unknown[];
    }>;
    const cardItems = cards
      .filter((card) => card.type === "product_recommendations")
      .flatMap((card) => card.items ?? []) as Product[];
    const knowledgeSources = normalizeKnowledgeSources(
      cards
        .filter((card) => card.type === "knowledge_sources")
        .flatMap((card) => card.items ?? []) as KnowledgeSource[],
    );
    const actions = (data.assistant_message.actions ?? []) as AgentAction[];
    agentMessages.value.push({
      role: "assistant",
      text: assistantText,
      html: renderAgentMarkdown(assistantText),
      enrollmentContext: inferEnrollmentContext(text),
      products: cardItems.slice(0, 2),
      sources: knowledgeSources.slice(0, 5),
      actions,
    });
    serviceNo.value = data.service_no ?? "";
    await loadAgentThreads();
  } catch (reason) {
    const optimisticIndex = agentMessages.value.lastIndexOf(optimisticMessage);
    if (optimisticIndex >= 0) agentMessages.value.splice(optimisticIndex, 1);
    if (!agentInput.value.trim()) agentInput.value = text;
    error.value = reason instanceof Error ? reason.message : "智能体响应失败";
  } finally {
    agentLoading.value = false;
    agentProgress.value = "";
  }
}

async function consumeAgentStream(response: Response) {
  if (!response.body) throw new Error("浏览器不支持流式响应");
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  let completed: Record<string, any> | null = null;
  while (true) {
    const { done, value } = await reader.read();
    buffer += decoder.decode(value, { stream: !done });
    const packets = buffer.split("\n\n");
    buffer = packets.pop() ?? "";
    for (const packet of packets) {
      const dataLine = packet
        .split("\n")
        .find((line) => line.startsWith("data: "));
      if (!dataLine) continue;
      const event = JSON.parse(dataLine.slice(6)) as RuntimeEvent;
      if (event.event === "tool.completed") {
        agentProgress.value = "正在检索已发布知识与业务规则";
      } else if (event.event === "ui.ready") {
        agentProgress.value = "正在整理可核验的答复";
      } else if (event.event === "run.failed") {
        throw new Error(String(event.data.message ?? "智能体响应失败"));
      } else if (event.event === "run.completed") {
        completed = event.data.result as Record<string, any>;
      }
    }
    if (done) break;
  }
  if (!completed) throw new Error("智能体运行未正常结束");
  return completed;
}

async function executeAgentAction(
  action: AgentAction,
  context?: ChatMessage["enrollmentContext"],
) {
  if (
    action.type === "open_orders" ||
    action.type === "open_policies"
  ) {
    activeTab.value = "coverage";
    coverageSection.value = "policies";
    await loadBusiness();
    return;
  }
  if (action.type === "open_surrenders") {
    activeTab.value = "coverage";
    coverageSection.value = "surrenders";
    await loadBusiness();
    return;
  }
  if (action.type !== "open_enrollment") return;
  const productCode =
    typeof action.payload.product_code === "string"
      ? action.payload.product_code
      : "";
  const product = products.value.find(
    (item) => item.product_code === productCode,
  );
  if (!product) {
    error.value = "推荐产品已不在当前在售目录，请重新咨询";
    return;
  }
  const age =
    typeof action.payload.age === "number"
      ? action.payload.age
      : context?.age;
  const rawRelationship = action.payload.relationship;
  const actionRelationship = (
    ["SELF", "PARENT", "SPOUSE", "CHILD"] as const
  ).find((item) => item === rawRelationship);
  await startDirectEnrollment(product, {
    age,
    relationship: actionRelationship ?? context?.relationship,
  });
}

function viewProduct(product: Product) {
  selectedProduct.value = product;
  activeTab.value = "plans";
  window.scrollTo({ top: 0, behavior: "smooth" });
}

async function startDirectEnrollment(
  product: Product,
  context?: ChatMessage["enrollmentContext"],
) {
  selectedProduct.value = product;
  if (context?.age !== undefined) insuredAge.value = context.age;
  if (context?.relationship) relationship.value = context.relationship;
  quote.value = null;
  resetEnrollmentFlow();
  preQuoteConfirming.value = true;
  enrollmentOpen.value = true;
  error.value = "";
  await nextTick();
  document
    .querySelector<HTMLElement>(".enrollment-body")
    ?.scrollTo({ top: 0 });
}

async function confirmDirectEnrollment() {
  if (!selectedProduct.value) return;
  if (
    !Number.isInteger(insuredAge.value) ||
    insuredAge.value < 0 ||
    insuredAge.value > 120
  ) {
    error.value = "请输入0至120之间的有效年龄";
    return;
  }
  await createQuote(selectedProduct.value);
}

function openPolicyDocument(item: Policy) {
  openedPolicy.value = item;
}

function closePolicyDocument() {
  openedPolicy.value = null;
}

function printPolicyDocument() {
  window.print();
}

function resetEnrollmentFlow() {
  enrollmentStep.value = 1;
  noticeAccepted.value = false;
  disclosureConfirmed.value = false;
  order.value = null;
  payment.value = null;
  policy.value = null;
}

async function setEnrollmentStep(step: number) {
  enrollmentStep.value = step;
  await nextTick();
  document
    .querySelector<HTMLElement>(".enrollment-body")
    ?.scrollTo({ top: 0, behavior: "smooth" });
}

async function createQuote(product: Product) {
  businessLoading.value = true;
  quotingProductId.value = product.product_id;
  quoteFeedback.value = "正在校验投保资格并试算保费…";
  error.value = "";
  try {
    const response = await authorizedFetch("/h5/quotes", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        product_id: product.product_id,
        plan_id: product.plans[0].id,
        insured: {
          age: insuredAge.value,
          region_code: "510100",
          occupation_code: "GENERAL",
        },
        relationship: relationship.value,
      }),
    });
    if (!response.ok) throw new Error(await responseError(response, "报价失败"));
    selectedProduct.value = product;
    quote.value = (await response.json()).data;
    resetEnrollmentFlow();
    preQuoteConfirming.value = false;
    enrollmentOpen.value = true;
    quoteFeedback.value = "";
    await setEnrollmentStep(1);
  } catch (reason) {
    error.value = reason instanceof Error ? reason.message : "报价失败";
    quoteFeedback.value = error.value;
  } finally {
    businessLoading.value = false;
    quotingProductId.value = "";
  }
}

function validateEnrollmentPeople() {
  error.value = "";
  if (!applicantName.value.trim() || !insuredName.value.trim()) {
    error.value = "请填写投保人和被保人姓名";
    return false;
  }
  const idPattern = /^\d{17}[\dXx]$/;
  if (
    !idPattern.test(applicantIdNo.value) ||
    !idPattern.test(insuredIdNo.value)
  ) {
    error.value = "请输入正确的18位身份证号码";
    return false;
  }
  if (!/^1\d{10}$/.test(contactMobile.value)) {
    error.value = "请输入正确的11位联系手机号";
    return false;
  }
  return true;
}

async function goToNotice() {
  if (!validateEnrollmentPeople()) return;
  error.value = "";
  await setEnrollmentStep(2);
}

async function goToConfirmation() {
  error.value = "";
  if (!noticeAccepted.value || !disclosureConfirmed.value) {
    error.value = "请阅读投保须知并完成两项确认";
    return;
  }
  await setEnrollmentStep(3);
}

async function confirmAndCreateOrder() {
  if (!quote.value) return;
  businessLoading.value = true;
  error.value = "";
  try {
    const draftResponse = await authorizedFetch("/h5/enrollment-drafts", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        quote_id: quote.value.quote_id,
        applicant: {
          name: applicantName.value.trim(),
          id_no: applicantIdNo.value.toUpperCase(),
        },
        insured: {
          name: insuredName.value.trim(),
          id_no: insuredIdNo.value.toUpperCase(),
        },
        contact: { mobile: contactMobile.value },
      }),
    });
    if (!draftResponse.ok) {
      throw new Error(await responseError(draftResponse, "投保草稿创建失败"));
    }
    const draft = (await draftResponse.json()).data;
    const confirmationResponse = await authorizedFetch(
      `/h5/enrollment-drafts/${draft.draft_id}/confirmation`,
      { method: "POST" },
    );
    if (!confirmationResponse.ok) {
      throw new Error(await responseError(confirmationResponse, "确认失败"));
    }
    const confirmation = (await confirmationResponse.json()).data;
    const orderResponse = await authorizedFetch("/h5/orders", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Idempotency-Key": crypto.randomUUID(),
      },
      body: JSON.stringify({
        draft_id: draft.draft_id,
        confirmation_token: confirmation.confirmation_token,
      }),
    });
    if (!orderResponse.ok) {
      throw new Error(await responseError(orderResponse, "订单创建失败"));
    }
    order.value = (await orderResponse.json()).data;
    const paymentCreated = await startPayment();
    if (paymentCreated) await setEnrollmentStep(4);
  } catch (reason) {
    error.value = reason instanceof Error ? reason.message : "订单创建失败";
  } finally {
    businessLoading.value = false;
  }
}

async function startPayment() {
  if (!order.value) return false;
  businessLoading.value = true;
  error.value = "";
  try {
    const response = await authorizedFetch(
      `/h5/orders/${order.value.order_id}/payments`,
      {
        method: "POST",
        headers: { "Idempotency-Key": crypto.randomUUID() },
      },
    );
    if (!response.ok) throw new Error(await responseError(response, "支付发起失败"));
    payment.value = (await response.json()).data;
    return true;
  } catch (reason) {
    error.value = reason instanceof Error ? reason.message : "支付发起失败";
    return false;
  } finally {
    businessLoading.value = false;
  }
}

async function completePayment() {
  if (!payment.value) return;
  businessLoading.value = true;
  error.value = "";
  try {
    const response = await authorizedFetch(
      `/dev/mock-payments/${payment.value.payment_id}/complete`,
      { method: "POST" },
    );
    if (!response.ok) throw new Error(await responseError(response, "支付回调失败"));
    const body = await response.json();
    payment.value.status = body.data.payment_status;
    if (order.value) order.value.status = body.data.order_status;
    policy.value = body.data.policy;
    await loadBusiness();
  } catch (reason) {
    error.value = reason instanceof Error ? reason.message : "支付回调失败";
  } finally {
    businessLoading.value = false;
  }
}

async function closeEnrollment() {
  enrollmentOpen.value = false;
  preQuoteConfirming.value = false;
  error.value = "";
  await loadBusiness();
}

async function viewCoverageAfterPayment() {
  enrollmentOpen.value = false;
  activeTab.value = "coverage";
  coverageSection.value = "policies";
  await loadBusiness();
  window.scrollTo({ top: 0, behavior: "smooth" });
}

async function openServiceCenter() {
  activeTab.value = "services";
  serviceError.value = "";
  await loadBusiness();
  window.scrollTo({ top: 0, behavior: "smooth" });
}

function startPolicyService(item: Policy) {
  if (item.status !== "ACTIVE") {
    error.value = "只有保障中的保单可以发起服务";
    return;
  }
  selectedServicePolicyId.value = item.policy_id;
  clearServiceMaterial();
  serviceError.value = "";
  activeTab.value = "services";
  serviceApplicationOpen.value = true;
  window.scrollTo({ top: 0, behavior: "smooth" });
}

// 退保前先向后端获取确定性试算；前端不自行计算退款金额。
async function startSurrender(item: Policy) {
  if (item.status !== "ACTIVE") {
    error.value = "只有保障中的保单可以申请退保";
    return;
  }
  selectedSurrenderPolicyId.value = item.policy_id;
  surrenderPreview.value = null;
  surrenderReason.value = "保障计划调整";
  surrenderReasonDetail.value = "";
  surrenderConfirmed.value = false;
  surrenderError.value = "";
  surrenderApplicationOpen.value = true;
  surrenderLoading.value = true;
  try {
    const response = await authorizedFetch(
      `/h5/policies/${item.policy_id}/surrender-preview`,
    );
    if (!response.ok) {
      throw new Error(await responseError(response, "退保试算失败"));
    }
    surrenderPreview.value = (await response.json()).data as SurrenderPreview;
  } catch (reason) {
    surrenderError.value =
      reason instanceof Error ? reason.message : "退保试算失败";
  } finally {
    surrenderLoading.value = false;
  }
}

function closeSurrenderApplication() {
  surrenderApplicationOpen.value = false;
  surrenderError.value = "";
  surrenderPreview.value = null;
  surrenderReasonDetail.value = "";
  surrenderConfirmed.value = false;
}

async function submitSurrender() {
  if (!surrenderPreview.value?.eligible || !selectedSurrenderPolicy.value) {
    surrenderError.value = surrenderPreview.value?.reason ?? "当前保单暂不能申请退保";
    return;
  }
  if (!surrenderReason.value.trim()) {
    surrenderError.value = "请选择退保原因";
    return;
  }
  if (
    ["扣费或服务问题", "其他原因"].includes(surrenderReason.value) &&
    surrenderReasonDetail.value.trim().length < 2
  ) {
    surrenderError.value = "请填写具体的退保原因说明";
    return;
  }
  if (!surrenderConfirmed.value) {
    surrenderError.value = "请确认退款试算与保障终止提示";
    return;
  }
  surrenderLoading.value = true;
  surrenderError.value = "";
  try {
    // 第一步创建待确认申请；随机幂等键可防止本次重复点击产生两笔申请。
    const createdResponse = await authorizedFetch("/h5/surrenders", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Idempotency-Key": crypto.randomUUID(),
      },
      body: JSON.stringify({
        policy_id: selectedSurrenderPolicy.value.policy_id,
        reason_category: surrenderReason.value.trim(),
        reason_detail: surrenderReasonDetail.value.trim() || null,
      }),
    });
    if (!createdResponse.ok) {
      throw new Error(await responseError(createdResponse, "退保申请提交失败"));
    }
    const created = (await createdResponse.json()).data as SurrenderRequestItem;
    // 第二步显式确认，后端此时才启动 LangGraph 风险评估和自动/人工路由。
    const confirmResponse = await authorizedFetch(
      `/h5/surrenders/${created.request_id}/confirm`,
      { method: "POST" },
    );
    if (!confirmResponse.ok) {
      throw new Error(await responseError(confirmResponse, "退保信息确认失败"));
    }
    await loadBusiness();
    closeSurrenderApplication();
    coverageSection.value = "surrenders";
    window.scrollTo({ top: 0, behavior: "smooth" });
  } catch (reason) {
    surrenderError.value =
      reason instanceof Error ? reason.message : "退保申请提交失败";
  } finally {
    surrenderLoading.value = false;
  }
}

function closeServiceApplication() {
  serviceApplicationOpen.value = false;
  serviceError.value = "";
  clearServiceMaterial();
}

function clearServiceMaterial() {
  if (serviceFilePreview.value) URL.revokeObjectURL(serviceFilePreview.value);
  serviceFile.value = null;
  serviceFilePreview.value = "";
  materialRecognition.value = null;
  serviceConfirmed.value = false;
  serviceFields.value = {
    invoice_no: "",
    hospital_name: "",
    visit_date: "",
    amount_yuan: "",
    contact_mobile: mobile.value,
  };
}

function handleServiceFile(event: Event) {
  serviceError.value = "";
  const input = event.target as HTMLInputElement;
  const file = input.files?.[0] ?? null;
  if (!file) return;
  if (!["image/jpeg", "image/png", "image/webp"].includes(file.type)) {
    serviceError.value = "请上传 JPG、PNG 或 WebP 格式的医疗费用发票";
    input.value = "";
    return;
  }
  if (file.size > 10 * 1024 * 1024) {
    serviceError.value = "单张材料不能超过 10MB";
    input.value = "";
    return;
  }
  clearServiceMaterial();
  serviceFile.value = file;
  serviceFilePreview.value = URL.createObjectURL(file);
}

async function recognizeServiceMaterial() {
  if (!selectedServicePolicyId.value) {
    serviceError.value = "请先选择需要申请服务的有效保单";
    return;
  }
  if (!serviceFile.value) {
    serviceError.value = "请先上传医疗费用发票";
    return;
  }
  serviceLoading.value = true;
  serviceError.value = "";
  try {
    const form = new FormData();
    form.append("policy_id", selectedServicePolicyId.value);
    form.append("material_type", "MEDICAL_INVOICE");
    form.append("file", serviceFile.value);
    const response = await authorizedFetch("/h5/service-materials/recognize", {
      method: "POST",
      body: form,
    });
    if (!response.ok) {
      throw new Error(await responseError(response, "医疗票据识别失败"));
    }
    materialRecognition.value = (await response.json()).data;
    const fields = materialRecognition.value?.fields ?? {};
    serviceFields.value = {
      invoice_no: fields.invoice_no ?? "",
      hospital_name: fields.hospital_name ?? "",
      visit_date: fields.visit_date ?? "",
      amount_yuan: fields.amount_yuan ?? "",
      contact_mobile: mobile.value,
    };
  } catch (reason) {
    serviceError.value =
      reason instanceof Error ? reason.message : "医疗票据识别失败";
  } finally {
    serviceLoading.value = false;
  }
}

async function submitServiceRequest() {
  if (!materialRecognition.value) {
    serviceError.value = "请先完成医疗票据识别";
    return;
  }
  const requiredFields = [
    serviceFields.value.invoice_no,
    serviceFields.value.hospital_name,
    serviceFields.value.amount_yuan,
    serviceFields.value.contact_mobile,
  ];
  if (requiredFields.some((value) => !value.trim())) {
    serviceError.value = "请完整填写票据号码、医疗机构、费用金额和联系电话";
    return;
  }
  if (!/^1\d{10}$/.test(serviceFields.value.contact_mobile)) {
    serviceError.value = "请输入正确的11位联系电话";
    return;
  }
  if (!serviceConfirmed.value) {
    serviceError.value = "请确认识别内容已经人工核对";
    return;
  }
  serviceLoading.value = true;
  serviceError.value = "";
  try {
    const response = await authorizedFetch("/h5/service-requests", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        policy_id: selectedServicePolicyId.value,
        request_type: "MEDICAL_REIMBURSEMENT_ASSISTANCE",
        recognition_id: materialRecognition.value.recognition_id,
        confirmed_fields: serviceFields.value,
      }),
    });
    if (!response.ok) {
      throw new Error(await responseError(response, "服务申请提交失败"));
    }
    clearServiceMaterial();
    await loadBusiness();
    serviceApplicationOpen.value = false;
    activeTab.value = "services";
    window.scrollTo({ top: 0, behavior: "smooth" });
  } catch (reason) {
    serviceError.value =
      reason instanceof Error ? reason.message : "服务申请提交失败";
  } finally {
    serviceLoading.value = false;
  }
}

async function loadBusiness() {
  if (!token.value) return;
  const [
    ordersResponse,
    policiesResponse,
    serviceRequestsResponse,
    surrenderRequestsResponse,
  ] = await Promise.all([
    authorizedFetch("/h5/orders"),
    authorizedFetch("/h5/policies"),
    authorizedFetch("/h5/service-requests"),
    authorizedFetch("/h5/surrenders"),
  ]);
  if (ordersResponse.ok) {
    const items = (await ordersResponse.json()).data.items as Order[];
    order.value = items[0] ?? null;
  }
  if (policiesResponse.ok) {
    const items = (await policiesResponse.json()).data.items as Policy[];
    policies.value = items;
    if (
      !selectedServicePolicyId.value ||
      !items.some(
        (item) =>
          item.policy_id === selectedServicePolicyId.value &&
          item.status === "ACTIVE",
      )
    ) {
      selectedServicePolicyId.value =
        items.find((item) => item.status === "ACTIVE")?.policy_id ?? "";
    }
  }
  if (serviceRequestsResponse.ok) {
    serviceRequests.value = (
      await serviceRequestsResponse.json()
    ).data.items as ServiceRequestItem[];
  }
  if (surrenderRequestsResponse.ok) {
    surrenderRequests.value = (
      await surrenderRequestsResponse.json()
    ).data.items as SurrenderRequestItem[];
  }
}

async function refreshProducts() {
  refreshing.value = true;
  error.value = "";
  try {
    await loadProducts();
  } catch (reason) {
    error.value = reason instanceof Error ? reason.message : "产品加载失败";
  } finally {
    refreshing.value = false;
  }
}

async function logout() {
  try {
    if (token.value) {
      await apiFetch("/h5/auth/logout", {
        method: "POST",
        headers: { Authorization: `Bearer ${token.value}` },
      });
    }
  } finally {
    token.value = "";
    contactMobile.value = "";
    products.value = [];
    error.value = "";
    agentThreadId.value = "";
    agentMessages.value = [];
    agentThreads.value = [];
    conversationHistoryOpen.value = false;
    serviceNo.value = "";
    activeTab.value = "advisor";
    coverageSection.value = "policies";
    selectedProduct.value = null;
    quote.value = null;
    order.value = null;
    payment.value = null;
    policy.value = null;
    policies.value = [];
    openedPolicy.value = null;
    serviceRequests.value = [];
    surrenderRequests.value = [];
    surrenderApplicationOpen.value = false;
    surrenderError.value = "";
    selectedSurrenderPolicyId.value = "";
    surrenderPreview.value = null;
    surrenderConfirmed.value = false;
    serviceApplicationOpen.value = false;
    serviceError.value = "";
    selectedServicePolicyId.value = "";
    clearServiceMaterial();
    quotingProductId.value = "";
    quoteFeedback.value = "";
    enrollmentOpen.value = false;
    preQuoteConfirming.value = false;
    enrollmentStep.value = 1;
    applicantName.value = "";
    applicantIdNo.value = "";
    insuredName.value = "";
    insuredIdNo.value = "";
    relationship.value = "PARENT";
    noticeAccepted.value = false;
    disclosureConfirmed.value = false;
    codeRequested.value = false;
    codeCountdown.value = 0;
    agreementAccepted.value = false;
    if (countdownTimer) {
      clearInterval(countdownTimer);
      countdownTimer = undefined;
    }
    createImageCode();
  }
}

onMounted(async () => {
  createImageCode();
  // 清理早期版本遗留的持久化Access Token，当前版本只在内存中保存。
  localStorage.removeItem("zbt_h5_token");
  try {
    if (await refreshAccessToken()) await loadProducts(false);
    if (token.value) {
      await Promise.all([loadBusiness(), loadAgentThreads()]);
    }
  } catch {
    token.value = "";
    products.value = [];
  } finally {
    restoring.value = false;
  }
});

onBeforeUnmount(() => {
  if (countdownTimer) clearInterval(countdownTimer);
  if (serviceFilePreview.value) URL.revokeObjectURL(serviceFilePreview.value);
});
</script>

<template>
  <main class="phone-shell">
    <section v-if="restoring || !token" class="hero">
      <div class="brand-line"><span class="seal">智</span> 智保通</div>
      <p class="eyebrow">AI 智能保险服务平台</p>
      <h1>把复杂条款，<br /><em>说成一句明白话。</em></h1>
      <div class="hero-orbit" aria-hidden="true"></div>
    </section>

    <section v-if="restoring" class="login-card session-loading" aria-live="polite">
      <p class="step">00 / 会话检查</p>
      <h2>正在恢复安全会话…</h2>
      <p class="hint">仅从安全 Cookie 轮换登录凭证。</p>
    </section>

    <section v-else-if="!token" class="login-card">
      <div class="login-heading">
        <div>
          <p class="step">01 / 安全登录</p>
          <h2>欢迎使用智保通</h2>
        </div>
        <span class="secure-badge"><i></i>安全连接</span>
      </div>
      <p class="login-intro">登录后继续咨询方案、查询订单与管理个人保单。</p>
      <p v-if="referralCode" class="referral-banner">
        <span>邀</span> 已识别专属服务邀请 · {{ referralCode }}
      </p>

      <form class="login-form" @submit.prevent="login">
        <label>
          手机号
          <span class="field-shell">
            <span class="field-prefix">+86</span>
            <input
              v-model.trim="mobile"
              inputmode="tel"
              autocomplete="tel"
              maxlength="11"
              placeholder="请输入手机号"
              aria-label="手机号"
            />
          </span>
        </label>

        <label>
          图形验证码
          <span class="field-row">
            <input
              v-model.trim="imageCodeInput"
              class="image-code-input"
              maxlength="4"
              autocomplete="off"
              placeholder="请输入图中字符"
              aria-label="图形验证码"
            />
            <button
              type="button"
              class="captcha"
              aria-label="刷新图形验证码"
              title="看不清？点击换一张"
              @click="createImageCode"
            >
              <span
                v-for="(letter, index) in imageCode"
                :key="`${letter}-${index}`"
                :style="{ transform: `rotate(${index % 2 === 0 ? -8 : 7}deg) translateY(${index % 3 - 1}px)` }"
              >{{ letter }}</span>
              <i></i><i></i>
            </button>
          </span>
          <small class="field-help">看不清？点击图片更换</small>
        </label>

        <label>
          短信验证码
          <span class="field-row">
            <input
              v-model.trim="code"
              inputmode="numeric"
              autocomplete="one-time-code"
              maxlength="6"
              placeholder="请输入6位验证码"
              aria-label="短信验证码"
            />
            <button
              type="button"
              class="send-code"
              :disabled="!canRequestCode"
              @click="requestCode"
            >
              {{
                requestingCode
                  ? "发送中…"
                  : codeCountdown > 0
                    ? `${codeCountdown}s 后重发`
                    : "获取验证码"
              }}
            </button>
          </span>
        </label>

        <label class="agreement">
          <input v-model="agreementAccepted" type="checkbox" />
          <span>我已阅读并同意<a href="#service-agreement">《用户服务协议》</a>和<a href="#privacy-policy">《隐私政策》</a></span>
        </label>

        <p v-if="codeMessage" class="success status-message" aria-live="polite">
          <span>✓</span>{{ codeMessage }}
        </p>
        <p v-if="error" class="error status-message" role="alert">
          <span>!</span>{{ error }}
        </p>

        <button class="login-submit" :disabled="loggingIn" type="submit">
          {{ loggingIn ? "正在安全登录…" : "登录" }}
          <span aria-hidden="true">→</span>
        </button>
      </form>
      <p class="privacy-note"><span>◇</span> 你的个人信息将被加密保护</p>
    </section>

    <div v-else class="app-shell">
      <header class="app-header">
        <div class="compact-brand">
          <span class="seal">智</span>
          <div><strong>智保通</strong><small>懂你的保险顾问</small></div>
        </div>
        <button class="header-action" aria-label="退出登录" @click="logout">
          <svg viewBox="0 0 24 24" aria-hidden="true">
            <path d="M10 4H5v16h5M14 8l4 4-4 4M18 12H9" />
          </svg>
        </button>
      </header>

      <div class="tab-content">
        <section v-if="activeTab === 'advisor'" class="tab-page advisor-page">
          <div class="page-heading">
            <div><p class="step">AI INSURANCE ADVISOR</p><h2>智能顾问</h2></div>
            <div class="advisor-heading-actions">
              <button type="button" class="conversation-action" @click="startNewConversation">
                <span>＋</span>新会话
              </button>
              <button type="button" class="conversation-action history" @click="openConversationHistory">
                历史<em v-if="agentThreads.length">{{ agentThreads.length }}</em>
              </button>
            </div>
          </div>
          <p class="page-lead">可以咨询保障方案，也可以查询保险条款和服务材料；知识回答会标明来源。</p>

          <div class="quick-prompts">
            <button @click="agentInput = '我想给65岁的父亲买医疗险，常住成都，普通职业。'">父母医疗</button>
            <button @click="agentInput = '我想给一家三口配置意外保障，请帮我推荐。'">家庭意外</button>
            <button @click="agentInput = '我想了解基础医疗保障，年龄32岁。'">成人医疗</button>
            <button @click="agentInput = '医疗险等待期是多少天？请给出条款来源。'">条款问答</button>
          </div>

          <div class="agent-panel">
            <div class="agent-head">
              <span class="agent-mark">AI</span>
              <div><strong>你好，我是智保通顾问</strong><small>在线服务 · 安全连接</small></div>
            </div>
            <div class="messages" aria-live="polite">
              <div v-if="agentMessages.length === 0" class="welcome-message">
                <strong>我可以帮你做什么？</strong>
                <p>告诉我为谁投保、年龄、常住地区和职业，我会先理解需求，再推荐方案。</p>
              </div>
              <div
                v-for="(message, index) in agentMessages"
                :key="index"
                :class="['message', message.role]"
              >
                <div class="message-meta">
                  <span v-if="message.role === 'assistant'">AI</span>
                  <small>{{ message.role === "user" ? "你" : "智保通顾问" }}</small>
                </div>
                <div
                  v-if="message.role === 'assistant'"
                  class="assistant-content"
                  v-html="message.html"
                ></div>
                <p v-else>{{ message.text }}</p>
                <div v-if="message.products?.length" class="recommendation-list">
                  <div
                    v-for="(product, productIndex) in message.products"
                    :key="product.product_id"
                    class="recommendation-card"
                  >
                    <div>
                      <span>{{ productIndex === 0 ? "优先推荐" : "备选方案" }}</span>
                      <strong>{{ product.name }}</strong>
                      <small>{{ product.summary }}</small>
                    </div>
                    <button @click="startDirectEnrollment(product, message.enrollmentContext)">
                      立即投保 →
                    </button>
                  </div>
                </div>
                <div v-if="message.sources?.length" class="knowledge-source-list">
                  <div class="knowledge-source-title">
                    <span>依据</span>
                    <strong>本次回答引用</strong>
                  </div>
                  <article
                    v-for="source in message.sources"
                    :key="`${source.document_id}-${source.document_version}-${source.source_name}`"
                  >
                    <span>V{{ source.document_version }}</span>
                    <div>
                      <strong>{{ source.title }}</strong>
                      <ul
                        v-if="source.excerpts && source.excerpts.length > 1"
                        class="knowledge-source-evidence"
                      >
                        <li
                          v-for="(excerpt, excerptIndex) in source.excerpts"
                          :key="`${source.document_id}-${source.document_version}-${excerptIndex}`"
                        >
                          {{ excerpt }}
                        </li>
                      </ul>
                      <p v-else>{{ source.excerpts?.[0] ?? source.excerpt }}</p>
                      <small>{{ source.source_name }}</small>
                    </div>
                  </article>
                </div>
                <div
                  v-if="
                    message.actions?.length &&
                    !message.products?.length &&
                    !message.sources?.length
                  "
                  class="agent-actions"
                >
                  <button
                    v-for="action in message.actions.filter((item) => item.type !== 'none')"
                    :key="`${action.type}-${action.label}`"
                    type="button"
                    @click="executeAgentAction(action, message.enrollmentContext)"
                  >
                    {{ action.label }} <span>→</span>
                  </button>
                </div>
              </div>
            </div>
            <div class="agent-composer">
              <textarea
                v-model="agentInput"
                rows="3"
                aria-label="投保需求"
                placeholder="请描述你的投保需求…"
              ></textarea>
              <button
                aria-label="发送消息"
                :disabled="agentLoading || !agentInput.trim()"
                @click="sendAgentMessage"
              >
                <svg viewBox="0 0 24 24" aria-hidden="true">
                  <path d="m4 5 16 7-16 7 3-7-3-7Zm3 7h13" />
                </svg>
              </button>
            </div>
            <p v-if="agentLoading" class="thinking"><i></i><i></i><i></i> {{ agentProgress }}</p>
            <p v-if="serviceNo" class="trace">服务流水号 · {{ serviceNo }}</p>
          </div>
          <p v-if="error" class="error status-message" role="alert"><span>!</span>{{ error }}</p>
        </section>

        <section v-else-if="activeTab === 'plans'" class="tab-page plans-page">
          <div class="page-heading">
            <div><p class="step">PROTECTION PLANS</p><h2>保障方案</h2></div>
            <button class="refresh-button" :disabled="refreshing" @click="refreshProducts">
              {{ refreshing ? "刷新中" : "刷新" }}
            </button>
          </div>
          <p class="page-lead">先填写被保人的基本情况，再查看确定性保费。</p>

          <div class="quote-conditions">
            <div class="condition-title"><span>01</span><strong>被保人信息</strong></div>
            <label>
              与投保人关系
              <span class="relation-options">
                <button
                  v-for="item in (['SELF', 'PARENT', 'SPOUSE', 'CHILD'] as Relationship[])"
                  :key="item"
                  :class="{ active: relationship === item }"
                  @click="relationship = item"
                >
                  {{ relationshipName(item) }}
                </button>
              </span>
            </label>
            <label>
              被保人年龄
              <input v-model.number="insuredAge" type="number" min="0" max="120" />
            </label>
            <div class="fixed-conditions">
              <span>常住地区：成都市</span><span>职业类别：普通职业</span>
            </div>
          </div>

          <div class="product-list">
            <div
              v-for="(product, index) in displayedProducts"
              :key="product.product_code"
              :class="['product-card', { recommended: selectedProduct?.product_id === product.product_id }]"
            >
              <div class="product-topline">
                <span>{{ String(index + 1).padStart(2, "0") }}</span>
                <em v-if="selectedProduct?.product_id === product.product_id">顾问推荐</em>
              </div>
              <h3>{{ product.name }}</h3>
              <p>{{ product.summary }}</p>
              <div class="plans">
                <span v-for="plan in product.plans" :key="plan.code">{{ plan.name }}</span>
              </div>
              <ul class="benefit-list">
                <li>电子保单即时查询</li>
                <li>支持家庭成员投保</li>
                <li>保费由规则引擎确定</li>
              </ul>
              <button
                class="product-action"
                :disabled="businessLoading"
                :aria-busy="quotingProductId === product.product_id"
                @click="createQuote(product)"
              >
                {{ quotingProductId === product.product_id ? "正在试算…" : "测算保费并投保" }} <span>→</span>
              </button>
            </div>
          </div>
          <div v-if="quoteFeedback" :class="['quote-feedback', { failed: !businessLoading }]">
            <span>{{ businessLoading ? "···" : "!" }}</span>{{ quoteFeedback }}
          </div>
          <p v-if="products.length === 0" class="empty-state">暂时没有可投保产品。</p>
          <p v-if="error" class="error status-message" role="alert"><span>!</span>{{ error }}</p>
        </section>

        <section v-else-if="activeTab === 'coverage'" class="tab-page coverage-page">
          <div class="page-heading">
            <div><p class="step">MY PROTECTION</p><h2>我的保障</h2></div>
            <span class="record-count">
              {{
                coverageSection === "policies"
                  ? (policies.length > 0 ? `${policies.length}张保单` : "暂无保单")
                  : `${surrenderRequests.length}笔记录`
              }}
            </span>
          </div>
          <p class="page-lead">
            {{
              coverageSection === "policies"
                ? "集中查看已经生效的保障，并从保单发起后续服务。"
                : "查看退保审核、退款处理与保障终止进度。"
            }}
          </p>

          <nav class="coverage-switch" aria-label="我的保障分类">
            <button
              type="button"
              :class="{ active: coverageSection === 'policies' }"
              @click="coverageSection = 'policies'"
            >
              <span>01</span>
              <div><strong>保障保单</strong><small>当前持有的保障</small></div>
              <em>{{ policies.length }}</em>
            </button>
            <button
              type="button"
              :class="{ active: coverageSection === 'surrenders' }"
              @click="coverageSection = 'surrenders'"
            >
              <span>02</span>
              <div><strong>退保记录</strong><small>审核与退款进度</small></div>
              <em>{{ surrenderRequests.length }}</em>
            </button>
          </nav>

          <div
            v-if="coverageSection === 'policies' && policies.length > 0"
            class="policy-list coverage-section-enter"
          >
            <article v-for="(item, index) in policies" :key="item.policy_id" class="policy-summary">
              <div class="policy-glow"></div>
              <div class="policy-heading">
                <span>电子保单 · {{ String(index + 1).padStart(2, "0") }}</span>
                <em>{{ policyStatus(item.status) }}</em>
              </div>
              <h3>{{ item.product_name }}</h3>
              <p class="policy-plan">{{ item.plan_name }}</p>
              <p class="policy-number">{{ item.policy_no }}</p>
              <div class="policy-data">
                <div><small>年度保费</small><strong>{{ money(item.premium_cents) }}</strong></div>
                <div><small>保障期限</small><strong>{{ item.coverage_start }} 至 {{ item.coverage_end }}</strong></div>
              </div>
              <div class="policy-actions">
                <button
                  type="button"
                  class="policy-document-action"
                  @click="openPolicyDocument(item)"
                >
                  <span class="policy-action-icon" aria-hidden="true">
                    <svg viewBox="0 0 24 24">
                      <path d="M7 3.5h7l3 3V20.5H7z" />
                      <path d="M14 3.5v4h4M9.5 11h5M9.5 14.5h5" />
                    </svg>
                  </span>
                  <span class="policy-action-copy">
                    <strong>电子保单</strong>
                    <small>查看保障凭证</small>
                  </span>
                  <span class="policy-action-arrow" aria-hidden="true">→</span>
                </button>
                <button
                  v-if="item.status === 'ACTIVE'"
                  type="button"
                  class="policy-service-action"
                  @click="startPolicyService(item)"
                >
                  <span class="policy-action-icon" aria-hidden="true">
                    <svg viewBox="0 0 24 24">
                      <path d="M5 5.5h14v13H5z" />
                      <path d="M9 5.5V4h6v1.5M12 9v6M9 12h6" />
                    </svg>
                  </span>
                  <span class="policy-action-copy">
                    <strong>保单服务</strong>
                    <small>提交服务申请</small>
                  </span>
                  <span class="policy-action-arrow" aria-hidden="true">→</span>
                </button>
                <button
                  v-if="item.status === 'ACTIVE'"
                  type="button"
                  class="policy-surrender-action"
                  @click="startSurrender(item)"
                >
                  <span class="policy-surrender-copy">
                    <small>保障计划需要调整？</small>
                    <strong>申请退保</strong>
                  </span>
                  <span class="policy-surrender-arrow" aria-hidden="true">→</span>
                </button>
              </div>
            </article>
          </div>

          <Teleport to="body">
            <div
              v-if="surrenderApplicationOpen"
              class="surrender-layer"
              role="dialog"
              aria-modal="true"
              aria-label="退保申请确认"
              @click.self="closeSurrenderApplication"
            >
              <article class="surrender-sheet">
                <header>
                  <div>
                    <p>SURRENDER REVIEW</p>
                    <h2>退保申请确认</h2>
                  </div>
                  <button type="button" aria-label="关闭退保申请" @click="closeSurrenderApplication">×</button>
                </header>

                <div v-if="surrenderLoading && !surrenderPreview" class="surrender-loading">
                  <i></i>
                  <strong>正在核对保单与计算退款金额</strong>
                  <span>资格与金额由业务规则确定，AI 不参与决策</span>
                </div>

                <template v-else-if="surrenderPreview && selectedSurrenderPolicy">
                  <section class="surrender-policy-card">
                    <span>关联保单</span>
                    <h3>{{ selectedSurrenderPolicy.product_name }}</h3>
                    <p>{{ selectedSurrenderPolicy.policy_no }}</p>
                    <em>{{ selectedSurrenderPolicy.plan_name }}</em>
                  </section>

                  <section :class="['surrender-quote', { denied: !surrenderPreview.eligible }]">
                    <div>
                      <span>{{ surrenderPreview.eligible ? "预计退还" : "暂不可退保" }}</span>
                      <strong v-if="surrenderPreview.eligible">{{ money(surrenderPreview.refund_amount_cents) }}</strong>
                      <strong v-else>—</strong>
                    </div>
                    <p>{{ surrenderPreview.reason }}</p>
                  </section>

                  <section
                    v-if="surrenderPreview.eligible"
                    :class="[
                      'surrender-refund-basis',
                      {
                        cooling:
                          surrenderPreview.calculation.pre_effective ||
                          surrenderPreview.calculation.cooling_off,
                      },
                    ]"
                  >
                    <span>{{
                      surrenderPreview.calculation.pre_effective
                        ? "保障生效前"
                        : surrenderPreview.calculation.cooling_off
                          ? "犹豫期内"
                          : "犹豫期外"
                    }}</span>
                    <div>
                      <strong>{{
                        surrenderPreview.calculation.pre_effective ||
                        surrenderPreview.calculation.cooling_off
                          ? "按已交保费全额退还"
                          : "按剩余保障期限折算"
                      }}</strong>
                      <p>
                        {{
                          surrenderPreview.refund_explanation ??
                          (surrenderPreview.calculation.pre_effective
                            ? "保单保障尚未开始，本次撤销不收取退保扣减。"
                            : surrenderPreview.calculation.cooling_off
                            ? "当前申请仍在犹豫期内，退保扣减为0元。"
                            : "当前申请按未满期保费折算，并按照规则扣减。")
                        }}
                      </p>
                    </div>
                  </section>

                  <dl v-if="surrenderPreview.eligible" class="surrender-calculation">
                    <div><dt>已交保费</dt><dd>{{ money(surrenderPreview.calculation.premium_cents) }}</dd></div>
                    <div><dt>未满期保费</dt><dd>{{ money(surrenderPreview.calculation.gross_unearned_cents) }}</dd></div>
                    <div><dt>退保扣减</dt><dd>-{{ money(surrenderPreview.calculation.deduction_cents) }}</dd></div>
                    <div><dt>剩余保障天数</dt><dd>{{ surrenderPreview.calculation.remaining_days }}天</dd></div>
                  </dl>

                  <section v-if="surrenderPreview.eligible" class="surrender-form">
                    <label>
                      <span>退保原因</span>
                      <select v-model="surrenderReason">
                        <option>保障计划调整</option>
                        <option>已有其他保障</option>
                        <option>暂时不需要该保障</option>
                        <option>扣费或服务问题</option>
                        <option>其他原因</option>
                      </select>
                    </label>
                    <label class="surrender-reason-detail">
                      <span>
                        具体说明
                        <em v-if="['扣费或服务问题', '其他原因'].includes(surrenderReason)">
                          必填
                        </em>
                        <small v-else>选填</small>
                      </span>
                      <textarea
                        v-model="surrenderReasonDetail"
                        rows="3"
                        maxlength="160"
                        placeholder="请描述具体情况，例如扣费、服务争议或身份异常"
                      ></textarea>
                      <small>{{ surrenderReasonDetail.length }}/160</small>
                    </label>
                    <label class="surrender-confirm">
                      <input v-model="surrenderConfirmed" type="checkbox" />
                      <span>我已确认退款试算；流程通过风险分流并退款成功后，本保单保障将终止。</span>
                    </label>
                  </section>
                </template>

                <p v-if="surrenderError" class="error status-message" role="alert"><span>!</span>{{ surrenderError }}</p>
                <footer>
                  <button type="button" class="surrender-cancel" @click="closeSurrenderApplication">暂不申请</button>
                  <button
                    type="button"
                    class="surrender-submit"
                    :disabled="surrenderLoading || !surrenderPreview?.eligible"
                    @click="submitSurrender"
                  >
                    {{ surrenderLoading ? "正在提交…" : "确认提交退保申请" }}
                  </button>
                </footer>
              </article>
            </div>
          </Teleport>

          <section
            v-if="coverageSection === 'surrenders' && surrenderRequests.length"
            class="surrender-records coverage-section-enter"
          >
            <div class="surrender-records-heading">
              <div><span>POLICY LIFECYCLE</span><h3>退保进度</h3></div>
              <em>{{ surrenderRequests.length }}笔</em>
            </div>
            <article v-for="item in surrenderRequests" :key="item.request_id">
              <div class="surrender-record-mark">退</div>
              <div>
                <strong>{{ item.request_no }}</strong>
                <p>{{ policyById(item.policy_id)?.product_name ?? "关联保单" }} · {{ item.reason }}</p>
                <small>{{ dateTime(item.created_at) }}</small>
              </div>
              <aside>
                <span :class="surrenderStatusTone(item.status)">{{ surrenderStatus(item.status) }}</span>
                <strong>{{ money(item.refund_amount_cents) }}</strong>
                <small class="surrender-record-basis">
                  {{
                    item.calculation?.pre_effective
                      ? "保障生效前全额退还"
                      : item.calculation?.cooling_off
                        ? "犹豫期内全额退还"
                        : "按剩余保障期折算"
                  }}
                </small>
              </aside>
            </article>
          </section>

          <div
            v-if="coverageSection === 'policies' && !order && policies.length === 0"
            class="coverage-empty"
          >
            <span class="empty-shield">◇</span>
            <h3>还没有保障记录</h3>
            <p>先和智能顾问聊聊，找到适合你的保障方案。</p>
            <button @click="activeTab = 'advisor'">咨询智能顾问</button>
          </div>
          <div
            v-if="coverageSection === 'surrenders' && surrenderRequests.length === 0"
            class="surrender-empty-records coverage-section-enter"
          >
            <span>00</span>
            <h3>还没有退保记录</h3>
            <p>当前保障仍在正常生效。如需调整保障计划，请从有效保单中发起申请。</p>
            <button type="button" @click="coverageSection = 'policies'">查看保障保单</button>
          </div>
          <p v-if="error" class="error status-message" role="alert"><span>!</span>{{ error }}</p>
        </section>

        <section v-else class="tab-page service-page">
          <div class="page-heading">
            <div><p class="step">POLICY SERVICE DESK</p><h2>保单服务</h2></div>
            <span class="record-count">{{ serviceRequests.length }}笔申请</span>
          </div>
          <p class="page-lead">查看从有效保单发起的服务申请，以及运营人员反馈的最新处理进度。</p>

          <Teleport to="body">
            <div v-if="serviceApplicationOpen" class="service-application-sheet">
              <div class="service-sheet-header">
              <button type="button" aria-label="关闭服务申请" @click="closeServiceApplication">←</button>
              <div><small>POLICY SERVICE APPLICATION</small><strong>发起保单服务</strong></div>
              <span>关联保单</span>
              </div>
              <div class="service-intro-card">
                <span class="service-intro-mark">AI</span>
                <div>
                  <strong>医疗费用报销协助</strong>
                  <p>识别结果只作为候选内容，不代表保险公司作出赔付承诺。</p>
                </div>
              </div>

              <div class="service-progress-rail">
                <div :class="{ active: true }"><span>01</span><small>选择保单</small></div>
                <i></i>
                <div :class="{ active: !!serviceFile }"><span>02</span><small>识别材料</small></div>
                <i></i>
                <div :class="{ active: !!materialRecognition }"><span>03</span><small>确认提交</small></div>
              </div>

              <div v-if="selectedServicePolicy" class="service-section">
                <div class="service-section-heading">
                  <span>01</span>
                  <div><strong>已关联保障中的保单</strong><small>服务申请将绑定到该保单</small></div>
                </div>
                <div class="service-linked-policy">
                  <span class="policy-choice-mark"></span>
                  <div>
                    <strong>{{ selectedServicePolicy.product_name }}</strong>
                    <small>{{ selectedServicePolicy.plan_name }} · {{ selectedServicePolicy.policy_no }}</small>
                  </div>
                  <em>{{ policyStatus(selectedServicePolicy.status) }}</em>
                </div>
              </div>

              <div v-if="selectedServicePolicy" class="service-section">
                <div class="service-section-heading">
                  <span>02</span>
                  <div><strong>上传医疗费用发票</strong><small>支持 JPG、PNG、WebP，单张不超过 10MB</small></div>
                </div>

                <label v-if="!serviceFilePreview" class="service-upload">
                  <input
                    type="file"
                    accept="image/jpeg,image/png,image/webp"
                    capture="environment"
                    @change="handleServiceFile"
                  />
                  <span class="upload-symbol">
                    <svg viewBox="0 0 24 24" aria-hidden="true">
                      <path d="M12 16V4m0 0L7 9m5-5 5 5M5 14v5h14v-5" />
                    </svg>
                  </span>
                  <strong>拍照或选择票据图片</strong>
                  <small>请保持票据完整、平整，文字和金额清晰可见</small>
                </label>

                <div v-else class="service-file-preview">
                  <img :src="serviceFilePreview" alt="待识别的医疗费用发票" />
                  <div>
                    <span>待识别材料</span>
                    <strong>{{ serviceFile?.name }}</strong>
                    <small>{{ serviceFile ? `${(serviceFile.size / 1024 / 1024).toFixed(2)} MB` : "" }}</small>
                    <button type="button" :disabled="serviceLoading" @click="clearServiceMaterial">重新选择</button>
                  </div>
                </div>

                <button
                  v-if="serviceFile && !materialRecognition"
                  type="button"
                  class="service-primary"
                  :disabled="serviceLoading"
                  @click="recognizeServiceMaterial"
                >
                  <span class="ai-button-mark">AI</span>
                  {{ serviceLoading ? "正在识别票据内容…" : "开始智能识别" }}
                  <em>→</em>
                </button>
                <p v-if="serviceError" class="error status-message service-recognition-error" role="alert">
                  <span>!</span>{{ serviceError }}
                </p>
              </div>

              <div v-if="materialRecognition" class="service-section recognition-section">
                <div class="service-section-heading">
                  <span>03</span>
                  <div><strong>核对识别结果</strong><small>所有字段都可以修改，请以票据原件为准</small></div>
                </div>

                <div v-if="materialRecognition.warnings.length" class="recognition-warnings">
                  <strong>请重点核对</strong>
                  <p v-for="warning in materialRecognition.warnings" :key="warning">{{ warning }}</p>
                </div>

                <div class="service-confirm-form">
                  <label>票据号码<input v-model.trim="serviceFields.invoice_no" placeholder="请输入票据号码" /></label>
                  <label>医疗机构<input v-model.trim="serviceFields.hospital_name" placeholder="请输入医疗机构名称" /></label>
                  <label>就诊日期<input v-model.trim="serviceFields.visit_date" type="date" /></label>
                  <label>费用金额（元）<input v-model.trim="serviceFields.amount_yuan" inputmode="decimal" placeholder="0.00" /></label>
                  <label>联系电话<input v-model.trim="serviceFields.contact_mobile" inputmode="tel" maxlength="11" /></label>
                </div>

                <div class="service-material-note">
                  <span>材料提醒</span>
                  <p>后续审核可能需要费用清单、诊断证明、入出院记录等材料，运营人员会在申请记录中反馈。</p>
                </div>

                <label class="service-confirm-check">
                  <input v-model="serviceConfirmed" type="checkbox" />
                  <i aria-hidden="true">✓</i>
                  <span>我已对照原始票据核对以上信息，并确认内容真实、完整。</span>
                </label>

                <button
                  type="button"
                  class="service-primary submit"
                  :disabled="serviceLoading"
                  @click="submitServiceRequest"
                >
                  {{ serviceLoading ? "正在提交申请…" : "确认并提交服务申请" }}
                  <em>→</em>
                </button>
              </div>
            </div>
          </Teleport>

          <template v-if="!serviceApplicationOpen">
            <div v-if="serviceRequests.length" class="service-request-list">
              <article v-for="item in serviceRequests" :key="item.request_id" class="service-request-card">
                <div class="service-request-topline">
                  <span>{{ item.request_no }}</span>
                  <em :class="serviceRequestStatusTone(item.status)">
                    {{ serviceRequestStatus(item.status) }}
                  </em>
                </div>
                <h3>{{ policyById(item.policy_id)?.product_name ?? "医疗费用报销协助" }}</h3>
                <p>{{ policyById(item.policy_id)?.policy_no ?? item.policy_id }}</p>
                <dl>
                  <div><dt>医疗机构</dt><dd>{{ item.confirmed_fields.hospital_name }}</dd></div>
                  <div><dt>申请金额</dt><dd>¥{{ item.confirmed_fields.amount_yuan }}</dd></div>
                  <div><dt>提交时间</dt><dd>{{ dateTime(item.submitted_at) }}</dd></div>
                </dl>
                <div v-if="item.review_note" class="service-review-note">
                  <span>处理意见</span>
                  <p>{{ item.review_note }}</p>
                </div>
              </article>
            </div>
            <div v-else class="service-empty-records">
              <span>00</span>
              <h3>还没有服务申请</h3>
              <p>请前往“我的保障”，从保障中的保单发起服务申请。</p>
              <button type="button" @click="activeTab = 'coverage'">前往我的保障</button>
            </div>
          </template>

          <p v-if="serviceError && !serviceApplicationOpen" class="error status-message" role="alert"><span>!</span>{{ serviceError }}</p>
        </section>
      </div>

      <nav class="bottom-nav" aria-label="主导航">
        <button :class="{ active: activeTab === 'advisor' }" @click="activeTab = 'advisor'">
          <svg viewBox="0 0 24 24" aria-hidden="true">
            <path d="M7 8h10M7 12h7M8 18l-4 2 1-4a8 8 0 1 1 3 2Z" />
          </svg>
          <span>智能顾问</span>
        </button>
        <button :class="{ active: activeTab === 'plans' }" @click="activeTab = 'plans'">
          <svg viewBox="0 0 24 24" aria-hidden="true">
            <path d="M12 3 4 6v5c0 5 3 8 8 10 5-2 8-5 8-10V6l-8-3Zm-3 9 2 2 4-5" />
          </svg>
          <span>保障方案</span>
        </button>
        <button :class="{ active: activeTab === 'coverage' }" @click="activeTab = 'coverage'">
          <svg viewBox="0 0 24 24" aria-hidden="true">
            <path d="M7 3h8l4 4v14H7V3Zm8 0v5h4M10 12h6M10 16h6" />
          </svg>
          <span>我的保障</span>
        </button>
        <button :class="{ active: activeTab === 'services' }" @click="openServiceCenter">
          <svg viewBox="0 0 24 24" aria-hidden="true">
            <path d="M4 6h16v13H4V6Zm4-3h8v3H8V3Zm1 9h6M12 9v6" />
          </svg>
          <span>保单服务</span>
        </button>
      </nav>

      <div
        v-if="conversationHistoryOpen"
        class="conversation-layer"
        role="dialog"
        aria-modal="true"
        aria-label="历史会话"
        @click.self="conversationHistoryOpen = false"
      >
        <section class="conversation-drawer">
          <header>
            <div>
              <p class="step">CONVERSATION ARCHIVE</p>
              <h2>历史会话</h2>
            </div>
            <button type="button" aria-label="关闭历史会话" @click="conversationHistoryOpen = false">×</button>
          </header>
          <div class="conversation-summary">
            <span>会话已安全保存在你的账户中</span>
            <strong>{{ agentThreads.length }}<small>次咨询</small></strong>
          </div>
          <button type="button" class="new-conversation-card" @click="startNewConversation">
            <span>＋</span>
            <div><strong>开始新会话</strong><small>创建一个独立的咨询上下文</small></div>
            <em>→</em>
          </button>
          <div v-if="conversationHistoryLoading" class="conversation-loading">
            <i></i><span>正在读取历史会话…</span>
          </div>
          <div v-else-if="agentThreads.length" class="conversation-list">
            <button
              v-for="thread in agentThreads"
              :key="thread.id"
              type="button"
              :class="{ current: thread.id === agentThreadId }"
              @click="restoreConversation(thread)"
            >
              <div class="conversation-index">
                <span>{{ String(agentThreads.indexOf(thread) + 1).padStart(2, "0") }}</span>
                <i v-if="thread.id === agentThreadId"></i>
              </div>
              <div>
                <strong>{{ thread.title }}</strong>
                <p>{{ thread.last_message?.content ?? "尚未发送消息" }}</p>
                <small>{{ dateTime(thread.updated_at) }} · {{ thread.message_count }}条消息</small>
              </div>
              <em>›</em>
            </button>
          </div>
          <div v-else class="conversation-empty">
            <span>◇</span>
            <strong>暂无历史会话</strong>
            <p>发送第一条咨询后，会话会自动保存到这里。</p>
          </div>
        </section>
      </div>

      <div v-if="enrollmentOpen" class="enrollment-layer" role="dialog" aria-modal="true">
        <header class="enrollment-header">
          <button aria-label="关闭投保流程" @click="closeEnrollment">×</button>
          <div>
            <span>{{ preQuoteConfirming ? "投保前确认" : "投保流程" }}</span>
            <strong>{{ selectedProduct?.name ?? "保障计划" }}</strong>
          </div>
          <small>{{ preQuoteConfirming ? "准备投保" : `${enrollmentStep}/4` }}</small>
        </header>
        <div v-if="!preQuoteConfirming" class="progress-track">
          <span v-for="stepNo in 4" :key="stepNo" :class="{ active: stepNo <= enrollmentStep }"></span>
        </div>
        <div v-else class="direct-entry-line"></div>

        <div class="enrollment-body">
          <section v-if="preQuoteConfirming" class="enrollment-step direct-enrollment-step">
            <p class="step">DIRECT ENROLLMENT</p>
            <h2>确认投保条件</h2>
            <p class="step-description">顾问已经为你选好方案，确认以下信息后即可测算保费并继续投保。</p>

            <div class="direct-product">
              <div>
                <span>顾问推荐</span>
                <h3>{{ selectedProduct?.name }}</h3>
                <p>{{ selectedProduct?.summary }}</p>
              </div>
              <em>已选择</em>
            </div>

            <div class="form-section direct-condition-form">
              <div class="form-section-title"><span>被保人情况</span><small>用于资格校验和保费测算</small></div>
              <label>
                与投保人关系
                <span class="relation-options">
                  <button
                    v-for="item in (['SELF', 'PARENT', 'SPOUSE', 'CHILD'] as Relationship[])"
                    :key="item"
                    type="button"
                    :class="{ active: relationship === item }"
                    @click="relationship = item"
                  >
                    {{ relationshipName(item) }}
                  </button>
                </span>
              </label>
              <label>
                被保人年龄
                <input v-model.number="insuredAge" type="number" min="0" max="120" inputmode="numeric" />
              </label>
              <div class="fixed-conditions">
                <span>常住地区：成都市</span><span>职业类别：普通职业</span>
              </div>
            </div>

            <div class="direct-entry-note">
              <span>AI</span>
              <p>以上信息已根据你的咨询自动带入，请确认无误。最终是否可投保及保费以本次系统测算结果为准。</p>
            </div>
            <p v-if="error" class="error status-message" role="alert"><span>!</span>{{ error }}</p>
            <button
              class="flow-primary"
              :disabled="businessLoading"
              @click="confirmDirectEnrollment"
            >
              {{ businessLoading ? "正在测算保费…" : "确认并开始投保" }} <span>→</span>
            </button>
          </section>

          <section v-else-if="enrollmentStep === 1" class="enrollment-step">
            <p class="step">STEP 01</p>
            <h2>填写投保信息</h2>
            <p class="step-description">请确认信息真实、准确，这些信息将写入投保订单。</p>
            <div class="quote-mini">
              <div><span>{{ selectedProduct?.name }}</span><small>{{ relationshipName(relationship) }} · {{ insuredAge }}岁</small></div>
              <strong>{{ quote ? money(quote.premium_cents) : "—" }}</strong>
            </div>

            <div class="form-section">
              <div class="form-section-title"><span>投保人</span><small>负责缴费和接收通知</small></div>
              <label>姓名<input v-model.trim="applicantName" autocomplete="name" placeholder="请输入真实姓名" /></label>
              <label>身份证号<input v-model.trim="applicantIdNo" maxlength="18" placeholder="请输入18位身份证号" /></label>
              <label>联系手机<input v-model.trim="contactMobile" inputmode="tel" maxlength="11" placeholder="请输入接收通知的手机号" /><small class="input-help">默认使用登录手机号，可修改</small></label>
            </div>
            <div class="form-section">
              <div class="form-section-title"><span>被保人</span><small>实际享受保障的人</small></div>
              <label>与投保人关系
                <select v-model="relationship">
                  <option value="SELF">本人</option><option value="PARENT">父母</option>
                  <option value="SPOUSE">配偶</option><option value="CHILD">子女</option>
                </select>
              </label>
              <label>姓名<input v-model.trim="insuredName" placeholder="请输入真实姓名" /></label>
              <label>身份证号<input v-model.trim="insuredIdNo" maxlength="18" placeholder="请输入18位身份证号" /></label>
            </div>
            <p v-if="error" class="error status-message" role="alert"><span>!</span>{{ error }}</p>
            <button class="flow-primary" @click="goToNotice">下一步：阅读投保须知 <span>→</span></button>
          </section>

          <section v-else-if="enrollmentStep === 2" class="enrollment-step notice-step">
            <p class="step">STEP 02</p>
            <h2>投保须知</h2>
            <p class="step-description">这些内容会影响承保和理赔，请完整阅读后再确认。</p>
            <div class="notice-document">
              <header><span>智保通投保须知</span><small>适用于 {{ selectedProduct?.name }}</small></header>
              <div>
                <h3>一、投保资格</h3>
                <p>被保人应符合产品约定的年龄、常住地区和职业范围。本次试算结果仅对当前填写信息有效。</p>
                <h3>二、如实告知</h3>
                <p>投保人应如实填写身份及健康相关信息。故意或因重大过失未履行如实告知义务，可能影响合同效力及理赔结果。</p>
                <h3>三、保障生效</h3>
                <p>支付成功并完成承保后生成电子保单，具体保障责任、等待期、免赔额和责任限额以电子保单及保险条款为准。</p>
                <h3>四、责任免除</h3>
                <p>既往症、违法行为、故意伤害以及条款明确列示的其他情形可能不在保障范围内，请重点阅读责任免除部分。</p>
                <h3>五、信息与退保</h3>
                <p>投保信息将用于承保、保全和服务处理。退保规则、可退金额及生效后的处理方式以保险条款和后续服务流程为准。</p>
              </div>
            </div>
            <label class="notice-check">
              <input v-model="noticeAccepted" type="checkbox" />
              <span>我已阅读并理解《投保须知》《保险条款》和《责任免除》</span>
            </label>
            <label class="notice-check">
              <input v-model="disclosureConfirmed" type="checkbox" />
              <span>我确认投保信息真实、完整，并同意进行电子投保</span>
            </label>
            <p v-if="error" class="error status-message" role="alert"><span>!</span>{{ error }}</p>
            <div class="flow-actions">
              <button class="flow-back" @click="setEnrollmentStep(1)">上一步</button>
              <button class="flow-primary" @click="goToConfirmation">同意并继续 <span>→</span></button>
            </div>
          </section>

          <section v-else-if="enrollmentStep === 3" class="enrollment-step confirmation-step">
            <p class="step">STEP 03</p>
            <h2>确认投保订单</h2>
            <p class="step-description">请最后核对保障方案和人员信息。</p>
            <div class="confirm-product">
              <span>保障方案</span>
              <h3>{{ selectedProduct?.name }}</h3>
              <p>{{ relationshipName(relationship) }} · {{ insuredAge }}岁 · 成都市 · 普通职业</p>
              <strong>{{ quote ? money(quote.premium_cents) : "—" }}<small>/年</small></strong>
            </div>
            <dl class="confirm-list">
              <div><dt>投保人</dt><dd>{{ applicantName }}</dd></div>
              <div><dt>投保人证件</dt><dd>{{ maskIdNo(applicantIdNo) }}</dd></div>
              <div><dt>被保人</dt><dd>{{ insuredName }}</dd></div>
              <div><dt>被保人证件</dt><dd>{{ maskIdNo(insuredIdNo) }}</dd></div>
              <div><dt>联系电话</dt><dd>{{ contactMobile }}</dd></div>
            </dl>
            <div class="confirmed-notice"><span>✓</span>已阅读并同意投保须知与责任免除</div>
            <p v-if="error" class="error status-message" role="alert"><span>!</span>{{ error }}</p>
            <div class="flow-actions">
              <button class="flow-back" @click="setEnrollmentStep(2)">上一步</button>
              <button class="flow-primary" :disabled="businessLoading" @click="confirmAndCreateOrder">
                {{ businessLoading ? "正在创建订单…" : `确认投保并支付 ${quote ? money(quote.premium_cents) : ""}` }}
              </button>
            </div>
          </section>

          <section v-else class="enrollment-step payment-step">
            <template v-if="!policy">
              <p class="step">STEP 04</p>
              <h2>安全支付</h2>
              <p class="step-description">确认支付后将立即完成承保并生成电子保单。</p>
              <div class="payment-amount">
                <span>应付金额</span>
                <strong>{{ order ? money(order.amount_cents) : quote ? money(quote.premium_cents) : "—" }}</strong>
                <small>{{ selectedProduct?.name ?? "智保通保障计划" }}</small>
              </div>
              <div class="payment-channel">
                <span class="channel-icon">¥</span>
                <div><strong>快捷支付</strong><small>支付过程由安全通道保护</small></div>
                <em>已选择</em>
              </div>
              <p v-if="order" class="payment-order">订单号 {{ order.order_no }}</p>
              <p v-if="error" class="error status-message" role="alert"><span>!</span>{{ error }}</p>
              <button
                class="flow-primary pay-button"
                :disabled="businessLoading || !payment"
                @click="completePayment"
              >
                {{ businessLoading ? "正在确认支付…" : payment ? "确认支付" : "正在准备支付…" }}
              </button>
            </template>

            <template v-else>
              <div class="success-symbol"><span>✓</span></div>
              <p class="step success-step">ENROLLMENT COMPLETED</p>
              <h2>投保成功</h2>
              <p class="step-description">电子保单已经生成，保障信息已保存到“我的保障”。</p>
              <div class="success-policy">
                <span>电子保单号</span><strong>{{ policy.policy_no }}</strong>
                <div><small>保障状态</small><em>{{ policyStatus(policy.status) }}</em></div>
                <div><small>保障期限</small><p>{{ policy.coverage_start }} 至 {{ policy.coverage_end }}</p></div>
              </div>
              <button class="flow-primary" @click="viewCoverageAfterPayment">查看我的保障 <span>→</span></button>
            </template>
          </section>
        </div>
      </div>

      <div
        v-if="openedPolicy"
        class="policy-document-layer"
        role="dialog"
        aria-modal="true"
        aria-label="电子保单"
        @click.self="closePolicyDocument"
      >
        <article class="policy-document">
          <header class="policy-document-header">
            <button type="button" aria-label="关闭电子保单" @click="closePolicyDocument">×</button>
            <div><span>智保通</span><small>电子保险凭证</small></div>
            <em>{{ policyStatus(openedPolicy.status) }}</em>
          </header>

          <section class="policy-document-hero">
            <p>POLICY CERTIFICATE</p>
            <h2>{{ openedPolicy.product_name }}</h2>
            <span>{{ openedPolicy.plan_name }}</span>
            <div><small>电子保单号</small><strong>{{ openedPolicy.policy_no }}</strong></div>
          </section>

          <section class="policy-document-body">
            <div class="document-section-title"><span>01</span><h3>合同信息</h3></div>
            <dl class="document-fields">
              <div><dt>保单状态</dt><dd class="active-status">{{ policyStatus(openedPolicy.status) }}</dd></div>
              <div><dt>订单编号</dt><dd>{{ openedPolicy.order_no }}</dd></div>
              <div><dt>签发时间</dt><dd>{{ dateTime(openedPolicy.issued_at) }}</dd></div>
              <div><dt>年度保费</dt><dd>{{ money(openedPolicy.premium_cents) }}</dd></div>
              <div class="wide-field"><dt>保障期限</dt><dd>{{ openedPolicy.coverage_start }} 至 {{ openedPolicy.coverage_end }}</dd></div>
            </dl>

            <div class="document-section-title"><span>02</span><h3>被保险人信息</h3></div>
            <dl class="document-fields">
              <div><dt>投保人</dt><dd>{{ openedPolicy.applicant.name ?? "—" }}</dd></div>
              <div><dt>投保人证件</dt><dd>{{ maskIdNo(openedPolicy.applicant.id_no ?? "") || "—" }}</dd></div>
              <div><dt>被保人</dt><dd>{{ openedPolicy.insured.name ?? "—" }}</dd></div>
              <div><dt>被保人证件</dt><dd>{{ maskIdNo(openedPolicy.insured.id_no ?? "") || "—" }}</dd></div>
            </dl>

            <div class="policy-document-note">
              <span>i</span>
              <p>本电子凭证记录本次承保结果。具体保障责任、责任免除、等待期及赔付限额以保险条款和投保须知为准。</p>
            </div>
            <div class="policy-seal"><span>智保通</span><small>电子承保凭证</small></div>
            <button type="button" class="document-print" @click="printPolicyDocument">打印 / 保存为 PDF</button>
          </section>
        </article>
      </div>
    </div>
  </main>
</template>
