/* modals.jsx — DayModal, SettingsModal, AutofillModal */
const { useState: useStateM } = React;

/* ============ Day modal ============ */
function DayModal({ dayKey, entry: liveEntry, mode, slot, defaultDist, onClose, onChange, onClear }) {
  const dismiss = useModalDismiss(onClose);
  const { close } = dismiss;
  // While the sheet slides away (after Clear day, say), keep showing the day
  // as it was, so its rows don't fold shut and drop the sheet mid-exit.
  const shown = React.useRef(liveEntry);
  if (!dismiss.closing) shown.current = liveEntry;
  const entry = shown.current;
  const date = fromYmd(dayKey);
  const autoHolName = holidayName(date);
  const isAutoHoliday = !!autoHolName;
  const isHol = entry.holiday == null ? isAutoHoliday : entry.holiday;
  const [showHol, setShowHol] = useStateM(entry.holiday != null);
  const toggle = (k) => onChange((e) => {
    const next = { ...e, [k]: !e[k], dist: { ...e.dist } };
    // Turning on: use the distance in effect, in every mode, so a later switch
    // to "Varies" keeps a Long commuter's shifts Long. A Straight Day earns
    // no taxi, so it has none.
    if (!e[k] && k !== "sd") next.dist[k] = defaultDist || "S";
    // turning off: drop any stale hours override or duty mark so neither can
    // silently re-apply when the shift is ticked again
    if (e[k] && e.hours && e.hours[k] != null) {
      const hours = { ...e.hours };
      delete hours[k];
      next.hours = Object.keys(hours).length ? hours : undefined;
    }
    if (e[k] && e.duty && e.duty[k] != null) {
      const duty = { ...e.duty };
      delete duty[k];
      next.duty = Object.keys(duty).length ? duty : undefined;
    }
    // ...and the same for an overtime mark, and a Straight Day's overtime.
    if (e[k] && e.ot && e.ot[k] != null) {
      const ot = { ...e.ot };
      delete ot[k];
      next.ot = Object.keys(ot).length ? ot : undefined;
    }
    if (e[k] && k === "sd") delete next.sdOt;
    return next;
  });
  const setDuty = (k, v) => onChange((e) => {
    const duty = { ...(e.duty || {}) };
    if (v) duty[k] = v; else delete duty[k];
    return { ...e, duty: Object.keys(duty).length ? duty : undefined };
  });
  // Only a choice that differs from the rotation's is kept (null = automatic).
  const setOvertime = (k, v) => onChange((e) => {
    const ot = { ...(e.ot || {}) };
    if (v == null) delete ot[k]; else ot[k] = v;
    return { ...e, ot: Object.keys(ot).length ? ot : undefined };
  });
  const setSdOt = (raw) => onChange((e) => {
    let v = sanitizeDecimal(raw);
    if (Number(v) > MAX_SD_OT) v = String(MAX_SD_OT);
    const next = { ...e };
    if (v === "") delete next.sdOt; else next.sdOt = v;
    return next;
  });
  const setHoliday = (v) => onChange((e) => ({ ...e, holiday: v }));
  const setDist = (k, v) => onChange((e) => ({ ...e, dist: { ...e.dist, [k]: v } }));
  const setHours = (k, raw) => onChange((e) => {
    // One shift is at most a day long (the math caps it there too).
    let v = sanitizeDecimal(raw);
    if (Number(v) > MAX_SHIFT_HOURS) v = String(MAX_SHIFT_HOURS);
    const hours = { ...(e.hours || {}) };
    if (v === "") delete hours[k]; else hours[k] = v;
    return { ...e, hours: Object.keys(hours).length ? hours : undefined };
  });

  const title = date.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric" });
  // Nothing to clear on a day with no shifts and no holiday override.
  const canClear = hasShifts(entry) || entry.holiday != null;

  return (
    <ModalFrame dismiss={dismiss} sheet zIndex={60} maxWidth={540} label={`Edit ${title}`} style={{ padding: 16 }}>
        <ModalHead eyebrow="Edit day" title={title} onClose={close}>
          {autoHolName && <div style={{ fontSize: 12.5, color: "var(--holiday)", marginTop: 2 }}>● {autoHolName}</div>}
          {slot && (
            <div style={{ fontSize: 12.5, color: "var(--ink-dim)", marginTop: 4 }}>
              Rotation: <span style={{ color: "var(--ink)", fontWeight: 600 }}>{slot === "off" ? "Off day" : `${SHIFT_LABEL[slot]} day`}</span>
              {slot === "off" ? " · a shift here is overtime unless you switch it off" : ""}
            </div>
          )}
        </ModalHead>

        {DAY_SHIFTS.map((sh) => (
          <ShiftBlock key={sh.k} sh={sh} entry={entry} mode={mode} slot={slot}
            onToggle={() => toggle(sh.k)} onHours={setHours}
            onDist={(v) => setDist(sh.k, v)} onDuty={(v) => setDuty(sh.k, v)}
            onOvertime={(v) => setOvertime(sh.k, v)} onSdOt={setSdOt} />
        ))}
        <Collapse open={hasShifts(entry)}>
          <div style={{ fontSize: 12, color: "var(--ink-faint)", lineHeight: 1.45, padding: "0 2px" }}>
            Overtime shifts show as extra (+) and go under Overtime on the record sheet. Pay is the same either way: overtime pay is worked out from your total hours.
          </div>
        </Collapse>

        <div style={{ height: 1, background: "var(--line-soft)", margin: "12px 0" }} />

        {/* Holidays are set automatically; the override stays folded away
            unless it's in use. */}
        <div style={{ padding: "10px 12px", background: "var(--bg-2)", borderRadius: 10 }}>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
            <div style={{ fontSize: 14 }}>
              Public holiday: <span style={{ fontWeight: 600 }}>{isHol ? "Yes" : "No"}</span>
              <span style={{ fontSize: 12.5, color: "var(--ink-faint)" }}>{entry.holiday == null ? " · automatic" : " · set by you"}</span>
            </div>
            {!showHol && (
              <button onClick={() => setShowHol(true)} style={linkBtn(13)}>Change</button>
            )}
          </div>
          {showHol && (
            <div style={{ marginTop: 10 }}>
              <SegToggle
                small full
                options={[{ v: "auto", l: "Automatic" }, { v: "off", l: "Not a holiday" }, { v: "on", l: "Holiday" }]}
                value={entry.holiday == null ? "auto" : entry.holiday ? "on" : "off"}
                onChange={(v) => setHoliday(v === "auto" ? null : v === "on")}
              />
            </div>
          )}
        </div>

        <div style={{ display: "flex", gap: 8, marginTop: 14 }}>
          {canClear && <button onClick={() => { onClear(); close(); }} style={ghostBtn()}>Clear day</button>}
          <div style={{ flex: 1 }} />
          <button onClick={close} style={primaryBtn()}>Done</button>
        </div>
    </ModalFrame>
  );
}

const DAY_SHIFTS = SHIFTS.map((s) => ({
  ...s,
  label: `${SHIFT_LABEL[s.k]} shift`,
  hours: s.k === "pm10" ? `${stdHours(s.k)}h · crosses midnight`
    : s.k === "sd" ? `${stdHours(s.k)}h · 8:30AM to 4:30PM`
    : `${stdHours(s.k)}h`,
  hint: s.k === "pm10" ? "total" : undefined,
}));

/* One shift in the day editor: the tick box, then (while it's on) its
   hours, overtime after it (a Straight Day), taxi distance, whether it's an
   overtime shift, and its duty, sliding open under it. */
function ShiftBlock({ sh, entry, mode, slot, onToggle, onHours, onDist, onDuty, onOvertime, onSdOt }) {
  const on = !!entry[sh.k];
  const duty = shiftDuty(entry, sh.k);
  const pay = duty ? DUTY_TYPES[duty].pay : "full";
  return (
    <div>
      <DayToggle label={sh.label} hours={sh.hours} color={sh.color} active={on} duty={duty}
        extra={isOvertimeShift(entry, sh.k, slot)} onClick={onToggle} />
      <Collapse open={on}>
        <div style={{ paddingBottom: 2 }}>
          {/* A given-away shift has no hours, and only a paid shift has a
              taxi distance, so those rows fold away instead of showing
              settings that can't change anything. A Straight Day has no
              taxi; overtime after it needs the day worked, not on leave. */}
          <Collapse open={pay !== "none"}>
            <TimeRow keyName={sh.k} entry={entry} onChange={onHours} hint={sh.hint} />
          </Collapse>
          {sh.k === "sd" && (
            <Collapse open={pay === "full"}>
              <HoursMinutesRow
                label="Overtime after 4:30PM" raw={entry.sdOt != null ? String(entry.sdOt) : ""} std={0}
                ariaHours="Overtime hours after the Straight Day" ariaMinutes="Overtime minutes after the Straight Day"
                onChange={onSdOt}
              />
            </Collapse>
          )}
          {mode === "advanced" && sh.k !== "sd" && (
            <Collapse open={pay === "full"}>
              <DistRow value={entry.dist?.[sh.k] || "S"} onChange={onDist} />
            </Collapse>
          )}
          <Collapse open={pay !== "none"}>
            <OvertimeRow k={sh.k} entry={entry} slot={slot} onChange={onOvertime} />
          </Collapse>
          <DutyRow value={duty} onChange={onDuty} shift={SHIFT_LABEL[sh.k]} />
        </div>
      </Collapse>
    </div>
  );
}

/* Whether this shift is an overtime (extra) shift. It starts at what the
   rotation says (outside it = overtime) and the switch changes it; switching
   back to the rotation's answer clears the choice. */
function OvertimeRow({ k, entry, slot, onChange }) {
  const auto = autoOvertime(k, slot);
  const set = otSet(entry, k);
  const on = set == null ? auto : set;
  const why = set != null ? (on ? "Set by you" : "Set by you · not overtime")
    : !slot ? "No rotation saved · switch on to mark it"
    : auto ? "Automatic · outside your rotation"
    : "Automatic · your rotation's shift";
  return (
    <label style={{ display: "flex", alignItems: "center", gap: 10, margin: "0 0 10px", padding: "8px 10px", background: "var(--bg-2)", borderRadius: 10, cursor: "pointer" }}>
      <span style={{ flex: 1, minWidth: 0 }}>
        <span style={{ display: "block", fontSize: 13 }}>
          <span style={{ color: "var(--ink-dim)" }}>Overtime shift: </span>
          <span style={{ fontWeight: 600 }}>{on ? "Yes" : "No"}</span>
        </span>
        <span style={{ display: "block", fontSize: 12, color: "var(--ink-faint)", marginTop: 1 }}>{why}</span>
      </span>
      <input type="checkbox" className="nsc-switch" checked={on}
        onChange={(e) => onChange(e.target.checked === auto ? null : e.target.checked)}
        aria-label={`${SHIFT_LABEL[k]} is an overtime shift`} />
    </label>
  );
}

/* Duty for one shift, folded to one line like the holiday setting. The
   choices are grouped by what they do to pay:
   - worked: paid as normal (regular, orderly, exchange for)
   - leave: hours kept as ordinary hours, no SP1, SP2, meal or taxi
   - given away: exchange leave, no hours and no allowances */
const DUTY_GROUPS = [
  { title: "Worked · paid as normal", options: [{ v: null, l: "Regular" }, ...DUTY_KEYS.filter((k) => DUTY_TYPES[k].pay === "full").map((k) => ({ v: k, l: DUTY_TYPES[k].short }))] },
  { title: "Leave · hours count, no allowances", options: DUTY_KEYS.filter((k) => DUTY_TYPES[k].pay === "hours").map((k) => ({ v: k, l: DUTY_TYPES[k].short })) },
  { title: "Swapped out · no hours, no allowances", options: DUTY_KEYS.filter((k) => DUTY_TYPES[k].pay === "none").map((k) => ({ v: k, l: DUTY_TYPES[k].short })) },
];
/* What the chosen duty does to this shift's pay, in one line. */
const DUTY_NOTES = {
  orderly: "Orderly duty: paid as a normal shift, and listed under Leave & duties.",
  exchangeFor: "Exchange for: you're covering someone else's shift. All hours and allowances count as normal.",
  exchangeLeave: "Exchange leave: someone else covers this shift. It earns no hours and no SP1, SP2, meal or taxi.",
};
const LEAVE_NOTE = "On leave: the hours count as ordinary hours (never holiday ×2), but no SP1, SP2, meal or taxi for this shift.";
function DutyRow({ value, onChange, shift }) {
  const [open, setOpen] = useStateM(!!value);
  const note = value ? (isLeave(value) ? LEAVE_NOTE : DUTY_NOTES[value]) : null;
  // Keep the last note on screen while it folds away, so the text doesn't
  // vanish before the collapse animation finishes.
  const lastNote = React.useRef(note);
  if (note) lastNote.current = note;
  return (
    <div style={{ margin: "0 0 10px", padding: "8px 10px", background: "var(--bg-2)", borderRadius: 10 }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, minHeight: 24 }}>
        <span style={{ fontSize: 13 }}>
          <span style={{ color: "var(--ink-dim)" }}>Duty: </span>
          <span style={{ fontWeight: 600 }}>{value ? DUTY_TYPES[value].label : "Regular shift"}</span>
        </span>
        <button onClick={() => setOpen((o) => !o)} aria-expanded={open} style={linkBtn(13)}>{open ? "Hide" : "Change"}</button>
      </div>
      <Collapse open={open}>
        <div role="radiogroup" aria-label={`Duty for the ${shift} shift`} style={{ paddingTop: 4 }}>
          {DUTY_GROUPS.map((g) => (
            <div key={g.title}>
              <div style={{ fontSize: 11.5, color: "var(--ink-faint)", margin: "8px 0 5px" }}>{g.title}</div>
              <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(96px, 1fr))", gap: 6 }}>
                {g.options.map((o) => {
                  const active = (value || null) === o.v;
                  return (
                    <button key={o.l} role="radio" aria-checked={active} onClick={() => onChange(o.v)} style={{
                      padding: "8px 4px", borderRadius: 8, minWidth: 0,
                      border: `1px solid ${active ? "var(--ink)" : "var(--line)"}`,
                      background: active ? "var(--ink)" : "var(--bg-1)",
                      color: active ? "var(--bg)" : "var(--ink-dim)",
                      fontSize: 12, fontWeight: active ? 600 : 500, cursor: "pointer", fontFamily: "inherit",
                      whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
                      transition: "background 0.18s, color 0.18s, border-color 0.18s, var(--press)",
                    }}>{o.l}</button>
                  );
                })}
              </div>
            </div>
          ))}
        </div>
      </Collapse>
      <Collapse open={!!note}>
        <div style={{ fontSize: 12, color: "var(--ink-faint)", lineHeight: 1.45, paddingTop: 6 }}>
          {note || lastNote.current}
        </div>
      </Collapse>
    </div>
  );
}

/* extra: this shift isn't on the rotation for the day, so ticking it marks
   an extra shift (shown in the extra colour). */
function DayToggle({ label, hours, color, active, extra, duty, onClick }) {
  if (extra) color = "var(--extra)";
  const pay = active && duty ? DUTY_TYPES[duty].pay : "full";
  const unpaid = pay !== "full"; // leave or given away: badge in neutral ink
  return (
    <button onClick={onClick} aria-pressed={active} className="nsc-daytoggle" style={{
      display: "flex", alignItems: "center", gap: 12,
      width: "100%", padding: "12px 12px",
      // A given-away shift is ticked but not worked: a fainter tint.
      background: active ? `color-mix(in oklab, ${color} ${pay === "none" ? 6 : 14}%, transparent)` : "var(--bg-2)",
      border: `1px ${pay === "none" ? "dashed" : "solid"} ${active ? "color-mix(in oklab, " + color + " 50%, transparent)" : "var(--line)"}`,
      borderRadius: 10, color: "var(--ink)",
      marginBottom: 8, cursor: "pointer", textAlign: "left",
      transition: "background 0.18s, border-color 0.18s, var(--press)",
      fontFamily: "inherit",
    }}>
      <span className={"nsc-check" + (active ? " is-on" : "")} style={{
        width: 22, height: 22, borderRadius: 6, flexShrink: 0,
        border: `1.5px solid ${active ? color : "var(--line)"}`,
        background: active ? color : "transparent",
        display: "flex", alignItems: "center", justifyContent: "center",
      }}>
        {active && <svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path className="nsc-tick" d="M2 6.5L4.8 9L10 3.5" stroke="var(--bg)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>}
      </span>
      <div style={{ flex: 1 }}>
        <div style={{ fontSize: 14, fontWeight: 500 }}>
          {label}
          {extra && active && <span style={{ marginLeft: 8, fontSize: 11, fontWeight: 700, color: "var(--extra)" }}>EXTRA</span>}
          {active && duty && (
            <span className="nsc-pop" style={{
              marginLeft: 8, fontSize: 11, fontWeight: 600, padding: "1px 7px", borderRadius: 999, whiteSpace: "nowrap",
              color: unpaid ? "var(--ink)" : color,
              border: `1px ${pay === "none" ? "dotted" : "solid"} ${unpaid ? "var(--ink-faint)" : "color-mix(in oklab, " + color + " 50%, transparent)"}`,
            }}>{DUTY_TYPES[duty].label}</span>
          )}
        </div>
        <div style={{ fontSize: 12.5, color: "var(--ink-faint)", marginTop: 2 }}>
          {pay === "none" ? "Not worked · no hours or allowances" : pay === "hours" ? `${hours} · no allowances` : hours}
        </div>
      </div>
    </button>
  );
}

function DistRow({ value, onChange }) {
  return (
    <div style={{ display: "flex", justifyContent: "flex-end", alignItems: "center", gap: 8, margin: "0 0 8px 38px" }}>
      <span style={{ fontSize: 12.5, color: "var(--ink-dim)" }}>Taxi</span>
      <SegToggle small options={[{ v: "S", l: "Short" }, { v: "L", l: "Long" }]} value={value} onChange={onChange} />
    </div>
  );
}

/* Editable actual time for a shift, as hours and minutes (easier to type
   than 6.67). Stored as decimal hours (6h 40m is 6.666667), which the math
   uses as is, or rounds back to the whole minute when overtime is counted in
   minutes. Both boxes blank = the standard hours, shown as placeholders. A
   partial shift's time counts toward total/overtime; only more than half a
   shift earns the per-shift allowance. */
const hmFromRaw = (r) => {
  if (r === "" || !(Number(r) >= 0)) return { h: "", m: "" };
  const t = Math.round(Number(r) * 60);
  return { h: String(Math.floor(t / 60)), m: t % 60 ? String(t % 60) : "" };
};
function TimeRow({ keyName, entry, onChange, hint }) {
  const raw = entry.hours && entry.hours[keyName] != null ? String(entry.hours[keyName]) : "";
  return (
    <HoursMinutesRow
      label={`Time worked${hint ? ` · ${hint}` : ""}`} raw={raw} std={stdHours(keyName)}
      ariaHours={`Hours worked for ${SHIFT_LABEL[keyName]} shift`} ariaMinutes={`Minutes worked for ${SHIFT_LABEL[keyName]} shift`}
      onChange={(v) => onChange(keyName, v)}
    />
  );
}

/* A time as two boxes, hours and minutes, sent on as decimal hours ("" when
   both are blank). std is what blank means, shown greyed in the boxes. */
function HoursMinutesRow({ label, raw, std, ariaHours, ariaMinutes, onChange }) {
  // The boxes keep what's typed (a blank minutes box mid-edit); they only
  // reload when the stored hours change from elsewhere (shift ticked off,
  // or a time over the most allowed, capped).
  const [draft, setDraft] = useStateM(() => hmFromRaw(raw));
  const sent = React.useRef(raw);
  React.useEffect(() => {
    if (raw !== sent.current) { sent.current = raw; setDraft(hmFromRaw(raw)); }
  }, [raw]);
  const update = (patch) => {
    const d = { ...draft, ...patch };
    setDraft(d);
    const out = d.h === "" && d.m === "" ? ""
      : String(Math.round(((Number(d.h) || 0) + (Number(d.m) || 0) / 60) * 1e6) / 1e6);
    sent.current = out;
    onChange(out);
  };
  const box = { width: 46, background: "var(--bg-2)", border: "1px solid var(--line)", borderRadius: 8, padding: "6px 8px", color: "var(--ink)", fontSize: 16, fontFamily: "inherit", textAlign: "right" };
  const unit = { fontSize: 11, color: "var(--ink-faint)" };
  const stdMin = Math.round((std % 1) * 60);
  // Tapping a box selects what's in it, so typing replaces it (6 → 7)
  // instead of running on to 67. After the tap, or iOS drops the selection.
  const selectAll = (e) => {
    const el = e.target;
    setTimeout(() => { try { el.setSelectionRange(0, el.value.length); } catch {} }, 0);
  };
  return (
    <div style={{ display: "flex", alignItems: "center", justifyContent: "flex-end", gap: 6, margin: "0 0 8px 38px", flexWrap: "wrap" }}>
      <span style={{ fontSize: 12, color: "var(--ink-faint)", marginRight: 2 }}>{label}</span>
      <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
        {/* Once minutes are typed, a blank hours box means 0 hours, so its
            placeholder says so instead of still showing the standard hours. */}
        <input
          inputMode="numeric" value={draft.h} placeholder={draft.m === "" ? String(Math.floor(std)) : "0"} maxLength={2}
          onChange={(e) => update({ h: e.target.value.replace(/\D/g, "").slice(0, 2) })} onFocus={selectAll}
          aria-label={ariaHours} style={box}
        />
        <span className="mono" style={unit}>h</span>
        <input
          inputMode="numeric" value={draft.m} placeholder={draft.h === "" ? String(stdMin).padStart(2, "0") : "00"} maxLength={2}
          onChange={(e) => {
            const v = e.target.value.replace(/\D/g, "").slice(0, 2);
            update({ m: v !== "" && Number(v) > 59 ? "59" : v });
          }} onFocus={selectAll}
          aria-label={ariaMinutes} style={box}
        />
        <span className="mono" style={unit}>m</span>
      </span>
    </div>
  );
}

/* ============ Settings ============ */
function SettingsModal({ rates, setRates, tax, setTax, ratesHistory, setRatesHistory, theme, setTheme, otUnit, setOtUnit, ratesEffective, onExportICS, onReset, onExport, onImport, onAbout, onClose }) {
  const [tab, setTab] = useStateM("rates");
  // About opens once Settings has closed (with its exit, and its Back entry
  // gone), so About's own entry lands where Back expects it.
  const toAbout = React.useRef(false);
  const dismiss = useModalDismiss(() => { onClose(); if (toAbout.current) onAbout(); });
  return (
    <ModalFrame dismiss={dismiss} top label="Settings">
        <ModalHead title="Settings" onClose={dismiss.close} />
        <div style={{ marginBottom: 16 }}>
          <SegToggle fit options={[
            { v: "rates", l: "Rates" },
            { v: "tax", l: "Tax" },
            { v: "history", l: "Past rates" },
            { v: "theme", l: "Theme" },
            { v: "data", l: "Backup" },
          ]} value={tab} onChange={setTab} small />
        </div>
        <div style={{ display: tab === "rates" ? "block" : "none" }}><RatesTab rates={rates} setRates={setRates} ratesEffective={ratesEffective} otUnit={otUnit} setOtUnit={setOtUnit} /></div>
        <div style={{ display: tab === "tax" ? "block" : "none" }}><TaxTab tax={tax} setTax={setTax} /></div>
        <div style={{ display: tab === "history" ? "block" : "none" }}><RateHistoryTab ratesHistory={ratesHistory} setRatesHistory={setRatesHistory} currentRates={rates} /></div>
        <div style={{ display: tab === "theme" ? "block" : "none" }}><ThemeTab theme={theme} setTheme={setTheme} /></div>
        <div style={{ display: tab === "data" ? "block" : "none" }}>
          <DataTab onExport={onExport} onImport={onImport} onExportICS={onExportICS} onReset={onReset} />
        </div>
        <div style={{ borderTop: "1px solid var(--line-soft)", marginTop: 16, paddingTop: 12, textAlign: "center" }}>
          <button onClick={() => { toAbout.current = true; dismiss.close(); }} style={linkBtn(13)}>About & disclaimers</button>
        </div>
    </ModalFrame>
  );
}

/* Backup and housekeeping, kept out of the everyday screens. */
function DataTab({ onExport, onImport, onExportICS, onReset }) {
  const item = (title, detail, onClick, danger) => (
    <button onClick={onClick} style={{
      display: "block", width: "100%", textAlign: "left", padding: "12px 14px", marginBottom: 8,
      background: "var(--bg-2)", border: "1px solid var(--line)", borderRadius: 12,
      color: danger ? "var(--holiday)" : "var(--ink)", cursor: "pointer", fontFamily: "inherit",
    }}>
      <div style={{ fontSize: 14, fontWeight: 500 }}>{title}</div>
      <div style={{ fontSize: 12.5, color: "var(--ink-faint)", marginTop: 2 }}>{detail}</div>
    </button>
  );
  return (
    <div>
      {item("Save a backup", "Download everything as a JSON file", onExport)}
      {item("Restore a backup", "Load a JSON file you saved before", onImport)}
      {item("Add shifts to my calendar", "Download this period as an .ics file", onExportICS)}
      {item("Reset this period", "Clears this period's shifts and roster counts. Pay, rates and History are kept.", onReset, true)}
      <div style={{ fontSize: 12, color: "var(--ink-faint)", lineHeight: 1.5, marginTop: 6 }}>
        Your data stays on this device. Save a backup before changing phones or clearing your browser.
      </div>
    </div>
  );
}

const ratesToDraft = (r) => {
  const out = {};
  for (const k of Object.keys(r)) out[k] = String(r[k]);
  return out;
};

/* Keep a tab's fields in step when its values change from outside (a backup
   restored from Backup, an Undo, Reset to defaults). A field that already
   holds the same number ("12." mid-typing) or isn't valid yet is left alone. */
function useSyncedDraft(value, toDraft, setDraft, errors) {
  React.useEffect(() => {
    const want = toDraft(value);
    setDraft((d) => {
      let next = d;
      for (const k of Object.keys(want)) {
        if (errors[k]) continue;
        const same = typeof want[k] === "boolean" ? d[k] === want[k] : Number(d[k]) === Number(want[k]);
        if (!same) next = { ...next, [k]: want[k] };
      }
      return next;
    });
  }, [value]);
}

/* Rates and tax save as you type: each valid field is applied straight away,
   and a field that isn't valid keeps its last saved value until it's fixed.
   Closing Settings never throws edits away. */
function RatesTab({ rates, setRates, ratesEffective, otUnit, setOtUnit }) {
  const [draft, setDraft] = useStateM(() => ratesToDraft(rates));
  const [errors, setErrors] = useStateM({});
  const set = (k, v) => {
    const raw = sanitizeDecimal(v);
    setDraft((p) => ({ ...p, [k]: raw }));
    const n = Number(raw);
    const err = raw === "" || !Number.isFinite(n) || n < 0 ? "Not saved. Must be 0 or more"
      : k === "threshold" && !(n > 0) ? "Not saved. Must be more than 0" : null;
    setErrors((e) => { const x = { ...e }; if (err) x[k] = err; else delete x[k]; return x; });
    if (!err) setRates((r) => ({ ...r, [k]: n }));
  };
  useSyncedDraft(rates, ratesToDraft, setDraft, errors);
  const resetDefaults = () => {
    const prev = rates;
    setRates({ ...DEFAULT_RATES });
    setErrors({});
    showToast("Rates reset to defaults", { action: { label: "Undo", onClick: () => setRates(prev) } });
  };
  // A Past rates entry covering the open period takes precedence over these.
  const overridden = ratesEffective && ratesEffective !== "Current rates";
  return (
    <div>
      {overridden && (
        <div style={{ marginBottom: 12, padding: "10px 12px", borderRadius: 10, fontSize: 12.5, lineHeight: 1.5, color: "var(--warn)", background: "color-mix(in oklab, var(--warn) 10%, transparent)", border: "1px solid color-mix(in oklab, var(--warn) 35%, transparent)" }}>
          The period you're viewing uses a saved rate from Past rates ({ratesEffective.replace("Rates effective ", "from ")}). Changes here only apply to periods before your first saved rate. To change this period, save a new entry in Past rates.
        </div>
      )}
      {/* Inputs line up along each row even when one label wraps. */}
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, alignItems: "end" }}>
        <RateInput label="SP1 (3PM, per shift)" unit="$" value={draft.sp1} onChange={(v) => set("sp1", v)} error={errors.sp1} />
        <RateInput label="SP2 (10PM, per shift)" unit="$" value={draft.sp2} onChange={(v) => set("sp2", v)} error={errors.sp2} />
        <RateInput label="Meal (per shift)" unit="$" value={draft.meal} onChange={(v) => set("meal", v)} error={errors.meal} />
        <RateInput label="Overtime after (monthly)" unit="h" value={draft.threshold} onChange={(v) => set("threshold", v)} error={errors.threshold} />
        <RateInput label="Taxi — Short" unit="$" value={draft.taxiShort} onChange={(v) => set("taxiShort", v)} error={errors.taxiShort} />
        <RateInput label="Taxi — Long" unit="$" value={draft.taxiLong} onChange={(v) => set("taxiLong", v)} error={errors.taxiLong} />
      </div>
      {setOtUnit && (
        <div style={{ marginTop: 14, padding: "10px 12px", background: "var(--bg-2)", borderRadius: 10 }}>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, flexWrap: "wrap" }}>
            <span style={{ fontSize: 13.5, fontWeight: 500 }}>Count overtime in</span>
            <SegToggle small options={[{ v: "hours", l: "Hours" }, { v: "minutes", l: "Minutes" }]} value={otUnit} onChange={setOtUnit} />
          </div>
          <div style={{ fontSize: 12, color: "var(--ink-faint)", marginTop: 6, lineHeight: 1.5 }}>
            {otUnit === "minutes"
              ? `Every shift counts to the whole minute and time shows as hours and minutes. Overtime is the minutes over ${fmtHM(Number(rates.threshold) || DEFAULT_RATES.threshold)}, paid at the hourly rate ÷ 60 × 1.5.`
              : "Time counts and shows as decimal hours (6h 30m is 6.5h). Switch to Minutes to count every shift to the whole minute and show time as hours and minutes."}
          </div>
        </div>
      )}
      <SettingsFooter onDefaults={resetDefaults} />
    </div>
  );
}

function SettingsFooter({ onDefaults }) {
  return (
    <div style={{ display: "flex", gap: 8, marginTop: 14, alignItems: "center" }}>
      <span style={{ fontSize: 12.5, color: "var(--ink-faint)", marginRight: "auto" }}>Changes save automatically.</span>
      <button onClick={onDefaults} style={{ ...ghostBtn(), whiteSpace: "nowrap" }}>Reset to defaults</button>
    </div>
  );
}

const taxToDraft = (t) => {
  const out = {};
  // Round for display: the NIS cap default is 5,000,000 / 12, which would
  // otherwise show as 416666.6666666667. The stored value stays exact until
  // the field is edited.
  for (const k of Object.keys(t)) {
    out[k] = typeof t[k] === "boolean" ? t[k]
      : typeof t[k] === "number" ? String(Math.round(t[k] * 100) / 100)
      : String(t[k]);
  }
  return out;
};

const TAX_PCT_KEYS = new Set(["nis", "nht", "eduTax", "payeRate1", "payeRate2", "pension"]);

function TaxTab({ tax, setTax }) {
  const [draft, setDraft] = useStateM(() => taxToDraft(tax));
  const [errors, setErrors] = useStateM({});
  const set = (k, v) => {
    if (typeof v === "boolean") {
      setDraft((p) => ({ ...p, [k]: v }));
      setTax((t) => ({ ...t, [k]: v }));
      return;
    }
    const raw = sanitizeDecimal(v);
    setDraft((p) => ({ ...p, [k]: raw }));
    const n = Number(raw);
    const err = raw === "" || !Number.isFinite(n) || n < 0 ? "Not saved. Must be 0 or more"
      : TAX_PCT_KEYS.has(k) && n > 100 ? "Not saved. Must be 0–100%" : null;
    setErrors((e) => { const x = { ...e }; if (err) x[k] = err; else delete x[k]; return x; });
    if (!err) setTax((t) => ({ ...t, [k]: n }));
  };
  useSyncedDraft(tax, taxToDraft, setDraft, errors);
  const resetDefaults = () => {
    const prev = tax;
    setTax({ ...DEFAULT_TAX });
    setErrors({});
    showToast("Tax reset to defaults", { action: { label: "Undo", onClick: () => setTax(prev) } });
  };
  const field = (k, label, unit) => <RateInput label={label} unit={unit} value={draft[k]} onChange={(v) => set(k, v)} error={errors[k]} />;
  return (
    <div>
      <label style={{ display: "flex", alignItems: "center", gap: 12, padding: "8px 0", cursor: "pointer" }}>
        <span style={{ fontSize: 14, flex: 1 }}>Calculate Jamaica payroll deductions (NIS, NHT, Education Tax, PAYE)</span>
        <input type="checkbox" className="nsc-switch" checked={draft.enabled} onChange={(e) => set("enabled", e.target.checked)} />
      </label>
      {draft.enabled && (
        <>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginTop: 10, alignItems: "end" }}>
            {field("nis", "NIS", "%")}
            {field("nisCapMonthly", "NIS cap (monthly)", "$")}
            {field("nht", "NHT", "%")}
            {field("eduTax", "Education Tax", "%")}
            {field("payeThreshold", "PAYE threshold (monthly)", "$")}
            {field("payeRate1", "PAYE rate (lower)", "%")}
            {field("payeBreak2", "PAYE break point (monthly)", "$")}
            {field("payeRate2", "PAYE rate (upper)", "%")}
            {field("pension", "Pension (if any)", "%")}
          </div>
          <div style={{ fontSize: 12, color: "var(--ink-faint)", marginTop: 10, lineHeight: 1.5 }}>
            Defaults follow TAJ 2025/26 rates (tax-free threshold from 1 April 2026). Check against your latest pay slip. Estimates only.
          </div>
        </>
      )}
      <SettingsFooter onDefaults={resetDefaults} />
    </div>
  );
}

function RateHistoryTab({ ratesHistory, setRatesHistory, currentRates }) {
  const [date, setDate] = useStateM("");
  const list = [...(ratesHistory || [])].sort((a, b) => effDate(b.effectiveFrom) - effDate(a.effectiveFrom));
  // Keyed by date (plus a count for an old backup's same-date pair), not by
  // position in ratesHistory: a delete shifts positions, and the rows after
  // it would remount and rise in again.
  const seen = {};
  const rowKey = (e) => { const d = String(e.effectiveFrom); seen[d] = (seen[d] || 0) + 1; return `${d}#${seen[d]}`; };
  const addEntry = () => {
    if (!date) { showToast("Pick the date these rates started first"); return; }
    const entry = { effectiveFrom: date, rates: { ...currentRates } };
    // One entry per date: saving the same date again replaces it. Two entries
    // with one date would tie in ratesAt(), and the older one would win.
    const others = (ratesHistory || []).filter((x) => String(x.effectiveFrom).slice(0, 10) !== date);
    setRatesHistory([...others, entry]);
    setDate("");
  };
  // Past periods are worked out with these rates, so a delete can be undone.
  const [isLeaving, remove] = useFoldAway((e) => {
    setRatesHistory((prev) => prev.filter((x) => x !== e));
    showToast("Past rates deleted", { action: { label: "Undo", onClick: () => setRatesHistory((prev) => (prev.includes(e) ? prev : [...prev, e])) } });
  });
  return (
    <div>
      <div style={{ fontSize: 12.5, color: "var(--ink-dim)", marginBottom: 12, lineHeight: 1.5 }}>
        Save the current rates with an effective-from date. Past periods use the rates active at the time, so historic snapshots stay correct when rates change.
      </div>
      {list.length === 0 ? (
        <div style={{
          padding: "16px", textAlign: "center",
          border: "1px dashed var(--line)", borderRadius: 10,
          color: "var(--ink-faint)", fontSize: 12.5,
        }}>
          No history yet — current rates apply to all periods.
        </div>
      ) : (
        <div style={{ display: "flex", flexDirection: "column" }}>
          {list.map((e, i) => (
            <AnimatedRow key={rowKey(e)} index={i} leaving={isLeaving(e)}>
              <div style={{
                display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8,
                padding: "10px 6px 10px 14px",
                background: "var(--bg-2)", border: "1px solid var(--line-soft)", borderRadius: 12,
              }}>
                <div style={{ minWidth: 0 }}>
                  <div style={{ fontSize: 13.5, fontWeight: 500, color: "var(--ink)" }}>From {effDate(e.effectiveFrom).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}</div>
                  <div className="mono" style={{ fontSize: 11, color: "var(--ink-faint)", marginTop: 2 }}>
                    SP1 {fmt(e.rates.sp1)} · SP2 {fmt(e.rates.sp2)} · Meal {fmt(e.rates.meal)}
                  </div>
                </div>
                {/* Delete by identity: `list` is sorted, so its index doesn't match ratesHistory's. */}
                <button onClick={() => remove(e)} style={{ ...iconBtn(), background: "transparent", border: "none", color: "var(--ink-faint)", flexShrink: 0 }} aria-label="Delete these rates" title="Delete"><XIcon /></button>
              </div>
            </AnimatedRow>
          ))}
        </div>
      )}
      <div style={{ display: "flex", gap: 8, marginTop: 14, alignItems: "center" }}>
        <input type="date" value={date} onChange={(e) => setDate(e.target.value)} style={{
          flex: 1, background: "var(--bg-2)", border: "1px solid var(--line)", borderRadius: 10,
          padding: "9px 12px", color: "var(--ink)", fontSize: 16, fontFamily: "inherit",
        }} />
        <button onClick={addEntry} style={primaryBtn()}>Save current rates</button>
      </div>
    </div>
  );
}

function ThemeTab({ theme, setTheme }) {
  return (
    <div>
      <div className="label" style={{ marginBottom: 12 }}>Appearance</div>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 10 }}>
        <ThemeCard active={theme === "dark"} onClick={() => setTheme("dark")} label="Dark" preview={{ bg: "#000000", ink: "#f5f5f7", accent: "#ff9f0a" }} />
        <ThemeCard active={theme === "light"} onClick={() => setTheme("light")} label="Light" preview={{ bg: "#f2f2f7", ink: "#1d1d1f", accent: "#b84f00" }} />
        <ThemeCard active={theme === "auto"} onClick={() => setTheme("auto")} label="Auto" preview={{ bg: "linear-gradient(135deg, #000000 0%, #000000 49%, #f2f2f7 51%, #f2f2f7 100%)", ink: "#8e8e93", accent: "#ff9f0a" }} />
      </div>
      <div style={{ fontSize: 12, color: "var(--ink-faint)", marginTop: 10 }}>
        Auto follows your phone's light or dark setting.
      </div>
    </div>
  );
}

function ThemeCard({ active, onClick, label, preview }) {
  return (
    <button onClick={onClick} style={{
      padding: 14, borderRadius: 12,
      background: active ? "var(--bg-2)" : "transparent",
      border: `1px solid ${active ? "var(--ink)" : "var(--line)"}`,
      cursor: "pointer", fontFamily: "inherit", textAlign: "left",
      display: "flex", flexDirection: "column", gap: 10,
    }}>
      <div style={{
        height: 50, borderRadius: 8, padding: 8,
        background: preview.bg,
        border: "1px solid var(--line-soft)",
        display: "flex", alignItems: "center", gap: 6,
      }}>
        <div style={{ width: 12, height: 12, borderRadius: "50%", background: preview.accent }} />
        <div style={{ flex: 1, height: 3, borderRadius: 2, background: preview.ink, opacity: 0.5 }} />
      </div>
      <div style={{ fontSize: 13, fontWeight: 600, color: "var(--ink)" }}>{label}</div>
    </button>
  );
}

/* unit: "$" shows before the value; "%" or "h" after it. */
function RateInput({ label, value, onChange, error, unit }) {
  const pre = unit === "$";
  return (
    <div>
      <div className="label" style={{ marginBottom: 6 }}>{label}</div>
      <div style={{ position: "relative" }}>
        {unit && (
          <span aria-hidden style={{
            position: "absolute", top: "50%", transform: "translateY(-50%)",
            [pre ? "left" : "right"]: 12, color: "var(--ink-faint)", fontSize: 14, pointerEvents: "none",
          }}>{unit}</span>
        )}
        <input
          inputMode="decimal" value={value}
          onChange={(e) => onChange(e.target.value)}
          aria-label={label}
          aria-invalid={error ? "true" : undefined}
          style={{
            width: "100%", background: "var(--bg-2)",
            border: `1px solid ${error ? "color-mix(in oklab, var(--warn) 55%, var(--line))" : "var(--line)"}`,
            borderRadius: 10, padding: `9px ${unit && !pre ? 30 : 12}px 9px ${pre ? 24 : 12}px`,
            color: "var(--ink)", fontSize: 16, fontFamily: "inherit",
          }}
        />
      </div>
      {error && <div style={{ fontSize: 11.5, color: "var(--warn)", marginTop: 4 }}>{error}</div>}
    </div>
  );
}

/* ============ Autofill ============ */
function AutofillModal({ period, mode, defaultDist, rotationAnchor, existing, onApply, onClose }) {
  const dismiss = useModalDismiss(onClose);
  const { close } = dismiss;
  const days = periodDays(period);
  const [startKey, setStartKey] = useStateM(ymd(period.start));
  // Rotation: any day the person works a 7AM, picked on the same calendar as
  // the Home setup. Starts from the saved rotation when there is one.
  const [am7Key, setAm7Key] = useStateM(() => am7InPeriod(period, rotationAnchor) || ymd(period.start));
  const [pattern, setPattern] = useStateM("rotation");
  const [span, setSpan] = useStateM("rest");
  const [dist, setDist] = useStateM(defaultDist || "S");
  const [preserve, setPreserve] = useStateM(true);
  const isRotation = pattern === "rotation";

  const preview = isRotation
    ? rotationEntries(period, am7Key, dist)
    : autofillPattern(period, { startKey, pattern, days: span, defaultDist: dist });
  const fillCount = Object.keys(preview).filter(
    (k) => !(preserve && hasShifts(existing?.[k]))
  ).length;
  // Without "keep", a rotation replaces the whole period (off days too).
  const loggedDays = days.filter((d) => hasShifts(existing?.[ymd(d)])).length;
  const replacesAll = isRotation && !preserve && loggedDays > 0;

  return (
    <ModalFrame dismiss={dismiss} top label="Auto-fill shifts">
        <ModalHead title="Auto-fill shifts" onClose={close} />

        <FieldLabel>Pattern</FieldLabel>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8, marginBottom: 14 }}>
          <ChoiceCard active={isRotation} onClick={() => setPattern("rotation")}
            title="Standard rotation" detail="4-day cycle with an off day" />
          {DAY_SHIFTS.map((sh) => (
            <ChoiceCard key={sh.k} active={pattern === sh.k} onClick={() => setPattern(sh.k)}
              title={`${SHIFT_LABEL[sh.k]} only`} detail={sh.hours} color={sh.color} />
          ))}
        </div>

        {isRotation ? (
          <RotationPicker period={period} value={am7Key} onChange={setAm7Key} />
        ) : (
          <>
            <FieldLabel>Start</FieldLabel>
            <select value={startKey} onChange={(e) => setStartKey(e.target.value)} style={selectStyle} aria-label="Start day">
              {days.map((d) => <option key={ymd(d)} value={ymd(d)}>
                {d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" })}
              </option>)}
            </select>

            <FieldLabel>Fill</FieldLabel>
            <SegToggle full options={[
              { v: 7, l: "7 days" },
              { v: 14, l: "14 days" },
              { v: "rest", l: "Rest of period" },
            ]} value={span} onChange={setSpan} />
          </>
        )}

        {/* Only matters when distance varies per shift; otherwise the
            period-wide Short/Long applies. */}
        {mode === "advanced" && (
          <>
            <FieldLabel>Taxi distance</FieldLabel>
            <SegToggle options={[{ v: "S", l: "Short" }, { v: "L", l: "Long" }]} value={dist} onChange={setDist} />
          </>
        )}

        <label style={{ display: "flex", alignItems: "center", gap: 12, marginTop: 14, cursor: "pointer", padding: "8px 0" }}>
          <span style={{ fontSize: 14, flex: 1 }}>Keep days that already have shifts</span>
          <input type="checkbox" className="nsc-switch" checked={preserve} onChange={(e) => setPreserve(e.target.checked)} />
        </label>

        <div style={{
          marginTop: 14, padding: "10px 12px", background: "var(--bg-2)", borderRadius: 10,
          fontSize: 12.5, color: "var(--ink-dim)",
        }}>
          Will fill <span className="mono" style={{ color: "var(--ink)", fontWeight: 600 }}>{fillCount}</span> day{fillCount === 1 ? "" : "s"} with shifts.
          {preserve && " Days you've already logged won't change."}
          {replacesAll && <span style={{ color: "var(--warn)" }}> Replaces the {loggedDays} day{loggedDays === 1 ? "" : "s"} already logged this period.</span>}
        </div>

        <div style={{ display: "flex", gap: 8, marginTop: 14, justifyContent: "flex-end" }}>
          <button onClick={close} style={ghostBtn()}>Cancel</button>
          <button onClick={() => {
            onApply(isRotation
              ? { pattern, am7Key, defaultDist: dist, preserve }
              : { startKey, pattern, days: span, defaultDist: dist, preserve });
            showToast(fillCount > 0 ? `Filled ${fillCount} day${fillCount === 1 ? "" : "s"}` : "Nothing to fill");
            close();
          }} style={accentBtn()}>
            Apply
          </button>
        </div>
    </ModalFrame>
  );
}

function FieldLabel({ children }) {
  return <div className="label" style={{ margin: "12px 0 6px" }}>{children}</div>;
}

const selectStyle = {
  width: "100%", background: "var(--bg-2)", border: "1px solid var(--line)",
  borderRadius: 10, padding: "10px 12px", color: "var(--ink)", fontSize: 16, fontFamily: "inherit",
};

Object.assign(window, { DayModal, SettingsModal, AutofillModal });
