"use client";

import React, { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";

import "@/components/trade-in/trade-in.css";
import { Topbar } from "@/components/trade-in/Topbar";
import { apiFetch } from "@/lib/api";
import {
  formatTime,
  useScraperSettings,
  useUpdateScraperSetting,
  ResultPanel,
  type ScraperSource,
  type ScheduleSourceKey,
  type SweepParamConfig,
} from "@/app/scrapers/_components/ScraperPage";
import { SystemHealthSection } from "@/components/scrapers/ProxyHealthCard";
import { RunsTable } from "@/components/scrapers/RunsTable";
import { ScheduleControl } from "@/components/scrapers/ScheduleControl";
import { ProviderProxySection } from "@/components/scrapers/ProviderProxySection";
import { PacingSection } from "@/components/scrapers/PacingControl";
import { DataQualitySection } from "@/components/scrapers/DataQualitySection";

// ── Types (provider-specific responses) ───────────────────────────────────

interface ScrapeAroundResp {
  total_fetched: number;
  total_inserted: number;
  total_updated: number;
  by_source: Array<{
    source: string;
    fetched: number;
    inserted: number;
    updated: number;
  }>;
}

interface HouseTriggerResp {
  ok: boolean;
  house_url: string;
  counters: Record<string, number | string>;
}

interface DetailTriggerResp {
  ok: boolean;
  item_id: string;
  updated: boolean;
}

interface ImvResp {
  ok: boolean;
  cache_key: string;
  recommended_price: number;
  range: [number, number];
  market_count: number | null;
  evaluation_id: number;
  history_saved: number;
}

interface CianDetailTriggerResp {
  ok: boolean;
  offer_url: string;
  cian_id: number | null;
  price_changes_count: number;
  saved: boolean;
  listing_id: number | null;
}

interface CianNewbuildingTriggerResp {
  ok: boolean;
  zhk_url: string;
  cian_internal_house_id: number | null;
  name: string | null;
  price_dynamics_count: number;
  has_management_company: boolean;
  saved: boolean;
  house_id: number | null;
}

interface TestAuthResp {
  authenticated: boolean;
  userId: number | null;
  reason: string | null;
}

interface UploadCookiesResp {
  ok?: boolean;
  userId?: number;
  cookieCount?: number;
  detail?: string;
}

interface CookieEditorEntry {
  name: string;
  value: string | number | boolean;
  [key: string]: unknown;
}

interface YandexDetailTriggerResp {
  ok: boolean;
  offer_url: string;
  offer_id: string | null;
  price_rub: number | null;
  title: string | null;
  photo_count: number;
}

interface YandexNewbuildingTriggerResp {
  ok: boolean;
  ext_id: string;
  ext_slug: string;
  name: string | null;
  lat: number | null;
  lon: number | null;
  rating: number | null;
  ratings_count: number | null;
  text_reviews_count: number | null;
  developer_name: string | null;
}

interface YandexValuationTriggerResp {
  ok: boolean;
  address: string;
  year_built: number | null;
  total_floors: number | null;
  house_type: string | null;
  has_lift: boolean | null;
  total_objects: number | null;
  history_items_count: number;
}

// ── Backfill / full-load response types ───────────────────────────────────

interface CianBackfillHistoryResp {
  ok: boolean;
  listings_total: number;
  listings_processed: number;
  listings_succeeded: number;
  listings_failed_fetch: number;
  listings_failed_save: number;
  price_changes_attempted: number;
  houses_total: number;
  houses_processed: number;
  houses_succeeded: number;
  houses_failed_fetch: number;
  houses_failed_save: number;
  valuations_total: number;
  valuations_processed: number;
  valuations_succeeded: number;
  valuations_failed: number;
  duration_sec: number;
}

interface BackfillCountersResp {
  ok: boolean;
  checked: number;
  saved: number;
  skipped: number;
  errors: number;
  duration_sec: number;
}

interface HouseIMVBackfillResp {
  ok: boolean;
  checked: number;
  saved: number;
  skipped: number;
  errors: number;
  duration_sec: number;
  status_counts: Record<string, number>;
}

interface GeocodeStatusResp {
  total_pending_listings: number;
  unique_addresses_pending: number;
  estimated_nominatim_minutes: number;
  cache_size_active: number;
  per_source: Array<{
    source: string;
    total: number;
    pending_geocode: number;
    has_coords: number;
  }>;
}

interface GeocodeBackfillResp {
  status: string;
  batch_size: number;
  dry_run: boolean;
  addresses_total?: number;
  addresses_processed?: number;
  addresses_geocoded?: number;
  addresses_failed?: number;
  listings_updated?: number;
  cache_hits?: number;
  cache_misses?: number;
  duration_sec?: number;
  note?: string;
}

interface FullLoadStartResp {
  run_id: number;
  status: string;
  pages_per_anchor: number;
  detail_top_n: number;
}

interface YandexNBSweepStartResp {
  run_id: number;
  status: string;
  limit: number;
  detail: string;
}

// ── Cookie helpers (Cian) ──────────────────────────────────────────────────

function convertCookiesFormat(input: string): Record<string, string> {
  const parsed: unknown = JSON.parse(input.trim());
  if (Array.isArray(parsed)) {
    return (parsed as CookieEditorEntry[]).reduce<Record<string, string>>(
      (acc, c) => {
        if (
          typeof c === "object" &&
          c !== null &&
          "name" in c &&
          "value" in c
        ) {
          acc[String(c.name)] = String(c.value);
        }
        return acc;
      },
      {},
    );
  }
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
    return Object.fromEntries(
      Object.entries(parsed as Record<string, unknown>).map(([k, v]) => [
        k,
        String(v),
      ]),
    );
  }
  throw new Error("Expected JSON array or plain object");
}

function parsePreview(raw: string): {
  cookies: Record<string, string> | null;
  error: string | null;
} {
  if (!raw.trim()) return { cookies: null, error: null };
  try {
    return { cookies: convertCookiesFormat(raw), error: null };
  } catch (e) {
    return {
      cookies: null,
      error: e instanceof Error ? e.message : String(e),
    };
  }
}

// ── Section 2: Глобально ──────────────────────────────────────────────────

function GlobalSection() {
  const settingsQ = useScraperSettings();
  const updateMut = useUpdateScraperSetting();

  const globalSetting = settingsQ.data?.find((s) => s.source === "global");
  const [delayInput, setDelayInput] = useState("0");
  const [initialized, setInitialized] = useState(false);

  if (globalSetting && !initialized) {
    setDelayInput(String(globalSetting.request_delay_sec));
    setInitialized(true);
  }

  return (
    <section className="scraper-section scraper-section--highlight">
      <h2>Глобально</h2>
      <p className="scraper-hint">
        Global delay — нижний порог для всех скрапперов. Изменение вступает
        в силу немедленно.
      </p>

      {settingsQ.isPending && (
        <p className="scraper-hint">Загрузка настроек…</p>
      )}
      {settingsQ.isError && (
        <p className="scraper-result scraper-result--error">
          Ошибка загрузки: {settingsQ.error.message}
        </p>
      )}

      {globalSetting && (
        <div className="schedule-status" style={{ marginBottom: 12 }}>
          <div className="schedule-status__row">
            <span className="schedule-status__label">
              Текущий global delay:
            </span>
            <span>
              <strong>{globalSetting.request_delay_sec} сек</strong>
            </span>
          </div>
          {globalSetting.updated_at && (
            <div className="schedule-status__row">
              <span className="schedule-status__label">Обновлено:</span>
              <span>{formatTime(globalSetting.updated_at)}</span>
            </div>
          )}
        </div>
      )}

      <form
        className="scraper-form"
        onSubmit={(e) => {
          e.preventDefault();
          updateMut.mutate({
            source: "global",
            request_delay_sec: parseFloat(delayInput),
          });
        }}
      >
        <label>
          Global delay (сек, 0–30)
          <input
            type="number"
            step={0.5}
            min={0}
            max={30}
            value={delayInput}
            onChange={(e) => setDelayInput(e.target.value)}
          />
        </label>
        <input
          type="range"
          min={0}
          max={30}
          step={0.5}
          value={delayInput}
          onChange={(e) => setDelayInput(e.target.value)}
          style={{ width: "100%", gridColumn: "1 / -1" }}
        />
        <button
          type="submit"
          disabled={updateMut.isPending}
          style={{ gridColumn: "1 / -1" }}
        >
          {updateMut.isPending ? "Сохраняем…" : "Сохранить global delay"}
        </button>
      </form>

      {updateMut.isError && (
        <div className="scraper-result scraper-result--error">
          Ошибка: {updateMut.error.message}
        </div>
      )}
      {updateMut.isSuccess && updateMut.data && (
        <div className="scraper-result">
          Global delay обновлён:{" "}
          <strong>
            {Number(updateMut.data.request_delay_sec).toFixed(1)} сек
          </strong>{" "}
          (обновлено {formatTime(updateMut.data.updated_at)})
        </div>
      )}

      {/* Per-source delay table (read-only summary) */}
      {settingsQ.data && settingsQ.data.length > 0 && (
        <div style={{ marginTop: 20 }}>
          <p
            style={{
              fontSize: "0.85rem",
              fontWeight: 600,
              marginBottom: 8,
            }}
          >
            Per-source задержки:
          </p>
          <table className="runs-table">
            <thead>
              <tr>
                <th>Source</th>
                <th>Delay (сек)</th>
                <th>Описание</th>
                <th>Обновлено</th>
              </tr>
            </thead>
            <tbody>
              {settingsQ.data.map((s) => (
                <tr
                  key={s.source}
                  className={
                    s.source === "global"
                      ? "run-row run-row--running"
                      : "run-row"
                  }
                >
                  <td>
                    <code>{s.source}</code>
                  </td>
                  <td>{s.request_delay_sec.toFixed(1)}</td>
                  <td
                    style={{
                      fontSize: "0.8rem",
                      color: "var(--fg-secondary, #5b6066)",
                    }}
                  >
                    {s.description ?? "—"}
                  </td>
                  <td style={{ fontSize: "0.8rem" }}>
                    {formatTime(s.updated_at)}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </section>
  );
}

// ── Collapsible wrapper ────────────────────────────────────────────────────

interface CollapsibleProps {
  title: string;
  defaultOpen?: boolean;
  children: React.ReactNode;
}

function Collapsible({ title, defaultOpen = false, children }: CollapsibleProps) {
  const [open, setOpen] = useState(defaultOpen);
  return (
    <div style={{ marginTop: 8 }}>
      <button
        type="button"
        onClick={() => setOpen((v) => !v)}
        style={{
          background: "none",
          border: "none",
          padding: "8px 0",
          cursor: "pointer",
          fontSize: "0.9rem",
          fontWeight: 600,
          color: "var(--accent, #1d4ed8)",
          display: "flex",
          alignItems: "center",
          gap: 6,
        }}
      >
        <span>{open ? "▼" : "▶"}</span>
        {title}
      </button>
      {open && <div>{children}</div>}
    </div>
  );
}

// ── Avito advanced triggers ────────────────────────────────────────────────

function AvitoAdvancedTriggers() {
  const [lat, setLat] = useState("56.8400");
  const [lon, setLon] = useState("60.6050");
  const [radius, setRadius] = useState("1500");
  const aroundMut = useMutation<
    ScrapeAroundResp,
    Error,
    { lat: number; lon: number; radius_m: number }
  >({
    mutationFn: (input) =>
      apiFetch<ScrapeAroundResp>("/api/v1/admin/scrape", {
        method: "POST",
        body: JSON.stringify({ ...input, sources: ["avito"] }),
      }),
  });

  const [houseUrl, setHouseUrl] = useState(
    "/catalog/houses/ekaterinburg/ul_akademika_postovskogo_17a/3171365",
  );
  const houseMut = useMutation<HouseTriggerResp, Error, { house_url: string }>({
    mutationFn: ({ house_url }) =>
      apiFetch<HouseTriggerResp>(
        `/api/v1/admin/scrape/avito-house?house_url=${encodeURIComponent(house_url)}`,
        { method: "POST" },
      ),
  });

  const [itemUrl, setItemUrl] = useState(
    "/ekaterinburg/kvartiry/3-k._kvartira_75_m_2026_et._7986882804",
  );
  const detailMut = useMutation<DetailTriggerResp, Error, { item_url: string }>({
    mutationFn: ({ item_url }) =>
      apiFetch<DetailTriggerResp>(
        `/api/v1/admin/scrape/avito-detail?item_url=${encodeURIComponent(item_url)}`,
        { method: "POST" },
      ),
  });

  const [imvAddress, setImvAddress] = useState(
    "Свердловская обл., Екатеринбург, ул. Чайковского, 66А",
  );
  const [imvRooms, setImvRooms] = useState("2");
  const [imvArea, setImvArea] = useState("42");
  const [imvFloor, setImvFloor] = useState("4");
  const [imvTotalFloors, setImvTotalFloors] = useState("5");
  const [imvHouseType, setImvHouseType] = useState("panel");
  const [imvRenovation, setImvRenovation] = useState("cosmetic");
  const [imvBalcony, setImvBalcony] = useState(true);
  const [imvLoggia, setImvLoggia] = useState(false);
  const imvMut = useMutation<
    ImvResp,
    Error,
    {
      address: string;
      rooms: number;
      area_m2: number;
      floor: number;
      floor_at_home: number;
      house_type: string;
      renovation_type: string;
      has_balcony: boolean;
      has_loggia: boolean;
    }
  >({
    mutationFn: (input) => {
      const qs = new URLSearchParams({
        address: input.address,
        rooms: String(input.rooms),
        area_m2: String(input.area_m2),
        floor: String(input.floor),
        floor_at_home: String(input.floor_at_home),
        house_type: input.house_type,
        renovation_type: input.renovation_type,
        has_balcony: String(input.has_balcony),
        has_loggia: String(input.has_loggia),
      });
      return apiFetch<ImvResp>(
        `/api/v1/admin/scrape/avito-imv?${qs.toString()}`,
        { method: "POST" },
      );
    },
  });

  return (
    <>
      <div className="scraper-section">
        <h3>1. Search around</h3>
        <form
          className="scraper-form"
          onSubmit={(e) => {
            e.preventDefault();
            aroundMut.mutate({
              lat: parseFloat(lat),
              lon: parseFloat(lon),
              radius_m: parseInt(radius, 10),
            });
          }}
        >
          <label>
            Lat
            <input value={lat} onChange={(e) => setLat(e.target.value)} required />
          </label>
          <label>
            Lon
            <input value={lon} onChange={(e) => setLon(e.target.value)} required />
          </label>
          <label>
            Radius (м)
            <input value={radius} onChange={(e) => setRadius(e.target.value)} required />
          </label>
          <button type="submit" disabled={aroundMut.isPending}>
            {aroundMut.isPending ? "Скрейпим…" : "Запустить"}
          </button>
        </form>
        <ResultPanel mut={aroundMut} />
      </div>

      <div className="scraper-section">
        <h3>2. Houses Catalog enrichment</h3>
        <form
          className="scraper-form"
          onSubmit={(e) => {
            e.preventDefault();
            houseMut.mutate({ house_url: houseUrl });
          }}
        >
          <label className="full">
            house_url
            <input
              value={houseUrl}
              onChange={(e) => setHouseUrl(e.target.value)}
              required
              placeholder="/catalog/houses/<city>/<slug>/<id>"
            />
          </label>
          <button type="submit" disabled={houseMut.isPending}>
            {houseMut.isPending ? "Парсим…" : "Запустить"}
          </button>
        </form>
        <ResultPanel mut={houseMut} />
      </div>

      <div className="scraper-section">
        <h3>3. Detail page enrichment</h3>
        <form
          className="scraper-form"
          onSubmit={(e) => {
            e.preventDefault();
            detailMut.mutate({ item_url: itemUrl });
          }}
        >
          <label className="full">
            item_url
            <input
              value={itemUrl}
              onChange={(e) => setItemUrl(e.target.value)}
              required
              placeholder="/ekaterinburg/kvartiry/..."
            />
          </label>
          <button type="submit" disabled={detailMut.isPending}>
            {detailMut.isPending ? "Парсим…" : "Запустить"}
          </button>
        </form>
        <ResultPanel mut={detailMut} />
      </div>

      <div className="scraper-section">
        <h3>4. IMV evaluation (debug)</h3>
        <form
          className="scraper-form"
          onSubmit={(e) => {
            e.preventDefault();
            imvMut.mutate({
              address: imvAddress,
              rooms: parseInt(imvRooms, 10),
              area_m2: parseFloat(imvArea),
              floor: parseInt(imvFloor, 10),
              floor_at_home: parseInt(imvTotalFloors, 10),
              house_type: imvHouseType,
              renovation_type: imvRenovation,
              has_balcony: imvBalcony,
              has_loggia: imvLoggia,
            });
          }}
        >
          <label className="full">
            Адрес
            <input value={imvAddress} onChange={(e) => setImvAddress(e.target.value)} required />
          </label>
          <label>
            Комн.
            <input value={imvRooms} onChange={(e) => setImvRooms(e.target.value)} required />
          </label>
          <label>
            Площадь (м²)
            <input value={imvArea} onChange={(e) => setImvArea(e.target.value)} required />
          </label>
          <label>
            Этаж
            <input value={imvFloor} onChange={(e) => setImvFloor(e.target.value)} required />
          </label>
          <label>
            Этажей в доме
            <input value={imvTotalFloors} onChange={(e) => setImvTotalFloors(e.target.value)} required />
          </label>
          <label>
            Тип дома
            <select value={imvHouseType} onChange={(e) => setImvHouseType(e.target.value)}>
              <option value="panel">панель</option>
              <option value="brick">кирпич</option>
              <option value="monolith">монолит</option>
              <option value="monolith_brick">монолит-кирпич</option>
              <option value="block">блочный</option>
              <option value="wood">дерево</option>
            </select>
          </label>
          <label>
            Ремонт
            <select value={imvRenovation} onChange={(e) => setImvRenovation(e.target.value)}>
              <option value="required">требуется</option>
              <option value="cosmetic">косметический</option>
              <option value="euro">евро</option>
              <option value="designer">дизайнерский</option>
            </select>
          </label>
          <label className="checkbox">
            <input type="checkbox" checked={imvBalcony} onChange={(e) => setImvBalcony(e.target.checked)} />
            Балкон
          </label>
          <label className="checkbox">
            <input type="checkbox" checked={imvLoggia} onChange={(e) => setImvLoggia(e.target.checked)} />
            Лоджия
          </label>
          <button type="submit" disabled={imvMut.isPending}>
            {imvMut.isPending ? "Запрашиваем Avito…" : "Запустить IMV"}
          </button>
        </form>
        <ResultPanel mut={imvMut} />
      </div>
    </>
  );
}

// ── Cian Auto-Login ────────────────────────────────────────────────────────

interface AutoLoginResp {
  ok: boolean;
  userId?: number;
  cookieCount?: number;
}

function CianAutoLoginButton() {
  const qc = useQueryClient();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [expanded, setExpanded] = useState(false);

  const autoLoginMut = useMutation<AutoLoginResp, Error, { email: string; password: string } | null>({
    mutationFn: (body) =>
      apiFetch<AutoLoginResp>("/api/v1/admin/scrape/cian/auto-login", {
        method: "POST",
        body: body && (body.email || body.password) ? JSON.stringify(body) : undefined,
      }),
    onSuccess: () => {
      void qc.invalidateQueries({ queryKey: ["cian-test-auth"] });
    },
  });

  return (
    <div style={{ marginTop: 16, borderTop: "1px solid var(--border-soft, #eef0f3)", paddingTop: 16 }}>
      <p style={{ fontSize: "0.85rem", color: "var(--fg-secondary, #5b6066)", marginBottom: 8 }}>
        <strong>Авто-логин</strong> — браузерный логин через camoufox (требует настроенных{" "}
        <code>CIAN_LOGIN_EMAIL</code>/<code>PASSWORD</code> в ENV или ввода ниже).
      </p>
      <button
        type="button"
        onClick={() => setExpanded((v) => !v)}
        style={{
          background: "none",
          border: "none",
          padding: "4px 0",
          cursor: "pointer",
          fontSize: 13,
          fontWeight: 600,
          color: "var(--accent, #1d4ed8)",
          display: "flex",
          alignItems: "center",
          gap: 6,
        }}
      >
        <span>{expanded ? "▼" : "▶"}</span>
        Переопределить credentials (опционально)
      </button>
      {expanded && (
        <div className="scraper-form" style={{ marginTop: 8 }}>
          <label>
            Email
            <input
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              placeholder="admin@example.com"
            />
          </label>
          <label>
            Password
            <input
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              placeholder="из ENV по умолчанию"
            />
          </label>
        </div>
      )}
      <div style={{ marginTop: 10 }}>
        <button
          type="button"
          disabled={autoLoginMut.isPending}
          onClick={() =>
            autoLoginMut.mutate(
              email || password ? { email, password } : null,
            )
          }
        >
          {autoLoginMut.isPending ? "Логинимся…" : "Авто-логин Cian"}
        </button>
      </div>
      {autoLoginMut.isSuccess && (
        <div
          className="scraper-result"
          style={{
            marginTop: 8,
            background: autoLoginMut.data.ok
              ? "var(--success-soft, #f0fdf4)"
              : "var(--danger-soft, #fef2f2)",
            color: autoLoginMut.data.ok
              ? "var(--success, #0a7a3a)"
              : "var(--danger, #b3261e)",
          }}
        >
          {autoLoginMut.data.ok ? (
            <>
              <strong>Залогинились.</strong> userId={autoLoginMut.data.userId ?? "—"} ·
              cookieCount={autoLoginMut.data.cookieCount ?? "—"}
            </>
          ) : (
            <strong>Авто-логин не удался.</strong>
          )}
        </div>
      )}
      {autoLoginMut.isError && (
        <div className="scraper-result scraper-result--error" style={{ marginTop: 8 }}>
          <strong>Ошибка:</strong> {autoLoginMut.error.message}
        </div>
      )}
    </div>
  );
}

// ── Cian Cookies section ───────────────────────────────────────────────────

function CianCookiesSection() {
  const [rawInput, setRawInput] = useState("");
  const [showInstructions, setShowInstructions] = useState(false);

  const preview = parsePreview(rawInput);
  const previewKeys = preview.cookies ? Object.keys(preview.cookies) : [];

  const uploadMutation = useMutation({
    mutationFn: () => {
      if (!preview.cookies) {
        throw new Error("Invalid JSON — fix the input first");
      }
      return apiFetch<UploadCookiesResp>(
        "/api/v1/admin/scrape/cian/upload-cookies",
        {
          method: "POST",
          body: JSON.stringify(preview.cookies),
        },
      );
    },
    onSuccess: () => setRawInput(""),
  });

  const testAuthQuery = useQuery({
    queryKey: ["cian-test-auth"],
    queryFn: () =>
      apiFetch<TestAuthResp>("/api/v1/admin/scrape/cian/test-auth"),
    enabled: false,
    retry: false,
  });

  const canUpload =
    !!preview.cookies && previewKeys.length > 0 && !uploadMutation.isPending;

  return (
    <section className="scraper-section">
      <h2>Cookies (DMIR_AUTH)</h2>
      <p className="scraper-hint">
        Обновление сессии Cian для скрапера оценщика без перезапуска backend.
      </p>

      <button
        type="button"
        onClick={() => setShowInstructions((v) => !v)}
        style={{
          background: "none",
          border: "none",
          padding: "4px 0",
          cursor: "pointer",
          fontSize: 14,
          fontWeight: 600,
          color: "var(--accent, #1d4ed8)",
          display: "flex",
          alignItems: "center",
          gap: 6,
          marginBottom: showInstructions ? 0 : 12,
        }}
      >
        <span>{showInstructions ? "▼" : "▶"}</span>
        Как получить cookies (инструкция)
      </button>
      {showInstructions && (
        <ol
          style={{
            marginTop: 12,
            marginBottom: 12,
            paddingLeft: 20,
            fontSize: 13,
            lineHeight: 1.7,
          }}
        >
          <li>
            Открой{" "}
            <a href="https://www.cian.ru/" target="_blank" rel="noopener noreferrer">
              cian.ru
            </a>{" "}
            и войди под нужным аккаунтом.
          </li>
          <li>
            Установи расширение <strong>Cookie-Editor</strong> (Chrome/Firefox)
            или DevTools → Application → Cookies → www.cian.ru.
          </li>
          <li>
            <strong>Cookie-Editor:</strong> Export → Export as JSON → скопируй массив.
          </li>
          <li>Вставь JSON в поле ниже — формат определится автоматически.</li>
          <li>Убедись что preview показывает нужные ключи, нажми Upload.</li>
        </ol>
      )}

      <div className="scraper-form">
        <label className="full">
          <textarea
            value={rawInput}
            onChange={(e) => {
              setRawInput(e.target.value);
              uploadMutation.reset();
            }}
            rows={8}
            placeholder={`Формат 1 — Cookie-Editor array:\n[{"name": "session_key", "value": "abc123", "domain": ".cian.ru"}]\n\nФормат 2 — dict:\n{"session_key": "abc123"}`}
            style={{
              fontFamily: "ui-monospace, monospace",
              fontSize: 12,
              resize: "vertical",
              minHeight: 160,
              padding: "8px 10px",
              border: "1px solid var(--border-strong, #d1d5db)",
              borderRadius: 6,
              background: "#fff",
              width: "100%",
            }}
          />
        </label>

        {rawInput.trim() && (
          <div className={`scraper-result${preview.error ? " scraper-result--error" : ""}`}>
            {preview.error ? (
              <span>JSON error: {preview.error}</span>
            ) : (
              <span style={{ color: "var(--success, #0a7a3a)" }}>
                Detected <strong>{previewKeys.length} cookies</strong>:{" "}
                {previewKeys.slice(0, 8).join(", ")}
                {previewKeys.length > 8 ? ` … +${previewKeys.length - 8} more` : ""}
              </span>
            )}
          </div>
        )}

        <button type="button" onClick={() => uploadMutation.mutate()} disabled={!canUpload}>
          {uploadMutation.isPending ? "Uploading…" : "Upload Cookies"}
        </button>

        <button
          type="button"
          onClick={() => {
            void testAuthQuery.refetch();
          }}
          disabled={testAuthQuery.isFetching}
          style={{
            background: "var(--bg-card-alt, #f3f4f6)",
            color: "var(--fg-primary, #1f2937)",
            border: "1px solid var(--border-strong, #d1d5db)",
          }}
        >
          {testAuthQuery.isFetching ? "Checking…" : "Test Current Session"}
        </button>
      </div>

      {uploadMutation.isSuccess && (
        <div
          className="scraper-result"
          style={{
            background: uploadMutation.data.ok
              ? "var(--success-soft, #f0fdf4)"
              : "var(--danger-soft, #fef2f2)",
            borderColor: uploadMutation.data.ok
              ? "var(--success, #0a7a3a)"
              : "var(--danger, #b3261e)",
            color: uploadMutation.data.ok
              ? "var(--success, #0a7a3a)"
              : "var(--danger, #b3261e)",
          }}
        >
          {uploadMutation.data.ok ? (
            <>
              <strong>Cookies uploaded.</strong> userId={" "}
              {uploadMutation.data.userId ?? "—"} · cookieCount={" "}
              {uploadMutation.data.cookieCount ?? previewKeys.length}
            </>
          ) : (
            <>
              <strong>Upload failed:</strong>{" "}
              {uploadMutation.data.detail ?? "Unknown error"}
            </>
          )}
        </div>
      )}
      {uploadMutation.isError && (
        <div className="scraper-result scraper-result--error">
          <strong>Error:</strong> {String(uploadMutation.error)}
        </div>
      )}
      {(testAuthQuery.isSuccess || testAuthQuery.isError) && (
        <div
          className="scraper-result"
          style={{
            background:
              testAuthQuery.isSuccess && testAuthQuery.data.authenticated
                ? "var(--success-soft, #f0fdf4)"
                : "var(--danger-soft, #fef2f2)",
            borderColor:
              testAuthQuery.isSuccess && testAuthQuery.data.authenticated
                ? "var(--success, #0a7a3a)"
                : "var(--danger, #b3261e)",
            color:
              testAuthQuery.isSuccess && testAuthQuery.data.authenticated
                ? "var(--success, #0a7a3a)"
                : "var(--danger, #b3261e)",
          }}
        >
          {testAuthQuery.isError ? (
            <>
              <strong>Test auth error:</strong> {String(testAuthQuery.error)}
            </>
          ) : testAuthQuery.data.authenticated ? (
            <>
              <strong>Authenticated.</strong> userId={" "}
              {testAuthQuery.data.userId ?? "—"}
            </>
          ) : (
            <>
              <strong>Not authenticated.</strong>{" "}
              {testAuthQuery.data.reason
                ? `Reason: ${testAuthQuery.data.reason}`
                : "Session expired or cookies invalid."}
            </>
          )}
        </div>
      )}

      {/* Auto-login (Variant B) */}
      <CianAutoLoginButton />
    </section>
  );
}

// ── Cian advanced triggers ─────────────────────────────────────────────────

function CianAdvancedTriggers() {
  const [lat, setLat] = useState("56.8400");
  const [lon, setLon] = useState("60.6050");
  const [radius, setRadius] = useState("1500");
  const [multiRoom, setMultiRoom] = useState(false);
  const aroundMut = useMutation<
    ScrapeAroundResp,
    Error,
    { lat: number; lon: number; radius_m: number; multi_room_cian: boolean }
  >({
    mutationFn: (input) =>
      apiFetch<ScrapeAroundResp>("/api/v1/admin/scrape", {
        method: "POST",
        body: JSON.stringify({ ...input, sources: ["cian"] }),
      }),
  });

  const [detailUrl, setDetailUrl] = useState("");
  const [detailListingId, setDetailListingId] = useState("");
  const detailMut = useMutation<
    CianDetailTriggerResp,
    Error,
    { offer_url: string; listing_id: number | undefined }
  >({
    mutationFn: ({ offer_url, listing_id }) => {
      const qs = new URLSearchParams({ offer_url });
      if (listing_id !== undefined) qs.set("listing_id", String(listing_id));
      return apiFetch<CianDetailTriggerResp>(
        `/api/v1/admin/scrape/cian-detail?${qs.toString()}`,
        { method: "POST" },
      );
    },
  });

  const [nbZhkUrl, setNbZhkUrl] = useState("");
  const [nbHouseId, setNbHouseId] = useState("");
  const nbMut = useMutation<
    CianNewbuildingTriggerResp,
    Error,
    { zhk_url: string; house_id: number | undefined }
  >({
    mutationFn: ({ zhk_url, house_id }) => {
      const qs = new URLSearchParams({ zhk_url });
      if (house_id !== undefined) qs.set("house_id", String(house_id));
      return apiFetch<CianNewbuildingTriggerResp>(
        `/api/v1/admin/scrape/cian-newbuilding?${qs.toString()}`,
        { method: "POST" },
      );
    },
  });

  const authQ = useQuery<TestAuthResp>({
    queryKey: ["cian-test-auth"],
    queryFn: () => apiFetch<TestAuthResp>("/api/v1/admin/scrape/cian/test-auth"),
    staleTime: 30_000,
    refetchInterval: 60_000,
  });

  return (
    <>
      <div
        className="scraper-section"
        style={{
          background: "var(--accent-soft, #dbeafe)",
          borderLeft: "4px solid var(--accent, #1d4ed8)",
          padding: "12px 16px",
        }}
      >
        <p style={{ margin: 0, fontSize: "0.9rem" }}>
          <strong>Cian Valuation</strong> требует загруженных cookies. Управление — в разделе «Cookies» выше.
        </p>
        <p style={{ margin: "6px 0 0", fontSize: "0.85rem", color: "var(--fg-secondary, #5b6066)" }}>
          Статус сессии:{" "}
          {authQ.isPending && "проверяем…"}
          {authQ.isError && <span style={{ color: "var(--danger, #b3261e)" }}>ошибка проверки</span>}
          {authQ.data && (
            <span
              style={{
                color: authQ.data.authenticated
                  ? "var(--success, #0a7a3a)"
                  : "var(--danger, #b3261e)",
                fontWeight: 600,
              }}
            >
              {authQ.data.authenticated
                ? `авторизован (userId: ${authQ.data.userId ?? "—"})`
                : `не авторизован — ${authQ.data.reason ?? "неизвестная причина"}`}
            </span>
          )}
        </p>
      </div>

      <div className="scraper-section">
        <h3>1. Search around</h3>
        <form
          className="scraper-form"
          onSubmit={(e) => {
            e.preventDefault();
            aroundMut.mutate({
              lat: parseFloat(lat),
              lon: parseFloat(lon),
              radius_m: parseInt(radius, 10),
              multi_room_cian: multiRoom,
            });
          }}
        >
          <label>
            Lat
            <input value={lat} onChange={(e) => setLat(e.target.value)} required />
          </label>
          <label>
            Lon
            <input value={lon} onChange={(e) => setLon(e.target.value)} required />
          </label>
          <label>
            Radius (м)
            <input value={radius} onChange={(e) => setRadius(e.target.value)} required />
          </label>
          <label className="checkbox">
            <input type="checkbox" checked={multiRoom} onChange={(e) => setMultiRoom(e.target.checked)} />
            multi_room_cian (4 запроса по комнатам)
          </label>
          <button type="submit" disabled={aroundMut.isPending}>
            {aroundMut.isPending ? "Скрейпим…" : "Запустить"}
          </button>
        </form>
        <ResultPanel mut={aroundMut} />
      </div>

      <div className="scraper-section">
        <h3>2. Detail page enrichment</h3>
        <form
          className="scraper-form"
          onSubmit={(e) => {
            e.preventDefault();
            detailMut.mutate({
              offer_url: detailUrl,
              listing_id: detailListingId !== "" ? parseInt(detailListingId, 10) : undefined,
            });
          }}
        >
          <label className="full">
            offer_url
            <input
              type="url"
              value={detailUrl}
              onChange={(e) => setDetailUrl(e.target.value)}
              required
              placeholder="https://ekb.cian.ru/sale/flat/XXXXX/"
            />
          </label>
          <label>
            listing_id (опционально)
            <input
              type="number"
              value={detailListingId}
              onChange={(e) => setDetailListingId(e.target.value)}
              placeholder="оставьте пустым для debug"
            />
          </label>
          <button type="submit" disabled={detailMut.isPending}>
            {detailMut.isPending ? "Парсим…" : "Запустить"}
          </button>
        </form>
        <ResultPanel mut={detailMut} />
      </div>

      <div className="scraper-section">
        <h3>3. Новостройки (ЖК)</h3>
        <form
          className="scraper-form"
          onSubmit={(e) => {
            e.preventDefault();
            nbMut.mutate({
              zhk_url: nbZhkUrl,
              house_id: nbHouseId !== "" ? parseInt(nbHouseId, 10) : undefined,
            });
          }}
        >
          <label className="full">
            zhk_url
            <input
              type="url"
              value={nbZhkUrl}
              onChange={(e) => setNbZhkUrl(e.target.value)}
              required
              placeholder="https://zhk-XXX-ekb-i.cian.ru/"
            />
          </label>
          <label>
            house_id (опционально)
            <input
              type="number"
              value={nbHouseId}
              onChange={(e) => setNbHouseId(e.target.value)}
              placeholder="оставьте пустым для debug"
            />
          </label>
          <button type="submit" disabled={nbMut.isPending}>
            {nbMut.isPending ? "Парсим…" : "Запустить"}
          </button>
        </form>
        <ResultPanel mut={nbMut} />
      </div>

      <div className="scraper-section">
        <h3>4. Valuation Calculator</h3>
        <p className="scraper-hint">
          Вызывается автоматически из /estimate flow (Stage 9). Admin trigger будет добавлен позже.
        </p>
      </div>

      <CianBackfillSection />
    </>
  );
}

// ── Cian Backfill section ─────────────────────────────────────────────────

function CianBackfillSection() {
  // ── cian-backfill-history ──────────────────────────────────────────────
  const [bfBatchSize, setBfBatchSize] = useState("50");
  const [bfDoListings, setBfDoListings] = useState(true);
  const [bfDoHouses, setBfDoHouses] = useState(true);
  const [bfDoValuations, setBfDoValuations] = useState(false);
  const [bfDryRun, setBfDryRun] = useState(false);

  const backfillHistoryMut = useMutation<CianBackfillHistoryResp, Error>({
    mutationFn: () => {
      const qs = new URLSearchParams({
        batch_size: bfBatchSize,
        do_listings: String(bfDoListings),
        do_houses: String(bfDoHouses),
        do_valuations: String(bfDoValuations),
        dry_run: String(bfDryRun),
      });
      return apiFetch<CianBackfillHistoryResp>(
        `/api/v1/admin/scrape/cian-backfill-history?${qs.toString()}`,
        { method: "POST" },
      );
    },
  });

  // ── cian-price-history ─────────────────────────────────────────────────
  const [phBatchSize, setPhBatchSize] = useState("50");
  const [phListingId, setPhListingId] = useState("");

  const priceHistoryMut = useMutation<BackfillCountersResp, Error>({
    mutationFn: () =>
      apiFetch<BackfillCountersResp>("/api/v1/admin/scrape/cian-price-history", {
        method: "POST",
        body: JSON.stringify({
          batch_size: parseInt(phBatchSize, 10),
          listing_id: phListingId !== "" ? parseInt(phListingId, 10) : null,
        }),
      }),
  });

  // ── cian-full-load ─────────────────────────────────────────────────────
  const [flDelay, setFlDelay] = useState("1.5");
  const [flConcurrency, setFlConcurrency] = useState("5");
  const [flPriceCap, setFlPriceCap] = useState("1400");
  const [flResumeRunId, setFlResumeRunId] = useState("");

  const fullLoadMut = useMutation<FullLoadStartResp, Error>({
    mutationFn: () =>
      apiFetch<FullLoadStartResp>("/api/v1/admin/scrape/cian-full-load", {
        method: "POST",
        body: JSON.stringify({
          price_cap_per_bucket: parseInt(flPriceCap, 10),
          request_delay_sec: parseFloat(flDelay),
          concurrency: parseInt(flConcurrency, 10),
          enrich_detail: false,
          detail_top_n: 0,
          resume_run_id: flResumeRunId !== "" ? parseInt(flResumeRunId, 10) : null,
        }),
      }),
  });

  return (
    <>
      <div className="scraper-section">
        <h3>5. Backfill — история цен (detail)</h3>
        <p className="scraper-hint">
          Дозаполняет offer_price_history / houses_price_dynamics для листингов и ЖК без истории.
        </p>
        <form
          className="scraper-form"
          onSubmit={(e) => {
            e.preventDefault();
            backfillHistoryMut.mutate();
          }}
        >
          <label>
            batch_size
            <input
              type="number"
              min={1}
              max={200}
              value={bfBatchSize}
              onChange={(e) => setBfBatchSize(e.target.value)}
              required
            />
          </label>
          <label className="checkbox">
            <input type="checkbox" checked={bfDoListings} onChange={(e) => setBfDoListings(e.target.checked)} />
            listings
          </label>
          <label className="checkbox">
            <input type="checkbox" checked={bfDoHouses} onChange={(e) => setBfDoHouses(e.target.checked)} />
            houses (ЖК)
          </label>
          <label className="checkbox">
            <input type="checkbox" checked={bfDoValuations} onChange={(e) => setBfDoValuations(e.target.checked)} />
            valuations
          </label>
          <label className="checkbox">
            <input type="checkbox" checked={bfDryRun} onChange={(e) => setBfDryRun(e.target.checked)} />
            dry_run
          </label>
          <button type="submit" disabled={backfillHistoryMut.isPending}>
            {backfillHistoryMut.isPending ? "Запускаем…" : "Запустить backfill-history"}
          </button>
        </form>
        <ResultPanel mut={backfillHistoryMut} />
      </div>

      <div className="scraper-section">
        <h3>6. Backfill — price-history (T8a)</h3>
        <p className="scraper-hint">
          T8a: Дозаполняет offer_price_history через detail-страницы (curl_cffi). Идемпотентен.
        </p>
        <form
          className="scraper-form"
          onSubmit={(e) => {
            e.preventDefault();
            priceHistoryMut.mutate();
          }}
        >
          <label>
            batch_size
            <input
              type="number"
              min={1}
              max={500}
              value={phBatchSize}
              onChange={(e) => setPhBatchSize(e.target.value)}
              required
            />
          </label>
          <label>
            listing_id (опционально, debug)
            <input
              type="number"
              value={phListingId}
              onChange={(e) => setPhListingId(e.target.value)}
              placeholder="оставьте пустым"
            />
          </label>
          <button type="submit" disabled={priceHistoryMut.isPending}>
            {priceHistoryMut.isPending ? "Запускаем…" : "Запустить price-history backfill"}
          </button>
        </form>
        <ResultPanel mut={priceHistoryMut} />
      </div>

      <div className="scraper-section">
        <h3>7. Full Load (полный обход без anchor&apos;ов)</h3>
        <p className="scraper-hint">
          Exhaustive Cian ЕКБ: адаптивное деление по цене, пагинация каждого бакета. Запускается в background.
        </p>
        <form
          className="scraper-form"
          onSubmit={(e) => {
            e.preventDefault();
            fullLoadMut.mutate();
          }}
        >
          <label>
            price_cap_per_bucket
            <input
              type="number"
              min={200}
              max={1500}
              value={flPriceCap}
              onChange={(e) => setFlPriceCap(e.target.value)}
            />
          </label>
          <label>
            request_delay_sec
            <input
              type="number"
              min={0.5}
              max={15}
              step={0.5}
              value={flDelay}
              onChange={(e) => setFlDelay(e.target.value)}
            />
          </label>
          <label>
            concurrency
            <input
              type="number"
              min={1}
              max={10}
              value={flConcurrency}
              onChange={(e) => setFlConcurrency(e.target.value)}
            />
          </label>
          <label>
            resume_run_id (опционально)
            <input
              type="number"
              value={flResumeRunId}
              onChange={(e) => setFlResumeRunId(e.target.value)}
              placeholder="оставьте пустым для нового прогона"
            />
          </label>
          <button type="submit" disabled={fullLoadMut.isPending}>
            {fullLoadMut.isPending ? "Запускаем…" : "Запустить Cian Full Load"}
          </button>
        </form>
        <ResultPanel mut={fullLoadMut} />
      </div>
    </>
  );
}

// ── Yandex advanced triggers ───────────────────────────────────────────────

function YandexAdvancedTriggers() {
  const [lat, setLat] = useState("56.8400");
  const [lon, setLon] = useState("60.6050");
  const [radius, setRadius] = useState("1500");
  const [multiRoom, setMultiRoom] = useState(false);
  const [deep, setDeep] = useState(false);
  const aroundMut = useMutation<
    ScrapeAroundResp,
    Error,
    {
      lat: number;
      lon: number;
      radius_m: number;
      sources: string[];
      multi_room_yandex?: boolean;
      deep_yandex?: boolean;
    }
  >({
    mutationFn: (input) =>
      apiFetch<ScrapeAroundResp>("/api/v1/admin/scrape", {
        method: "POST",
        body: JSON.stringify(input),
      }),
  });

  const [detailUrl, setDetailUrl] = useState(
    "https://realty.yandex.ru/offer/7567094292504417257/",
  );
  const detailMut = useMutation<
    YandexDetailTriggerResp,
    Error,
    { offer_url: string }
  >({
    mutationFn: ({ offer_url }) =>
      apiFetch<YandexDetailTriggerResp>(
        `/api/v1/admin/scrape/yandex-detail?offer_url=${encodeURIComponent(offer_url)}`,
        { method: "POST" },
      ),
  });

  const [nbSlug, setNbSlug] = useState("tatlin");
  const [nbId, setNbId] = useState("1592987");
  const [nbCity, setNbCity] = useState("ekaterinburg");
  const nbMut = useMutation<
    YandexNewbuildingTriggerResp,
    Error,
    { slug: string; id: string; city: string }
  >({
    mutationFn: ({ slug, id, city }) => {
      const qs = new URLSearchParams({ slug, id, city });
      return apiFetch<YandexNewbuildingTriggerResp>(
        `/api/v1/admin/scrape/yandex-newbuilding?${qs.toString()}`,
        { method: "POST" },
      );
    },
  });

  const [valAddress, setValAddress] = useState(
    "Свердловская область, Екатеринбург, улица Учителей, 18",
  );
  const [valCategory, setValCategory] = useState("APARTMENT");
  const [valType, setValType] = useState("SELL");
  const [valPage, setValPage] = useState("1");
  const valMut = useMutation<
    YandexValuationTriggerResp,
    Error,
    { address: string; offer_category: string; offer_type: string; page: number }
  >({
    mutationFn: ({ address, offer_category, offer_type, page }) => {
      const qs = new URLSearchParams({ address, offer_category, offer_type, page: String(page) });
      return apiFetch<YandexValuationTriggerResp>(
        `/api/v1/admin/scrape/yandex-valuation?${qs.toString()}`,
        { method: "POST" },
      );
    },
  });

  return (
    <>
      <div className="scraper-section">
        <h3>1. Search around</h3>
        <form
          className="scraper-form"
          onSubmit={(e) => {
            e.preventDefault();
            aroundMut.mutate({
              lat: parseFloat(lat),
              lon: parseFloat(lon),
              radius_m: parseInt(radius, 10),
              sources: ["yandex"],
              multi_room_yandex: multiRoom,
              deep_yandex: deep,
            });
          }}
        >
          <label>
            Lat
            <input value={lat} onChange={(e) => setLat(e.target.value)} required />
          </label>
          <label>
            Lon
            <input value={lon} onChange={(e) => setLon(e.target.value)} required />
          </label>
          <label>
            Radius (м)
            <input
              type="number"
              min={100}
              max={10000}
              step={100}
              value={radius}
              onChange={(e) => setRadius(e.target.value)}
              required
            />
          </label>
          <label className="checkbox">
            <input type="checkbox" checked={multiRoom} onChange={(e) => setMultiRoom(e.target.checked)} />
            multi_room_yandex (5 rooms split)
          </label>
          <label className="checkbox">
            <input type="checkbox" checked={deep} onChange={(e) => setDeep(e.target.checked)} />
            deep_yandex (5 × 3 × 2 = 30 requests)
          </label>
          <button type="submit" disabled={aroundMut.isPending}>
            {aroundMut.isPending ? "Скрейпим…" : "Запустить"}
          </button>
        </form>
        <ResultPanel mut={aroundMut} />
      </div>

      <div className="scraper-section">
        <h3>2. Detail page enrichment</h3>
        <form
          className="scraper-form"
          onSubmit={(e) => {
            e.preventDefault();
            detailMut.mutate({ offer_url: detailUrl });
          }}
        >
          <label className="full">
            offer_url
            <input
              value={detailUrl}
              onChange={(e) => setDetailUrl(e.target.value)}
              required
              placeholder="https://realty.yandex.ru/offer/..."
            />
          </label>
          <button type="submit" disabled={detailMut.isPending}>
            {detailMut.isPending ? "Парсим…" : "Запустить"}
          </button>
        </form>
        <ResultPanel mut={detailMut} />
      </div>

      <div className="scraper-section">
        <h3>3. ЖК (Newbuilding) landing</h3>
        <form
          className="scraper-form"
          onSubmit={(e) => {
            e.preventDefault();
            nbMut.mutate({ slug: nbSlug, id: nbId, city: nbCity });
          }}
        >
          <label>
            slug
            <input value={nbSlug} onChange={(e) => setNbSlug(e.target.value)} required placeholder="tatlin" />
          </label>
          <label>
            id
            <input value={nbId} onChange={(e) => setNbId(e.target.value)} required placeholder="1592987" />
          </label>
          <label>
            city
            <input value={nbCity} onChange={(e) => setNbCity(e.target.value)} required placeholder="ekaterinburg" />
          </label>
          <button type="submit" disabled={nbMut.isPending}>
            {nbMut.isPending ? "Парсим…" : "Запустить"}
          </button>
        </form>
        <ResultPanel mut={nbMut} />
      </div>

      <div className="scraper-section">
        <h3>4. Valuation (anonymous history)</h3>
        <form
          className="scraper-form"
          onSubmit={(e) => {
            e.preventDefault();
            valMut.mutate({
              address: valAddress,
              offer_category: valCategory,
              offer_type: valType,
              page: parseInt(valPage, 10),
            });
          }}
        >
          <label className="full">
            Адрес
            <input value={valAddress} onChange={(e) => setValAddress(e.target.value)} required />
          </label>
          <label>
            offer_category
            <select value={valCategory} onChange={(e) => setValCategory(e.target.value)}>
              <option value="APARTMENT">APARTMENT</option>
              <option value="HOUSE">HOUSE</option>
              <option value="GARAGE">GARAGE</option>
              <option value="COMMERCIAL">COMMERCIAL</option>
              <option value="ROOM">ROOM</option>
            </select>
          </label>
          <label>
            offer_type
            <select value={valType} onChange={(e) => setValType(e.target.value)}>
              <option value="SELL">SELL</option>
              <option value="RENT">RENT</option>
            </select>
          </label>
          <label>
            page
            <input
              type="number"
              min={1}
              max={100}
              value={valPage}
              onChange={(e) => setValPage(e.target.value)}
              required
            />
          </label>
          <button type="submit" disabled={valMut.isPending}>
            {valMut.isPending ? "Запрашиваем…" : "Запустить"}
          </button>
        </form>
        <ResultPanel mut={valMut} />
      </div>

      <YandexBackfillSection />
    </>
  );
}

// ── Yandex Backfill section ────────────────────────────────────────────────

function YandexBackfillSection() {
  // ── yandex-address-backfill ────────────────────────────────────────────
  const [abLimit, setAbLimit] = useState("200");
  const [abDelay, setAbDelay] = useState("3.0");

  const addressBackfillMut = useMutation<BackfillCountersResp, Error>({
    mutationFn: () =>
      apiFetch<BackfillCountersResp>("/api/v1/admin/scrape/yandex-address-backfill", {
        method: "POST",
        body: JSON.stringify({
          limit: parseInt(abLimit, 10),
          request_delay_sec: parseFloat(abDelay),
        }),
      }),
  });

  // ── yandex-full-load ──────────────────────────────────────────────────
  const [flDelay, setFlDelay] = useState("2.0");
  const [flConcurrency, setFlConcurrency] = useState("4");
  const [flPriceCap, setFlPriceCap] = useState("500");
  const [flResumeRunId, setFlResumeRunId] = useState("");

  const fullLoadMut = useMutation<FullLoadStartResp, Error>({
    mutationFn: () =>
      apiFetch<FullLoadStartResp>("/api/v1/admin/scrape/yandex-full-load", {
        method: "POST",
        body: JSON.stringify({
          price_cap_per_bucket: parseInt(flPriceCap, 10),
          request_delay_sec: parseFloat(flDelay),
          concurrency: parseInt(flConcurrency, 10),
          resume_run_id: flResumeRunId !== "" ? parseInt(flResumeRunId, 10) : null,
        }),
      }),
  });

  // ── yandex-newbuilding-sweep ───────────────────────────────────────────
  const [nbLimit, setNbLimit] = useState("5");
  const [nbDelay, setNbDelay] = useState("8.0");
  const [nbCity, setNbCity] = useState("ekaterinburg");
  const [nbForce, setNbForce] = useState(false);

  const nbSweepMut = useMutation<YandexNBSweepStartResp, Error>({
    mutationFn: () =>
      apiFetch<YandexNBSweepStartResp>("/api/v1/admin/scrape/yandex-newbuilding-sweep", {
        method: "POST",
        body: JSON.stringify({
          limit: parseInt(nbLimit, 10),
          request_delay_sec: parseFloat(nbDelay),
          force: nbForce,
          city: nbCity,
        }),
      }),
  });

  return (
    <>
      <div className="scraper-section">
        <h3>5. Backfill — address (T10)</h3>
        <p className="scraper-hint">
          Дозаполняет listings.address для Yandex-листингов без номера дома (detail-страница → полный адрес).
        </p>
        <form
          className="scraper-form"
          onSubmit={(e) => {
            e.preventDefault();
            addressBackfillMut.mutate();
          }}
        >
          <label>
            limit
            <input
              type="number"
              min={1}
              max={2000}
              value={abLimit}
              onChange={(e) => setAbLimit(e.target.value)}
              required
            />
          </label>
          <label>
            request_delay_sec
            <input
              type="number"
              min={1}
              max={15}
              step={0.5}
              value={abDelay}
              onChange={(e) => setAbDelay(e.target.value)}
            />
          </label>
          <button type="submit" disabled={addressBackfillMut.isPending}>
            {addressBackfillMut.isPending ? "Запускаем…" : "Запустить address backfill"}
          </button>
        </form>
        <ResultPanel mut={addressBackfillMut} />
      </div>

      <div className="scraper-section">
        <h3>6. Full Load (полный обход без anchor&apos;ов)</h3>
        <p className="scraper-hint">
          Exhaustive Yandex ЕКБ: адаптивное деление по цене, пагинация каждого бакета. Background.
        </p>
        <form
          className="scraper-form"
          onSubmit={(e) => {
            e.preventDefault();
            fullLoadMut.mutate();
          }}
        >
          <label>
            price_cap_per_bucket
            <input
              type="number"
              min={100}
              max={575}
              value={flPriceCap}
              onChange={(e) => setFlPriceCap(e.target.value)}
            />
          </label>
          <label>
            request_delay_sec
            <input
              type="number"
              min={1}
              max={15}
              step={0.5}
              value={flDelay}
              onChange={(e) => setFlDelay(e.target.value)}
            />
          </label>
          <label>
            concurrency
            <input
              type="number"
              min={1}
              max={8}
              value={flConcurrency}
              onChange={(e) => setFlConcurrency(e.target.value)}
            />
          </label>
          <label>
            resume_run_id (опционально)
            <input
              type="number"
              value={flResumeRunId}
              onChange={(e) => setFlResumeRunId(e.target.value)}
              placeholder="оставьте пустым"
            />
          </label>
          <button type="submit" disabled={fullLoadMut.isPending}>
            {fullLoadMut.isPending ? "Запускаем…" : "Запустить Yandex Full Load"}
          </button>
        </form>
        <ResultPanel mut={fullLoadMut} />
      </div>

      <div className="scraper-section">
        <h3>7. Newbuilding enrichment sweep</h3>
        <p className="scraper-hint">
          Обогащение pending ЖК-домов: resolve slug → fetch_jk → UPSERT yandex_jk_enrichment.
        </p>
        <form
          className="scraper-form"
          onSubmit={(e) => {
            e.preventDefault();
            nbSweepMut.mutate();
          }}
        >
          <label>
            limit
            <input
              type="number"
              min={1}
              max={200}
              value={nbLimit}
              onChange={(e) => setNbLimit(e.target.value)}
            />
          </label>
          <label>
            request_delay_sec
            <input
              type="number"
              min={3}
              max={30}
              step={0.5}
              value={nbDelay}
              onChange={(e) => setNbDelay(e.target.value)}
            />
          </label>
          <label>
            city
            <input
              value={nbCity}
              onChange={(e) => setNbCity(e.target.value)}
              placeholder="ekaterinburg"
            />
          </label>
          <label className="checkbox">
            <input type="checkbox" checked={nbForce} onChange={(e) => setNbForce(e.target.checked)} />
            force (перезаписать уже обогащённые)
          </label>
          <button type="submit" disabled={nbSweepMut.isPending}>
            {nbSweepMut.isPending ? "Запускаем…" : "Запустить NB sweep"}
          </button>
        </form>
        <ResultPanel mut={nbSweepMut} />
      </div>
    </>
  );
}

// ── Per-source delay editor ────────────────────────────────────────────────

interface PerSourceDelayEditorProps {
  source: ScraperSource;
  delayMin: number;
  delayMax: number;
}

function PerSourceDelayEditor({ source, delayMin, delayMax }: PerSourceDelayEditorProps) {
  const settingsQ = useScraperSettings();
  const updateMut = useUpdateScraperSetting();

  const currentSetting = settingsQ.data?.find((s) => s.source === source);
  const [delayInput, setDelayInput] = useState<string>("5.0");
  const [initialized, setInitialized] = useState(false);

  if (currentSetting && !initialized) {
    setDelayInput(String(currentSetting.request_delay_sec));
    setInitialized(true);
  }

  return (
    <section className="scraper-section">
      <h2>Задержка запросов</h2>
      <p className="scraper-hint">
        Задержка применяется только к <strong>{source}</strong>. Изменение вступает в силу немедленно.
      </p>

      {currentSetting && (
        <div className="schedule-status" style={{ marginBottom: 12 }}>
          <div className="schedule-status__row">
            <span className="schedule-status__label">Текущая задержка:</span>
            <span>
              <strong>{currentSetting.request_delay_sec} сек</strong>
            </span>
          </div>
        </div>
      )}

      <form
        className="scraper-form"
        onSubmit={(e) => {
          e.preventDefault();
          updateMut.mutate({ source, request_delay_sec: parseFloat(delayInput) });
        }}
      >
        <label>
          Задержка (сек, {delayMin}–{delayMax})
          <input
            type="number"
            min={delayMin}
            max={delayMax}
            step={0.5}
            value={delayInput}
            onChange={(e) => setDelayInput(e.target.value)}
            required
          />
        </label>
        <button type="submit" disabled={updateMut.isPending}>
          {updateMut.isPending ? "Сохраняем…" : "Сохранить задержку"}
        </button>
      </form>

      {updateMut.isError && (
        <div className="scraper-result scraper-result--error">
          <strong>Ошибка:</strong> {updateMut.error.message}
        </div>
      )}
      {updateMut.isSuccess && updateMut.data && (
        <div className="scraper-result">
          Сохранено.{" "}
          <strong>{Number(updateMut.data.request_delay_sec).toFixed(1)} сек</strong>
        </div>
      )}
    </section>
  );
}

// ── Cross-source Backfill & Sync (System tab) ─────────────────────────────

function HouseImvBackfillSection() {
  const [batchSize, setBatchSize] = useState("50");
  const [delay, setDelay] = useState("5.0");
  const [onlyStatus, setOnlyStatus] = useState("pending");
  const [houseId, setHouseId] = useState("");

  const imvMut = useMutation<HouseIMVBackfillResp, Error>({
    mutationFn: () =>
      apiFetch<HouseIMVBackfillResp>("/api/v1/admin/scrape/house-imv-backfill", {
        method: "POST",
        body: JSON.stringify({
          batch_size: parseInt(batchSize, 10),
          request_delay_sec: parseFloat(delay),
          only_status: onlyStatus,
          house_id: houseId !== "" ? parseInt(houseId, 10) : null,
        }),
      }),
  });

  return (
    <section className="scraper-section">
      <h2>House IMV Backfill (T7)</h2>
      <p className="scraper-hint">
        Batch оценка домов через Avito IMV API. Итерирует дома по{" "}
        <code>imv_status</code>, сохраняет в{" "}
        <code>house_imv_evaluations</code> / <code>house_placement_history</code> /{" "}
        <code>house_suggestions</code>. Не снижать delay ниже 3s.
      </p>
      <form
        className="scraper-form"
        onSubmit={(e) => {
          e.preventDefault();
          imvMut.mutate();
        }}
      >
        <label>
          batch_size
          <input
            type="number"
            min={1}
            max={500}
            value={batchSize}
            onChange={(e) => setBatchSize(e.target.value)}
            required
          />
        </label>
        <label>
          request_delay_sec
          <input
            type="number"
            min={1}
            max={30}
            step={0.5}
            value={delay}
            onChange={(e) => setDelay(e.target.value)}
          />
        </label>
        <label>
          only_status
          <select value={onlyStatus} onChange={(e) => setOnlyStatus(e.target.value)}>
            <option value="pending">pending (обработать новые)</option>
            <option value="transient_error">transient_error (retry failed)</option>
          </select>
        </label>
        <label>
          house_id (опционально, debug)
          <input
            type="number"
            value={houseId}
            onChange={(e) => setHouseId(e.target.value)}
            placeholder="оставьте пустым"
          />
        </label>
        <button type="submit" disabled={imvMut.isPending} style={{ gridColumn: "1 / -1" }}>
          {imvMut.isPending ? "Запускаем IMV backfill…" : "Запустить House IMV backfill"}
        </button>
      </form>
      <ResultPanel mut={imvMut} />
    </section>
  );
}

function GeocodeSection() {
  const [batchSize, setGcBatchSize] = useState("200");
  const [dryRun, setGcDryRun] = useState(false);
  const [background, setGcBackground] = useState(false);

  const statusQ = useQuery<GeocodeStatusResp>({
    queryKey: ["geocode-missing-status"],
    queryFn: () =>
      apiFetch<GeocodeStatusResp>("/api/v1/admin/scrape/geocode-missing-listings/status"),
    staleTime: 30_000,
    refetchInterval: 60_000,
    retry: 1,
  });

  const geocodeMut = useMutation<GeocodeBackfillResp, Error>({
    mutationFn: () => {
      const qs = new URLSearchParams({
        batch_size: batchSize,
        dry_run: String(dryRun),
        background: String(background),
      });
      return apiFetch<GeocodeBackfillResp>(
        `/api/v1/admin/scrape/geocode-missing-listings?${qs.toString()}`,
        { method: "POST" },
      );
    },
  });

  return (
    <section className="scraper-section">
      <h2>Geocode missing listings</h2>
      <p className="scraper-hint">
        Дозаполняет координаты листингов через Nominatim (address-dedup).
        Безопасно повторять. Background=true → возвращает немедленно, статус — в логах.
      </p>

      {/* Status block */}
      {statusQ.isError && (
        <p className="scraper-result scraper-result--error" style={{ marginBottom: 8 }}>
          Ошибка загрузки статуса: {statusQ.error.message}
        </p>
      )}
      {statusQ.isSuccess && (
        <div className="schedule-status" style={{ marginBottom: 12 }}>
          <div className="schedule-status__row">
            <span className="schedule-status__label">Pending listings:</span>
            <span>{statusQ.data.total_pending_listings.toLocaleString("ru-RU")}</span>
          </div>
          <div className="schedule-status__row">
            <span className="schedule-status__label">Уникальных адресов:</span>
            <span>{statusQ.data.unique_addresses_pending.toLocaleString("ru-RU")}</span>
          </div>
          <div className="schedule-status__row">
            <span className="schedule-status__label">~Минут (Nominatim):</span>
            <span>{statusQ.data.estimated_nominatim_minutes}</span>
          </div>
          <div className="schedule-status__row">
            <span className="schedule-status__label">Geocode cache (active):</span>
            <span>{statusQ.data.cache_size_active.toLocaleString("ru-RU")}</span>
          </div>
        </div>
      )}

      <form
        className="scraper-form"
        onSubmit={(e) => {
          e.preventDefault();
          geocodeMut.mutate();
        }}
      >
        <label>
          batch_size
          <input
            type="number"
            min={1}
            max={2000}
            value={batchSize}
            onChange={(e) => setGcBatchSize(e.target.value)}
            required
          />
        </label>
        <label className="checkbox">
          <input type="checkbox" checked={dryRun} onChange={(e) => setGcDryRun(e.target.checked)} />
          dry_run (показать без записи)
        </label>
        <label className="checkbox">
          <input type="checkbox" checked={background} onChange={(e) => setGcBackground(e.target.checked)} />
          background (не ждать результат)
        </label>
        <button type="submit" disabled={geocodeMut.isPending} style={{ gridColumn: "1 / -1" }}>
          {geocodeMut.isPending ? "Геокодируем…" : "Запустить geocode backfill"}
        </button>
      </form>
      <ResultPanel mut={geocodeMut} />
    </section>
  );
}

// ── Provider configs ───────────────────────────────────────────────────────

interface ProviderConfig {
  source: ScraperSource;
  label: string;
  scheduleSource: ScheduleSourceKey;
  paramConfig: SweepParamConfig;
  delayMin: number;
  delayMax: number;
}

const PROVIDER_CONFIGS: ProviderConfig[] = [
  {
    source: "avito",
    label: "Avito",
    scheduleSource: "avito_city_sweep",
    paramConfig: {
      hasDetailParams: true,
      hasEnrichAddress: false,
      defaults: {
        pages_per_anchor: 3,
        detail_top_n: 20,
        request_delay_sec: 7,
        radius_m: 1500,
        enrich_houses: true,
      },
    },
    delayMin: 3,
    delayMax: 30,
  },
  {
    source: "cian",
    label: "Cian",
    scheduleSource: "cian_city_sweep",
    paramConfig: {
      hasDetailParams: true,
      hasEnrichAddress: false,
      defaults: {
        pages_per_anchor: 3,
        detail_top_n: 20,
        request_delay_sec: 5,
        radius_m: 1500,
        enrich_houses: true,
      },
    },
    delayMin: 3,
    delayMax: 30,
  },
  {
    source: "yandex",
    label: "Yandex",
    scheduleSource: "yandex_city_sweep",
    paramConfig: {
      hasDetailParams: false,
      hasEnrichAddress: true,
      defaults: {
        pages_per_anchor: 3,
        request_delay_sec: 5,
        radius_m: 1500,
        enrich_address: false,
      },
    },
    delayMin: 1,
    delayMax: 60,
  },
];

// ── Provider tab content ───────────────────────────────────────────────────

interface ProviderTabContentProps {
  config: ProviderConfig;
}

function ProviderTabContent({ config }: ProviderTabContentProps) {
  const { source, scheduleSource, paramConfig, delayMin, delayMax } = config;

  return (
    <div>
      <ScheduleControl source={source} scheduleSource={scheduleSource} paramConfig={paramConfig} />
      <ProviderProxySection source={source} />
      <RunsTable source={source} />
      <PerSourceDelayEditor source={source} delayMin={delayMin} delayMax={delayMax} />
      {source === "cian" && <CianCookiesSection />}
      <Collapsible title="Расширенное (отдельные триггеры)">
        <div style={{ marginTop: 8 }}>
          {source === "avito" && <AvitoAdvancedTriggers />}
          {source === "cian" && <CianAdvancedTriggers />}
          {source === "yandex" && <YandexAdvancedTriggers />}
        </div>
      </Collapsible>
    </div>
  );
}

// ── Tab types ──────────────────────────────────────────────────────────────

type ProviderTab = "avito" | "cian" | "yandex";

const TAB_LABELS: Record<ProviderTab, string> = {
  avito: "Avito",
  cian: "Cian",
  yandex: "Yandex",
};

// ── Page ───────────────────────────────────────────────────────────────────

export default function ScrapersUnifiedPage() {
  const [activeProvider, setActiveProvider] = useState<ProviderTab>("avito");

  const activeConfig = PROVIDER_CONFIGS.find((c) => c.source === activeProvider)!;

  return (
    <>
      <Topbar active="scrapers" />
      <main className="page scraper-page">
        <h1 className="scraper-h1">Скрапперы</h1>
        <p className="scraper-subtitle">
          Центр управления всеми скрапперами: система, расписания, прокси,
          история прогонов, триггеры.
        </p>

        {/* Section 1: Система */}
        <SystemHealthSection />

        {/* Section 2: Pacing */}
        <PacingSection />

        {/* Section 3: Глобально */}
        <GlobalSection />

        {/* Section 4: Backfill & Sync (cross-source) */}
        <HouseImvBackfillSection />
        <GeocodeSection />

        {/* Section 5: Качество данных */}
        <DataQualitySection />

        {/* Tabs: Avito / Cian / Yandex */}
        <div style={{ marginTop: 24 }}>
          {/* Tab bar */}
          <div
            style={{
              display: "flex",
              gap: 0,
              borderBottom: "2px solid var(--border-card, #e6e8ec)",
              overflowX: "auto",
            }}
            role="tablist"
          >
            {(Object.keys(TAB_LABELS) as ProviderTab[]).map((tab) => (
              <button
                key={tab}
                type="button"
                role="tab"
                aria-selected={activeProvider === tab}
                onClick={() => setActiveProvider(tab)}
                style={{
                  padding: "10px 20px",
                  border: "none",
                  borderBottom:
                    activeProvider === tab
                      ? "2px solid var(--accent, #1d4ed8)"
                      : "2px solid transparent",
                  marginBottom: -2,
                  background: "none",
                  cursor: "pointer",
                  fontSize: "0.9rem",
                  fontWeight: activeProvider === tab ? 600 : 400,
                  color:
                    activeProvider === tab
                      ? "var(--accent, #1d4ed8)"
                      : "var(--fg-secondary, #5b6066)",
                  whiteSpace: "nowrap",
                }}
              >
                {TAB_LABELS[tab]}
              </button>
            ))}
          </div>

          {/* Tab content */}
          <div style={{ marginTop: 24 }}>
            <ProviderTabContent config={activeConfig} />
          </div>
        </div>
      </main>
    </>
  );
}
