/* global React, ReactDOM */
const { useState, useMemo, useEffect, useRef, useCallback } = React;

const MAX_ZONES = 3;   // zones + districts combined
const MAX_METRO = 1;   // metro segments

// ─── helpers ─────────────────────────────────────────────────────────────────

function parseCode(raw, D) {
  const c = (raw || "").trim().toUpperCase();
  if (!c) return null;
  if (/^([RSG])-(RP|C|LP)$/.test(c)) {
    return { kind: "metro", code: c };
  }
  const zoneMatch = c.match(/^(\d{1,2})([A-Z]+)$/);
  if (zoneMatch) {
    const did = parseInt(zoneMatch[1], 10);
    const d = D.districts.find((x) => Number(x.id) === did);
    if (d && d.subzones.find((z) => z.id === c)) return { kind: "zone", code: c, district: did };
  }
  const dm = c.match(/^(\d{1,2})$/);
  if (dm) {
    const did = parseInt(dm[1], 10);
    if (D.districts.find((x) => Number(x.id) === did)) return { kind: "district", code: c, district: did };
  }
  return null;
}

function categorize(parsed) {
  const zones = parsed.filter((p) => p.kind === "zone" || p.kind === "district");
  const metros = parsed.filter((p) => p.kind === "metro");
  return { zones, metros };
}

function listingMatches(listing, parsed) {
  if (parsed.length === 0) return true;
  const { zones, metros } = categorize(parsed);
  // Zones: OR. Metros: AND with zones.
  const zoneOk = zones.length === 0 ? true : zones.some((p) =>
    p.kind === "metro" ? false :
    p.kind === "zone" ? listing.zone === p.code :
    listing.district === p.district
  );
  const metroOk = metros.length === 0 ? true : metros.some((p) => listing.metro.includes(p.code));
  return zoneOk && metroOk;
}

function readUrlSelection() {
  const h = window.location.hash.replace(/^#/, "");
  if (!h) return [];
  return h.split("+").map((s) => s.trim()).filter(Boolean).slice(0, MAX_ZONES + MAX_METRO);
}

function writeUrlSelection(codes) {
  const next = codes.length ? "#" + codes.join("+") : "";
  if (window.location.hash !== next) {
    window.history.replaceState(null, "", window.location.pathname + window.location.search + next);
  }
}

// ─── Map ─────────────────────────────────────────────────────────────────────

// ─── pinch-zoom / pan ──────────────────────────────────────────────────────
function usePinchZoomPan(ref, { min = 1, max = 3 } = {}) {
  const [transform, setTransform] = useState({ scale: min, x: 0, y: 0 });
  const transformRef = useRef(transform);
  transformRef.current = transform;
  const gesture = useRef({ mode: null, lastTap: 0 });

  const clamp = useCallback((t) => {
    const el = ref.current;
    const rect = el ? el.getBoundingClientRect() : { width: 0, height: 0 };
    const scale = Math.min(max, Math.max(min, t.scale));
    const minX = -(rect.width * (scale - 1));
    const minY = -(rect.height * (scale - 1));
    return {
      scale,
      x: Math.min(0, Math.max(minX, t.x)),
      y: Math.min(0, Math.max(minY, t.y)),
    };
  }, [ref, min, max]);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;

    const dist = (a, b) => Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY);
    const mid = (a, b) => ({ x: (a.clientX + b.clientX) / 2, y: (a.clientY + b.clientY) / 2 });

    function onTouchStart(e) {
      const touches = e.touches;
      const rect = el.getBoundingClientRect();
      const g = gesture.current;
      if (touches.length === 2) {
        g.mode = "pinch";
        g.startDist = dist(touches[0], touches[1]);
        g.startScale = transformRef.current.scale;
        const m = mid(touches[0], touches[1]);
        g.anchorX = m.x - rect.left;
        g.anchorY = m.y - rect.top;
        g.startX = transformRef.current.x;
        g.startY = transformRef.current.y;
      } else if (touches.length === 1) {
        const now = Date.now();
        const wasDoubleTap = now - g.lastTap < 300;
        g.lastTap = now;
        if (wasDoubleTap) {
          setTransform({ scale: min, x: 0, y: 0 });
          g.mode = null;
          return;
        }
        if (transformRef.current.scale > min + 0.01) {
          g.mode = "pan";
          g.startClientX = touches[0].clientX;
          g.startClientY = touches[0].clientY;
          g.startX = transformRef.current.x;
          g.startY = transformRef.current.y;
        } else {
          g.mode = null;
        }
      }
    }

    function onTouchMove(e) {
      const g = gesture.current;
      const touches = e.touches;
      if (g.mode === "pinch" && touches.length === 2) {
        e.preventDefault();
        const d = dist(touches[0], touches[1]);
        const scaleDelta = d / (g.startDist || d);
        const newScale = g.startScale * scaleDelta;
        const nx = g.anchorX * (1 - scaleDelta) + scaleDelta * g.startX;
        const ny = g.anchorY * (1 - scaleDelta) + scaleDelta * g.startY;
        setTransform(clamp({ scale: newScale, x: nx, y: ny }));
      } else if (g.mode === "pan" && touches.length === 1) {
        e.preventDefault();
        const dx = touches[0].clientX - g.startClientX;
        const dy = touches[0].clientY - g.startClientY;
        setTransform(clamp({ scale: transformRef.current.scale, x: g.startX + dx, y: g.startY + dy }));
      }
    }

    function onTouchEnd(e) {
      const g = gesture.current;
      if (e.touches.length === 0) {
        g.mode = null;
      } else if (e.touches.length === 1 && g.mode === "pinch") {
        g.mode = transformRef.current.scale > min + 0.01 ? "pan" : null;
        g.startClientX = e.touches[0].clientX;
        g.startClientY = e.touches[0].clientY;
        g.startX = transformRef.current.x;
        g.startY = transformRef.current.y;
      }
    }

    el.addEventListener("touchstart", onTouchStart, { passive: true });
    el.addEventListener("touchmove", onTouchMove, { passive: false });
    el.addEventListener("touchend", onTouchEnd, { passive: true });
    el.addEventListener("touchcancel", onTouchEnd, { passive: true });
    return () => {
      el.removeEventListener("touchstart", onTouchStart);
      el.removeEventListener("touchmove", onTouchMove);
      el.removeEventListener("touchend", onTouchEnd);
      el.removeEventListener("touchcancel", onTouchEnd);
    };
  }, [ref, clamp, min]);

  const reset = useCallback(() => setTransform({ scale: min, x: 0, y: 0 }), [min]);

  return { transform, reset };
}

function KyivMap({ D, parsed, selected, onToggle, hideLabels }) {
  const [hover, setHover] = useState(null);
  const isOn = (c) => selected.includes(c);
  const districtIsTouched = (d) => parsed.some((p) =>
    (p.kind === "district" && p.district === d.id) || (p.kind === "zone" && p.district === d.id)
  );
  const anySelection = parsed.length > 0;

  return (
    <svg viewBox="0 0 658 640" xmlns="http://www.w3.org/2000/svg" className="kyiv-svg">
      <rect x="0" y="0" width="658" height="640" fill="#F2EFE8" />

      <g opacity="0.3">
        {Array.from({ length: 8 }).map((_, i) => (
          <line key={"vg" + i} x1={i*94} y1="0" x2={i*94} y2="640" stroke="#E2DDD2" strokeWidth="0.5" />
        ))}
        {Array.from({ length: 8 }).map((_, i) => (
          <line key={"hg" + i} x1="0" y1={i*92} x2="658" y2={i*92} stroke="#E2DDD2" strokeWidth="0.5" />
        ))}
      </g>

      {/* L1: District + subzone fills, no labels */}
      <g>
        {D.districts.map((d) => {
          const districtTouched = districtIsTouched(d);
          const dim = anySelection && !districtTouched;
          return (
            <g key={d.id} style={{ opacity: dim ? 0.35 : 1, transition: "opacity .25s" }}>
              {!d.subzones.some((z) => z.path) && <path d={d.path} fill={d.accent} stroke="none" />}
              {d.subzones.map((z, zi) => {
                const on = isOn(z.id);
                const hov = hover === `z-${z.id}`;
                const baseFill = zi === 1 ? `color-mix(in srgb, ${d.accent} 82%, black)` : d.accent;
                return (
                  <g key={z.id}
                     onMouseEnter={() => setHover(`z-${z.id}`)}
                     onMouseLeave={() => setHover(null)}
                     onClick={(e) => { e.stopPropagation(); onToggle(z.id); }}
                     style={{ cursor: "pointer" }}>
                    {z.path && (
                      <path d={z.path}
                            fill={on ? "#1A1F4D" : hov ? `color-mix(in srgb, ${baseFill} 70%, #1A1F4D)` : baseFill}
                            stroke="#F2EFE8"
                            strokeWidth="1.5"
                            style={{ transition: "fill .15s" }} />
                    )}
                    <circle cx={z.cx} cy={z.cy} r={(z.r || 20) * 1.3 * (on ? 1.2 : 1)}
                          fill={on ? "#1A1F4D" : hov ? "rgba(26,31,77,0.18)" : "rgba(255,255,255,0.5)"}
                          stroke="#1A1F4D"
                          strokeOpacity={on ? 1 : 0.4}
                          strokeWidth={on ? 1.6 : 1}
                          strokeDasharray={on ? "0" : "3 3"}
                          style={{ transition: "fill .15s" }} />
                  </g>
                );
              })}
              <path d={d.path}
                    fill="none"
                    stroke={districtTouched ? "#1A1F4D" : "#8C8676"}
                    strokeWidth={districtTouched ? 2.2 : 1.4}
                    style={{ pointerEvents: "none" }} />
            </g>
          );
        })}
      </g>

      {/* River */}
      <g>
        <path d={D.river} fill="#BFD6E3" opacity="0.95" />
        <path d={D.river} fill="none" stroke="#7FA9BF" strokeWidth="1" opacity="0.6" />
      </g>

      <g transform="translate(615, 30)" opacity="0.55">
        <circle cx="0" cy="0" r="22" fill="none" stroke="#1A1F4D" strokeWidth="1" />
        <path d="M 0 -16 L 4 0 L 0 16 L -4 0 Z" fill="#1A1F4D" />
        <text x="0" y="-26" textAnchor="middle" fontFamily="JetBrains Mono, monospace" fontSize="10" fill="#1A1F4D">N</text>
      </g>

      {/* L3: TOP LABEL LAYER — drawn last so labels are never covered by metro lines.
            Pass A: district badges (number + short name pill).
            Pass B: subzone codes — drawn AFTER badges so codes always read through. */}
      <g style={{ pointerEvents: "none" }}>
        {/* A: badges */}
        {D.districts.map((d) => {
          const districtTouched = districtIsTouched(d);
          const dim = anySelection && !districtTouched;
          if (hideLabels) return null;
          const [bx, by] = d.badge || d.centroid;
          const short = d.short || "";
          const w = short ? 56 : 30;
          const h = 24;
          return (
            <g key={`badge-${d.id}`} style={{ opacity: dim ? 0.45 : 1, transition: "opacity .25s" }}>
              <rect x={bx - w/2} y={by - h/2} width={w} height={h}
                    rx="12" ry="12"
                    fill="#FFF" stroke="#1A1F4D" strokeWidth="1.6" />
              <text x={bx - (short ? 14 : 0)} y={by + 5}
                    textAnchor="middle" fontFamily="JetBrains Mono, monospace"
                    fontWeight="800" fontSize="14" fill="#1A1F4D">{d.id}</text>
              {short && (
                <g>
                  <line x1={bx - 4} y1={by - 7} x2={bx - 4} y2={by + 7}
                        stroke="#1A1F4D" strokeOpacity="0.25" strokeWidth="1" />
                  <text x={bx + 12} y={by + 5}
                        textAnchor="middle" fontFamily="Manrope, sans-serif"
                        fontWeight="700" fontSize="12" fill="#1A1F4D">{short}</text>
                </g>
              )}
            </g>
          );
        })}
        {/* B: subzone codes (drawn last) */}
        {D.districts.map((d) => {
          const districtTouched = districtIsTouched(d);
          const dim = anySelection && !districtTouched;
          if (hideLabels) return null;
          // For dark district fills, flip text+halo so codes stay readable.
          const hex = d.accent.replace("#","");
          const r = parseInt(hex.slice(0,2),16), g = parseInt(hex.slice(2,4),16), b = parseInt(hex.slice(4,6),16);
          const darkFill = (0.299*r + 0.587*g + 0.114*b) < 150;
          const codeFill = darkFill ? "#FFFFFF" : "#1A1F4D";
          const codeHalo = darkFill ? "#1A1F4D" : "#F2EFE8";
          return (
            <g key={`codes-${d.id}`} style={{ opacity: dim ? 0.45 : 1, transition: "opacity .25s" }}>
              {d.subzones.map((z) => {
                const on = isOn(z.id);
                return (
                  <g key={`zl-${z.id}`}>
                    <text x={z.cx} y={z.cy + 4}
                          textAnchor="middle"
                          fontFamily="JetBrains Mono, monospace"
                          fontWeight="700"
                          fontSize="12"
                          stroke={codeHalo}
                          strokeWidth="3.5"
                          strokeLinejoin="round"
                          fill="none"
                          opacity={on ? 0 : 0.95}>
                      {z.id}
                    </text>
                    <text x={z.cx} y={z.cy + 4}
                          textAnchor="middle"
                          fontFamily="JetBrains Mono, monospace"
                          fontWeight="700"
                          fontSize="12"
                          fill={on ? "#F5F2EC" : codeFill}
                          opacity={on ? 1 : 0.95}>
                      {z.id}
                    </text>
                  </g>
                );
              })}
            </g>
          );
        })}
      </g>
    </svg>
  );
}

// ─── Selection bar ───────────────────────────────────────────────────────────

function SelectionChips({ codes, parsed, onRemove, onClear, onShare, shareState, zoneCount, metroCount }) {
  if (codes.length === 0) {
    return (
      <div className="sel-empty">
        <span className="mono sel-empty-glyph">▢</span>
        <span>Оберіть до <strong>{MAX_ZONES}</strong> зон + <strong>{MAX_METRO}</strong> сегмент метро. Метро звужує вибірку (AND), зони — додають (OR).</span>
      </div>
    );
  }
  return (
    <div className="sel-chips-row">
      <div className="sel-chips">
        {codes.map((c) => {
          const p = parsed.find((x) => x.code === c);
          const cls = p?.kind === "metro" ? "sel-chip metro" : "sel-chip";
          return (
            <span key={c} className={cls}>
              <span className="mono">{c}</span>
              <button className="sel-chip-x" onClick={() => onRemove(c)} aria-label="remove">×</button>
            </span>
          );
        })}
        <span className="sel-counter mono">
          зони {zoneCount}/{MAX_ZONES} · метро {metroCount}/{MAX_METRO}
        </span>
      </div>
      <div className="sel-actions">
        <button className="sel-action" onClick={onShare}>{shareState || "Поділитись"}</button>
        <button className="sel-action ghost" onClick={onClear}>Скинути</button>
      </div>
    </div>
  );
}

// ─── Filter bar ──────────────────────────────────────────────────────────────

function FilterBar({ value, onChange, onSubmit, suggestions, recents, onPickRecent, error, hint }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    const close = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", close);
    return () => document.removeEventListener("mousedown", close);
  }, []);

  return (
    <div className="filter-bar" ref={ref}>
      <div className={`filter-input-wrap ${error ? "err" : ""}`}>
        <span className="filter-prefix">КОД</span>
        <input
          className="filter-input"
          placeholder="напр. 2A · S-RP · R-C"
          value={value}
          onChange={(e) => { onChange(e.target.value.toUpperCase()); setOpen(true); }}
          onFocus={() => setOpen(true)}
          onKeyDown={(e) => { if (e.key === "Enter") { onSubmit(value); setOpen(false); } }}
        />
        {value && (
          <button className="filter-clear" onClick={() => onChange("")} aria-label="clear">×</button>
        )}
      </div>

      {open && (suggestions.length > 0 || recents.length > 0) && (
        <div className="filter-pop">
          {recents.length > 0 && (
            <div className="filter-section">
              <div className="filter-section-title">Нещодавні</div>
              <div className="filter-chips">
                {recents.map((r) => (
                  <button key={r} className="chip mono" onClick={() => onPickRecent(r)}>{r}</button>
                ))}
              </div>
            </div>
          )}
          {suggestions.length > 0 && (
            <div className="filter-section">
              <div className="filter-section-title">Підказки</div>
              <div className="filter-suggestions">
                {suggestions.map((s) => (
                  <button key={s.code} className="suggestion" onClick={() => onPickRecent(s.code)}>
                    <span className="mono suggestion-code">{s.code}</span>
                    <span className="suggestion-label">{s.label}</span>
                  </button>
                ))}
              </div>
            </div>
          )}
        </div>
      )}
      {error && <div className="filter-error">{hint || "Невідомий код. Формат: 2A або S-RP."}</div>}
    </div>
  );
}

// ─── Legend ──────────────────────────────────────────────────────────────────

function Legend({ D, selected, onToggle, counts }) {
  const isOn = (c) => selected.includes(c);
  return (
    <div className="legend">
      <div className="legend-block">
        <div className="legend-h">Райони <span className="legend-hint">(клік — додати)</span></div>
        <div className="legend-districts">
          {D.districts.map((d) => {
            const on = isOn(String(d.id));
            return (
              <button key={d.id}
                      className={`d-pill ${on ? "on" : ""}`}
                      onClick={() => onToggle(String(d.id))}
                      style={{ "--c": d.accent }}>
                <span className="mono d-pill-num">{d.id}</span>
                <span className="d-pill-name">{d.name}</span>
                {counts[d.id] != null && <span className="d-pill-count mono">{counts[d.id]}</span>}
              </button>
            );
          })}
        </div>
      </div>

      <div className="legend-block">
        <div className="legend-h">Метро <span className="legend-hint">(один сегмент)</span></div>
        <div className="legend-metro">
          {Object.entries(D.metro).map(([key, line]) => (
            <div key={key} className="m-row">
              <div className="m-row-head">
                <span className="m-dot" style={{ background: line.color }}></span>
                <span className="m-name">{line.name}</span>
              </div>
              <div className="m-segs">
                {line.segments.map((seg, sidx) => {
                  const code = seg.code;
                  const first = seg.stations[0]?.label, last = seg.stations[seg.stations.length-1]?.label;
                  return (
                    <button key={key + sidx}
                      className={`m-seg ${isOn(code) ? "on" : ""}`}
                      style={{ "--c": line.color }}
                      onClick={() => onToggle(code)}
                      title={first && last ? `${first} — ${last}` : ""}>
                      <span className="mono">{code}</span>
                      {first && last && <span className="m-seg-range">{first} — {last}</span>}
                    </button>
                  );
                })}
              </div>
            </div>
          ))}
        </div>
        <div className="legend-foot">
          <span className="mono">RP</span> правий берег ·
          <span className="mono"> C</span> центр ·
          <span className="mono"> LP</span> лівий берег
        </div>
      </div>

      <div className="legend-block">
        <div className="legend-h">Підзони</div>
        <div className="legend-sub">
          <span><span className="mono">A</span> центр</span>
          <span><span className="mono">N</span> північ</span>
          <span><span className="mono">S</span> південь</span>
          <span><span className="mono">E</span> схід</span>
          <span><span className="mono">W</span> захід</span>
        </div>
      </div>
    </div>
  );
}

// ─── Listings ────────────────────────────────────────────────────────────────

function pluralize(n) {
  if (n === 1) return "об'єкт";
  if (n >= 2 && n <= 4) return "об'єкти";
  return "об'єктів";
}

function Listings({ items }) {
  return (
    <div className="listings">
      <div className="listings-header">
        <div>
          <div className="listings-title">Знайдено</div>
          <div className="listings-count">{items.length} <span className="listings-noun">{pluralize(items.length)}</span></div>
        </div>
      </div>

      <div className="listings-list">
        {items.length === 0 && (
          <div className="empty">
            <div className="empty-glyph mono">∅</div>
            <div className="empty-title">Нічого не знайдено</div>
            <div className="empty-sub">Спробуйте інший код або скиньте фільтр</div>
          </div>
        )}
        {items.map((l) => (
          <div key={l.id} className="card">
            <div className="card-row1">
              <div className="card-title">{l.title}</div>
              <div className="card-price">{l.price}</div>
            </div>
            <div className="card-meta">
              <span>{l.area}</span><span>·</span>
              <span>{l.rooms === 1 ? "1 кімн." : l.rooms === 2 ? "2 кімн." : "3 кімн."}</span>
            </div>
            <div className="card-tags">
              <span className="tag mono">{l.zone}</span>
              {l.metro.map((m) => <span key={m} className="tag mono metro-tag">{m}</span>)}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ─── Root ────────────────────────────────────────────────────────────────────

function App() {
  const D = window.KYIV_DATA;
  const [code, setCode] = useState("");
  const [selected, setSelected] = useState(() => readUrlSelection());
  const [recents, setRecents] = useState(["2E", "S-RP", "6N"]);
  const [error, setError] = useState(false);
  const [errorHint, setErrorHint] = useState(null);
  const [hideLabels, setHideLabels] = useState(false);
  const [shareState, setShareState] = useState(null);

  const mapCanvasRef = useRef(null);
  const { transform: mapTransform, reset: resetMapZoom } = usePinchZoomPan(mapCanvasRef, { min: 1, max: 3 });

  useEffect(() => { writeUrlSelection(selected); }, [selected]);

  const parsed = useMemo(
    () => selected.map((c) => parseCode(c, D)).filter(Boolean),
    [selected]
  );

  const { zones, metros } = useMemo(() => categorize(parsed), [parsed]);
  const zoneCount = zones.length;
  const metroCount = metros.length;

  const filtered = useMemo(
    () => D.listings.filter((l) => listingMatches(l, parsed)),
    [parsed]
  );

  const counts = useMemo(() => {
    const m = {};
    filtered.forEach((l) => { m[l.district] = (m[l.district] || 0) + 1; });
    return m;
  }, [filtered]);

  const showError = (hint) => {
    setError(true);
    setErrorHint(hint || null);
    setTimeout(() => { setError(false); setErrorHint(null); }, 2200);
  };

  const toggle = useCallback((rawCode) => {
    const c = (rawCode || "").trim().toUpperCase();
    if (!c) return;
    const p = parseCode(c, D);
    if (!p) { showError(); return; }
    setSelected((prev) => {
      if (prev.includes(c)) return prev.filter((x) => x !== c);
      const prevParsed = prev.map((x) => parseCode(x, D)).filter(Boolean);
      const { zones: pz, metros: pm } = categorize(prevParsed);
      if (p.kind === "metro" && pm.length >= MAX_METRO) {
        // replace existing metro
        const filtered = prev.filter((x) => {
          const xp = parseCode(x, D); return xp?.kind !== "metro";
        });
        setRecents((r) => [c, ...r.filter((x) => x !== c)].slice(0, 6));
        return [...filtered, c];
      }
      if ((p.kind === "zone" || p.kind === "district") && pz.length >= MAX_ZONES) {
        showError(`Максимум ${MAX_ZONES} зон. Зніміть одну, щоб додати нову.`);
        return prev;
      }
      setRecents((r) => [c, ...r.filter((x) => x !== c)].slice(0, 6));
      return [...prev, c];
    });
  }, []);

  const submit = useCallback((val) => {
    const v = (val || "").trim().toUpperCase();
    if (!v) { setCode(""); return; }
    const p = parseCode(v, D);
    if (!p) { showError(); return; }
    if (selected.includes(v)) { setCode(""); return; }
    toggle(v);
    setCode("");
  }, [selected, toggle]);

  const remove = (c) => setSelected((prev) => prev.filter((x) => x !== c));
  const clearAll = () => { setSelected([]); setCode(""); };

  const suggestions = useMemo(() => {
    const q = code.trim().toUpperCase();
    if (!q) return [];
    const out = [];
    D.districts.forEach((d) => {
      d.subzones.forEach((z) => {
        if (z.id.startsWith(q) && !selected.includes(z.id)) out.push({ code: z.id, label: `${d.name} · ${z.label}` });
      });
    });
    Object.entries(D.metro).forEach(([key, line]) => {
      ["RP","C","LP"].forEach((seg) => {
        if (key === "S" && seg === "LP") return;
        const c = `${key}-${seg}`;
        if (c.startsWith(q) && !selected.includes(c)) {
          out.push({ code: c, label: `${line.name} · ${seg === "RP" ? "правий берег" : seg === "LP" ? "лівий берег" : "центр"}` });
        }
      });
    });
    return out.slice(0, 6);
  }, [code, selected]);

  const share = () => {
    writeUrlSelection(selected);
    const url = window.location.href;
    if (navigator.clipboard) {
      navigator.clipboard.writeText(url).then(() => {
        setShareState("Скопійовано ✓");
        setTimeout(() => setShareState(null), 1800);
      });
    } else {
      setShareState("URL у адресі");
      setTimeout(() => setShareState(null), 1800);
    }
  };

  return (
    <div className="app">
      <header className="topbar">
        <div className="brand">
          <div className="brand-mark">
            <svg width="28" height="28" viewBox="0 0 32 32">
              <circle cx="16" cy="16" r="14" fill="none" stroke="#1A1F4D" strokeWidth="2" />
              <path d="M16 4 v24 M4 16 h24" stroke="#1A1F4D" strokeWidth="2" />
              <circle cx="16" cy="16" r="3" fill="#1A1F4D" />
            </svg>
          </div>
          <div className="brand-text">
            <div className="brand-title">Київ · Зони</div>
            <div className="brand-sub">до {MAX_ZONES} зон + {MAX_METRO} метро · {filtered.length} {pluralize(filtered.length)}</div>
          </div>
        </div>
        <FilterBar
          value={code}
          onChange={setCode}
          onSubmit={submit}
          suggestions={suggestions}
          recents={recents.filter((r) => !selected.includes(r))}
          onPickRecent={(c) => submit(c)}
          error={error}
          hint={errorHint}
        />
      </header>

      <SelectionChips
        codes={selected}
        parsed={parsed}
        onRemove={remove}
        onClear={clearAll}
        onShare={share}
        shareState={shareState}
        zoneCount={zoneCount}
        metroCount={metroCount}
      />

      <main className="main">
        <section className="map-pane">
          <div className="map-header">
            <div className="map-title">Схема міста</div>
            <div className="map-tools">
              {mapTransform.scale > 1.01 && (
                <button className="tool" onClick={resetMapZoom}>Скинути масштаб</button>
              )}
              <button className={`tool ${hideLabels ? "" : "on"}`} onClick={() => setHideLabels(!hideLabels)}>Назви</button>
            </div>
          </div>
          <div className="map-canvas" ref={mapCanvasRef}>
            <div
              className="map-canvas-zoom"
              style={{
                transform: `translate(${mapTransform.x}px, ${mapTransform.y}px) scale(${mapTransform.scale})`,
                transformOrigin: "0 0",
              }}
            >
              <KyivMap
                D={D}
                parsed={parsed}
                selected={selected}
                onToggle={toggle}
                hideLabels={hideLabels}
              />
            </div>
          </div>
          <Legend D={D} selected={selected} onToggle={toggle} counts={counts} />
        </section>

        <aside className="side-pane">
          <Listings items={filtered} />
        </aside>
      </main>
    </div>
  );
}

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