// views.jsx — Echo Views
// 全文置換版：Inbox / Timeline / Ripen / Settings
// 目的：Timeline上の着想カードを予定と明確に区別する

// =========================
// Common helpers
// =========================

function safeText(v, fallback = '') {
  return v == null ? fallback : String(v);
}

function formatDayLabel(d) {
  const x = new Date(d);
  const w = ['日', '月', '火', '水', '木', '金', '土'][x.getDay()];
  return `${x.getMonth() + 1}/${x.getDate()}(${w})`;
}

function startOfDayLocal(d) {
  const x = new Date(d);
  x.setHours(0, 0, 0, 0);
  return x;
}

function endOfDayLocal(d) {
  const x = startOfDayLocal(d);
  x.setDate(x.getDate() + 1);
  return x;
}

function addDaysLocal(d, n) {
  const x = new Date(d);
  x.setDate(x.getDate() + n);
  return x;
}

function startOfWeekLocal(d) {
  const x = startOfDayLocal(d);
  x.setDate(x.getDate() - x.getDay());
  return x;
}

function sameDayLocal(a, b) {
  return startOfDayLocal(a).getTime() === startOfDayLocal(b).getTime();
}

function dateKeyLocal(d) {
  const x = new Date(d);
  return `${x.getFullYear()}-${String(x.getMonth() + 1).padStart(2, '0')}-${String(x.getDate()).padStart(2, '0')}`;
}

function timeLabel(ts) {
  try {
    if (typeof formatTime === 'function') return formatTime(ts);
  } catch (e) {}
  const d = new Date(ts);
  return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}

function relLabel(ts) {
  try {
    if (typeof relTime === 'function') return relTime(ts);
  } catch (e) {}

  const diff = Date.now() - ts;
  const min = Math.floor(diff / 60000);
  if (min < 1) return '今';
  if (min < 60) return `${min}分前`;
  const h = Math.floor(min / 60);
  if (h < 24) return `${h}時間前`;
  const d = Math.floor(h / 24);
  return `${d}日前`;
}

function splitTitleAndBody(content) {
  const text = safeText(content).trim();
  if (!text) return { title: '', body: '' };

  const firstNl = text.indexOf('\n');
  if (firstNl > 0 && firstNl < 64) {
    return {
      title: text.slice(0, firstNl).trim(),
      body: text.slice(firstNl + 1).trim(),
    };
  }

  if (text.length <= 42) {
    return { title: text, body: '' };
  }

  const head = text.slice(0, 56);
  const m = head.match(/[。！？.!?]/);
  const cut = m ? head.indexOf(m[0]) + 1 : 42;

  return {
    title: text.slice(0, cut).trim(),
    body: text.slice(cut).trim(),
  };
}

function getNoteTone(note, t) {
  const type = note && note.type ? note.type : 'memo';

  const tones = {
    idea: {
      label: 'IDEA',
      short: '着想',
      bg: '#FFB020',
      bgSoft: '#FFF1D6',
      border: '#8A4B00',
      text: '#241300',
      dot: '#FFB020',
      shadow: '0 0 0 2px #fff, 0 8px 18px rgba(138,75,0,0.35)',
    },
    memo: {
      label: 'MEMO',
      short: '断片',
      bg: '#111827',
      bgSoft: '#E5E7EB',
      border: '#000000',
      text: '#FFFFFF',
      dot: '#111827',
      shadow: '0 0 0 2px #fff, 0 8px 18px rgba(0,0,0,0.28)',
    },
    task: {
      label: 'STEP',
      short: '一歩',
      bg: '#00A896',
      bgSoft: '#D8FFF8',
      border: '#00695C',
      text: '#FFFFFF',
      dot: '#00A896',
      shadow: '0 0 0 2px #fff, 0 8px 18px rgba(0,105,92,0.32)',
    },
    output: {
      label: 'OUT',
      short: '出力',
      bg: '#7C3AED',
      bgSoft: '#EDE9FE',
      border: '#4C1D95',
      text: '#FFFFFF',
      dot: '#7C3AED',
      shadow: '0 0 0 2px #fff, 0 8px 18px rgba(76,29,149,0.32)',
    },
    outcome: {
      label: 'WIN',
      short: '成果',
      bg: '#22C55E',
      bgSoft: '#DCFCE7',
      border: '#166534',
      text: '#062B12',
      dot: '#22C55E',
      shadow: '0 0 0 2px #fff, 0 8px 18px rgba(22,101,52,0.30)',
    },
  };

  return tones[type] || tones.memo;
}

function SectionCard({ children, style }) {
  const t = useT();
  return (
    <section
      style={{
        background: t.bgCard,
        border: `1px solid ${t.line}`,
        borderRadius: 14,
        padding: 16,
        fontFamily: APP_FONT,
        ...style,
      }}
    >
      {children}
    </section>
  );
}

function EmptyState() {
  const t = useT();
  return (
    <div
      style={{
        padding: '48px 20px',
        textAlign: 'center',
        color: t.ink4,
        fontFamily: APP_FONT,
      }}
    >
      <div style={{ fontSize: 28, marginBottom: 10 }}>＋</div>
      <div style={{ fontSize: 14, fontWeight: 700, color: t.ink2 }}>
        まだ何も投下されていない
      </div>
      <div style={{ fontSize: 12, marginTop: 6, lineHeight: 1.6 }}>
        右下の「＋」から、今ある思考をそのまま投げる。
      </div>
    </div>
  );
}

// =========================
// Inbox / Flow
// =========================

function InboxView({ notes = [], onUpdate, onDelete, onGoRipen }) {
  const t = useT();

  const active = notes
    .filter(n => n.status !== 'resting')
    .slice()
    .sort((a, b) => b.ts - a.ts);

  const resting = notes
    .filter(n => n.status === 'resting')
    .slice()
    .sort((a, b) => (b.restedAt || b.ts) - (a.restedAt || a.ts));

  const echoed = notes.filter(n => n.echo || n.echoedSimilarity != null).length;

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
      <SectionCard
        style={{
          background: `linear-gradient(135deg, ${t.bgCard}, ${t.primarySoft || t.bgSoft})`,
          border: `1px solid ${t.primary}33`,
        }}
      >
        <div
          style={{
            fontSize: 10,
            fontWeight: 900,
            letterSpacing: '0.16em',
            color: t.primary,
            marginBottom: 6,
          }}
        >
          FLOW
        </div>
        <div
          style={{
            fontSize: 20,
            fontWeight: 900,
            letterSpacing: '-0.04em',
            color: t.ink,
            lineHeight: 1.25,
          }}
        >
          思考を投げる。あとで返ってくる。
        </div>
        <div
          style={{
            fontSize: 12.5,
            color: t.ink3,
            lineHeight: 1.65,
            marginTop: 8,
          }}
        >
          整理しなくていい。今の断片を投下して、Echoが過去の自分との接続を見つける。
        </div>

        <div
          style={{
            display: 'flex',
            gap: 8,
            flexWrap: 'wrap',
            marginTop: 14,
          }}
        >
          <MiniStat label="投下" value={notes.length} />
          <MiniStat label="Echo" value={echoed} />
          <MiniStat label="寝かせ中" value={resting.length} />
        </div>
      </SectionCard>

      {active.length > 0 ? (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {active.map(n => (
            <NoteCard
              key={n.id}
              note={n}
              onUpdate={onUpdate}
              onDelete={onDelete}
            />
          ))}
        </div>
      ) : (
        <EmptyState />
      )}

      {resting.length > 0 && (
        <RestingFold
          items={resting}
          onUpdate={onUpdate}
          onDelete={onDelete}
          onGoRipen={onGoRipen}
        />
      )}
    </div>
  );
}

function MiniStat({ label, value }) {
  const t = useT();
  return (
    <div
      style={{
        background: t.bgCard + 'cc',
        border: `1px solid ${t.line}`,
        borderRadius: 999,
        padding: '6px 10px',
        display: 'inline-flex',
        alignItems: 'center',
        gap: 6,
        fontFamily: APP_FONT,
      }}
    >
      <span style={{ fontSize: 10, color: t.ink4, fontWeight: 700 }}>
        {label}
      </span>
      <span style={{ fontSize: 12, color: t.ink, fontWeight: 900 }}>
        {value}
      </span>
    </div>
  );
}

function RestingFold({ items = [], onUpdate, onDelete, onGoRipen }) {
  const t = useT();
  const [open, setOpen] = React.useState(false);

  return (
    <section style={{ marginTop: 4 }}>
      <button
        onClick={() => setOpen(!open)}
        style={{
          width: '100%',
          background: 'transparent',
          border: 'none',
          padding: '10px 2px',
          cursor: 'pointer',
          fontFamily: APP_FONT,
          display: 'flex',
          alignItems: 'center',
          gap: 8,
          color: t.ink4,
          fontSize: 12,
          fontWeight: 700,
          WebkitTapHighlightColor: 'transparent',
        }}
      >
        <span
          style={{
            display: 'inline-block',
            transform: open ? 'rotate(90deg)' : 'rotate(0deg)',
            transition: 'transform 0.15s',
            fontSize: 10,
          }}
        >
          ▶
        </span>
        <span>寝かせ中 {items.length}</span>
        <span style={{ fontWeight: 500 }}>· 今じゃないもの</span>
        <span style={{ flex: 1 }} />
        {onGoRipen && (
          <span
            onClick={(e) => {
              e.stopPropagation();
              onGoRipen();
            }}
            style={{
              color: t.primary,
              fontWeight: 800,
            }}
          >
            Ripenへ
          </span>
        )}
      </button>

      {open && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 8 }}>
          {items.map(n => (
            <NoteCard
              key={n.id}
              note={n}
              onUpdate={onUpdate}
              onDelete={onDelete}
            />
          ))}
        </div>
      )}
    </section>
  );
}

// =========================
// Timeline
// =========================

function TimelineView({ notes = [], events = [], onUpdate, onDelete }) {
  const [mode, setMode] = React.useState(() => {
    try {
      const saved = localStorage.getItem('echo.timelineMode') || 'day';
      return saved === 'month' ? 'week' : saved;
    } catch (e) {
      return 'day';
    }
  });

  const [focusDate, setFocusDate] = React.useState(new Date());

  const changeMode = (m) => {
    setMode(m);
    try {
      localStorage.setItem('echo.timelineMode', m);
    } catch (e) {}
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      <TimelineTopBar
        mode={mode}
        setMode={changeMode}
        focusDate={focusDate}
        setFocusDate={setFocusDate}
      />

      {mode === 'day' && (
        <TimelineDayView
          notes={notes}
          events={events}
          focusDate={focusDate}
          setFocusDate={setFocusDate}
          onUpdate={onUpdate}
          onDelete={onDelete}
        />
      )}

      {mode === 'week' && (
        <TimelineWeekView
          notes={notes}
          events={events}
          focusDate={focusDate}
          setFocusDate={setFocusDate}
          onUpdate={onUpdate}
          onDelete={onDelete}
        />
      )}
    </div>
  );
}

function TimelineTopBar({ mode, setMode, focusDate, setFocusDate }) {
  const t = useT();

  const btn = (label, onClick, active) => (
    <button
      onClick={onClick}
      style={{
        border: active ? 'none' : `1px solid ${t.line}`,
        background: active ? t.primary : t.bgCard,
        color: active ? '#fff' : t.ink3,
        borderRadius: 9,
        padding: '7px 11px',
        fontFamily: APP_FONT,
        fontSize: 12,
        fontWeight: 800,
        cursor: 'pointer',
        WebkitTapHighlightColor: 'transparent',
      }}
    >
      {label}
    </button>
  );

  return (
    <div
      style={{
        position: 'sticky',
        top: 64,
        zIndex: 20,
        background: t.bg + 'ee',
        backdropFilter: 'blur(12px)',
        WebkitBackdropFilter: 'blur(12px)',
        padding: '8px 0 10px',
        display: 'flex',
        flexDirection: 'column',
        gap: 10,
        fontFamily: APP_FONT,
      }}
    >
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        {btn('日', () => setMode('day'), mode === 'day')}
        {btn('週', () => setMode('week'), mode === 'week')}

        <div style={{ flex: 1 }} />

        <button
          onClick={() => setFocusDate(addDaysLocal(focusDate, -1))}
          style={smallNavBtn(t)}
        >
          ←
        </button>
        <button
          onClick={() => setFocusDate(new Date())}
          style={smallNavBtn(t, true)}
        >
          今へ
        </button>
        <button
          onClick={() => setFocusDate(addDaysLocal(focusDate, 1))}
          style={smallNavBtn(t)}
        >
          →
        </button>
      </div>

      <div
        style={{
          fontSize: 18,
          fontWeight: 900,
          letterSpacing: '-0.04em',
          color: t.ink,
        }}
      >
        {mode === 'day'
          ? formatDayLabel(focusDate)
          : `${formatDayLabel(startOfWeekLocal(focusDate))} – ${formatDayLabel(addDaysLocal(startOfWeekLocal(focusDate), 6))}`}
      </div>
    </div>
  );
}

function smallNavBtn(t, primary) {
  return {
    border: primary ? 'none' : `1px solid ${t.line}`,
    background: primary ? t.primary : t.bgCard,
    color: primary ? '#fff' : t.ink3,
    borderRadius: 9,
    padding: '7px 10px',
    fontFamily: APP_FONT,
    fontSize: 12,
    fontWeight: 800,
    cursor: 'pointer',
    WebkitTapHighlightColor: 'transparent',
  };
}

// 5分以内に投下されたメモを1グループにまとめる
const STACK_WINDOW_MS = 5 * 60 * 1000;

function groupSameTimeNotes(dayNotes) {
  const groups = [];
  for (const n of dayNotes) {
    const last = groups[groups.length - 1];
    if (last && (n.ts - last.notes[last.notes.length - 1].ts) <= STACK_WINDOW_MS) {
      last.notes.push(n);
    } else {
      groups.push({ ts: n.ts, notes: [n] });
    }
  }
  return groups;
}

function TimelineDayView({ notes = [], events = [], focusDate, setFocusDate, onUpdate, onDelete }) {
  // SwipeableView 内でレンダーされる中身。renderAt(date) として渡す。
  const renderDay = React.useCallback((date) => (
    <TimelineDayContent
      date={date}
      notes={notes}
      events={events}
      onUpdate={onUpdate}
      onDelete={onDelete}
    />
  ), [notes, events, onUpdate, onDelete]);

  return (
    <SwipeableView
      current={focusDate}
      onChange={setFocusDate}
      renderAt={renderDay}
      prevOf={(d) => addDaysLocal(d, -1)}
      nextOf={(d) => addDaysLocal(d, 1)}
      keyOf={(d) => startOfDayLocal(d).getTime()}
    />
  );
}

function TimelineDayContent({ date, notes, events, onUpdate, onDelete }) {
  const t = useT();

  const dayStart = startOfDayLocal(date).getTime();
  const dayEnd = endOfDayLocal(date).getTime();

  const dayNotes = notes
    .filter(n => n.ts >= dayStart && n.ts < dayEnd)
    .slice()
    .sort((a, b) => a.ts - b.ts);

  const dayEvents = events
    .filter(e => e.ts >= dayStart && e.ts < dayEnd)
    .slice()
    .sort((a, b) => a.ts - b.ts);

  // メモはスタックグループに、予定はそのまま単体
  const noteGroups = groupSameTimeNotes(dayNotes);

  const items = [
    ...dayEvents.map(e => ({ kind: 'event', ts: e.ts, data: e })),
    ...noteGroups.map(g => ({
      kind: g.notes.length > 1 ? 'stack' : 'note',
      ts: g.ts,
      data: g.notes.length > 1 ? g.notes : g.notes[0],
    })),
  ].sort((a, b) => a.ts - b.ts);

  if (items.length === 0) {
    return (
      <SectionCard>
        <div style={{ color: t.ink4, fontSize: 13, textAlign: 'center', padding: 28 }}>
          この日はまだ何もない。
        </div>
      </SectionCard>
    );
  }

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
      {items.map((item, idx) => {
        const common = {
          isFirst: idx === 0,
          isLast: idx === items.length - 1,
        };
        if (item.kind === 'event') {
          return (
            <TimelineEventEntry
              key={`ev-${item.data.id || idx}`}
              event={item.data}
              {...common}
            />
          );
        }
        if (item.kind === 'stack') {
          return (
            <TimelineNoteStack
              key={`stack-${item.ts}-${item.data[0].id}`}
              notes={item.data}
              onUpdate={onUpdate}
              onDelete={onDelete}
              {...common}
            />
          );
        }
        return (
          <TimelineNoteEntry
            key={`note-${item.data.id}`}
            note={item.data}
            onUpdate={onUpdate}
            onDelete={onDelete}
            {...common}
          />
        );
      })}
    </div>
  );
}

// v1.5: 付箋を更に小さく。スケジュール（多色）と被らないように
// メモはニュートラル（白/グレー）固定。tone は付箋頭の小ドットだけで示す。
function TimelineNoteEntry({ note, isFirst, isLast, onUpdate, onDelete }) {
  const t = useT();
  const tone = getNoteTone(note, t);
  const [detailOpen, setDetailOpen] = React.useState(false);

  const hasEcho = note.echo || note.echoedSimilarity != null;

  // 付箋プレビュー: 改行・連続空白を潰して 10 文字でカット
  const flat = (note.text || '').replace(/\s+/g, ' ').trim();
  const PREVIEW = 10;
  const preview = flat.length <= PREVIEW ? flat : flat.slice(0, PREVIEW) + '…';

  // 予定との視覚分離:
  //  - 予定: 色付きカード（左ボーダー強調、ソース色）
  //  - メモ: ニュートラル（薄背景・点線ボーダー・小さい）
  const isDark = t.mode === 'dark';
  const noteBg = isDark ? 'rgba(255,255,255,0.04)' : '#FFFFFF';
  const noteBorder = isDark ? 'rgba(255,255,255,0.12)' : t.line;

  return (
    <div
      style={{
        display: 'grid',
        gridTemplateColumns: '52px 14px 1fr',
        columnGap: 0,
        position: 'relative',
        paddingBottom: isLast ? 2 : 8,
      }}
    >
      {/* 時刻列 */}
      <div
        style={{
          fontFamily: APP_FONT,
          fontSize: 10,
          fontWeight: 600,
          color: t.ink4,
          textAlign: 'right',
          paddingTop: 4,
          paddingRight: 10,
          fontVariantNumeric: 'tabular-nums',
        }}
      >
        {timeLabel(note.ts)}
      </div>

      {/* 縦軸列（線のみ。ドットは付箋側に集約して視覚ノイズを減らす） */}
      <div style={{ position: 'relative', width: 14 }}>
        <div
          style={{
            position: 'absolute',
            top: 0,
            bottom: isLast ? 'auto' : 0,
            height: isLast ? 12 : 'auto',
            left: 6,
            width: 1.5,
            background: t.line,
          }}
        />
      </div>

      {/* 付箋本体 — 小さく・無彩色 */}
      <div style={{ paddingLeft: 8 }}>
        <button
          onClick={() => setDetailOpen(true)}
          title={flat}
          style={{
            display: 'inline-flex',
            alignItems: 'center',
            gap: 5,
            maxWidth: '100%',
            textAlign: 'left',
            border: `1px dashed ${noteBorder}`,
            background: noteBg,
            borderRadius: 999,
            padding: '2px 9px 2px 7px',
            cursor: 'pointer',
            fontFamily: APP_FONT,
            color: t.ink3,
            fontSize: 11,
            fontWeight: 500,
            letterSpacing: '-0.01em',
            lineHeight: 1.3,
            WebkitTapHighlightColor: 'transparent',
            transition: 'border-color 0.12s, color 0.12s',
          }}
          onMouseEnter={(e) => {
            e.currentTarget.style.borderColor = t.ink4;
            e.currentTarget.style.color = t.ink2;
          }}
          onMouseLeave={(e) => {
            e.currentTarget.style.borderColor = noteBorder;
            e.currentTarget.style.color = t.ink3;
          }}
        >
          {/* tone を示す小ドット（唯一の色アクセント） */}
          <span
            style={{
              width: 5, height: 5, borderRadius: '50%',
              background: tone.bg, flexShrink: 0,
            }}
          />
          <span
            style={{
              overflow: 'hidden',
              textOverflow: 'ellipsis',
              whiteSpace: 'nowrap',
              minWidth: 0,
            }}
          >
            {preview || '空メモ'}
          </span>
          {hasEcho && (
            <span
              style={{
                color: t.primary,
                fontSize: 9,
                fontWeight: 800,
                flexShrink: 0,
                lineHeight: 1,
              }}
            >
              ✦
            </span>
          )}
        </button>
      </div>

      <NoteDetailModal
        note={note}
        open={detailOpen}
        onClose={() => setDetailOpen(false)}
        onUpdate={onUpdate}
        onDelete={onDelete}
      />
    </div>
  );
}

// v1.5: 同時刻（5分以内）のメモを重ねて表示。タップで展開。
function TimelineNoteStack({ notes, isFirst, isLast, onUpdate, onDelete }) {
  const t = useT();
  const [expanded, setExpanded] = React.useState(false);
  const [detailNote, setDetailNote] = React.useState(null);

  const isDark = t.mode === 'dark';
  const noteBg = isDark ? 'rgba(255,255,255,0.04)' : '#FFFFFF';
  const noteBorder = isDark ? 'rgba(255,255,255,0.12)' : t.line;

  const PREVIEW = 10;
  const flatten = (txt) => (txt || '').replace(/\s+/g, ' ').trim();
  const previewOf = (n) => {
    const flat = flatten(n.text);
    return flat.length <= PREVIEW ? flat : flat.slice(0, PREVIEW) + '…';
  };

  // 折り畳み時に見せる代表メモ（最初の1件）と、後ろに重ねる枚数
  const head = notes[0];
  const headTone = getNoteTone(head, t);
  const stackCount = notes.length;

  if (expanded) {
    // 展開時: 全件を縦に並べる。先頭に「まとめる」ボタン。
    return (
      <div
        style={{
          display: 'grid',
          gridTemplateColumns: '52px 14px 1fr',
          columnGap: 0,
          position: 'relative',
          paddingBottom: isLast ? 2 : 8,
        }}
      >
        <div
          style={{
            fontFamily: APP_FONT,
            fontSize: 10,
            fontWeight: 600,
            color: t.ink4,
            textAlign: 'right',
            paddingTop: 4,
            paddingRight: 10,
            fontVariantNumeric: 'tabular-nums',
          }}
        >
          {timeLabel(head.ts)}
        </div>
        <div style={{ position: 'relative', width: 14 }}>
          <div
            style={{
              position: 'absolute',
              top: 0,
              bottom: isLast ? 'auto' : 0,
              height: isLast ? 12 : 'auto',
              left: 6,
              width: 1.5,
              background: t.line,
            }}
          />
        </div>
        <div style={{ paddingLeft: 8, display: 'flex', flexDirection: 'column', gap: 4 }}>
          {notes.map((n) => {
            const flat = flatten(n.text);
            const tone = getNoteTone(n, t);
            return (
              <button
                key={n.id}
                onClick={() => setDetailNote(n)}
                title={flat}
                style={{
                  display: 'inline-flex',
                  alignItems: 'center',
                  gap: 5,
                  maxWidth: '100%',
                  textAlign: 'left',
                  border: `1px dashed ${noteBorder}`,
                  background: noteBg,
                  borderRadius: 999,
                  padding: '2px 9px 2px 7px',
                  cursor: 'pointer',
                  fontFamily: APP_FONT,
                  color: t.ink3,
                  fontSize: 11,
                  fontWeight: 500,
                  letterSpacing: '-0.01em',
                  lineHeight: 1.3,
                  WebkitTapHighlightColor: 'transparent',
                }}
              >
                <span
                  style={{
                    width: 5, height: 5, borderRadius: '50%',
                    background: tone.bg, flexShrink: 0,
                  }}
                />
                <span
                  style={{
                    overflow: 'hidden',
                    textOverflow: 'ellipsis',
                    whiteSpace: 'nowrap',
                    minWidth: 0,
                  }}
                >
                  {previewOf(n) || '空メモ'}
                </span>
              </button>
            );
          })}
          <button
            onClick={() => setExpanded(false)}
            style={{
              alignSelf: 'flex-start',
              border: 'none',
              background: 'transparent',
              color: t.ink4,
              fontSize: 10,
              fontWeight: 700,
              cursor: 'pointer',
              padding: '2px 4px',
              fontFamily: APP_FONT,
              letterSpacing: '0.04em',
              WebkitTapHighlightColor: 'transparent',
            }}
          >
            ▴ まとめる
          </button>
        </div>

        <NoteDetailModal
          note={detailNote}
          open={!!detailNote}
          onClose={() => setDetailNote(null)}
          onUpdate={onUpdate}
          onDelete={onDelete}
        />
      </div>
    );
  }

  // 折り畳み時: 先頭の付箋 + 後ろに重なって見える擬似スタック
  return (
    <div
      style={{
        display: 'grid',
        gridTemplateColumns: '52px 14px 1fr',
        columnGap: 0,
        position: 'relative',
        paddingBottom: isLast ? 6 : 12,
      }}
    >
      <div
        style={{
          fontFamily: APP_FONT,
          fontSize: 10,
          fontWeight: 600,
          color: t.ink4,
          textAlign: 'right',
          paddingTop: 4,
          paddingRight: 10,
          fontVariantNumeric: 'tabular-nums',
        }}
      >
        {timeLabel(head.ts)}
      </div>
      <div style={{ position: 'relative', width: 14 }}>
        <div
          style={{
            position: 'absolute',
            top: 0,
            bottom: isLast ? 'auto' : 0,
            height: isLast ? 12 : 'auto',
            left: 6,
            width: 1.5,
            background: t.line,
          }}
        />
      </div>
      <div style={{ paddingLeft: 8, position: 'relative' }}>
        {/* 後ろに重なるシート（max3枚分演出） */}
        {[...Array(Math.min(stackCount - 1, 2))].map((_, i) => {
          const offset = (i + 1);
          return (
            <div
              key={i}
              aria-hidden
              style={{
                position: 'absolute',
                top: offset * 3,
                left: 8 + offset * 4,
                right: -offset * 2,
                height: 22,
                border: `1px dashed ${noteBorder}`,
                background: noteBg,
                borderRadius: 999,
                opacity: 0.55 - i * 0.18,
                pointerEvents: 'none',
                zIndex: 0,
              }}
            />
          );
        })}

        <button
          onClick={() => setExpanded(true)}
          title={`${stackCount}件のメモ`}
          style={{
            position: 'relative',
            zIndex: 1,
            display: 'inline-flex',
            alignItems: 'center',
            gap: 5,
            maxWidth: '100%',
            textAlign: 'left',
            border: `1px dashed ${noteBorder}`,
            background: noteBg,
            borderRadius: 999,
            padding: '2px 9px 2px 7px',
            cursor: 'pointer',
            fontFamily: APP_FONT,
            color: t.ink3,
            fontSize: 11,
            fontWeight: 500,
            letterSpacing: '-0.01em',
            lineHeight: 1.3,
            WebkitTapHighlightColor: 'transparent',
          }}
        >
          <span
            style={{
              width: 5, height: 5, borderRadius: '50%',
              background: headTone.bg, flexShrink: 0,
            }}
          />
          <span
            style={{
              overflow: 'hidden',
              textOverflow: 'ellipsis',
              whiteSpace: 'nowrap',
              minWidth: 0,
            }}
          >
            {previewOf(head) || '空メモ'}
          </span>
          <span
            style={{
              flexShrink: 0,
              color: t.ink4,
              fontSize: 10,
              fontWeight: 800,
              letterSpacing: '0.02em',
              padding: '0 4px',
              borderLeft: `1px solid ${noteBorder}`,
              marginLeft: 2,
            }}
          >
            +{stackCount - 1}
          </span>
        </button>
      </div>
    </div>
  );
}

function TimelineEventEntry({ event, isFirst, isLast }) {
  const t = useT();
  const color = event.sourceColor || '#4AA3FF';

  return (
    <div
      style={{
        display: 'grid',
        gridTemplateColumns: '52px 14px 1fr',
        columnGap: 0,
        position: 'relative',
        paddingBottom: isLast ? 4 : 22,
      }}
    >
      <div
        style={{
          fontFamily: APP_FONT,
          fontSize: 11,
          fontWeight: 700,
          color: t.ink4,
          textAlign: 'right',
          paddingTop: 14,
          paddingRight: 10,
          fontVariantNumeric: 'tabular-nums',
        }}
      >
        {event.allDay ? '終日' : timeLabel(event.ts)}
      </div>

      <div style={{ position: 'relative', width: 14 }}>
        <div
          style={{
            position: 'absolute',
            top: isFirst ? 16 : 0,
            bottom: isLast ? 'auto' : 0,
            height: isLast ? 22 : 'auto',
            left: 6,
            width: 1.5,
            background: t.line,
          }}
        />
        <div
          style={{
            position: 'absolute',
            top: 12,
            left: 1,
            width: 11,
            height: 11,
            borderRadius: '50%',
            background: color,
            boxShadow: `0 0 0 3px ${t.bg}`,
          }}
        />
      </div>

      <div style={{ paddingLeft: 10 }}>
        <div
          style={{
            background: t.bgCard,
            border: `1px solid ${t.line}`,
            borderLeft: `4px solid ${color}`,
            borderRadius: 10,
            padding: '10px 12px',
            fontFamily: APP_FONT,
          }}
        >
          <div
            style={{
              fontSize: 9.5,
              fontWeight: 900,
              letterSpacing: '0.14em',
              color,
              marginBottom: 4,
              textTransform: 'uppercase',
            }}
          >
            {event.category || 'CALENDAR'}
            {event.sourceName ? ` · ${event.sourceName}` : ''}
          </div>

          <div
            style={{
              fontSize: 14,
              fontWeight: 800,
              color: t.ink,
              lineHeight: 1.4,
              letterSpacing: '-0.02em',
              wordBreak: 'break-word',
            }}
          >
            {event.title || '予定'}
          </div>

          {(event.endTs || event.location) && (
            <div
              style={{
                marginTop: 4,
                fontSize: 11,
                color: t.ink4,
                display: 'flex',
                gap: 8,
                flexWrap: 'wrap',
              }}
            >
              {!event.allDay && event.endTs && (
                <span>
                  {timeLabel(event.ts)}–{timeLabel(event.endTs)}
                </span>
              )}
              {event.location && <span>· {event.location}</span>}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

// =========================
// Week timeline with strong sticky notes
// =========================

function TimelineWeekView({ notes = [], events = [], focusDate, setFocusDate, onUpdate, onDelete }) {
  const t = useT();
  const weekStart = startOfWeekLocal(focusDate);
  const days = [0, 1, 2, 3, 4, 5, 6].map(i => addDaysLocal(weekStart, i));
  const weekEnd = addDaysLocal(weekStart, 7).getTime();

  const weekEvents = events.filter(e => e.ts >= weekStart.getTime() && e.ts < weekEnd);
  const weekNotes = notes.filter(n => n.ts >= weekStart.getTime() && n.ts < weekEnd);

  let startHour = 7;
  let endHour = 22;

  weekEvents.forEach(e => {
    const s = new Date(e.ts).getHours();
    const ed = new Date(e.endTs || e.ts + 3600000).getHours();
    startHour = Math.min(startHour, Math.max(0, s));
    endHour = Math.max(endHour, Math.min(24, ed + 1));
  });

  weekNotes.forEach(n => {
    const h = new Date(n.ts).getHours();
    startHour = Math.min(startHour, Math.max(0, h));
    endHour = Math.max(endHour, Math.min(24, h + 1));
  });

  startHour = Math.max(0, startHour);
  endHour = Math.min(24, endHour);

  const hours = [];
  for (let h = startHour; h <= endHour; h++) hours.push(h);

  const ROW_H = 48;
  const today = startOfDayLocal(new Date()).getTime();

  return (
    <div style={{ fontFamily: APP_FONT }}>
      <div
        style={{
          display: 'grid',
          gridTemplateColumns: '42px repeat(7, minmax(52px, 1fr))',
          gap: 1,
          marginBottom: 5,
        }}
      >
        <div />
        {days.map(d => {
          const isToday = startOfDayLocal(d).getTime() === today;
          return (
            <button
              key={d.getTime()}
              onClick={() => setFocusDate(d)}
              style={{
                border: 'none',
                background: 'transparent',
                padding: '6px 2px',
                cursor: 'pointer',
                fontFamily: APP_FONT,
                WebkitTapHighlightColor: 'transparent',
              }}
            >
              <div
                style={{
                  fontSize: 10,
                  color: t.ink4,
                  fontWeight: 800,
                  letterSpacing: '0.1em',
                }}
              >
                {['日', '月', '火', '水', '木', '金', '土'][d.getDay()]}
              </div>
              <div
                style={{
                  width: 28,
                  height: 28,
                  borderRadius: '50%',
                  margin: '2px auto 0',
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                  background: isToday ? t.primary : 'transparent',
                  color: isToday ? '#fff' : t.ink,
                  fontSize: 15,
                  fontWeight: 900,
                }}
              >
                {d.getDate()}
              </div>
            </button>
          );
        })}
      </div>

      <div
        style={{
          background: t.bgCard,
          border: `1px solid ${t.line}`,
          borderRadius: 12,
          overflowX: 'hidden',
          overflowY: 'auto',
          maxHeight: 'calc(100vh - 270px)',
          WebkitOverflowScrolling: 'touch',
          width: '100%',
        }}
      >
        <div
          style={{
            display: 'grid',
            gridTemplateColumns: '42px repeat(7, minmax(0, 1fr))',
            position: 'relative',
            minHeight: hours.length * ROW_H,
            width: '100%',
          }}
        >
          <div style={{ position: 'relative' }}>
            {hours.map((h, idx) => (
              <div
                key={h}
                style={{
                  height: ROW_H,
                  borderTop: idx === 0 ? 'none' : `1px solid ${t.line}`,
                  color: t.ink4,
                  fontSize: 9,
                  textAlign: 'right',
                  padding: '3px 5px 0 0',
                  fontVariantNumeric: 'tabular-nums',
                }}
              >
                {String(h).padStart(2, '0')}:00
              </div>
            ))}
          </div>

          {days.map((day, colIdx) => {
            const dayStart = startOfDayLocal(day).getTime();
            const dayEnd = endOfDayLocal(day).getTime();

            const dayEvents = weekEvents.filter(e => e.ts >= dayStart && e.ts < dayEnd);
            const dayNotes = weekNotes.filter(n => n.ts >= dayStart && n.ts < dayEnd);

            const isToday = dayStart === today;

            return (
              <div
                key={dayStart}
                style={{
                  position: 'relative',
                  borderLeft: `1px solid ${t.line}`,
                  background: isToday ? `${t.primary}08` : 'transparent',
                  minHeight: hours.length * ROW_H,
                }}
                onClick={() => setFocusDate(day)}
              >
                {hours.map((h, idx) => (
                  <div
                    key={h}
                    style={{
                      height: ROW_H,
                      borderTop: idx === 0 ? 'none' : `1px solid ${t.line}`,
                    }}
                  />
                ))}

                {dayEvents.map(ev => {
                  const start = new Date(ev.ts);
                  const end = new Date(ev.endTs || ev.ts + 3600000);
                  const topH = start.getHours() + start.getMinutes() / 60 - startHour;
                  const durH = Math.max(0.5, (end - start) / 3600000);
                  const color = ev.sourceColor || '#4AA3FF';

                  return (
                    <div
                      key={ev.id}
                      onClick={(e) => {
                        e.stopPropagation();
                        if (ev.url) window.open(ev.url, '_blank');
                      }}
                      title={ev.title}
                      style={{
                        position: 'absolute',
                        top: topH * ROW_H + 2,
                        left: 3,
                        right: 3,
                        height: Math.max(24, durH * ROW_H - 4),
                        background: `${color}DD`,
                        borderLeft: `4px solid ${color}`,
                        color: '#fff',
                        borderRadius: 6,
                        padding: '4px 5px',
                        fontSize: 9.5,
                        fontWeight: 700,
                        overflow: 'hidden',
                        cursor: 'pointer',
                        boxShadow: '0 2px 6px rgba(0,0,0,0.18)',
                        zIndex: 5,
                      }}
                    >
                      <div style={{ fontSize: 8, opacity: 0.9 }}>
                        {timeLabel(ev.ts)}
                      </div>
                      <div
                        style={{
                          overflow: 'hidden',
                          textOverflow: 'ellipsis',
                          whiteSpace: 'nowrap',
                        }}
                      >
                        {ev.title}
                      </div>
                    </div>
                  );
                })}

                {dayNotes.map((note, idx) => {
                  const d = new Date(note.ts);
                  const topH = d.getHours() + d.getMinutes() / 60 - startHour;
                  const lane = idx % 2;

                  return (
                    <div
                      key={note.id}
                      style={{
                        position: 'absolute',
                        top: topH * ROW_H + 6 + lane * 18,
                        left: lane === 0 ? 6 : 30,
                        right: 4,
                        zIndex: 30 + idx,
                        pointerEvents: 'auto',
                      }}
                      onClick={(e) => e.stopPropagation()}
                    >
                      <StickyNote
                        note={note}
                        onUpdate={onUpdate}
                        onDelete={onDelete}
                      />
                    </div>
                  );
                })}
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

function StickyNote({ note, onUpdate, onDelete }) {
  const t = useT();
  const [detailOpen, setDetailOpen] = React.useState(false);
  const tone = getNoteTone(note, t);

  const displayText = safeText(note.text).trim();

  const preview = (() => {
    const flat = displayText.replace(/\s+/g, ' ');
    if (!flat) return tone.short;
    if (flat.length <= 8) return flat;
    return flat.slice(0, 8) + '…';
  })();

  return (
    <>
      <button
        onClick={(e) => {
          e.stopPropagation();
          setDetailOpen(true);
        }}
        title={displayText}
        style={{
          display: 'inline-flex',
          flexDirection: 'column',
          alignItems: 'flex-start',
          justifyContent: 'center',
          gap: 2,

          minWidth: 58,
          maxWidth: 110,
          minHeight: 34,

          padding: '5px 7px 6px',
          borderRadius: 8,
          border: `1.5px solid ${tone.border}`,
          background: tone.bg,
          color: tone.text,

          fontFamily: APP_FONT,
          fontSize: 10.5,
          fontWeight: 900,
          lineHeight: 1.15,
          letterSpacing: '-0.03em',
          textAlign: 'left',

          cursor: 'pointer',
          overflow: 'hidden',
          whiteSpace: 'nowrap',
          textOverflow: 'ellipsis',

          boxShadow: tone.shadow,
          zIndex: 50,
          position: 'relative',

          WebkitTapHighlightColor: 'transparent',
        }}
      >
        <span
          style={{
            fontSize: 7,
            fontWeight: 900,
            letterSpacing: '0.12em',
            opacity: 0.78,
            lineHeight: 1,
          }}
        >
          {tone.label}
        </span>

        <span
          style={{
            display: 'block',
            maxWidth: '100%',
            overflow: 'hidden',
            whiteSpace: 'nowrap',
            textOverflow: 'ellipsis',
          }}
        >
          {preview}
        </span>
      </button>

      <NoteDetailModal
        note={note}
        open={detailOpen}
        onClose={() => setDetailOpen(false)}
        onUpdate={onUpdate}
        onDelete={onDelete}
      />
    </>
  );
}

// =========================
// Ripen
// =========================

function RipenView({ notes = [], onUpdate, onDelete }) {
  const t = useT();

  const oldNotes = notes
    .filter(n => Date.now() - n.ts > 3 * 24 * 3600 * 1000)
    .slice()
    .sort((a, b) => b.ts - a.ts);

  const resting = notes
    .filter(n => n.status === 'resting')
    .slice()
    .sort((a, b) => (b.restedAt || b.ts) - (a.restedAt || a.ts));

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
      <SectionCard
        style={{
          background: `linear-gradient(135deg, ${t.bgCard}, ${t.primarySoft || t.bgSoft})`,
        }}
      >
        <div
          style={{
            fontSize: 10,
            fontWeight: 900,
            letterSpacing: '0.16em',
            color: t.primary,
            marginBottom: 6,
          }}
        >
          RIPEN
        </div>
        <div
          style={{
            fontSize: 20,
            fontWeight: 900,
            letterSpacing: '-0.04em',
            color: t.ink,
            lineHeight: 1.25,
          }}
        >
          今じゃない思考を、寝かせて育てる。
        </div>
        <div style={{ fontSize: 12.5, color: t.ink3, lineHeight: 1.65, marginTop: 8 }}>
          すぐタスク化しない。時間が経って意味が出るものをここで拾う。
        </div>
      </SectionCard>

      {typeof DigestHero === 'function' && (
        <DigestHero notes={notes} />
      )}

      {typeof RandomRemix === 'function' && (
        <RandomRemix notes={notes} />
      )}

      {resting.length > 0 && (
        <SectionCard>
          <div
            style={{
              fontSize: 13,
              fontWeight: 900,
              color: t.ink,
              marginBottom: 10,
            }}
          >
            寝かせ中
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {resting.map(n => (
              <NoteCard
                key={n.id}
                note={n}
                onUpdate={onUpdate}
                onDelete={onDelete}
              />
            ))}
          </div>
        </SectionCard>
      )}

      {oldNotes.length > 0 && (
        <SectionCard>
          <div
            style={{
              fontSize: 13,
              fontWeight: 900,
              color: t.ink,
              marginBottom: 10,
            }}
          >
            熟成候補
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {oldNotes.slice(0, 12).map(n => (
              <NoteCard
                key={n.id}
                note={n}
                onUpdate={onUpdate}
                onDelete={onDelete}
              />
            ))}
          </div>
        </SectionCard>
      )}

      {notes.length === 0 && <EmptyState />}
    </div>
  );
}

// =========================
// Settings
// =========================

function SettingsView({ notes = [], onReset, onCalendarChange }) {
  const t = useT();
  const fileRef = React.useRef(null);
  const [message, setMessage] = React.useState(null);
  const [settings, setSettings] =
    typeof useEchoSettings === 'function'
      ? useEchoSettings()
      : React.useState({ fabPosition: 'right', dayBreakHour: 0 });

  const exportJSON = () => {
    const payload = {
      app: 'Echo',
      version: 1,
      exportedAt: new Date().toISOString(),
      notes,
    };

    const blob = new Blob([JSON.stringify(payload, null, 2)], {
      type: 'application/json',
    });

    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `echo-export-${dateKeyLocal(new Date())}.json`;
    a.click();
    URL.revokeObjectURL(url);
  };

  const importJSON = async (e) => {
    const file = e.target.files && e.target.files[0];
    if (!file) return;

    try {
      const text = await file.text();
      const json = JSON.parse(text);
      const imported = Array.isArray(json) ? json : json.notes;

      if (!Array.isArray(imported)) {
        throw new Error('notes配列が見つからない');
      }

      if (!confirm(`${imported.length}件をインポートしますか？`)) return;

      if (DB && typeof DB.putMany === 'function') {
        await DB.putMany(imported);
      } else {
        for (const n of imported) {
          await DB.put(n);
        }
      }

      setMessage('インポート完了。画面を再読み込みして確認。');
      setTimeout(() => location.reload(), 800);
    } catch (err) {
      console.error(err);
      setMessage('インポート失敗: ' + err.message);
    } finally {
      if (fileRef.current) fileRef.current.value = '';
    }
  };

  const syncCalendar = async () => {
    try {
      if (typeof syncGoogleCalendars === 'function') {
        setMessage('Calendar同期中…');
        await syncGoogleCalendars();
        if (onCalendarChange) await onCalendarChange();
        setMessage('Calendar同期完了');
      } else {
        setMessage('Calendar同期関数が見つからない');
      }
    } catch (e) {
      console.error(e);
      setMessage('Calendar同期失敗: ' + e.message);
    }
  };

  const setFab = (pos) => {
    const next = { ...settings, fabPosition: pos };
    setSettings(next);
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
      <SectionCard>
        <div
          style={{
            fontSize: 10,
            fontWeight: 900,
            letterSpacing: '0.16em',
            color: t.primary,
            marginBottom: 6,
          }}
        >
          SETTINGS
        </div>
        <div
          style={{
            fontSize: 20,
            fontWeight: 900,
            letterSpacing: '-0.04em',
            color: t.ink,
          }}
        >
          Echo 設定
        </div>
        <div style={{ fontSize: 12.5, color: t.ink3, marginTop: 6 }}>
          データ保護と使いやすさを優先。
        </div>
      </SectionCard>

      <SectionCard>
        <SettingsTitle title="データ" />
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
          <button style={settingsBtn(t, true)} onClick={exportJSON}>
            JSON Export
          </button>

          <button
            style={settingsBtn(t)}
            onClick={() => fileRef.current && fileRef.current.click()}
          >
            JSON Import
          </button>

          <input
            ref={fileRef}
            type="file"
            accept="application/json"
            onChange={importJSON}
            style={{ display: 'none' }}
          />

          <button style={settingsBtn(t)} onClick={onReset}>
            Reset Demo
          </button>
        </div>

        <div style={{ fontSize: 11, color: t.ink4, marginTop: 10 }}>
          現在のメモ数: {notes.length}
        </div>
      </SectionCard>

      <SectionCard>
        <SettingsTitle title="投下ボタン" />
        <div style={{ display: 'flex', gap: 8 }}>
          <button
            style={settingsBtn(t, settings.fabPosition !== 'left')}
            onClick={() => setFab('right')}
          >
            右下
          </button>
          <button
            style={settingsBtn(t, settings.fabPosition === 'left')}
            onClick={() => setFab('left')}
          >
            左下
          </button>
        </div>
      </SectionCard>

      <SectionCard>
        <SettingsTitle title="Calendar" />
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
          <button style={settingsBtn(t)} onClick={syncCalendar}>
            Google Calendar Sync
          </button>
        </div>
        <div style={{ fontSize: 11, color: t.ink4, marginTop: 10, lineHeight: 1.6 }}>
          予定と着想を重ねて見る。今回の更新で、着想カードは強い色に変更済み。
        </div>
      </SectionCard>

      <SectionCard>
        <SettingsTitle title="Install" />
        <button
          style={settingsBtn(t)}
          onClick={() => {
            if (window.__echoOpenInstall) window.__echoOpenInstall();
            else setMessage('Safariの共有ボタン → ホーム画面に追加');
          }}
        >
          ホーム画面に追加
        </button>
      </SectionCard>

      {message && (
        <div
          style={{
            background: t.bgSoft,
            border: `1px solid ${t.line}`,
            borderRadius: 10,
            padding: 12,
            color: t.ink2,
            fontSize: 12,
            fontWeight: 700,
            fontFamily: APP_FONT,
          }}
        >
          {message}
        </div>
      )}
    </div>
  );
}

function SettingsTitle({ title }) {
  const t = useT();
  return (
    <div
      style={{
        fontSize: 13,
        fontWeight: 900,
        color: t.ink,
        marginBottom: 10,
      }}
    >
      {title}
    </div>
  );
}

function settingsBtn(t, primary) {
  return {
    border: primary ? 'none' : `1px solid ${t.line}`,
    background: primary ? t.primary : t.bgCard,
    color: primary ? '#fff' : t.ink3,
    borderRadius: 10,
    padding: '9px 12px',
    fontFamily: APP_FONT,
    fontSize: 12,
    fontWeight: 800,
    cursor: 'pointer',
    WebkitTapHighlightColor: 'transparent',
  };
}

// =========================
// Export globals
// =========================

Object.assign(window, {
  InboxView,
  TimelineView,
  TimelineDayView,
  TimelineWeekView,
  RipenView,
  SettingsView,
  EmptyState,
  StickyNote,
});
