// 工程表 (Schedule view) — beautiful chronological timeline of the entire trip.
//
// The view is meant to feel like a "wow, the whole trip is now organised!"
// moment: one click and the disorganised diagram becomes a proper itinerary
// with cumulative totals, day separators, and one-click export.
//
// Hero actions (top of panel):
//   📅 カレンダーに登録 (.ics)  — single button, all events at once
//   📊 経費精算CSV              — categorised expense report
//   🖨 印刷                     — print-friendly layout
//
// Cost categories (matches Japanese expense reports):
//   旅費交通費 = sum of edge.cost
//   宿泊費 = sum of node.cost where node represents lodging
//   雑費   = remaining node.cost
function Schedule({ nodes, edges, groups, notes = [], defaultCurrency, title, subtitle, onClose }){
  const dt = window.tourDateTime;
  const nodeById = Object.fromEntries(nodes.map(n => [n.id, n]));
  // Two display modes: 🎨 timeline (default) and 📋 elegant table
  const [viewMode, setViewMode] = React.useState('timeline');
  // Show / hide linked notes inline. Default ON so the schedule reads as a
  // self-contained "shiori" (栞). Users can toggle OFF for a clean version
  // when sharing with management or when notes are personal-only.
  // Toggle is ephemeral (not persisted) — every reopen starts ON so the
  // notes don't go missing-by-default after a reimbursement print.
  const [showNotes, setShowNotes] = React.useState(true);

  // Index notes by anchor for fast inline lookup.
  // notesByNode[id] / notesByEdge[id] = Note[] in the order they appear.
  const notesByNode = {}, notesByEdge = {};
  const orphanNotes = [];  // anchor === null (or anchor target deleted)
  notes.forEach(note => {
    const a = note.anchor;
    if (!a || !a.type || !a.id) { orphanNotes.push(note); return; }
    if (a.type === 'node'){
      if (!nodeById[a.id]) { orphanNotes.push(note); return; }
      (notesByNode[a.id] = notesByNode[a.id] || []).push(note);
    } else if (a.type === 'edge'){
      const e = edges.find(x => x.id === a.id);
      if (!e) { orphanNotes.push(note); return; }
      (notesByEdge[a.id] = notesByEdge[a.id] || []).push(note);
    } else {
      orphanNotes.push(note);
    }
  });

  // ── Build chronological items, grouped by date ──────────
  // Multi-day nodes (e.g. an overnight hotel with arrival on day A and
  // departure on day B) appear in BOTH days so Day 2 starts naturally with
  // the hotel rather than the first outgoing edge. Each day-instance gets
  // a `perspective` flag ('arrival' | 'departure' | 'full') and a sortAt
  // tied to whichever event is relevant on that day:
  //   arrival   day perspective → sortAt = node.arrival   (first event of arrival day)
  //   departure day perspective → sortAt = node.departure (first event of departure day)
  // → Day 2's hotel sortAt = 8:00 (departure), so it lands at the top of
  //   Day 2 ahead of the 朝の散歩 walk that leaves at 8:00 too.
  const groupsOfNode = (nodeId) => (groups || []).filter(g => (g.nodeIds||[]).includes(nodeId));
  const dayKeyOf = (d) => d
    ? `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
    : '__undated__';

  const dayMap = new Map();    // key → items[]
  const dayFirstDate = new Map();  // key → earliest Date in that day (for label/header)
  const pushItem = (key, dateForHeader, item) => {
    if (!dayMap.has(key)) dayMap.set(key, []);
    dayMap.get(key).push(item);
    if (dateForHeader){
      const cur = dayFirstDate.get(key);
      if (!cur || dateForHeader < cur) dayFirstDate.set(key, dateForHeader);
    }
  };

  nodes.forEach(n => {
    const a = dt?.toDate(n.arrival);
    const d = dt?.toDate(n.departure);
    const aKey = a ? dayKeyOf(a) : null;
    const dKey = d ? dayKeyOf(d) : null;
    const ngroups = groupsOfNode(n.id);
    const baseInfo = {
      kind:'node', node:n,
      group: ngroups[0] || null,
      groups: ngroups,
    };

    if (!aKey && !dKey){
      pushItem('__undated__', null, { ...baseInfo, sortAt:null, perspective:'full' });
    } else if (!dKey || aKey === dKey){
      // Single-day stay (or arrival-only with no departure)
      pushItem(aKey || dKey, a || d, { ...baseInfo, sortAt: a || d, perspective:'full' });
    } else if (!aKey){
      // Departure-only (e.g. start-of-trip node with only departure set)
      pushItem(dKey, d, { ...baseInfo, sortAt:d, perspective:'full' });
    } else {
      // Multi-day node — appears at the END of the arrival day AND at the
      // START of the departure day.
      pushItem(aKey, a, { ...baseInfo, sortAt:a, perspective:'arrival' });
      pushItem(dKey, d, { ...baseInfo, sortAt:d, perspective:'departure' });
    }
  });

  edges.forEach(e => {
    const from = nodeById[e.from];
    const to   = nodeById[e.to];
    const sortAt = dt?.toDate(from?.departure) || dt?.toDate(to?.arrival) || null;
    pushItem(sortAt ? dayKeyOf(sortAt) : '__undated__', sortAt,
             { kind:'edge', edge:e, from, to, sortAt });
  });

  // Sort items within each day. Two phases:
  //   1. Items with a real sortAt (datetime) → sorted by time as before.
  //   2. Items WITHOUT sortAt (the "未定" / undated bucket) → THREADED by
  //      edge connections so the schedule reads as "A → edge1 → B → edge2 → C"
  //      rather than "all nodes / then all edges" (which loses the graph
  //      structure when no times are set).
  // Phase 1 is per-day sort; phase 2 only kicks in when sortAt is null.
  const threadUndated = (items) => {
    // items here all have sortAt = null. Walk edges in seq order and emit
    // from-node → edge → to-node, skipping nodes already placed. Orphan
    // nodes (not referenced by any edge) get appended at the end.
    const edgeItems = items.filter(i => i.kind === 'edge')
                           .sort((a,b) => ((a.edge.seq||0) - (b.edge.seq||0)));
    const nodeItems = items.filter(i => i.kind === 'node');
    const nodeIndex = new Map();
    nodeItems.forEach(ni => {
      // A node may have multiple instances if it spans days (perspective).
      // For undated bucket, perspective is always 'full' so 1 instance.
      if (!nodeIndex.has(ni.node.id)) nodeIndex.set(ni.node.id, ni);
    });
    const out = [];
    const placed = new Set();
    edgeItems.forEach(ei => {
      const fromNi = nodeIndex.get(ei.edge.from);
      if (fromNi && !placed.has(ei.edge.from)){
        out.push(fromNi); placed.add(ei.edge.from);
      }
      out.push(ei);
      const toNi = nodeIndex.get(ei.edge.to);
      if (toNi && !placed.has(ei.edge.to)){
        out.push(toNi); placed.add(ei.edge.to);
      }
    });
    // Orphan nodes (no incoming/outgoing edges) — append at end.
    nodeItems.forEach(ni => {
      if (!placed.has(ni.node.id)) out.push(ni);
    });
    return out;
  };

  dayMap.forEach((arr, key) => {
    const dated   = arr.filter(i => i.sortAt);
    const undated = arr.filter(i => !i.sortAt);
    dated.sort((a, b) => {
      const aT = a.sortAt.getTime();
      const bT = b.sortAt.getTime();
      if (aT !== bT) return aT - bT;
      if (a.kind !== b.kind) return a.kind === 'node' ? -1 : 1;
      if (a.kind === 'edge') return ((a.edge.seq||0) - (b.edge.seq||0));
      return 0;
    });
    const threaded = threadUndated(undated);
    arr.length = 0;
    arr.push(...dated, ...threaded);
  });

  const days = [...dayMap.entries()]
    .sort((a, b) => a[0].localeCompare(b[0]))   // YYYY-MM-DD strings sort lexically
    .map(([key, items]) => ({
      key,
      date: dayFirstDate.get(key) || null,
      items,
    }));

  // Number day labels (skip undated)
  let dayNum = 0;
  days.forEach(day => {
    if (day.date) day.label = `${++dayNum}日目`;
    else day.label = '未定';
  });

  // Flat list (no multi-day duplication) — used for CSV export and length
  // checks where we DON'T want hotels counted twice.
  const flatItems = [];
  nodes.forEach(n => {
    const sortAt = dt?.toDate(n.arrival) || dt?.toDate(n.departure) || null;
    flatItems.push({ kind:'node', node:n, sortAt });
  });
  edges.forEach(e => {
    const from = nodeById[e.from];
    const to   = nodeById[e.to];
    const sortAt = dt?.toDate(from?.departure) || dt?.toDate(to?.arrival) || null;
    flatItems.push({ kind:'edge', edge:e, from, to, sortAt });
  });
  flatItems.sort((a, b) => {
    const aT = a.sortAt ? a.sortAt.getTime() : Infinity;
    const bT = b.sortAt ? b.sortAt.getTime() : Infinity;
    if (aT !== bT) return aT - bT;
    if (a.kind !== b.kind) return a.kind === 'node' ? -1 : 1;
    if (a.kind === 'edge') return ((a.edge.seq||0) - (b.edge.seq||0));
    return 0;
  });

  // ── Cost categorisation totals (5 buckets × currency) ─────
  const cat = window.nodeCostCategory || (n => (n?.icon === 'bed' || n?.icon === 'tent') ? 'lodging' : 'misc');
  const fallbackCur = defaultCurrency || '¥';
  const extractCur = window.extractCurrency || ((_s, fb) => fb || '¥');
  // Each "bucket" is a Map<symbol, amount> so a trip mixing ¥ and $
  // surfaces both subtotals separately rather than producing a wrong
  // lumped sum (we never auto-convert between currencies).
  const transportBucket = new Map();
  const lodgingBucket   = new Map();
  const miscBucket      = new Map();
  const meetingBucket   = new Map();
  const entertainmentBucket = new Map();
  const grandBucket     = new Map();
  const addToBucket = (b, sym, amt) => b.set(sym, (b.get(sym) || 0) + amt);
  edges.forEach(e => {
    const c = parseCost(e.cost);
    if (c <= 0) return;
    const sym = extractCur(e.cost, fallbackCur);
    addToBucket(transportBucket, sym, c);
    addToBucket(grandBucket, sym, c);
  });
  nodes.forEach(n => {
    const c = parseCost(n.cost);
    if (c <= 0) return;
    const sym = extractCur(n.cost, fallbackCur);
    const k = cat(n);
    if      (k === 'lodging')       addToBucket(lodgingBucket, sym, c);
    else if (k === 'meeting')       addToBucket(meetingBucket, sym, c);
    else if (k === 'entertainment') addToBucket(entertainmentBucket, sym, c);
    else                            addToBucket(miscBucket, sym, c);
    addToBucket(grandBucket, sym, c);
  });
  // Sum across all currencies inside a bucket — used only where we need
  // a single "is this empty?" check (e.g. hiding the totals card when
  // nothing has any cost). For DISPLAY we always render per-currency.
  const bucketSum = (b) => [...b.values()].reduce((s, v) => s + v, 0);
  const transportCost = bucketSum(transportBucket);
  const lodgingCost   = bucketSum(lodgingBucket);
  const miscCost      = bucketSum(miscBucket);
  const meetingCost   = bucketSum(meetingBucket);
  const entertainmentCost = bucketSum(entertainmentBucket);
  const grandCost     = bucketSum(grandBucket);
  // Render helper — prints "¥3,450 + $25.50" sorted with default first.
  const renderBucket = (bucket) => {
    const entries = [...bucket.entries()].filter(([_, v]) => v > 0);
    if (entries.length === 0) return formatCostWith(fallbackCur, 0);
    entries.sort((a, b) => {
      if (a[0] === fallbackCur) return -1;
      if (b[0] === fallbackCur) return 1;
      return a[0] < b[0] ? -1 : 1;
    });
    return entries.map(([sym, v]) => formatCostWith(sym, v)).join(' + ');
  };

  // Helper: nice formatted clock time for the timeline column.
  const fmtTime = (s) => {
    const d = dt?.toDate(s);
    if (!d) return '';
    return `${d.getHours()}:${String(d.getMinutes()).padStart(2,'0')}`;
  };

  // ── Calendar export (.ics) ────────────────────────────────
  // When showNotes=true the inline memo text is also stitched into each
  // event's DESCRIPTION so notes ride along into Apple/Google/Outlook.
  // NOTE: Use REAL newline characters ('\n') in description content. The
  // escapeIcs() helper will convert them to the iCal-required '\\n' escape
  // when emitting the file. Earlier code used '\\n' here directly, which
  // got double-escaped and showed up as literal "\n" text in calendar apps
  // instead of an actual line break.
  const exportIcs = () => {
    if (!dt) return;
    const events = [];
    const noteLines = (arr) => (arr || []).map(nt => `📝 ${nt.text || ''}`).join('\n');
    nodes.forEach(n => {
      if (n.arrival && n.departure){
        const costLine = n.cost ? `${cat(n)==='lodging'?'宿泊費':'雑費'}: ${n.cost}` : '';
        const notesPart = showNotes ? noteLines(notesByNode[n.id]) : '';
        const descParts = [n.subtitle || '', costLine, notesPart].filter(Boolean);
        events.push({
          uid: n.id + '@amix-tour',
          start: n.arrival, end: n.departure,
          summary: n.name || '(no name)',
          description: descParts.join('\n'),
          location: n.name,
        });
      }
    });
    edges.forEach(e => {
      const from = nodeById[e.from];
      const to   = nodeById[e.to];
      if (!from?.departure || !to?.arrival) return;
      const rel = window.RELATIONS?.find(r => r.id === e.relation);
      const notesPart = showNotes ? noteLines(notesByEdge[e.id]) : '';
      const costPart  = e.cost ? `旅費交通費: ${e.cost}` : '';
      const descParts = [costPart, notesPart].filter(Boolean);
      events.push({
        uid: e.id + '@amix-tour',
        start: from.departure, end: to.arrival,
        summary: `${e.label || rel?.label || '区間'}：${from.name} → ${to.name}`,
        description: descParts.join('\n'),
        location: '',
      });
    });
    if (events.length === 0){
      alert('カレンダーに書き出す予定がありません。各地点の到着・出発日時を入力してください。');
      return;
    }
    const ics = dt.buildIcs(title || '旅程', events);
    const fname = (title || 'tour').replace(/[\\/:*?"<>|]/g, '_') + '.ics';
    dt.downloadIcs(fname, ics);
  };

  // ── Expense CSV export (経費精算用) ───────────────────────
  const exportCsv = () => {
    const fmtDate = d => d ? `${d.getFullYear()}/${d.getMonth()+1}/${d.getDate()}` : '';
    const fmtCsv = s => `"${String(s ?? '').replace(/"/g, '""')}"`;
    // 金額は "¥190" / "$25.50" のように記号 + 数値で1セルに書き出します。
    // SUMIF で通貨ごとに合計したい場合は通貨記号がある別列が必要ですが、
    // この CSV は (種別 × 通貨) ごとの小計行を末尾にまとめて出すので、
    // ユーザーは目視で必要な合計を読み取れます。Excel の SUM が直接効か
    // ない代わりに、見た時点で「¥190」と把握しやすいのを優先しました。
    const headers = ['日付', '時刻', '種別', '内容', '金額', '備考'];
    const lines = [headers.map(fmtCsv).join(',')];

    flatItems.forEach(it => {
      if (it.kind === 'node'){
        const n = it.node;
        const c = parseCost(n.cost);
        if (c <= 0) return;  // only output rows that have a cost
        const category = categoryLabel(cat(n));
        const d = dt?.toDate(n.arrival) || dt?.toDate(n.departure);
        lines.push([
          fmtDate(d),
          fmtTime(n.arrival || n.departure),
          category,
          n.name + (n.subtitle ? ` (${n.subtitle})` : ''),
          formatCostWith(extractCur(n.cost, fallbackCur), c),
          '',
        ].map(fmtCsv).join(','));
      } else {
        const e = it.edge;
        const c = parseCost(e.cost);
        if (c <= 0) return;
        const rel = window.RELATIONS?.find(r => r.id === e.relation);
        const d = dt?.toDate(it.from?.departure) || dt?.toDate(it.to?.arrival);
        lines.push([
          fmtDate(d),
          fmtTime(it.from?.departure || it.to?.arrival),
          '旅費交通費',
          `${e.label || rel?.label || '区間'} (${it.from?.name || '?'} → ${it.to?.name || '?'})`,
          formatCostWith(extractCur(e.cost, fallbackCur), c),
          '',
        ].map(fmtCsv).join(','));
      }
    });

    // Subtotal + total rows — one row per (category × currency) so totals
    // stay split when the trip mixes JPY and foreign currency. The amount
    // column carries the currency symbol inline ("¥190", "$25.50").
    const pushBucketRows = (label, bucket) => {
      [...bucket.entries()]
        .filter(([_, v]) => v > 0)
        .sort((a, b) => a[0] === fallbackCur ? -1 : b[0] === fallbackCur ? 1 : (a[0] < b[0] ? -1 : 1))
        .forEach(([sym, v]) => {
          lines.push(['', '', label, '小計', formatCostWith(sym, v), ''].map(fmtCsv).join(','));
        });
    };
    lines.push('');
    pushBucketRows('旅費交通費', transportBucket);
    pushBucketRows('宿泊費',    lodgingBucket);
    pushBucketRows('雑費',      miscBucket);
    pushBucketRows('会議費',    meetingBucket);
    pushBucketRows('交際費',    entertainmentBucket);
    // Grand total per currency.
    [...grandBucket.entries()]
      .filter(([_, v]) => v > 0)
      .sort((a, b) => a[0] === fallbackCur ? -1 : b[0] === fallbackCur ? 1 : (a[0] < b[0] ? -1 : 1))
      .forEach(([sym, v]) => {
        lines.push(['', '', '合計', '', formatCostWith(sym, v), ''].map(fmtCsv).join(','));
      });

    // BOM + CRLF for Excel compatibility
    const csv = '﻿' + lines.join('\r\n');
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = (title || 'tour').replace(/[\\/:*?"<>|]/g, '_') + '_経費.csv';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  };

  // ── Itinerary CSV export (旅程用) ───────────────────────────
  // Same row vocabulary as the on-screen schedule but in spreadsheet form,
  // useful for sharing with travel companions / pasting into a planning
  // doc / archiving the trip outline. Unlike the expense CSV (which only
  // shows rows with costs), the itinerary CSV shows EVERY entry.
  const exportItineraryCsv = () => {
    const fmtDateLong = d => d ? `${d.getFullYear()}/${d.getMonth()+1}/${d.getDate()}` : '';
    const fmtCsv = s => `"${String(s ?? '').replace(/"/g, '""')}"`;
    // 費用は "¥190" / "$25.50" のように記号付きで 1 セルに書き出します。
    // 共有ドキュメントとして読みやすさを優先 — Excel の SUM は効きませんが、
    // 経費精算 CSV 側に通貨別小計が出ているのでこの旅程 CSV では不要です。
    const headers = ['日付', '時刻', '種別', '地点／区間', '内容', '所要時間', '費用', '備考'];
    const lines = [headers.map(fmtCsv).join(',')];
    flatItems.forEach(it => {
      if (it.kind === 'node'){
        const n = it.node;
        const arrD = dt?.toDate(n.arrival);
        const depD = dt?.toDate(n.departure);
        const dayD = arrD || depD;
        const stayMins = (arrD && depD) ? dt.minutesBetween(n.arrival, n.departure) : null;
        const stayTxt = (stayMins && stayMins > 0) ? dt.formatDuration(stayMins) : '';
        const c = parseCost(n.cost);
        const cost = c > 0 ? formatCostWith(extractCur(n.cost, fallbackCur), c) : '';
        const timeRange = (n.arrival && n.departure)
          ? `${fmtTime(n.arrival)} → ${fmtTime(n.departure)}`
          : (n.arrival ? `${fmtTime(n.arrival)} 着` : (n.departure ? `${fmtTime(n.departure)} 発` : ''));
        const notesTxt = (notesByNode[n.id] || []).map(nt => (nt.text || '').replace(/\r?\n/g, ' ')).join(' / ');
        lines.push([
          fmtDateLong(dayD),
          timeRange,
          '地点',
          n.name || '',
          n.subtitle || '',
          stayTxt,
          cost,
          notesTxt,
        ].map(fmtCsv).join(','));
      } else {
        const e = it.edge;
        const fromN = it.from, toN = it.to;
        const dep = dt?.toDate(fromN?.departure);
        const arr = dt?.toDate(toN?.arrival);
        const dayD = dep || arr;
        const transitMins = (fromN?.departure && toN?.arrival)
          ? dt.minutesBetween(fromN.departure, toN.arrival) : null;
        const dur = (transitMins && transitMins > 0) ? dt.formatDuration(transitMins) : '';
        const c = parseCost(e.cost);
        const cost = c > 0 ? formatCostWith(extractCur(e.cost, fallbackCur), c) : '';
        const rel = window.RELATIONS?.find(r => r.id === e.relation);
        const timeRange = (fromN?.departure && toN?.arrival)
          ? `${fmtTime(fromN.departure)} → ${fmtTime(toN.arrival)}`
          : (fromN?.departure ? `${fmtTime(fromN.departure)} 発` : '');
        const notesTxt = (notesByEdge[e.id] || []).map(nt => (nt.text || '').replace(/\r?\n/g, ' ')).join(' / ');
        lines.push([
          fmtDateLong(dayD),
          timeRange,
          '区間',
          `${fromN?.name || '?'} → ${toN?.name || '?'}`,
          e.label || rel?.label || '',
          dur,
          cost,
          notesTxt,
        ].map(fmtCsv).join(','));
      }
    });
    const csv = '﻿' + lines.join('\r\n');
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = (title || 'tour').replace(/[\\/:*?"<>|]/g, '_') + '_旅程.csv';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  };

  return (
    <div className="schedule-overlay" onClick={onClose}>
      <div className="schedule-panel" onClick={e=>e.stopPropagation()}>
        {/* ── HERO HEADER ── */}
        <div className="sch-hero">
          <button className="sch-close" onClick={onClose} title="閉じる">
            <window.TourIcon name="x" size={14}/>
          </button>
          <div className="sch-title-block">
            <div className="sch-eyebrow">
              <window.TourIcon name="route" size={14} stroke="currentColor"/> 工程表
            </div>
            <h2>{title || '旅程'}</h2>
            {subtitle && <div className="sch-sub">{subtitle}</div>}
            <div className="sch-stats">
              <span><b>{nodes.length}</b> 地点</span>
              <span>·</span>
              <span><b>{edges.length}</b> 区間</span>
              {days[0]?.date && <><span>·</span><span><b>{days.filter(d=>d.date).length}</b> 日</span></>}
            </div>
          </div>

          <div className="sch-actions">
            <button className="sch-btn-primary" onClick={exportIcs}
                    title="iCalendar (.ics) で書き出し — Apple / Google / Outlook で取り込める1ファイル">
              <span className="sch-btn-icon"><window.TourIcon name="calendar-plus" size={22} stroke="#fff"/></span>
              <span className="sch-btn-body">
                <span className="sch-btn-label">カレンダーに登録</span>
                <span className="sch-btn-sub">.ics — 全予定をまとめて</span>
              </span>
            </button>
            <button className="sch-btn-primary alt" onClick={exportItineraryCsv}
                    title="旅程をスプレッドシート形式で書き出し — Excel / Numbers / Google スプレッドシートで開けます">
              <span className="sch-btn-icon"><window.TourIcon name="list" size={22} stroke="#fff"/></span>
              <span className="sch-btn-body">
                <span className="sch-btn-label">旅程 CSV</span>
                <span className="sch-btn-sub">表形式で全行程を出力</span>
              </span>
            </button>
            <button className="sch-btn-primary alt" onClick={exportCsv}
                    title="経費精算用 CSV を書き出し — 旅費交通費/宿泊費/雑費/会議費/交際費に自動分類">
              <span className="sch-btn-icon"><window.TourIcon name="file-spreadsheet" size={22} stroke="#fff"/></span>
              <span className="sch-btn-body">
                <span className="sch-btn-label">経費精算 CSV</span>
                <span className="sch-btn-sub">5カテゴリ自動分類</span>
              </span>
            </button>
            <button className="sch-btn-secondary" onClick={()=>window.print()}>
              <window.TourIcon name="printer" size={14}/> 印刷
            </button>
          </div>
        </div>

        {/* View toggle: timeline / table — and notes ON/OFF */}
        {flatItems.length > 0 && (
          <div className="sch-view-toggle-row">
            <div className="sch-view-toggle">
              <button className={viewMode==='timeline'?'on':''} onClick={()=>setViewMode('timeline')}>
                <window.TourIcon name="route" size={12}/> タイムライン
              </button>
              <button className={viewMode==='table'?'on':''} onClick={()=>setViewMode('table')}>
                <window.TourIcon name="table" size={12}/> 表
              </button>
            </div>
            {notes.length > 0 && (
              <label className="sch-notes-toggle" title="メモを工程表に表示する">
                <input type="checkbox" checked={showNotes} onChange={e=>setShowNotes(e.target.checked)}/>
                <window.TourIcon name="sticky-note" size={13} stroke="currentColor"/>
                <span>メモを表示</span>
              </label>
            )}
          </div>
        )}

        {/* ── TIMELINE / TABLE ── */}
        {flatItems.length === 0 ? (
          <div style={{padding:'40px 20px', textAlign:'center', color:'var(--ink-muted)'}}>
            まだ地点・矢印がありません。
          </div>
        ) : viewMode === 'table' ? (
          <ScheduleTable days={days} dt={dt} fmtTime={fmtTime} cat={cat}
                         defaultCurrency={fallbackCur}
                         notesByNode={notesByNode} notesByEdge={notesByEdge}
                         orphanNotes={orphanNotes} showNotes={showNotes}/>
        ) : (
          <div className="sch-timeline">
            {days.map((day, di) => (
              <div key={di}>
                <div className="sch-day-sep">
                  <div className="sch-day-num">{day.label}</div>
                  <div className="sch-day-date">
                    {day.date
                      ? `${day.date.getFullYear()}/${day.date.getMonth()+1}/${day.date.getDate()} (${'日月火水木金土'[day.date.getDay()]})`
                      : '日時未定'}
                  </div>
                </div>
                {day.items.map((it, i) => {
                  if (it.kind === 'node'){
                    const n = it.node;
                    const stamp = n.icon;
                    const persp = it.perspective || 'full';   // 'full' | 'arrival' | 'departure'
                    const isDepView = persp === 'departure';  // shown at start of Day 2 etc.
                    const isArrView = persp === 'arrival';    // shown at end of Day 1 etc.
                    // Cost/stay info appears once — on the arrival side (or full single-day).
                    const stayMins = !isDepView && dt && n.arrival && n.departure
                      ? dt.minutesBetween(n.arrival, n.departure) : null;
                    const c = isDepView ? 0 : parseCost(n.cost);
                    const cTag = cat(n);
                    const hi = n.highlight || null;
                    return (
                      <div key={'i'+i} className="sch-row sch-row-node"
                           style={hi ? {borderLeft:`3px solid ${hi}`} : null}>
                        <div className="sch-time">
                          {/* Show only the relevant time per perspective so Day 2's
                              hotel reads as "発 8:00" rather than the full pair. */}
                          {(persp === 'full' || isArrView) && n.arrival && (
                            <div>着 <b>{fmtTime(n.arrival)}</b></div>
                          )}
                          {(persp === 'full' || isDepView) && n.departure && (
                            <div className={isDepView ? '' : 'muted'}>
                              発 {isDepView ? <b>{fmtTime(n.departure)}</b> : fmtTime(n.departure)}
                            </div>
                          )}
                          {!n.arrival && !n.departure && <div className="muted">—</div>}
                        </div>
                        <div className="sch-rail">
                          <div className="sch-bullet"
                               style={hi
                                 ? {background:hi, borderColor:hi}
                                 : it.group
                                   ? {background:it.group.color, borderColor:it.group.color}
                                   : null}>
                            {stamp && window.TourIcon
                              ? <window.TourIcon name={stamp} size={14} stroke="#fff"/>
                              : null}
                          </div>
                        </div>
                        <div className="sch-card sch-card-node"
                             style={hi ? {borderColor: hi, boxShadow:`inset 3px 0 0 ${hi}`} : null}>
                          <div className="sch-card-head">
                            {hi && (
                              <span style={{
                                display:'inline-flex', alignItems:'center', gap:3,
                                padding:'1px 7px', borderRadius:5,
                                background: hi, color:'#fff',
                                fontSize:10, fontWeight:800, letterSpacing:'.04em',
                              }}>
                                <window.TourIcon name="star-fill" size={9} stroke="#fff" fill="#fff"/>
                                注目
                              </span>
                            )}
                            <h4>{n.name || '(無名)'}</h4>
                            {n.subtitle && <span className="sch-sub2">{n.subtitle}</span>}
                            {(it.groups || []).map(g => (
                              <span key={g.id} className="sch-pill" style={{background:g.color}}>{g.name}</span>
                            ))}
                          </div>
                          {(stayMins || c > 0) && (
                            <div className="sch-card-meta">
                              {stayMins ? <span><window.TourIcon name="clock" size={12} stroke="currentColor"/> 滞在 <b>{dt.formatDuration(stayMins)}</b></span> : null}
                              {c > 0 && (
                                <span className={`sch-cost-tag ${cTag}`}>
                                  {categoryLabel(cTag)} {formatCostWith(extractCur(n.cost, fallbackCur), c)}
                                </span>
                              )}
                            </div>
                          )}
                          {/* Inline notes — one per anchored memo. Only on
                              the arrival/full perspective so multi-day hotels
                              don't repeat the memo on Day 2. */}
                          {showNotes && !isDepView && (notesByNode[n.id] || []).map(note => (
                            <div key={note.id} className="sch-note">
                              <window.TourIcon name="sticky-note" size={11} stroke="currentColor"/>
                              <span>{note.text || ''}</span>
                            </div>
                          ))}
                        </div>
                      </div>
                    );
                  }
                  // EDGE row
                  const e = it.edge;
                  const rel = window.RELATIONS?.find(rr=>rr.id===e.relation);
                  const stamp = e.stamp || rel?.stamp;
                  const c = parseCost(e.cost);
                  const transitMins = (dt && it.from?.departure && it.to?.arrival)
                    ? dt.minutesBetween(it.from.departure, it.to.arrival) : null;
                  return (
                    <div key={'i'+i} className="sch-row sch-row-edge">
                      <div className="sch-time">
                        {it.from?.departure && <div>発 <b>{fmtTime(it.from.departure)}</b></div>}
                        {it.to?.arrival     && <div className="muted">→ {fmtTime(it.to.arrival)}</div>}
                      </div>
                      <div className="sch-rail">
                        <div className="sch-tube" style={{background:e.color}}/>
                      </div>
                      <div className="sch-card sch-card-edge" style={{borderLeftColor:e.color}}>
                        <div className="sch-card-head">
                          <span className="sch-seq" style={{background:e.color}}>{e.seq ?? ''}</span>
                          {stamp && window.TourIcon
                            ? <span style={{display:'inline-flex',alignItems:'center',gap:4, color:e.color, fontWeight:700}}>
                                <window.TourIcon name={stamp} size={14}/>
                                {e.label || rel?.label || ''}
                              </span>
                            : <span style={{color:e.color, fontWeight:700}}>{e.label || rel?.label || ''}</span>}
                          <span className="sch-route">
                            {it.from?.name || '?'} → {it.to?.name || '?'}
                          </span>
                        </div>
                        {(transitMins || c > 0) && (
                          <div className="sch-card-meta">
                            {transitMins ? <span><window.TourIcon name="clock" size={12} stroke="currentColor"/> 移動 <b>{dt.formatDuration(transitMins)}</b></span> : null}
                            {c > 0 && (
                              <span className="sch-cost-tag transport">旅費交通費 {formatCostWith(extractCur(it.edge.cost, fallbackCur), c)}</span>
                            )}
                          </div>
                        )}
                        {showNotes && (notesByEdge[e.id] || []).map(note => (
                          <div key={note.id} className="sch-note">
                            <window.TourIcon name="sticky-note" size={11} stroke="currentColor"/>
                            <span>{note.text || ''}</span>
                          </div>
                        ))}
                      </div>
                    </div>
                  );
                })}
                {/* End-of-day orphan notes (anchor=null) — only show on the
                    last day so they don't repeat. */}
                {showNotes && di === days.length - 1 && orphanNotes.length > 0 && (
                  <div className="sch-orphan-notes">
                    <div className="sch-orphan-head">
                      <window.TourIcon name="sticky-note" size={12} stroke="currentColor"/> その他のメモ
                    </div>
                    {orphanNotes.map(note => (
                      <div key={note.id} className="sch-note sch-note-orphan">
                        <span>{note.text || ''}</span>
                      </div>
                    ))}
                  </div>
                )}
              </div>
            ))}
          </div>
        )}

        {/* ── TOTALS FOOTER ── */}
        {grandCost > 0 && (
          <div className="sch-totals">
            <div className="sch-totals-row">
              <div className="sch-cat">
                <div className="sch-cat-label">旅費交通費</div>
                <div className="sch-cat-val">{renderBucket(transportBucket)}</div>
              </div>
              <div className="sch-cat">
                <div className="sch-cat-label">宿泊費</div>
                <div className="sch-cat-val">{renderBucket(lodgingBucket)}</div>
              </div>
              <div className="sch-cat">
                <div className="sch-cat-label">雑費</div>
                <div className="sch-cat-val">{renderBucket(miscBucket)}</div>
              </div>
              {meetingCost > 0 && (
                <div className="sch-cat">
                  <div className="sch-cat-label">会議費</div>
                  <div className="sch-cat-val">{renderBucket(meetingBucket)}</div>
                </div>
              )}
              {entertainmentCost > 0 && (
                <div className="sch-cat">
                  <div className="sch-cat-label">交際費</div>
                  <div className="sch-cat-val">{renderBucket(entertainmentBucket)}</div>
                </div>
              )}
              <div className="sch-cat sch-cat-grand">
                <div className="sch-cat-label">合計</div>
                <div className="sch-cat-val">{renderBucket(grandBucket)}</div>
              </div>
            </div>
            <div className="sch-totals-hint">
              <window.TourIcon name="circle-help" size={12} stroke="currentColor"/> 経費精算 CSV はこの分類のまま書き出されます — Excel / Google Sheets / 既存の精算フォームに貼付け可能。
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

// Cost category → JP label mapping (used in tags / table cells / CSV).
function categoryLabel(k){
  switch(k){
    case 'lodging': return '宿泊費';
    case 'meeting': return '会議費';
    case 'entertainment': return '交際費';
    default: return '雑費';
  }
}

function parseCost(s){
  if (!s) return 0;
  // Same symbol set as SummaryBar — keep them in sync so totals match.
  const t = String(s)
    .replace(/A\$/g, '')
    .replace(/CHF/gi, '')
    .replace(/[,¥$€£円元₩฿₫\s]/g,'')
    .trim();
  const n = parseFloat(t);
  return isFinite(n) ? n : 0;
}
// Format an amount with an arbitrary currency symbol prefix. Integer-only
// currencies (yen, won) display whole numbers; fractional currencies
// (USD, EUR, etc.) keep up to 2 decimals to avoid rounding loss.
function formatCostWith(symbol, amt){
  const sym = symbol || '¥';
  const v = Math.round((amt || 0) * 100) / 100;
  if (v === Math.floor(v)) return sym + v.toLocaleString('en-US');
  return sym + v.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
// Backwards-compat: `formatCost` assumes ¥ — used in spots that haven't
// been threaded with the trip's defaultCurrency yet.
function formatCost(yen){ return formatCostWith('¥', yen); }

// ── Table view — Excel-like elegance, optimized for printing ───
function ScheduleTable({ days, dt, fmtTime, cat, defaultCurrency, notesByNode = {}, notesByEdge = {}, orphanNotes = [], showNotes = true }){
  const fmtDateOnly = (d) => d ? `${d.getMonth()+1}/${d.getDate()}` : '—';
  const fallbackCur = defaultCurrency || '¥';
  const extractCur = window.extractCurrency || ((_s, fb) => fb || '¥');

  return (
    <div className="sch-table-wrap">
      <table className="sch-table">
        <thead>
          <tr>
            <th style={{width:60}}>日付</th>
            <th style={{width:100}}>時刻</th>
            <th style={{width:60}}>種別</th>
            <th>内容</th>
            <th style={{width:115}} className="right">時間</th>
            <th style={{width:90}} className="right">金額</th>
            <th style={{width:90}} className="nowrap">分類</th>
          </tr>
        </thead>
        <tbody>
          {days.map((day, di) => (
            <React.Fragment key={di}>
              <tr className="day-row">
                <td colSpan={7}>
                  <span className="day-pill">{day.label}</span>
                  {day.date
                    ? `${day.date.getFullYear()}/${day.date.getMonth()+1}/${day.date.getDate()} (${'日月火水木金土'[day.date.getDay()]})`
                    : '日時未定'}
                </td>
              </tr>
              {day.items.map((it, i) => {
                if (it.kind === 'node'){
                  const n = it.node;
                  const persp = it.perspective || 'full';
                  const isDepView = persp === 'departure';
                  const isArrView = persp === 'arrival';
                  const c = isDepView ? 0 : parseCost(n.cost);
                  const cTag = cat(n);
                  const stayMins = !isDepView && dt && n.arrival && n.departure
                    ? dt.minutesBetween(n.arrival, n.departure) : null;
                  const d = isDepView
                    ? (dt?.toDate(n.departure) || null)
                    : (dt?.toDate(n.arrival) || dt?.toDate(n.departure));
                  const hi = n.highlight || null;
                  const ngroups = it.groups || [];
                  return (
                  <React.Fragment key={'n'+i}>
                    <tr style={hi ? {boxShadow:`inset 3px 0 0 ${hi}`} : null}>
                      <td className="ink-soft nowrap">{fmtDateOnly(d)}</td>
                      <td className="nowrap">
                        {(persp==='full' || isArrView) && n.arrival && (
                          <div>着 <b>{fmtTime(n.arrival)}</b></div>
                        )}
                        {(persp==='full' || isDepView) && n.departure && (
                          <div className={isDepView ? '' : 'ink-soft'}>
                            発 {isDepView ? <b>{fmtTime(n.departure)}</b> : fmtTime(n.departure)}
                          </div>
                        )}
                      </td>
                      <td className="ink-soft" style={{whiteSpace:'nowrap'}}>
                        <window.TourIcon name="map-pin" size={12} stroke="currentColor"/> 地点
                        {ngroups.length > 0 && (
                          <div style={{display:'flex', flexWrap:'wrap', gap:3, marginTop:4}}>
                            {ngroups.map(g => (
                              <span key={g.id} style={{
                                background:g.color, color:'#fff',
                                fontSize:10, fontWeight:700,
                                padding:'1px 6px', borderRadius:4,
                                whiteSpace:'nowrap',
                              }}>{g.name}</span>
                            ))}
                          </div>
                        )}
                      </td>
                      <td>
                        {hi && (
                          <span style={{
                            display:'inline-flex', alignItems:'center', gap:3, marginRight:6,
                            padding:'1px 7px', borderRadius:5,
                            background: hi, color:'#fff',
                            fontSize:10, fontWeight:800, letterSpacing:'.04em',
                            verticalAlign:'middle',
                          }}>
                            <window.TourIcon name="star-fill" size={9} stroke="#fff" fill="#fff"/>
                            注目
                          </span>
                        )}
                        <b>{n.name || '(無名)'}</b>
                        {n.subtitle && <span className="ink-soft" style={{marginLeft:6, fontSize:12}}>{n.subtitle}</span>}
                      </td>
                      <td className="right nowrap">
                        {stayMins ? <span>滞在 <b>{dt.formatDuration(stayMins)}</b></span> : ''}
                      </td>
                      <td className="right nowrap"><b>{c > 0 ? formatCostWith(extractCur(it.kind === 'node' ? it.node.cost : it.edge.cost, fallbackCur), c) : ''}</b></td>
                      <td className="nowrap">
                        {c > 0 && <span className={`sch-cost-tag ${cTag}`}>{categoryLabel(cTag)}</span>}
                      </td>
                    </tr>
                    {/* Inline notes — second row spans the content + right cols */}
                    {showNotes && !isDepView && (notesByNode[n.id] || []).map(note => (
                      <tr key={'nn'+i+note.id} className="sch-note-row">
                        <td colSpan={3}></td>
                        <td colSpan={4}>
                          <div className="sch-note">
                            <window.TourIcon name="sticky-note" size={11} stroke="currentColor"/>
                            <span>{note.text || ''}</span>
                          </div>
                        </td>
                      </tr>
                    ))}
                  </React.Fragment>
                  );
                }
                const e = it.edge;
                const rel = window.RELATIONS?.find(rr=>rr.id===e.relation);
                const c = parseCost(e.cost);
                const transitMins = (dt && it.from?.departure && it.to?.arrival)
                  ? dt.minutesBetween(it.from.departure, it.to.arrival) : null;
                const d = dt?.toDate(it.from?.departure) || dt?.toDate(it.to?.arrival);
                return (
                <React.Fragment key={'e'+i}>
                  <tr className="edge-row color-line" style={{borderLeft:`3px solid ${e.color}`}}>
                    <td className="ink-soft nowrap">{fmtDateOnly(d)}</td>
                    <td className="nowrap">
                      {it.from?.departure && <div>発 <b>{fmtTime(it.from.departure)}</b></div>}
                      {it.to?.arrival     && <div className="ink-soft">→ {fmtTime(it.to.arrival)}</div>}
                    </td>
                    <td>
                      <span className="sch-seq" style={{background:e.color}}>{e.seq ?? ''}</span>
                    </td>
                    <td>
                      <b style={{color:e.color}}>{e.label || rel?.label || ''}</b>
                      <span className="ink-soft" style={{marginLeft:6, fontSize:12}}>
                        {it.from?.name || '?'} → {it.to?.name || '?'}
                      </span>
                    </td>
                    <td className="right nowrap">
                      {transitMins ? <span>移動 <b>{dt.formatDuration(transitMins)}</b></span> : ''}
                    </td>
                    <td className="right nowrap"><b>{c > 0 ? formatCost(c) : ''}</b></td>
                    <td className="nowrap">
                      {c > 0 && <span className="sch-cost-tag transport">旅費交通費</span>}
                    </td>
                  </tr>
                  {showNotes && (notesByEdge[e.id] || []).map(note => (
                    <tr key={'en'+i+note.id} className="sch-note-row">
                      <td colSpan={3}></td>
                      <td colSpan={4}>
                        <div className="sch-note">
                          <window.TourIcon name="sticky-note" size={11} stroke="currentColor"/>
                          <span>{note.text || ''}</span>
                        </div>
                      </td>
                    </tr>
                  ))}
                </React.Fragment>
                );
              })}
              {/* Orphan notes at end of table (last day only) */}
              {showNotes && di === days.length - 1 && orphanNotes.length > 0 && (
                <tr className="sch-note-row">
                  <td colSpan={7}>
                    <div className="sch-orphan-notes" style={{margin:0}}>
                      <div className="sch-orphan-head">
                        <window.TourIcon name="sticky-note" size={12} stroke="currentColor"/> その他のメモ
                      </div>
                      {orphanNotes.map(note => (
                        <div key={note.id} className="sch-note sch-note-orphan">
                          <span>{note.text || ''}</span>
                        </div>
                      ))}
                    </div>
                  </td>
                </tr>
              )}
            </React.Fragment>
          ))}
        </tbody>
      </table>
    </div>
  );
}

window.Schedule = Schedule;
