// recurring.jsx — M4 Recurring (固定支出) feature components.
// Loaded BEFORE app.jsx; exposes everything via window.Recurring.
//
// Babel-standalone: no imports/exports. Uses React globals + window.LEDGER_API.

(function () {
  const { useState, useEffect, useMemo, useCallback } = React;

  // ── Local helpers ────────────────────────────────────────────────────
  function _fmt(n) {
    const v = Number(n) || 0;
    return v.toLocaleString('en-US');
  }
  function _parseDate(s) {
    if (!s) return null;
    const [y, m, d] = String(s).slice(0, 10).split('-').map(Number);
    if (!y || !m || !d) return null;
    return new Date(y, m - 1, d);
  }
  function _fmtMD(s) {
    const d = s instanceof Date ? s : _parseDate(s);
    if (!d) return '';
    return (d.getMonth() + 1) + '/' + d.getDate();
  }
  function _fmtYMD(s) {
    const d = s instanceof Date ? s : _parseDate(s);
    if (!d) return '';
    return d.getFullYear() + '/' + (d.getMonth() + 1) + '/' + d.getDate();
  }
  function _todayLocal() {
    const n = new Date();
    return new Date(n.getFullYear(), n.getMonth(), n.getDate());
  }
  function _todayIso() {
    const t = _todayLocal();
    const mm = String(t.getMonth() + 1).padStart(2, '0');
    const dd = String(t.getDate()).padStart(2, '0');
    return t.getFullYear() + '-' + mm + '-' + dd;
  }
  function _dayDelta(target) {
    const d = target instanceof Date ? target : _parseDate(target);
    if (!d) return null;
    const t = new Date(d); t.setHours(0, 0, 0, 0);
    const n = _todayLocal();
    return Math.round((t - n) / 86400000);
  }
  function _relativeDay(target) {
    const d = _dayDelta(target);
    if (d == null) return '';
    if (d === 0) return '今天';
    if (d === 1) return '明天';
    if (d === -1) return '昨天';
    if (d > 0) return d + ' 天後';
    return Math.abs(d) + ' 天前';
  }
  function _weekday(target) {
    const d = target instanceof Date ? target : _parseDate(target);
    if (!d) return '';
    return '日一二三四五六'[d.getDay()];
  }
  function _scheduleText(schedule) {
    if (!schedule) return '';
    if (schedule.type === 'monthly') {
      const days = (schedule.days || []).slice().sort((a, b) => a - b);
      return '每月 ' + days.join('、') + ' 日';
    }
    if (schedule.type === 'interval') {
      return '每 ' + schedule.days + ' 天';
    }
    return '';
  }

  function _catSlug(categories, categoryId) {
    if (!Array.isArray(categories) || categoryId == null) return 'other';
    const found = categories.find((c) => c.id === categoryId);
    return (found && found.slug) || 'other';
  }
  function _catName(categories, categoryId) {
    if (!Array.isArray(categories) || categoryId == null) return '';
    const found = categories.find((c) => c.id === categoryId);
    return (found && found.name) || '';
  }
  function _subName(categories, categoryId, subId) {
    if (!Array.isArray(categories) || categoryId == null || subId == null) return '';
    const cat = categories.find((c) => c.id === categoryId);
    if (!cat) return '';
    const sub = (cat.sub || []).find((s) => s.id === subId);
    return (sub && sub.name) || '';
  }

  // ── RuleTimeline ─────────────────────────────────────────────────────
  function RuleTimeline({ rule }) {
    const past = rule.last_fired_at ? [rule.last_fired_at] : [];
    const next = rule.enabled && rule.next_run ? [rule.next_run] : [];
    if (!past.length && !next.length) {
      return (
        <div className="rule-timeline">
          <span className="rule-timeline-lbl">紀錄</span>
          <span className="rule-timeline-amt">尚無自動產生</span>
        </div>
      );
    }
    return (
      <div className="rule-timeline">
        <span className="rule-timeline-lbl">紀錄</span>
        {past.map((d, i) => (
          <React.Fragment key={'p' + i}>
            <span className="tl-dot tl-past" title={'上次 ' + _fmtMD(d)} />
            <span className="tl-line" />
          </React.Fragment>
        ))}
        {next.map((d, i) => (
          <React.Fragment key={'n' + i}>
            <span className="tl-dot tl-next" title={'下次 ' + _fmtMD(d)} />
            {i < next.length - 1 && <span className="tl-line" />}
          </React.Fragment>
        ))}
      </div>
    );
  }

  // ── RuleCard ─────────────────────────────────────────────────────────
  function RuleCard({ rule, categories, onOpen, onEdit, onToggle, toggleBusy }) {
    const slug = _catSlug(categories, rule.category_id);
    const catName = _catName(categories, rule.category_id);
    const subName = _subName(categories, rule.category_id, rule.subcategory_id);
    const isOff = !rule.enabled;
    const delta = rule.enabled && rule.next_run ? _dayDelta(rule.next_run) : null;
    const isSoon = delta != null && delta >= 0 && delta <= 3;
    return (
      <div className={'rule-card ' + (isOff ? 'is-off' : '')}>
        <div className="rule-actions">
          <button
            type="button"
            className="rule-act-btn"
            onClick={(e) => { e.stopPropagation(); onEdit && onEdit(); }}
          >編輯</button>
        </div>
        <div className="rule-head" onClick={onOpen} style={{ cursor: 'pointer' }}>
          <span className={'rule-icon tx-cat-' + slug}>
            <span className="cat-dot" />
          </span>
          <div className="rule-head-text">
            <div className="rule-name">{rule.name}</div>
            <div className="rule-meta">
              <span>{_scheduleText(rule.schedule)}</span>
              {(catName || subName) && (
                <>
                  <span className="dot">·</span>
                  <span>{catName}{subName ? '／' + subName : ''}</span>
                </>
              )}
            </div>
          </div>
          <span
            className={'switch ' + (rule.enabled ? 'is-on' : '') + (toggleBusy ? ' is-busy' : '')}
            role="switch"
            aria-checked={rule.enabled}
            aria-busy={toggleBusy ? 'true' : undefined}
            onClick={(e) => {
              e.stopPropagation();
              if (toggleBusy) return;  // drop click while in-flight
              if (onToggle) onToggle();
            }}
          />
        </div>
        <div className="rule-body" onClick={onOpen} style={{ cursor: 'pointer' }}>
          <div className="rule-amt">
            <span className="mark">NT$</span>
            <span className="num">{_fmt(rule.amount)}</span>
          </div>
          <div className="rule-next">
            <span className="rule-next-date">
              {rule.enabled && rule.next_run ? '下次 ' + _fmtMD(rule.next_run) : '已停用'}
            </span>
            {rule.enabled && rule.next_run && (
              <span className={'rule-next-rel ' + (isSoon ? 'is-soon' : '')}>
                {_relativeDay(rule.next_run)}
              </span>
            )}
          </div>
        </div>
        <RuleTimeline rule={rule} />
        {(rule.fired_count > 0) && (
          <div className="rule-timeline-amt" style={{ marginTop: -8 }}>
            已自動產生 <b>{rule.fired_count}</b> 筆 · 合計 NT$ <b>{_fmt(rule.fired_total)}</b>
          </div>
        )}
      </div>
    );
  }

  // ── RecurringPage (list view) ────────────────────────────────────────
  function RecurringPage({ refreshKey, onCreateRule, onOpenRule, onEditRule }) {
    const [rules, setRules] = useState([]);
    const [stats, setStats] = useState(null);
    const [categories, setCategories] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    const [showOff, setShowOff] = useState(true);
    const [healthOpen, setHealthOpen] = useState(false);
    const [health, setHealth] = useState(null);
    // Per-rule in-flight guard. Repeated clicks on the same switch
    // before the previous /toggle round-trip completes would otherwise
    // fan out concurrent POSTs against a stale `rule.enabled` value.
    const [togglingIds, setTogglingIds] = useState(() => new Set());

    const reload = useCallback(() => {
      setLoading(true);
      setError(null);
      const api = window.LEDGER_API;
      return Promise.all([
        api.apiListRecurring(),
        api.apiRecurringStats(),
        api.apiCategoriesFull(),
      ])
        .then(([rs, st, cats]) => {
          setRules(rs);
          setStats(st);
          setCategories(cats);
          setLoading(false);
        })
        .catch((err) => {
          setError(err);
          setLoading(false);
        });
    }, []);

    useEffect(() => {
      let cancelled = false;
      setLoading(true);
      setError(null);
      const api = window.LEDGER_API;
      Promise.all([
        api.apiListRecurring(),
        api.apiRecurringStats(),
        api.apiCategoriesFull(),
      ])
        .then(([rs, st, cats]) => {
          if (cancelled) return;
          setRules(rs);
          setStats(st);
          setCategories(cats);
          setLoading(false);
        })
        .catch((err) => {
          if (cancelled) return;
          setError(err);
          setLoading(false);
        });
      return () => { cancelled = true; };
    }, [refreshKey]);

    const handleToggle = useCallback(async (rule) => {
      // Drop the click if a previous toggle for this rule is still in
      // flight — server already accepted one state change; the second
      // would post a stale rule.enabled and could end up wrong.
      let alreadyToggling = false;
      setTogglingIds((prev) => {
        if (prev.has(rule.id)) { alreadyToggling = true; return prev; }
        const next = new Set(prev); next.add(rule.id); return next;
      });
      if (alreadyToggling) return;
      try {
        await window.LEDGER_API.apiToggleRecurring(rule.id, !rule.enabled);
        await reload();
      } catch (err) {
        alert('切換失敗：' + (err.message || String(err)));
      } finally {
        setTogglingIds((prev) => {
          if (!prev.has(rule.id)) return prev;
          const next = new Set(prev); next.delete(rule.id); return next;
        });
      }
    }, [reload]);

    const openHealth = useCallback(async () => {
      try {
        const h = await window.LEDGER_API.apiRecurringHealth();
        setHealth(h);
        setHealthOpen(true);
      } catch (err) {
        alert('載入健康狀態失敗：' + (err.message || String(err)));
      }
    }, []);

    const active = useMemo(() => rules.filter((r) => r.enabled), [rules]);
    const inactive = useMemo(() => rules.filter((r) => !r.enabled), [rules]);

    return (
      <div className="recurring-page">
        <div className="page">
          <div className="ph">
            <div className="ph-l">
              <span className="ph-eyebrow">RECURRING · 自動記帳</span>
              <h2 className="ph-title">固定支出</h2>
              <span className="ph-sub">系統會依規則自動寫入記錄 · 與 Telegram 通知連動</span>
            </div>
            <div className="ph-r">
              <button type="button" className="btn-utility-ghost" onClick={openHealth}>健康狀態</button>
              <button type="button" className="btn-primary" onClick={onCreateRule}>
                <span className="plus">＋</span>新增規則
              </button>
            </div>
          </div>

          {stats && (
            <div className="mstat-row">
              <div className="mstat">
                <span className="mstat-k">本月已自動產生</span>
                <div className="mstat-v">
                  <span className="mark">NT$</span>
                  <span className="num">{_fmt(stats.this_month_fired_total || 0)}</span>
                </div>
                <div className="mstat-sub">
                  {stats.this_month_fired_count || 0} 筆 · 涵蓋 {active.length} 條規則
                </div>
              </div>
              <div className="mstat">
                <span className="mstat-k">下次自動記帳</span>
                <div className="mstat-v">
                  <span className="num">{stats.next_run ? _fmtMD(stats.next_run.next_run) : '—'}</span>
                  {stats.next_run && (
                    <span className="mstat-pill">{_relativeDay(stats.next_run.next_run)}</span>
                  )}
                </div>
                <div className="mstat-sub">
                  {stats.next_run
                    ? stats.next_run.name + ' · NT$ ' + _fmt(stats.next_run.amount)
                    : '無啟用規則'}
                </div>
              </div>
              <div className="mstat">
                <span className="mstat-k">每月預估支出</span>
                <div className="mstat-v">
                  <span className="mark">NT$</span>
                  <span className="num">{_fmt(stats.expected_monthly || 0)}</span>
                </div>
                <div className="mstat-sub">依 {active.length} 條啟用規則推算</div>
              </div>
            </div>
          )}

          {loading && (
            <div className="tx-empty"><div className="tx-empty-h">載入中…</div></div>
          )}
          {error && (
            <div className="tx-empty">
              <div className="tx-empty-h">載入失敗：{error.message || String(error)}</div>
            </div>
          )}

          {!loading && !error && (
            <>
              <div className="sec-head">
                <div className="sec-head-l">
                  <span className="sec-title">啟用中</span>
                  <span className="sec-count">{active.length} 條</span>
                </div>
                <div className="sec-head-r">點規則卡查看歷史紀錄 · 切換開關暫停 / 啟用</div>
              </div>
              {active.length === 0 ? (
                <div className="tx-empty"><div className="tx-empty-h">尚未建立規則</div></div>
              ) : (
                <div className="rule-grid">
                  {active.map((r) => (
                    <RuleCard
                      key={r.id}
                      rule={r}
                      categories={categories}
                      onOpen={() => onOpenRule && onOpenRule(r.id)}
                      onEdit={() => onEditRule && onEditRule(r)}
                      onToggle={() => handleToggle(r)}
                      toggleBusy={togglingIds.has(r.id)}
                    />
                  ))}
                </div>
              )}

              {inactive.length > 0 && (
                <div className="disabled-block">
                  <div className="sec-head">
                    <div className="sec-head-l">
                      <button
                        type="button"
                        className="sec-title"
                        style={{ background: 'transparent', border: 0, padding: 0, cursor: 'pointer' }}
                        onClick={() => setShowOff((v) => !v)}
                      >
                        已停用 {showOff ? '▾' : '▸'}
                      </button>
                      <span className="sec-count">{inactive.length} 條</span>
                    </div>
                    <div className="sec-head-r">停用中不會自動產生紀錄；可隨時切回啟用</div>
                  </div>
                  {showOff && (
                    <div className="rule-grid">
                      {inactive.map((r) => (
                        <RuleCard
                          key={r.id}
                          rule={r}
                          categories={categories}
                          onOpen={() => onOpenRule && onOpenRule(r.id)}
                          onEdit={() => onEditRule && onEditRule(r)}
                          onToggle={() => handleToggle(r)}
                        />
                      ))}
                    </div>
                  )}
                </div>
              )}
            </>
          )}

          {healthOpen && health && (
            <HealthModal health={health} onClose={() => setHealthOpen(false)} />
          )}
        </div>
      </div>
    );
  }

  // ── HealthModal ──────────────────────────────────────────────────────
  function HealthModal({ health, onClose }) {
    return (
      <div className="rec-panel-frame" onClick={onClose}>
        <div className="rec-panel" onClick={(e) => e.stopPropagation()}>
          <div className="rec-panel-head">
            <button type="button" className="rec-panel-x" onClick={onClose}>✕</button>
            <span className="rec-panel-eyebrow">排程健康狀態</span>
          </div>
          <div className="rec-panel-fields" style={{ paddingTop: 18, borderTop: 0 }}>
            <div className="fp-section">
              <div className="fp-helper" style={{ background: 'var(--parchment)', padding: '10px 12px', borderRadius: 9, lineHeight: 1.7 }}>
                <div>排程啟用：<b>{health.scheduler_enabled ? '是' : '否'}</b></div>
                <div>排程運行中：<b>{health.scheduler_running ? '是' : '否'}</b></div>
                <div>最近 tick：{health.last_tick_at || '—'}</div>
                <div>最近成功：{health.last_success_at || '—'}</div>
                <div>最近錯誤：{health.last_error || '—'}</div>
                <div>上次處理 / 產生：{health.last_processed_count || 0} / {health.last_created_count || 0}</div>
                <div>上次耗時：{health.last_duration_ms || 0} ms</div>
                <div>待 fire 規則數：{health.next_due_count || 0}</div>
                <div>最早待 fire：{health.oldest_due_next_run || '—'}</div>
              </div>
            </div>
          </div>
          <div className="rec-panel-foot">
            <div className="spacer" />
            <button type="button" className="btn-cancel" onClick={onClose}>關閉</button>
          </div>
        </div>
      </div>
    );
  }

  // ── RuleEditPanel ────────────────────────────────────────────────────
  function RuleEditPanel({ mode, rule, categories: catsProp, onClose, onSaved, onDelete }) {
    const isEdit = mode === 'edit' && rule;
    // Categories prop may be the lightweight {id:name, slug, name, sub} shape
    // from app.jsx — for create/edit we need int IDs, so fetch the full list.
    const [categories, setCategories] = useState([]);
    useEffect(() => {
      let cancelled = false;
      window.LEDGER_API.apiCategoriesFull()
        .then((cats) => { if (!cancelled) setCategories(cats); })
        .catch(() => { if (!cancelled) setCategories([]); });
      return () => { cancelled = true; };
    }, []);

    // Initial state pulled from rule when editing.
    const initSched = isEdit ? rule.schedule : { type: 'monthly', days: [_todayLocal().getDate()] };
    const [name, setName] = useState(isEdit ? rule.name : '');
    const [scheduleKind, setScheduleKind] = useState(initSched.type || 'monthly');
    const [monthlyDays, setMonthlyDays] = useState(() => {
      if (initSched.type === 'monthly') return new Set(initSched.days || []);
      return new Set([_todayLocal().getDate()]);
    });
    const [intervalDays, setIntervalDays] = useState(() => {
      if (initSched.type === 'interval') return initSched.days || 7;
      return 7;
    });
    const [startDate, setStartDate] = useState(isEdit ? rule.start_date : _todayIso());
    const [endDate, setEndDate] = useState(isEdit && rule.end_date ? rule.end_date : '');
    const [amount, setAmount] = useState(isEdit ? String(rule.amount || '') : '');
    const [categoryId, setCategoryId] = useState(isEdit ? rule.category_id : null);
    const [subId, setSubId] = useState(isEdit ? rule.subcategory_id : null);
    const [item, setItem] = useState(isEdit ? (rule.item || '') : '');
    const [note, setNote] = useState(isEdit ? (rule.note || '') : '');
    const [enabled, setEnabled] = useState(isEdit ? !!rule.enabled : true);
    const [notify, setNotify] = useState(isEdit ? !!rule.notify : true);
    const [busy, setBusy] = useState(false);
    const [err, setErr] = useState(null);

    const activeCat = categories.find((c) => c.id === categoryId);
    const hasRiskyDay = scheduleKind === 'monthly'
      && [29, 30, 31].some((d) => monthlyDays.has(d));

    const toggleDay = (d) => {
      setMonthlyDays((cur) => {
        const next = new Set(cur);
        if (next.has(d)) next.delete(d); else next.add(d);
        return next;
      });
    };

    // When categories load and we're creating from scratch, pre-pick first.
    useEffect(() => {
      if (!categories.length) return;
      if (categoryId == null) {
        const first = categories[0];
        setCategoryId(first.id);
        if (first.sub && first.sub.length) setSubId(first.sub[0].id);
      } else if (subId == null && activeCat && activeCat.sub && activeCat.sub.length) {
        setSubId(activeCat.sub[0].id);
      }
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [categories]);

    const buildPayload = () => {
      const trimmed = name.trim();
      if (!trimmed) throw new Error('請輸入名稱');
      if (!amount || Number(amount) < 0) throw new Error('請輸入金額');
      if (categoryId == null) throw new Error('請選擇分類');
      let schedule;
      if (scheduleKind === 'monthly') {
        const days = Array.from(monthlyDays).sort((a, b) => a - b);
        if (!days.length) throw new Error('請選擇至少一個月份日期');
        schedule = { type: 'monthly', days };
      } else {
        const n = parseInt(intervalDays, 10);
        if (!n || n < 1 || n > 365) throw new Error('間隔天數必須是 1-365');
        schedule = { type: 'interval', days: n };
      }
      const payload = {
        name: trimmed,
        schedule,
        start_date: startDate || _todayIso(),
        amount: Number(amount),
        category_id: categoryId,
        item: item.trim(),
        note: note.trim(),
        enabled,
        notify,
      };
      // end_date and subcategory_id are nullable on the server. In edit
      // mode we must send explicit JSON null when the user cleared the
      // field, otherwise RulePatch leaves the old value untouched and
      // the UI silently drops the user's intent. Create mode treats
      // missing == null so we can keep the payload tight there.
      if (isEdit) {
        payload.end_date = endDate || null;
        payload.subcategory_id = subId != null ? subId : null;
      } else {
        if (subId != null) payload.subcategory_id = subId;
        if (endDate) payload.end_date = endDate;
      }
      return payload;
    };

    const save = async () => {
      if (busy) return;
      setBusy(true);
      setErr(null);
      try {
        const payload = buildPayload();
        let saved;
        if (isEdit) {
          saved = await window.LEDGER_API.apiUpdateRecurring(rule.id, payload);
        } else {
          saved = await window.LEDGER_API.apiCreateRecurring(payload);
        }
        if (onSaved) onSaved(saved);
      } catch (e) {
        setErr(e.message || String(e));
        setBusy(false);
      }
    };

    const remove = async () => {
      if (!isEdit) return;
      if (!window.confirm('刪除此規則？歷史紀錄會保留但不再標記為自動。')) return;
      setBusy(true);
      try {
        await window.LEDGER_API.apiDeleteRecurring(rule.id);
        if (onDelete) onDelete(rule.id);
      } catch (e) {
        setErr(e.message || String(e));
        setBusy(false);
      }
    };

    return (
      <div className="recurring-page rec-panel-frame">
        <div className="rec-panel" onClick={(e) => e.stopPropagation()}>
          <div className="rec-panel-head">
            <button type="button" className="rec-panel-x" onClick={onClose}>✕</button>
            <span className="rec-panel-eyebrow">{isEdit ? '編輯規則' : '新增規則'}</span>
          </div>

          <div className="rec-panel-fields" style={{ paddingTop: 18, borderTop: 0 }}>
            <div className="fp-section">
              <span className="df-label">名稱</span>
              <input
                className="df-input"
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder="例：Netflix、健身房月費"
                autoFocus
              />
            </div>

            <div className="fp-section">
              <span className="df-label">排程</span>
              <div className="fp-segmented">
                <button
                  type="button"
                  className={scheduleKind === 'monthly' ? 'is-on' : ''}
                  onClick={() => setScheduleKind('monthly')}
                >每月固定日</button>
                <button
                  type="button"
                  className={scheduleKind === 'interval' ? 'is-on' : ''}
                  onClick={() => setScheduleKind('interval')}
                >每 N 天</button>
              </div>

              {scheduleKind === 'monthly' && (
                <>
                  <div className="day-grid" style={{ marginTop: 4 }}>
                    {Array.from({ length: 31 }, (_, i) => i + 1).map((d) => (
                      <button
                        type="button"
                        key={d}
                        className={
                          'day-chip ' +
                          (monthlyDays.has(d) ? 'is-on ' : '') +
                          (d >= 29 ? 'is-warn' : '')
                        }
                        onClick={() => toggleDay(d)}
                      >{d}</button>
                    ))}
                  </div>
                  {hasRiskyDay && (
                    <div className="fp-helper is-warn">
                      <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
                        <circle cx="8" cy="8" r="6.5" />
                        <path d="M8 5v3.5M8 10.6v.4" strokeLinecap="round" />
                      </svg>
                      <span>
                        29 / 30 / 31 日：2、4、6、9、11 月該日不存在時自動跳到下一個合法日，<b>不會</b>退為該月最後一日。
                      </span>
                    </div>
                  )}
                </>
              )}

              {scheduleKind === 'interval' && (
                <div className="interval-row" style={{ marginTop: 4 }}>
                  <span className="unit">每</span>
                  <input
                    type="number"
                    min="1"
                    max="365"
                    value={intervalDays}
                    onChange={(e) => setIntervalDays(+e.target.value || 1)}
                  />
                  <span className="unit">天 fire 一次 · 從 start_date 起算</span>
                </div>
              )}
            </div>

            <div className="fp-section">
              <div className="fp-row-2">
                <div>
                  <span className="df-label">開始日期</span>
                  <input
                    className="df-input"
                    type="date"
                    value={startDate}
                    onChange={(e) => setStartDate(e.target.value)}
                  />
                </div>
                <div>
                  <span className="df-label">結束日期（可選）</span>
                  <input
                    className="df-input"
                    type="date"
                    value={endDate}
                    onChange={(e) => setEndDate(e.target.value)}
                  />
                </div>
              </div>
              <div className="fp-helper">空白代表永久執行 · 結束日當天仍會 fire 一次</div>
            </div>

            <div className="fp-section">
              <span className="df-label">金額</span>
              <div className="money-input-wrap">
                <span className="mark">NT$</span>
                <input
                  inputMode="numeric"
                  value={amount}
                  onChange={(e) => setAmount(e.target.value.replace(/[^\d.]/g, ''))}
                  placeholder="0"
                />
              </div>
            </div>

            <div className="fp-section">
              <span className="df-label">分類</span>
              <div className="df-pills">
                {categories.map((c) => (
                  <button
                    type="button"
                    key={c.id}
                    className={'df-pill ' + (categoryId === c.id ? 'is-on' : '')}
                    onClick={() => {
                      setCategoryId(c.id);
                      const firstSub = c.sub && c.sub[0];
                      setSubId(firstSub ? firstSub.id : null);
                    }}
                  >
                    <span className={'cat-dot tx-cat-' + (c.slug || 'other')} />
                    {c.name}
                  </button>
                ))}
              </div>
              {activeCat && activeCat.sub && activeCat.sub.length > 0 && (
                <>
                  <span className="df-label" style={{ marginTop: 10 }}>子分類</span>
                  <div className="df-pills">
                    {activeCat.sub.map((s) => (
                      <button
                        type="button"
                        key={s.id}
                        className={'df-pill ' + (subId === s.id ? 'is-on' : '')}
                        onClick={() => setSubId(s.id)}
                      >{s.name}</button>
                    ))}
                  </div>
                </>
              )}
            </div>

            <div className="fp-section">
              <span className="df-label">品項（寫入 record）</span>
              <input
                className="df-input"
                value={item}
                onChange={(e) => setItem(e.target.value)}
                placeholder="例：長榮街公寓、Netflix 高級方案"
              />
            </div>

            <div className="fp-section">
              <span className="df-label">備註</span>
              <input
                className="df-input"
                value={note}
                onChange={(e) => setNote(e.target.value)}
                placeholder="可選"
              />
            </div>

            <div className="fp-switches">
              <div className="fp-switch-row">
                <div className="fp-switch-meta">
                  <span className="fp-switch-title">啟用</span>
                  <span className="fp-switch-sub">關閉後不會自動產生紀錄，但歷史紀錄保留</span>
                </div>
                <span
                  className={'switch switch-sm ' + (enabled ? 'is-on' : '')}
                  role="switch"
                  aria-checked={enabled}
                  onClick={() => setEnabled((v) => !v)}
                />
              </div>
              <div className="fp-switch-row">
                <div className="fp-switch-meta">
                  <span className="fp-switch-title">Telegram 通知</span>
                  <span className="fp-switch-sub">每次自動記帳後，bot 會發送該筆摘要</span>
                </div>
                <span
                  className={'switch switch-sm ' + (notify ? 'is-on' : '')}
                  role="switch"
                  aria-checked={notify}
                  onClick={() => setNotify((v) => !v)}
                />
              </div>
            </div>

            {err && <div className="fp-helper is-warn">錯誤：{err}</div>}
          </div>

          <div className="rec-panel-foot">
            {isEdit && (
              <button type="button" className="btn-text-danger" onClick={remove} disabled={busy}>
                刪除規則
              </button>
            )}
            <div className="spacer" />
            <button type="button" className="btn-cancel" onClick={onClose} disabled={busy}>取消</button>
            <button type="button" className="btn-save" onClick={save} disabled={busy || !name.trim()}>
              {isEdit ? '儲存變更' : '建立規則'}
            </button>
          </div>
        </div>
      </div>
    );
  }

  // ── RuleDetailView ───────────────────────────────────────────────────
  function RuleDetailView({ ruleId, refreshKey, onBack, onEdit, onDelete }) {
    const [rule, setRule] = useState(null);
    const [history, setHistory] = useState([]);
    const [categories, setCategories] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
      let cancelled = false;
      setLoading(true);
      setError(null);
      const api = window.LEDGER_API;
      Promise.all([
        api.apiGetRecurring(ruleId),
        api.apiListRuleRecords(ruleId, 50),
        api.apiCategoriesFull(),
      ])
        .then(([r, recs, cats]) => {
          if (cancelled) return;
          setRule(r);
          setHistory(recs);
          setCategories(cats);
          setLoading(false);
        })
        .catch((err) => {
          if (cancelled) return;
          setError(err);
          setLoading(false);
        });
      return () => { cancelled = true; };
    }, [ruleId, refreshKey]);

    // Per-request guard: drop the click while a previous toggle on this
    // rule is in flight; the second call would post a stale rule.enabled.
    const [toggling, setToggling] = useState(false);
    const handleToggle = async () => {
      if (!rule || toggling) return;
      setToggling(true);
      try {
        const updated = await window.LEDGER_API.apiToggleRecurring(rule.id, !rule.enabled);
        setRule(updated);
      } catch (err) {
        alert('切換失敗：' + (err.message || String(err)));
      } finally {
        setToggling(false);
      }
    };

    const handleDelete = async () => {
      if (!rule) return;
      if (!window.confirm('刪除此規則？歷史紀錄會保留但不再標記為自動。')) return;
      try {
        await window.LEDGER_API.apiDeleteRecurring(rule.id);
        if (onDelete) onDelete(rule.id);
      } catch (err) {
        alert('刪除失敗：' + (err.message || String(err)));
      }
    };

    if (loading) {
      return (
        <div className="recurring-page"><div className="page">
          <button type="button" className="dv-back" onClick={onBack}>← 全部規則</button>
          <div className="tx-empty"><div className="tx-empty-h">載入中…</div></div>
        </div></div>
      );
    }
    if (error || !rule) {
      return (
        <div className="recurring-page"><div className="page">
          <button type="button" className="dv-back" onClick={onBack}>← 全部規則</button>
          <div className="tx-empty"><div className="tx-empty-h">載入失敗：{error ? (error.message || String(error)) : '找不到規則'}</div></div>
        </div></div>
      );
    }

    const slug = _catSlug(categories, rule.category_id);
    const catName = _catName(categories, rule.category_id);
    const subName = _subName(categories, rule.category_id, rule.subcategory_id);

    // Average interval days — only meaningful if we know first_fire_at and
    // there have been at least 2 fires.
    let avgInterval = '—';
    if (rule.first_fire_at && rule.fired_count > 1) {
      const first = _parseDate(rule.first_fire_at);
      const today = _todayLocal();
      const spanDays = Math.max(0, Math.round((today - first) / 86400000));
      const per = spanDays / (rule.fired_count - 1);
      if (per > 0 && Number.isFinite(per)) avgInterval = per.toFixed(1) + ' 天';
    }

    return (
      <div className="recurring-page">
        <div className="page">
          <button type="button" className="dv-back" onClick={onBack}>← 全部規則</button>

          <div className={'rd-hero tx-cat-' + slug}>
            <span className="rd-hero-icon">
              <span className="cat-dot" />
            </span>
            <div className="rd-hero-mid">
              <span className="rd-hero-name">{rule.name}</span>
              <div className="rd-hero-meta">
                <span>{_scheduleText(rule.schedule)}</span>
                {(catName || subName) && (
                  <>
                    <span className="dot">·</span>
                    <span>{catName}{subName ? '／' + subName : ''}</span>
                  </>
                )}
                {rule.item && (
                  <>
                    <span className="dot">·</span>
                    <span>{rule.item}</span>
                  </>
                )}
              </div>
              <div className="rd-hero-amt">
                <span className="mark">NT$</span>
                <span className="num">{_fmt(rule.amount)}</span>
              </div>
            </div>
            <div className="rd-hero-r">
              <span
                className={'switch ' + (rule.enabled ? 'is-on' : '') + (toggling ? ' is-busy' : '')}
                role="switch"
                aria-checked={rule.enabled}
                aria-busy={toggling ? 'true' : undefined}
                onClick={handleToggle}
              />
              {rule.enabled && rule.next_run && (
                <div className="rd-next-card">
                  <span className="rd-next-eyebrow">下次自動記帳</span>
                  <span className="rd-next-date">{_fmtMD(rule.next_run)}</span>
                  <span className="rd-next-rel-pill">{_relativeDay(rule.next_run)}</span>
                </div>
              )}
              <div style={{ display: 'flex', gap: 6 }}>
                <button type="button" className="rule-act-btn" onClick={() => onEdit && onEdit(rule)}>編輯</button>
                <button type="button" className="btn-text-danger" style={{ padding: '0 8px' }} onClick={handleDelete}>刪除</button>
              </div>
            </div>
          </div>

          <div className="stat-strip">
            <div className="ss-cell">
              <span className="ss-k">已自動產生</span>
              <span className="ss-v">{rule.fired_count || 0} 筆</span>
              <span className="ss-sub">
                {rule.first_fire_at ? '最早 ' + _fmtYMD(rule.first_fire_at) : '尚未 fire'}
              </span>
            </div>
            <div className="ss-cell">
              <span className="ss-k">合計金額</span>
              <span className="ss-v">
                <span className="mark">NT$</span>{_fmt(rule.fired_total || 0)}
              </span>
              <span className="ss-sub">所有自動產生紀錄</span>
            </div>
            <div className="ss-cell">
              <span className="ss-k">平均週期</span>
              <span className="ss-v">{avgInterval}</span>
              <span className="ss-sub">由首次至今</span>
            </div>
            <div className="ss-cell">
              <span className="ss-k">通知</span>
              <span className="ss-v">{rule.notify ? '已開' : '已關'}</span>
              <span className="ss-sub">Telegram bot</span>
            </div>
          </div>

          <div className="history-card">
            <div className="history-head">
              <div className="history-head-l">
                <div className="sec-title">產生紀錄</div>
                <div className="history-head-meta">共 {history.length} 筆</div>
              </div>
            </div>
            {history.length === 0 && (
              <div className="tx-empty"><div className="tx-empty-h">尚無自動產生紀錄</div></div>
            )}
            {history.map((h) => (
              <div className="history-row" key={h.id}>
                <div className="h-date">
                  <b>{_fmtYMD(h.date)}</b>
                  <span>
                    {_weekday(h.date)}
                    {_dayDelta(h.date) != null && (
                      <> · {_dayDelta(h.date) === 0 ? '今天' : Math.abs(_dayDelta(h.date)) + ' 天前'}</>
                    )}
                  </span>
                </div>
                <div>
                  <span className="h-name">{h.item || rule.item || rule.name}</span>
                  {h.note && <span className="h-note">· {h.note}</span>}
                  {h.auto !== false && <span className="auto-badge">⤿ 自動</span>}
                </div>
                <div className="h-sub">
                  <span className={'cat-dot tx-cat-' + slug} />
                  {catName}{h.sub_category ? '／' + h.sub_category : ''}
                </div>
                <div className="h-amt">
                  <span className="mark">NT$</span>{_fmt(h.amount)}
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>
    );
  }

  // ── Export to global ─────────────────────────────────────────────────
  window.Recurring = window.Recurring || {};
  Object.assign(window.Recurring, {
    RecurringPage,
    RuleCard,
    RuleEditPanel,
    RuleDetailView,
    HealthModal,
  });
})();
