// web/screens/shoots.jsx — экран «Мои съёмки», по дизайну _design/.../shoots.jsx.

// Стадии конвейера, в порядке отображения. Соответствуют значениям
// shoot.Stage из pkg/pipeline/orchestrator.go.
const STAGES = [
  { id: "recognizing", short: "распз",  label: "Распознавание" },
  { id: "plan",        short: "план",   label: "План вариативности" },
  { id: "analyze",     short: "анализ", label: "Анализ" },
  { id: "review1",     short: "р1",     label: "Ревью 1" },
  { id: "generate",    short: "тексты", label: "Тексты" },
  { id: "review2",     short: "р2",     label: "Ревью 2" },
  { id: "csv",         short: "csv",    label: "csv" },
  { id: "done",        short: "exif",   label: "EXIF · watcher" },
];

// stageInfo(stage, shoot?) — возвращает { idx, state, statusText } для shoot.
//   idx        — индекс текущей стадии в STAGES
//   state      — pending | running | waiting | done | failed
//   statusText — короткая русская подпись для пилюли
//
// shoot опционален: если передан, мы учитываем фактический прогресс
// (Recognized/Photos), чтобы не показывать spinner когда все фотки готовы,
// но автоматический переход stage не случился (правка 2026-06-05).
function stageInfo(stage, shoot) {
  if (stage === "new")    return { idx: 0,  state: "pending", statusText: "В очереди на распознавание" };
  if (stage === "failed") return { idx: 0,  state: "failed",  statusText: "Сбой обработки (детали в логах)" };
  if (stage === "done")   return { idx: 7,  state: "done",    statusText: "Готово" };
  if (stage === "review1") return { idx: 3, state: "waiting", statusText: "Ждёт ревью 1" };
  if (stage === "review2") return { idx: 5, state: "waiting", statusText: "Ждёт ревью 2" };

  // Если recognize фактически завершён (все фотки в recognized) — показываем
  // как «готов к анализу», даём кнопку «Сделать анализ →».
  if (stage === "recognizing" && shoot && shoot.Photos > 0 && shoot.Recognized >= shoot.Photos) {
    return { idx: 0, state: "waiting", statusText: "Готов к анализу" };
  }

  const idx = STAGES.findIndex((s) => s.id === stage);
  if (idx >= 0) return { idx, state: "running", statusText: STAGES[idx].label };
  return { idx: 0, state: "pending", statusText: stage || "—" };
}

const MEDIUM_META = {
  photo:  { label: "Фото",   icon: "image" },
  video:  { label: "Видео",  icon: "film" },
  vector: { label: "Vector", icon: "layers" },
};

function KindPills({ medium, editorial }) {
  const m = MEDIUM_META[medium];
  return (
    <>
      {medium !== "photo" && m && (
        <span className={"kind-pill " + medium}>
          <Icon name={m.icon} size={10} stroke={2} /> {m.label}
        </span>
      )}
      {editorial && (
        <span className="kind-pill editorial">
          <Icon name="edit" size={10} stroke={2} /> Editorial
        </span>
      )}
    </>
  );
}

function StagePipeline({ info }) {
  return (
    <div className="stages">
      {STAGES.map((s, i) => {
        let state = "pending";
        if (info.state === "done") state = "done";
        else if (i < info.idx) state = "done";
        else if (i === info.idx) state = info.state;
        return (
          <div key={s.id} className={"stage " + state} title={s.label}>
            <div className="stage-bar" />
            <div className="stage-label">{s.label}</div>
          </div>
        );
      })}
    </div>
  );
}

function ShootStatusPill({ info }) {
  let cls = "pill";
  if (info.state === "done") cls = "pill ok";
  else if (info.state === "waiting") cls = "pill accent";
  else if (info.state === "running") cls = "pill accent";
  else if (info.state === "failed") cls = "pill warn";
  else if (info.state === "pending" && info.idx === 0) cls = "pill warn";
  return (
    <span className={cls}><span className="pdot" />{info.statusText}</span>
  );
}

// shootDateISO — дата съёмки серии из префикса Title «ГГММДД …» (задаётся при
// импорте по EXIF, см. pkg/ingest). Возвращает "YYYY-MM-DD" или null, если
// префикса нет. По этой дате в БД датируются релизы (ключ release.releaseDate).
function shootDateISO(title) {
  const m = /^(\d{2})(\d{2})(\d{2})(?:\D|$)/.exec(title || "");
  if (!m) return null;
  return "20" + m[1] + "-" + m[2] + "-" + m[3];
}

// ShootReleases — блок «Релизы» в карточке серии (T16, arch.md §16.1): готовые
// релизы моделей на дату этой съёмки (матчинг shootDateISO == release.releaseDate)
// как чипы + вход в создание релиза прямо из серии (дата фиксирована = дата серии).
function ShootReleases({ dateISO, setRoute }) {
  const [, toast] = useToasts();
  const [rels, setRels] = React.useState(null);
  const [creating, setCreating] = React.useState(false);
  const load = React.useCallback(() => {
    api.release.ListByDate(dateISO)
      .then((r) => setRels(Array.isArray(r) ? r : []))
      .catch(() => setRels([]));
  }, [dateISO]);
  React.useEffect(() => { load(); }, [load]);

  return (
    <div style={{ marginTop: 8, paddingTop: 8, borderTop: "1px dashed var(--line)" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
        <span className="label" style={{ margin: 0, cursor: "pointer" }}
              onClick={() => setRoute("releases")} title="Открыть экран релизов">Релизы:</span>
        {(rels || []).map((r) => (
          <span key={r.ID} className="pill ok" title={r.OutputName}>
            <span className="pdot" />{r.Model}
          </span>
        ))}
        {rels && rels.length === 0 && <span className="muted" style={{ fontSize: 12 }}>пока нет</span>}
        <button className="btn btn-xs" onClick={() => setCreating((v) => !v)}>
          {creating ? "Отмена" : "+ создать релиз"}
        </button>
      </div>
      {creating && (
        <ShootReleaseForm dateISO={dateISO} toast={toast} onReload={load}/>
      )}
    </div>
  );
}

// ShootReleaseForm — инлайн-форма создания релизов из серии: выбор НЕСКОЛЬКИХ
// моделей чекбоксами + одно общее описание на всех (arch.md §16.1: «бывает общее
// на всю съёмку») + генерация на дату серии одним запросом. Дата фиксирована.
function ShootReleaseForm({ dateISO, onReload, toast }) {
  const [models, setModels] = React.useState(null);
  const [sel, setSel] = React.useState({}); // { [modelID]: true }
  const [desc, setDesc] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const [translating, setTranslating] = React.useState(false);
  const [results, setResults] = React.useState(null); // [{ModelID, OutputName, Error}]

  React.useEffect(() => {
    api.release.ListModels().then((m) => {
      // только модели, которыми можно сгенерировать (есть бланк и фото)
      const flat = [];
      ["Adult", "Child", "Property"].forEach((g) => (m[g] || []).forEach((x) => {
        if (x.HasBlank && (x.Years || []).length) flat.push(x);
      }));
      setModels(flat);
    }).catch(() => setModels([]));
  }, []);

  const toggle = (id) => setSel((s) => ({ ...s, [id]: !s[id] }));
  const selectedIDs = models ? models.filter((m) => sel[m.ID]).map((m) => m.ID) : [];

  const translate = () => {
    if (!desc.trim()) return;
    setTranslating(true);
    api.release.TranslateDescription(desc)
      .then((en) => { setTranslating(false); if (en && en !== desc) setDesc(en);
                      else toast("Перевод не сработал (модель недоступна?)", "warn"); })
      .catch((e) => { setTranslating(false); toast("Перевод не удался: " + e.message, "error"); });
  };

  const generate = () => {
    if (!selectedIDs.length) return;
    setBusy(true); setResults(null);
    const items = selectedIDs.map((id) => ({ ModelID: id, Description: desc }));
    api.release.Generate(dateISO, items)
      .then((res) => {
        setBusy(false);
        const arr = res || [];
        setResults(arr);
        const ok = arr.filter((r) => !r.Error).length;
        const fail = arr.filter((r) => r.Error).length;
        if (ok) toast(`Создано релизов: ${ok}` + (fail ? `, с ошибкой: ${fail}` : ""), fail ? "warn" : "ok");
        else if (fail) toast(`Не удалось: ${fail}`, "error");
        onReload && onReload();
      })
      .catch((e) => { setBusy(false); toast("Ошибка генерации: " + e.message, "error"); });
  };

  if (models === null) return <div className="muted" style={{ fontSize: 12, marginTop: 8 }}><Spinner/> загрузка моделей…</div>;
  if (models.length === 0) return <div className="muted" style={{ fontSize: 12, marginTop: 8 }}>нет готовых моделей (нужны бланк и фото) — добавь на экране «Релизы»</div>;

  const nameByID = {};
  models.forEach((m) => { nameByID[m.ID] = m.Surname + " " + m.GivenName; });

  return (
    <div style={{ marginTop: 10, display: "flex", flexDirection: "column", gap: 8 }}>
      <span className="muted" style={{ fontSize: 12 }}>дата: <span className="mono">{dateISO}</span> · выбери модели (одно описание на всех)</span>
      <div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
        {models.map((m) => (
          <label key={m.ID}
                 style={{ display: "inline-flex", alignItems: "center", gap: 5, cursor: "pointer",
                          fontSize: 12, padding: "3px 8px", borderRadius: 6,
                          border: "1px solid var(--line)",
                          background: sel[m.ID] ? "var(--accent-soft, #eef2ff)" : "transparent" }}>
            <input type="checkbox" checked={!!sel[m.ID]} onChange={() => toggle(m.ID)}/>
            {m.Surname} {m.GivenName} <span className="muted">({m.Type})</span>
          </label>
        ))}
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
        <MicButton onInsert={(t) => setDesc((d) => (d ? d + " " : "") + t)} title="Надиктовать (можно по-русски)"/>
        <button className="btn btn-xs" disabled={translating || !desc.trim()} onClick={translate}>
          {translating ? <><Spinner/> перевожу…</> : "Перевести на английский"}
        </button>
        <span className="muted" style={{ fontSize: 11 }}>EN напрямую или RU → «Перевести»</span>
      </div>
      <textarea rows={2} value={desc} onChange={(e) => setDesc(e.target.value)}
                placeholder="общее описание на все выбранные модели (на английском)"/>
      <div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
        <button className="btn btn-primary btn-sm" disabled={busy || !selectedIDs.length} onClick={generate}>
          {busy ? <><Spinner/> Генерирую…</> : `Сгенерировать (${selectedIDs.length})`}
        </button>
        {!selectedIDs.length && <span className="muted" style={{ fontSize: 12 }}>отметь хотя бы одну модель</span>}
      </div>
      {results && (
        <div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
          {results.map((r) => (
            <span key={r.ModelID} className={"pill " + (r.Error ? "warn" : "ok")} style={{ fontSize: 11 }}>
              <span className="pdot"/>{nameByID[r.ModelID] || ("модель " + r.ModelID)}
              {r.Error ? " — " + r.Error : " — " + r.OutputName}
            </span>
          ))}
        </div>
      )}
    </div>
  );
}

function ShootsScreen({ setRoute, me }) {
  const canRelease = !!(me && (me.isAdmin || (me.rights || []).includes("release.access")));
  const [shoots, reloadShoots] = useAsync(() => api.feed.shoots("false"), []);
  const [archiveCount] = useAsync(async () => {
    try { const a = await api.feed.shoots("true"); return Array.isArray(a) ? a.length : 0; }
    catch { return 0; }
  }, []);
  const [yandex] = useAsync(() => api.yandex.Status(), []);
  const [_, toast] = useToasts();

  // Тихий refresh списка без обнуления. useAsync.reload() ставит data=null →
  // список схлопывается в спиннер → страница прыгает вверх. После действий по
  // серии (Распознать / Анализ / Архив / Заново) обновляем именно так, оставаясь
  // на том же месте (правка 2026-06-08).
  const [liveShoots, setLiveShoots] = React.useState(null);
  React.useEffect(() => { setLiveShoots(null); }, [shoots.data]);
  const refreshShootsLive = React.useCallback(() => {
    api.feed.shoots("false").then((r) => { if (Array.isArray(r)) setLiveShoots(r); }).catch(() => {});
  }, []);

  const [typeFilter, setTypeFilter] = React.useState("all");
  const [stateFilter, setStateFilter] = React.useState("all"); // all | active | done
  const [sortBy, setSortBy] = React.useState("date");
  const [query, setQuery] = React.useState("");

  const list = (liveShoots || shoots.data || []).map((s) => ({ ...s, _info: stageInfo(s.Stage, s) }));

  const byType = (s) => {
    if (typeFilter === "all") return true;
    if (typeFilter === "editorial") return s.IsEditorial;
    return s.ContentType === typeFilter;
  };
  const byState = (s) => {
    if (stateFilter === "all")    return true;
    if (stateFilter === "done")   return s._info.state === "done";
    return s._info.state !== "done";
  };
  const byQuery = (s) => {
    if (!query) return true;
    const q = query.toLowerCase();
    return (s.Title || "").toLowerCase().includes(q);
  };

  const sortFn = (a, b) => {
    if (sortBy === "date")   return (a.CreatedAt < b.CreatedAt) ? 1 : -1;
    if (sortBy === "count")  return b.Photos - a.Photos;
    if (sortBy === "status") {
      // По этапам конвейера: распознавание → план → анализ → ревью1 → тексты →
      // ревью2 → csv → готово. _info.idx — позиция в STAGES (правка 2026-06-08,
      // раньше сортировало по абстрактному state, не по ходу конвейера).
      const d = (a._info.idx ?? 99) - (b._info.idx ?? 99);
      if (d !== 0) return d;
      return (a.CreatedAt < b.CreatedAt) ? 1 : -1; // в пределах этапа — новые выше
    }
    return 0;
  };

  const filtered = list.filter((s) => byType(s) && byState(s) && byQuery(s)).slice().sort(sortFn);

  const countByType = list.reduce((acc, s) => {
    acc[s.ContentType] = (acc[s.ContentType] || 0) + 1;
    if (s.IsEditorial) acc.editorial = (acc.editorial || 0) + 1;
    acc.all = (acc.all || 0) + 1;
    return acc;
  }, {});

  const types = [
    { id: "all",       label: "Все" },
    { id: "photo",     label: "Фото",      icon: "image" },
    { id: "video",     label: "Видео",     icon: "film" },
    { id: "vector",    label: "Vector",    icon: "layers" },
    { id: "editorial", label: "Editorial", icon: "edit" },
  ];

  // Действия по серии. На MVP без OpenClaw полноценный конвейер не работает —
  // показываем подсказку через title-tooltip, но кнопку оставляем кликабельной
  // (Recognize отвалится с понятной серверной ошибкой, если pipeline не настроен).
  const action = (s) => {
    const info = s._info;
    // Серия отправлена в очередь анализа и ждёт воркера (анализатор крутит
    // другую серию). Стадия ещё «Готов к анализу», поэтому отдельная метка —
    // иначе непонятно, что серия уже в очереди (правка 2026-06-22 по запросу Лены).
    if (s.AnalyseQueued) {
      return (
        <div style={{ display: "inline-flex", gap: 8, alignItems: "center" }}>
          <Spinner size={12}/> <span className="muted" style={{ fontSize: 12 }}>в очереди на анализ…</span>
        </div>
      );
    }
    if (info.state === "pending" && info.idx === 0) {
      return (
        <button className="btn btn-primary btn-sm"
                onClick={async () => {
                  try { await api.shoot.Recognize(s.ID); toast("Запущено распознавание", "info"); refreshShootsLive(); }
                  catch (e) { toast("Не удалось запустить: " + e.message, "error"); }
                }}>
          <Icon name="play" size={12} stroke={2}/> Распознать
        </button>
      );
    }
    if (info.state === "running") {
      // Для stage=csv серия уже отработала анализ и тексты — CSV выложен,
      // ждём watcher. Лена может зайти в Ревью 2 и **отредактировать** итог
      // (правка 2026-06-05 по запросу). Если внесёт правки — снова жмёт
      // «Принять серию», CSV перезаливается и watcher переписывает EXIF.
      if (info.idx === 6 /* csv */) {
        return (
          <div style={{ display: "inline-flex", gap: 8, alignItems: "center" }}>
            <Spinner /> <span className="muted" style={{ fontSize: 12 }}>EXIF пишется…</span>
            <button className="btn btn-accent btn-sm" onClick={() => setRoute("review2", s.ID)}>
              Ревью 2 →
            </button>
          </div>
        );
      }
      return <div style={{ display: "inline-flex", gap: 10, alignItems: "center" }}>
        <Spinner /> <span className="muted" style={{ fontSize: 12 }}>в работе…</span>
      </div>;
    }
    // Распознавание фактически готово (все фотки recognized), но анализ
    // ещё не запускали — Лена жмёт «Сделать анализ» прямо с главной.
    if (info.statusText === "Готов к анализу") {
      return (
        <div style={{ display: "inline-flex", gap: 6 }}>
          {/* Если в серии есть кадры с ошибкой — кнопка перераспознать их прямо
              из списка, перед анализом. Оранжевая, как надпись «N/M · ⚠ E»
              (правка 2026-06-08). */}
          {s.Errored > 0 && (
            <button className="btn btn-sm"
                    style={{ color: "var(--warn)", borderColor: "var(--warn)" }}
                    title={`${s.Errored} кадров с ошибкой — перераспознать (failed + битые)`}
                    onClick={async () => {
                      try {
                        const r = await api.feed.recognizeErrored(s.ID);
                        toast(r.photos > 0
                          ? `Перераспознаю с ошибкой: ${r.photos}`
                          : "Нет кадров с ошибкой для перезапуска", r.photos > 0 ? "info" : "warn");
                        refreshShootsLive();
                      } catch (e) { toast("Не удалось: " + e.message, "error"); }
                    }}>
              <Icon name="refresh" size={12} stroke={2}/> Распознать ошибочные{" "}
              (<span style={{ display: "inline-block", minWidth: "1.7em", textAlign: "center" }}>{s.Errored}</span>)
            </button>
          )}
          {/* Явная кнопка на этап «посмотреть/поправить распознавание» — открывает
              страницу серии. Без неё неочевидно, что для правок надо просто
              кликнуть по серии (правка 2026-06-22 по запросу Лены). */}
          <button className="btn btn-sm"
                  onClick={() => setRoute("series", s.ID)}
                  title="Открыть серию: посмотреть и поправить распознавание перед анализом">
            <Icon name="edit" size={12} stroke={2}/> Внести правки распознавания
          </button>
          <button className="btn btn-primary btn-sm"
                  onClick={async () => {
                    try { await api.shoot.RunPlanAndAnalyse(s.ID); toast("Анализ запущен", "info"); refreshShootsLive(); }
                    catch (e) { toast("Не удалось: " + e.message, "error"); }
                  }}>
            <Icon name="play" size={12} stroke={2}/> Сделать анализ
          </button>
        </div>
      );
    }
    if (info.state === "waiting" && info.idx <= 3) {
      return <button className="btn btn-accent btn-sm" onClick={() => setRoute("review1", s.ID)}>Ревью 1 →</button>;
    }
    if (info.state === "waiting") {
      return <button className="btn btn-accent btn-sm" onClick={() => setRoute("review2", s.ID)}>Ревью 2 →</button>;
    }
    if (info.state === "done") {
      // Готовая серия — кроме архивации даём открыть Ревью 2 для правок
      // (правка 2026-06-05 по запросу Лены). Если правки внесены и нажмёшь
      // «Принять серию» — CSV перезаливается, watcher переписывает EXIF.
      return (
        <div style={{ display: "inline-flex", gap: 6 }}>
          <button className="btn btn-accent btn-sm" onClick={() => setRoute("review2", s.ID)}>
            Ревью 2 →
          </button>
          <button className="btn btn-primary btn-sm"
                  onClick={async () => {
                    try { await api.shoot.Archive(s.ID); toast("В архиве", "info"); refreshShootsLive(); }
                    catch (e) { toast("Не удалось архивировать: " + e.message, "error"); }
                  }}>
            <Icon name="archive" size={12} stroke={2}/> В архив
          </button>
        </div>
      );
    }
    if (info.state === "failed") {
      // recognizeErrored (а не RecognizeFailed) — ловит и Stage=failed, и
      // «битые» recognized (текст-ошибка / нет GIST). Старый RecognizeFailed
      // на упавшей серии находил 0 фоток и молча ничего не делал — кнопка
      // «не срабатывала» (правка 2026-06-08).
      return (
        <button className="btn btn-sm"
                onClick={async () => {
                  try {
                    const r = await api.feed.recognizeErrored(s.ID);
                    toast(r.photos > 0
                      ? `Перераспознаю с ошибкой: ${r.photos}`
                      : "Нет фоток с ошибкой для перезапуска", r.photos > 0 ? "info" : "warn");
                    refreshShootsLive();
                  } catch (e) { toast("Не удалось: " + e.message, "error"); }
                }}>
          <Icon name="refresh" size={12} stroke={2}/> Заново
        </button>
      );
    }
    return null;
  };

  return (
    <div className="page">
      <div className="page-header">
        <div>
          <h1 className="page-title">Мои съёмки</h1>
          <div className="page-sub">
            {list.length} активных серий
            {archiveCount.data ? ` · ${archiveCount.data} в архиве` : ""}
            {" · рабочая папка "} <span className="mono">_Pikasrv/</span>
            {yandex.data && !yandex.data.connected && " · Я.Диск не подключён"}
          </div>
        </div>
        <div className="page-actions">
          <button className="btn" onClick={() => setRoute("archive")}>
            <Icon name="archive" size={13}/> Архив{archiveCount.data ? ` · ${archiveCount.data}` : ""}
          </button>
          <button className="btn btn-primary" onClick={() => setRoute("upload")}>
            <Icon name="image" size={13} stroke={2}/> Загрузить
          </button>
          {me && (me.isAdmin || me.ownYandex) && (
            <button className="btn" onClick={() => setRoute("ingest")}
                    disabled={!yandex.data || !yandex.data.connected}
                    title={!yandex.data || !yandex.data.connected ? "Сначала подключи Я.Диск в Настройках" : ""}>
              <Icon name="refresh" size={13} stroke={2}/> Обновить из Яндекса
            </button>
          )}
        </div>
      </div>

      {/* Type filter chips */}
      <div className="chip-row">
        {types.map((f) => (
          <button key={f.id} onClick={() => setTypeFilter(f.id)}
                  className={"chip" + (typeFilter === f.id ? " active" : "")}>
            {f.icon && <Icon name={f.icon} size={12} stroke={1.8}/>}
            <span>{f.label}</span>
            <span className="chip-count">{countByType[f.id] || 0}</span>
          </button>
        ))}
      </div>

      {/* State filter + sort + search */}
      <div className="row" style={{ marginBottom: 14, gap: 4, display: "flex", alignItems: "center" }}>
        {[
          { id: "all",    label: "Все статусы", n: list.length },
          { id: "active", label: "В работе",    n: list.filter((s) => s._info.state !== "done").length },
          { id: "done",   label: "Готовые",     n: list.filter((s) => s._info.state === "done").length },
        ].map((f) => (
          <button key={f.id} onClick={() => setStateFilter(f.id)}
                  className="btn btn-sm"
                  style={{
                    background: stateFilter === f.id ? "var(--primary, var(--accent))" : "transparent",
                    color: stateFilter === f.id ? "var(--bg)" : "var(--ink-2)",
                    border: stateFilter === f.id ? "1px solid var(--primary, var(--accent))" : "1px solid transparent",
                  }}>
            {f.label} <span className="mono" style={{ fontSize: 11, opacity: 0.7, marginLeft: 4 }}>{f.n}</span>
          </button>
        ))}
        <div className="spacer" style={{ flex: 1 }}/>
        <div style={{ display: "flex", alignItems: "center", gap: 6, color: "var(--ink-3)", fontSize: 12.5 }}>
          <label htmlFor="sort-by" className="muted" style={{ fontSize: 12 }}>Сортировка:</label>
          <select id="sort-by" value={sortBy} onChange={(e) => setSortBy(e.target.value)}
                  className="input" style={{ padding: "5px 24px 5px 10px", fontSize: 12.5, cursor: "pointer" }}>
            <option value="date">По дате</option>
            <option value="status">По статусу</option>
            <option value="count">По количеству фото</option>
          </select>
          <div style={{ width: 1, height: 22, background: "var(--line)", margin: "0 6px" }}/>
          <input className="input" placeholder="Поиск по серии…" value={query}
                 onChange={(e) => setQuery(e.target.value)}
                 style={{ padding: "5px 10px", fontSize: 12.5, width: 180 }}/>
        </div>
      </div>

      {shoots.loading && <div className="muted" style={{ padding: 24 }}><Spinner/> загружаем…</div>}
      {shoots.error && (
        <div className="muted" style={{ padding: 24, color: "var(--warn)" }}>
          Ошибка: {shoots.error.message}
        </div>
      )}

      {!shoots.loading && list.length === 0 && (
        <div style={{
          marginTop: 24, padding: "48px 24px", textAlign: "center",
          color: "var(--ink-3)", background: "var(--surface)",
          border: "1px dashed var(--line-2)", borderRadius: "var(--r-lg)",
        }}>
          <Icon name="folder" size={26} className="muted-2" style={{ marginBottom: 8 }}/>
          <div style={{ fontWeight: 500, fontSize: 14, color: "var(--ink-2)" }}>
            Серий пока нет
          </div>
          <div style={{ marginTop: 4, fontSize: 12.5 }}>
            {yandex.data && yandex.data.connected
              ? <>Закинь фото в <span className="mono">_Pikasrv/</span> и нажми «Обновить из Яндекса».</>
              : <>Сначала подключи Я.Диск в Настройках.</>}
          </div>
        </div>
      )}

      {filtered.length > 0 && (
        <div className="card">
          {filtered.map((s) => (
            <div key={s.ID} className="shoot-row-v3">
              <div className="top">
                <div className="shoot-thumb" style={{ cursor: "pointer" }}
                     onClick={() => setRoute("series", s.ID)}
                     title="Открыть серию">
                  {s.Cover
                    ? <img src={"/v1/preview?photoID=" + (s.CoverPhotoID || 0) + "&path=" + encodeURIComponent(s.Cover) + "&size=M"}
                           alt={s.Title || ""} loading="lazy"
                           style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }}/>
                    : <div style={{
                        width: "100%", height: "100%",
                        background: `hsl(${(s.ID * 37) % 360}, 25%, 80%)`,
                        display: "flex", alignItems: "center", justifyContent: "center",
                        fontSize: 11, color: "rgba(0,0,0,0.4)",
                      }}>{s.Title || `Серия ${s.ID}`}</div>}
                </div>
                <div className="shoot-meta" style={{ cursor: "pointer" }}
                     onClick={() => setRoute("series", s.ID)}>

                  <div className="shoot-title">
                    <span>{s.Title || `Серия ${s.ID}`}</span>
                    <KindPills medium={s.ContentType} editorial={s.IsEditorial}/>
                  </div>
                  <div className="shoot-sub">
                    <span className="mono">{new Date(s.CreatedAt).toLocaleString("ru-RU", {
                      day: "2-digit", month: "long", year: "numeric",
                      hour: "2-digit", minute: "2-digit",
                    })}</span>
                    <span className="sep">·</span>
                    <span>{s.Photos} файлов</span>
                    <span className="sep">·</span>
                    <ShootStatusPill info={s._info}/>
                    {/* Не вся серия распозналась чисто: есть кадры с ошибкой
                        (failed / битое). Показываем «чисто/всего · ⚠ N»
                        (правка 2026-06-08). */}
                    {s.Errored > 0 && (
                      <>
                        <span className="sep">·</span>
                        <span style={{ color: "var(--warn)", fontWeight: 500, whiteSpace: "nowrap" }}
                              title={`${s.Errored} кадров с ошибкой распознавания — открой серию: опиши вручную или перераспознай`}>
                          {s.RecognizedClean}/{s.Photos} · ⚠ {s.Errored}
                        </span>
                      </>
                    )}
                  </div>
                </div>
                <div className="shoot-actions">{action(s)}</div>
              </div>
              <div className="stages-row">
                <StagePipeline info={s._info}/>
              </div>
              {canRelease && shootDateISO(s.Title) && (
                <ShootReleases dateISO={shootDateISO(s.Title)} setRoute={setRoute}/>
              )}
            </div>
          ))}
        </div>
      )}

      {filtered.length === 0 && list.length > 0 && (
        <div style={{ padding: "40px 18px", textAlign: "center", color: "var(--ink-3)" }}>
          Серий с такими фильтрами нет.
        </div>
      )}
    </div>
  );
}

window.ShootsScreen = ShootsScreen;
window.STAGES = STAGES;
window.stageInfo = stageInfo;
