// 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)" },
];

const SOCIAL_LOGIN_PROVIDERS = [
  { id: "google", label: "Google", handle: "Google", mark: "G" },
  { id: "x", label: "X", handle: "Xアカウント", mark: "𝕏" },
  { id: "threads", label: "Threads", handle: "Threads", mark: "◎" },
  { id: "instagram", label: "Instagram", handle: "Instagram", mark: "◐" },
];

const DEFAULT_LINE_FRIEND_URL = "https://line.me/R/ti/p/%40962bychq";

function getLineFriendUrl() {
  return window.__DREAM_LINE_FRIEND_URL__ || window.DREAM_LINE_FRIEND_URL || DEFAULT_LINE_FRIEND_URL;
}

async function fetchAuthConfig() {
  const response = await fetch('/api/auth/config', { cache: 'no-store' });
  if (!response.ok) throw new Error('auth config failed');
  return response.json();
}

const HOME_DREAM_CARDS = [
  { roman: "I", code: "M-01", type: "はじまりの月型", nameJp: "はじまりの月", nameEn: "The First Moon", image: "uploads/hard-cards/hard-dream-card-01-first-moon.png" },
  { roman: "VIII", code: "K-08", type: "金の鍵型", nameJp: "金の鍵", nameEn: "The Golden Key", image: "uploads/hard-cards/hard-dream-card-08-golden-key.png" },
  { roman: "XVII", code: "S-17", type: "願い星型", nameJp: "願い星", nameEn: "The Wishing Star", image: "uploads/hard-cards/hard-dream-card-17-wishing-star.png" },
  { roman: "XVIII", code: "M-18", type: "月の使者型", nameJp: "月の使者", nameEn: "The Moon Messenger", image: "uploads/hard-cards/hard-dream-card-18-moon-messenger.png" },
];
const HOME_ROMANS = HOME_DREAM_CARDS.map(card => card.roman);
function getHomeDreamCard(roman) {
  return HOME_DREAM_CARDS.find(card => card.roman === roman) || HOME_DREAM_CARDS[3];
}

const DREAM_CARD_MEANINGS = {
  I: "はじまりの月型は、まだ形になっていない本音や新しい感情が、静かに目を覚まし始めている時に現れる型です。夢の中の始まり・光・新しい場所は、心が次の一歩を探しているサインです。",
  VIII: "金の鍵型は、迷いや停滞の中に、すでに答えの手がかりが隠れている時に現れる型です。鍵・扉・受け取る物・探し物は、自分で選び直せる力を取り戻すサインです。",
  XVII: "願い星型は、諦めきれない願いや未来への期待が、心の奥でまだ光っている時に現れる型です。星・空・遠くの光は、希望をもう一度見つめ直すサインです。",
  XVIII: "月の使者型は、言葉にしきれていない不安や本音を、夢がそっと届けている時に現れる型です。夜・月・影・静かな場所は、自分を守りながら心の声を聞くサインです。",
};

function getDreamSymbol(symbolId) {
  const fallback = window.STICKERS?.find(s => s.id === "moon-crescent") || window.STICKERS?.[0] || null;
  return window.STICKERS?.find(s => s.id === symbolId) || fallback;
}

function getDreamSymbolMeta(symbolId) {
  const symbol = getDreamSymbol(symbolId);
  const index = Math.max(0, window.STICKERS?.findIndex(s => s.id === symbol?.id) ?? 0) + 1;
  const id = symbol?.id || "moon-crescent";
  const nameJp = symbol?.name || "三日月";
  const nameEn = symbol?.nameEn || "Dream Symbol";
  return {
    id,
    index,
    no: String(index).padStart(2, "0"),
    typeCode: `SYM-${String(index).padStart(2, "0")}`,
    typeJp: `${nameJp}のサイン`,
    nameJp,
    nameEn,
    image: `uploads/symbol-cards-web/symbol-card-${String(index).padStart(2, "0")}-${id}.webp`,
  };
}

function trimText(value, max = 92) {
  const text = String(value || "").replace(/\s+/g, " ").trim();
  return text.length > max ? `${text.slice(0, max)}…` : text;
}

function fallbackDreamSummary({ dream, mood, people, colors }) {
  const parts = [`「${trimText(dream, 72)}」という夢`];
  if (mood) parts.push(`気分は「${mood}」`);
  if (Array.isArray(people) && people.length) parts.push(`登場した人は「${people.join("・")}」`);
  if (Array.isArray(colors) && colors.length) parts.push(`印象的な色は「${colors.join("・")}」`);
  return `${parts.join("、")}。夢の場面・気分・色を合わせて見ると、今の心が何に反応しているかを読み解く材料になります。`;
}

function fallbackSignMessage(reading) {
  const words = (reading.keywords || []).slice(0, 3).map(k => k.word).filter(Boolean);
  if (!words.length) return "この夢のサインは、まだ言葉にしきれていない心の反応を、やわらかく知らせるものです。焦って答えを出すより、気になった場面を一つずつ思い出すことで意味が見えてきます。";
  return `この夢では「${words.join("」「")}」が主なサインです。これらは出来事そのものより、今のあなたが何を守りたいか、どこで安心を求めているかを映しています。`;
}

function buildFixedDiagnosis({ dream, mood, people, colors }) {
  const text = [dream, mood, ...(people || []), ...(colors || [])].join(" ").toLowerCase();
  const scoreSymbol = (symbol) => {
    let score = 0;
    const words = [symbol.name, symbol.nameEn, ...(symbol.keywords || [])].filter(Boolean);
    for (const word of words) {
      const w = String(word).toLowerCase();
      if (w && text.includes(w)) score += w.length >= 2 ? 3 : 1;
    }
    return score;
  };
  const symbols = window.STICKERS || [];
  const ranked = symbols
    .map(symbol => ({ symbol, score: scoreSymbol(symbol) }))
    .sort((a, b) => b.score - a.score);
  const primary = (ranked.find(item => item.score > 0)?.symbol || getDreamSymbol("moon-crescent"));
  const secondary = ranked.find(item => item.score > 0 && item.symbol.id !== primary.id)?.symbol || null;
  const primaryMeta = getDreamSymbolMeta(primary.id);
  const fallbackKeywords = [primary, secondary, getDreamSymbol("moon-crescent")]
    .filter(Boolean)
    .filter((symbol, index, arr) => arr.findIndex(s => s.id === symbol.id) === index)
    .slice(0, 3)
    .map(symbol => ({
      word: symbol.name,
      symbol_id: symbol.id,
      meaning: `${symbol.name}は、この夢の中で印象に残った固定シンボルです。詳しい受け止め方は診断文でやさしく補います。`,
    }));
  return {
    primary_symbol: primary.id,
    secondary_symbol: secondary?.id || "",
    dream_summary: fallbackDreamSummary({ dream, mood, people, colors }),
    type_description: `${primaryMeta.nameJp}のカードは、この夢でいちばん目立つ象徴として選ばれました。ここでは意味を断定せず、今の気持ちをやさしく整理するための手がかりとして受け取ってください。`,
    keywords: fallbackKeywords,
  };
}

// ========== InputForm ==========
const DREAM_MAX_CHARS = 500;

function InputForm({ onSubmit, initialData }) {
  const [dream, setDream] = useState((initialData?.dream || "").slice(0, DREAM_MAX_CHARS));
  const [mood, setMood] = useState(initialData?.mood || null);
  const [people, setPeople] = useState(initialData?.people || []);
  const [colors, setColors] = useState(initialData?.colors || []);
  const [shaking, setShaking] = useState(false);
  const taRef = 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 submit = () => {
    if (dream.trim().length < 10) {
      setShaking(true);
      setTimeout(() => setShaking(false), 350);
      taRef.current?.focus();
      return;
    }
    onSubmit({ dream: dream.trim().slice(0, DREAM_MAX_CHARS), mood, people, colors });
  };

  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}
          maxLength={DREAM_MAX_CHARS}
          onChange={(e) => setDream(e.target.value.slice(0, DREAM_MAX_CHARS))}
        />
        <div className={`char-count ${dream.length >= DREAM_MAX_CHARS ? "limit" : ""}`}>{dream.length}/{DREAM_MAX_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="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 symbolMeta = getDreamSymbolMeta(reading.primary_symbol);
  const cardImage = symbolMeta.image;
  const primary = getDreamSymbol(reading.primary_symbol);
  const secondary = getDreamSymbol(reading.secondary_symbol);
  const hasImage = !!cardImage;
  return (
    <div className="tarot-stage">
      <div className={`tarot generated-card ${hasImage ? "has-image" : ""}`} style={cardImage ? { backgroundImage: `url(${cardImage})`, backgroundSize: "cover", backgroundPosition: "center", backgroundRepeat: "no-repeat" } : undefined}>
        {cardImage && <img className="tarot-generated-bg" src={cardImage} alt={`${symbolMeta.nameJp}のカード`}/>}
        {cardImage && <div className="tarot-direct-symbol" style={{ backgroundImage: `url(${cardImage})` }} aria-hidden="true"></div>}
        <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 || symbolMeta.typeCode}</span>
        </div>

        <div className="tarot-roman">{symbolMeta.no}</div>



        <div className="tarot-name-en">{reading.card_name_en || symbolMeta.nameEn || "Dream Symbol"}</div>
        <div className="tarot-name-jp">{reading.card_name_jp || symbolMeta.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="result-detail-grid">
        <div className="card result-detail-card type-meaning-card">
          <div className="detail-label">{reading.card_name_jp}のサインとは</div>
          <div className="detail-text">{reading.type_description}</div>
        </div>
      </div>

      <div className="card sign-message-card">
        <div className="detail-label">夢が告げるサイン</div>
        <div className="detail-text">{reading.sign_message}</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="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 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>

      <LineRegisterBlock />
    </div>
  );
}

function LineRegisterBlock() {
  const lineUrl = getLineFriendUrl();
  const disabled = !lineUrl;
  const openLine = () => {
    if (!lineUrl) return;
    window.open(lineUrl, "_blank", "noopener,noreferrer");
  };
  return (
    <section className="line-register-block">
      <div className="line-register-glow"></div>
      <div className="line-register-head">
        <img className="line-register-icon" src="uploads/line-miraizu-icon.png" alt="ミライズ公式LINE" />
        <div>
          <div className="line-register-badge">公式LINE</div>
          <h2>ミライズで、明日のサインも受け取る</h2>
        </div>
      </div>
      <p>診断結果の続きや、毎朝の占い・夢のサイン・開運メッセージをLINEで受け取れます。</p>
      <div className="line-register-benefits">
        <span>毎朝の運勢</span>
        <span>夢の見返し</span>
        <span>開運アドバイス</span>
      </div>
      <button className="line-register-button" onClick={openLine} disabled={disabled}>
        <span className="line-logo-dot">LINE</span>
        <strong>{disabled ? "公式LINE準備中" : "ミライズを友だち追加"}</strong>
      </button>
      <div className="line-register-note">{disabled ? "登録URLを設定すると、このボタンから公式LINEへ誘導できます。" : "LINEアプリが開きます。通知はいつでも停止できます。"}</div>
    </section>
  );
}

function HomeLanding({ onStart }) {
  return (
    <section className="home-landing card">
      <div className="home-kicker">AI DREAM READING</div>
      <h2>夢の内容から、今の気持ちをやさしく整理します</h2>
      <p>
        怖い夢や不思議な夢を、不吉な意味として決めつけず、
        「今なにが不安なのか」「今日は何を一つ整えると楽になるか」に置き換えて診断します。
      </p>
      <div className="home-card-strip" aria-label="夢診断カードの例">
        {HOME_DREAM_CARDS.map(card => (
          <article className="home-dream-card" key={card.roman}>
            <img src={card.image} alt={`${card.nameJp}のカード`} loading="lazy" />
            <div className="home-dream-card-meta">
              <span>{card.roman}</span>
              <strong>{card.nameJp}</strong>
            </div>
          </article>
        ))}
      </div>
      <div className="home-points">
        <span>37種類の夢シンボルカード</span>
        <span>1日3回まで無料診断</span>
        <span>SNSログインで利用確認</span>
      </div>
      <button className="btn-divine home-start" onClick={onStart}>夢を診断する</button>
      <p className="home-note">入力内容と診断結果は、診断履歴・サービス改善のため保存されます。</p>
    </section>
  );
}

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

【役割】
あなたの役割は「夢の分析を当てにいくこと」ではありません。
サイト側で決まっている夢診断の構造・カード・象徴をもとに、ユーザーが安心できるように、やさしく寄り添った言葉へ整えることです。

ゴールは次の2つです。
1. ユーザーに「怖い夢にも、自分を責めなくていい意味がある」と感じてもらう。
2. 今日を少し前向きに過ごせるよう、背中を押す。

【診断文の基本方針】
- 分析結果は固定情報として扱う。過度に新しい解釈を足さない。
- 夢の内容を、不吉・予言・運命として断定しない。
- ユーザーの不安を増やさない。怖がらせない。依存させない。
- 「あなたはこうです」と決めつけず、「そう感じていたのかもしれません」「少し疲れていた可能性があります」のように柔らかく言う。
- 文章の中心は、分析の正しさではなく、安心・自己受容・小さな前進。
- 夢の象徴は、専門用語ではなく日常の言葉に翻訳する。
- 最後は必ず、今日できる小さな行動や気持ちの置き方で背中を押す。

【LPノウハウ由来の文章設計】
占いジャンルの文章は「当てる」より「誰にも言えない不安を安全に整理できる」ことが大切です。
文章の流れは、次を守ってください。

曖昧な不安をやさしく言語化する
→ 夢の具体場面に軽く触れる
→ それを日常の気持ちへ翻訳する
→ ユーザーを責めずに安心させる
→ 今日できる小さな一歩で背中を押す

【文章トーン】
- 友達や相談相手に近い、穏やかな言葉。
- 説教しない。
- 鑑定士っぽく言い切りすぎない。
- ポエムに逃げない。
- 「つまり、今どうすれば少し楽になるか」が分かる文章にする。

【禁止表現】
- 未来断定: 必ず起きます、運命です、復縁できます、成功します。
- 不安煽り: このままだと危険です、見逃すと悪化します。
- スピリチュアル過多: 魂、運命、内なる光、覚醒、浄化、宇宙、心の鍵穴。
- 意味が曖昧な抽象語の連発: 流れ、整う、開く、導き、サインです、映しています。
- 象徴辞典のような説明だけで終わる文章。

【OKの方向性】
- 「この夢は、何かを決める前にもう少し確認したい気持ちがある時に見やすい夢です。」
- 「怖い夢だったとしても、あなたを責める夢ではありません。心が少し立ち止まって、整理する時間を欲しがっていたのかもしれません。」
- 「今日は大きな結論を急がず、気になっていることを一つだけメモしてみてください。それだけでも少し落ち着きやすくなります。」

【NGの方向性】
- 「鍵は秘密・確認・開けたい扉を表します。」だけで終わる。
- 「あなたは自分で選び直したい気持ちが強くなっています。」と断定だけする。
- 「扉を急いで開けるより、鍵が合うかを確かめるように」など、比喩が続いて現実の行動が分からない。

夢の内容に登場する物・場所・色・人物を象徴として分析し、感情メタファーとして解釈してください。
ユーザーのヒアリング内容（夢本文、気分、登場人物、色）は必ず診断文のどこかに具体的に含めてください。未入力の項目は無理に作らず、入力された項目を優先してください。
夢本文の中に「赤い部屋」「青い空」「白い光」「暗い背景」「夕焼け」「黒い影」のような色・背景・明暗・光の記載がある場合は、色選択肢と重複していても無視せず、本文側の文脈を優先して診断材料にしてください。
選択肢の色・人物・気分は補助情報です。夢本文と選択肢に重複がある場合は、重複として捨てず「夢本文にも選択にも現れた強い象徴」として扱い、ただし同じ内容を文章上で不自然に二重説明しないでください。
表示崩れを避けるため、各項目の文字数目安を必ず守ってください。

【利用者の夢】
${dream}

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

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

【固定診断結果（サイト側で決める。DEEPSEEKは変更しない）】
${fixedDiagnosisForPrompt}

【37種類の夢シンボルカード】
夢に最も強く出ている中心シンボルを「利用可能な象徴ID」から1つ選び、primary_symbolに入れてください。結果画面には uploads/symbol-cards/ の37種類シンボルカードのうち、primary_symbolに対応するカード画像が大きく表示されます。補助シンボルはsecondary_symbolとkeywordsに入れてください。type_code / dream_type_jp / card_name_jp / card_name_en / roman はサイト側で中心シンボルから固定表示します。

【結果ページとの対応 / 出力内容の順番】
DEEPSEEKが考えるのは次の6項目だけです。他の項目（カード、中心シンボル、夢のまとめ、象徴リスト、カード説明）は固定診断結果を使います。

1. DREAM ANALYSIS = main_message:
   - 固定診断結果をもとに、ユーザーへ寄り添う中心メッセージを書く。
   - 3文構成。気持ちの言語化 → 夢の場面の受け止め → 安心と小さな一歩。
2. 夢が告げるサイン = sign_message:
   - 夢から受け取れる、安心できるメッセージを書く。
   - 今日、何を急がなくてよいか / 何を一つやればよいかを入れる。
3. 恋愛 = themes.love:
   - 入力情報が少ない場合は断定せず、相手を操作する助言にしない。
   - 安心できる見方と、今日できる小さな行動を書く。
4. 仕事 = themes.work:
   - 責めない。頑張れで終わらせない。
   - 今日一つだけ進めるなら何かを書く。
5. 対人 = themes.relations:
   - 距離感をやさしく整える言葉にする。
   - 返信・相談・断り方など、日常の小さな行動へ落とす。
6. 今日のアドバイス = advice:
   - 最後に読む背中押しの言葉。
   - 大丈夫、急がなくていい、一つだけやればいいという安心感を入れる。

【出力ルール】
- 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文字程度にする。

【出力フォーマット】
{
  "main_message": "（DREAM ANALYSIS。150〜220字。3文構成。固定診断結果をもとに、夢から読み取れる気持ちをやわらかく言語化し、安心と今日の小さな一歩で背中を押す）",
  "sign_message": "（夢が告げるサイン。90〜140字。今日何を急がなくてよいか、何を一つやればよいかをやさしく伝える）",
  "themes": {
    "love":      { "score": 3, "text": "（恋愛・愛情面。55〜85字。断定せず、安心できる見方と今日の小さな行動を書く）" },
    "work":      { "score": 3, "text": "（仕事・学び・行動面。55〜85字。責めずに、今日一つだけ進める行動を書く）" },
    "relations": { "score": 3, "text": "（対人関係。55〜85字。距離感をやさしく整える言葉と小さな行動を書く）" }
  },
  "advice": "（今日のアドバイス。80〜120字。最後に読む背中押しの言葉。大丈夫、急がなくていい、一つだけやればいいという安心感を含める）"
}

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, fixedDiagnosis, social_auth_provider: socialAuth?.provider || "" })
    });
    const data = await response.json().catch(() => ({}));
    if (!response.ok) {
      if (response.status === 429 && data.reason === "account_limit") {
        const limitError = new Error("今日の無料診断は3回までです。明日また、気になった夢を聞かせてください。");
        limitError.noModelFallback = true;
        throw limitError;
      }
      if (response.status === 429 && data.reason === "site_daily_limit") {
        const limitError = new Error("今日は診断が混み合っています。少し時間をおいてから、もう一度試してください。");
        limitError.noModelFallback = true;
        throw limitError;
      }
      if (response.status === 429 && data.reason === "deepseek_generation_limit") {
        const limitError = new Error("今日は夢分析の生成枠がいっぱいです。明日また試してください。");
        limitError.noModelFallback = true;
        throw limitError;
      }
      if (response.status === 401) {
        const authError = new Error("診断するにはログインが必要です。Google・X・Instagramのいずれかでログインしてください。");
        authError.noModelFallback = true;
        throw authError;
      }
      throw new Error("api failed");
    }
    if (data.fixedDiagnosis) {
      fixedDiagnosis = data.fixedDiagnosis;
      fixedDiagnosisForPrompt = JSON.stringify(fixedDiagnosis, null, 2);
    }
    raw = data.text;
    if (!raw) throw new Error("empty result");
  } catch (e) {
    if (e?.noModelFallback) throw 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("結果が読み取れませんでした");
  }

  // Fixed analysis fields are owned by the site, not by the model.
  parsed.primary_symbol = fixedDiagnosis.primary_symbol;
  parsed.secondary_symbol = fixedDiagnosis.secondary_symbol;
  const symbolMeta = getDreamSymbolMeta(parsed.primary_symbol);
  parsed.roman = symbolMeta.no;
  parsed.type_code = symbolMeta.typeCode;
  parsed.dream_type_jp = symbolMeta.typeJp;
  parsed.card_name_jp = symbolMeta.nameJp;
  parsed.card_name_en = symbolMeta.nameEn;
  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.dream_summary = fixedDiagnosis.dream_summary;
  parsed.type_description = fixedDiagnosis.type_description;
  parsed.themes = parsed.themes || {};
  ["love", "work", "relations"].forEach(k => {
    if (!parsed.themes[k]) parsed.themes[k] = { score: 3, text: "" };
  });
  parsed.keywords = fixedDiagnosis.keywords;
  parsed.sign_message = parsed.sign_message || fallbackSignMessage(parsed);
  parsed.advice = parsed.advice || "今日は予定を一つ減らし、静かに考えを整理する時間を作ってください。夢に出た小さなサインは、心を休ませる必要を示しています。";
  parsed.share_summary = parsed.share_summary || parsed.main_message || "夢に現れた象徴から、今日の心の流れを読み解きました。";
  return parsed;
}

// ========== App ==========
function App() {
  const [phase, setPhase] = useState("home"); // home | 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 [socialAuth, setSocialAuth] = useState(null);
  const [authProviders, setAuthProviders] = useState(SOCIAL_LOGIN_PROVIDERS.map(p => ({ ...p, enabled: false })));
  const [loginOpen, setLoginOpen] = useState(false);
  const shareRef = useRef(null);
  const date = useMemo(() => todayInfo(), []);
  const refreshAuth = async () => {
    try {
      const config = await fetchAuthConfig();
      const mergedProviders = SOCIAL_LOGIN_PROVIDERS.map(provider => ({
        ...provider,
        ...(config.providers || []).find(item => item.id === provider.id),
      }));
      setAuthProviders(mergedProviders);
      window.__DREAM_PREVIEW_LOGIN_ENABLED__ = !!config.preview_login_enabled;
      window.__DREAM_PREVIEW_LOGGED_IN__ = !!config.user;
      setSocialAuth(config.user || null);
      if (config.user) {
        try {
          if (sessionStorage.getItem("yumeshindan_after_login") === "input") {
            sessionStorage.removeItem("yumeshindan_after_login");
            setPhase("input");
          }
        } catch (e) {}
      }
    } catch (e) {
      setAuthProviders(SOCIAL_LOGIN_PROVIDERS.map(p => ({ ...p, enabled: false })));
      window.__DREAM_PREVIEW_LOGIN_ENABLED__ = true;
      window.__DREAM_PREVIEW_LOGGED_IN__ = false;
      setSocialAuth(null);
    }
  };

  useEffect(() => {
    refreshAuth();
  }, []);

  const startSocialLogin = (provider) => {
    try { sessionStorage.setItem("yumeshindan_after_login", "input"); } catch (e) {}
    window.location.href = `/api/auth/start?provider=${encodeURIComponent(provider.id)}`;
  };

  const startPreviewLogin = async () => {
    try {
      const response = await fetch('/api/auth/preview', { method: 'POST' });
      if (!response.ok) throw new Error('preview login failed');
      window.__DREAM_PREVIEW_LOGGED_IN__ = true;
      await refreshAuth();
      setLoginOpen(false);
      const saved = pendingInput || JSON.parse(sessionStorage.getItem("yumeshindan_pending_input") || "null");
      if (saved?.dream) setTimeout(() => onSubmit(saved), 80);
      else setPhase("input");
    } catch (e) {
      setError("プレビューログインに失敗しました");
      setPhase("error");
      setLoginOpen(false);
    }
  };

  const logoutSocial = async () => {
    try { await fetch('/api/auth/logout', { method: 'POST' }); } catch (e) {}
    try { sessionStorage.removeItem("yumeshindan_after_login"); } catch (e) {}
    window.__DREAM_PREVIEW_LOGGED_IN__ = false;
    setSocialAuth(null);
    await refreshAuth();
  };

  const startDiagnosisFromHome = () => {
    if (socialAuth || window.__DREAM_PREVIEW_LOGGED_IN__) {
      setPhase("input");
      window.scrollTo({ top: 0, behavior: "smooth" });
      return;
    }
    try { sessionStorage.setItem("yumeshindan_after_login", "input"); } catch (e) {}
    setLoginOpen(true);
  };

  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 images = [...node.querySelectorAll("img")];
      await Promise.all(images.map(img => img.complete && img.naturalWidth > 0 ? true : new Promise(resolve => {
        img.onload = resolve;
        img.onerror = resolve;
        setTimeout(resolve, 3000);
      })));
      await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
      const canvas = await window.html2canvas(node, {
        backgroundColor: null,
        scale: 1,
        useCORS: true,
        logging: false,
        width: 1200,
        height: 675,
        windowWidth: 1200,
        windowHeight: 675,
      });
      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_x_${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 postToX = async () => {
    const clipText = (value, max) => {
      const text = String(value || "").trim();
      return text.length > max ? `${text.slice(0, max)}…` : text;
    };
    const dreamLine = clipText(reading?._dream || "今朝見た夢", 28);
    const typeName = reading?.dream_type_jp || "夢タイプ";
    const summaryLine = clipText(reading?.share_summary || reading?.main_message || "夢に出たしるしが、今の心を映していた。", 44);
    const buzzText = `夢をAI夢診断にかけたら、思ったより刺さった。

「${dreamLine}」
診断：${typeName}
${summaryLine}

自分の夢も見てほしい人は試してみて。
https://miniappliquidus.duckdns.org/
#今朝の夢診断 #夢占い`;
    const text = encodeURIComponent(buzzText);
    if (shareImg && shareImg !== "error") {
      try {
        const blob = await (await fetch(shareImg)).blob();
        if (navigator.share && navigator.canShare && navigator.canShare({ files: [new File([blob], `yumeshindan_x_${date.iso}.png`, { type: "image/png" })] })) {
          const file = new File([blob], `yumeshindan_x_${date.iso}.png`, { type: "image/png" });
          await navigator.share({ files: [file], text: buzzText });
          return;
        }
      } catch (e) {}
    }
    downloadShareImage();
    window.open(`https://twitter.com/intent/tweet?text=${text}`, "_blank", "noopener,noreferrer");
  };


  const onSubmit = async (data) => {
    if (!socialAuth && !window.__DREAM_PREVIEW_LOGGED_IN__) {
      setPendingInput(data);
      try { sessionStorage.setItem("yumeshindan_pending_input", JSON.stringify(data)); } catch (e) {}
      setLoginOpen(true);
      return;
    }
    setPendingInput(data);
    try {
      sessionStorage.setItem("yumeshindan_pending_input", JSON.stringify(data));
    } catch (e) {}
    setPhase("loading");
    setError(null);
    try {
      const result = await divine({ ...data, socialAuth });
      result._dream = data.dream || "";
      result._mood = data.mood || "";
      result._people = data.people || [];
      result._colors = data.colors || [];
      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 continueAfterLogin = () => {
    setLoginOpen(false);
    if (pendingInput) {
      setTimeout(() => onSubmit(pendingInput), 60);
      return;
    }
    try {
      const saved = JSON.parse(sessionStorage.getItem("yumeshindan_pending_input") || "null");
      if (saved?.dream) {
        setTimeout(() => onSubmit(saved), 60);
        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>

      <AuthStatusBar socialAuth={socialAuth} onLogout={logoutSocial} onLogin={() => setLoginOpen(true)} providers={authProviders}/>

      {phase === "home" && <HomeLanding onStart={startDiagnosisFromHome}/>} 
      {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>
      )}

      <SocialLoginGate
        open={loginOpen}
        onClose={() => setLoginOpen(false)}
        socialAuth={socialAuth}
        providers={authProviders}
        onLogin={startSocialLogin}
        onPreviewLogin={startPreviewLogin}
        onContinue={continueAfterLogin}
        onLogout={logoutSocial}
      />

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


function AuthStatusBar({ socialAuth, onLogout }) {
  const loggedIn = !!socialAuth || !!window.__DREAM_PREVIEW_LOGGED_IN__;
  if (!loggedIn) return null;
  const label = socialAuth?.display_name || socialAuth?.username || "ゲスト";
  return (
    <div className="auth-status-bar is-logged-in">
      <span>{label}でログイン中</span>
      <button onClick={onLogout}>ログアウト</button>
    </div>
  );
}


function SocialLoginGate({ open, onClose, socialAuth, providers, onLogin, onPreviewLogin, onContinue, onLogout }) {
  if (!open) return null;
  const providerListRaw = providers?.length ? providers : SOCIAL_LOGIN_PROVIDERS.map(p => ({ ...p, enabled: false }));
  const anyEnabled = providerListRaw.some(provider => provider.enabled);
  const providerList = anyEnabled ? providerListRaw.filter(provider => provider.enabled) : providerListRaw;
  const previewEnabled = !!window.__DREAM_PREVIEW_LOGIN_ENABLED__;
  return (
    <div className="login-backdrop" onClick={onClose}>
      <div className="login-modal" onClick={(e) => e.stopPropagation()}>
        <div className="login-orb">✦</div>
        <div className="login-kicker">LOGIN REQUIRED</div>
        <h3>{previewEnabled && !anyEnabled ? "ゲストログインで診断できます" : "診断にはログインが必要です"}</h3>
        <p>{previewEnabled && !anyEnabled ? "現在はゲストログインで診断できます。Googleログインなどは準備ができ次第、ここから選べるようになります。" : `夢診断を始めるには、${providerList.map(provider => provider.label).join("・")} のいずれかでログインしてください。`}</p>
        {socialAuth ? (
          <div className="login-current-wrap">
            <div className="login-current">
              <span>{socialAuth.label || socialAuth.provider || "プレビュー"}でログイン済み</span>
              <button onClick={onLogout}>ログアウト</button>
            </div>
            <button className="login-continue" onClick={onContinue}>診断結果を見る</button>
          </div>
        ) : (
          <div className="login-provider-list">
            {previewEnabled && !anyEnabled && (
              <button className="login-provider preview" onClick={onPreviewLogin}>
                <span className="login-provider-mark">✦</span>
                <span>ゲストログインして診断する</span>
                <em>現在利用できます</em>
              </button>
            )}
            {providerList.map(provider => {
              const enabled = !!provider.enabled;
              return (
                <button key={provider.id} className={`login-provider ${provider.id}`} disabled={!enabled} onClick={() => onLogin(provider)}>
                  <span className="login-provider-mark">{provider.mark}</span>
                  <span>{provider.handle}でログイン</span>
                  {!enabled && <em>準備中</em>}
                </button>
              );
            })}
          </div>
        )}
        <div className="login-note">※ ログイン情報は診断利用確認と回数制限のために使います。パスワードやDM内容は取得しません。ゲストログインはSNS情報を保存しません。</div>
        <button className="login-close" onClick={onClose}>閉じる</button>
      </div>
    </div>
  );
}

// ========== ShareModal ==========
function ShareModal({ img, busy, onClose, onDownload, onCopy, onPostX }) {
  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={onPostX} disabled={busy || !img || img === "error"}>Xに投稿する</button>
          <button 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;
