// FormParts — 小さなモーダルの部品(K18-49): 年月日の選択(MrDatePicker)とラジオ(MrRadioGroup)。
//
// 池田さん版(reference/ikeda-2026-09-16/screens の DS DatePicker・Choice)の見た目に、
// 既存のトークン(app/ui/index.html :root)の範囲で寄せる。DS の部品を取り込むか(K18-40)は
// 大島さんの判断待ちなので、DS の束(_ds_bundle.js)は読まない。表示の形('yyyy年mm月dd日（曜）')と
// 使い方(入力欄を押すとカレンダーが開く・打っても入れられる)は DS に合わせてあるので、
// 取り込みが決まれば呼び出し側をそのまま DS の部品へ差し替えられる。
//
// ■ 値の約束(ここが要 ─ docs/21 K18-49「mstore-diff.js の M/D の解釈とそろえる」)
//   画面の店(MStore)は日付を「M/D」(年なし)で持ち、書き込み層(app/write/mstore-diff.js uiDateToIso)が
//   「年の無い表記が半年(183日)より先に見えるなら去年」の規則で年を補って保存する。
//   選択で年まで選べるようにすると、選んだ年と、書き込み層が補う年が食い違いうる
//   (例: 9/24 に来年の 4/1 を選ぶ → "4/1" は 183 日以内に見えないので去年の 4/1 として保存される)。
//   そこで、この部品が店へ返す値は
//     ・「M/D」で書いて uiDateToIso が同じ日に戻すなら「M/D」(今までと同じ形。ほとんどはこれ)
//     ・戻らないなら「YYYY/M/D」(uiDateToIso は年つきも読む ─ 選んだ日がそのまま保存される)
//   にする。店の値を表示するときも同じ規則で年を補う(mrUiToDate)。規則を変えるときは
//   uiDateToIso と tests/ui/form-parts.test.js を一緒に直すこと。

const mrPad2 = n => String(n).padStart(2, "0");
const MR_WD = ["日", "月", "火", "水", "木", "金", "土"];
const MR_DAY_MS = 86400000;
const mrDay = d => new Date(d.getFullYear(), d.getMonth(), d.getDate());
const mrSameDay = (a, b) => !!(a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate());
// 2/30 のような存在しない日は読まない(Date は黙って 3/2 に繰り上げるため)。
const mrYmd = (y, m, d) => {
  const t = new Date(y, m - 1, d);
  return (t.getFullYear() === y && t.getMonth() === m - 1 && t.getDate() === d) ? t : null;
};

/** 店の日付表記("M/D"・"MM/DD"・"YYYY/M/D")→ Date。読める形は uiDateToIso(app/write/mstore-diff.js)と同じで、
 *  年の無い表記は同じ規則で年を補う。読めなければ null(存在しない日も null)。 */
function mrUiToDate(s, today) {
  if (s === null || s === undefined) return null;
  const t = String(s).trim();
  if (t === "" || t === "—" || t === "-") return null;
  let m = /^(\d{4})\/(\d{1,2})\/(\d{1,2})$/.exec(t);
  if (m) return mrYmd(+m[1], +m[2], +m[3]);
  m = /^(\d{1,2})\/(\d{1,2})$/.exec(t);
  if (!m) return null;
  const mo = +m[1], d = +m[2];
  const base = mrDay(today || new Date());
  let y = base.getFullYear();
  if (new Date(y, mo - 1, d) - base > 183 * MR_DAY_MS) y -= 1;
  return mrYmd(y, mo, d);
}

/** Date → 店の日付表記。"M/D" で書いて同じ日に戻るなら "M/D"、戻らないなら "YYYY/M/D"。 */
function mrDateToUi(date, today) {
  if (!date) return "";
  const md = (date.getMonth() + 1) + "/" + date.getDate();
  return mrSameDay(mrUiToDate(md, today), date) ? md : date.getFullYear() + "/" + md;
}

/** 表示の形(DS の DatePicker と同じ): 2026年09月24日（木） */
function mrFmtJa(date) {
  return date.getFullYear() + "年" + mrPad2(date.getMonth() + 1) + "月" + mrPad2(date.getDate()) + "日（" + MR_WD[date.getDay()] + "）";
}

/** 入力欄に打った文字 → Date。'2026年9月24日（木）'・'2026/9/24'・'2026-09-24'・'9/24'・'9月24日' を読む。
 *  全角の数字・スラッシュも読む(現場の端末で打つと全角になりやすい)。 */
function mrParseTyped(s, today) {
  const t = String(s == null ? "" : s).trim()
    .replace(/[０-９]/g, c => String.fromCharCode(c.charCodeAt(0) - 0xFEE0))
    .replace(/[／－-]/g, "/")
    .replace(/\s*[（(][日月火水木金土][）)]$/, "").trim();
  if (!t) return null;
  let m = /^(\d{4})年(\d{1,2})月(\d{1,2})日$/.exec(t);
  if (m) return mrYmd(+m[1], +m[2], +m[3]);
  m = /^(\d{1,2})月(\d{1,2})日$/.exec(t);
  if (m) return mrUiToDate(m[1] + "/" + m[2], today);
  return mrUiToDate(t, today);
}

/* カレンダーの日のボタン(DS の DayCell と同じ見た目) */
function MrDayCell({ d, selected, isToday, onPick }) {
  const [hov, setHov] = React.useState(false);
  const weekend = d.getDay() === 0 || d.getDay() === 6;
  return (
    <button type="button" aria-label={mrFmtJa(d)} aria-pressed={selected} onClick={() => onPick(d)}
      onMouseEnter={() => setHov(true)} onMouseLeave={() => setHov(false)}
      style={{ all: "unset", boxSizing: "border-box", height: 28, display: "flex", alignItems: "center", justifyContent: "center",
        borderRadius: "var(--radius-md)", fontVariantNumeric: "tabular-nums", cursor: "pointer",
        color: selected ? "var(--brand-foreground)" : weekend ? "var(--muted-foreground)" : "var(--foreground)",
        fontWeight: selected || isToday ? 600 : 400,
        background: selected ? "var(--brand)" : hov ? "var(--muted)" : "transparent",
        border: "1px solid " + (selected ? "var(--brand)" : isToday ? "var(--border-strong)" : "transparent") }}>
      {d.getDate()}
    </button>
  );
}

function MrCalNav({ children, onClick, label }) {
  const [hov, setHov] = React.useState(false);
  return (
    <button type="button" aria-label={label} onClick={onClick} onMouseEnter={() => setHov(true)} onMouseLeave={() => setHov(false)}
      style={{ all: "unset", boxSizing: "border-box", height: 22, padding: "0 6px", borderRadius: "var(--radius-md)",
        fontSize: "var(--text-body-sm)", color: "var(--muted-foreground)", cursor: "pointer",
        background: hov ? "var(--muted)" : "transparent", border: "1px solid " + (hov ? "var(--border-strong)" : "transparent") }}>
      {children}
    </button>
  );
}

/* 月のカレンダー(DS の Calendar と同じ並び: 前月・年月・翌月 / 日〜土 / 今日) */
function MrCalendar({ value, view, onView, today, onPick }) {
  const y = view.getFullYear(), m = view.getMonth();
  const first = new Date(y, m, 1).getDay(), days = new Date(y, m + 1, 0).getDate();
  const cells = [];
  for (let i = 0; i < first; i++) cells.push(null);
  for (let d = 1; d <= days; d++) cells.push(new Date(y, m, d));
  while (cells.length % 7) cells.push(null);
  return (
    <div style={{ width: 252, padding: 10, background: "var(--card)", border: "1px solid var(--border)", borderRadius: "var(--radius-lg)",
      fontFamily: "var(--font-sans)", fontSize: "var(--text-body-md)", userSelect: "none", boxSizing: "border-box" }}>
      <div style={{ display: "flex", alignItems: "center", height: 24, marginBottom: 6 }}>
        <MrCalNav label="前の月" onClick={() => onView(new Date(y, m - 1, 1))}>前月</MrCalNav>
        <span style={{ flex: 1, textAlign: "center", fontWeight: 600, fontVariantNumeric: "tabular-nums" }}>{y}年{mrPad2(m + 1)}月</span>
        <MrCalNav label="次の月" onClick={() => onView(new Date(y, m + 1, 1))}>翌月</MrCalNav>
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(7,1fr)", gap: 2 }}>
        {MR_WD.map((w, i) => (
          <div key={w} style={{ height: 20, display: "flex", alignItems: "center", justifyContent: "center", fontSize: "var(--text-caption)",
            fontWeight: 600, color: i === 0 || i === 6 ? "var(--foreground-subtle)" : "var(--muted-foreground)" }}>{w}</div>
        ))}
        {cells.map((d, i) => d
          ? <MrDayCell key={i} d={d} selected={mrSameDay(d, value)} isToday={mrSameDay(d, today)} onPick={onPick} />
          : <div key={i} />)}
      </div>
      <div style={{ display: "flex", justifyContent: "flex-end", marginTop: 6 }}>
        <MrCalNav label="今日を選ぶ" onClick={() => onPick(mrDay(today))}>今日</MrCalNav>
      </div>
    </div>
  );
}

/** 年月日の選択。value / onChange は店の日付表記(上の「値の約束」)。
 *  label は読み上げ・E2E 用の名前(画面の見出しは呼び出し側が出す)。
 *  required のときは空にできない(消して離れると元の日付に戻す)。
 *  読めない文字を打って離れたときも元に戻す(DS と同じ)。黙って別の日にしない。 */
function MrDatePicker({ value, onChange, label, placeholder = "yyyy年mm月dd日", required = false, width = 184, today }) {
  const now = today || new Date();
  const sel = mrUiToDate(value, now);
  // 読めない値(旧デモデータの説明文など)はそのまま見せる(黙って消さない)。
  const shown = sel ? mrFmtJa(sel) : (value == null ? "" : String(value));
  const [text, setText] = React.useState(shown);
  const [open, setOpen] = React.useState(false);
  const [up, setUp] = React.useState(false);
  const [focus, setFocus] = React.useState(false);
  const [view, setView] = React.useState(() => { const b = sel || now; return new Date(b.getFullYear(), b.getMonth(), 1); });
  const ref = React.useRef(null);
  const inputRef = React.useRef(null);
  React.useEffect(() => { setText(shown); }, [shown]);
  React.useEffect(() => {
    if (!open) return undefined;
    const away = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", away);
    document.addEventListener("touchstart", away);
    return () => { document.removeEventListener("mousedown", away); document.removeEventListener("touchstart", away); };
  }, [open]);
  // 同じ日なら onChange を呼ばない("09/24" と "9/24" のような書き方の違いで差分を作らない)。
  const emit = d => {
    setText(d ? mrFmtJa(d) : "");
    if (d) setView(new Date(d.getFullYear(), d.getMonth(), 1));
    if (d ? mrSameDay(d, sel) : (value == null || String(value).trim() === "")) return;
    if (onChange) onChange(d ? mrDateToUi(d, now) : "");
  };
  const commit = () => {
    if (text === shown) return;
    if (!text.trim()) { if (required) setText(shown); else emit(null); return; }
    const d = mrParseTyped(text, now);
    if (d) emit(d); else setText(shown);
  };
  const openCal = () => {
    // 下に余白が無ければ上に開く(工程のモーダルの下の方の欄で、カレンダーが画面の外に出ないように)。
    if (ref.current && ref.current.getBoundingClientRect && typeof window !== "undefined" && window.innerHeight) {
      const r = ref.current.getBoundingClientRect();
      const below = window.innerHeight - r.bottom;
      setUp(below < 300 && r.top > below);
    }
    const b = mrParseTyped(text, now) || sel || now;
    setView(new Date(b.getFullYear(), b.getMonth(), 1));
    setOpen(true);
  };
  const bad = text !== shown && text.trim() !== "" && !mrParseTyped(text, now);
  const lit = focus || open;
  return (
    // フォーカスが部品の外へ出たら(Tab で抜けたとき)カレンダーを閉じる。部品の中(カレンダーのボタン)なら開いたまま。
    // Esc は入力欄でもカレンダーの中でも閉じる(中で押したときはフォーカスを入力欄へ戻す)。
    <div ref={ref} style={{ position: "relative", display: "inline-flex", width, maxWidth: "100%" }}
      onBlur={e => { if (open && ref.current && !ref.current.contains(e.relatedTarget)) setOpen(false); }}
      onKeyDown={e => {
        if (e.key !== "Escape" || !open) return;
        e.stopPropagation(); setOpen(false);
        if (inputRef.current && e.target !== inputRef.current) inputRef.current.focus();
      }}>
      <input ref={inputRef} type="text" aria-label={label} aria-invalid={bad || undefined} aria-haspopup="dialog" aria-expanded={open}
        value={text} placeholder={placeholder} autoComplete="off"
        onChange={e => setText(e.target.value)}
        onFocus={() => setFocus(true)}
        onBlur={() => { setFocus(false); commit(); }}
        onClick={() => { if (!open) openCal(); }}
        onKeyDown={e => {
          if (e.key === "Enter") { e.preventDefault(); commit(); setOpen(false); }
          else if (e.key === "ArrowDown" && !open) { e.preventDefault(); openCal(); }
        }}
        style={{ boxSizing: "border-box", width: "100%", height: 28, padding: "0 8px", borderRadius: "var(--radius-md)",
          border: "1px solid " + (bad ? "var(--destructive)" : lit ? "var(--brand)" : "var(--input)"),
          boxShadow: lit ? "var(--ring-focus)" : "none", background: "var(--card)", outline: "none",
          fontSize: "var(--text-body-md)", fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums", color: "var(--foreground)",
          transition: "border-color var(--duration-fast) var(--easing), box-shadow var(--duration-fast) var(--easing)" }} />
      {open && (
        // mousedown を止めて入力欄のフォーカスを外さない(外すと日を押す前に打ちかけの文字で確定してしまう)。
        <div role="dialog" aria-label={(label || "日付") + "のカレンダー"} onMouseDown={e => e.preventDefault()}
          style={{ position: "absolute", left: 0, zIndex: 60, boxShadow: "var(--shadow-md)", borderRadius: "var(--radius-lg)",
            ...(up ? { bottom: "100%", marginBottom: 4 } : { top: "100%", marginTop: 4 }) }}>
          <MrCalendar value={sel} view={view} onView={setView} today={now} onPick={d => { emit(d); setOpen(false); }} />
        </div>
      )}
    </div>
  );
}

/* ラジオ 1 つ(DS の Choice type="radio" と同じ見た目: 枠つきの小さな札・選ぶとブランド色) */
function MrRadio({ name, checked, label, onChange, disabled }) {
  const [hov, setHov] = React.useState(false);
  const [focus, setFocus] = React.useState(false);
  return (
    <label onMouseEnter={() => setHov(true)} onMouseLeave={() => setHov(false)}
      style={{ display: "inline-flex", alignItems: "center", gap: 6, height: 28, padding: "0 10px 0 8px", boxSizing: "border-box",
        borderRadius: "var(--radius-md)", fontSize: "var(--text-body-md)", fontFamily: "var(--font-sans)", whiteSpace: "nowrap",
        cursor: disabled ? "not-allowed" : "pointer", color: disabled ? "var(--foreground-subtle)" : "var(--foreground)",
        border: "1px solid " + (checked ? "var(--brand)" : hov && !disabled ? "var(--border-strong)" : "var(--input)"),
        background: checked ? "var(--brand-muted)" : disabled ? "var(--muted)" : "var(--card)",
        boxShadow: focus ? "var(--ring-focus)" : "none",
        transition: "border-color var(--duration-fast) var(--easing), background var(--duration-fast) var(--easing)" }}>
      <input type="radio" name={name} checked={!!checked} disabled={disabled} onChange={() => onChange && onChange()}
        onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
        style={{ width: 14, height: 14, margin: 0, accentColor: "var(--brand)", cursor: "inherit" }} />
      {label}
    </label>
  );
}

/** ラジオの組。options は ["その他", …] か [{ value, label }, …]。label は組の名前(読み上げ・E2E 用)。 */
function MrRadioGroup({ name, value, options, onChange, label, style = {} }) {
  const opts = (options || []).map(o => (o && typeof o === "object") ? o : { value: o, label: o });
  return (
    <div role="radiogroup" aria-label={label} style={{ display: "flex", gap: 6, flexWrap: "wrap", ...style }}>
      {opts.map(o => (
        <MrRadio key={o.value} name={name} label={o.label} checked={value === o.value} disabled={o.disabled}
          onChange={() => onChange && onChange(o.value)} />
      ))}
    </div>
  );
}

Object.assign(window, { MrDatePicker, MrCalendar, MrRadioGroup, MrRadio, mrUiToDate, mrDateToUi, mrFmtJa, mrParseTyped });
