/* ============================================================================
   ReMeDe Weight Journey — หน้าจอฝั่งผู้ป่วย (P1–P11)

   ไฟล์นี้ทำหน้าที่ "แสดงผล + state ของหน้าจอ" เท่านั้น
     - ข้อมูลทั้งหมดมาจาก window.RMD_API (ui_kits/api.js) — ห้ามเรียก supabase ตรงนี้
     - กฎธุรกิจ (แปลงคลิก→mg, ยาคงเหลือ, จัดรูปแบบตัวเลข) อยู่ที่ window.RMDLogic
     - ทุกหน้าที่ดึงข้อมูลต้องมีครบสามสถานะ: กำลังโหลด / ว่าง / มีปัญหา พร้อมปุ่มลองใหม่

   กฎของฝั่งผู้ป่วยที่ห้ามพลาด
     - ผู้ป่วยต้องไม่เห็นสัญญาณเตือนของคลินิก (watch/act) และไม่เห็นศัพท์แพทย์
       ข้อความแจ้งปัญหาจึงใช้โทน info เสมอ ไม่ใช่ act
     - น้ำหนักขึ้นเป็นข้อมูลปกติ ห้ามเตือน ห้ามใช้สีแดง
     - ผู้ป่วยที่อยู่โปรแกรม "ปรับอาหารอย่างเดียว" (program_type = 'diet_only')
       ต้องไม่เห็นอะไรที่เกี่ยวกับยาฉีดเลย ทั้งโดส เกจยา ปุ่มยืนยันฉีด และแท่งโดสในกราฟ
   ============================================================================ */

const NS = window.ReMeDeWeightJourneyDesignSystem_3d7adc;
const { Button, Card, Badge, Icon, IconButton, Tag,
  FormField, Input, NumberStepper, Select, Checkbox, Switch, SegmentedControl, SeverityScale,
  MetricStat, DoseReadout, PenGauge, WeightDoseChart,
  Callout, Dialog, Toast, EmptyState, AppBar, BottomNav } = NS;

const L = window.RMDLogic;
const API = window.RMD_API;

const gut = { padding: "var(--page-gutter-mobile)", display: "flex", flexDirection: "column", gap: "var(--space-5)" };

// AppBar ไม่รู้จัก safe-area ของเครื่อง — ถ้าวางชิดขอบบน หัวข้อจะโดนติ่งจอบัง
// ค่าเริ่มต้น 54/26px คือระยะของ "กรอบเครื่องจำลอง" ตอนดูบนเดสก์ท็อป
// ส่วนบนมือถือจริง index.html จะ override เป็น env(safe-area-inset-*) ของเครื่องนั้น ๆ
const STATUS_BAR = "var(--rmd-safe-top, 54px)";
const HOME_INDICATOR = "var(--rmd-safe-bottom, 26px)";

const DASH = "—";

const TopBar = ({ style, ...rest }) => (
  <AppBar {...rest} style={{ paddingTop: STATUS_BAR, minHeight: `calc(60px + ${STATUS_BAR})`, ...style }} />
);

/* ---------------------------------------------------------------------------
   1. คำแปลค่า enum ของฐานข้อมูล → ภาษาที่ผู้ป่วยอ่านเข้าใจ
--------------------------------------------------------------------------- */
const SYMPTOMS = [
  { value: "nausea", label: "คลื่นไส้" },
  { value: "vomiting", label: "อาเจียน" },
  { value: "constipation", label: "ท้องผูก" },
  { value: "diarrhea", label: "ท้องเสีย" },
  { value: "appetite_loss", label: "เบื่ออาหาร" },
  { value: "fatigue", label: "อ่อนเพลีย" },
];
// อาการที่ตำแหน่งฉีดถามได้เฉพาะคนที่ใช้ยาฉีด
const INJECTION_SYMPTOM = { value: "injection_site_reaction", label: "เจ็บหรือบวมตรงที่ฉีด" };

const SITES = [
  { value: "abdomen_left", label: "หน้าท้องซ้าย" },
  { value: "abdomen_right", label: "หน้าท้องขวา" },
  { value: "thigh_left", label: "ต้นขาซ้าย" },
  { value: "thigh_right", label: "ต้นขาขวา" },
  { value: "upper_arm", label: "ต้นแขน" },
];

const OFF_RX_REASONS = [
  { value: "side_effect", label: "มีผลข้างเคียง จึงลดลงเอง" },
  { value: "not_enough_drug", label: "ยาในด้ามเหลือไม่พอ" },
  { value: "doctor_advice", label: "แพทย์แนะนำทางโทรศัพท์" },
  { value: "miscount", label: "กดคลิกผิด" },
  { value: "other", label: "เหตุผลอื่น" },
];

const SYMPTOM_LABEL = {};
SYMPTOMS.concat([INJECTION_SYMPTOM]).forEach((s) => { SYMPTOM_LABEL[s.value] = s.label; });

// วันในสัปดาห์ของ notification_prefs.weigh_dow — 0 อาทิตย์ ถึง 6 เสาร์
const DOW_OPTIONS = [
  { value: "0", label: "วันอาทิตย์" },
  { value: "1", label: "วันจันทร์" },
  { value: "2", label: "วันอังคาร" },
  { value: "3", label: "วันพุธ" },
  { value: "4", label: "วันพฤหัสบดี" },
  { value: "5", label: "วันศุกร์" },
  { value: "6", label: "วันเสาร์" },
];

// คำขอใช้สิทธิตาม PDPA — ผู้ป่วยต้องไม่เห็นคำว่า export/erase/open ดิบ ๆ
const REQUEST_KIND_TH = { export: "ขอสำเนาข้อมูลของฉัน", erase: "ขอลบข้อมูลของฉัน" };
// โทนที่ใช้ได้กับผู้ป่วยมีแค่ neutral/brand/accent/ok — ห้ามใช้ watch/act ของฝั่งคลินิก
const REQUEST_STATUS_TH = {
  open: ["brand", "กำลังดำเนินการ"],
  done: ["ok", "เสร็จแล้ว"],
  rejected: ["neutral", "ปฏิเสธคำขอ"],
};

/* ---------------------------------------------------------------------------
   2. ตัวช่วยจัดรูปแบบ (เรื่องการแสดงผล ไม่ใช่กฎธุรกิจ จึงอยู่ที่หน้าจอ)
--------------------------------------------------------------------------- */
function fmtDate(value) {
  if (!value) return DASH;
  const d = new Date(value);
  if (isNaN(d.getTime())) return DASH;
  return d.toLocaleDateString("th-TH", { day: "numeric", month: "short", year: "numeric" });
}

function fmtDayMonth(value) {
  if (!value) return DASH;
  const d = new Date(value);
  if (isNaN(d.getTime())) return DASH;
  return d.toLocaleDateString("th-TH", { day: "numeric", month: "short" });
}

function fmtSince(value) {
  if (!value) return "ยังไม่มีบันทึก";
  const d = new Date(value);
  if (isNaN(d.getTime())) return DASH;
  const days = Math.floor((Date.now() - d.getTime()) / 86400000);
  if (days <= 0) return "วันนี้";
  if (days === 1) return "เมื่อวาน";
  return days + " วันที่แล้ว";
}

function daysSince(value) {
  if (!value) return null;
  const t = new Date(value).getTime();
  if (!Number.isFinite(t)) return null;
  return Math.floor((Date.now() - t) / 86400000);
}

/** ฐานข้อมูลคืนเวลาเป็น "HH:MM:SS" แต่ input type=time ต้องการ "HH:MM" */
function hhmm(value) {
  const s = String(value || "");
  return /^\d{2}:\d{2}/.test(s) ? s.slice(0, 5) : "";
}

/**
 * วันที่วันนี้ในรูป YYYY-MM-DD **ตามโซนเวลาไทย**
 * ต้องตรงกับที่ DB ใช้เติม reported_on/measured_on (trigger ใช้ Asia/Bangkok)
 * เดิมใช้เวลาเครื่อง — ผู้ป่วยที่ตั้งโซนอื่นหรืออยู่ต่างประเทศจะได้คนละวันกับ DB
 * ทำให้หน้าจอคิดว่า "ยังไม่เคยบันทึกวันนี้" แล้วไปเข้าทาง insert ที่ชนของเดิม
 */
function todayISO() {
  return new Intl.DateTimeFormat("en-CA", {
    timeZone: "Asia/Bangkok", year: "numeric", month: "2-digit", day: "2-digit",
  }).format(new Date());
}

/**
 * รวมข้อมูลดิบเป็นซีรีส์รายสัปดาห์สำหรับกราฟ
 * สัปดาห์ที่ไม่มีบันทึกต้องได้ weight = null เพื่อให้กราฟ "ขาดเส้น"
 * ห้ามเติม 0 หรือลากค่าเดิมต่อ — นั่นคือการเดาค่าที่ผู้ป่วยไม่ได้บันทึก
 */
function buildWeekSeries({ programStartAt, weights, injections, sideEffects }) {
  const all = []
    .concat((weights || []).map((w) => w.measured_at))
    .concat((injections || []).map((i) => i.injected_at))
    .concat((sideEffects || []).map((s) => s.reported_at))
    .map((v) => new Date(v).getTime())
    .filter((t) => Number.isFinite(t));
  if (!all.length) return [];

  const startCandidates = all.slice();
  if (programStartAt) {
    const ps = new Date(programStartAt).getTime();
    if (Number.isFinite(ps)) startCandidates.push(ps);
  }
  const start = Math.min.apply(null, startCandidates);
  const weekOf = (value) => Math.max(0, Math.floor((new Date(value).getTime() - start) / 604800000));

  const lastWeek = Math.max(weekOf(Date.now()), weekOf(Math.max.apply(null, all)));
  const series = [];
  for (let i = 0; i <= lastWeek; i++) {
    series.push({ label: "ส." + (i + 1), weight: null, mg: null, se: null });
  }

  (weights || []).forEach((w) => {
    const slot = series[weekOf(w.measured_at)];
    if (slot) slot.weight = Number(w.weight_kg);
  });
  (injections || []).forEach((inj) => {
    const slot = series[weekOf(inj.injected_at)];
    if (!slot) return;
    slot.mg = (slot.mg || 0) + Number(inj.mg || 0);
  });
  (sideEffects || []).forEach((se) => {
    const slot = series[weekOf(se.reported_at)];
    if (!slot) return;
    const level = Number(se.severity);
    if (Number.isFinite(level)) slot.se = Math.max(slot.se || 0, level);
  });
  return series;
}

/* ---------------------------------------------------------------------------
   3. สถานะการโหลด — ใช้ซ้ำทุกหน้า จะได้ไม่มีหน้าไหนลืมสถานะใดสถานะหนึ่ง
--------------------------------------------------------------------------- */
function useAsync(run, deps) {
  const [state, setState] = React.useState({ loading: true, error: null, data: null });
  const [tick, setTick] = React.useState(0);
  const keys = (deps || []).concat([tick]);

  React.useEffect(() => {
    let alive = true;
    // คงข้อมูลเดิมไว้ระหว่างโหลดรอบใหม่ หน้าจอจะได้ไม่กระพริบเป็นหน้าโหลดทุกครั้งที่กดบันทึก
    setState((s) => ({ loading: true, error: null, data: s.data }));
    Promise.resolve()
      .then(run)
      .then(
        (data) => { if (alive) setState({ loading: false, error: null, data: data }); },
        (err) => { if (alive) setState({ loading: false, error: (err && err.message) || "โหลดข้อมูลไม่สำเร็จ", data: null }); }
      );
    return () => { alive = false; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, keys);

  return { loading: state.loading, error: state.error, data: state.data, reload: () => setTick((t) => t + 1) };
}

function ScreenShell({ title, subtitle, tone, onBack, action, children, footer }) {
  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
      <TopBar title={title} subtitle={subtitle} tone={tone} onBack={onBack} action={action} />
      <div className="rmd-scroll" style={{ ...gut, flex: 1, overflowY: "auto", paddingBottom: "var(--space-8)" }}>
        {children}
      </div>
      {footer ? (
        <div style={{ padding: "var(--page-gutter-mobile)", borderTop: "1px solid var(--border-hairline)", background: "var(--surface-card)" }}>
          {footer}
        </div>
      ) : null}
    </div>
  );
}

function LoadingBlock({ label }) {
  return <EmptyState icon="loader" title={label || "กำลังโหลดข้อมูล"} description="สักครู่นะคะ" />;
}

/** แจ้งปัญหาแบบไม่ทำให้ตกใจ — ผู้ป่วยต้องไม่เห็นโทนเตือนของคลินิก */
function ProblemBlock({ message, onRetry }) {
  return (
    <EmptyState icon="cloud-off" title="ยังโหลดข้อมูลไม่ได้"
      description={message || "ตอนนี้เชื่อมต่อระบบไม่ได้ ลองใหม่อีกครั้งได้เลย"}
      action={onRetry ? <Button variant="secondary" icon="refresh-cw" onClick={onRetry}>ลองใหม่</Button> : null} />
  );
}

/** กล่องข้อความเมื่อบันทึกไม่สำเร็จ — ใช้โทน info เสมอ ไม่ใช่ act */
function SaveProblem({ message }) {
  if (!message) return null;
  return <Callout tone="info" icon="info" title="ยังบันทึกไม่ได้">{message}</Callout>;
}

/**
 * ป้ายบอกว่าตอนนี้อยู่ในโหมดดูอย่างเดียว (ถอนความยินยอมไว้)
 * ต้องบอกทางออกเสมอ ไม่ใช่แค่บอกว่าทำไม่ได้
 */
function ReadOnlyNote({ onGoProfile }) {
  return (
    <Callout tone="info" icon="lock" title="ตอนนี้เป็นโหมดดูอย่างเดียว"
      action={onGoProfile ? <Button size="sm" variant="secondary" onClick={onGoProfile}>ไปหน้าโปรไฟล์</Button> : null}>
      คุณถอนความยินยอมให้เก็บข้อมูลไว้ จึงบันทึกน้ำหนักและอาการใหม่ไม่ได้
      ข้อมูลเดิมยังอยู่ครบและดูได้ตามปกติ · ยินยอมใหม่ได้ที่หน้าโปรไฟล์
    </Callout>
  );
}

const brandHead = (
  <div style={{ textAlign: "center", marginBottom: "var(--space-7)" }}>
    <p className="rmd-wordmark" style={{ fontSize: 34 }}>ReMeDe</p>
    <p style={{ font: "var(--type-caption)", color: "var(--bronze-700)", letterSpacing: "var(--ls-eyebrow)", textTransform: "uppercase", marginTop: 2 }}>Weight Journey</p>
  </div>
);

/* ===========================================================================
   P1 — เข้าสู่ระบบด้วยรหัสผู้ป่วย (HN) + รหัสผ่าน

   ทำไมไม่ใช้อีเมลหรือ LINE: คลินิกมีผู้ป่วยไม่ถึงสิบคนต่อเดือน ผู้ป่วยหลายคน
   ไม่มีอีเมลที่ใช้จริง และคลินิกไม่อยากเพิ่ม channel LINE เพราะมี API ตัวอื่นเชื่อมอยู่แล้ว
   HN เป็นสิ่งที่ผู้ป่วยมีติดตัวอยู่แล้วตั้งแต่วันแรกที่มาคลินิก

   บัญชีถูกเปิดโดยคลินิกเท่านั้น (ปิด self-signup ที่ Supabase)
   ข้อความ error ไม่แยกว่า "ไม่มี HN นี้" กับ "รหัสผ่านผิด" — ไม่งั้นคนนอกไล่เดา
   ได้ว่า HN ไหนเป็นคนไข้ของคลินิกจริง
=========================================================================== */
function LoginScreen() {
  const [hn, setHn] = React.useState("");
  const [password, setPassword] = React.useState("");
  const [showPw, setShowPw] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);

  const canSubmit = hn.trim() && password && !busy;

  async function submit(e) {
    if (e) e.preventDefault();
    if (!canSubmit) return;
    setBusy(true);
    setError(null);
    try {
      // สำเร็จแล้วไม่ต้องทำอะไรต่อ — onAuthStateChange ใน PatientApp จะพาไปหน้าถัดไปเอง
      await API.auth.signInPatient(hn, password);
    } catch (err) {
      setError((err && err.message) || "เข้าสู่ระบบไม่สำเร็จ กรุณาลองใหม่");
      setBusy(false);
    }
  }

  return (
    <form onSubmit={submit} className="rmd-scroll"
      style={{ ...gut, height: "100%", overflowY: "auto", background: "var(--surface-page)", justifyContent: "center" }}>
      {brandHead}

      <FormField label="รหัสผู้ป่วย (HN)" hint="ตามที่คลินิกให้ไว้ พิมพ์ตัวเล็กตัวใหญ่ก็ได้">
        {/* autoCapitalize/autoCorrect ปิดไว้ — คีย์บอร์ดมือถือชอบขึ้นตัวใหญ่ให้เองแล้วเติมจุด */}
        <Input inputMode="text" autoComplete="username" autoCapitalize="off" autoCorrect="off"
          spellCheck={false} placeholder="HN-24118"
          value={hn} onChange={(e) => setHn(e.target.value)} />
      </FormField>

      <FormField label="รหัสผ่าน">
        <div style={{ display: "flex", gap: "var(--space-3, 8px)", alignItems: "center" }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            <Input type={showPw ? "text" : "password"} autoComplete="current-password"
              placeholder="รหัสผ่านของคุณ"
              value={password} onChange={(e) => setPassword(e.target.value)} />
          </div>
          {/* รหัสตั้งต้นเป็นตัวสุ่มยาว พิมพ์ผิดง่ายมากถ้ามองไม่เห็น */}
          <Button type="button" variant="ghost" size="sm" icon={showPw ? "eye-off" : "eye"}
            onClick={() => setShowPw((v) => !v)} aria-label={showPw ? "ซ่อนรหัสผ่าน" : "แสดงรหัสผ่าน"} />
        </div>
      </FormField>

      {error ? <Callout tone="info" icon="info" title="เข้าสู่ระบบไม่สำเร็จ">{error}</Callout> : null}

      <Button block size="lg" type="submit" disabled={!canSubmit}>
        {busy ? "กำลังเข้าสู่ระบบ" : "เข้าสู่ระบบ"}
      </Button>

      <p style={{ font: "var(--type-caption)", color: "var(--text-subtle)", textAlign: "center", marginTop: "var(--space-5)" }}>
        ลืมรหัสผ่าน ติดต่อคลินิกเพื่อขอรหัสใหม่
        <br />ข้อมูลของคุณถูกเก็บตามมาตรฐาน PDPA
      </p>
    </form>
  );
}

/* ===========================================================================
   P1b — ตั้งรหัสผ่านของตัวเองตอนเข้าครั้งแรก

   รหัสตั้งต้นถูกสร้างโดยระบบและผ่านมือเจ้าหน้าที่มาแล้ว ผู้ป่วยจึงควรเปลี่ยนทันที
   หน้านี้ขวางอยู่ก่อนหน้าอื่นทั้งหมด แต่ยังออกจากระบบได้ ไม่ขังไว้
=========================================================================== */
function FirstPasswordScreen({ onDone, onLogout }) {
  const [pw, setPw] = React.useState("");
  const [pw2, setPw2] = React.useState("");
  const [showPw, setShowPw] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);

  const tooShort = pw.length > 0 && pw.length < 8;
  const mismatch = pw2.length > 0 && pw !== pw2;
  const canSubmit = pw.length >= 8 && pw === pw2 && !busy;

  async function submit(e) {
    if (e) e.preventDefault();
    if (!canSubmit) return;
    setBusy(true);
    setError(null);
    try {
      await API.auth.changeOwnPassword(pw);
      onDone();
    } catch (err) {
      setError((err && err.message) || "ตั้งรหัสผ่านไม่สำเร็จ กรุณาลองใหม่");
      setBusy(false);
    }
  }

  return (
    <form onSubmit={submit} className="rmd-scroll"
      style={{ ...gut, height: "100%", overflowY: "auto", background: "var(--surface-page)", justifyContent: "center" }}>
      {brandHead}

      <Callout tone="info" icon="key-round" title="ตั้งรหัสผ่านของคุณเอง">
        รหัสที่คลินิกให้มาเป็นรหัสชั่วคราว กรุณาตั้งรหัสใหม่ที่คุณจำได้เพื่อใช้ครั้งต่อไป
      </Callout>

      <FormField label="รหัสผ่านใหม่" hint="อย่างน้อย 8 ตัวอักษร"
        error={tooShort ? "รหัสผ่านสั้นเกินไป ต้องยาวอย่างน้อย 8 ตัวอักษร" : null}>
        <div style={{ display: "flex", gap: "var(--space-3, 8px)", alignItems: "center" }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            <Input type={showPw ? "text" : "password"} autoComplete="new-password"
              value={pw} onChange={(e) => setPw(e.target.value)} />
          </div>
          <Button type="button" variant="ghost" size="sm" icon={showPw ? "eye-off" : "eye"}
            onClick={() => setShowPw((v) => !v)} aria-label={showPw ? "ซ่อนรหัสผ่าน" : "แสดงรหัสผ่าน"} />
        </div>
      </FormField>

      <FormField label="พิมพ์รหัสผ่านใหม่อีกครั้ง"
        error={mismatch ? "รหัสผ่านสองช่องไม่ตรงกัน" : null}>
        <Input type={showPw ? "text" : "password"} autoComplete="new-password"
          value={pw2} onChange={(e) => setPw2(e.target.value)} />
      </FormField>

      {error ? <Callout tone="act" icon="triangle-alert" title="ตั้งรหัสผ่านไม่สำเร็จ">{error}</Callout> : null}

      <Button block size="lg" type="submit" disabled={!canSubmit}>
        {busy ? "กำลังบันทึก" : "บันทึกรหัสผ่านใหม่"}
      </Button>

      <Button block variant="ghost" type="button" onClick={onLogout} disabled={busy}>
        ออกจากระบบ
      </Button>
    </form>
  );
}

/* ===========================================================================
   P2 — ยินยอมการเก็บข้อมูล (PDPA)
   ฐานข้อมูลบังคับไว้ว่าถ้ายังไม่ยินยอม จะบันทึกน้ำหนัก/อาการ/การฉีดยาไม่ได้เลย
   หน้านี้จึงต้องผ่านให้ได้ก่อนถึงจะเข้าแอปได้
=========================================================================== */
function ConsentScreen({ patientId, dietOnly, onDone, onLogout }) {
  const [treatment, setTreatment] = React.useState(false);
  const [line, setLine] = React.useState(false);
  const [saving, setSaving] = React.useState(false);
  const [error, setError] = React.useState(null);

  async function submit() {
    setSaving(true);
    setError(null);
    try {
      await API.acceptConsent(patientId, "treatment_data", API.CONSENT_VERSION);
      if (line) await API.acceptConsent(patientId, "line_notify", API.CONSENT_VERSION);
      await onDone();
    } catch (err) {
      setError((err && err.message) || "บันทึกคำยินยอมไม่สำเร็จ กรุณาลองใหม่");
    } finally {
      setSaving(false);
    }
  }

  return (
    <ScreenShell title="การเก็บข้อมูลของคุณ"
      action={<IconButton icon="log-out" label="ออกจากระบบ" onClick={onLogout} />}
      footer={<Button block size="lg" disabled={!treatment || saving} onClick={submit}>
        {saving ? "กำลังบันทึก" : "ยินยอมและเริ่มใช้งาน"}
      </Button>}>
      <p style={{ font: "var(--type-body)" }}>
        {dietOnly
          ? "เพื่อให้ทีมแพทย์ติดตามการรักษาได้ต่อเนื่อง ระบบจะเก็บข้อมูลสองอย่างนี้เท่านั้น"
          : "เพื่อให้ทีมแพทย์ติดตามการรักษาได้ต่อเนื่อง ระบบจะเก็บข้อมูลสามอย่างนี้เท่านั้น"}
      </p>
      {[["scale", "น้ำหนักและสัดส่วนที่คุณบันทึก"]]
        .concat(dietOnly ? [] : [["syringe", "โดสยาและวันที่ฉีด"]])
        .concat([["clipboard-list", "อาการที่คุณรายงาน"]]).map(([ic, tx]) => (
        <div key={tx} style={{ display: "flex", gap: "var(--space-4)", alignItems: "center" }}>
          <span style={{ color: "var(--bronze-600)" }}><Icon name={ic} size={20} /></span>
          <span style={{ font: "var(--type-body)" }}>{tx}</span>
        </div>
      ))}
      <Callout tone="quiet" title="ใครเห็นข้อมูลได้">แพทย์และพยาบาลที่ดูแลคุณเท่านั้น · คุณขอลบข้อมูลได้ทุกเมื่อ</Callout>
      <Checkbox checked={treatment} onChange={setTreatment} label="ยินยอมให้เก็บและใช้ข้อมูลเพื่อการรักษา"
        description="จำเป็นต้องยินยอมข้อนี้ ระบบจึงจะบันทึกข้อมูลให้คุณได้" />
      <Checkbox checked={line} onChange={setLine} label="ยินยอมให้ส่งการแจ้งเตือนผ่าน LINE"
        description={(dietOnly ? "เตือนวันชั่งน้ำหนักประจำสัปดาห์" : "เตือนวันชั่งน้ำหนักและวันฉีดยา")
          + " · เปลี่ยนใจภายหลังได้ในหน้าโปรไฟล์"} />
      <SaveProblem message={error} />
    </ScreenShell>
  );
}

/* ===========================================================================
   P4 — หน้าแรก
=========================================================================== */
function HomeScreen({ data, onLog, onInject, onSideEffect, onChart, onProfile }) {
  const { patient, dietOnly, weights, prescriptions, pens, injections, series, readOnly } = data;

  const latest = weights.length ? weights[weights.length - 1] : null;
  const previous = weights.length > 1 ? weights[weights.length - 2] : null;
  const delta = latest && previous ? L.weightDelta(latest.weight_kg, previous.weight_kg) : null;
  const startWeight = patient.start_weight_kg != null
    ? Number(patient.start_weight_kg)
    : (weights.length ? Number(weights[0].weight_kg) : null);
  const lossPct = latest ? L.lossPercentFrom(startWeight, latest.weight_kg) : null;
  const lossKg = latest && startWeight != null ? startWeight - Number(latest.weight_kg) : null;
  const targetWeight = patient.target_weight_kg != null ? Number(patient.target_weight_kg) : null;
  const toTarget = latest && targetWeight != null ? Number(latest.weight_kg) - targetWeight : null;

  const programWeek = patient.program_start_at
    ? Math.floor((daysSince(patient.program_start_at) || 0) / 7) + 1 : null;

  const currentRx = prescriptions.find((r) => r.is_current) || null;
  const activePens = pens.filter((p) => p.status === "in_use");
  const mainPen = activePens.slice().sort((a, b) => String(a.expiry).localeCompare(String(b.expiry)))[0] || null;
  const lastInjection = injections.length ? injections[injections.length - 1] : null;
  const daysToInjection = lastInjection ? 7 - (daysSince(lastInjection.injected_at) || 0) : null;

  const weeksWithLog = series.filter((w) => w.weight != null).length;

  return (
    <ScreenShell tone="brand"
      title={"สวัสดี คุณ" + (patient.nickname || "")}
      subtitle={programWeek ? "สัปดาห์ที่ " + programWeek + " ของโปรแกรม" : "ยินดีต้อนรับเข้าโปรแกรม"}>

      {readOnly ? <ReadOnlyNote onGoProfile={onProfile} /> : null}

      <Card tone="inverse" padding="lg">
        <MetricStat tone="inverse" size="lg"
          label={latest ? "น้ำหนักล่าสุด · " + fmtDayMonth(latest.measured_at) : "น้ำหนักล่าสุด"}
          value={latest ? L.formatKg(latest.weight_kg) : DASH} unit={latest ? "กก." : undefined}
          delta={delta == null ? undefined : delta} deltaLabel={delta == null ? undefined : "จากครั้งก่อน"}
          footnote={latest ? undefined : "ยังไม่มีบันทึก เริ่มบันทึกครั้งแรกได้เลย"} />
        {latest ? (
          <React.Fragment>
            <div style={{ height: 1, background: "rgba(255,255,255,.14)", margin: "var(--space-5) 0" }} />
            <div style={{ display: "flex", gap: "var(--space-8)" }}>
              <MetricStat tone="inverse" size="sm" label="ลดลงสะสม"
                value={lossPct == null ? DASH : L.formatPercent(lossPct)} unit={lossPct == null ? undefined : "%"} />
              <MetricStat tone="inverse" size="sm" label="ลดลงทั้งหมด"
                value={lossKg == null ? DASH : L.formatKg(lossKg)} unit={lossKg == null ? undefined : "กก."} />
            </div>
          </React.Fragment>
        ) : null}
      </Card>

      {dietOnly ? (
        /* โปรแกรมปรับอาหารอย่างเดียว — ไม่มีเรื่องยาเลย
           ที่ว่างตรงนี้จึงใช้บอกความคืบหน้าสู่เป้าหมายและรอบการบันทึกแทน */
        <Card title="ความคืบหน้าของคุณ">
          <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-5)" }}>
            <div style={{ display: "flex", gap: "var(--space-7)" }}>
              <MetricStat size="sm" label="น้ำหนักเริ่มต้น" value={startWeight == null ? DASH : L.formatKg(startWeight)} unit={startWeight == null ? undefined : "กก."} />
              <MetricStat size="sm" label="เป้าหมาย" value={targetWeight == null ? DASH : L.formatKg(targetWeight)} unit={targetWeight == null ? undefined : "กก."} />
            </div>
            <MetricStat size="sm" icon="target" label="เหลืออีก"
              value={toTarget == null ? DASH : L.formatKg(Math.max(0, toTarget))}
              unit={toTarget == null ? undefined : "กก."}
              footnote={toTarget == null ? "ตั้งเป้าหมายร่วมกับแพทย์ได้ที่คลินิก"
                : toTarget <= 0 ? "ถึงเป้าหมายที่ตั้งไว้แล้ว" : "จนถึงเป้าหมายที่ตั้งไว้"} />
          </div>
          <div style={{ marginTop: "var(--space-5)", display: "flex", flexDirection: "column", gap: "var(--space-3)" }}>
            <Button block size="lg" icon="scale" disabled={readOnly} onClick={onLog}>บันทึกน้ำหนักสัปดาห์นี้</Button>
            <Button block size="lg" variant="secondary" icon="clipboard-list" disabled={readOnly} onClick={onSideEffect}>บันทึกอาการสัปดาห์นี้</Button>
          </div>
        </Card>
      ) : (
        <Card>
          {currentRx ? (
            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "var(--space-4)" }}>
              <MetricStat size="sm" icon="syringe" label="โดสสัปดาห์นี้"
                value={L.formatMg(currentRx.prescribed_mg)} unit="mg"
                footnote={currentRx.prescribed_clicks + " คลิก · " + currentRx.pen_label} />
              {daysToInjection == null ? (
                <Badge tone="brand" icon="calendar-days">ยังไม่เคยบันทึกการฉีด</Badge>
              ) : daysToInjection > 0 ? (
                <Badge tone="brand" icon="calendar-days">อีก {daysToInjection} วัน</Badge>
              ) : (
                <Badge tone="brand" icon="calendar-days">ถึงรอบฉีดแล้ว</Badge>
              )}
            </div>
          ) : (
            <Callout tone="quiet" title="ยังไม่มีโดสที่แพทย์กำหนด">
              แพทย์จะกำหนดจำนวนคลิกให้ในวันที่คุณมารับยาที่คลินิก
            </Callout>
          )}

          {mainPen ? (
            <div style={{ marginTop: "var(--space-5)" }}>
              <PenGauge remaining={mainPen.clicks_remaining} total={mainPen.clicks_total}
                needed={currentRx ? currentRx.prescribed_clicks : undefined}
                expiry={fmtDate(mainPen.expiry)} />
            </div>
          ) : (
            <div style={{ marginTop: "var(--space-5)" }}>
              <Callout tone="quiet" title="ยังไม่มีปากกาที่ใช้งานอยู่">
                เมื่อรับปากกาที่คลินิกแล้ว ยาคงเหลือจะแสดงที่นี่
              </Callout>
            </div>
          )}

          <div style={{ marginTop: "var(--space-5)", display: "flex", flexDirection: "column", gap: "var(--space-3)" }}>
            <Button block size="lg" icon="scale" disabled={readOnly} onClick={onLog}>บันทึกน้ำหนักสัปดาห์นี้</Button>
            <Button block size="lg" variant="secondary" icon="syringe" disabled={readOnly} onClick={onInject}>ยืนยันการฉีดยา</Button>
            <Button block variant="ghost" icon="clipboard-list" disabled={readOnly} onClick={onSideEffect}>บันทึกอาการสัปดาห์นี้</Button>
          </div>
        </Card>
      )}

      <Callout tone="guide" collapsible defaultOpen={false} title="ชั่งน้ำหนักให้ได้มาตรฐาน">
        วันเดิมของสัปดาห์ · ตอนเช้าหลังเข้าห้องน้ำ · ก่อนอาหาร · ตาชั่งเดิมทุกครั้ง
      </Callout>

      {series.length ? (
        <Card title={"แนวโน้ม " + Math.min(series.length, 12) + " สัปดาห์"} padding="sm"
          action={<Button variant="ghost" size="sm" iconAfter="chevron-right" onClick={onChart}>ดูกราฟ</Button>}>
          <WeightDoseChart data={series.slice(-12)} width={340} height={150}
            showDose={!dietOnly} showSideEffects={false}
            targetWeight={targetWeight == null ? undefined : targetWeight} />
          <p style={{ font: "var(--type-caption)", color: "var(--text-subtle)", marginTop: "var(--space-4)" }}>
            บันทึกแล้ว {weeksWithLog} จาก {series.length} สัปดาห์ · สัปดาห์ที่ไม่ได้บันทึกจะเว้นว่างไว้
          </p>
        </Card>
      ) : (
        <Card padding="md">
          <EmptyState compact icon="chart-line" title="ยังไม่มีกราฟให้ดู"
            description="เมื่อบันทึกน้ำหนักครั้งแรกแล้ว กราฟของคุณจะเริ่มขึ้นที่นี่" />
        </Card>
      )}
    </ScreenShell>
  );
}

/* ===========================================================================
   P5 — บันทึกน้ำหนักสัปดาห์นี้
=========================================================================== */
function LogWeightScreen({ data, onBack, onSaved }) {
  const [weight, setWeight] = React.useState("");
  const [waist, setWaist] = React.useState("");
  const [hip, setHip] = React.useState("");
  const [saving, setSaving] = React.useState(false);
  const [error, setError] = React.useState(null);

  // ใช้กฎเดียวกับหน้ากรอกย้อนหลังและกับ CHECK constraint ฝั่ง DB (25–300)
  const check = L.validateWeight(weight);

  async function save() {
    setSaving(true);
    setError(null);
    try {
      await API.savePatientWeight({
        patientId: data.patient.id,
        weightKg: check.value,
        waistCm: waist,
        hipCm: hip,
      });
      await onSaved();
    } catch (err) {
      setError((err && err.message) || "บันทึกไม่สำเร็จ กรุณาลองใหม่");
    } finally {
      setSaving(false);
    }
  }

  return (
    <ScreenShell title="บันทึกสัปดาห์นี้" onBack={onBack}
      footer={<Button block size="lg" disabled={!check.valid || saving || data.readOnly} onClick={save}>
        {data.readOnly ? "ตอนนี้บันทึกไม่ได้" : saving ? "กำลังบันทึก" : "บันทึก"}
      </Button>}>
      {data.readOnly ? <ReadOnlyNote /> : null}
      <Callout tone="guide" collapsible defaultOpen={false} title="ชั่งน้ำหนักให้ได้มาตรฐาน">
        วันเดิม · ตอนเช้า · ก่อนอาหาร · ตาชั่งเดิม
      </Callout>
      <FormField label="น้ำหนักเช้านี้" required hint="ทศนิยม 1 ตำแหน่ง เช่น 84.2" error={check.message || undefined}>
        <Input big suffix="กก." inputMode="decimal" placeholder="0.0" invalid={!!check.message}
          value={weight} onChange={(e) => setWeight(e.target.value)} />
      </FormField>
      {/* flex-basis ขั้นต่ำ + wrap — จอแคบให้ตกบรรทัดแทนที่จะดันช่องที่สองหลุดออกนอกจอ
          (ป้าย "รอบสะโพก (ไม่บังคับ)" ยาวกว่าครึ่งจอมือถือหลายรุ่น) */}
      <div style={{ display: "flex", gap: "var(--space-4)", flexWrap: "wrap" }}>
        <FormField style={{ flex: "1 1 150px", minWidth: 0 }} label="รอบเอว" optional>
          <Input suffix="ซม." inputMode="decimal" placeholder="0.0" value={waist} onChange={(e) => setWaist(e.target.value)} />
        </FormField>
        <FormField style={{ flex: "1 1 150px", minWidth: 0 }} label="รอบสะโพก" optional>
          <Input suffix="ซม." inputMode="decimal" placeholder="0.0" value={hip} onChange={(e) => setHip(e.target.value)} />
        </FormField>
      </div>
      <Callout tone="quiet">น้ำหนักขึ้นลงในบางสัปดาห์เป็นเรื่องปกติ ระบบบันทึกเป็นข้อมูลตามจริงเสมอ</Callout>
      <SaveProblem message={error} />
    </ScreenShell>
  );
}

/* ===========================================================================
   P6 — ยืนยันการฉีดยา (เฉพาะผู้ที่ใช้ยาฉีด)
   บันทึกผ่าน rpc record_injection เท่านั้น เพราะต้องหักคลิกในด้ามในทรานแซกชันเดียวกัน
   ข้อความผิดพลาดจาก rpc เป็นภาษาไทยอยู่แล้ว — แสดงตรง ๆ ไม่ต้องแปลใหม่
=========================================================================== */
function InjectionScreen({ data, onBack, onSaved }) {
  const { prescriptions, pens } = data;
  const currentRx = prescriptions.find((r) => r.is_current) || null;
  // ใช้ด้ามที่หมดอายุเร็วที่สุดก่อน จะได้ไม่เหลือยาค้างจนหมดอายุ
  const activePens = pens.filter((p) => p.status === "in_use")
    .slice().sort((a, b) => String(a.expiry).localeCompare(String(b.expiry)));

  const [penId, setPenId] = React.useState(activePens.length ? activePens[0].id : "");
  const [clicks, setClicks] = React.useState(currentRx ? currentRx.prescribed_clicks : 1);
  const [site, setSite] = React.useState("");
  const [reason, setReason] = React.useState("");
  const [split, setSplit] = React.useState(false);
  const [secondPenId, setSecondPenId] = React.useState("");
  const [sheetOpen, setSheetOpen] = React.useState(false);
  const [askedShortage, setAskedShortage] = React.useState(false);
  const [saving, setSaving] = React.useState(false);
  const [error, setError] = React.useState(null);

  // id เดียวต่อการเข้าหน้าหนึ่งครั้ง — กันเน็ตหลุดแล้วกดซ้ำจนบันทึกสองรอบ
  const requestId = React.useRef(
    (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : "inj-" + Date.now() + "-" + Math.random().toString(36).slice(2, 10)
  );

  const pen = activePens.find((p) => p.id === penId) || null;
  const model = pen && pen.pen_models ? pen.pen_models : null;
  const secondPen = activePens.find((p) => p.id === secondPenId) || null;
  const otherPens = activePens.filter((p) => p.id !== penId);

  const remaining = pen ? Number(pen.clicks_remaining) : 0;
  const prescribed = currentRx ? currentRx.prescribed_clicks : null;

  // ยาไม่พอ = ถามตั้งแต่เข้าหน้า ไม่ปล่อยให้กดยืนยันแล้วค่อยรู้
  React.useEffect(() => {
    if (askedShortage || !pen || prescribed == null) return;
    if (!L.isEnoughDrug(remaining, prescribed)) {
      setSheetOpen(true);
      setAskedShortage(true);
    }
  }, [pen, prescribed, remaining, askedShortage]);

  // แบ่งฉีด: ใช้ของด้ามเดิมให้หมดก่อน ที่เหลือค่อยเอาจากด้ามใหม่ (กฎอยู่ใน logic.js)
  const plan = L.splitPlan(remaining, clicks);
  const fromCurrent = split ? plan.fromCurrent : clicks;
  const fromSecond = split ? plan.fromNew : 0;
  const offRx = prescribed != null && clicks !== prescribed;

  const canSave = !!pen && !!site && clicks > 0 && (!offRx || !!reason)
    && (!split || (!!secondPen && fromSecond > 0)) && !saving && !data.readOnly;

  async function save() {
    setSaving(true);
    setError(null);
    try {
      await API.recordInjection({
        penId: penId,
        clicks: split ? fromCurrent : clicks,
        site: site,
        offRxReason: offRx ? reason : null,
        secondPenId: split && fromSecond > 0 ? secondPenId : null,
        secondClicks: split && fromSecond > 0 ? fromSecond : null,
        clientRequestId: requestId.current,
      });
      await onSaved();
    } catch (err) {
      setError((err && err.message) || "บันทึกไม่สำเร็จ กรุณาลองใหม่");
    } finally {
      setSaving(false);
    }
  }

  if (!activePens.length) {
    return (
      <ScreenShell title="ยืนยันการฉีดยา" onBack={onBack}>
        <EmptyState icon="syringe" title="ยังไม่มีปากกาที่ใช้งานอยู่"
          description="เมื่อรับปากกาที่คลินิกแล้ว หน้านี้จะให้บันทึกการฉีดได้"
          action={<Button variant="secondary" onClick={onBack}>กลับหน้าแรก</Button>} />
      </ScreenShell>
    );
  }

  return (
    <div style={{ position: "relative", height: "100%" }}>
      <ScreenShell title="ยืนยันการฉีดยา" onBack={onBack}
        footer={<Button block size="lg" disabled={!canSave} onClick={save}>
          {data.readOnly ? "ตอนนี้บันทึกไม่ได้" : saving ? "กำลังบันทึก" : "ยืนยันว่าฉีดแล้ว"}
        </Button>}>

        {data.readOnly ? <ReadOnlyNote /> : null}

        <FormField label="ปากกาที่ใช้" required>
          <Select value={penId} onChange={(e) => { setPenId(e.target.value); setAskedShortage(false); setSplit(false); }}
            options={activePens.map((p) => ({
              value: p.id,
              label: (p.pen_models ? p.pen_models.label : "ปากกา") + " · เหลือ " + p.clicks_remaining + " คลิก",
            }))} />
        </FormField>

        {pen ? (
          <PenGauge label="ยาคงเหลือในด้ามนี้" remaining={pen.clicks_remaining} total={pen.clicks_total}
            needed={prescribed == null ? undefined : prescribed} expiry={fmtDate(pen.expiry)} />
        ) : null}

        <FormField label="จำนวนคลิกที่กดจริง" required>
          {/* เพดานคือ "ที่เหลือจริงในด้าม" ไม่ใช่ความจุทั้งด้าม — กันตั้งแต่หน้าจอ
              ไม่ต้องปล่อยให้ไปตายที่ rpc แล้วค่อยบอกผู้ป่วยว่ายาไม่พอ
              ยกเว้นตอนแบ่งฉีดสองด้าม ที่ยอดรวมเกินด้ามเดียวได้ */}
          <NumberStepper value={Number(clicks) || 0} onChange={(v) => setClicks(Math.round(Number(v) || 0))}
            min={1}
            max={pen
              ? (split
                  ? Number(pen.clicks_remaining) + Number(secondPen ? secondPen.clicks_remaining : 0)
                  : Number(pen.clicks_remaining))
              : 200}
            unit="คลิก"
            hint={prescribed == null ? "ยังไม่มีใบสั่งจากแพทย์" : "ใบสั่งล่าสุด " + prescribed + " คลิก"} />
        </FormField>

        {/* mg คิดจากรุ่นปากกาที่เลือกจริงเสมอ — ห้ามใช้ค่าคงที่ */}
        {model ? (
          <DoseReadout clicks={clicks} mgPerClick={Number(model.mg_per_click)} penLabel={model.label}
            prescribed={prescribed == null ? undefined : prescribed} />
        ) : null}

        {split ? (
          <React.Fragment>
            <FormField label="ด้ามที่สองที่ใช้ต่อ" required>
              <Select value={secondPenId} onChange={(e) => setSecondPenId(e.target.value)} placeholder="เลือกด้ามที่สอง"
                options={otherPens.map((p) => ({
                  value: p.id,
                  label: (p.pen_models ? p.pen_models.label : "ปากกา") + " · เหลือ " + p.clicks_remaining + " คลิก",
                }))} />
            </FormField>
            <Callout tone="info" title="แบ่งฉีดสองด้าม">
              ด้ามแรก {fromCurrent} คลิก + ด้ามที่สอง {fromSecond} คลิก
            </Callout>
          </React.Fragment>
        ) : null}

        {offRx ? (
          <FormField label="เหตุผลที่จำนวนคลิกต่างจากใบสั่ง" required>
            <Select value={reason} onChange={(e) => setReason(e.target.value)} placeholder="เลือกเหตุผล"
              options={OFF_RX_REASONS} />
          </FormField>
        ) : null}

        <FormField label="ตำแหน่งที่ฉีด" required hint="สลับตำแหน่งทุกสัปดาห์">
          <div style={{ display: "flex", gap: "var(--space-3)", flexWrap: "wrap" }}>
            {SITES.map((s) => (
              <Tag key={s.value} selected={site === s.value} onClick={() => setSite(s.value)}>{s.label}</Tag>
            ))}
          </div>
        </FormField>

        <SaveProblem message={error} />
      </ScreenShell>

      <Dialog open={sheetOpen} sheet title="ยาเหลือไม่พอ"
        description={"ด้ามนี้เหลือ " + remaining + " คลิก แต่โดสสัปดาห์นี้ต้องใช้ " + (prescribed || 0) + " คลิก"}
        onClose={() => setSheetOpen(false)}
        footer={<React.Fragment>
          <Button variant="secondary" onClick={() => {
            setClicks(remaining);
            setReason("not_enough_drug");
            setSplit(false);
            setSheetOpen(false);
          }}>ฉีดเท่าที่เหลือ</Button>
          {otherPens.length ? (
            <Button onClick={() => {
              setSplit(true);
              setSecondPenId(otherPens[0].id);
              setClicks(prescribed || clicks);
              setSheetOpen(false);
            }}>แบ่งฉีดสองด้าม</Button>
          ) : null}
        </React.Fragment>}>
        <p style={{ font: "var(--type-body-sm)", color: "var(--text-muted)" }}>
          {otherPens.length
            ? "ทั้งสองทางเลือกจะถูกบันทึกให้แพทย์เห็นในการนัดครั้งถัดไป"
            : "ตอนนี้มีปากกาด้ามเดียว จึงแบ่งฉีดไม่ได้ · ฉีดเท่าที่เหลือได้ และแพทย์จะเห็นบันทึกนี้ในการนัดครั้งถัดไป"}
        </p>
      </Dialog>
    </div>
  );
}

/* ===========================================================================
   P7 — อาการสัปดาห์นี้
   ตารางกันบันทึกซ้ำต่อ (ผู้ป่วย, วัน, อาการ) แต่ผู้ป่วยแก้ "ของวันนี้" ได้
   จึงโหลดค่าที่บันทึกไว้วันนี้มาเป็นค่าตั้งต้นของสเกล 0–3
     - อาการที่ยังไม่เคยบันทึกวันนี้ → เพิ่มใหม่ (savePatientSideEffects)
     - อาการที่บันทึกไว้วันนี้แล้วและค่าเปลี่ยน → แก้ของเดิม (updatePatientSideEffects)
   ของวันก่อนหน้าแก้ไม่ได้ (นโยบายฝั่งฐานข้อมูลบังคับไว้) จึงต้องบอกผู้ป่วยให้ชัด
=========================================================================== */
function SideEffectScreen({ data, onBack, onSaved }) {
  const { dietOnly, sideEffects, readOnly } = data;
  const items = dietOnly ? SYMPTOMS : SYMPTOMS.concat([INJECTION_SYMPTOM]);

  const today = todayISO();
  const savedToday = {};
  (sideEffects || []).forEach((s) => { if (s.reported_on === today) savedToday[s.symptom] = s; });
  const hasToday = items.some((it) => savedToday[it.value]);

  // ค่าตั้งต้น = ค่าที่บันทึกไว้วันนี้ (ถ้ามี) เพื่อให้ผู้ป่วยเห็นของเดิมแล้วปรับต่อได้
  const [levels, setLevels] = React.useState(() => {
    const init = {};
    items.forEach((it) => {
      init[it.value] = savedToday[it.value] ? Number(savedToday[it.value].severity) || 0 : 0;
    });
    return init;
  });
  const [saving, setSaving] = React.useState(false);
  const [error, setError] = React.useState(null);

  const levelOf = (key) => Number(levels[key] || 0);
  const toInsert = items.filter((it) => !savedToday[it.value] && levelOf(it.value) > 0);
  const toUpdate = items.filter((it) => savedToday[it.value]
    && levelOf(it.value) !== Number(savedToday[it.value].severity));
  const nothingChanged = !toInsert.length && !toUpdate.length;

  async function save() {
    if (nothingChanged) {
      await onSaved(false, hasToday);
      return;
    }
    setSaving(true);
    setError(null);
    try {
      if (toInsert.length) {
        await API.savePatientSideEffects(data.patient.id,
          toInsert.map((it) => ({ symptom: it.value, severity: levelOf(it.value) })));
      }
      if (toUpdate.length) {
        await API.updatePatientSideEffects(data.patient.id,
          toUpdate.map((it) => ({ symptom: it.value, severity: levelOf(it.value) })));
      }
      await onSaved(true, hasToday);
    } catch (err) {
      setError((err && err.message) || "บันทึกไม่สำเร็จ กรุณาลองใหม่");
    } finally {
      setSaving(false);
    }
  }

  const buttonLabel = saving ? "กำลังบันทึก"
    : hasToday ? "อัปเดตอาการวันนี้"
    : toInsert.length ? "บันทึกอาการ"
    : "ไม่มีอาการในสัปดาห์นี้";

  return (
    <ScreenShell title="อาการสัปดาห์นี้" onBack={onBack}
      footer={<Button block size="lg" disabled={saving || readOnly} onClick={save}>
        {readOnly ? "ตอนนี้บันทึกไม่ได้" : buttonLabel}
      </Button>}>
      <p style={{ font: "var(--type-body-sm)", color: "var(--text-muted)" }}>
        แตะเลือกระดับอาการ ไม่ต้องพิมพ์ · ข้อมูลนี้ช่วยแพทย์ดูแลคุณได้ตรงขึ้น
      </p>

      {readOnly ? <ReadOnlyNote /> : null}

      {hasToday ? (
        <Callout tone="quiet" title="วันนี้คุณบันทึกไว้แล้ว">
          ค่าที่เห็นคือค่าที่บันทึกไว้วันนี้ ปรับใหม่ได้เลย · แก้ได้เฉพาะของวันนี้เท่านั้น
          ถ้าอยากแก้ของวันก่อนหน้า แจ้งคลินิกได้เลย
        </Callout>
      ) : null}

      {items.map((it) => (
        <div key={it.value} style={{ display: "flex", flexDirection: "column", gap: "var(--space-3)" }}>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "var(--space-4)" }}>
            <span style={{ font: "var(--type-label)" }}>{it.label}</span>
            {savedToday[it.value] ? <Badge tone="neutral" size="sm">บันทึกไว้วันนี้</Badge> : null}
          </div>
          <SeverityScale compact name={it.label} value={levelOf(it.value)}
            onChange={(n) => { if (!readOnly) setLevels(Object.assign({}, levels, { [it.value]: n })); }} />
        </div>
      ))}

      <SaveProblem message={error} />
    </ScreenShell>
  );
}

/* ===========================================================================
   P8 — ความก้าวหน้าของคุณ
=========================================================================== */
function ProgressScreen({ data }) {
  const { patient, dietOnly, weights, prescriptions, series } = data;
  const [range, setRange] = React.useState("12w");

  const shown = range === "6w" ? series.slice(-6) : series.slice(-12);
  const latest = weights.length ? weights[weights.length - 1] : null;
  const startWeight = patient.start_weight_kg != null
    ? Number(patient.start_weight_kg)
    : (weights.length ? Number(weights[0].weight_kg) : null);
  const totalDelta = latest && startWeight != null ? L.weightDelta(latest.weight_kg, startWeight) : null;
  const targetWeight = patient.target_weight_kg != null ? Number(patient.target_weight_kg) : null;
  const currentRx = prescriptions.find((r) => r.is_current) || null;

  return (
    <ScreenShell title="ความก้าวหน้าของคุณ">
      {series.length === 0 ? (
        <Card padding="md">
          <EmptyState icon="chart-line" title="ยังไม่มีข้อมูลให้แสดง"
            description="เมื่อคุณบันทึกน้ำหนักแล้ว กราฟและสรุปของคุณจะขึ้นที่นี่" />
        </Card>
      ) : (
        <React.Fragment>
          <SegmentedControl block value={range} onChange={setRange}
            options={[{ value: "6w", label: "6 สัปดาห์" }, { value: "12w", label: "12 สัปดาห์" }]} />
          <Card padding="sm">
            <WeightDoseChart data={shown} width={340} height={230}
              targetWeight={targetWeight == null ? undefined : targetWeight}
              showDose={!dietOnly} showSideEffects={false} />
          </Card>
          <Card title="สรุป">
            <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-5)" }}>
              <MetricStat size="sm" label="น้ำหนักเริ่มต้น"
                value={startWeight == null ? DASH : L.formatKg(startWeight)} unit={startWeight == null ? undefined : "กก."} />
              <MetricStat size="sm" label="น้ำหนักล่าสุด"
                value={latest ? L.formatKg(latest.weight_kg) : DASH} unit={latest ? "กก." : undefined}
                delta={totalDelta == null ? undefined : totalDelta}
                deltaLabel={totalDelta == null ? undefined : "จากวันเริ่มโปรแกรม"} />
              {targetWeight != null ? (
                <MetricStat size="sm" label="เป้าหมาย" value={L.formatKg(targetWeight)} unit="กก." />
              ) : null}
              {!dietOnly && currentRx ? (
                <MetricStat size="sm" label="โดสปัจจุบัน" value={L.formatMg(currentRx.prescribed_mg)} unit="mg"
                  footnote={currentRx.prescribed_clicks + " คลิก ต่อสัปดาห์"} />
              ) : null}
            </div>
          </Card>
        </React.Fragment>
      )}
      <Callout tone="quiet">กราฟนี้เป็นข้อมูลของคุณเท่านั้น ไม่มีการเปรียบเทียบกับผู้ป่วยคนอื่น</Callout>
    </ScreenShell>
  );
}

/* ===========================================================================
   P9 — กรอกน้ำหนักย้อนหลัง (สำหรับสัปดาห์ที่ลืมบันทึก หรือจดไว้ในกระดาษ)
=========================================================================== */
function BackfillScreen({ data, onBack, onSaved }) {
  const { patient, weights } = data;
  const [day, setDay] = React.useState("");
  const [weight, setWeight] = React.useState("");
  const [waist, setWaist] = React.useState("");
  const [saving, setSaving] = React.useState(false);
  const [error, setError] = React.useState(null);

  const check = L.validateWeight(weight);
  const alreadyLogged = day ? weights.some((w) => w.measured_on === day) : false;
  const canSave = !!day && check.valid && !saving && !data.readOnly;

  async function save() {
    setSaving(true);
    setError(null);
    try {
      // เวลาที่ชั่งจริงไม่รู้ จึงใช้ 08:00 ของวันนั้นเป็นตัวแทน (รอบชั่งตอนเช้า)
      // ถ้าเลือกวันนี้แล้วตอนนี้ยังไม่ถึง 08:00 ต้องใช้เวลาปัจจุบันแทน — DB ห้ามบันทึกเวลาในอนาคต
      let at = new Date(day + "T08:00:00");
      if (isNaN(at.getTime()) || at.getTime() > Date.now()) at = new Date();
      await API.savePatientWeight({
        patientId: patient.id,
        weightKg: check.value,
        waistCm: waist,
        measuredAt: at.toISOString(),
        isBackfill: true,
      });
      await onSaved();
    } catch (err) {
      setError((err && err.message) || "บันทึกไม่สำเร็จ กรุณาลองใหม่");
    } finally {
      setSaving(false);
    }
  }

  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
      <TopBar title="เพิ่มข้อมูลย้อนหลัง" onBack={onBack} />
      <div className="rmd-scroll" style={{ ...gut, flex: 1, overflowY: "auto" }}>
        <p style={{ font: "var(--type-body-sm)", color: "var(--text-muted)" }}>
          กรอกน้ำหนักของสัปดาห์ที่ผ่านมาได้ ถ้าจดไว้ในกระดาษหรือลืมบันทึกในวันนั้น
        </p>

        {data.readOnly ? <ReadOnlyNote /> : null}

        <FormField label="วันที่ชั่ง" required hint="เลือกได้ตั้งแต่วันที่เริ่มโปรแกรมจนถึงวันนี้">
          <Input type="date" value={day} onChange={(e) => setDay(e.target.value)}
            min={patient.program_start_at || undefined} max={todayISO()} />
        </FormField>

        {/* บอกตรง ๆ ว่าจะมีสองค่าในวันเดียวกัน ดีกว่าให้รู้ตอนกดบันทึกไปแล้ว */}
        {alreadyLogged ? (
          <Callout tone="info" title="วันนี้มีบันทึกอยู่แล้ว">
            ถ้าบันทึกใหม่ ระบบจะเก็บทั้งสองค่าไว้ และให้แพทย์เป็นผู้เลือกว่าจะใช้ค่าไหน
          </Callout>
        ) : null}

        <FormField label="น้ำหนักของวันนั้น" required hint="ทศนิยม 1 ตำแหน่ง เช่น 85.4" error={check.message || undefined}>
          <Input big suffix="กก." inputMode="decimal" placeholder="0.0" invalid={!!check.message}
            value={weight} onChange={(e) => setWeight(e.target.value)} />
        </FormField>

        <FormField label="รอบเอว" optional>
          <Input suffix="ซม." inputMode="decimal" placeholder="0.0" value={waist} onChange={(e) => setWaist(e.target.value)} />
        </FormField>

        <Callout tone="quiet" title="ข้อมูลย้อนหลังจะมีเครื่องหมายกำกับ">
          แพทย์จะเห็นว่าค่านี้กรอกย้อนหลัง ไม่ได้ชั่งในวันนั้น · ข้อมูลนี้ช่วยให้กราฟของคุณต่อเนื่อง
        </Callout>

        <SaveProblem message={error} />
      </div>
      <div style={{ padding: "var(--page-gutter-mobile)", paddingBottom: `calc(var(--page-gutter-mobile) + ${HOME_INDICATOR})`, borderTop: "1px solid var(--border-hairline)", background: "var(--surface-card)" }}>
        <Button block size="lg" disabled={!canSave} onClick={save}>
          {data.readOnly ? "ตอนนี้บันทึกไม่ได้" : saving ? "กำลังบันทึก" : "บันทึกย้อนหลัง"}
        </Button>
      </div>
    </div>
  );
}

/* ===========================================================================
   P10 — ตั้งเวลาแจ้งเตือน (notification_prefs)
   ตารางนี้เก็บ "ค่าที่ตั้งไว้" อย่างเดียว ระบบยังไม่ได้ส่งข้อความจริง
   จึงต้องเขียนบอกให้ชัด อย่าให้ผู้ป่วยนั่งรอการเตือนที่ไม่มีวันมา
=========================================================================== */
function NotificationCard({ patientId, dietOnly, readOnly, onToast }) {
  const state = useAsync(() => API.getNotificationPrefs(patientId), [patientId]);
  const [form, setForm] = React.useState(null);
  const [saving, setSaving] = React.useState(false);
  const [error, setError] = React.useState(null);

  // ค่าที่โหลดมาเป็นค่าตั้งต้นของฟอร์ม (ยังไม่เคยตั้ง API จะคืนค่าเริ่มต้นมาให้เอง)
  React.useEffect(() => {
    const prefs = state.data;
    if (!prefs) return;
    setForm({
      weighReminder: prefs.weigh_reminder !== false,
      weighDow: String(prefs.weigh_dow == null ? 4 : prefs.weigh_dow),
      weighTime: hhmm(prefs.weigh_time) || "07:00",
      injectionReminder: prefs.injection_reminder !== false,
      injectionTime: hhmm(prefs.injection_time) || "09:00",
    });
  }, [state.data]);

  const set = (key, value) => setForm((f) => Object.assign({}, f, { [key]: value }));

  async function save() {
    setSaving(true);
    setError(null);
    try {
      await API.saveNotificationPrefs(patientId, {
        weigh_reminder: form.weighReminder,
        weigh_dow: Number(form.weighDow),
        weigh_time: form.weighTime,
        // คนที่ปรับอาหารอย่างเดียวไม่มีเรื่องฉีดยา — ส่งค่าเดิมไปเฉย ๆ ไม่ให้เปิดเตือน
        injection_reminder: dietOnly ? false : form.injectionReminder,
        injection_time: form.injectionTime,
      });
      onToast("บันทึกเวลาแจ้งเตือนแล้ว");
    } catch (err) {
      setError((err && err.message) || "บันทึกการตั้งค่าไม่สำเร็จ กรุณาลองใหม่");
    } finally {
      setSaving(false);
    }
  }

  return (
    <Card title="ตั้งเวลาแจ้งเตือน" subtitle="เลือกวันและเวลาที่อยากให้เตือน">
      {state.loading && !form ? (
        <LoadingBlock label="กำลังโหลดการตั้งค่า" />
      ) : state.error && !form ? (
        <ProblemBlock message={state.error} onRetry={state.reload} />
      ) : !form ? null : (
        <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-5)" }}>
          <Switch checked={form.weighReminder} onChange={(v) => set("weighReminder", v)}
            label="เตือนให้ชั่งน้ำหนัก" description="สัปดาห์ละครั้งในวันเดิม" />
          {form.weighReminder ? (
            <div style={{ display: "flex", gap: "var(--space-4)" }}>
              <FormField style={{ flex: 1 }} label="วันที่ชั่ง">
                <Select value={form.weighDow} onChange={(e) => set("weighDow", e.target.value)} options={DOW_OPTIONS} />
              </FormField>
              <FormField style={{ width: 130 }} label="เวลา">
                <Input type="time" value={form.weighTime} onChange={(e) => set("weighTime", e.target.value)} />
              </FormField>
            </div>
          ) : null}

          {/* โปรแกรมปรับอาหารอย่างเดียวต้องไม่เห็นอะไรที่เกี่ยวกับยาฉีดเลย */}
          {dietOnly ? null : (
            <React.Fragment>
              <div style={{ height: 1, background: "var(--border-hairline)" }} />
              <Switch checked={form.injectionReminder} onChange={(v) => set("injectionReminder", v)}
                label="เตือนวันฉีดยา" description="เตือนตามรอบฉีดของคุณ" />
              {form.injectionReminder ? (
                <FormField style={{ width: 130 }} label="เวลา">
                  <Input type="time" value={form.injectionTime} onChange={(e) => set("injectionTime", e.target.value)} />
                </FormField>
              ) : null}
            </React.Fragment>
          )}

          {/* ถอนความยินยอมแล้วต้องบันทึกอะไรไม่ได้เลย รวมถึงค่าตั้งเตือน */}
          <Button block variant="secondary" disabled={saving || readOnly} onClick={save}>
            {readOnly ? "ตอนนี้บันทึกไม่ได้" : saving ? "กำลังบันทึก" : "บันทึกเวลาแจ้งเตือน"}
          </Button>

          <Callout tone="quiet" title="ตอนนี้ยังไม่มีการแจ้งเตือนส่งออกไปจริง">
            ระบบเก็บเวลาที่คุณตั้งไว้เท่านั้น ยังไม่ได้ส่งข้อความเตือนให้
            เมื่อเปิดใช้งานส่วนนี้จริงแล้ว คลินิกจะแจ้งให้คุณทราบอีกครั้ง
          </Callout>

          <SaveProblem message={error} />
        </div>
      )}
    </Card>
  );
}

/* ===========================================================================
   P11 — สิทธิในข้อมูลของคุณตาม PDPA
   ขอสำเนา / ขอลบ / ถอนความยินยอม — ทุกอย่างต้องอธิบายผลก่อนกด
=========================================================================== */
function DataRightsCard({ patientId, readOnly, onReload, onToast }) {
  const state = useAsync(() => API.getMyDataRequests(patientId), [patientId]);
  const [busy, setBusy] = React.useState(null);   // ปุ่มที่กำลังทำงานอยู่
  const [confirm, setConfirm] = React.useState(null); // 'erase' | 'revoke'
  const [error, setError] = React.useState(null);

  const requests = state.data || [];

  async function request(kind) {
    setBusy(kind);
    setError(null);
    try {
      await API.createDataRequest(patientId, kind);
      setConfirm(null);
      state.reload();
      onToast(kind === "export" ? "ส่งคำขอสำเนาข้อมูลแล้ว" : "ส่งคำขอลบข้อมูลแล้ว");
    } catch (err) {
      setConfirm(null);
      setError((err && err.message) || "ยื่นคำขอไม่สำเร็จ กรุณาลองใหม่");
    } finally {
      setBusy(null);
    }
  }

  async function revoke() {
    setBusy("revoke");
    setError(null);
    try {
      await API.revokeConsent(patientId, "treatment_data");
      setConfirm(null);
      await onReload();
      onToast("ถอนความยินยอมแล้ว ตอนนี้เป็นโหมดดูอย่างเดียว");
    } catch (err) {
      setConfirm(null);
      setError((err && err.message) || "ถอนความยินยอมไม่สำเร็จ กรุณาลองใหม่");
    } finally {
      setBusy(null);
    }
  }

  async function reconsent() {
    setBusy("consent");
    setError(null);
    try {
      await API.acceptConsent(patientId, "treatment_data", API.CONSENT_VERSION);
      await onReload();
      onToast("ยินยอมใหม่แล้ว บันทึกข้อมูลได้ตามปกติ");
    } catch (err) {
      setError((err && err.message) || "บันทึกคำยินยอมไม่สำเร็จ กรุณาลองใหม่");
    } finally {
      setBusy(null);
    }
  }

  return (
    <React.Fragment>
      <Card title="สิทธิในข้อมูลของคุณ" subtitle="ขอสำเนา ขอลบ หรือถอนความยินยอมได้ด้วยตัวเอง">
        <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-5)" }}>
          <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-3)" }}>
            <Button block variant="secondary" icon="download" disabled={busy === "export"}
              onClick={() => request("export")}>
              {busy === "export" ? "กำลังส่งคำขอ" : "ขอสำเนาข้อมูลของฉัน"}
            </Button>
            <Button block variant="ghost" icon="trash-2" disabled={busy === "erase"}
              onClick={() => { setError(null); setConfirm("erase"); }}>
              ขอลบข้อมูลของฉัน
            </Button>
          </div>

          <p style={{ font: "var(--type-caption)", color: "var(--text-subtle)" }}>
            คลินิกจะติดต่อกลับเมื่อดำเนินการเสร็จ · การขอลบข้อมูลต้องให้เจ้าหน้าที่ตรวจสอบก่อนเสมอ
          </p>

          {/* คำขอที่เคยยื่น — สถานะต้องเห็นได้ ไม่ใช่ยื่นแล้วเงียบหาย */}
          {state.loading && !state.data ? (
            <LoadingBlock label="กำลังโหลดคำขอของคุณ" />
          ) : state.error ? (
            <ProblemBlock message={state.error} onRetry={state.reload} />
          ) : requests.length ? (
            <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-4)" }}>
              <p style={{ font: "var(--type-label)" }}>คำขอที่เคยยื่น</p>
              {requests.map((r) => {
                const meta = REQUEST_STATUS_TH[r.status] || ["neutral", r.status];
                return (
                  <div key={r.id} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "var(--space-4)" }}>
                    <div style={{ minWidth: 0 }}>
                      <p style={{ font: "var(--type-body-sm)" }}>{REQUEST_KIND_TH[r.kind] || r.kind}</p>
                      <p style={{ font: "var(--type-caption)", color: "var(--text-subtle)" }}>
                        ยื่นเมื่อ {fmtDate(r.requested_at)}
                        {r.note ? " · " + r.note : ""}
                      </p>
                    </div>
                    <Badge tone={meta[0]} size="sm">{meta[1]}</Badge>
                  </div>
                );
              })}
            </div>
          ) : (
            <p style={{ font: "var(--type-body-sm)", color: "var(--text-muted)" }}>
              ยังไม่เคยยื่นคำขอ
            </p>
          )}

          <div style={{ height: 1, background: "var(--border-hairline)" }} />

          {/* ถอน/ให้ความยินยอมใหม่ — ต้องอธิบายผลก่อนกดเสมอ */}
          {readOnly ? (
            <React.Fragment>
              <Callout tone="info" icon="lock" title="ตอนนี้เป็นโหมดดูอย่างเดียว">
                คุณถอนความยินยอมไว้ จึงบันทึกน้ำหนักและอาการใหม่ไม่ได้
                ข้อมูลเดิมยังอยู่ครบและยังดูได้ทั้งหมด
              </Callout>
              {/* key ต่างกันสองปุ่มนี้ เพื่อให้ React สร้างปุ่มใหม่แทนการแก้สไตล์ปุ่มเดิม
                  (ปุ่มคนละ variant ใช้ node เดียวกันแล้วเส้นขอบจะเพี้ยน) */}
              <Button key="reconsent" block icon="user-check" disabled={busy === "consent"} onClick={reconsent}>
                {busy === "consent" ? "กำลังบันทึก" : "ยินยอมอีกครั้งเพื่อกลับมาบันทึกได้"}
              </Button>
            </React.Fragment>
          ) : (
            <React.Fragment>
              <Callout tone="quiet" title="ถ้าถอนความยินยอมให้เก็บข้อมูล">
                คุณจะบันทึกน้ำหนักและอาการใหม่ไม่ได้ · ข้อมูลเดิมยังอยู่ครบและยังดูได้
                · กลับมายินยอมใหม่ได้ทุกเมื่อที่หน้านี้
              </Callout>
              <Button key="revoke" block variant="ghost" icon="shield-check" disabled={busy === "revoke"}
                onClick={() => { setError(null); setConfirm("revoke"); }}>
                ถอนความยินยอมให้เก็บข้อมูล
              </Button>
            </React.Fragment>
          )}

          <SaveProblem message={error} />
        </div>
      </Card>

      <Dialog open={confirm === "erase"} title="ยืนยันการขอลบข้อมูล"
        description="คำขอนี้จะส่งให้เจ้าหน้าที่คลินิกตรวจสอบก่อนดำเนินการ"
        onClose={() => setConfirm(null)}
        footer={<React.Fragment>
          <Button variant="secondary" disabled={busy === "erase"} onClick={() => setConfirm(null)}>ยกเลิก</Button>
          <Button disabled={busy === "erase"} onClick={() => request("erase")}>
            {busy === "erase" ? "กำลังส่งคำขอ" : "ยืนยันขอลบข้อมูล"}
          </Button>
        </React.Fragment>}>
        <p style={{ font: "var(--type-body-sm)", color: "var(--text-muted)" }}>
          เมื่อคลินิกดำเนินการแล้ว ข้อมูลน้ำหนัก อาการ และประวัติการรักษาของคุณจะถูกลบ
          และย้อนกลับไม่ได้ · บางส่วนที่กฎหมายกำหนดให้เก็บไว้ คลินิกจะแจ้งให้คุณทราบ
        </p>
      </Dialog>

      <Dialog open={confirm === "revoke"} title="ยืนยันการถอนความยินยอม"
        description="ถอนแล้วแอปจะเปลี่ยนเป็นโหมดดูอย่างเดียว"
        onClose={() => setConfirm(null)}
        footer={<React.Fragment>
          <Button variant="secondary" disabled={busy === "revoke"} onClick={() => setConfirm(null)}>ยกเลิก</Button>
          <Button disabled={busy === "revoke"} onClick={revoke}>
            {busy === "revoke" ? "กำลังบันทึก" : "ยืนยันถอนความยินยอม"}
          </Button>
        </React.Fragment>}>
        <p style={{ font: "var(--type-body-sm)", color: "var(--text-muted)" }}>
          หลังถอนแล้วคุณจะบันทึกน้ำหนักและอาการใหม่ไม่ได้
          ข้อมูลเดิมทั้งหมดยังอยู่และยังเปิดดูได้ตามปกติ
          ถ้าอยากลบข้อมูลด้วย ให้กด “ขอลบข้อมูลของฉัน” แยกอีกครั้ง
          · กลับมายินยอมใหม่ได้ทุกเมื่อ
        </p>
      </Dialog>
    </React.Fragment>
  );
}

/* ===========================================================================
   P10 + P11 — โปรไฟล์ การแจ้งเตือน และการเก็บยา
=========================================================================== */
/**
 * เปลี่ยนรหัสผ่านเองในหน้าโปรไฟล์
 *
 * ผู้ป่วยไม่มีอีเมลจริงในระบบ จึงกดลิงก์ "ลืมรหัสผ่าน" ด้วยตัวเองไม่ได้
 * ถ้าไม่มีปุ่มนี้ คนที่สงสัยว่ามีคนเห็นรหัสของตัวเองจะต้องรอโทรหาคลินิกอย่างเดียว
 * ไม่ถามรหัสเดิม เพราะ Supabase ยอมให้เปลี่ยนได้เฉพาะเซสชันที่ล็อกอินอยู่แล้วเท่านั้น
 */
function ChangePasswordCard({ onToast }) {
  const [open, setOpen] = React.useState(false);
  const [pw, setPw] = React.useState("");
  const [pw2, setPw2] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);

  const mismatch = pw2.length > 0 && pw !== pw2;
  const canSave = pw.length >= 8 && pw === pw2 && !busy;

  function close() { setOpen(false); setPw(""); setPw2(""); setError(null); }

  async function save() {
    if (!canSave) return;
    setBusy(true);
    setError(null);
    try {
      await API.auth.changeOwnPassword(pw);
      close();
      if (onToast) onToast("เปลี่ยนรหัสผ่านเรียบร้อย");
    } catch (err) {
      setError((err && err.message) || "เปลี่ยนรหัสผ่านไม่สำเร็จ");
    } finally {
      setBusy(false);
    }
  }

  return (
    <Card title="รหัสผ่าน" subtitle="ใช้คู่กับรหัสผู้ป่วยเวลาเข้าสู่ระบบ" padding="md">
      {open ? (
        <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-5)" }}>
          <FormField label="รหัสผ่านใหม่" hint="อย่างน้อย 8 ตัวอักษร">
            <Input type="password" autoComplete="new-password" value={pw} onChange={(e) => setPw(e.target.value)} />
          </FormField>
          <FormField label="พิมพ์อีกครั้ง" error={mismatch ? "รหัสผ่านสองช่องไม่ตรงกัน" : null}>
            <Input type="password" autoComplete="new-password" value={pw2} onChange={(e) => setPw2(e.target.value)} />
          </FormField>
          <SaveProblem message={error} />
          <Button block disabled={!canSave} onClick={save}>{busy ? "กำลังบันทึก" : "บันทึกรหัสผ่านใหม่"}</Button>
          <Button block variant="ghost" onClick={close} disabled={busy}>ยกเลิก</Button>
        </div>
      ) : (
        <Button variant="secondary" block icon="key-round" onClick={() => setOpen(true)}>เปลี่ยนรหัสผ่าน</Button>
      )}
    </Card>
  );
}

function ProfileScreen({ data, onBackfill, onReload, onLogout, onToast }) {
  const { patient, dietOnly, consents, weights, readOnly } = data;

  const lineConsent = consents.find((c) => c.scope === "line_notify" && !c.revoked_at) || null;
  const [lineOn, setLineOn] = React.useState(!!lineConsent);
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);

  const [nickname, setNickname] = React.useState(patient.nickname || "");
  const [savingName, setSavingName] = React.useState(false);

  const latest = weights.length ? weights[weights.length - 1] : null;

  async function toggleLine(next) {
    setLineOn(next);
    setBusy(true);
    setError(null);
    try {
      if (next) await API.acceptConsent(patient.id, "line_notify", API.CONSENT_VERSION);
      else await API.revokeConsent(patient.id, "line_notify");
      await onReload();
      onToast(next ? "เปิดการแจ้งเตือนผ่าน LINE แล้ว" : "ปิดการแจ้งเตือนผ่าน LINE แล้ว");
    } catch (err) {
      setLineOn(!next); // กลับสถานะเดิมเมื่อบันทึกไม่สำเร็จ ไม่ให้สวิตช์โกหกผู้ใช้
      setError((err && err.message) || "บันทึกการตั้งค่าไม่สำเร็จ กรุณาลองใหม่");
    } finally {
      setBusy(false);
    }
  }

  async function saveName() {
    setSavingName(true);
    setError(null);
    try {
      await API.updateMyNickname(patient.id, nickname);
      await onReload();
      onToast("บันทึกชื่อเรียกแล้ว");
    } catch (err) {
      setError((err && err.message) || "บันทึกชื่อไม่สำเร็จ กรุณาลองใหม่");
    } finally {
      setSavingName(false);
    }
  }

  return (
    <ScreenShell title="โปรไฟล์และการตั้งค่า"
      action={<IconButton icon="log-out" label="ออกจากระบบ" onClick={onLogout} />}>
      <Card>
        <div style={{ display: "flex", alignItems: "center", gap: "var(--space-5)" }}>
          <span style={{ width: 48, height: 48, borderRadius: "50%", background: "var(--surface-brand-soft)", border: "1px solid var(--border-brand)", display: "inline-flex", alignItems: "center", justifyContent: "center", color: "var(--navy-600)", flex: "0 0 auto" }}>
            <Icon name="user" size={22} />
          </span>
          <div style={{ minWidth: 0 }}>
            <p style={{ font: "var(--type-card-title)" }}>{patient.nickname || "ยังไม่ได้ตั้งชื่อเรียก"}</p>
            <p style={{ font: "var(--type-body-sm)", color: "var(--text-muted)" }}>
              เริ่มโปรแกรม {fmtDate(patient.program_start_at)}
            </p>
          </div>
        </div>
        <div style={{ marginTop: "var(--space-5)" }}>
          <Badge tone={dietOnly ? "accent" : "brand"} icon={dietOnly ? "utensils" : "syringe"}>
            โปรแกรม{L.programLabel(patient.program_type)}
          </Badge>
        </div>
        <p style={{ font: "var(--type-caption)", color: "var(--text-subtle)", marginTop: "var(--space-4)" }}>
          บันทึกน้ำหนักล่าสุด {fmtSince(latest ? latest.measured_at : null)}
        </p>
      </Card>

      {readOnly ? <ReadOnlyNote /> : null}

      <Card title="ชื่อที่อยากให้ระบบเรียก">
        <div style={{ display: "flex", gap: "var(--space-4)", alignItems: "flex-end" }}>
          <FormField style={{ flex: 1 }} label="ชื่อเรียก">
            <Input value={nickname} onChange={(e) => setNickname(e.target.value)} placeholder="เช่น คุณสุดา" disabled={readOnly} />
          </FormField>
          <Button variant="secondary" disabled={readOnly || savingName || !nickname.trim() || nickname.trim() === (patient.nickname || "")}
            onClick={saveName}>{savingName ? "กำลังบันทึก" : "บันทึก"}</Button>
        </div>
      </Card>

      <Card title="ช่องทางแจ้งเตือน">
        <Switch checked={lineOn} onChange={toggleLine} disabled={busy}
          label="แจ้งเตือนผ่าน LINE"
          description={dietOnly ? "เตือนวันชั่งน้ำหนักประจำสัปดาห์" : "เตือนวันชั่งน้ำหนักและวันฉีดยา"} />
      </Card>

      <NotificationCard patientId={patient.id} dietOnly={dietOnly} readOnly={readOnly} onToast={onToast} />

      {dietOnly ? (
        <Callout tone="guide" icon="utensils" title="โปรแกรมปรับอาหารของคุณ">
          ชั่งน้ำหนักสัปดาห์ละครั้งในวันเดิม · บันทึกอาการถ้ามี · นำผลไปคุยกับแพทย์ในนัดถัดไป
        </Callout>
      ) : (
        <Callout tone="info" icon="refrigerator" title="การเก็บรักษายา">
          เก็บในตู้เย็น 2–8°C ห้ามแช่แข็ง · เมื่อเดินทางให้ใช้กระเป๋าเก็บความเย็นและไม่วางในช่องแช่แข็ง · ด้ามที่เริ่มใช้แล้วเก็บได้ 30 วัน
        </Callout>
      )}

      <Card title="บันทึกย้อนหลัง" subtitle="สำหรับข้อมูลที่จดไว้ในกระดาษ" padding="md">
        <Button variant="secondary" block icon="calendar-plus" disabled={readOnly} onClick={onBackfill}>เพิ่มข้อมูลย้อนหลัง</Button>
      </Card>

      <ChangePasswordCard onToast={onToast} />

      <DataRightsCard patientId={patient.id} readOnly={readOnly} onReload={onReload} onToast={onToast} />

      <SaveProblem message={error} />

      <Button variant="ghost" block icon="log-out" onClick={onLogout}>ออกจากระบบ</Button>
    </ScreenShell>
  );
}

/* ===========================================================================
   โครงแอปฝั่งผู้ป่วย — เซสชัน คำยินยอม การโหลดข้อมูล และการนำทาง
=========================================================================== */
// ?screen=inject / ?tab=chart — เปิดตรงเข้าหน้าใดหน้าหนึ่งได้ ใช้ทำหน้ารวม preview
// รับเฉพาะค่าที่รู้จัก และยังต้องล็อกอิน + ยินยอมก่อนเสมอ
const QUERY = new URLSearchParams(location.search);
const START_SCREEN = ["app", "inject", "se", "backfill"].indexOf(QUERY.get("screen")) >= 0 ? QUERY.get("screen") : "app";
const START_TAB = ["home", "log", "chart", "me"].indexOf(QUERY.get("tab")) >= 0 ? QUERY.get("tab") : "home";

function PatientApp() {
  const [profile, setProfile] = React.useState(null);
  const [authReady, setAuthReady] = React.useState(false);
  const [authError, setAuthError] = React.useState(null);
  const [screen, setScreen] = React.useState(START_SCREEN);
  const [tab, setTab] = React.useState(START_TAB);
  const [toast, setToast] = React.useState(null);
  // เปลี่ยนรหัสสำเร็จแล้วในรอบการใช้งานนี้ (ดูเหตุผลที่ต้องมีตัวนี้ตรงหน้าจอ FirstPasswordScreen)
  const [pwChanged, setPwChanged] = React.useState(false);

  React.useEffect(() => { if (!toast) return; const t = setTimeout(() => setToast(null), 3200); return () => clearTimeout(t); }, [toast]);

  const mounted = React.useRef(true);
  React.useEffect(() => () => { mounted.current = false; }, []);

  const applySession = React.useCallback(async (session) => {
    if (!mounted.current) return;
    if (!session) {
      setProfile(null);
      setAuthError(null);
      setAuthReady(true);
      return;
    }
    try {
      const p = await API.auth.getProfile();
      if (!mounted.current) return;
      setProfile(p);
      setAuthError(null);
    } catch (err) {
      if (!mounted.current) return;
      setAuthError((err && err.message) || "โหลดข้อมูลของคุณไม่สำเร็จ");
      setProfile(null);
    } finally {
      if (mounted.current) setAuthReady(true);
    }
  }, []);

  React.useEffect(() => {
    let stop = null;
    API.auth.getSession()
      .then(applySession)
      .then(() => { stop = API.auth.onChange((event, session) => { applySession(session); }); })
      .catch((err) => {
        if (!mounted.current) return;
        setAuthError((err && err.message) || "เชื่อมต่อระบบไม่สำเร็จ");
        setAuthReady(true);
      });
    return () => { if (stop) stop(); };
  }, [applySession]);

  const patientId = profile && profile.kind === "patient" ? profile.id : null;

  // โหลดข้อมูลของตัวเองทั้งชุดที่เดียว แล้วส่งต่อให้ทุกหน้าจอ
  // ผู้ป่วยโปรแกรมปรับอาหารอย่างเดียวไม่ต้องยิงถามเรื่องยาเลย (ฐานข้อมูลไม่มีให้อยู่แล้ว)
  const state = useAsync(async () => {
    if (!patientId) return null;
    const patient = await API.getPatient(patientId);
    const dietOnly = L.isDietOnly(patient.program_type);
    const [consents, weights, sideEffects] = await Promise.all([
      API.getConsents(patientId),
      API.getWeightLogs(patientId),
      API.getSideEffects(patientId),
    ]);
    let prescriptions = [], pens = [], injections = [];
    if (!dietOnly) {
      const meds = await Promise.all([
        API.getPrescriptions(patientId),
        API.getPens(patientId),
        API.getInjections(patientId),
      ]);
      prescriptions = meds[0]; pens = meds[1]; injections = meds[2];
    }
    // แยกให้ชัดระหว่าง "ยังไม่เคยยินยอม" (ต้องผ่านหน้ายินยอมก่อน)
    // กับ "เคยยินยอมแล้วถอนทีหลัง" (เข้าแอปได้แต่เป็นโหมดดูอย่างเดียว)
    const treatmentConsents = consents.filter((c) => c.scope === "treatment_data");
    const hasTreatmentConsent = treatmentConsents.some((c) => !c.revoked_at);
    return {
      patient, dietOnly, consents, weights, sideEffects, prescriptions, pens, injections,
      series: buildWeekSeries({ programStartAt: patient.program_start_at, weights, injections, sideEffects }),
      hasTreatmentConsent,
      everConsented: treatmentConsents.length > 0,
      readOnly: !hasTreatmentConsent,
    };
  }, [patientId]);

  async function handleLogout() {
    try { await API.auth.logout(); } catch (err) { setToast((err && err.message) || "ออกจากระบบไม่สำเร็จ"); }
    setProfile(null);
    setScreen("app");
    setTab("home");
    setPwChanged(false); // คนถัดไปที่ล็อกอินบนเครื่องนี้ต้องเจอหน้าตั้งรหัสของตัวเอง
  }

  const frame = (children, showNav) => (
    <div style={{ position: "relative", display: "flex", flexDirection: "column", height: "100%", background: "var(--surface-page)", fontFamily: "var(--font-ui)" }}>
      <div style={{ flex: 1, minHeight: 0 }}>{children}</div>
      {showNav ? (
        <BottomNav value={tab} onChange={(v) => { setTab(v); setScreen("app"); }}
          style={{ paddingBottom: HOME_INDICATOR }} items={[
            { value: "home", label: "หน้าหลัก", icon: "house" },
            { value: "log", label: "บันทึก", icon: "square-pen" },
            // ชื่อไอคอนของ lucide รุ่นนี้คือ chart-line (line-chart เป็นชื่อเก่าที่ถูกถอดออกแล้ว โหลดไม่ขึ้น)
            { value: "chart", label: "กราฟ", icon: "chart-line" },
            { value: "me", label: "โปรไฟล์", icon: "user" }]} />
      ) : null}
      {toast ? (
        <div style={{ position: "absolute", left: 16, right: 16, bottom: showNav ? 96 : 24, zIndex: 60 }}>
          <Toast tone="ok" title={toast} onClose={() => setToast(null)} />
        </div>
      ) : null}
    </div>
  );

  if (!authReady) {
    return frame(<div style={{ ...gut, height: "100%", justifyContent: "center" }}><LoadingBlock label="กำลังเข้าสู่ระบบ" /></div>, false);
  }

  if (!profile) {
    return frame(
      <div style={{ height: "100%", display: "flex", flexDirection: "column" }}>
        {authError ? <div style={{ padding: "var(--page-gutter-mobile)", paddingTop: STATUS_BAR }}>
          <Callout tone="info" icon="info" title="เข้าใช้งานไม่ได้">{authError}</Callout>
        </div> : null}
        <div style={{ flex: 1, minHeight: 0 }}>
          {/* ไม่ต้องส่ง callback — ล็อกอินสำเร็จแล้ว onChange (บรรทัดบน) จะยิง applySession ต่อเอง */}
          <LoginScreen />
        </div>
      </div>, false);
  }

  // บุคลากรที่เผลอเปิดลิงก์ของผู้ป่วยต้องไม่เห็นหน้าจอนี้
  if (profile.kind !== "patient") {
    return frame(
      <div style={{ ...gut, height: "100%", justifyContent: "center" }}>
        <EmptyState icon="shield-alert" title="หน้านี้สำหรับผู้ป่วย"
          description="บัญชีของคุณเป็นบัญชีของบุคลากร กรุณาใช้หน้าจอฝั่งคลินิกแทน"
          action={<Button variant="secondary" icon="log-out" onClick={handleLogout}>ออกจากระบบ</Button>} />
      </div>, false);
  }

  if (state.loading && !state.data) {
    return frame(<div style={{ ...gut, height: "100%", justifyContent: "center" }}><LoadingBlock label="กำลังโหลดข้อมูลของคุณ" /></div>, false);
  }
  if (state.error || !state.data) {
    return frame(
      <div style={{ ...gut, height: "100%", justifyContent: "center" }}>
        <ProblemBlock message={state.error} onRetry={state.reload} />
        <Button variant="ghost" block icon="log-out" onClick={handleLogout}>ออกจากระบบ</Button>
      </div>, false);
  }

  const data = state.data;

  // รหัสตั้งต้นมาจากคลินิกและผ่านมือคนอื่นมาแล้ว ต้องเปลี่ยนก่อนใช้งานจริง
  // ขวางก่อนหน้ายินยอม เพราะการกดยินยอมเป็นหลักฐานทางกฎหมาย ควรทำหลังบัญชีเป็นของผู้ป่วยจริง ๆ แล้ว
  //
  // เชื่อ pwChanged ของรอบนี้ด้วย ไม่ใช่แค่ค่าจากฐานข้อมูล — ถ้าเปลี่ยนรหัสสำเร็จแล้ว
  // แต่ปลดธงใน DB ไม่ผ่าน ผู้ป่วยจะติดวนอยู่หน้านี้ตลอดไปโดยไม่มีทางออก
  // ผลเสียของการเชื่อฝั่งเครื่องคือแค่ "หน้านี้เด้งอีกครั้งตอนเข้าใหม่" ซึ่งเบากว่ากันมาก
  if (data.patient.must_change_password && !pwChanged) {
    return frame(
      <FirstPasswordScreen onLogout={handleLogout}
        onDone={async () => { setPwChanged(true); setToast("ตั้งรหัสผ่านใหม่เรียบร้อย"); state.reload(); }} />, false);
  }

  // PDPA: ยังไม่เคยยินยอมเลย = ฐานข้อมูลจะไม่ให้บันทึกอะไรทั้งนั้น จึงต้องผ่านหน้านี้ก่อน
  // ส่วนคนที่เคยยินยอมแล้วถอนเอง ต้องเข้าแอปได้เพื่อดูข้อมูลเดิมของตัวเอง (โหมดดูอย่างเดียว)
  if (!data.hasTreatmentConsent && !data.everConsented) {
    return frame(
      <ConsentScreen patientId={data.patient.id} dietOnly={data.dietOnly} onLogout={handleLogout}
        onDone={async () => { setToast("บันทึกคำยินยอมแล้ว"); state.reload(); }} />, false);
  }

  const reload = async () => { state.reload(); };

  let body;
  if (screen === "inject" && !data.dietOnly) {
    body = <InjectionScreen data={data} onBack={() => setScreen("app")}
      onSaved={async () => { setToast("บันทึกการฉีดยาเรียบร้อย"); setScreen("app"); state.reload(); }} />;
  } else if (screen === "se") {
    body = <SideEffectScreen data={data} onBack={() => setScreen("app")}
      onSaved={async (saved, hadToday) => {
        setToast(saved ? (hadToday ? "อัปเดตอาการของวันนี้แล้ว" : "บันทึกอาการเรียบร้อย")
          : hadToday ? "อาการวันนี้ยังเหมือนเดิม ไม่มีอะไรต้องอัปเดต"
          : "รับทราบว่าไม่มีอาการในสัปดาห์นี้");
        setScreen("app");
        if (saved) state.reload();
      }} />;
  } else if (screen === "backfill") {
    body = <BackfillScreen data={data} onBack={() => setScreen("app")}
      onSaved={async () => { setToast("บันทึกย้อนหลังเรียบร้อย"); setScreen("app"); state.reload(); }} />;
  } else if (tab === "chart") {
    body = <ProgressScreen data={data} />;
  } else if (tab === "me") {
    body = <ProfileScreen data={data} onReload={reload} onLogout={handleLogout} onToast={setToast}
      onBackfill={() => setScreen("backfill")} />;
  } else if (tab === "log") {
    // บันทึกน้ำหนักเสร็จแล้วพาไปหน้าอาการต่อ — เป็นรอบเดียวกันของสัปดาห์ ไม่ต้องให้ผู้ป่วยหาเอง
    body = <LogWeightScreen data={data} onBack={() => setTab("home")}
      onSaved={async () => { setToast("บันทึกน้ำหนักเรียบร้อย"); setTab("home"); setScreen("se"); state.reload(); }} />;
  } else {
    body = <HomeScreen data={data}
      onLog={() => setTab("log")}
      onInject={() => setScreen("inject")}
      onSideEffect={() => setScreen("se")}
      onChart={() => setTab("chart")}
      onProfile={() => setTab("me")} />;
  }

  return frame(body, screen === "app");
}

Object.assign(window, {
  PatientApp, LoginScreen, ConsentScreen, HomeScreen, LogWeightScreen,
  InjectionScreen, SideEffectScreen, ProgressScreen, ProfileScreen, BackfillScreen,
});
