/* ============================================================
   ПЕРСОНАЛИКА — мастер онлайн-записи
   Шаги: услуга → врач → дата/время → контакты → подтверждение
   ============================================================ */
const { useState, useEffect, useMemo, useRef } = React;

/* ---------- Иконки ---------- */
const Ic = ({ d, s = 22 }) => (
  <svg width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
    {d.map((p, i) => <path key={i} d={p} />)}
  </svg>
);
const IC = {
  inject: ["M18 2l4 4", "M17 3l-9 9-1 4 4-1 9-9", "M14 6l4 4"],
  device: ["M3 4h18v12H3z", "M8 20h8M12 16v4"],
  laser:  ["M12 2v4", "M12 18v4", "M2 12h4", "M18 12h4", "M5 5l2.5 2.5", "M16.5 16.5L19 19", "M19 5l-2.5 2.5", "M7.5 16.5L5 19", "M12 9a3 3 0 100 6 3 3 0 000-6z"],
  derm:   ["M9 11l3 3L22 4", "M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"],
  care:   ["M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 10-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 000-7.78z"],
  body:   ["M12 2a3 3 0 100 6 3 3 0 000-6z", "M7 22v-7l-2-2 1-5h8l1 5-2 2v7"],
  check:  ["M20 6L9 17l-5-5"],
  cal:    ["M3 4h18v18H3z", "M16 2v4M8 2v4M3 10h18"],
  clock:  ["M12 2a10 10 0 100 20 10 10 0 000-20z", "M12 7v5l3 2"],
  user:   ["M20 21a8 8 0 10-16 0", "M12 11a4 4 0 100-8 4 4 0 000 8z"],
  pin:    ["M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z", "M12 13a3 3 0 100-6 3 3 0 000 6z"],
  arrowL: ["M19 12H5", "M12 19l-7-7 7-7"],
  arrowR: ["M5 12h14", "M12 5l7 7-7 7"],
  shield: ["M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"],
  spark:  ["M12 3l1.9 5.8L20 10l-5.8 2L12 18l-2.2-6L4 10l6.1-1.2z"],
  people: ["M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2", "M9 11a4 4 0 100-8 4 4 0 000 8z", "M23 21v-2a4 4 0 00-3-3.87", "M16 3.13a4 4 0 010 7.75"],
  list:   ["M8 6h13", "M8 12h13", "M8 18h13", "M3 6h.01", "M3 12h.01", "M3 18h.01"],
  chev:   ["M9 18l6-6-6-6"],
};

/* ---------- Данные ---------- */
const SERVICES = [
  { id: "checkup", cat: "Диагностика", name: "Цифровой чек-ап", dur: 60, price: 4000, ic: "check" },
  { id: "inj",  cat: "Инъекционная косметология", name: "Биоревитализация", dur: 60, price: 7900,  ic: "inject" },
  { id: "lips", cat: "Инъекционная косметология", name: "Контурная пластика губ", dur: 60, price: 18500, ic: "inject" },
  { id: "rf",   cat: "Аппаратная косметология", name: "RF-лифтинг лица", dur: 75, price: 9900,  ic: "device" },
  { id: "smas", cat: "Аппаратная косметология", name: "SMAS-лифтинг", dur: 90, price: 24000, ic: "device" },
  { id: "epi",  cat: "Лазерные технологии", name: "Лазерная эпиляция", dur: 45, price: 1500,  ic: "laser" },
  { id: "frac", cat: "Лазерные технологии", name: "Фракционное омоложение", dur: 60, price: 12000, ic: "laser" },
  { id: "diag", cat: "Дерматология", name: "Консультация дерматолога", dur: 40, price: 2500,  ic: "derm" },
  { id: "peel", cat: "Уход и пилинги", name: "Химический пилинг", dur: 50, price: 5500,  ic: "care" },
  { id: "clean",cat: "Уход и пилинги", name: "Ультразвуковая чистка", dur: 60, price: 4900,  ic: "care" },
  { id: "shape",cat: "Коррекция фигуры", name: "Лимфодренаж аппаратный", dur: 60, price: 4200,  ic: "body" },
];

const DOCTORS = [
  { id: "sokolova", name: "Анна Соколова",  role: "Главный врач, дерматолог", exp: "16 лет · к.м.н.", init: "АС", does: ["checkup","inj","lips","diag","peel","frac"] },
  { id: "lebedeva", name: "Марина Лебедева", role: "Косметолог-инъекционист", exp: "11 лет", init: "МЛ", does: ["checkup","inj","lips","peel","clean"] },
  { id: "orlov",    name: "Игорь Орлов",     role: "Лазерный хирург", exp: "9 лет", init: "ИО", does: ["checkup","epi","frac","smas","rf"] },
  { id: "vlasova",  name: "Дарья Власова",   role: "Косметолог-эстетист", exp: "7 лет", init: "ДВ", does: ["checkup","rf","clean","peel","shape","care"] },
];

const STEPS = [
  { k: "Услуга",        ic: "spark" },
  { k: "Врач",          ic: "user" },
  { k: "Дата и время",  ic: "cal" },
  { k: "Контакты",      ic: "check" },
];

const fmtPrice = n => n.toLocaleString("ru-RU") + " ₽";
const MONTHS = ["янв","фев","мар","апр","мая","июн","июл","авг","сен","окт","ноя","дек"];
const DOW = ["вс","пн","вт","ср","чт","пт","сб"];

/* ---------- Связь с бэкендом ----------
   Если страница открыта с самого бэкенда (порт 3000) — бьём по
   относительным путям. Если открыта со статик-сервера (:8000) или
   из файла — стучимся напрямую в бэкенд на 127.0.0.1:3000. */
const API = (location.port === "3000" || location.port === "") ? "" : "http://127.0.0.1:3000";
async function apiPost(path, body, token) {
  const headers = { "Content-Type": "application/json" };
  if (token) headers.Authorization = "Bearer " + token;
  const r = await fetch(API + path, { method: "POST", headers, body: JSON.stringify(body) });
  const j = await r.json().catch(() => ({ ok: false, error: "Сервер недоступен" }));
  if (!r.ok || !j.ok) { const e = new Error(j.error || ("Ошибка " + r.status)); e.code = j.code; e.status = r.status; throw e; }
  return j;
}
async function apiGet(path, token) {
  const headers = {};
  if (token) headers.Authorization = "Bearer " + token;
  const r = await fetch(API + path, { headers });
  const j = await r.json().catch(() => ({ ok: false, error: "Сервер недоступен" }));
  if (!r.ok || !j.ok) { const e = new Error(j.error || ("Ошибка " + r.status)); e.code = j.code; e.status = r.status; throw e; }
  return j;
}

/* «сегодня» по времени клиники — Благовещенск, UTC+9 (без летнего времени) */
function clinicToday() {
  try {
    const s = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Yakutsk" }).format(new Date());
    const p = s.split("-").map(Number);
    return new Date(p[0], p[1] - 1, p[2]);
  } catch (e) { const x = new Date(); x.setHours(0, 0, 0, 0); return x; }
}
/* генерируем 14 дней начиная с завтра */
function buildDays() {
  const arr = []; const base = clinicToday();
  for (let i = 1; i <= 14; i++) {
    const d = new Date(base); d.setDate(base.getDate() + i);
    const key = d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
    arr.push({ date: d, dow: DOW[d.getDay()], dn: d.getDate(), mo: MONTHS[d.getMonth()], off: d.getDay() === 0, key });
  }
  return arr;
}
const SLOTS = {
  "Утро":   ["09:00", "10:00", "11:00"],
  "День":   ["12:00", "13:30", "15:00"],
  "Вечер":  ["17:00", "18:30", "20:00"],
};
/* плоский список всех слотов времени (для «ближайшего свободного») */
const ALL_SLOTS = Object.values(SLOTS).flat();
const CLOSE_MIN = 21 * 60; /* клиника закрывается в 21:00 */
function toMin(t) { const [h, m] = t.split(":").map(Number); return h * 60 + m; }
/* Слот доступен под услугу длительностью dur, если процедура помещается до закрытия
   и её интервал [старт, старт+dur) не пересекается с уже занятыми слотами врача. */
function slotFitsService(startT, dur, taken) {
  const start = toMin(startT), end = start + (dur || 60);
  if (end > CLOSE_MIN) return false;
  return !taken.some(tk => { const ts = toMin(tk); return ts >= start && ts < end; });
}
function freeSlotsFor(taken, dur) {
  return ALL_SLOTS.filter(t => slotFitsService(t, dur, taken || []));
}
const WEEKDAYS = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"];
const MONTHS_FULL = ["Январь","Февраль","Март","Апрель","Май","Июнь","Июль","Август","Сентябрь","Октябрь","Ноябрь","Декабрь"];

/* локальная дата → "YYYY-MM-DD" (без сдвига часового пояса) */
function ymd(d) {
  const m = String(d.getMonth() + 1).padStart(2, "0");
  const day = String(d.getDate()).padStart(2, "0");
  return `${d.getFullYear()}-${m}-${day}`;
}
function startOfDay(d) { const x = new Date(d); x.setHours(0,0,0,0); return x; }
function isOff(d) { return d.getDay() === 0; } /* воскресенье — выходной */

/* ячейки месяца для календаря (с ведущими пустыми до первого дня недели, Пн=0) */
function buildMonth(year, month) {
  const first = new Date(year, month, 1);
  const lead = (first.getDay() + 6) % 7;
  const total = new Date(year, month + 1, 0).getDate();
  const cells = [];
  for (let i = 0; i < lead; i++) cells.push(null);
  for (let dn = 1; dn <= total; dn++) {
    const dt = new Date(year, month, dn);
    cells.push({ dn, date: dt, key: ymd(dt) });
  }
  return cells;
}

/* ============================================================ */
function getParam(name) {
  try { return new URLSearchParams(window.location.search).get(name); } catch (e) { return null; }
}
const PRESELECT = (() => {
  const id = getParam("service");
  return SERVICES.some(s => s.id === id) ? id : null;
})();
// Атрибуция маркетинга: UTM первого визита + реферер. Сохраняем в sessionStorage,
// чтобы не потерять при переходах внутри сайта до отправки записи.
const UTM = (() => {
  try {
    const saved = JSON.parse(sessionStorage.getItem("pl_utm") || "null");
    if (saved) return saved;
    const q = new URLSearchParams(window.location.search);
    const u = { source: q.get("utm_source") || "", medium: q.get("utm_medium") || "", campaign: q.get("utm_campaign") || "", content: q.get("utm_content") || "", term: q.get("utm_term") || "" };
    const ref = document.referrer && !document.referrer.includes(location.host) ? document.referrer : "";
    const data = { utm: u, referrer: ref };
    if (u.source || u.medium || u.campaign || ref) sessionStorage.setItem("pl_utm", JSON.stringify(data));
    return data;
  } catch (e) { return { utm: {}, referrer: "" }; }
})();

/* ---------- Хаб: точка входа в запись (как в ТЗ) ---------- */
function HubRow({ icon, label, onClick }) {
  return (
    <button className="hub-row" onClick={onClick}>
      <span className="hub-ic"><Ic d={IC[icon]} s={22} /></span>
      <span className="hub-label">{label}</span>
      <span className="hub-chev"><Ic d={IC.chev} s={20} /></span>
    </button>
  );
}
function Hub({ onPick }) {
  return (
    <div>
      <header className="bk-nav">
        <div className="wrap bk-nav-inner">
          <a href="index.html" className="logo">Персоналик<span className="dot">а</span></a>
          <a href="index.html" className="back-link"><Ic d={IC.arrowL} s={18} /> На сайт</a>
        </div>
      </header>
      <div className="hub-wrap">
        <div className="hub-card">
          <div className="hub-logo"><Ic d={IC.spark} s={34} /></div>
          <h1 className="hub-name">Клиника «Персоналика»</h1>
          <div className="hub-addr">ул. Уралова, 1 · Благовещенск</div>
          <div className="hub-list">
            <HubRow icon="people" label="Выбрать специалиста" onClick={() => onPick(1)} />
            <HubRow icon="list" label="Выбрать услуги" onClick={() => onPick(0)} />
          </div>
        </div>
      </div>
    </div>
  );
}

function App() {
  const [step, setStep] = useState(PRESELECT ? 1 : 0);
  const [serviceId, setServiceId] = useState(PRESELECT);
  const [doctorId, setDoctorId] = useState(null);   // null = не выбран, "any" = любой
  const [dayKey, setDayKey] = useState(null);
  const [time, setTime] = useState(null);
  const [form, setForm] = useState({ name: "", phone: "", email: "", comment: "", consentPdn: false, consentMed: false, consentPhoto: false, consentAds: false });
  const [touched, setTouched] = useState(false);
  const [done, setDone] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  const [submitError, setSubmitError] = useState("");
  const [cancelToken, setCancelToken] = useState(null);
  const [started, setStarted] = useState(!!PRESELECT);
  const [mode, setMode] = useState("service"); // 'service' (по услуге) | 'doctor' (по врачу) — задаёт порядок шагов

  // --- Авторизация через мессенджер (для напоминаний без СМС) ---
  const [requireAuth, setRequireAuth] = useState(true);  // по умолчанию требуем (точно узнаём из конфига)
  const [onlinePayments, setOnlinePayments] = useState(false);
  const [bookingId, setBookingId] = useState(null);
  const [auth, setAuth] = useState(() => { try { return JSON.parse(sessionStorage.getItem("pl_auth") || "null"); } catch (e) { return null; } });
  const [verify, setVerify] = useState({ status: "idle", channel: "", msg: "" }); // idle|loading|waiting|error
  const pollRef = useRef(null);
  const stopPoll = () => { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } };

  useEffect(() => {
    apiGet("/api/booking-config").then(c => { setRequireAuth(!!c.requireAuth); setOnlinePayments(!!c.onlinePayments); }).catch(() => {});
    return () => stopPoll();
  }, []);

  const saveAuth = (a) => { try { sessionStorage.setItem("pl_auth", JSON.stringify(a)); } catch (e) {} setAuth(a); };
  const logoutAuth = () => { try { sessionStorage.removeItem("pl_auth"); } catch (e) {} setAuth(null); setVerify({ status: "idle", channel: "", msg: "" }); };

  async function startVerify(channel) {
    stopPoll();
    setVerify({ status: "loading", channel, msg: "" });
    try {
      const ep = channel === "max" ? "/api/auth/request-login-max" : "/api/auth/request-login";
      const r = await apiPost(ep, {});
      if (r.deepLink) window.open(r.deepLink, "_blank", "noopener");
      setVerify({ status: "waiting", channel, msg: `Откройте ${channel === "max" ? "MAX" : "Telegram"} и нажмите «Старт». Ждём подтверждения…` });
      let tries = 0;
      pollRef.current = setInterval(async () => {
        tries++;
        if (tries > 80) { stopPoll(); setVerify({ status: "error", channel, msg: "Время ожидания вышло. Попробуйте ещё раз." }); return; }
        try {
          const c = await apiGet("/api/auth/check?t=" + r.loginToken);
          if (c.status === "confirmed") {
            stopPoll();
            const token = c.authToken;
            let prof = {};
            try { prof = (await apiGet("/api/me", token)).user || {}; } catch (e) {}
            saveAuth({ token, name: prof.full_name || "", phone: prof.phone || "", channel });
            setVerify({ status: "idle", channel: "", msg: "" });
            setForm(f => ({ ...f, name: f.name || prof.full_name || "", phone: f.phone || prof.phone || "" }));
          } else if (c.status === "expired") { stopPoll(); setVerify({ status: "error", channel, msg: "Ссылка устарела. Попробуйте ещё раз." }); }
        } catch (e) { /* продолжаем опрашивать */ }
      }, 2500);
    } catch (e) {
      setVerify({ status: "error", channel, msg: e.message || "Не удалось начать вход" });
    }
  }

  const days = useMemo(buildDays, []);
  const service = SERVICES.find(s => s.id === serviceId) || null;
  const doctor  = doctorId === "any" ? { name: "Любой свободный специалист", init: "—", role: "подберём по расписанию" } : DOCTORS.find(d => d.id === doctorId) || null;
  // Дата выводится прямо из выбранного ключа — поддерживает любой день в пределах 3 месяцев.
  const day = dayKey ? (() => { const d = new Date(dayKey + "T00:00:00"); return { key: dayKey, date: d, dn: d.getDate(), mo: MONTHS[d.getMonth()], dow: DOW[d.getDay()] }; })() : null;

  // Быстрый выбор слота у врача: дата/время заданы; дальше — услуга (если ещё не выбрана) или контакты
  const quickPick = (docId, key, t) => { setDoctorId(docId); setDayKey(key); setTime(t); setStarted(true); setStep(serviceId ? 3 : 0); };

  useEffect(() => { window.scrollTo({ top: 0, behavior: "smooth" }); }, [step, done]);

  // доступные врачи под услугу
  const avail = serviceId ? DOCTORS.filter(d => d.does.includes(serviceId)) : DOCTORS;

  const canNext = [!!serviceId, !!doctorId, !!(dayKey && time), false][step];
  const allReady = !!serviceId && !!doctorId && !!(dayKey && time);
  // Порядок шагов зависит от точки входа:
  //  • по услуге  → Услуга → Врач → Дата → Контакты
  //  • по врачу   → Врач → Услуга → Дата → Контакты
  const order = mode === "doctor" ? [1, 0, 2, 3] : [0, 1, 2, 3];
  const stepReady = (i) => i === 0 ? !!serviceId : i === 1 ? !!doctorId : i === 2 ? !!(dayKey && time) : true;
  // Следующий незаполненный шаг — в порядке текущего режима (контакты в конце).
  const firstUnfilled = () => { for (const i of order) { if (i !== 3 && !stepReady(i)) return i; } return 3; };
  const goNext = () => setStep(firstUnfilled());
  const jump = (i) => { if (i === 3 && !allReady) return; setStep(i); };

  const errors = {
    name: form.name.trim().length < 2 ? "Укажите имя" : "",
    phone: form.phone.replace(/\D/g, "").length < 10 ? "Введите корректный номер" : "",
    email: form.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email) ? "Проверьте e-mail" : "",
    consentPdn: !form.consentPdn ? "Нужно согласие на обработку персональных данных" : "",
    consentMed: !form.consentMed ? "Нужно информированное согласие на медицинское вмешательство" : "",
  };
  const formValid = !errors.name && !errors.phone && !errors.email && !errors.consentPdn && !errors.consentMed;

  const go = (n) => setStep(s => Math.max(0, Math.min(3, s + n)));

  const setField = (k, v) => setForm(f => ({ ...f, [k]: v }));

  // маска телефона
  const onPhone = (v) => {
    let d = v.replace(/\D/g, "");
    if (d.startsWith("8")) d = "7" + d.slice(1);
    if (!d.startsWith("7")) d = "7" + d;
    d = d.slice(0, 11);
    let out = "+7";
    if (d.length > 1) out += " (" + d.slice(1, 4);
    if (d.length >= 4) out += ") " + d.slice(4, 7);
    if (d.length >= 7) out += "-" + d.slice(7, 9);
    if (d.length >= 9) out += "-" + d.slice(9, 11);
    setField("phone", out);
  };

  const submit = async () => {
    setTouched(true);
    if (!formValid || submitting) return;
    if (requireAuth && !auth) { setSubmitError("Подтвердите личность через Telegram или MAX — это нужно для напоминаний о приёме."); return; }
    setSubmitError("");
    setSubmitting(true);
    try {
      const payload = {
        serviceId: service ? service.id : "",
        service: service ? service.name : "Консультация",
        doctor: doctor ? doctor.name : "Любой свободный специалист",
        appointmentDate: dayKey,
        appointmentTime: time,
        name: form.name.trim(),
        phone: form.phone,
        email: form.email.trim(),
        comment: form.comment.trim(),
        consent: form.consentPdn,
        consentMed: form.consentMed,
        consentPhoto: form.consentPhoto,
        consentAds: form.consentAds,
        utm: UTM.utm,
        referrer: UTM.referrer,
      };
      const res = await apiPost("/api/booking", payload, auth ? auth.token : null);
      setCancelToken(res.cancelToken || null);
      setBookingId(res.id || null);
      setDone(true);
    } catch (e) {
      if (e.code === "AUTH_REQUIRED") { logoutAuth(); setSubmitError("Сессия истекла — подтвердите личность ещё раз."); }
      else setSubmitError(e.message || "Не удалось отправить заявку. Попробуйте ещё раз.");
    } finally {
      setSubmitting(false);
    }
  };

  if (done) return <Success {...{ service, doctor, day, time, form, cancelToken, bookingId, authToken: auth ? auth.token : null, onlinePayments, reset: () => { setDone(false); setStep(0); setServiceId(null); setDoctorId(null); setDayKey(null); setTime(null); setForm({ name:"",phone:"",email:"",comment:"",consentPdn:false,consentMed:false,consentPhoto:false,consentAds:false }); setTouched(false); setCancelToken(null); setBookingId(null); setSubmitError(""); setStarted(false); } }} />;

  if (!started) return <Hub onPick={(s) => { setStep(s); setStarted(true); setMode(s === 1 ? "doctor" : "service"); }} />;

  return (
    <div>
      <header className="bk-nav">
        <div className="wrap bk-nav-inner">
          <a href="index.html" className="logo">Персоналик<span className="dot">а</span></a>
          <a href="index.html" className="back-link"><Ic d={IC.arrowL} s={18} /> На сайт</a>
        </div>
      </header>

      <div className="wrap bk-shell">
        <main className="bk-main">
          <div className="intro">
            <span className="eyebrow">Онлайн-запись</span>
            <h1 style={{ marginTop: 14 }}>Запишитесь за минуту</h1>
            <p>Выберите процедуру, специалиста и удобное время. Первичная консультация — бесплатно.</p>
          </div>

          <Stepper step={step} filled={[!!serviceId, !!doctorId, !!(dayKey && time), allReady]} onJump={jump} order={order} />

          {step === 0 && <ServiceStep {...{ serviceId, setServiceId }} />}
          {step === 1 && <DoctorStep {...{ avail, doctorId, setDoctorId, service, days, onQuickPick: quickPick, onOther: (id) => { setDoctorId(id); setStep(2); } }} />}
          {step === 2 && <TimeStep {...{ days, dayKey, setDayKey, time, setTime, doctorName: doctor ? doctor.name : null, dur: service ? service.dur : 60 }} />}
          {step === 3 && <ContactStep {...{ form, setField, onPhone, errors, touched, requireAuth, auth, verify, onVerify: startVerify, onLogout: logoutAuth }} />}

          <div className="step-nav">
            <button className="btn btn-ghost" onClick={() => go(-1)} disabled={step === 0}>
              <Ic d={IC.arrowL} s={18} /> Назад
            </button>
            {step < 3
              ? <button className="btn btn-primary" onClick={goNext} disabled={!canNext}>Далее <span className="arrow"><Ic d={IC.arrowR} s={18} /></span></button>
              : <button className="btn btn-accent btn-lg" onClick={submit} disabled={submitting || (requireAuth && !auth)}>{submitting ? "Отправляем…" : "Подтвердить запись"} <span className="arrow"><Ic d={IC.arrowR} s={18} /></span></button>}
          </div>
          {submitError && <div className="hint" style={{ color: "#d8533a", marginTop: 14, textAlign: "right" }}>{submitError}</div>}
        </main>

        <aside>
          <Summary {...{ service, doctor, day, time, step }} />
        </aside>
      </div>
    </div>
  );
}

/* ---------- Степпер (кликабельный) ---------- */
function Stepper({ step, filled = [], onJump, order }) {
  const seq = order || STEPS.map((_, i) => i);
  return (
    <div className="stepper">
      {seq.map((i, pos) => {
        const s = STEPS[i];
        const done = filled[i] && i !== step;
        const clickable = onJump && (i !== 3 || filled[3]);
        return (
          <button
            key={i}
            type="button"
            className={"step-pip " + (done ? "done" : i === step ? "active" : "") + (clickable ? " clickable" : "")}
            onClick={() => clickable && onJump(i)}
            disabled={!clickable}
          >
            <div className="bar"><i /></div>
            <div className="lbl">
              <span className="nm">{done ? <Ic d={IC.check} s={12} /> : pos + 1}</span>
              <span className="txt">{s.k}</span>
            </div>
          </button>
        );
      })}
    </div>
  );
}

/* ---------- Шаг 1: услуга ---------- */
function ServiceStep({ serviceId, setServiceId }) {
  const cats = [...new Set(SERVICES.map(s => s.cat))];
  return (
    <div className="panel">
      <h2 className="panel-h">Какая процедура вас интересует?</h2>
      <p className="panel-sub">Не уверены в выборе — запишитесь на консультацию, врач подберёт программу.</p>
      {cats.map(cat => (
        <div key={cat} style={{ marginBottom: 26 }}>
          <div className="daypart" style={{ marginTop: 0 }}>{cat}</div>
          <div className="opt-grid">
            {SERVICES.filter(s => s.cat === cat).map(s => (
              <button key={s.id} className={"opt" + (serviceId === s.id ? " sel" : "")} onClick={() => setServiceId(s.id)}>
                <div className="opt-ic"><Ic d={IC[s.ic]} /></div>
                <div style={{ flex: 1 }}>
                  <h4>{s.name}</h4>
                  <div className="meta">{s.dur} мин · {s.cat.split(" ")[0].toLowerCase()}</div>
                  <div className="price">{fmtPrice(s.price)}</div>
                </div>
                <div className="opt-check"><Ic d={IC.check} s={13} /></div>
              </button>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

/* ---------- Шаг 2: врач (с ближайшими свободными слотами) ---------- */
function DoctorStep({ avail, doctorId, setDoctorId, service, days, onQuickPick, onOther }) {
  const nextDay = days.find(d => !d.off) || days[0];
  const nextKey = ymd(nextDay.date);
  const isTomorrow = nextKey === ymd(days[0].date);
  const dayLabel = isTomorrow ? "завтра" : `${nextDay.dn} ${nextDay.mo}`;
  const dur = service ? service.dur : 60; // длительность услуги (для подбора подходящих слотов)

  // Свободные слоты ближайшего дня по каждому врачу — с учётом длительности услуги
  const [free, setFree] = useState({});
  useEffect(() => {
    let cancelled = false;
    const reqs = avail.map(d =>
      fetch(API + `/api/slots?doctor=${encodeURIComponent(d.name)}&date=${nextKey}`)
        .then(r => r.json())
        .then(j => ({ id: d.id, list: freeSlotsFor((j && j.taken) || [], dur) }))
        .catch(() => ({ id: d.id, list: freeSlotsFor([], dur) }))
    );
    Promise.all(reqs).then(res => {
      if (cancelled) return;
      const m = { any: freeSlotsFor([], dur) };
      res.forEach(r => m[r.id] = r.list);
      setFree(m);
    });
    return () => { cancelled = true; };
  }, [avail.map(d => d.id).join(","), nextKey, dur]);

  const Slots = ({ id }) => {
    const list = (free[id] || freeSlotsFor([], dur)).slice(0, 4);
    return (
      <div className="doc-slots">
        <div className="doc-slots-lbl">Ближайшее время для записи {dayLabel}:</div>
        <div className="doc-slots-row">
          {list.length === 0 && <span className="doc-slots-lbl">на этот день всё занято —</span>}
          {list.map(t => (
            <button key={t} className="doc-slot" onClick={(e) => { e.stopPropagation(); onQuickPick(id, nextKey, t); }}>{t}</button>
          ))}
          <button className="doc-slot doc-slot-other" onClick={(e) => { e.stopPropagation(); onOther(id); }}>Другое время</button>
        </div>
      </div>
    );
  };

  return (
    <div className="panel">
      <h2 className="panel-h">Выберите специалиста</h2>
      <p className="panel-sub">{service ? `Врачи, выполняющие «${service.name}»` : "Доступные врачи клиники"}. Нажмите на свободное время или «Другое время» для календаря.</p>
      <div className="doc-list">
        <div className={"doc-card" + (doctorId === "any" ? " sel" : "")} onClick={() => setDoctorId("any")}>
          <div className="doc-head">
            <div className="doc-av"><Ic d={IC.spark} s={22} /></div>
            <div style={{ flex: 1 }}>
              <h4>Любой свободный специалист</h4>
              <div className="meta">Подберём врача под ближайшее удобное время</div>
            </div>
            <div className="opt-check"><Ic d={IC.check} s={13} /></div>
          </div>
          <Slots id="any" />
        </div>
        {avail.map(d => (
          <div key={d.id} className={"doc-card" + (doctorId === d.id ? " sel" : "")} onClick={() => setDoctorId(d.id)}>
            <div className="doc-head">
              <div className="doc-av">{d.init}</div>
              <div style={{ flex: 1 }}>
                <h4>{d.name}</h4>
                <div className="meta">{d.role}{service ? " · " + fmtPrice(service.price) : ""}</div>
                <div className="chip-any" style={{ marginTop: 6 }}>{d.exp}</div>
              </div>
              <div className="opt-check"><Ic d={IC.check} s={13} /></div>
            </div>
            <Slots id={d.id} />
          </div>
        ))}
      </div>
    </div>
  );
}

/* ---------- Шаг 3: дата и время (календарь на 3 месяца) ---------- */
function TimeStep({ dayKey, setDayKey, time, setTime, doctorName, dur = 60 }) {
  const today = clinicToday();
  const minDate = (() => { const d = new Date(today); d.setDate(d.getDate() + 1); return d; })(); // с завтра
  const maxDate = (() => { const d = new Date(today); d.setMonth(d.getMonth() + 3); return d; })(); // +3 месяца
  const [view, setView] = useState({ y: minDate.getFullYear(), m: minDate.getMonth() });

  const [taken, setTaken] = useState([]);
  const [loading, setLoading] = useState(false);
  // Конфиг календаря: выходные дни недели + заблокированные даты (управляется менеджером).
  const [cfg, setCfg] = useState({ daysOff: [0], blocked: [] });
  useEffect(() => {
    let cancelled = false;
    const q = doctorName ? `?doctor=${encodeURIComponent(doctorName)}` : "";
    fetch(API + "/api/calendar-config" + q)
      .then(r => r.json())
      .then(j => { if (!cancelled && j.ok) setCfg({ daysOff: j.daysOff || [0], blocked: j.blocked || [] }); })
      .catch(() => {});
    return () => { cancelled = true; };
  }, [doctorName]);

  useEffect(() => {
    setTaken([]);
    if (!dayKey || !doctorName) return;
    let cancelled = false;
    setLoading(true);
    fetch(API + `/api/slots?doctor=${encodeURIComponent(doctorName)}&date=${dayKey}`)
      .then(r => r.json())
      .then(j => { if (!cancelled && j.ok) setTaken(j.taken || []); })
      .catch(() => {})
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [dayKey, doctorName]);

  const cells = buildMonth(view.y, view.m);
  const canPrev = view.y > minDate.getFullYear() || (view.y === minDate.getFullYear() && view.m > minDate.getMonth());
  const canNext = view.y < maxDate.getFullYear() || (view.y === maxDate.getFullYear() && view.m < maxDate.getMonth());
  const dis = (c) => !c || cfg.daysOff.includes(c.date.getDay()) || cfg.blocked.includes(c.key) || c.date < minDate || c.date > maxDate;
  const shift = (n) => setView(v => { const d = new Date(v.y, v.m + n, 1); return { y: d.getFullYear(), m: d.getMonth() }; });

  return (
    <div className="panel">
      <h2 className="panel-h">Когда вам удобно?</h2>
      <p className="panel-sub">Выберите любую дату в пределах 3 месяцев и свободное время. Перенести визит можно бесплатно за 24 часа.</p>
      <div className="cal">
        <div className="cal-head">
          <button className="cal-nav" disabled={!canPrev} onClick={() => shift(-1)} aria-label="Предыдущий месяц"><Ic d={IC.arrowL} s={18} /></button>
          <div className="cal-title">{MONTHS_FULL[view.m]} {view.y}</div>
          <button className="cal-nav" disabled={!canNext} onClick={() => shift(1)} aria-label="Следующий месяц"><Ic d={IC.arrowR} s={18} /></button>
        </div>
        <div className="cal-grid cal-wd">{WEEKDAYS.map(w => <div key={w} className="cal-wd-cell">{w}</div>)}</div>
        <div className="cal-grid">
          {cells.map((c, i) => c
            ? <button key={i} className={"cal-day" + (dayKey === c.key ? " sel" : "")} disabled={dis(c)} onClick={() => { setDayKey(c.key); setTime(null); }}>{c.dn}</button>
            : <span key={i} />)}
        </div>
      </div>
      {dayKey && loading && <div style={{ color: "var(--ink-faint)", marginTop: 18, fontSize: 14 }}>Проверяем свободное время…</div>}
      {dayKey && Object.entries(SLOTS).map(([part, list]) => (
        <div key={part}>
          <div className="daypart">{part}</div>
          <div className="slots">
            {list.map(t => {
              const off = !slotFitsService(t, dur, taken);
              return (
                <div key={t} className={"slot" + (off ? " off" : "") + (time === t ? " sel" : "")}
                     onClick={() => { if (!off) setTime(t); }}>{t}</div>
              );
            })}
          </div>
        </div>
      ))}
      {!dayKey && <div style={{ color: "var(--ink-faint)", marginTop: 28, fontSize: 15 }}>Выберите дату в календаре ↑</div>}
    </div>
  );
}

/* ---------- Шаг 4: контакты ---------- */
function AuthGate({ auth, verify, onVerify, onLogout }) {
  if (auth) {
    const chan = auth.channel === "max" ? "MAX" : "Telegram";
    return (
      <div className="auth-gate ok" style={{ display: "flex", alignItems: "center", gap: 12, padding: "14px 16px", background: "#eef3e6", border: "1px solid #cdd9b4", borderRadius: 14, marginBottom: 18 }}>
        <span style={{ fontSize: 22 }}>✅</span>
        <div style={{ flex: 1 }}>
          <div style={{ fontWeight: 700, color: "#4c5430" }}>Личность подтверждена{auth.name ? ` · ${auth.name}` : ""}</div>
          <div style={{ fontSize: 13, color: "#5c6340" }}>Напоминание о приёме придёт в {chan} — без СМС.</div>
        </div>
        <button type="button" onClick={onLogout} style={{ background: "none", border: "none", color: "var(--olive)", fontWeight: 600, cursor: "pointer", fontSize: 13 }}>Сменить</button>
      </div>
    );
  }
  const waiting = verify.status === "waiting" || verify.status === "loading";
  return (
    <div className="auth-gate" style={{ padding: "16px 18px", background: "#faf7f0", border: "1px solid var(--line)", borderRadius: 14, marginBottom: 18 }}>
      <div style={{ fontWeight: 700, marginBottom: 4 }}>Подтвердите личность</div>
      <p style={{ margin: "0 0 12px", fontSize: 13.5, color: "var(--ink-soft)" }}>
        Это нужно один раз — чтобы прислать напоминание о приёме в мессенджер <b>без СМС</b>. Имя и телефон подставятся автоматически.
      </p>
      <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
        <button type="button" className="btn btn-primary" disabled={waiting} onClick={() => onVerify("tg")} style={{ flex: "1 1 180px" }}>
          {verify.channel === "tg" && waiting ? "Ожидаем…" : "Войти через Telegram"}
        </button>
        <button type="button" className="btn btn-ghost" disabled={waiting} onClick={() => onVerify("max")} style={{ flex: "1 1 140px" }}>
          {verify.channel === "max" && waiting ? "Ожидаем…" : "Войти через MAX"}
        </button>
      </div>
      {verify.msg && <div className="hint" style={{ marginTop: 10, color: verify.status === "error" ? "#d8533a" : "var(--olive)" }}>{verify.msg}</div>}
    </div>
  );
}
function ContactStep({ form, setField, onPhone, errors, touched, requireAuth, auth, verify, onVerify, onLogout }) {
  return (
    <div className="panel">
      <h2 className="panel-h">Ваши контактные данные</h2>
      <p className="panel-sub">Администратор позвонит, чтобы подтвердить запись.</p>
      {requireAuth && <AuthGate {...{ auth, verify, onVerify, onLogout }} />}
      <div className="field-row">
        <div className="field">
          <label>Имя и фамилия</label>
          <input className={touched && errors.name ? "err" : ""} value={form.name} placeholder="Анна Иванова" onChange={e => setField("name", e.target.value)} />
          <div className="hint">{touched ? errors.name : ""}</div>
        </div>
        <div className="field">
          <label>Телефон</label>
          <input className={touched && errors.phone ? "err" : ""} value={form.phone} placeholder="+7 (___) ___-__-__" inputMode="tel" onChange={e => onPhone(e.target.value)} />
          <div className="hint">{touched ? errors.phone : ""}</div>
        </div>
      </div>
      <div className="field">
        <label>E-mail <span style={{ color: "var(--ink-faint)", fontWeight: 400 }}>— необязательно</span></label>
        <input className={touched && errors.email ? "err" : ""} value={form.email} placeholder="you@mail.ru" inputMode="email" onChange={e => setField("email", e.target.value)} />
        <div className="hint">{touched ? errors.email : ""}</div>
      </div>
      <div className="field">
        <label>Комментарий <span style={{ color: "var(--ink-faint)", fontWeight: 400 }}>— необязательно</span></label>
        <textarea rows="3" value={form.comment} placeholder="Пожелания, вопросы или показания" onChange={e => setField("comment", e.target.value)}></textarea>
      </div>
      <label className="consent">
        <input type="checkbox" checked={form.consentPdn} onChange={e => setField("consentPdn", e.target.checked)} />
        <span>Я согласен(а) на <a href="Конфиденциальность.html" target="_blank" rel="noopener" style={{ color: "var(--olive)" }}>обработку персональных данных</a> и принимаю <a href="Соглашение.html" target="_blank" rel="noopener" style={{ color: "var(--olive)" }}>пользовательское соглашение</a>.</span>
      </label>
      {touched && errors.consentPdn && <div className="hint" style={{ color: "#d8533a" }}>{errors.consentPdn}</div>}
      <label className="consent">
        <input type="checkbox" checked={form.consentMed} onChange={e => setField("consentMed", e.target.checked)} />
        <span>Я даю <a href="ИДС.html" target="_blank" rel="noopener" style={{ color: "var(--olive)" }}>информированное добровольное согласие на медицинское вмешательство</a> и ознакомлен(а) с тем, что имеются противопоказания и необходима консультация специалиста.</span>
      </label>
      {touched && errors.consentMed && <div className="hint" style={{ color: "#d8533a" }}>{errors.consentMed}</div>}
      <label className="consent">
        <input type="checkbox" checked={form.consentPhoto} onChange={e => setField("consentPhoto", e.target.checked)} />
        <span>Согласен(а) на фото «до/после» для оценки результата <span style={{ color: "var(--ink-faint)", fontWeight: 400 }}>— необязательно</span>.</span>
      </label>
      <label className="consent">
        <input type="checkbox" checked={form.consentAds} onChange={e => setField("consentAds", e.target.checked)} />
        <span>Согласен(а) получать информационные и рекламные сообщения клиники (ст. 18 Закона «О рекламе») <span style={{ color: "var(--ink-faint)", fontWeight: 400 }}>— необязательно</span>.</span>
      </label>
    </div>
  );
}

/* ---------- Сводка ---------- */
function SumRow({ icon, k, v }) {
  return (
    <div className="sum-row">
      <div className="si"><Ic d={IC[icon]} s={18} /></div>
      <div>
        <div className="sk">{k}</div>
        <div className={"sv" + (v ? "" : " empty")}>{v || "—"}</div>
      </div>
    </div>
  );
}
function Summary({ service, doctor, day, time }) {
  const dateStr = day ? `${day.dn} ${day.mo}${time ? ", " + time : ""}` : "";
  return (
    <div className="summary">
      <h3>Ваша запись</h3>
      <p className="sb">Заполняется по мере выбора</p>
      <SumRow icon="spark" k="Услуга" v={service ? service.name : ""} />
      <SumRow icon="user" k="Специалист" v={doctor ? doctor.name : ""} />
      <SumRow icon="clock" k="Дата и время" v={dateStr} />
      <div className="sum-total">
        <span style={{ fontWeight: 600 }}>Стоимость</span>
        <span className="tp">{service ? fmtPrice(service.price) : "—"}</span>
      </div>
      <div className="sum-note"><Ic d={IC.shield} s={16} /> <span>Оплата в клинике после приёма. Запись ни к чему не обязывает.</span></div>
    </div>
  );
}

/* ---------- Успех ---------- */
function Success({ service, doctor, day, time, form, reset, cancelToken, bookingId, authToken, onlinePayments }) {
  const [cancelled, setCancelled] = useState(false);
  const [busy, setBusy] = useState(false);
  const [cancelErr, setCancelErr] = useState("");
  const [payBusy, setPayBusy] = useState(false);
  const [payErr, setPayErr] = useState("");
  const canPay = onlinePayments && bookingId && service && service.price > 0 && !cancelled;
  const doPay = async () => {
    if (!canPay || payBusy) return;
    setPayErr(""); setPayBusy(true);
    try {
      const res = await apiPost("/api/payments/create", { bookingId, cancelToken }, authToken);
      if (res && res.confirmationUrl) { window.location.href = res.confirmationUrl; return; }
      setPayErr("Не удалось создать платёж. Попробуйте позже.");
    } catch (e) {
      setPayErr(e.message || "Не удалось перейти к оплате");
    } finally {
      setPayBusy(false);
    }
  };
  const doCancel = async () => {
    if (!cancelToken || busy) return;
    setCancelErr(""); setBusy(true);
    try {
      await apiPost("/api/booking/cancel-by-token", { token: cancelToken });
      setCancelled(true);
    } catch (e) {
      setCancelErr(e.message || "Не удалось отменить");
    } finally {
      setBusy(false);
    }
  };
  return (
    <div>
      <header className="bk-nav">
        <div className="wrap bk-nav-inner">
          <a href="index.html" className="logo">Персоналик<span className="dot">а</span></a>
          <a href="index.html" className="back-link"><Ic d={IC.arrowL} s={18} /> На сайт</a>
        </div>
      </header>
      <div className="wrap">
        <div className="success">
          <div className="succ-ic"><Ic d={IC.check} s={46} /></div>
          <h2>Вы записаны!</h2>
          <p>Спасибо, {form.name.split(" ")[0] || "что выбрали нас"}. Администратор позвонит на {form.phone} в ближайшее время, чтобы подтвердить визит.</p>
          <div className="succ-card">
            <div className="row"><span className="k">Услуга</span><span className="v">{service ? service.name : "Консультация"}</span></div>
            <div className="row"><span className="k">Специалист</span><span className="v">{doctor ? doctor.name : "—"}</span></div>
            <div className="row"><span className="k">Дата и время</span><span className="v">{day ? `${day.dn} ${day.mo}, ${time}` : "—"}</span></div>
            <div className="row"><span className="k">Стоимость</span><span className="v">{service ? fmtPrice(service.price) : "бесплатно"}</span></div>
          </div>
          {cancelled
            ? <div className="succ-card" style={{ color: "#b2502d", textAlign: "center" }}>Запись отменена. Вы можете записаться заново.</div>
            : null}
          {canPay &&
            <div style={{ textAlign: "center", marginBottom: 6 }}>
              <button className="btn btn-accent btn-lg" onClick={doPay} disabled={payBusy}>
                {payBusy ? "Переходим к оплате…" : `Оплатить онлайн · ${fmtPrice(service.price)}`}
              </button>
              <div className="hint" style={{ marginTop: 8, color: "var(--ink-faint)" }}>Оплата картой через ЮKassa. Можно оплатить и в клинике.</div>
              {payErr && <div className="hint" style={{ color: "#d8533a", marginTop: 8 }}>{payErr}</div>}
            </div>}
          <div style={{ display: "flex", gap: 14, justifyContent: "center", flexWrap: "wrap" }}>
            <a href="index.html" className="btn btn-primary btn-lg">Вернуться на сайт</a>
            <button className="btn btn-ghost btn-lg" onClick={reset}>Записаться ещё</button>
            {cancelToken && !cancelled &&
              <button className="btn btn-ghost btn-lg" onClick={doCancel} disabled={busy} style={{ color: "#b2502d" }}>
                {busy ? "Отменяем…" : "Отменить запись"}
              </button>}
          </div>
          {cancelErr && <div className="hint" style={{ color: "#d8533a", marginTop: 12 }}>{cancelErr}</div>}
        </div>
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
