type AnswerValue = {
  selected: string[];
  otherText?: string;
};

type AnswerMap = Record<string, AnswerValue>;

type ResultState = {
  primaryId: string;
  secondaryId: string;
  scores: Record<string, number>;
  intensity: {
    level: "light" | "standard" | "advanced";
    label: string;
    description: string;
  };
  createdAt: string;
};

type PlanItem = {
  id: string;
  taskId: string;
  name: string;
  category: string;
  startDay: number;
  duration: number;
  completed: boolean;
  daily: string;
  difficulty: string;
  outcome: string;
};

type DraftTask = {
  id: string;
  name: string;
  category: string;
  duration: number;
  daily: string;
  difficulty: string;
  audience: string;
  outcome: string;
};

type PosterGender = "male" | "female";

const { useEffect, useMemo, useRef, useState } = React;
const config = (window as any).SummerPlanApp;
const STORAGE_KEY = "gaokao-summer-planner-state-v1";
const POSTER_GENDER_KEY = "summer_planner_poster_gender";
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
const PLAN_START_UTC = Date.UTC(2026, 5, 10);
const PLAN_END_UTC = Date.UTC(2026, 7, 31);
const DEFAULT_PLAN_LENGTH = Math.round((PLAN_END_UTC - PLAN_START_UTC) / ONE_DAY_MS) + 1;
const PLAN_START_INPUT = "2026-06-10";
const PLAN_END_INPUT = "2026-08-31";
const WEEKDAYS = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
const PLAN_DATE_EVENTS = [
  { startDay: 15, endDay: 15, label: "出高考成绩" },
  { startDay: 16, endDay: 21, label: "志愿填报" }
];
const POSTER_GENDER_OPTIONS: Array<{ value: PosterGender; title: string; description: string; icon: string }> = [
  {
    value: "male",
    title: "男生版",
    description: "阳光、清爽、准大学生男生形象",
    icon: "user-round"
  },
  {
    value: "female",
    title: "女生版",
    description: "元气、清新、准大学生女生形象",
    icon: "sparkles"
  }
];

type PosterImageSet = Partial<Record<PosterGender | "default", string>>;

const personaPosterImages: Record<string, PosterImageSet> = {
  "自由松弛型": {
    male: "/posters/1-male.png",
    female: "/posters/1-female.png",
    default: "/posters/1-female.png"
  },
  "大学领跑型": {
    male: "/posters/2-male.png",
    female: "/posters/2-female.png",
    default: "/posters/2-female.png"
  },
  "技能成长型": {
    male: "/posters/3-male.png",
    female: "/posters/3-female.png",
    default: "/posters/3-female.png"
  },
  "兼职赚钱型": {
    male: "/posters/4-male.png",
    female: "/posters/4-female.png",
    default: "/posters/4-female.png"
  },
  "逆袭准备型": {
    male: "/posters/5-male.png",
    female: "/posters/5-female.png",
    default: "/posters/5-female.png"
  },
  "社交适应型": {
    male: "/posters/6-male.png",
    female: "/posters/6-female.png",
    default: "/posters/6-female.png"
  },
  "迷茫探索型": {
    male: "/posters/7-male.png",
    female: "/posters/7-female.png",
    default: "/posters/7-female.png"
  },
  "生活独立型": {
    male: "/posters/8-male.png",
    female: "/posters/8-female.png",
    default: "/posters/8-female.png"
  },
  "兴趣享受型": {
    male: "/posters/9-male.png",
    female: "/posters/9-female.png",
    default: "/posters/9-female.png"
  },
  "自律成长型": {
    male: "/posters/9-male.png",
    female: "/posters/9-female.png",
    default: "/posters/9-female.png"
  }
} as const;

function cx(...parts: Array<string | false | null | undefined>) {
  return parts.filter(Boolean).join(" ");
}

function Icon({ name, className = "h-4 w-4" }: { name: string; className?: string }) {
  const iconName = name
    .split("-")
    .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
    .join("");
  const icon = (window as any).lucide?.icons?.[iconName] || (window as any).lucide?.[iconName];
  const nodes = icon?.[2] || [["circle", { cx: "12", cy: "12", r: "9" }]];

  return (
    <svg
      className={className}
      xmlns="http://www.w3.org/2000/svg"
      width="24"
      height="24"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      {nodes.map((node: any, index: number) => renderIconNode(node, index))}
    </svg>
  );
}

function renderIconNode(node: any, key: number | string): any {
  const [tag, attrs = {}, children = []] = node;
  const normalizedAttrs = Object.entries(attrs).reduce((memo: Record<string, string>, [attr, value]) => {
    const normalized = attr.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
    memo[normalized === "class" ? "className" : normalized] = String(value);
    return memo;
  }, {});

  return React.createElement(
    tag,
    { ...normalizedAttrs, key },
    children.map((child: any, index: number) => renderIconNode(child, `${key}-${index}`))
  );
}

function findPersona(id: string) {
  return config.personas.find((persona: any) => persona.id === id) || config.personas[0];
}

function findTask(id: string) {
  return config.tasks.find((task: any) => task.id === id);
}

function findCategory(id: string) {
  return config.taskCategories.find((category: any) => category.id === id) || config.taskCategories[0];
}

function categoryTokenClass(categoryId: string) {
  switch (categoryId) {
    case "relax":
      return "bg-emerald-100 text-emerald-800 border-emerald-200";
    case "skill":
      return "bg-sky-100 text-sky-800 border-sky-200";
    case "study":
      return "bg-violet-100 text-violet-800 border-violet-200";
    case "life":
      return "bg-orange-100 text-orange-800 border-orange-200";
    case "college":
      return "bg-amber-100 text-amber-800 border-amber-200";
    case "ai":
      return "bg-indigo-100 text-indigo-800 border-indigo-200";
    default:
      return "bg-slate-100 text-slate-800 border-slate-200";
  }
}

function categoryBarClass(categoryId: string) {
  switch (categoryId) {
    case "relax":
      return "bg-emerald-500";
    case "skill":
      return "bg-sky-500";
    case "study":
      return "bg-violet-500";
    case "life":
      return "bg-orange-500";
    case "college":
      return "bg-amber-400";
    case "ai":
      return "bg-gradient-to-r from-violet-500 to-sky-500";
    default:
      return "bg-slate-500";
  }
}

function clampNumber(value: number, min: number, max: number) {
  if (Number.isNaN(value)) return min;
  return Math.max(min, Math.min(max, value));
}

function getPlanDate(day: number) {
  return new Date(PLAN_START_UTC + (clampNumber(day, 1, DEFAULT_PLAN_LENGTH) - 1) * ONE_DAY_MS);
}

function formatPlanDate(day: number) {
  const date = getPlanDate(day);
  return `${date.getUTCMonth() + 1}月${date.getUTCDate()}日`;
}

function formatPlanDateWithWeekday(day: number) {
  const date = getPlanDate(day);
  return `${formatPlanDate(day)} ${WEEKDAYS[date.getUTCDay()]}`;
}

function formatPlanDateRange(startDay: number, endDay: number) {
  return `${formatPlanDate(startDay)} - ${formatPlanDate(endDay)}`;
}

function toDateInputValue(day: number) {
  const date = getPlanDate(day);
  const month = String(date.getUTCMonth() + 1).padStart(2, "0");
  const dateOfMonth = String(date.getUTCDate()).padStart(2, "0");
  return `${date.getUTCFullYear()}-${month}-${dateOfMonth}`;
}

function fromDateInputValue(value: string) {
  if (!value) return 1;
  const [year, month, day] = value.split("-").map(Number);
  const dateUtc = Date.UTC(year, month - 1, day);
  return clampNumber(Math.round((dateUtc - PLAN_START_UTC) / ONE_DAY_MS) + 1, 1, DEFAULT_PLAN_LENGTH);
}

function getPlanDateEvents(day: number) {
  return PLAN_DATE_EVENTS.filter((event) => day >= event.startDay && day <= event.endDay);
}

function loadSavedState() {
  try {
    const raw = window.localStorage.getItem(STORAGE_KEY);
    return raw ? JSON.parse(raw) : {};
  } catch (error) {
    return {};
  }
}

function saveState(state: any) {
  try {
    window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
  } catch (error) {
    console.warn("Unable to save planner state", error);
  }
}

function loadPosterGender(): PosterGender | null {
  try {
    const gender = window.localStorage.getItem(POSTER_GENDER_KEY);
    return gender === "male" || gender === "female" ? gender : null;
  } catch (error) {
    return null;
  }
}

function savePosterGender(gender: PosterGender) {
  try {
    window.localStorage.setItem(POSTER_GENDER_KEY, gender);
  } catch (error) {
    console.warn("Unable to save poster gender", error);
  }
}

function isMultipleQuestion(question: any) {
  return question?.type === "multi" || question?.type === "multiple";
}

function getQuestionMaxSelect(question: any) {
  return question?.maxSelect ?? question?.max;
}

function calculateIntensity(answers: AnswerMap) {
  const time = answers.daily_time?.selected?.[0];
  const discipline = answers.self_discipline?.selected?.[0];
  let score = 1;

  if (time === "30m" || time === "mood") score += 0;
  if (time === "1h") score += 1;
  if (time === "2h") score += 2;
  if (time === "halfday" || time === "many") score += 3;

  if (discipline === "high") score += 1;
  if (discipline === "delay" || discipline === "needcare") score -= 1;

  if (score <= 1) {
    return {
      level: "light",
      label: "轻量版",
      description: "每天留一个小动作就够了，先把节奏找回来。"
    };
  }

  if (score <= 3) {
    return {
      level: "standard",
      label: "标准版",
      description: "适合每天稳定推进，放松和成长都留出空间。"
    };
  }

  return {
    level: "advanced",
    label: "进阶版",
    description: "可以安排更连续的技能或学习任务，但记得保留休息日。"
  };
}

function calculateResult(answers: AnswerMap): ResultState {
  const scores = config.personas.reduce((memo: Record<string, number>, persona: any) => {
    memo[persona.id] = 0;
    return memo;
  }, {});

  Object.entries(answers).forEach(([questionId, answer]) => {
    answer.selected.forEach((optionId) => {
      const weights = config.scoringRules[`${questionId}.${optionId}`] || {};
      Object.entries(weights).forEach(([personaId, weight]) => {
        scores[personaId] = (scores[personaId] || 0) + Number(weight);
      });
    });
  });

  if (Object.values(scores).every((score) => Number(score) === 0)) {
    scores.explore = 1;
  }

  const ranked = Object.entries(scores).sort((a, b) => Number(b[1]) - Number(a[1]));
  return {
    primaryId: ranked[0][0],
    secondaryId: ranked[1]?.[0] || ranked[0][0],
    scores,
    intensity: calculateIntensity(answers),
    createdAt: new Date().toISOString()
  };
}

function getStageRanges(planLength: number) {
  const descriptions = config.stageDescriptions;
  let ranges;

  if (planLength <= 14) {
    ranges = [
      [1, 3],
      [4, 6],
      [7, 11],
      [12, planLength]
    ];
  } else if (planLength <= 21) {
    ranges = [
      [1, 5],
      [6, 10],
      [11, 16],
      [17, planLength]
    ];
  } else if (planLength <= 30) {
    ranges = [
      [1, 7],
      [8, 15],
      [16, 23],
      [24, planLength]
    ];
  } else if (planLength <= 45) {
    ranges = [
      [1, 7],
      [8, 15],
      [16, 35],
      [36, planLength]
    ];
  } else {
    ranges = [
      [1, 7],
      [8, 15],
      [16, 50],
      [51, planLength]
    ];
  }

  return descriptions.map((stage: any, index: number) => ({
    ...stage,
    start: ranges[index][0],
    end: ranges[index][1]
  }));
}

function makePlanItem(task: any, startDay: number, duration: number): PlanItem {
  return {
    id: `plan-${Date.now()}-${Math.random().toString(16).slice(2)}`,
    taskId: task.id,
    name: task.name,
    category: task.category,
    startDay,
    duration,
    completed: false,
    daily: task.daily,
    difficulty: task.difficulty,
    outcome: task.outcome
  };
}

function createDefaultPlan(personaId: string, planLength: number): PlanItem[] {
  const template = config.planTemplates[personaId] || config.planTemplates.explore;
  const stageRanges = getStageRanges(planLength);
  const starts = [
    stageRanges[0].start,
    Math.min(stageRanges[0].end, stageRanges[0].start + 2),
    stageRanges[1].start,
    stageRanges[2].start,
    Math.min(stageRanges[2].end, stageRanges[2].start + Math.max(2, Math.floor((stageRanges[2].end - stageRanges[2].start) / 2))),
    stageRanges[3].start
  ];

  return template.taskIds
    .map((taskId: string, index: number) => {
      const task = findTask(taskId);
      if (!task) return null;
      const startDay = clampNumber(starts[index] || 1, 1, planLength);
      const remainingDays = Math.max(1, planLength - startDay + 1);
      const duration = clampNumber(Number(task.duration || 1), 1, remainingDays);
      return makePlanItem(task, startDay, duration);
    })
    .filter(Boolean) as PlanItem[];
}

function getPlanKeywords(result: ResultState | null, planItems: PlanItem[]) {
  if (!result) return ["暑假", "计划", "成长", "开学准备"];
  const persona = findPersona(result.primaryId);
  const categoryNames = Array.from(new Set(planItems.map((item) => findCategory(item.category).name))).slice(0, 2);
  return [...persona.keywords.slice(0, 3), ...categoryNames].slice(0, 5);
}

function getShareText(result: ResultState | null, planItems: PlanItem[]) {
  const persona = result ? findPersona(result.primaryId) : findPersona("explore");
  const taskNames = Array.from(new Set(planItems.map((item) => item.name))).slice(0, 3).join("、") || "几个小目标";
  return `我刚生成了自己的高考后暑假计划！
我的暑假人格是「${persona.name}」。
这个假期，我准备一边放松，一边完成几个小目标：${taskNames}。
高考结束不是终点，而是大学生活的预热。
你也来测测你的暑假人格吧。`;
}

function findQuestionOption(questionId: string, optionId?: string) {
  const question = config.questions.find((item: any) => item.id === questionId);
  return question?.options?.find((option: any) => option.id === optionId);
}

function getSelectedOptionLabel(answers: AnswerMap, questionId: string) {
  const optionId = answers[questionId]?.selected?.[0];
  return findQuestionOption(questionId, optionId)?.label || "";
}

function compactModeLabel(label: string) {
  return label ? label.split("：")[0] : "自由探索模式";
}

function getAvoidStateSupport(answers: AnswerMap) {
  const fallback = {
    labels: [] as string[],
    pitfalls: [] as string[],
    tasks: [] as string[]
  };
  const map: Record<string, { pitfalls: string[]; tasks: string[] }> = {
    night_reversed: {
      pitfalls: ["别让作息一路滑到下午醒。先把起床时间往前挪一点，再加一个轻量打卡。"],
      tasks: ["恢复作息", "每天散步", "轻量成长打卡"]
    },
    phone_stuck: {
      pitfalls: ["刷手机可以放松，但别让短视频和游戏把一整天吞掉。给娱乐设一个结束点。"],
      tasks: ["手机防沉迷", "轻量运动", "AI生成每日计划"]
    },
    plan_failed: {
      pitfalls: ["计划别写太满。每天只抓一件小事，完成感比排满日程更重要。"],
      tasks: ["轻量暑假计划", "每日小任务", "每周复盘"]
    },
    totally_lost: {
      pitfalls: ["迷茫时先别逼自己立刻确定人生方向，从一个低成本探索开始就够了。"],
      tasks: ["专业探索", "AI整理专业资料", "和学长学姐聊一次"]
    },
    school_panic: {
      pitfalls: ["别把开学准备拖到最后几天。证件、宿舍用品和第一个月节奏可以提前拆开做。"],
      tasks: ["开学清单", "宿舍用品准备", "大学第一个月计划"]
    },
    skill_blank: {
      pitfalls: ["不用变成全能选手，但至少给自己留一个能说出口的小技能。"],
      tasks: ["AI基础入门", "PPT基础", "英语打卡", "摄影剪辑入门"]
    },
    social_missing: {
      pitfalls: ["不用强迫自己热闹，但也别完全消失。保持一点真实交流，会让开学更顺。"],
      tasks: ["和同学朋友聚会", "新生自我介绍", "准大学生交流群"]
    },
    family_conflict: {
      pitfalls: ["在家待久了容易互相看不顺眼，提前安排一点独处、外出和沟通空间。"],
      tasks: ["陪陪家人", "外出放松安排", "开学前家庭沟通"]
    }
  };

  const selectedIds = (answers.avoid_state?.selected || []).filter((id) => map[id]);
  if (!selectedIds.length) return fallback;

  return selectedIds.reduce(
    (memo, avoidId) => ({
      labels: [...memo.labels, findQuestionOption("avoid_state", avoidId)?.label || ""].filter(Boolean),
      pitfalls: [...memo.pitfalls, ...map[avoidId].pitfalls],
      tasks: [...memo.tasks, ...map[avoidId].tasks]
    }),
    fallback
  );
}

function getCalendarPlanRows(planItems: PlanItem[], planLength: number) {
  return Array.from({ length: planLength }, (_, index) => {
    const day = index + 1;
    const date = getPlanDate(day);
    const items = planItems.filter((item) => day >= item.startDay && day < item.startDay + item.duration);
    const events = getPlanDateEvents(day).map((event) => event.label);
    return {
      day,
      date: toDateInputValue(day),
      displayDate: formatPlanDate(day),
      weekday: WEEKDAYS[date.getUTCDay()],
      events: events.join("；") || "-",
      tasks: items.map((item) => item.name).join("；") || "自由安排",
      categories: Array.from(new Set(items.map((item) => findCategory(item.category).name))).join("；") || "-",
      completed: items.length ? `${items.filter((item) => item.completed).length}/${items.length}` : "-",
      outcomes: items.map((item) => item.outcome).join("；") || "-"
    };
  });
}

async function downloadCalendarPlanTable(planItems: PlanItem[], planLength: number) {
  const rows = getCalendarPlanRows(planItems, planLength);
  const captureNode = document.createElement("div");
  captureNode.style.position = "fixed";
  captureNode.style.left = "-1400px";
  captureNode.style.top = "0";
  captureNode.style.width = "1180px";
  captureNode.style.padding = "42px";
  captureNode.style.background = "#f6f9ff";
  captureNode.style.color = "#172033";
  captureNode.style.fontFamily = '"PingFang SC", "Microsoft YaHei", Arial, sans-serif';

  const title = document.createElement("h1");
  title.textContent = "高考后暑假日历计划表";
  title.style.margin = "0";
  title.style.fontSize = "34px";
  title.style.fontWeight = "900";
  captureNode.appendChild(title);

  const subtitle = document.createElement("p");
  subtitle.textContent = `${formatPlanDateRange(1, planLength)} · 共 ${planLength} 天 · 亲手定制暑假生活`;
  subtitle.style.margin = "10px 0 24px";
  subtitle.style.fontSize = "18px";
  subtitle.style.color = "#64748b";
  subtitle.style.fontWeight = "700";
  captureNode.appendChild(subtitle);

  const table = document.createElement("table");
  table.style.width = "100%";
  table.style.borderCollapse = "separate";
  table.style.borderSpacing = "0";
  table.style.overflow = "hidden";
  table.style.borderRadius = "22px";
  table.style.background = "#ffffff";
  table.style.boxShadow = "0 18px 48px rgba(23, 32, 51, 0.12)";

  const headers = ["日期", "星期", "关键事项", "任务安排", "分类", "完成进度", "完成成果"];
  const thead = document.createElement("thead");
  const headerRow = document.createElement("tr");
  headers.forEach((header) => {
    const cell = document.createElement("th");
    cell.textContent = header;
    cell.style.padding = "16px 14px";
    cell.style.background = "#eef2ff";
    cell.style.color = "#4f46e5";
    cell.style.fontSize = "15px";
    cell.style.textAlign = "left";
    cell.style.fontWeight = "900";
    headerRow.appendChild(cell);
  });
  thead.appendChild(headerRow);
  table.appendChild(thead);

  const tbody = document.createElement("tbody");
  rows.forEach((row, index) => {
    const tableRow = document.createElement("tr");
    const values = [row.displayDate, row.weekday, row.events, row.tasks, row.categories, row.completed, row.outcomes];
    values.forEach((value, cellIndex) => {
      const cell = document.createElement("td");
      cell.textContent = value;
      cell.style.padding = "13px 14px";
      cell.style.borderTop = "1px solid #e8eef8";
      cell.style.background = index % 2 === 0 ? "#ffffff" : "#f8fbff";
      cell.style.color = cellIndex === 0 ? "#172033" : "#475569";
      cell.style.fontSize = cellIndex >= 2 ? "13px" : "15px";
      cell.style.fontWeight = cellIndex === 0 ? "900" : "600";
      cell.style.lineHeight = "1.55";
      cell.style.verticalAlign = "top";
      if (cellIndex === 0) cell.style.width = "90px";
      if (cellIndex === 1) cell.style.width = "76px";
      if (cellIndex === 2) cell.style.width = "96px";
      if (cellIndex === 4) cell.style.width = "110px";
      if (cellIndex === 5) cell.style.width = "90px";
      tableRow.appendChild(cell);
    });
    tbody.appendChild(tableRow);
  });
  table.appendChild(tbody);
  captureNode.appendChild(table);

  document.body.appendChild(captureNode);
  try {
    const canvas = await (window as any).html2canvas(captureNode, {
      backgroundColor: "#f6f9ff",
      scale: 2,
      useCORS: true
    });
    const link = document.createElement("a");
    link.download = "高考后暑假日历计划表.png";
    link.href = canvas.toDataURL("image/png");
    link.click();
  } finally {
    document.body.removeChild(captureNode);
  }
}

function App() {
  const saved = useMemo(loadSavedState, []);
  const savedPosterGender = useMemo(loadPosterGender, []);
  const [page, setPage] = useState(saved.page || "home");
  const [answers, setAnswers] = useState<AnswerMap>(saved.answers || {});
  const [result, setResult] = useState<ResultState | null>(saved.result || null);
  const [planLength, setPlanLength] = useState<number>(DEFAULT_PLAN_LENGTH);
  const [planItems, setPlanItems] = useState<PlanItem[]>(saved.planItems || []);
  const [currentQuestion, setCurrentQuestion] = useState<number>(clampNumber(Number(saved.currentQuestion || 0), 0, config.questions.length - 1));
  const [posterGender, setPosterGender] = useState<PosterGender>(savedPosterGender || "male");
  const [showPosterGenderModal, setShowPosterGenderModal] = useState(false);
  const [isGenerating, setIsGenerating] = useState(false);

  useEffect(() => {
    saveState({ page, answers, result, planLength, planItems, currentQuestion });
  }, [page, answers, result, planLength, planItems, currentQuestion]);

  useEffect(() => {
    if (page !== "result") return;
    window.requestAnimationFrame(() => {
      window.scrollTo({ top: 0, left: 0, behavior: "auto" });
    });
  }, [page]);

  function startFlow() {
    setCurrentQuestion(0);
    setPage("questionnaire");
  }

  function submitQuestionnaire() {
    setIsGenerating(true);
    window.setTimeout(() => {
      const nextResult = calculateResult(answers);
      setResult(nextResult);
      setPlanItems(createDefaultPlan(nextResult.primaryId, planLength));
      setPage("result");
      setIsGenerating(false);
    }, 1300);
  }

  function resetQuestionnaire() {
    setAnswers({});
    setResult(null);
    setPlanItems([]);
    setCurrentQuestion(0);
    setPage("questionnaire");
  }

  function restoreRecommendedPlan() {
    if (!result) return;
    setPlanItems(createDefaultPlan(result.primaryId, planLength));
  }

  function requestPoster() {
    if (!result) return;
    setShowPosterGenderModal(true);
  }

  function confirmPosterGender(gender: PosterGender) {
    setPosterGender(gender);
    savePosterGender(gender);
    setShowPosterGenderModal(false);
    setPage("poster");
  }

  return (
    <div className="min-h-screen text-ink">
      <AppHeader page={page} setPage={setPage} result={result} onPoster={requestPoster} />
      {isGenerating && <GeneratingOverlay />}
      {showPosterGenderModal && (
        <PosterGenderModal
          initialGender={posterGender}
          onConfirm={confirmPosterGender}
          onClose={() => setShowPosterGenderModal(false)}
        />
      )}
      {page === "home" && <HomePage onStart={startFlow} />}
      {page === "questionnaire" && (
        <QuestionnairePage
          answers={answers}
          setAnswers={setAnswers}
          currentQuestion={currentQuestion}
          setCurrentQuestion={setCurrentQuestion}
          onSubmit={submitQuestionnaire}
          onBackHome={() => setPage("home")}
        />
      )}
      {page === "result" && (
        <ResultPage
          result={result}
          answers={answers}
          planLength={planLength}
          onGoPlanner={() => setPage("planner")}
          onRestart={resetQuestionnaire}
          onPoster={requestPoster}
          planItems={planItems}
        />
      )}
      {page === "planner" && (
        <PlannerPage
          result={result}
          planLength={planLength}
          planItems={planItems}
          setPlanItems={setPlanItems}
          onRestore={restoreRecommendedPlan}
          onPoster={requestPoster}
          onResult={() => setPage("result")}
        />
      )}
      {page === "poster" && (
        <PosterPage
          result={result}
          answers={answers}
          planItems={planItems}
          planLength={planLength}
          posterGender={posterGender}
          onPlanner={() => setPage("planner")}
          onRestart={resetQuestionnaire}
        />
      )}
    </div>
  );
}

function AppHeader({
  page,
  setPage,
  result,
  onPoster
}: {
  page: string;
  setPage: (page: string) => void;
  result: ResultState | null;
  onPoster: () => void;
}) {
  return (
    <header className="sticky top-0 z-30 border-b border-white/70 bg-white/78 backdrop-blur-xl">
      <div className="mx-auto flex max-w-7xl items-center justify-between gap-3 px-4 py-3 sm:px-6">
        <button
          className="flex min-w-0 items-center gap-2 rounded-full px-2 py-1 text-left font-semibold text-ink"
          onClick={() => setPage("home")}
        >
          <span className="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-gradient-to-br from-violet to-ocean text-white shadow-soft">
            <Icon name="calendar-check" className="h-5 w-5" />
          </span>
          <span className="hidden sm:block">暑假计划生成器</span>
        </button>
        <nav className="flex items-center gap-1 rounded-full bg-slate-100 p-1 text-sm">
          <HeaderNavButton active={page === "questionnaire"} onClick={() => setPage("questionnaire")} label="测试" />
          <HeaderNavButton active={page === "result"} disabled={!result} onClick={() => setPage("result")} label="结果" />
          <HeaderNavButton active={page === "planner"} disabled={!result} onClick={() => setPage("planner")} label="计划" />
          <HeaderNavButton active={page === "poster"} disabled={!result} onClick={onPoster} label="海报" />
        </nav>
      </div>
    </header>
  );
}

function HeaderNavButton({ active, disabled, onClick, label }: { active: boolean; disabled?: boolean; onClick: () => void; label: string }) {
  return (
    <button
      className={cx(
        "rounded-full px-3 py-1.5 transition",
        active ? "bg-white text-violet shadow-sm" : "text-slate-600 hover:bg-white/70",
        disabled && "cursor-not-allowed opacity-40"
      )}
      disabled={disabled}
      onClick={onClick}
    >
      {label}
    </button>
  );
}

function PosterGenderModal({
  initialGender,
  onConfirm,
  onClose
}: {
  initialGender: PosterGender;
  onConfirm: (gender: PosterGender) => void;
  onClose: () => void;
}) {
  const [selectedGender, setSelectedGender] = useState<PosterGender>(initialGender);

  return (
    <div className="fixed inset-0 z-50 grid place-items-center bg-slate-900/38 p-4 backdrop-blur-xl">
      <section className="w-full max-w-2xl rounded-3xl bg-white p-5 shadow-glow sm:p-7">
        <div className="flex items-start justify-between gap-4">
          <div>
            <p className="text-sm font-bold text-violet">生成海报前</p>
            <h2 className="mt-2 text-2xl font-black">选择你的海报主角</h2>
            <p className="mt-2 leading-7 text-slate-600">生成一张更像你的专属暑假人格海报</p>
          </div>
          <button className="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-slate-100 text-slate-500" onClick={onClose} aria-label="关闭">
            <Icon name="x" />
          </button>
        </div>

        <div className="mt-6 grid gap-3 sm:grid-cols-2">
          {POSTER_GENDER_OPTIONS.map((option) => {
            const selected = selectedGender === option.value;
            return (
              <button
                key={option.value}
                className={cx(
                  "rounded-3xl border p-5 text-left transition",
                  selected ? "border-violet bg-indigo-50 shadow-soft" : "border-slate-200 bg-white hover:border-violet/35 hover:bg-slate-50"
                )}
                onClick={() => setSelectedGender(option.value)}
              >
                <span className={cx("mb-4 grid h-12 w-12 place-items-center rounded-2xl", selected ? "bg-violet text-white" : "bg-slate-100 text-slate-600")}>
                  <Icon name={option.icon} className="h-6 w-6" />
                </span>
                <span className="block text-xl font-black text-slate-900">{option.title}</span>
                <span className="mt-2 block leading-7 text-slate-600">{option.description}</span>
              </button>
            );
          })}
        </div>

        <button className="mt-6 inline-flex w-full items-center justify-center gap-2 rounded-full bg-gradient-to-r from-violet to-ocean px-6 py-4 font-bold text-white shadow-glow" onClick={() => onConfirm(selectedGender)}>
          确认生成海报
          <Icon name="image-down" />
        </button>
      </section>
    </div>
  );
}

function HomePage({ onStart }: { onStart: () => void }) {
  return (
    <main>
      <section className="hero-art relative overflow-hidden">
        <div className="absolute inset-0 bg-gradient-to-r from-white via-white/86 to-white/18"></div>
        <div className="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-t from-cloud to-transparent"></div>
        <div className="absolute right-4 top-4 z-10 inline-flex items-center gap-1.5 rounded-full bg-white/86 px-3 py-2 text-xs font-bold text-slate-700 shadow-sm backdrop-blur sm:right-6 sm:top-6 sm:text-sm">
          建议在浏览器中打开
          <Icon name="arrow-up-right" className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
        </div>
        <div className="relative mx-auto flex min-h-[78vh] max-w-7xl flex-col justify-center px-4 py-14 sm:px-6 lg:py-20">
          <div className="max-w-2xl">
            <div className="mb-5 inline-flex items-center gap-2 rounded-full border border-violet/20 bg-white/82 px-4 py-2 text-sm font-medium text-violet shadow-sm">
              <Icon name="sparkles" />
              高考结束后的第一份生活副本
            </div>
            <h1 className="text-4xl font-black leading-tight text-ink sm:text-6xl">
              暑假计划生成器
              <span className="mt-2 block text-2xl font-black text-violet sm:text-4xl">2026高考生专属</span>
            </h1>
            <p className="mt-5 max-w-xl text-lg leading-8 text-slate-700">
              高考结束后，终于可以喘口气了。但这个暑假不只有睡觉、刷手机、打游戏。
              用 1 分钟测出你的暑假人格，生成一份属于你的假期计划。
            </p>
            <div className="mt-8 flex flex-col gap-3 sm:flex-row">
              <button
                className="inline-flex items-center justify-center gap-2 rounded-full bg-gradient-to-r from-violet to-ocean px-7 py-4 text-lg font-black text-white shadow-glow transition hover:-translate-y-0.5 sm:text-base"
                onClick={onStart}
              >
                开始定制我的暑假计划
                <Icon name="arrow-right" />
              </button>
              <a
                className="inline-flex items-center justify-center gap-2 rounded-full border border-white bg-white/82 px-6 py-4 font-semibold text-slate-700 shadow-sm"
                href="#preview"
              >
                先看看能生成什么
                <Icon name="chevron-down" />
              </a>
            </div>
          </div>
        </div>
      </section>
      <section id="preview" className="mx-auto grid max-w-7xl gap-4 px-4 pb-12 sm:grid-cols-3 sm:px-6">
        {[
          ["1分钟测试", "9 道题判断你的暑假人格和计划强度", "timer"],
          ["卡片式计划", "点击任务卡，安排到 6月10日-8月31日的暑假日历里", "layout-dashboard"],
          ["分享海报", "生成可截图、可复制文案的专属海报", "image-down"]
        ].map(([title, body, icon]) => (
          <div key={title} className="glass-panel rounded-2xl p-5 shadow-soft">
            <div className="mb-4 grid h-11 w-11 place-items-center rounded-xl bg-white text-violet shadow-sm">
              <Icon name={icon} className="h-5 w-5" />
            </div>
            <h2 className="text-lg font-bold">{title}</h2>
            <p className="mt-2 leading-7 text-slate-600">{body}</p>
          </div>
        ))}
      </section>
    </main>
  );
}

function QuestionnairePage({
  answers,
  setAnswers,
  currentQuestion,
  setCurrentQuestion,
  onSubmit,
  onBackHome
}: {
  answers: AnswerMap;
  setAnswers: (answers: AnswerMap | ((answers: AnswerMap) => AnswerMap)) => void;
  currentQuestion: number;
  setCurrentQuestion: (index: number) => void;
  onSubmit: () => void;
  onBackHome: () => void;
}) {
  const questions = config.questions;
  const question = questions[currentQuestion];
  const isMultiple = isMultipleQuestion(question);
  const maxSelect = getQuestionMaxSelect(question);
  const optionIds = question.options.map((option: any) => option.id);
  const rawAnswer = answers[question.id] || { selected: [] };
  const answer = {
    ...rawAnswer,
    selected: rawAnswer.selected.filter((id) => optionIds.includes(id)).slice(0, maxSelect || undefined)
  };
  const progress = Math.round(((currentQuestion + 1) / questions.length) * 100);
  const canGoNext = answer.selected.length > 0;

  function toggleOption(option: any) {
    setAnswers((current) => {
      const existing = current[question.id] || { selected: [] };
      let selected = existing.selected.filter((id) => optionIds.includes(id)).slice(0, maxSelect || undefined);

      if (!isMultiple) {
        selected = [option.id];
      } else if (selected.includes(option.id)) {
        selected = selected.filter((id) => id !== option.id);
      } else if (!maxSelect || selected.length < maxSelect) {
        selected.push(option.id);
      }

      return {
        ...current,
        [question.id]: {
          ...existing,
          selected
        }
      };
    });
  }

  function updateOtherText(value: string) {
    setAnswers((current) => ({
      ...current,
      [question.id]: {
        ...(current[question.id] || { selected: ["other"] }),
        selected: Array.from(new Set([...(current[question.id]?.selected || []).filter((id) => optionIds.includes(id)), "other"])),
        otherText: value
      }
    }));
  }

  function goNext() {
    if (!canGoNext) return;
    if (currentQuestion === questions.length - 1) {
      onSubmit();
      return;
    }
    setCurrentQuestion(currentQuestion + 1);
  }

  return (
    <main className="mx-auto max-w-5xl px-4 py-8 sm:px-6">
      <div className="mb-6 flex flex-wrap items-center justify-between gap-3">
        <button className="inline-flex items-center gap-2 rounded-full bg-white px-4 py-2 text-sm font-semibold text-slate-600 shadow-sm" onClick={onBackHome}>
          <Icon name="chevron-left" />
          返回首页
        </button>
        <span className="rounded-full bg-white px-4 py-2 text-sm font-semibold text-violet shadow-sm">
          第 {currentQuestion + 1} / {questions.length} 题
        </span>
      </div>

      <div className="mb-6 h-3 overflow-hidden rounded-full bg-white shadow-inner">
        <div className="h-full rounded-full bg-gradient-to-r from-violet to-ocean transition-all" style={{ width: `${progress}%` }}></div>
      </div>

      <section className="glass-panel rounded-3xl p-5 shadow-glow sm:p-8">
        <div className="mb-6">
          <p className="text-sm font-bold text-violet">第 {currentQuestion + 1} 题</p>
          <h1 className="mt-2 text-2xl font-black sm:text-3xl">{question.prompt || question.title}</h1>
          {isMultiple && maxSelect && (
            <p className="mt-3 inline-flex rounded-full bg-amber-50 px-3 py-1 text-sm font-semibold text-amber-700">
              最多选择 {maxSelect} 项
            </p>
          )}
        </div>

        <div className="grid gap-3 sm:grid-cols-2">
          {question.options.map((option: any) => {
            const selected = answer.selected.includes(option.id);
            const limitReached = isMultiple && maxSelect && answer.selected.length >= maxSelect && !selected;
            return (
              <button
                key={option.id}
                className={cx(
                  "min-h-20 rounded-2xl border p-4 text-left transition hover:-translate-y-0.5",
                  selected ? "bounce-in border-violet bg-violet text-white shadow-glow" : "border-white bg-white/82 text-slate-700 shadow-sm hover:border-violet/30",
                  limitReached && "opacity-45"
                )}
                onClick={() => toggleOption(option)}
                type="button"
              >
                <div className="flex items-start gap-3">
                  <span
                    className={cx(
                      "mt-0.5 grid h-6 w-6 shrink-0 place-items-center rounded-full border",
                      selected ? "border-white bg-white text-violet" : "border-slate-300 bg-slate-50"
                    )}
                  >
                    {selected && <Icon name="check" className="h-4 w-4" />}
                  </span>
                  <span>
                    <span className="block font-bold">{option.label}</span>
                    {option.detail && <span className={cx("mt-1 block text-sm leading-6", selected ? "text-white/84" : "text-slate-500")}>{option.detail}</span>}
                  </span>
                </div>
              </button>
            );
          })}
        </div>

        {answer.selected.includes("other") && (
          <label className="mt-4 block">
            <span className="mb-2 block text-sm font-semibold text-slate-600">写下你的自定义答案</span>
            <input
              className="w-full rounded-2xl border border-violet/20 bg-white px-4 py-3 outline-none ring-violet/20 focus:ring-4"
              placeholder="比如：想先去海边住几天，再开始学点东西"
              value={answer.otherText || ""}
              onChange={(event) => updateOtherText(event.target.value)}
            />
          </label>
        )}

        <div className="mt-8 flex flex-col-reverse gap-3 sm:flex-row sm:items-center sm:justify-between">
          <button
            className="inline-flex items-center justify-center gap-2 rounded-full bg-white px-5 py-3 font-semibold text-slate-600 shadow-sm disabled:opacity-40"
            disabled={currentQuestion === 0}
            onClick={() => setCurrentQuestion(Math.max(0, currentQuestion - 1))}
          >
            <Icon name="chevron-left" />
            上一题
          </button>
          <button
            className="inline-flex items-center justify-center gap-2 rounded-full bg-gradient-to-r from-violet to-ocean px-6 py-3 font-bold text-white shadow-glow disabled:cursor-not-allowed disabled:opacity-45"
            disabled={!canGoNext}
            onClick={goNext}
          >
            {currentQuestion === questions.length - 1 ? "生成我的暑假人格" : "下一题"}
            <Icon name={currentQuestion === questions.length - 1 ? "wand-sparkles" : "arrow-right"} />
          </button>
        </div>
      </section>
    </main>
  );
}

function GeneratingOverlay() {
  return (
    <div className="fixed inset-0 z-50 grid place-items-center bg-white/80 p-4 backdrop-blur-xl">
      <div className="glass-panel max-w-sm rounded-3xl p-7 text-center shadow-glow">
        <div className="mx-auto mb-5 grid h-16 w-16 place-items-center rounded-2xl bg-gradient-to-br from-violet to-ocean text-white pulse-card">
          <Icon name="sparkles" className="h-8 w-8" />
        </div>
        <h2 className="text-xl font-black">正在分析你的暑假状态……</h2>
        <p className="mt-3 leading-7 text-slate-600">正在生成你的专属计划，马上给你一份不焦虑也不散掉的暑假节奏。</p>
      </div>
    </div>
  );
}

function ResultPage({
  result,
  answers,
  planLength,
  onGoPlanner,
  onRestart,
  onPoster,
  planItems
}: {
  result: ResultState | null;
  answers: AnswerMap;
  planLength: number;
  onGoPlanner: () => void;
  onRestart: () => void;
  onPoster: () => void;
  planItems: PlanItem[];
}) {
  const [stageAdviceOpen, setStageAdviceOpen] = useState(false);

  if (!result) {
    return <EmptyState title="还没有生成结果" body="先完成 9 道题，就能看到你的暑假人格和推荐计划。" action="开始测试" onAction={onRestart} />;
  }

  const persona = findPersona(result.primaryId);
  const sidePersona = findPersona(result.secondaryId);
  const stages = getStageRanges(planLength);
  const template = config.planTemplates[result.primaryId] || config.planTemplates.explore;
  const keywords = getPlanKeywords(result, planItems);
  const gameMode = getSelectedOptionLabel(answers, "summer_game_mode");
  const avoidState = getAvoidStateSupport(answers);
  const resultSummary = `${persona.shortName}的关键词不是“逼自己变强”，而是找到适合自己的暑假打法。先把状态接住，再给大学生活留一点提前量。`;
  const pitfalls = Array.from(new Set([...persona.pitfalls, ...avoidState.pitfalls]));
  const recommendedTasks = Array.from(new Set([...persona.recommendedTasks, ...avoidState.tasks])).slice(0, 8);

  return (
    <main className="mx-auto max-w-7xl px-4 py-8 sm:px-6">
      <div className="glass-panel rounded-3xl p-5 shadow-soft sm:p-6">
        <h2 className="flex items-center gap-2 text-xl font-black">
          <Icon name="tags" className="h-5 w-5 text-violet" />
          你的暑假关键词
        </h2>
        <div className="mt-4 flex flex-wrap gap-2">
          {keywords.map((keyword) => (
            <span key={keyword} className="rounded-full bg-white px-4 py-2 text-sm font-bold text-slate-700 shadow-sm">
              {keyword}
            </span>
          ))}
        </div>
        <div className="mt-6 flex flex-col gap-3">
          <button className="inline-flex items-center justify-center gap-2 rounded-full bg-gradient-to-r from-violet to-ocean px-6 py-3 font-bold text-white shadow-glow" onClick={onGoPlanner}>
            进入计划编辑器
            <Icon name="calendar-days" />
          </button>
          <button className="inline-flex items-center justify-center gap-2 rounded-full bg-white px-6 py-3 font-semibold text-slate-700 shadow-sm" onClick={onRestart}>
            重新测试
            <Icon name="rotate-ccw" />
          </button>
          <button className="inline-flex items-center justify-center gap-2 rounded-full bg-gradient-to-r from-coral via-violet to-ocean px-6 py-4 text-lg font-black text-white shadow-glow ring-2 ring-white/80 transition hover:-translate-y-0.5 sm:text-xl" onClick={onPoster}>
            直接生成分享海报
            <Icon name="image-down" />
          </button>
        </div>
      </div>

      <section className="mt-6 grid gap-5">
        <div className="glass-panel overflow-hidden rounded-3xl shadow-glow">
          <div className={cx("p-6 text-white sm:p-8", "bg-gradient-to-br", persona.accent)}>
            <div className="mb-5 flex flex-wrap items-center gap-2">
              <span className="rounded-full bg-white/22 px-3 py-1 text-sm font-bold">测试结果揭晓</span>
              <span className="rounded-full bg-white/22 px-3 py-1 text-sm font-bold">{result.intensity.label}</span>
            </div>
            <p className="text-sm font-bold text-white/78">你的暑假人格</p>
            <h1 className="mt-2 text-4xl font-black sm:text-5xl">{persona.name}</h1>
            <p className="mt-5 max-w-2xl text-lg leading-8 text-white/88">{persona.description}</p>
            <p className="mt-4 max-w-2xl leading-7 text-white/82">{resultSummary}</p>
          </div>
          <div className="grid gap-5 p-5 sm:grid-cols-2 sm:p-7 lg:grid-cols-3">
            <InfoPanel title="隐藏副人格" icon="sparkle">
              <p className="text-2xl font-black text-violet">{sidePersona.name}</p>
              <p className="mt-2 leading-7 text-slate-600">{sidePersona.description}</p>
            </InfoPanel>
            <InfoPanel title="你的暑假玩法" icon="gamepad-2">
              <p className="text-2xl font-black text-violet">{compactModeLabel(gameMode)}</p>
              <p className="mt-2 leading-7 text-slate-600">{gameMode || "按自己的节奏探索，也给每天留一点点锚点。"}</p>
            </InfoPanel>
            <InfoPanel title="计划强度" icon="gauge">
              <p className="text-2xl font-black text-violet">{result.intensity.label}</p>
              <p className="mt-2 leading-7 text-slate-600">{result.intensity.description}</p>
            </InfoPanel>
          </div>
        </div>
      </section>

      <section className="mt-6 grid gap-5 lg:grid-cols-3">
        <ResultListCard title="适合做的事" items={persona.suited} icon="check-circle" />
        <ResultListCard title="最需要避开的坑" items={pitfalls} icon="shield-alert" />
        <ResultListCard title="推荐任务模块" items={recommendedTasks} icon="clipboard-list" />
      </section>

      <section className="mt-6 glass-panel rounded-3xl p-5 shadow-soft sm:p-6">
        <div className="flex items-center justify-between gap-3 sm:hidden">
          <h2 className="text-xl font-black">四阶段计划建议</h2>
          <button
            className="grid h-10 w-10 shrink-0 place-items-center rounded-full bg-white text-violet shadow-sm"
            onClick={() => setStageAdviceOpen((open) => !open)}
            aria-expanded={stageAdviceOpen}
            aria-controls="stage-advice-content"
            aria-label={stageAdviceOpen ? "收起四阶段计划建议" : "展开四阶段计划建议"}
          >
            <Icon name={stageAdviceOpen ? "minus" : "plus"} className="h-5 w-5" />
          </button>
        </div>
        <div id="stage-advice-content" className={cx(stageAdviceOpen ? "mt-4 block" : "hidden", "sm:block")}>
          <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
            <div>
              <h2 className="hidden text-2xl font-black sm:block">四阶段计划建议</h2>
              <p className="mt-0 text-sm leading-6 text-slate-600 sm:mt-2 sm:text-base sm:leading-7">
                计划日期固定为 {formatPlanDate(1)} 到 {formatPlanDate(planLength)}，覆盖高考后的完整暑假。
              </p>
            </div>
            <span className="w-fit rounded-full bg-white px-4 py-2 text-sm font-black text-violet shadow-sm">共 {planLength} 天</span>
          </div>

          <div className="mt-5 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
            {stages.map((stage: any, index: number) => (
              <div key={stage.id} className="rounded-2xl bg-white p-4 shadow-sm">
                <div className="mb-4 flex items-center justify-between gap-3">
                  <span className="grid h-9 w-9 place-items-center rounded-xl bg-violet/10 font-black text-violet">{index + 1}</span>
                  <span className="rounded-full bg-slate-100 px-3 py-1 text-xs font-bold text-slate-500">
                    {formatPlanDateRange(stage.start, stage.end)}
                  </span>
                </div>
                <h3 className="text-lg font-black">{stage.name}</h3>
                <p className="mt-2 min-h-20 leading-7 text-slate-600">{stage.goal}</p>
                <div className="mt-3 flex flex-wrap gap-2">
                  {stage.modules.slice(0, 3).map((moduleName: string) => (
                    <span key={moduleName} className="rounded-full bg-slate-50 px-3 py-1 text-xs font-semibold text-slate-600">
                      {moduleName}
                    </span>
                  ))}
                </div>
              </div>
            ))}
          </div>
          <div className="mt-5 flex flex-wrap gap-2">
            {template.focus.map((item: string) => (
              <span key={item} className="rounded-full bg-indigo-50 px-4 py-2 text-sm font-bold text-indigo-700">
                {item}
              </span>
            ))}
          </div>
        </div>
      </section>

      <LeadMagnetSection />
    </main>
  );
}

function InfoPanel({ title, icon, children }: { title: string; icon: string; children: any }) {
  return (
    <div className="rounded-2xl bg-white p-4 shadow-sm">
      <h2 className="mb-3 flex items-center gap-2 text-sm font-bold text-slate-500">
        <Icon name={icon} className="h-4 w-4 text-violet" />
        {title}
      </h2>
      {children}
    </div>
  );
}

function ResultListCard({ title, items, icon }: { title: string; items: string[]; icon: string }) {
  return (
    <section className="glass-panel rounded-3xl p-5 shadow-soft">
      <h2 className="mb-4 flex items-center gap-2 text-lg font-black">
        <Icon name={icon} className="h-5 w-5 text-violet" />
        {title}
      </h2>
      <ul className="space-y-3">
        {items.map((item) => (
          <li key={item} className="flex gap-3 leading-7 text-slate-600">
            <span className="mt-2 h-2 w-2 shrink-0 rounded-full bg-violet"></span>
            <span>{item}</span>
          </li>
        ))}
      </ul>
    </section>
  );
}

function PlannerPage({
  result,
  planLength,
  planItems,
  setPlanItems,
  onRestore,
  onPoster,
  onResult
}: {
  result: ResultState | null;
  planLength: number;
  planItems: PlanItem[];
  setPlanItems: (items: PlanItem[] | ((items: PlanItem[]) => PlanItem[])) => void;
  onRestore: () => void;
  onPoster: () => void;
  onResult: () => void;
}) {
  const [activeCategory, setActiveCategory] = useState("relax");
  const [draft, setDraft] = useState<any>(null);
  const [manualSelection, setManualSelection] = useState({
    active: false,
    isSelecting: false,
    startDay: 0,
    endDay: 0
  });
  const manualSelectionRef = useRef(manualSelection);
  const draftPanelRef = useRef<HTMLDivElement | null>(null);
  const [customTask, setCustomTask] = useState({
    name: "",
    duration: 3,
    daily: "30分钟",
    difficulty: "轻松",
    outcome: "完成一个自己定下的小目标"
  });
  const persona = result ? findPersona(result.primaryId) : null;
  const progress = planItems.length ? Math.round((planItems.filter((item) => item.completed).length / planItems.length) * 100) : 0;
  const keywords = getPlanKeywords(result, planItems);
  const visibleTasks = config.tasks.filter((task: any) => task.category === activeCategory);

  useEffect(() => {
    manualSelectionRef.current = manualSelection;
  }, [manualSelection]);

  function openDraft(task: DraftTask) {
    setDraft({
      task,
      name: task.name,
      startDay: 1,
      duration: clampNumber(Number(task.duration) || 1, 1, planLength)
    });
    setManualSelection({ active: false, isSelecting: false, startDay: 0, endDay: 0 });
    window.setTimeout(() => {
      draftPanelRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
    }, 0);
  }

  function addDraftTask(range?: { startDay: number; endDay: number }, keepManualMode = false) {
    if (!draft) return;
    const task = draft.task;
    const startDay = range ? clampNumber(range.startDay, 1, planLength) : clampNumber(Number(draft.startDay), 1, planLength);
    const endDay = range ? clampNumber(range.endDay, startDay, planLength) : startDay + clampNumber(Number(draft.duration), 1, planLength - startDay + 1) - 1;
    const duration = clampNumber(endDay - startDay + 1, 1, planLength - startDay + 1);
    setPlanItems((items) => {
      const existingIndex = items.findIndex((item) => {
        if (item.taskId !== task.id) return false;
        if (item.startDay === startDay && item.duration === duration) return true;
        return duration === 1 && startDay >= item.startDay && startDay < item.startDay + item.duration;
      });

      if (existingIndex >= 0) {
        return items.filter((_, index) => index !== existingIndex);
      }

      return [
        ...items,
        {
          id: `plan-${Date.now()}-${Math.random().toString(16).slice(2)}`,
          taskId: task.id,
          name: draft.name.trim() || task.name,
          category: task.category,
          startDay,
          duration,
          completed: false,
          daily: task.daily,
          difficulty: task.difficulty,
          outcome: task.outcome
        }
      ];
    });
    if (keepManualMode) {
      const nextSelection = { active: true, isSelecting: false, startDay: 0, endDay: 0 };
      manualSelectionRef.current = nextSelection;
      setManualSelection(nextSelection);
      return;
    }
    setDraft(null);
    cancelManualSelection();
  }

  function beginManualSelection() {
    if (!draft) return;
    const nextSelection = { active: true, isSelecting: false, startDay: 0, endDay: 0 };
    manualSelectionRef.current = nextSelection;
    setManualSelection(nextSelection);
  }

  function cancelManualSelection() {
    const nextSelection = { active: false, isSelecting: false, startDay: 0, endDay: 0 };
    manualSelectionRef.current = nextSelection;
    setManualSelection(nextSelection);
  }

  function startCalendarSelection(day: number) {
    if (!manualSelection.active || !draft) return;
    const nextSelection = { active: true, isSelecting: true, startDay: day, endDay: day };
    manualSelectionRef.current = nextSelection;
    setManualSelection(nextSelection);
  }

  function moveCalendarSelection(day: number) {
    setManualSelection((current) => {
      const nextSelection = current.active && current.isSelecting ? { ...current, endDay: day } : current;
      manualSelectionRef.current = nextSelection;
      return nextSelection;
    });
  }

  function finishCalendarSelection(day: number) {
    const currentSelection = manualSelectionRef.current;
    if (!currentSelection.active || !draft) return;
    const startDay = currentSelection.startDay || day;
    const endDay = day || currentSelection.endDay || startDay;
    addDraftTask({
      startDay: Math.min(startDay, endDay),
      endDay: Math.max(startDay, endDay)
    }, true);
  }

  function addCustomTask() {
    if (!customTask.name.trim()) return;
    openDraft({
      id: `custom-${Date.now()}`,
      name: customTask.name.trim(),
      category: "custom",
      duration: Number(customTask.duration) || 1,
      daily: customTask.daily || "自由安排",
      difficulty: customTask.difficulty || "轻松",
      audience: "自己",
      outcome: customTask.outcome || "完成一个自己定下的小目标"
    });
  }

  function updatePlanItem(id: string, patch: Partial<PlanItem>) {
    setPlanItems((items) =>
      items.map((item) => {
        if (item.id !== id) return item;
        const merged = { ...item, ...patch };
        merged.startDay = clampNumber(Number(merged.startDay), 1, planLength);
        merged.duration = clampNumber(Number(merged.duration), 1, planLength - merged.startDay + 1);
        return merged;
      })
    );
  }

  function updateEndDay(id: string, endDay: number) {
    setPlanItems((items) =>
      items.map((item) => {
        if (item.id !== id) return item;
        const safeEnd = clampNumber(endDay, item.startDay, planLength);
        return { ...item, duration: safeEnd - item.startDay + 1 };
      })
    );
  }

  if (!result || !persona) {
    return <EmptyState title="还没有计划" body="先完成测试，系统会帮你生成推荐任务。" action="开始测试" onAction={onResult} />;
  }

  return (
    <main className="mx-auto w-full max-w-7xl overflow-x-hidden px-4 py-8 sm:px-6">
      <section className="mb-6 grid w-full max-w-full gap-4">
        <div className="rounded-3xl bg-gradient-to-r from-violet to-ocean p-5 text-white shadow-glow sm:p-6">
          <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
            <div>
              <p className="text-sm font-bold text-white/75">不想排任务也可以</p>
              <h2 className="mt-1 text-2xl font-black sm:text-3xl">跳过计划直接生成海报</h2>
              <p className="mt-2 max-w-3xl leading-7 text-white/86">
                先用当前测试结果生成专属暑假人格海报，日历计划之后想补再回来慢慢排。
              </p>
            </div>
            <button className="inline-flex w-full items-center justify-center gap-2 rounded-full bg-white px-6 py-3 font-black text-violet shadow-sm sm:w-auto" onClick={onPoster}>
              跳过计划直接生成海报
              <Icon name="image-down" />
            </button>
          </div>
        </div>

        <div className="glass-panel rounded-3xl p-5 shadow-soft sm:p-6">
          <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
            <div>
              <p className="text-sm font-bold text-violet">计划编辑器</p>
              <h1 className="mt-1 text-3xl font-black">亲手排出你的暑假日历</h1>
              <p className="mt-2 leading-7 text-slate-600">
                当前人格：{persona.name}。计划从 {formatPlanDate(1)} 开始，到 {formatPlanDate(planLength)} 结束；点击任务卡，选择具体日期和持续天数。
              </p>
            </div>
            <span className="rounded-full bg-white px-4 py-2 text-sm font-black text-violet shadow-sm">
              {formatPlanDateRange(1, planLength)} · 共 {planLength} 天
            </span>
          </div>
          <div className="mt-5 grid gap-4 lg:grid-cols-[1fr_1.2fr]">
            <div>
              <div className="mb-2 flex items-center justify-between">
                <span className="font-bold text-slate-700">完成度</span>
                <span className="font-black text-violet">{progress}%</span>
              </div>
              <div className="h-3 overflow-hidden rounded-full bg-white shadow-inner">
                <div className="h-full rounded-full bg-gradient-to-r from-mint via-ocean to-violet transition-all" style={{ width: `${progress}%` }}></div>
              </div>
            </div>
            <div className="flex flex-wrap items-center gap-2">
              {keywords.map((keyword) => (
                <span key={keyword} className="rounded-full bg-white px-3 py-1.5 text-sm font-bold text-slate-600 shadow-sm">
                  {keyword}
                </span>
              ))}
            </div>
          </div>
        </div>
      </section>

      <section className="grid w-full max-w-full gap-5 lg:grid-cols-[390px_minmax(0,1fr)]">
        <aside className="min-w-0 space-y-5">
          <div className="glass-panel w-full max-w-full overflow-hidden rounded-3xl p-4 shadow-soft">
            <div className="flex w-full max-w-full gap-2 overflow-x-auto pb-2 thin-scrollbar lg:flex-wrap">
              {config.taskCategories.map((category: any) => (
                <button
                  key={category.id}
                  className={cx(
                    "shrink-0 rounded-full px-4 py-2 text-sm font-bold transition",
                    activeCategory === category.id ? "bg-violet text-white shadow-sm" : "bg-white text-slate-600 hover:bg-slate-50"
                  )}
                  onClick={() => setActiveCategory(category.id)}
                >
                  {category.name}
                </button>
              ))}
            </div>

            {activeCategory === "custom" ? (
              <CustomTaskForm customTask={customTask} setCustomTask={setCustomTask} onAdd={addCustomTask} />
            ) : (
              <div className="mt-4 grid w-full max-w-full grid-cols-1 gap-3 lg:max-h-[680px] lg:overflow-y-auto">
                {visibleTasks.map((task: DraftTask) => (
                  <TaskCard key={task.id} task={task} onAdd={() => openDraft(task)} />
                ))}
              </div>
            )}
          </div>

          {draft && (
            <div ref={draftPanelRef} className="glass-panel w-full max-w-full scroll-mt-24 overflow-hidden rounded-3xl p-5 shadow-glow">
              <div className="mb-4 flex items-start justify-between gap-3">
                <div>
                  <p className="text-sm font-bold text-violet">安排任务</p>
                  <h2 className="mt-1 text-xl font-black">{draft.task.name}</h2>
                </div>
                <button
                  className="rounded-full bg-white p-2 text-slate-500 shadow-sm"
                  onClick={() => {
                    setDraft(null);
                    cancelManualSelection();
                  }}
                  title="关闭"
                >
                  <Icon name="x" />
                </button>
              </div>
              <div className="space-y-3">
                <PlannerInput label="任务名称">
                  <input className="w-full rounded-xl border border-slate-200 px-3 py-2" value={draft.name} onChange={(event) => setDraft({ ...draft, name: event.target.value })} />
                </PlannerInput>
                <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
                  <PlannerInput label="开始日期">
                    <input
                      className="w-full rounded-xl border border-slate-200 px-3 py-2"
                      type="date"
                      min={PLAN_START_INPUT}
                      max={PLAN_END_INPUT}
                      value={toDateInputValue(draft.startDay)}
                      onChange={(event) => setDraft({ ...draft, startDay: fromDateInputValue(event.target.value) })}
                    />
                  </PlannerInput>
                  <PlannerInput label="持续天数">
                    <input
                      className="w-full rounded-xl border border-slate-200 px-3 py-2"
                      type="number"
                      min="1"
                      max={planLength}
                      value={draft.duration}
                      onChange={(event) => setDraft({ ...draft, duration: Number(event.target.value) })}
                    />
                  </PlannerInput>
                </div>
                <div className="grid gap-3 sm:grid-cols-2">
                  <button className="inline-flex items-center justify-center gap-2 rounded-full bg-gradient-to-r from-violet to-ocean px-5 py-3 font-bold text-white shadow-glow" onClick={() => addDraftTask()}>
                    添加到日历
                    <Icon name="plus" />
                  </button>
                  <button
                    className={cx(
                      "inline-flex items-center justify-center gap-2 rounded-full px-5 py-3 font-bold shadow-sm transition",
                      manualSelection.active ? "bg-slate-900 text-white" : "bg-white text-violet"
                    )}
                    onClick={manualSelection.active ? cancelManualSelection : beginManualSelection}
                  >
                    {manualSelection.active ? "取消点选" : "手动点选"}
                    <Icon name={manualSelection.active ? "x" : "mouse-pointer-click"} />
                  </button>
                </div>
                {manualSelection.active && (
                  <p className="rounded-2xl bg-indigo-50 px-4 py-3 text-sm font-semibold leading-6 text-indigo-700">
                    在右侧日历点击一天可安排单日任务；再次点同一天会取消这次安排。按住并拖动可选择连续日期，添加后会保留当前任务，可以继续点选。
                    {manualSelection.startDay > 0 && (
                      <span className="mt-1 block text-indigo-900">
                        当前选择：{formatPlanDateRange(Math.min(manualSelection.startDay, manualSelection.endDay), Math.max(manualSelection.startDay, manualSelection.endDay))}
                      </span>
                    )}
                  </p>
                )}
              </div>
            </div>
          )}
        </aside>

        <div className="min-w-0 space-y-5">
          <div className="glass-panel w-full max-w-full overflow-hidden rounded-3xl p-4 shadow-soft sm:p-5">
            <div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
              <h2 className="flex items-center gap-2 text-xl font-black">
                <Icon name="calendar-days" className="h-5 w-5 text-violet" />
                日历计划区
              </h2>
              <div className="flex flex-wrap gap-2">
                <button className="inline-flex items-center gap-2 rounded-full bg-white px-4 py-2 text-sm font-bold text-slate-600 shadow-sm" onClick={() => setPlanItems([])}>
                  <Icon name="trash-2" />
                  清空计划
                </button>
                <button className="inline-flex items-center gap-2 rounded-full bg-white px-4 py-2 text-sm font-bold text-slate-600 shadow-sm" onClick={onRestore}>
                  <Icon name="refresh-cw" />
                  恢复推荐
                </button>
                <button className="inline-flex items-center gap-2 rounded-full bg-gradient-to-r from-coral to-sunny px-4 py-2 text-sm font-bold text-white shadow-sm" onClick={onPoster}>
                  <Icon name="image-down" />
                  生成海报
                </button>
              </div>
            </div>
            <CalendarGrid
              planLength={planLength}
              planItems={planItems}
              updatePlanItem={updatePlanItem}
              manualSelection={manualSelection}
              onSelectionStart={startCalendarSelection}
              onSelectionMove={moveCalendarSelection}
              onSelectionEnd={finishCalendarSelection}
            />
          </div>

          <div className="glass-panel w-full max-w-full overflow-hidden rounded-3xl p-4 shadow-soft sm:p-5">
            <h2 className="mb-4 flex items-center gap-2 text-xl font-black">
              <Icon name="sliders-horizontal" className="h-5 w-5 text-violet" />
              已安排任务
            </h2>
            {planItems.length === 0 ? (
              <p className="rounded-2xl bg-white p-5 text-slate-500">还没有任务。先从任务卡片库里挑一个放进日历。</p>
            ) : (
              <div className="space-y-3">
                {planItems.map((item) => (
                  <PlanItemEditor
                    key={item.id}
                    item={item}
                    planLength={planLength}
                    updatePlanItem={updatePlanItem}
                    updateEndDay={updateEndDay}
                    removeItem={() => setPlanItems((items) => items.filter((existing) => existing.id !== item.id))}
                  />
                ))}
              </div>
            )}
          </div>
        </div>
      </section>
    </main>
  );
}

function TaskCard({ task, onAdd }: { task: DraftTask; onAdd: () => void }) {
  const taskMeta = `推荐 ${task.duration} 天 · 每天 ${task.daily} · 难度 ${task.difficulty} · ${task.audience}`;
  return (
    <article className="w-full max-w-full min-w-0 rounded-2xl border border-white bg-white p-3 shadow-sm transition hover:-translate-y-0.5 hover:shadow-soft sm:p-4">
      <div className="mb-2 flex items-start justify-between gap-3">
        <div className="min-w-0">
          <h3 className="truncate text-base font-black sm:text-lg" title={task.name}>{task.name}</h3>
        </div>
        <span className={cx("h-7 w-1.5 shrink-0 rounded-full", categoryBarClass(task.category))}></span>
      </div>
      <p className="min-w-0 truncate rounded-xl bg-slate-50 px-3 py-1.5 text-xs font-semibold text-slate-500" title={taskMeta}>
        {taskMeta}
      </p>
      <p className="mt-2 min-w-0 truncate text-sm leading-6 text-slate-600" title={task.outcome}>{task.outcome}</p>
      <button className="mt-3 inline-flex w-full items-center justify-center gap-2 rounded-full bg-slate-900 px-4 py-2 text-sm font-bold text-white" onClick={onAdd}>
        安排这个任务
        <Icon name="plus" />
      </button>
    </article>
  );
}

function CustomTaskForm({ customTask, setCustomTask, onAdd }: { customTask: any; setCustomTask: (value: any) => void; onAdd: () => void }) {
  return (
    <div className="mt-4 w-full max-w-full min-w-0 rounded-2xl bg-white p-4 shadow-sm">
      <h2 className="mb-4 text-lg font-black">添加自定义任务</h2>
      <div className="space-y-3">
        <PlannerInput label="任务名称">
          <input className="w-full rounded-xl border border-slate-200 px-3 py-2" value={customTask.name} onChange={(event) => setCustomTask({ ...customTask, name: event.target.value })} placeholder="比如：和朋友拍毕业照" />
        </PlannerInput>
        <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
          <PlannerInput label="推荐天数">
            <input className="w-full rounded-xl border border-slate-200 px-3 py-2" type="number" min="1" value={customTask.duration} onChange={(event) => setCustomTask({ ...customTask, duration: Number(event.target.value) })} />
          </PlannerInput>
          <PlannerInput label="每天建议">
            <input className="w-full rounded-xl border border-slate-200 px-3 py-2" value={customTask.daily} onChange={(event) => setCustomTask({ ...customTask, daily: event.target.value })} />
          </PlannerInput>
        </div>
        <PlannerInput label="难度">
          <select className="w-full rounded-xl border border-slate-200 px-3 py-2" value={customTask.difficulty} onChange={(event) => setCustomTask({ ...customTask, difficulty: event.target.value })}>
            <option>轻松</option>
            <option>中等</option>
            <option>进阶</option>
          </select>
        </PlannerInput>
        <PlannerInput label="完成成果">
          <textarea className="min-h-20 w-full rounded-xl border border-slate-200 px-3 py-2" value={customTask.outcome} onChange={(event) => setCustomTask({ ...customTask, outcome: event.target.value })} />
        </PlannerInput>
        <button className="inline-flex w-full items-center justify-center gap-2 rounded-full bg-gradient-to-r from-violet to-ocean px-5 py-3 font-bold text-white shadow-glow" onClick={onAdd}>
          创建并安排
          <Icon name="plus" />
        </button>
      </div>
    </div>
  );
}

function PlannerInput({ label, children }: { label: string; children: any }) {
  return (
    <label className="block">
      <span className="mb-1.5 block text-sm font-bold text-slate-600">{label}</span>
      {children}
    </label>
  );
}

function CalendarGrid({
  planLength,
  planItems,
  updatePlanItem,
  manualSelection,
  onSelectionStart,
  onSelectionMove,
  onSelectionEnd
}: {
  planLength: number;
  planItems: PlanItem[];
  updatePlanItem: (id: string, patch: Partial<PlanItem>) => void;
  manualSelection?: { active: boolean; isSelecting: boolean; startDay: number; endDay: number };
  onSelectionStart?: (day: number) => void;
  onSelectionMove?: (day: number) => void;
  onSelectionEnd?: (day: number) => void;
}) {
  const days = Array.from({ length: planLength }, (_, index) => index + 1);
  const selectedStart = manualSelection?.startDay ? Math.min(manualSelection.startDay, manualSelection.endDay) : 0;
  const selectedEnd = manualSelection?.startDay ? Math.max(manualSelection.startDay, manualSelection.endDay) : 0;
  return (
    <div className={cx("grid w-full max-w-full min-w-0 grid-cols-2 gap-2 sm:grid-cols-3 sm:gap-3 xl:grid-cols-6", manualSelection?.active && "select-none")}>
      {days.map((day) => {
        const items = planItems.filter((item) => day >= item.startDay && day < item.startDay + item.duration);
        const events = getPlanDateEvents(day);
        const selected = manualSelection?.active && day >= selectedStart && day <= selectedEnd;
        return (
          <article
            key={day}
            className={cx(
              "min-w-0 overflow-hidden rounded-2xl border bg-white/82 p-2 shadow-sm transition sm:min-h-36 sm:p-3",
              manualSelection?.active ? "cursor-crosshair border-violet/35 hover:border-violet hover:bg-indigo-50/80" : "border-white",
              selected && "border-violet bg-indigo-100/90 ring-2 ring-violet/30"
            )}
            onPointerDown={(event) => {
              if (!manualSelection?.active) return;
              event.preventDefault();
              onSelectionStart?.(day);
            }}
            onPointerEnter={() => {
              if (!manualSelection?.active) return;
              onSelectionMove?.(day);
            }}
            onPointerUp={(event) => {
              if (!manualSelection?.active) return;
              event.preventDefault();
              onSelectionEnd?.(day);
            }}
          >
            <div className="mb-2 flex min-w-0 items-center justify-between gap-1">
              <div className="min-w-0">
                <span className="block truncate text-xs font-black text-slate-700 sm:text-sm">{formatPlanDate(day)}</span>
                <span className="block text-xs font-bold text-slate-400">{formatPlanDateWithWeekday(day).split(" ")[1]}</span>
              </div>
              {items.length > 0 && <span className="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-bold text-slate-500">{items.length}</span>}
            </div>
            {events.length > 0 && (
              <div className="mb-2 flex flex-wrap gap-1.5">
                {events.map((event) => (
                  <span key={event.label} className="min-w-0 rounded-full border border-amber-200 bg-amber-50 px-1.5 py-1 text-[10px] font-black leading-none text-amber-700 sm:px-2 sm:text-[11px]">
                    {event.label}
                  </span>
                ))}
              </div>
            )}
            <div className="space-y-1.5">
              {items.map((item) => (
                <button
                  key={item.id}
                  className={cx(
                    "task-token w-full min-w-0 truncate rounded-xl border px-2 py-1.5 text-left text-[10px] font-bold leading-5 transition sm:text-xs",
                    categoryTokenClass(item.category),
                    item.completed && "opacity-55 line-through",
                    manualSelection?.active && "pointer-events-none"
                  )}
                  title="点击标记完成 / 未完成"
                  onClick={() => {
                    if (manualSelection?.active) return;
                    updatePlanItem(item.id, { completed: !item.completed });
                  }}
                >
                  {item.name}
                </button>
              ))}
            </div>
          </article>
        );
      })}
    </div>
  );
}

function PlanItemEditor({
  item,
  planLength,
  updatePlanItem,
  updateEndDay,
  removeItem
}: {
  item: PlanItem;
  planLength: number;
  updatePlanItem: (id: string, patch: Partial<PlanItem>) => void;
  updateEndDay: (id: string, endDay: number) => void;
  removeItem: () => void;
}) {
  const endDay = item.startDay + item.duration - 1;
  return (
    <article className="w-full max-w-full min-w-0 overflow-hidden rounded-2xl bg-white p-4 shadow-sm">
      <div className="flex min-w-0 flex-col gap-3 lg:flex-row lg:items-center">
        <button
          className={cx(
            "inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-xl border",
            item.completed ? "border-violet bg-violet text-white" : "border-slate-200 bg-slate-50 text-slate-500"
          )}
          onClick={() => updatePlanItem(item.id, { completed: !item.completed })}
          title="标记完成"
        >
          <Icon name={item.completed ? "check-square" : "square"} />
        </button>
        <input
          className={cx("w-full min-w-0 flex-1 rounded-xl border border-slate-200 px-3 py-2 font-bold", item.completed && "text-slate-400 line-through")}
          value={item.name}
          onChange={(event) => updatePlanItem(item.id, { name: event.target.value })}
        />
        <div className="grid w-full min-w-0 grid-cols-1 gap-2 sm:grid-cols-2 lg:w-[560px] lg:grid-cols-4">
          <input
            className="w-full min-w-0 rounded-xl border border-slate-200 px-3 py-2 text-sm"
            type="date"
            min={PLAN_START_INPUT}
            max={PLAN_END_INPUT}
            value={toDateInputValue(item.startDay)}
            onChange={(event) => updatePlanItem(item.id, { startDay: fromDateInputValue(event.target.value) })}
            title="开始日期"
          />
          <input
            className="w-full min-w-0 rounded-xl border border-slate-200 px-3 py-2 text-sm"
            type="date"
            min={toDateInputValue(item.startDay)}
            max={PLAN_END_INPUT}
            value={toDateInputValue(endDay)}
            onChange={(event) => updateEndDay(item.id, fromDateInputValue(event.target.value))}
            title="结束日期"
          />
          <span className={cx("min-w-0 rounded-xl border px-3 py-2 text-center text-sm font-bold", categoryTokenClass(item.category))}>{findCategory(item.category).name}</span>
          <button className="inline-flex items-center justify-center gap-1 rounded-xl bg-slate-100 px-3 py-2 text-sm font-bold text-slate-600" onClick={removeItem} title="删除任务">
            <Icon name="trash-2" />
            删除
          </button>
        </div>
      </div>
      <p className="mt-3 break-words text-sm leading-6 text-slate-500">每天建议：{item.daily} · 难度：{item.difficulty} · 成果：{item.outcome}</p>
    </article>
  );
}

function PosterPage({
  result,
  answers,
  planItems,
  planLength,
  posterGender,
  onPlanner,
  onRestart
}: {
  result: ResultState | null;
  answers: AnswerMap;
  planItems: PlanItem[];
  planLength: number;
  posterGender: PosterGender;
  onPlanner: () => void;
  onRestart: () => void;
}) {
  const posterRef = useRef<any>(null);
  const [downloading, setDownloading] = useState(false);
  const [copied, setCopied] = useState(false);

  if (!result) {
    return <EmptyState title="还没有海报内容" body="先完成测试并生成计划，再来制作分享海报。" action="开始测试" onAction={onRestart} />;
  }

  const persona = findPersona(result.primaryId);
  const sidePersona = findPersona(result.secondaryId);
  const gameMode = getSelectedOptionLabel(answers, "summer_game_mode");
  const planRows = getCalendarPlanRows(planItems, planLength);
  const posterImageSet = personaPosterImages[persona.name] || personaPosterImages["自由松弛型"];
  const posterImage = posterImageSet?.[posterGender] || posterImageSet?.default || "";
  const posterGenderLabel = POSTER_GENDER_OPTIONS.find((option) => option.value === posterGender)?.title || "男生版";

  async function downloadPoster() {
    if (!posterRef.current) return;
    setDownloading(true);
    try {
      const canvas = await (window as any).html2canvas(posterRef.current, {
        backgroundColor: null,
        scale: 2,
        useCORS: true
      });
      const link = document.createElement("a");
      link.download = "我的高考后暑假计划.png";
      link.href = canvas.toDataURL("image/png");
      link.click();
    } finally {
      setDownloading(false);
    }
  }

  async function copyShareText() {
    const text = getShareText(result, planItems);
    try {
      if (navigator.clipboard?.writeText) {
        await navigator.clipboard.writeText(text);
      } else {
        const textarea = document.createElement("textarea");
        textarea.value = text;
        textarea.style.position = "fixed";
        textarea.style.left = "-9999px";
        document.body.appendChild(textarea);
        textarea.select();
        document.execCommand("copy");
        document.body.removeChild(textarea);
      }
      setCopied(true);
      window.setTimeout(() => setCopied(false), 1800);
    } catch (error) {
      window.alert(text);
    }
  }

  return (
    <main className="w-full max-w-full overflow-x-hidden px-4 py-8 sm:px-6">
      <section className="mx-auto grid w-full max-w-7xl min-w-0 gap-6 lg:grid-cols-[minmax(0,480px)_minmax(0,1fr)]">
        <div className="flex w-full max-w-full min-w-0 justify-center">
          <article ref={posterRef} className="poster-card mx-auto w-full max-w-[420px] min-w-0 overflow-hidden rounded-[28px] border border-white p-3 shadow-glow sm:max-w-[430px] sm:p-5 lg:p-6">
            <div className="w-full max-w-full overflow-hidden rounded-3xl bg-white/82 p-1.5 shadow-sm sm:p-2">
              <img className="block h-auto w-full rounded-[20px] object-contain" src={posterImage} alt={`${persona.name}${posterGenderLabel}海报主视觉`} />
            </div>

            <div className="mt-4 rounded-2xl bg-gradient-to-br from-violet to-ocean p-4 text-white">
              <p className="text-xs font-bold text-white/78">我的高考后暑假计划</p>
              <h1 className="mt-2 break-words text-xl font-black leading-tight">我是「{persona.name}」准大学生</h1>
              <div className="mt-3 grid gap-1.5 text-sm font-semibold leading-6 text-white/86">
                <p>隐藏副人格：{sidePersona.name}</p>
                <p>暑假玩法：{compactModeLabel(gameMode)}</p>
              </div>
            </div>

          </article>
        </div>

        <div className="min-w-0 space-y-5">
          <div className="glass-panel w-full max-w-full min-w-0 overflow-hidden rounded-3xl p-5 shadow-soft sm:p-6">
            <p className="text-sm font-bold text-violet">分享海报页</p>
            <h1 className="mt-2 break-words text-2xl font-black sm:text-3xl">分享给你的好友，看看他/她是什么暑假人格</h1>
            <p className="mt-3 break-words leading-7 text-slate-600">使用外部浏览器可直接下载海报，微信浏览器可点右上角“···”转发给朋友、群或分享到朋友圈，还可加入2026考生专属交流群，领取诸多福利！</p>
            <div className="mt-5 grid w-full max-w-full grid-cols-1 gap-3 sm:grid-cols-2">
              <button className="inline-flex w-full min-w-0 items-center justify-center gap-2 rounded-full bg-gradient-to-r from-violet to-ocean px-5 py-3 text-center font-bold text-white shadow-glow" onClick={downloadPoster} disabled={downloading}>
                <Icon name="download" />
                {downloading ? "正在生成..." : "下载海报"}
              </button>
              <button className="inline-flex w-full min-w-0 items-center justify-center gap-2 rounded-full bg-white px-5 py-3 text-center font-bold text-slate-700 shadow-sm" onClick={() => downloadCalendarPlanTable(planItems, planLength)}>
                <Icon name="table" />
                导出日历计划表
              </button>
              <button className="inline-flex w-full min-w-0 items-center justify-center gap-2 rounded-full bg-white px-5 py-3 text-center font-bold text-slate-700 shadow-sm" onClick={copyShareText}>
                <Icon name={copied ? "check" : "copy"} />
                {copied ? "已复制文案" : "复制分享文案"}
              </button>
              <button className="inline-flex w-full min-w-0 items-center justify-center gap-2 rounded-full bg-white px-5 py-3 text-center font-bold text-slate-700 shadow-sm" onClick={onRestart}>
                <Icon name="rotate-ccw" />
                重新生成计划
              </button>
              <button className="inline-flex w-full min-w-0 items-center justify-center gap-2 rounded-full bg-white px-5 py-3 text-center font-bold text-slate-700 shadow-sm" onClick={onPlanner}>
                <Icon name="calendar-days" />
                返回计划编辑器
              </button>
              <a className="inline-flex w-full min-w-0 items-center justify-center gap-2 rounded-full bg-gradient-to-r from-coral via-violet to-ocean px-5 py-4 text-center text-lg font-black text-white shadow-glow ring-2 ring-white/80 transition hover:-translate-y-0.5 sm:text-xl" href="#student-group">
                <Icon name="messages-square" />
                加入考生专属交流群
              </a>
            </div>
          </div>

          <div className="w-full max-w-full overflow-hidden rounded-3xl bg-white p-4 shadow-soft">
            <h2 className="mb-2 text-lg font-black">日历计划表预览</h2>
            <div className="max-h-40 overflow-auto rounded-2xl bg-slate-50 thin-scrollbar">
              <table className="w-full min-w-[520px] text-left text-xs">
                <thead className="sticky top-0 bg-slate-100 text-slate-600">
                  <tr>
                    <th className="px-3 py-2 font-black">日期</th>
                    <th className="px-3 py-2 font-black">星期</th>
                    <th className="px-3 py-2 font-black">关键事项</th>
                    <th className="px-3 py-2 font-black">任务</th>
                    <th className="px-3 py-2 font-black">分类</th>
                  </tr>
                </thead>
                <tbody>
                  {planRows.slice(0, 14).map((row) => (
                    <tr key={row.day} className="border-t border-white">
                      <td className="px-3 py-2 font-bold text-slate-700">{row.displayDate}</td>
                      <td className="px-3 py-2 text-slate-500">{row.weekday}</td>
                      <td className="px-3 py-2 text-slate-600">{row.events}</td>
                      <td className="px-3 py-2 text-slate-600">{row.tasks}</td>
                      <td className="px-3 py-2 text-slate-500">{row.categories}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
            <p className="mt-2 text-xs font-semibold text-slate-500">预览前 14 天，导出文件包含 {planLength} 天完整计划。</p>
          </div>

          <LeadMagnetSection compact />
        </div>
      </section>
    </main>
  );
}

function LeadMagnetSection({ compact = false }: { compact?: boolean }) {
  const items = [
    {
      title: "《大学开局必备装备清单》",
      description: "证件、宿舍用品、电子用品推荐，军训无伤通关指南。"
    },
    {
      title: "《新生社交破冰话术包》",
      description: "自我介绍、班委竞选、社团面试、宿舍夜聊，让你永不尴尬。"
    },
    {
      title: "《大学生活避坑地图》",
      description: "衣食住行+人际交往+课业绩点，让你不用吃一堑也能长一智。"
    }
  ];
  const coupons = [
    {
      title: "《AI创作/编程大额减免券》",
      description: "想学会用AI做短剧/漫剧吗？想零基础手搓个人网页/小程序吗？来就是了！"
    },
    {
      title: "《大学四六级/雅思体验券》",
      description: "绩点、保研、考研、留学，英语都是硬指标，早学早渡劫。"
    },
    {
      title: "《口才表达&形象气质提升券》",
      description: "想要趁假期逆袭，改头换面、气质超凡、说话又好听的看过来。"
    }
  ];

  return (
    <section id="student-group" className={cx("mt-6 w-full max-w-full min-w-0 rounded-3xl p-5 shadow-soft sm:p-6", compact ? "bg-white" : "glass-panel")}>
      <div className="mb-5 flex flex-wrap gap-3">
        <button className="inline-flex items-center justify-center gap-2 rounded-full bg-slate-900 px-5 py-3 font-bold text-white">
          加入2026考生专属交流群
          <Icon name="messages-square" />
        </button>
        <button className="inline-flex items-center justify-center gap-2 rounded-full bg-white px-5 py-3 font-bold text-slate-700 shadow-sm">
          不仅能交友，还能领福利
          <Icon name="gift" />
        </button>
      </div>
      <div className={cx("mb-5 flex", compact ? "justify-start sm:justify-end lg:pr-16" : "justify-end")}>
        <div className={cx("flex max-w-full gap-3 rounded-3xl bg-white p-3 shadow-sm", compact ? "w-full flex-col items-start sm:w-auto sm:flex-row sm:items-center" : "items-center")}>
          <div className={cx("shrink-0 overflow-hidden rounded-2xl bg-white p-1 shadow-sm", compact ? "h-20 w-20 sm:h-24 sm:w-24" : "h-24 w-24")}>
            <img className="block h-full w-full rounded-xl object-contain" src="/assets/group-qr.png" alt="扫码加入2026考生专属交流群" />
          </div>
          <div className="min-w-0">
            <p className="inline-flex items-center gap-1 font-black text-slate-900">
              扫码进群
              <Icon name="arrow-down" className="h-4 w-4 text-violet" />
            </p>
            <p className="mt-1 break-words text-sm font-black leading-6 text-violet">如码已失效，请添加器主微信：259399683</p>
            <p className="mt-1 break-words text-sm leading-6 text-slate-500">群内只有器主一个上古神登和一众小登，家长请自觉</p>
          </div>
        </div>
      </div>
      <div className="grid gap-4 lg:grid-cols-2">
        <BenefitColumn title="开学资料包" icon="folder-open" items={items} tone="light" />
        <BenefitColumn
          title="技能升级券"
          description="不是逼你学习，但是暑假漫漫，学几个又好玩又实用的技能又何妨。"
          icon="ticket"
          items={coupons}
          tone="dark"
        />
      </div>
    </section>
  );
}

function BenefitColumn({
  title,
  description,
  icon,
  items,
  tone
}: {
  title: string;
  description?: string;
  icon: string;
  items: Array<string | { title: string; description?: string }>;
  tone: "light" | "dark";
}) {
  const dark = tone === "dark";
  return (
    <div className={cx("min-w-0 rounded-3xl p-5", dark ? "bg-slate-900 text-white" : "bg-white text-slate-900 shadow-sm")}>
      <h3 className="mb-4 flex items-center gap-2 text-xl font-black">
        <span className={cx("grid h-10 w-10 place-items-center rounded-2xl", dark ? "bg-white/12 text-white" : "bg-indigo-50 text-violet")}>
          <Icon name={icon} className="h-5 w-5" />
        </span>
        {title}
      </h3>
      {description && <p className={cx("mb-4 leading-7", dark ? "text-white/72" : "text-slate-500")}>{description}</p>}
      <div className="grid gap-2">
        {items.map((item, index) => {
          const itemTitle = typeof item === "string" ? item : item.title;
          const itemDescription = typeof item === "string" ? "" : item.description;
          return (
          <div key={itemTitle} className={cx("flex min-w-0 items-start gap-3 rounded-2xl px-4 py-3", dark ? "bg-white/10" : "bg-slate-50 text-slate-700")}>
            <span className={cx("grid h-7 w-7 shrink-0 place-items-center rounded-full text-xs", dark ? "bg-white text-slate-900" : "bg-violet text-white")}>{index + 1}</span>
            <span className="min-w-0">
              <span className="block break-words font-bold">{itemTitle}</span>
              {itemDescription && <span className={cx("mt-1 block break-words text-sm leading-6", dark ? "text-white/70" : "text-slate-500")}>{itemDescription}</span>}
            </span>
          </div>
          );
        })}
      </div>
    </div>
  );
}

function EmptyState({ title, body, action, onAction }: { title: string; body: string; action: string; onAction: () => void }) {
  return (
    <main className="mx-auto grid min-h-[70vh] max-w-xl place-items-center px-4">
      <section className="glass-panel rounded-3xl p-8 text-center shadow-glow">
        <div className="mx-auto mb-5 grid h-16 w-16 place-items-center rounded-2xl bg-gradient-to-br from-violet to-ocean text-white">
          <Icon name="sparkles" className="h-8 w-8" />
        </div>
        <h1 className="text-2xl font-black">{title}</h1>
        <p className="mt-3 leading-7 text-slate-600">{body}</p>
        <button className="mt-6 inline-flex items-center justify-center gap-2 rounded-full bg-gradient-to-r from-violet to-ocean px-6 py-3 font-bold text-white shadow-glow" onClick={onAction}>
          {action}
          <Icon name="arrow-right" />
        </button>
      </section>
    </main>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
