/* ============================================================
   Paella — рекомендательное ядро v1 («подобрано для тебя»).
   Принцип: вкусовой профиль строится из РЕАЛЬНЫХ действий пользователя
   (просмотры карточек, сохранения, оценки) и анкеты; кандидаты
   ранжируются контентным скорингом с разнообразием (MMR-лайт).
   Всё считается на устройстве — никаких внешних вызовов, нет точек сбоя.
   Профиль хранится локально и синхронизируется с аккаунтом (user_state).
   Семантический слой (свободный запрос через RAG) — этап облака.
   ============================================================ */

const TasteStore = (function () {
  const KEY = "paella_taste_v1";
  let p = { cats: {}, subs: {}, cuis: {}, priceSum: 0, priceN: 0, seen: {}, avoid: {}, noSpicy: 0, ts: 0, decayTs: 0 };
  try { p = { ...p, ...(JSON.parse(localStorage.getItem(KEY) || "{}")) } ; } catch (e) {}
  const subs = new Set();
  const save = () => { p.ts = Date.now(); try { localStorage.setItem(KEY, JSON.stringify(p)); } catch (e) {} subs.forEach(f => f()); };
  const bump = (obj, k, w) => { if (!k) return; obj[k] = Math.max(-30, Math.min(60, (obj[k] || 0) + w)); };
  /* затухание двухуровневое (исследования RecSys: вкусы к кухням живут ~150 дней):
     категории/кухни — медленно (0.95/нед), сигналы по конкретным подвидам —
     быстро (0.85/нед), ограничения «не ем морепродукты» — почти вечные (0.97) */
  const decay = () => {
    const now = Date.now();
    if (!p.decayTs) { p.decayTs = now; return; }
    const weeks = Math.floor((now - p.decayTs) / (7 * 86400000));
    if (weeks < 1) return;
    const shrink = (o, rate) => {
      const k = Math.pow(rate, Math.min(weeks, 12));
      Object.keys(o).forEach(x => { o[x] = Math.round(o[x] * k * 10) / 10; if (Math.abs(o[x]) < 0.5) delete o[x]; });
    };
    shrink(p.cats, 0.95); shrink(p.cuis, 0.95); shrink(p.subs, 0.85); shrink(p.avoid, 0.97);
    p.decayTs = now;
  };
  return {
    /* сигнал: пользователь провзаимодействовал с блюдом.
       вес: просмотр 1 · сохранение 4 · «норм» 2 · «огонь» 6 · «не зашло» −4 */
    note(dish, w) {
      if (!dish) return;
      decay();
      bump(p.cats, dish.cat, w);
      bump(p.subs, dish.sub, w * 1.25);
      bump(p.cuis, dish.cuisine, w * 0.6);
      if (dish.price && w > 0) { p.priceSum += dish.price * w; p.priceN += w; }
      if (w >= 1) p.seen[dish.id] = (p.seen[dish.id] || 0) + 1;
      save();
    },
    /* анкета вкусов: любимые категории, бюджет и ограничения */
    quiz(cats, maxPrice, avoidCats, noSpicy) {
      decay();
      (cats || []).forEach(c => bump(p.cats, c, 8));
      (avoidCats || []).forEach(c => bump(p.avoid, c, 12));
      if (noSpicy) p.noSpicy = 1;
      if (maxPrice) { p.priceSum += maxPrice * 6; p.priceN += 6; }
      save();
    },
    profile: () => p,
    hasSignal: () => Object.keys(p.cats).length > 0 || p.priceN > 0,
    avgPrice: () => (p.priceN ? p.priceSum / p.priceN : null),
    subscribe: (f) => { subs.add(f); return () => subs.delete(f); },
  };
})();
function useTaste() {
  const [, force] = useState(0);
  useEffect(() => TasteStore.subscribe(() => force(x => x + 1)), []);
  return TasteStore;
}

/* контекст времени суток: утром тянет к завтракам, вечером — к ужину */
function dayContext() {
  const h = new Date().getHours();
  if (h >= 6 && h < 11) return { boost: { "Завтраки": 0.09, "Десерты": 0.03 }, label: "к завтраку" };
  if (h >= 11 && h < 16) return { boost: { "Супы": 0.05, "Салаты": 0.04, "Паста": 0.03, "Стритфуд": 0.03 }, label: "к обеду" };
  if (h >= 16 && h < 24) return { boost: { "Стейки": 0.05, "Мясо": 0.04, "Морепродукты": 0.04, "Грузинская кухня": 0.03 }, label: "к ужину" };
  return { boost: {}, label: "" };
}

/* похожесть блюда на сохранённые: та же категория/подвид/кухня/ценовой пояс */
function simToSaved(d, savedDishes) {
  let best = 0;
  for (const s of savedDishes) {
    let sim = 0;
    if (s.cat === d.cat) sim += 0.4;
    if (s.sub && s.sub === d.sub) sim += 0.35;
    if (s.cuisine === d.cuisine) sim += 0.15;
    if (s.price && d.price && Math.abs(s.price - d.price) / s.price < 0.35) sim += 0.1;
    if (sim > best) best = sim;
  }
  return best;
}

/* ---- главный отбор: топ-N блюд под профиль ---- */
function recommendDishes(n, shuffleSeed) {
  const P = window.PAELLA;
  const t = TasteStore.profile();
  const avgP = TasteStore.avgPrice();
  const ctx = dayContext();
  let savedIds = new Set();
  try { savedIds = new Set(JSON.parse(localStorage.getItem("paella_saved_v1") || "[]")); } catch (e) {}
  const savedDishes = [...savedIds].map(id => P.byId(id)).filter(Boolean).slice(-6);   // свежие сохранения важнее

  const cand = P.dishes.filter(d => d.photo && P.heroEligible(d) && P.dataBacked(d));
  const maxCat = Math.max(1, ...Object.values(t.cats).map(Math.abs));
  const jitter = shuffleSeed ? (i) => ((Math.sin(i * 127.1 + shuffleSeed * 311.7) + 1) / 2) * 0.05 : () => 0;
  const scored = cand.map((d, i) => {
    let s = (d.score / 100) * 0.42;                          // качество блюда — база
    s += ((t.cats[d.cat] || 0) / maxCat) * 0.20;             // любимые категории
    s += ((t.subs[d.sub] || 0) / maxCat) * 0.15;             // любимые подвиды — точнее всего
    s += ((t.cuis[d.cuisine] || 0) / maxCat) * 0.07;         // кухня заведения
    s += simToSaved(d, savedDishes) * 0.12;                  // похоже на то, что уже сохранил
    s += ctx.boost[d.cat] || 0;                              // время суток
    s -= ((t.avoid[d.cat] || 0) / 12) * 0.25;                // «не люблю …» из анкеты
    if (t.noSpicy && (d.allergens || []).includes("острое")) s -= 0.2;
    if (avgP && d.price) {                                   // ценовая совместимость
      const ratio = d.price / avgP;
      s += (ratio > 0.4 && ratio < 1.6) ? 0.08 : ratio >= 2.2 ? -0.06 : 0;
    }
    if (savedIds.has(d.id)) s -= 0.5;                        // уже в избранном — не советуем повторно
    s -= Math.min(0.12, (t.seen[d.id] || 0) * 0.04);         // виденное — слегка вниз, карусель свежего
    s += jitter(i);                                          // «обновить подборку» — лёгкая пересдача
    return { d, s };
  }).sort((a, b) => b.s - a.s);

  /* разнообразие: не больше 2 блюд категории и 2 блюд одного заведения */
  const N = n || 8;
  const out = [], catN = {}, restN = {};
  for (const { d } of scored) {
    if ((catN[d.cat] || 0) >= 2 || (restN[d.restaurantId] || 0) >= 2) continue;
    out.push(d);
    catN[d.cat] = (catN[d.cat] || 0) + 1;
    restN[d.restaurantId] = (restN[d.restaurantId] || 0) + 1;
    if (out.length >= N - 1) break;
  }
  /* слот открытия: одно сильное блюдо из категории, которую пользователь ещё не трогал —
     иначе рекомендации замыкаются в пузыре и надоедают */
  const untouched = scored.filter(({ d }) => !(d.cat in t.cats) && !catN[d.cat] && !out.includes(d));
  if (untouched.length) { untouched[0].d._explore = true; out.push(untouched[0].d); }
  else if (scored[out.length]) out.push(scored[out.length].d);
  return out;
}

/* почему рекомендуем: главный положительный фактор профиля */
function recReason(d) {
  const t = TasteStore.profile();
  if (d._explore) return "попробуй новое ✨";
  if ((t.subs[d.sub] || 0) >= 6) return `ты любишь: ${d.sub}`;
  if ((t.cats[d.cat] || 0) >= 6) return `под твою любовь к «${d.cat}»`;
  if ((t.cuis[d.cuisine] || 0) >= 4) return `${d.cuisine} — твоя кухня`;
  const ctx = dayContext();
  if (ctx.boost[d.cat]) return `самое время — ${ctx.label}`;
  return d.reviewSnippet ? "гости очень хвалят" : "высокий балл рядом";
}

/* ---------- блок «Подобрано для тебя» (главная) ---------- */
function ForYouRail({ ctx }) {
  const T = useTaste();
  useVisited();
  const [seed, setSeed] = useState(0);
  const has = T.hasSignal();
  const recs = useMemo(() => recommendDishes(8, seed), [T.profile().ts, has, seed]);
  if (!recs.length) return null;
  return (
    <section className="wrap sec" style={{ marginTop: 34 }}>
      <div className="row" style={{ marginBottom: 4, gap: 9, flexWrap: "wrap" }}>
        <span className="eyebrow">Подобрано для тебя</span>
        {has
          ? <span className="badge b-green"><Icon name="sparkles" size={11} />по твоему вкусовому профилю</span>
          : <span className="badge b-sand">начни сохранять и оценивать — подстроимся точнее</span>}
        <button className="btn btn-soft btn-sm" style={{ marginLeft: "auto" }} onClick={() => setSeed(s => s + 1)}>
          <Icon name="sparkles" size={14} />Обновить подборку
        </button>
      </div>
      <p className="muted" style={{ margin: "0 0 14px", fontSize: 13 }}>
        {has ? "Профиль обновляется от каждого сохранения и оценки — рекомендации живые." : "Пока показываем сильное рядом: профиль появится от твоих действий."}
      </p>
      <div className="foryou-rail">
        {recs.map((d) => (
          <div key={d.id} className="foryou-card" onClick={() => ctx.openDish(d)}>
            <div className="fy-media">
              <DishPhoto src={d.photo} alt={d.name} />
              <ScoreTag u={uOf(d)} float />
            </div>
            <div className="fy-body">
              <div className="fy-name">{d.name}</div>
              <div className="fy-meta">{d.restaurantName}{d.price ? ` · ${d.price.toLocaleString("ru-RU")} ₽` : ""}</div>
              <div className="fy-why"><Icon name="sparkles" size={11} /> {recReason(d)}</div>
            </div>
          </div>
        ))}
      </div>
    </section>
  );
}

/* ============================================================
   «Лучшее рядом» — настоящая геолокация: балл × близость,
   подпись «N мин пешком». Позиция запоминается на устройстве.
   ============================================================ */
const GeoStore = (function () {
  const KEY = "paella_geo_v1";
  let g = null;
  try { g = JSON.parse(localStorage.getItem(KEY) || "null"); } catch (e) {}
  const subs = new Set();
  return {
    get: () => g,
    set(lat, lng) { g = { lat, lng, ts: Date.now() }; try { localStorage.setItem(KEY, JSON.stringify(g)); } catch (e) {} subs.forEach(f => f()); },
    subscribe: (f) => { subs.add(f); return () => subs.delete(f); },
  };
})();
const geoDistM = (la1, lo1, la2, lo2) => {
  const R = 6371000, r = Math.PI / 180;
  const dLa = (la2 - la1) * r, dLo = (lo2 - lo1) * r;
  const a = Math.sin(dLa / 2) ** 2 + Math.cos(la1 * r) * Math.cos(la2 * r) * Math.sin(dLo / 2) ** 2;
  return Math.round(2 * R * Math.asin(Math.sqrt(a)));
};
const walkMin = (m) => Math.max(1, Math.round(m / 80));

/* топ рядом: качество × близость, максимум 1 блюдо на заведение */
function nearbyBest(lat, lng, n) {
  const P = window.PAELLA;
  const t = TasteStore.profile();
  const cand = P.dishes.filter(d => d.photo && d.latitude && P.heroEligible(d) && P.dataBacked(d));
  const scored = cand.map(d => {
    const dist = geoDistM(lat, lng, d.latitude, d.longitude);
    if (dist > 3000) return null;                                  // дальше 3 км — не «рядом»
    const prox = Math.max(0, 1 - dist / 2800);                     // близость затухает плавно
    let s = (d.score / 100) * 0.55 + prox * 0.38;
    s += ((t.cats[d.cat] || 0) > 4 ? 0.05 : 0);                    // лёгкий персональный акцент
    return { d, dist, s };
  }).filter(Boolean).sort((a, b) => b.s - a.s);
  const out = [], restN = {}, catN = {};
  for (const x of scored) {
    if ((restN[x.d.restaurantId] || 0) >= 1 || (catN[x.d.cat] || 0) >= 3) continue;
    out.push(x);
    restN[x.d.restaurantId] = 1;
    catN[x.d.cat] = (catN[x.d.cat] || 0) + 1;
    if (out.length >= (n || 8)) break;
  }
  return out;
}

function NearbyBestRail({ ctx }) {
  const [, force] = useState(0);
  useEffect(() => GeoStore.subscribe(() => force(x => x + 1)), []);
  const [st, setSt] = useState("");   // '', loading, err
  const g = GeoStore.get();
  const items = useMemo(() => (g ? nearbyBest(g.lat, g.lng, 8) : []), [g && g.ts]);
  const locate = () => {
    if (!navigator.geolocation) { setSt("err"); return; }
    setSt("loading");
    navigator.geolocation.getCurrentPosition(
      (pos) => { GeoStore.set(pos.coords.latitude, pos.coords.longitude); setSt(""); },
      () => setSt("err"),
      { enableHighAccuracy: true, timeout: 9000 }
    );
  };
  return (
    <section className="wrap sec" style={{ marginTop: 34 }}>
      <div className="row" style={{ marginBottom: 4, gap: 9, flexWrap: "wrap" }}>
        <span className="eyebrow">Лучшее рядом</span>
        {g && <span className="badge b-green"><Icon name="pin" size={11} />по твоей геолокации</span>}
        <button className="btn btn-soft btn-sm" style={{ marginLeft: "auto" }} onClick={locate}>
          <Icon name={st === "loading" ? "clock" : "compass"} size={14} />{g ? "Обновить моё место" : "Показать лучшее рядом"}
        </button>
      </div>
      {st === "err" && <p className="muted" style={{ fontSize: 13 }}>Не получилось определить место — разреши геолокацию в браузере.</p>}
      {!g && st !== "err" && <p className="muted" style={{ margin: "0 0 6px", fontSize: 13 }}>Нажми кнопку — покажем самые сильные блюда в минутах ходьбы от тебя.</p>}
      {g && items.length === 0 && <p className="muted" style={{ fontSize: 13 }}>В радиусе 3 км наших заведений пока нет — мы в центре Москвы, у Охотного Ряда.</p>}
      {items.length > 0 && (
        <div className="foryou-rail">
          {items.map(({ d, dist }) => (
            <div key={d.id} className="foryou-card" onClick={() => ctx.openDish(d)}>
              <div className="fy-media">
                <DishPhoto src={d.photo} alt={d.name} />
                <ScoreTag u={uOf(d)} float />
              </div>
              <div className="fy-body">
                <div className="fy-name">{d.name}</div>
                <div className="fy-meta">{d.restaurantName}{d.price ? ` · ${d.price.toLocaleString("ru-RU")} ₽` : ""}</div>
                <div className="fy-why"><Icon name="pin" size={11} /> {walkMin(dist)} мин пешком</div>
              </div>
            </div>
          ))}
        </div>
      )}
    </section>
  );
}

/* ============================================================
   «Восьмёрка дня» — 8 блюд, каждый день новые (у всех одинаковые):
   детерминированная ротация топа + все из РАЗНЫХ категорий;
   при живом профиле 2 слота подменяются персональными.
   ============================================================ */
function dailyEight() {
  const P = window.PAELLA;
  const d0 = new Date();
  const seed = d0.getFullYear() * 10000 + (d0.getMonth() + 1) * 100 + d0.getDate();
  let s = seed * 2654435761 % 4294967296;
  const rnd = () => { s |= 0; s = (s + 0x6D2B79F5) | 0; let t = Math.imul(s ^ (s >>> 15), 1 | s); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
  const pool = P.dishes.filter(d => d.photo && P.heroEligible(d) && P.dataBacked(d) && d.score >= 82)
    .sort((a, b) => b.q - a.q).slice(0, 120);
  for (let i = pool.length - 1; i > 0; i--) { const j = Math.floor(rnd() * (i + 1)); [pool[i], pool[j]] = [pool[j], pool[i]]; }
  const out = [], seenCat = new Set(), seenRest = new Set();
  for (const d of pool) {
    if (seenCat.has(d.cat) || seenRest.has(d.restaurantId)) continue;
    out.push(d); seenCat.add(d.cat); seenRest.add(d.restaurantId);
    if (out.length >= 8) break;
  }
  /* два персональных слота, если профиль уже живой */
  if (TasteStore.hasSignal()) {
    const pers = recommendDishes(6).filter(x => !out.some(o => o.id === x.id) && !seenRest.has(x.restaurantId)).slice(0, 2);
    pers.forEach((x, i) => { if (out.length >= 8) out[out.length - 1 - i] = x; else out.push(x); });
  }
  return out.slice(0, 8);
}

function DailyEightRail({ ctx }) {
  useTaste();
  const items = useMemo(() => dailyEight(), [new Date().getDate()]);
  if (items.length < 4) return null;
  const dateStr = new Date().toLocaleDateString("ru-RU", { day: "numeric", month: "long" });
  return (
    <section className="wrap sec" style={{ marginTop: 34 }}>
      <div className="row" style={{ marginBottom: 4, gap: 9, flexWrap: "wrap" }}>
        <span className="eyebrow">Восьмёрка дня</span>
        <span className="badge b-orange"><Icon name="flame" size={11} />{dateStr} · завтра будет новая</span>
      </div>
      <p className="muted" style={{ margin: "0 0 14px", fontSize: 13 }}>Восемь сильных блюд из восьми разных категорий — сегодняшний срез вкуса города.</p>
      <div className="foryou-rail">
        {items.map((d) => (
          <div key={d.id} className="foryou-card" onClick={() => ctx.openDish(d)}>
            <div className="fy-media">
              <DishPhoto src={d.photo} alt={d.name} />
              <ScoreTag u={uOf(d)} float />
            </div>
            <div className="fy-body">
              <div className="fy-name">{d.name}</div>
              <div className="fy-meta">{d.restaurantName}{d.price ? ` · ${d.price.toLocaleString("ru-RU")} ₽` : ""}</div>
              <div className="fy-why"><Icon name="utensils" size={11} /> {d.cat}</div>
            </div>
          </div>
        ))}
      </div>
    </section>
  );
}

/* ============================================================
   «Дуэль вкуса» — попарные сравнения (механика Beli): «что вкуснее?».
   Сравнение калибровано лучше звёзд: «моя четвёрка ≠ твоя четвёрка»,
   а «А вкуснее Б» — универсально. 3 раунда в день, стрик за серию.
   Пары — внутри одной категории (пасту сравниваем с пастой).
   ============================================================ */
const DuelStore = (function () {
  const KEY = "paella_duel_v1";
  let s = { date: "", votes: [], streak: 0, lastDone: "" };
  try { s = { ...s, ...(JSON.parse(localStorage.getItem(KEY) || "{}")) }; } catch (e) {}
  const today = () => new Date().toISOString().slice(0, 10);
  const save = () => { try { localStorage.setItem(KEY, JSON.stringify(s)); } catch (e) {} };
  return {
    state() { const t = today(); if (s.date !== t) { s.date = t; s.votes = []; save(); } return s; },
    vote(aId, bId, winId) {
      const t = today();
      s.votes.push({ a: aId, b: bId, w: winId });
      if (s.votes.length >= 3 && s.lastDone !== t) {
        const y = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
        s.streak = s.lastDone === y ? (s.streak || 0) + 1 : 1;
        s.lastDone = t;
      }
      save();
    },
  };
})();

function duelPairs() {
  const P = window.PAELLA;
  const t = TasteStore.profile();
  const d0 = new Date();
  let sd = (d0.getFullYear() * 10000 + (d0.getMonth() + 1) * 100 + d0.getDate() + 7) * 2654435761 % 4294967296;
  const rnd = () => { sd |= 0; sd = (sd + 0x6D2B79F5) | 0; let x = Math.imul(sd ^ (sd >>> 15), 1 | sd); x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x; return ((x ^ (x >>> 14)) >>> 0) / 4294967296; };
  const pool = P.dishes.filter(d => d.photo && P.heroEligible(d) && P.dataBacked(d) && d.score >= 78);
  const byCat = {};
  pool.forEach(d => (byCat[d.cat] = byCat[d.cat] || []).push(d));
  /* сначала любимые категории юзера — дуэли должны быть про то, что он ест */
  const liked = Object.entries(t.cats || {}).filter(([, w]) => w > 2).sort((a, b) => b[1] - a[1]).map(([c]) => c);
  const cats = [...liked, ...Object.keys(byCat).sort((a, b) => byCat[b].length - byCat[a].length)]
    .filter((c, i, A) => byCat[c] && byCat[c].length >= 6 && A.indexOf(c) === i).slice(0, 3);
  const pairs = [];
  for (const c of cats) {
    const arr = byCat[c].slice();
    for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(rnd() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; }
    const a = arr[0];
    const b = arr.find(x => x.restaurantId !== a.restaurantId);
    if (a && b) pairs.push([a, b]);
  }
  return pairs;
}

function DuelCardSide({ d, onPick }) {
  return (
    <div className="duel-card" onClick={onPick}>
      <div className="duel-media"><DishPhoto src={d.photo} alt={d.name} /></div>
      <div className="duel-body">
        <div className="duel-name">{d.name}</div>
        <div className="duel-meta">{d.restaurantName}{d.price ? ` · ${d.price.toLocaleString("ru-RU")} ₽` : ""}</div>
      </div>
    </div>
  );
}

function DuelRail() {
  const [, force] = useState(0);
  const st = DuelStore.state();
  const pairs = useMemo(() => duelPairs(), []);
  const total = Math.min(3, pairs.length);
  const round = Math.min(st.votes.length, total);
  const done = round >= total || total === 0;
  const cur = !done ? pairs[round] : null;
  const vote = (win, lose) => {
    try { TasteStore.note(win, 3); TasteStore.note(lose, -1); } catch (e) {}
    DuelStore.vote(cur[0].id, cur[1].id, win.id);
    force(x => x + 1);
  };
  if (total === 0) return null;
  return (
    <section className="wrap sec" style={{ marginTop: 34 }}>
      <div className="row" style={{ marginBottom: 4, gap: 9, flexWrap: "wrap" }}>
        <span className="eyebrow">Дуэль вкуса</span>
        {st.streak > 0 && <span className="badge b-orange"><Icon name="flame" size={11} />{st.streak} дн. подряд</span>}
        {!done && <span className="muted" style={{ marginLeft: "auto", fontSize: 13, fontWeight: 700 }}>Раунд {round + 1} из {total}</span>}
      </div>
      {done ? (
        <p className="muted" style={{ margin: "6px 0 0", fontSize: 13.5 }}>Дуэли на сегодня сыграны — профиль вкуса стал точнее. Возвращайся завтра за новой парой!</p>
      ) : (
        <>
          <p className="muted" style={{ margin: "0 0 12px", fontSize: 13 }}>Что вкуснее? Один тап — и подборки станут точнее под тебя.</p>
          <div className="duel-row">
            <DuelCardSide d={cur[0]} onPick={() => vote(cur[0], cur[1])} />
            <div className="duel-vs">VS</div>
            <DuelCardSide d={cur[1]} onPick={() => vote(cur[1], cur[0])} />
          </div>
        </>
      )}
    </section>
  );
}

Object.assign(window, { TasteStore, useTaste, recommendDishes, recReason, ForYouRail, NearbyBestRail, DailyEightRail, GeoStore, DuelRail });
