// app.jsx — Dream diagnosis single-flow app
// User writes a dream + small Q's → AI generates a diagnosis + dream type card

const { useState, useEffect, useRef, useMemo } = React;

// ============== util ==============
function todayInfo() {
  const d = new Date();
  const yobi = ["日", "月", "火", "水", "木", "金", "土"];
  const month = ["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"];
  // moon phase
  const lp = 29.530588853;
  const known = new Date(2000, 0, 6, 18, 14).getTime() / 86400000;
  const days = d.getTime() / 86400000;
  const phase = ((days - known) % lp + lp) % lp / lp;
  let phaseName, phaseGlyph;
  if (phase < 0.04 || phase > 0.96) { phaseName = "新月"; phaseGlyph = "new"; }
  else if (phase < 0.22) { phaseName = "三日月"; phaseGlyph = "crescent-w"; }
  else if (phase < 0.28) { phaseName = "上弦"; phaseGlyph = "half-w"; }
  else if (phase < 0.47) { phaseName = "十三夜"; phaseGlyph = "gibbous-w"; }
  else if (phase < 0.53) { phaseName = "満月"; phaseGlyph = "full"; }
  else if (phase < 0.72) { phaseName = "居待月"; phaseGlyph = "gibbous-e"; }
  else if (phase < 0.78) { phaseName = "下弦"; phaseGlyph = "half-e"; }
  else { phaseName = "有明月"; phaseGlyph = "crescent-e"; }
  return {
    iso: d.toISOString().slice(0,10),
    full: `${d.getFullYear()}.${String(d.getMonth()+1).padStart(2,"0")}.${String(d.getDate()).padStart(2,"0")} ${yobi[d.getDay()]}`,
    en: `${month[d.getMonth()]} ${String(d.getDate()).padStart(2,"0")}, ${d.getFullYear()}`,
    phase, phaseName, phaseGlyph,
  };
}

function MoonGlyph({ kind, size = 14 }) {
  const cx = size/2, cy = size/2, r = size/2 - 1;
  const off = "#C89B5E";
  const on  = "#FAF2E1";
  switch (kind) {
    case "full": return <svg width={size} height={size}><circle cx={cx} cy={cy} r={r} fill={off}/></svg>;
    case "new":  return <svg width={size} height={size}><circle cx={cx} cy={cy} r={r} fill="none" stroke="#735C46" strokeWidth="1"/></svg>;
    case "half-w": return <svg width={size} height={size}><circle cx={cx} cy={cy} r={r} fill="#735C46"/><path d={`M ${cx},${cy-r} A ${r},${r} 0 0,1 ${cx},${cy+r} Z`} fill={off}/></svg>;
    case "half-e": return <svg width={size} height={size}><circle cx={cx} cy={cy} r={r} fill="#735C46"/><path d={`M ${cx},${cy-r} A ${r},${r} 0 0,0 ${cx},${cy+r} Z`} fill={off}/></svg>;
    case "crescent-w": return <svg width={size} height={size}><circle cx={cx} cy={cy} r={r} fill="#735C46"/><path d={`M ${cx-1},${cy-r+1} A ${r-0.5},${r-0.5} 0 0,1 ${cx-1},${cy+r-1} A ${r-3},${r-2} 0 0,0 ${cx-1},${cy-r+1} Z`} fill={off}/></svg>;
    case "crescent-e": return <svg width={size} height={size}><circle cx={cx} cy={cy} r={r} fill="#735C46"/><path d={`M ${cx+1},${cy-r+1} A ${r-0.5},${r-0.5} 0 0,0 ${cx+1},${cy+r-1} A ${r-3},${r-2} 0 0,1 ${cx+1},${cy-r+1} Z`} fill={off}/></svg>;
    case "gibbous-w": return <svg width={size} height={size}><circle cx={cx} cy={cy} r={r} fill={off}/><path d={`M ${cx+1},${cy-r} A ${r-2},${r} 0 0,0 ${cx+1},${cy+r} A ${r},${r} 0 0,0 ${cx+1},${cy-r} Z`} fill="#735C46" opacity="0.55"/></svg>;
    case "gibbous-e": return <svg width={size} height={size}><circle cx={cx} cy={cy} r={r} fill={off}/><path d={`M ${cx-1},${cy-r} A ${r-2},${r} 0 0,1 ${cx-1},${cy+r} A ${r},${r} 0 0,1 ${cx-1},${cy-r} Z`} fill="#735C46" opacity="0.55"/></svg>;
    default: return null;
  }
}

const MOODS = [
  { id: "しあわせ", emoji: "✿" },
  { id: "ふしぎ",   emoji: "✦" },
  { id: "なつかしい", emoji: "❀" },
  { id: "せつない", emoji: "✧" },
  { id: "ふあん",   emoji: "✶" },
  { id: "こわい",   emoji: "✺" },
  { id: "わからない", emoji: "◌" },
];

const PEOPLE = ["自分だけ", "知っている人", "知らない人", "好きな人", "家族", "動物", "誰もいない"];

const COLORS = [
  { id: "白", sw: "#F4ECDA" },
  { id: "青", sw: "#7A9BB8" },
  { id: "赤", sw: "#C76060" },
  { id: "ピンク", sw: "#E8B5BA" },
  { id: "黄", sw: "#E8C868" },
  { id: "緑", sw: "#9CB89A" },
  { id: "紫", sw: "#A893B8" },
  { id: "金", sw: "#D4A574" },
  { id: "黒", sw: "#3A2E26" },
  { id: "たくさん", sw: "linear-gradient(135deg,#E8B5BA,#C9B8D9,#9CB89A)" },
  { id: "覚えてない", sw: "rgba(74,56,40,0.1)" },
];

// ========== InputForm ==========
function InputForm({ onSubmit, initialData }) {
  const [dream, setDream] = useState(initialData?.dream || "");
  const [mood, setMood] = useState(initialData?.mood || null);
  const [people, setPeople] = useState(initialData?.people || []);
  const [colors, setColors] = useState(initialData?.colors || []);
  const [image, setImage] = useState(initialData?.image || null); // dataURL
  const [shaking, setShaking] = useState(false);
  const taRef = useRef(null);
  const fileRef = useRef(null);

  const togglePerson = (p) => setPeople(prev => prev.includes(p) ? prev.filter(x => x !== p) : [...prev, p]);
  const toggleColor  = (c) => setColors(prev => prev.includes(c) ? prev.filter(x => x !== c) : [...prev, c]);

  const handleFile = (file) => {
    if (!file || !file.type.startsWith("image/")) return;
    const reader = new FileReader();
    reader.onload = (ev) => {
      // Resize for storage efficiency
      const img = new Image();
      img.onload = () => {
        const max = 1200;
        const ratio = Math.min(1, max / Math.max(img.width, img.height));
        const w = Math.round(img.width * ratio);
        const h = Math.round(img.height * ratio);
        const c = document.createElement("canvas");
        c.width = w; c.height = h;
        c.getContext("2d").drawImage(img, 0, 0, w, h);
        setImage(c.toDataURL("image/jpeg", 0.85));
      };
      img.src = ev.target.result;
    };
    reader.readAsDataURL(file);
  };

  const onFileChange = (e) => handleFile(e.target.files?.[0]);
  const onDrop = (e) => { e.preventDefault(); handleFile(e.dataTransfer.files?.[0]); };

  const submit = () => {
    if (dream.trim().length < 10) {
      setShaking(true);
      setTimeout(() => setShaking(false), 350);
      taRef.current?.focus();
      return;
    }
    onSubmit({ dream: dream.trim(), mood, people, colors, image });
  };

  return (
    <div className="input-section">
      <div className="card">
        <div className="card-title-row">
          <span className="card-step">01</span>
          <h2 className="card-title">夢の内容を聞かせてください</h2>
          <span className="card-title-en">Tell me your dream</span>
        </div>

        <textarea
          ref={taRef}
          className={`dream-textarea ${shaking ? "shake" : ""}`}
          placeholder="例：海の底を歩いていた。古い時計が砂に埋もれていて、知らない人がそっと鍵を渡してくれた。空はとても青かった——"
          value={dream}
          onChange={(e) => setDream(e.target.value)}
        />
        <div className="char-count">{dream.length} chars · 10字以上で診断できます</div>

        <div className="q-group">
          <div className="q-label">夢の気分 <span className="hint">— mood</span></div>
          <div className="chips">
            {MOODS.map(m => (
              <button
                key={m.id}
                className={`chip ${mood === m.id ? "active" : ""}`}
                onClick={() => setMood(mood === m.id ? null : m.id)}
              >
                <span style={{opacity: 0.7}}>{m.emoji}</span> {m.id}
              </button>
            ))}
          </div>
        </div>

        <div className="q-group">
          <div className="q-label">登場した人 <span className="hint">— who appeared</span></div>
          <div className="chips">
            {PEOPLE.map(p => (
              <button key={p} className={`chip ${people.includes(p) ? "active" : ""}`} onClick={() => togglePerson(p)}>{p}</button>
            ))}
          </div>
        </div>

        <div className="q-group">
          <div className="q-label">印象的だった色 <span className="hint">— colors</span></div>
          <div className="chips">
            {COLORS.map(c => (
              <button key={c.id} className={`chip ${colors.includes(c.id) ? "active" : ""}`} onClick={() => toggleColor(c.id)}>
                <span className="swatch" style={{ background: c.sw }}></span>{c.id}
              </button>
            ))}
          </div>
        </div>

        <div className="q-group">
          <div className="q-label">あなたのアイコン <span className="hint">— optional / share avatar</span></div>
          {image ? (
            <div className="image-preview avatar">
              <img src={image} alt="あなたのアイコン"/>
              <div className="image-preview-overlay">
                <button onClick={() => fileRef.current?.click()}>差し替える</button>
                <button onClick={() => setImage(null)}>削除</button>
              </div>
            </div>
          ) : (
            <label
              className="image-drop"
              onDragOver={(e) => { e.preventDefault(); e.currentTarget.classList.add("over"); }}
              onDragLeave={(e) => e.currentTarget.classList.remove("over")}
              onDrop={(e) => { e.currentTarget.classList.remove("over"); onDrop(e); }}
            >
              <svg viewBox="0 0 60 60" width="44" height="44" fill="none">
                <circle cx="30" cy="22" r="9" stroke="currentColor" strokeWidth="1.4"/>
                <path d="M 12,50 C 12,38 22,32 30,32 C 38,32 48,38 48,50" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
                <circle cx="46" cy="14" r="6" fill="none" stroke="currentColor" strokeWidth="1.2"/>
                <path d="M 43,14 L 49,14 M 46,11 L 46,17" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round"/>
              </svg>
              <div className="image-drop-text">
                <div>SNSのアイコンを添えて<br/>シェア用カードに登場させる</div>
                <div className="image-drop-hint">タップ or ドロップで設定 · 結果画像に反映されます</div>
              </div>
              <input ref={fileRef} type="file" accept="image/*" onChange={onFileChange} style={{display: "none"}}/>
            </label>
          )}
          <input ref={fileRef} type="file" accept="image/*" onChange={onFileChange} style={{display: "none"}}/>
        </div>

        <div className="submit-row">
          <button className="btn-divine" onClick={submit}>診　断</button>
        </div>
      </div>
    </div>
  );
}

// ========== Loading ==========
function LoadingPanel() {
  return (
    <div className="card">
      <div className="loading-state">
        <div className="loading-sparkles">
          <svg viewBox="0 0 80 80">
            <g fill="none" stroke="#C89B5E" strokeWidth="0.8" strokeLinecap="round">
              <circle cx="40" cy="40" r="30" opacity="0.3"/>
              <path d="M 40,10 L 42,38 L 70,40 L 42,42 L 40,70 L 38,42 L 10,40 L 38,38 Z" fill="#C89B5E" opacity="0.6"/>
            </g>
          </svg>
          <svg viewBox="0 0 80 80">
            <g fill="#C97B7B" opacity="0.7">
              <circle cx="20" cy="20" r="2"/>
              <circle cx="60" cy="22" r="1.5"/>
              <circle cx="64" cy="60" r="2"/>
              <circle cx="18" cy="58" r="1.5"/>
            </g>
          </svg>
        </div>
        <div className="loading-text">夢を解析しています<span className="dots"></span></div>
      </div>
    </div>
  );
}

// ========== Tarot Card ==========
const ROMAN = ["I","II","III","IV","V","VI","VII","VIII","IX","X","XI","XII","XIII","XIV","XV","XVI","XVII","XVIII","XIX","XX","XXI","XXII"];

function TarotCard({ reading, dateStr }) {
  const dreamCard = window.getDreamCardByRoman ? window.getDreamCardByRoman(reading.roman) : null;
  const primary = window.STICKERS.find(s => s.id === reading.primary_symbol)
              || window.STICKERS.find(s => s.id === "moon-crescent");
  const secondary = window.STICKERS.find(s => s.id === reading.secondary_symbol);
  const hasImage = !!reading._image;
  return (
    <div className="tarot-stage">
      <div className={`tarot generated-card ${hasImage ? "has-image" : ""}`}>
        {dreamCard?.image && <img className="tarot-generated-bg" src={dreamCard.image} alt=""/>}
        <div className="tarot-generated-overlay"></div>
        <div className="tarot-corner tl"></div>
        <div className="tarot-corner tr"></div>
        <div className="tarot-corner bl"></div>
        <div className="tarot-corner br"></div>

        <div className="tarot-typecode">
          <span className="label">TYPE</span>
          <span className="value">{reading.type_code || dreamCard?.code || "M-XVIII"}</span>
        </div>

        <div className="tarot-roman">{reading.roman || dreamCard?.roman || "I"}</div>

        {hasImage && (
          <div className="tarot-illust user-avatar-layer">
            {secondary && <div className="secondary s1">{secondary.render()}</div>}
            <div className="tarot-avatar">
              <img src={reading._image} alt=""/>
            </div>
            <div className="tarot-avatar-halo">{primary.render()}</div>
            <span className="sparkle sp1">✦</span>
            <span className="sparkle sp2">✧</span>
            <span className="sparkle sp3">·</span>
          </div>
        )}

        <div className="tarot-name-en">{reading.card_name_en || dreamCard?.nameEn || "The Dream"}</div>
        <div className="tarot-name-jp">{reading.card_name_jp || dreamCard?.nameJp || "夢"}</div>

        <div className="tarot-ornament">
          <span className="line"></span>
          <svg width="12" height="12" viewBox="0 0 12 12"><path d="M 6,1 L 7,5 L 11,6 L 7,7 L 6,11 L 5,7 L 1,6 L 5,5 Z" fill="currentColor"/></svg>
          <span className="line"></span>
        </div>
        <div className="tarot-date">{dateStr}</div>
      </div>
    </div>
  );
}

// ========== Theme Icons ==========
function ThemeIcon({ kind }) {
  if (kind === "love") return (
    <svg viewBox="0 0 36 36" fill="none">
      <path d="M 18,30 C 8,22 4,14 9,9 C 13,5 17,8 18,12 C 19,8 23,5 27,9 C 32,14 28,22 18,30 Z"
            fill="#E8C4C4" stroke="#C97B7B" strokeWidth="1.2" strokeLinejoin="round"/>
    </svg>
  );
  if (kind === "work") return (
    <svg viewBox="0 0 36 36" fill="none">
      <path d="M 6,14 L 30,14 L 30,28 L 6,28 Z M 14,14 L 14,10 L 22,10 L 22,14" stroke="#C89B5E" strokeWidth="1.3" strokeLinejoin="round" fill="#F2E0CC"/>
      <path d="M 6,20 L 30,20" stroke="#C89B5E" strokeWidth="1" opacity="0.5"/>
    </svg>
  );
  if (kind === "relations") return (
    <svg viewBox="0 0 36 36" fill="none">
      <circle cx="13" cy="14" r="4.5" fill="#DDD2E8" stroke="#B8A8CC" strokeWidth="1.2"/>
      <circle cx="23" cy="14" r="4.5" fill="#DDD2E8" stroke="#B8A8CC" strokeWidth="1.2"/>
      <path d="M 6,30 C 6,24 12,21 13,21 C 14,21 16,22 18,22 C 20,22 22,21 23,21 C 24,21 30,24 30,30" stroke="#B8A8CC" strokeWidth="1.2" fill="#EFE7F2"/>
    </svg>
  );
  return null;
}

function ScoreDots({ score = 3 }) {
  const labels = ["弱", "やや弱", "並", "やや強", "強"];
  return (
    <div className="theme-score">
      <div className="theme-bar">
        <div className="theme-bar-fill" style={{ width: `${(score / 5) * 100}%` }}></div>
      </div>
      <div className="theme-score-label">
        <span>{labels[Math.max(0, Math.min(4, score - 1))]}</span>
        <span className="theme-score-num">{score}/5</span>
      </div>
    </div>
  );
}

// ========== Result ==========
function ResultPanel({ reading, dateStr, onReset, onShare }) {
  const themeMeta = {
    love: { jp: "恋愛", en: "Love" },
    work: { jp: "仕事", en: "Work" },
    relations: { jp: "対人", en: "Relations" },
  };
  const order = ["love", "work", "relations"];

  return (
    <div className="result">
      <div className="result-divider"><span className="label">★ 今朝の夢診断書 ★</span></div>

      <div className="result-card-wrap">
        <TarotCard reading={reading} dateStr={dateStr}/>
        <div className="result-card-meta">
          <div className="result-type-row">
            <span className="result-type-label">あなたの夢タイプ</span>
            <span className="result-type-name">{reading.dream_type_jp}</span>
            <span className="result-match">MATCH {reading.match_score}%</span>
          </div>
          <span className="jp-name">{reading.card_name_jp}</span>
          <span>{reading.card_name_en} · {dateStr.toUpperCase()}</span>
        </div>

        {reading.aura_color && (
          <div className="aura-strip">
            <span className="aura-label">TODAY'S AURA</span>
            <span className="aura-swatch" style={{ background: reading.aura_color.hex }}></span>
            <span className="aura-name">{reading.aura_color.name}</span>
            <span className="aura-hex">{reading.aura_color.hex.toUpperCase()}</span>
          </div>
        )}
      </div>

      <div className="main-message">
        <div className="label">DREAM ANALYSIS</div>
        <div className="text">{reading.main_message}</div>
      </div>

      <div className="themes">
        {order.map(k => {
          const t = reading.themes?.[k] || { score: 3, text: "" };
          return (
            <div key={k} className={`theme-card ${k}`}>
              <div className="theme-icon"><ThemeIcon kind={k}/></div>
              <div className="theme-name">{themeMeta[k].jp}</div>
              <div className="theme-name-en">{themeMeta[k].en}</div>
              <ScoreDots score={t.score}/>
              <div className="theme-text">{t.text}</div>
            </div>
          );
        })}
      </div>

      <div className="card keywords-card">
        <div className="card-title-row">
          <span className="card-step">04</span>
          <h2 className="card-title">夢に現れた象徴</h2>
          <span className="card-title-en">Symbol Analysis</span>
        </div>
        <div className="keyword-list">
          {(reading.keywords || []).map((k, i) => {
            const sticker = window.STICKERS.find(s => s.id === k.symbol_id)
                       || window.STICKERS.find(s => s.id === reading.primary_symbol)
                       || window.STICKERS[0];
            return (
              <div key={i} className="keyword-row">
                <div className="keyword-icon">{sticker.render()}</div>
                <div className="keyword-word">{k.word}</div>
                <div className="keyword-meaning">{k.meaning}</div>
              </div>
            );
          })}
        </div>
      </div>

      <div className="card advice-card">
        <div className="card-title-row">
          <span className="card-step">05</span>
          <h2 className="card-title">今日のアドバイス</h2>
          <span className="card-title-en">Today's Recommendation</span>
        </div>
        <div className="advice-quote">{reading.advice}</div>
      </div>

      <div className="actions">
        <button className="btn-divine" onClick={onShare}>画像で保存 ・ シェア</button>
        <button className="btn-text" onClick={onReset}>もう一度診断する</button>
      </div>
    </div>
  );
}

// ========== AI integration ==========
async function divine({ dream, mood, people, colors }) {
  const symbolList = window.STICKERS.map(s => `${s.id}:${s.name}`).join(", ");
  const prompt = `あなたは「夢 診断」サイト専用の、優しく詩的な夢分析AIです。
このサイトには診断結果ページがすでにあり、返答はそのページの各表示欄にそのまま入ります。
UIや構成に合うよう、必ず下記JSONフォーマットだけで返してください。前置き、説明、Markdown、コードフェンスは禁止です。

【診断の方針】
あなたは「不安を煽る占い師」ではなく、ユーザーの感情を整理し安心へ導く“夢感情分析カウンセラー”です。
夢の内容を、恐怖・不吉・未来予言として断定せず、以下のような感情メタファーとして解釈してください。
- 最近の心理状態
- 心の緊張
- 未整理感情
- 回復願望
- 変化への不安

出力では以下を徹底してください。
- 不安を増幅しない。
- 死や不幸を断定しない。
- 優しく納得感のある言葉を使う。
- “怖い夢にも意味がある” と感じられる表現にする。
- 最後は必ず安心感・回復感へ着地する。
- 詩的すぎず、理解できる文章にする。
- 説教しない。
- スピリチュアル断定をしない。
- ユーザーを否定しない。
- 「今のあなたは疲れていたのかもしれません」のような自己受容へ繋げる。

夢の内容に登場する物・場所・色・人物を象徴として分析し、感情メタファーとして解釈してください。
ユーザーのヒアリング内容（夢本文、気分、登場人物、色）は必ず診断文のどこかに具体的に含めてください。未入力の項目は無理に作らず、入力された項目を優先してください。
表示崩れを避けるため、各項目の文字数目安を必ず守ってください。

【利用者の夢】
${dream}

【気分】${mood || "不明"}
【登場】${people.join("、") || "不明"}
【色】${colors.join("、") || "不明"}

【利用可能な象徴ID（symbol_id）】
${symbolList}

【22枚の固定カード】
夢に最も合うカードをこの中から1枚選び、romanを返してください。type_code / dream_type_jp / card_name_jp / card_name_en はサイト側でromanに合わせて固定表示されます。
I はじまりの月 / II 静かな鏡 / III 花ひらく庭 / IV 眠れる家 / V 古い時計 / VI 結びの糸 / VII 夜の列車 / VIII 金の鍵 / IX 白猫の駅 / X 星の輪 / XI やさしい獅子 / XII 水底の灯 / XIII ほどける羽 / XIV 虹の杯 / XV 影の小部屋 / XVI ひらく塔 / XVII 願い星 / XVIII 月の使者 / XIX 朝日の扉 / XX 呼ぶ声 / XXI 夢の円環 / XXII 旅する子鹿

【結果ページとの対応 / 出力内容】
結果はサイト上では以下の5項目として表示されます。JSONの各項目にこの役割を持たせてください。
1. 夢の要約分析: main_message。夢本文・気分・人物・色を少なくとも2つ以上具体的に含め、心理状態/未整理感情/回復願望として整理する。
2. 分野別診断（恋愛・仕事・対人）: themes.love / themes.work / themes.relations。夢の象徴を各分野に自然に接続する。
3. 象徴分析: keywords。夢に出た物・場所・色・人物を拾い、感情メタファーとして解釈する。
4. 今日のアドバイス: advice。説教せず、今日できる小さな回復行動で安心へ着地する。
5. 夢タイプカード: romanを22枚の固定カードから1枚選ぶ。type_code / dream_type_jp / card_name_jp / card_name_en はサイト側でromanに合わせて固定表示されます。

【出力ルール】
- JSONとしてパース可能な形式だけを返す。コメント、注釈、末尾カンマは禁止。
- 文字列内の改行は避ける。
- symbol_id は必ず上の「利用可能な象徴ID」から選ぶ。存在しないIDを作らない。
- primary_symbol と secondary_symbol は別のIDにする。補助象徴が不要な場合のみ secondary_symbol は空文字にする。
- score は整数。match_score は 85〜99、themes の score は 1〜5。
- aura_color.hex は必ず #RRGGBB。name は和色風の短い名前。
- keywords は3〜5個。夢本文から印象的な語を拾い、wordは2〜5文字程度にする。

【出力フォーマット】
{
  "type_code": "（3〜5字。例：M-18, T-09, E-04。romanの番号と雰囲気を合わせる）",
  "dream_type_jp": "（5〜8字の日本語の夢タイプ名。例：月の使者型、忘れ鍵型、青い扉型）",
  "card_name_jp": "（10字以内の詩的なカード名。例：月の使者、忘れた鍵、青い扉）",
  "card_name_en": "（The 〜 形式の短い英語名。例：The Moon's Messenger）",
  "roman": "（I〜XXIIのローマ数字から1つ）",
  "primary_symbol": "（symbol_idリストから中心象徴を1つ）",
  "secondary_symbol": "（symbol_idリストから補助象徴を1つ。不要なら空文字）",
  "aura_color": { "hex": "#RRGGBB", "name": "（2〜6字の和色風の色名）" },
  "match_score": 92,
  "main_message": "（1. 夢の要約分析。90〜130字。夢本文・気分・人物・色のうち入力された内容を必ず具体的に含め、最近の心理状態/心の緊張/未整理感情/回復願望として優しく整理する）",
  "themes": {
    "love":      { "score": 3, "text": "（2. 恋愛・愛情面。30〜45字。夢の象徴を使い、不安を煽らず自己受容へ繋げる）" },
    "work":      { "score": 3, "text": "（2. 仕事・学び・行動面。30〜45字。緊張や変化への不安を整理する）" },
    "relations": { "score": 3, "text": "（2. 対人関係。30〜45字。人物/距離感/安心感に触れる）" }
  },
  "keywords": [
    { "word": "（3. 夢から拾った物・場所・色・人物）", "symbol_id": "（symbol_idリストから1つ）", "meaning": "（象徴分析。30〜55字。感情メタファーとして解釈）" },
    { "word": "（3. 夢から拾った物・場所・色・人物）", "symbol_id": "（symbol_idリストから1つ）", "meaning": "（象徴分析。30〜55字。感情メタファーとして解釈）" },
    { "word": "（3. 夢から拾った物・場所・色・人物）", "symbol_id": "（symbol_idリストから1つ）", "meaning": "（象徴分析。30〜55字。感情メタファーとして解釈）" }
  ],
  "advice": "（4. 今日のアドバイス。45〜65字。今日できる小さな回復行動。最後は必ず安心感・回復感へ着地）"
}

JSON以外は何も出力しないでください。`;

  let raw;
  try {
    const response = await fetch("/api/dream-diagnosis", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ dream, mood, people, colors, symbolList })
    });
    if (!response.ok) throw new Error("api failed");
    const data = await response.json();
    raw = data.text;
    if (!raw) throw new Error("empty result");
  } catch (e) {
    if (window.claude && typeof window.claude.complete === "function") {
      try {
        raw = await window.claude.complete(prompt);
      } catch (fallbackError) {
        throw new Error("夢分析士にうまく届かなかったみたい…");
      }
    } else {
      throw new Error("夢分析士にうまく届かなかったみたい…");
    }
  }

  // strip code fences if any
  let text = raw.trim();
  if (text.startsWith("```")) {
    text = text.replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/, "");
  }
  // grab first { ... last }
  const first = text.indexOf("{");
  const last = text.lastIndexOf("}");
  if (first >= 0 && last > first) text = text.slice(first, last + 1);

  let parsed;
  try {
    parsed = JSON.parse(text);
  } catch (e) {
    throw new Error("結果が読み取れませんでした");
  }

  // validate / fallback fill
  parsed.roman = ROMAN.includes(parsed.roman) ? parsed.roman : "XVIII";
  const fixedCard = window.getDreamCardByRoman ? window.getDreamCardByRoman(parsed.roman) : null;
  parsed.type_code = fixedCard?.code || parsed.type_code || "M-XVIII";
  parsed.dream_type_jp = fixedCard?.type || parsed.dream_type_jp || "月の使者型";
  parsed.card_name_jp = fixedCard?.nameJp || parsed.card_name_jp || "夢の使者";
  parsed.card_name_en = fixedCard?.nameEn || parsed.card_name_en || "The Dream Messenger";
  parsed.primary_symbol = parsed.primary_symbol || "moon-crescent";
  parsed.secondary_symbol = parsed.secondary_symbol || "";
  parsed.aura_color = parsed.aura_color && parsed.aura_color.hex
    ? parsed.aura_color
    : { hex: "#C89255", name: "蜜色" };
  parsed.match_score = typeof parsed.match_score === "number" ? parsed.match_score : 92;
  parsed.main_message = parsed.main_message || "あなたの夢は、まだ言葉にならない予感を運んでくれました。";
  parsed.themes = parsed.themes || {};
  ["love", "work", "relations"].forEach(k => {
    if (!parsed.themes[k]) parsed.themes[k] = { score: 3, text: "" };
  });
  parsed.keywords = Array.isArray(parsed.keywords) ? parsed.keywords : [];
  parsed.advice = parsed.advice || "今日は、心の声に耳を澄ませて。";
  return parsed;
}

// ========== App ==========
function App() {
  const [phase, setPhase] = useState("input"); // input | loading | result | error
  const [reading, setReading] = useState(null);
  const [error, setError] = useState(null);
  const [pendingInput, setPendingInput] = useState(null);
  const [shareOpen, setShareOpen] = useState(false);
  const [shareImg, setShareImg] = useState(null); // dataURL
  const [shareBusy, setShareBusy] = useState(false);
  const shareRef = useRef(null);
  const date = useMemo(() => todayInfo(), []);

  const generateShareImage = async () => {
    setShareOpen(true);
    setShareImg(null);
    setShareBusy(true);
    try {
      // wait for fonts + a paint
      if (document.fonts && document.fonts.ready) await document.fonts.ready;
      await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
      const node = shareRef.current;
      if (!node) throw new Error("share node missing");
      const canvas = await window.html2canvas(node, {
        backgroundColor: null,
        scale: 1,
        useCORS: true,
        logging: false,
        width: 1080,
        height: 1920,
        windowWidth: 1080,
        windowHeight: 1920,
      });
      const dataUrl = canvas.toDataURL("image/png");
      setShareImg(dataUrl);
    } catch (e) {
      console.error("share image gen failed", e);
      setShareImg("error");
    } finally {
      setShareBusy(false);
    }
  };

  const downloadShareImage = () => {
    if (!shareImg || shareImg === "error") return;
    const a = document.createElement("a");
    a.href = shareImg;
    a.download = `yumeshindan_${date.iso}.png`;
    document.body.appendChild(a);
    a.click();
    a.remove();
  };

  const copyShareImage = async () => {
    if (!shareImg || shareImg === "error") return;
    try {
      const blob = await (await fetch(shareImg)).blob();
      await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
      alert("画像をコピーしました ✨");
    } catch (e) {
      alert("コピーできませんでした。ダウンロードをお試しください。");
    }
  };

  const onSubmit = async (data) => {
    setPendingInput(data);
    try {
      sessionStorage.setItem("yumeshindan_pending_input", JSON.stringify(data));
    } catch (e) {}
    setPhase("loading");
    setError(null);
    try {
      const result = await divine(data);
      result._image = data.image || null; // attach image for rendering
      setReading(result);
      setPendingInput(null);
      try { sessionStorage.removeItem("yumeshindan_pending_input"); } catch (e) {}
      setPhase("result");
      setTimeout(() => window.scrollTo({ top: 0, behavior: "smooth" }), 50);
    } catch (e) {
      setError(e.message || "うまく診断できませんでした");
      setPhase("error");
    }
  };

  const retryLastInput = () => {
    if (pendingInput) {
      onSubmit(pendingInput);
      return;
    }
    try {
      const saved = JSON.parse(sessionStorage.getItem("yumeshindan_pending_input") || "null");
      if (saved?.dream) {
        onSubmit(saved);
        return;
      }
    } catch (e) {}
    setPhase("input");
  };

  const onReset = () => {
    setReading(null);
    setPendingInput(null);
    try { sessionStorage.removeItem("yumeshindan_pending_input"); } catch (e) {}
    setPhase("input");
    window.scrollTo({ top: 0, behavior: "smooth" });
  };

  return (
    <div className="page">
      <header className="header">
        <div className="date-pill">
          <span className="moon-glyph"><MoonGlyph kind={date.phaseGlyph} size={14}/></span>
          <span>{date.en}</span>
          <span className="dot"></span>
          <span style={{fontFamily: "var(--font-jp)", letterSpacing: "0.3em", paddingLeft: "0.3em"}}>{date.phaseName}</span>
        </div>
        <h1 className="brand-title">夢　診　断</h1>
        <div className="brand-sub">YUMESHINDAN · A MORNING DREAM ANALYSIS</div>
        <div className="ornament">
          <span className="line"></span>
          <svg width="14" height="14" viewBox="0 0 14 14"><path d="M 7,1 L 8,6 L 13,7 L 8,8 L 7,13 L 6,8 L 1,7 L 6,6 Z" fill="currentColor"/></svg>
          <span className="line"></span>
        </div>
        <p className="brand-tagline">今朝の夢から、あなたの今日を読み解く</p>
      </header>

      {phase === "input" && <InputForm onSubmit={onSubmit} initialData={pendingInput}/>}
      {phase === "loading" && <LoadingPanel/>}
      {phase === "result" && reading && (
        <ResultPanel
          reading={reading}
          dateStr={date.full}
          onReset={onReset}
          onShare={generateShareImage}
        />
      )}
      {phase === "error" && (
        <div className="card">
          <div style={{textAlign: "center", padding: "32px 16px"}}>
            <div style={{fontSize: 32, marginBottom: 12}}>✦</div>
            <h3 style={{fontFamily: "var(--font-jp)", fontSize: 16, letterSpacing: "0.2em", marginBottom: 12}}>{error}</h3>
            <button className="btn-text" onClick={retryLastInput}>もう一度試す</button>
          </div>
        </div>
      )}

      <div className="footer">
        <span>✦</span><span>Made with care for your morning</span><span>✦</span>
      </div>

      {/* Offscreen share-card for capture */}
      {reading && (
        <div className="share-stage" aria-hidden="true">
          <window.ShareCard ref={shareRef} reading={reading} dateStr={date.full}/>
        </div>
      )}

      {shareOpen && (
        <ShareModal
          img={shareImg}
          busy={shareBusy}
          onClose={() => setShareOpen(false)}
          onDownload={downloadShareImage}
          onCopy={copyShareImage}
        />
      )}
    </div>
  );
}

// ========== ShareModal ==========
function ShareModal({ img, busy, onClose, onDownload, onCopy }) {
  return (
    <div className="share-backdrop" onClick={onClose}>
      <div className="share-modal" onClick={(e) => e.stopPropagation()}>
        {busy || !img ? (
          <div className="share-preview-placeholder">
            <span style={{opacity: 0.8}}>画像を生成中<span className="dots"></span></span>
          </div>
        ) : img === "error" ? (
          <div className="share-preview-placeholder">
            <span>画像が作れませんでした…</span>
          </div>
        ) : (
          <img className="share-preview" src={img} alt="診断結果"/>
        )}
        <div className="share-actions">
          <button className="primary" onClick={onDownload} disabled={busy || !img || img === "error"}>画像をダウンロード</button>
          <button onClick={onCopy} disabled={busy || !img || img === "error"}>コピー</button>
          <button onClick={onClose}>閉じる</button>
        </div>
      </div>
    </div>
  );
}

window.App = App;
