// BEGIN CHANGE: Admin - catalogo de itens (GIF + XML + cid + class) + Sets inventario
"use client";

import { useCallback, useEffect, useMemo, useState } from "react";
import { ItemIcon } from "@/components/ItemIcon";
import { apiUrl, equipmentSlotUrl } from "@/lib/api";

const GIF_OVERRIDE_KEY = "wa_admin_item_gif_override_v2";

function loadGifOverride(): Record<number, number> {
  try {
    const raw = sessionStorage.getItem(GIF_OVERRIDE_KEY);
    if (!raw) return {};
    const obj = JSON.parse(raw) as Record<string, number>;
    const out: Record<number, number> = {};
    for (const [k, v] of Object.entries(obj)) {
      const id = Number(k);
      const src = Number(v);
      if (id > 0 && src > 0) out[id] = src;
    }
    return out;
  } catch {
    return {};
  }
}

function persistGifOverride(map: Record<number, number>) {
  try {
    sessionStorage.setItem(GIF_OVERRIDE_KEY, JSON.stringify(map));
  } catch {
    /* ignore */
  }
}

type SourceFilter = "all" | "named" | "unnamed" | "xml_only";
type MainTab = "catalog" | "sets";

type ClassOption = { value: string; label: string };

type Row = {
  id: number;
  clientId: number | null;
  class: string | null;
  name: string;
  hasGif: boolean;
  inXml: boolean;
};

type Stats = {
  gifCount: number;
  namedCount: number;
  clientIdCount?: number;
  classifiedCount?: number;
  containerCount?: number;
  gifDir: string;
};

type SetSummary = {
  id: number;
  name: string;
  notes: string;
  updatedAt: number;
  filled: number;
  pending: number;
};

type SetSlot = {
  slot: number;
  key: string;
  label: string;
  itemId: number | null;
  pendingClientId: number | null;
  name: string | null;
  clientId: number | null;
  hasGif: boolean;
};

type ItemSet = {
  id: number;
  name: string;
  notes: string;
  slots: SetSlot[];
  grid: (number | null)[][];
};

const EMPTY_GRID: (number | null)[][] = [
  [2, 1, 3],
  [6, 4, 5],
  [9, 7, 10],
  [null, 8, null],
];

function emptySlotsFromGrid(): SetSlot[] {
  const labels: Record<number, string> = {
    1: "Helmet",
    2: "Necklace",
    3: "Backpack",
    4: "Armor",
    5: "Right hand",
    6: "Left hand",
    7: "Legs",
    8: "Boots",
    9: "Ring",
    10: "Ammo",
  };
  const keys: Record<number, string> = {
    1: "head",
    2: "necklace",
    3: "backpack",
    4: "armor",
    5: "right",
    6: "left",
    7: "legs",
    8: "feet",
    9: "ring",
    10: "ammo",
  };
  return Array.from({ length: 10 }, (_, i) => {
    const slot = i + 1;
    return {
      slot,
      key: keys[slot],
      label: labels[slot],
      itemId: null,
      pendingClientId: null,
      name: null,
      clientId: null,
      hasGif: false,
    };
  });
}

export function AdminItems({
  token,
  pageAccess = 0,
}: {
  token: string;
  pageAccess?: number;
}) {
  const canWriteOtb = pageAccess >= 6;
  const [tab, setTab] = useState<MainTab>("catalog");

  // --- Catalog state ---
  const [filter, setFilter] = useState<SourceFilter>("all");
  const [classFilter, setClassFilter] = useState<string>("__none__");
  const [q, setQ] = useState("");
  const [qDraft, setQDraft] = useState("");
  const [rows, setRows] = useState<Row[]>([]);
  const [classes, setClasses] = useState<ClassOption[]>([]);
  const [classCounts, setClassCounts] = useState<Record<string, number>>({});
  const [total, setTotal] = useState(0);
  const [stats, setStats] = useState<Stats | null>(null);
  const [status, setStatus] = useState<"idle" | "loading" | "ok" | "error">(
    "idle",
  );
  const [err, setErr] = useState("");
  const [savingId, setSavingId] = useState<number | null>(null);
  // BEGIN CHANGE: catalog clientId drafts + preview + gif override apos OTB
  const [cidDrafts, setCidDrafts] = useState<Record<number, string>>({});
  const [cidPreview, setCidPreview] = useState<Record<number, number>>({});
  const [cidOwners, setCidOwners] = useState<Record<string, number>>({});
  const [cidPeers, setCidPeers] = useState<Record<string, number[]>>({});
  /** GIF source item_id (extract-time); keep after OTB write so sprite nao volta ao antigo. */
  const [gifOverride, setGifOverride] = useState<Record<number, number>>(() =>
    loadGifOverride(),
  );
  const [cidBusy, setCidBusy] = useState(false);
  const [cidMsg, setCidMsg] = useState("");
  const [slotCidDrafts, setSlotCidDrafts] = useState<Record<number, string>>(
    {},
  );
  // END CHANGE

  // --- Sets state ---
  const [sets, setSets] = useState<SetSummary[]>([]);
  const [activeSet, setActiveSet] = useState<ItemSet | null>(null);
  const [setSlots, setSetSlots] = useState<SetSlot[]>(emptySlotsFromGrid());
  const [selectedSlot, setSelectedSlot] = useState<number | null>(null);
  const [setsMsg, setSetsMsg] = useState("");
  const [setsBusy, setSetsBusy] = useState(false);
  const [pickerQ, setPickerQ] = useState("");
  const [pickerRows, setPickerRows] = useState<Row[]>([]);
  const [pickerClass, setPickerClass] = useState("");

  const reloadCatalog = async (opts?: {
    quiet?: boolean;
    applyGifSuggest?: boolean;
  }) => {
    if (classFilter === "__none__") {
      return false;
    }
    if (!opts?.quiet) {
      setStatus("loading");
      setErr("");
    }
    const qs = new URLSearchParams({
      page: "0",
      pageSize: "0",
      filter,
    });
    if (q.trim()) qs.set("q", q.trim());
    if (classFilter && classFilter !== "__none__") qs.set("class", classFilter);
    const r = await fetch(apiUrl(`items-catalog.php?${qs.toString()}`), {
      headers: { Authorization: `Bearer ${token}` },
    });
    const json = await r.json().catch(() => ({}));
    if (!r.ok) {
      setErr(
        typeof json.error === "string" && json.error
          ? `Falha ao carregar itens (${r.status}): ${json.error}`
          : `Falha ao carregar itens (HTTP ${r.status})`,
      );
      setStatus("error");
      return false;
    }
    setRows(Array.isArray(json.rows) ? json.rows : []);
    setClasses(Array.isArray(json.classes) ? json.classes : []);
    setClassCounts(
      json.classCounts && typeof json.classCounts === "object"
        ? json.classCounts
        : {},
    );
    setTotal(Number(json.total) || 0);
    setStats(json.stats ?? null);
    if (json.cidOwners && typeof json.cidOwners === "object") {
      setCidOwners(json.cidOwners as Record<string, number>);
    }
    if (json.cidPeers && typeof json.cidPeers === "object") {
      setCidPeers(json.cidPeers as Record<string, number[]>);
    }
    // Server-persisted remap GIF sources (survive reload / Recarregar OTB).
    if (json.gifSources && typeof json.gifSources === "object") {
      mergeGifSources(json.gifSources as Record<string, number>);
    } else if (
      json.gifSuggest &&
      typeof json.gifSuggest === "object" &&
      opts?.applyGifSuggest
    ) {
      const suggest = json.gifSuggest as Record<string, number>;
      const next: Record<number, number> = {};
      for (const [k, v] of Object.entries(suggest)) {
        const id = Number(k);
        const src = Number(v);
        if (id > 0 && src > 0 && src !== id) next[id] = src;
      }
      setGifOverride(next);
      persistGifOverride(next);
    }
    setStatus("ok");
    return true;
  };

  const loadCatalogMeta = useCallback(async () => {
    try {
      const r = await fetch(apiUrl("items-catalog.php?metaOnly=1"), {
        headers: { Authorization: `Bearer ${token}` },
      });
      const json = await r.json().catch(() => ({}));
      if (!r.ok) {
        setErr(
          typeof json.error === "string" && json.error
            ? `Falha ao carregar classes (${r.status}): ${json.error}`
            : `Falha ao carregar classes (HTTP ${r.status})`,
        );
        setStatus("error");
        return;
      }
      setClasses(Array.isArray(json.classes) ? json.classes : []);
      setClassCounts(
        json.classCounts && typeof json.classCounts === "object"
          ? json.classCounts
          : {},
      );
      setStats(json.stats ?? null);
      setStatus((prev) =>
        prev === "loading" || prev === "idle" ? "idle" : prev,
      );
    } catch {
      setErr("Falha de rede ao carregar classes.");
      setStatus("error");
    }
  }, [token]);

  useEffect(() => {
    if (tab !== "catalog") return;
    void loadCatalogMeta();
  }, [tab, loadCatalogMeta]);

  useEffect(() => {
    if (tab !== "catalog" || classFilter === "__none__") return;
    let cancelled = false;
    (async () => {
      try {
        const ok = await reloadCatalog();
        if (cancelled && !ok) return;
      } catch {
        if (!cancelled) {
          setErr("Falha de rede");
          setStatus("error");
        }
      }
    })();
    return () => {
      cancelled = true;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [token, filter, q, classFilter, tab]);

  const loadSets = async (preferId?: number) => {
    setSetsBusy(true);
    setSetsMsg("");
    try {
      const r = await fetch(apiUrl("items-sets.php"), {
        headers: { Authorization: `Bearer ${token}` },
      });
      const json = await r.json().catch(() => ({}));
      if (!r.ok) {
        setSetsMsg(
          typeof json.error === "string" ? json.error : `HTTP ${r.status}`,
        );
        return;
      }
      const list: SetSummary[] = Array.isArray(json.sets) ? json.sets : [];
      setSets(list);
      if (json.gifSources && typeof json.gifSources === "object") {
        mergeGifSources(json.gifSources as Record<string, number>);
      }
      const target =
        preferId && list.some((s) => s.id === preferId)
          ? preferId
          : activeSet?.id && list.some((s) => s.id === activeSet.id)
            ? activeSet.id
            : list[0]?.id;
      if (target) {
        await openSet(target);
      } else {
        setActiveSet(null);
        setSetSlots(emptySlotsFromGrid());
      }
    } catch {
      setSetsMsg("Falha de rede ao listar sets");
    } finally {
      setSetsBusy(false);
    }
  };

  const openSet = async (id: number) => {
    setSetsBusy(true);
    setSetsMsg("");
    try {
      const r = await fetch(apiUrl(`items-sets.php?action=get&id=${id}`), {
        headers: { Authorization: `Bearer ${token}` },
      });
      const json = await r.json().catch(() => ({}));
      if (!r.ok) {
        setSetsMsg(
          typeof json.error === "string" ? json.error : `HTTP ${r.status}`,
        );
        return;
      }
      const set = json.set as ItemSet;
      setActiveSet(set);
      setSetSlots(Array.isArray(set.slots) ? set.slots : emptySlotsFromGrid());
      if (json.gifSources && typeof json.gifSources === "object") {
        mergeGifSources(json.gifSources as Record<string, number>);
      }
      setSlotCidDrafts({});
      setSelectedSlot(null);
    } catch {
      setSetsMsg("Falha de rede ao abrir set");
    } finally {
      setSetsBusy(false);
    }
  };

  useEffect(() => {
    if (tab !== "sets") return;
    void loadSets();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [tab, token]);

  // Picker search when a slot is selected
  useEffect(() => {
    if (tab !== "sets" || selectedSlot == null) {
      setPickerRows([]);
      return;
    }
    let cancelled = false;
    const t = window.setTimeout(async () => {
      const qs = new URLSearchParams({
        page: "0",
        pageSize: "48",
        filter: "all",
      });
      if (pickerQ.trim()) qs.set("q", pickerQ.trim());
      if (pickerClass) qs.set("class", pickerClass);
      try {
        const r = await fetch(apiUrl(`items-catalog.php?${qs.toString()}`), {
          headers: { Authorization: `Bearer ${token}` },
        });
        const json = await r.json().catch(() => ({}));
        if (cancelled || !r.ok) return;
        setPickerRows(Array.isArray(json.rows) ? json.rows : []);
        if (Array.isArray(json.classes) && classes.length === 0) {
          setClasses(json.classes);
        }
        if (json.cidOwners && typeof json.cidOwners === "object") {
          setCidOwners(json.cidOwners as Record<string, number>);
        }
        if (json.cidPeers && typeof json.cidPeers === "object") {
          setCidPeers(json.cidPeers as Record<string, number[]>);
        }
      } catch {
        /* ignore */
      }
    }, 250);
    return () => {
      cancelled = true;
      window.clearTimeout(t);
    };
  }, [tab, selectedSlot, pickerQ, pickerClass, token, classes.length]);

  const slotById = useMemo(() => {
    const m = new Map<number, SetSlot>();
    for (const s of setSlots) m.set(s.slot, s);
    return m;
  }, [setSlots]);

  const pendingCount = useMemo(
    () =>
      setSlots.filter(
        (s) =>
          s.itemId != null &&
          s.pendingClientId != null &&
          s.pendingClientId !== s.clientId,
      ).length,
    [setSlots],
  );

  const reloadOtbFromDisk = async () => {
    setCidBusy(true);
    setCidMsg("");
    setErr("");
    try {
      const r = await fetch(apiUrl("items-catalog.php"), {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ action: "reload_otb" }),
      });
      const json = await r.json().catch(() => ({}));
      if (!r.ok) {
        setCidMsg(
          typeof json.error === "string" ? json.error : `HTTP ${r.status}`,
        );
        return;
      }
      // Keep session overrides only as cache; reload from server gifSources next.
      setGifOverride({});
      persistGifOverride({});
      setCidPreview({});
      setCidDrafts({});
      if (json.cidOwners && typeof json.cidOwners === "object") {
        setCidOwners(json.cidOwners as Record<string, number>);
      }
      if (json.cidPeers && typeof json.cidPeers === "object") {
        setCidPeers(json.cidPeers as Record<string, number[]>);
      }
      if (json.gifSources && typeof json.gifSources === "object") {
        mergeGifSources(json.gifSources as Record<string, number>);
      }
      const ok = await reloadCatalog({ quiet: false, applyGifSuggest: false });
      if (ok) {
        setCidMsg(
          `OTB recarregado (${json.clientIdCount ?? "?"} cids). Preview GIF dos remaps restaurado do servidor.`,
        );
      }
      if (tab === "sets" && activeSet?.id) {
        await openSet(activeSet.id);
      }
    } catch {
      setCidMsg("Falha de rede ao recarregar OTB");
    } finally {
      setCidBusy(false);
    }
  };

  const saveClass = async (itemId: number, next: string) => {
    setSavingId(itemId);
    const prev = rows.find((r) => r.id === itemId)?.class ?? null;
    setRows((list) =>
      list.map((r) =>
        r.id === itemId
          ? { ...r, class: next === "" || next === "none" ? null : next }
          : r,
      ),
    );
    try {
      const r = await fetch(apiUrl("items-catalog.php"), {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ itemId, class: next }),
      });
      const json = await r.json().catch(() => ({}));
      if (!r.ok) {
        setRows((list) =>
          list.map((row) => (row.id === itemId ? { ...row, class: prev } : row)),
        );
        setErr(
          typeof json.error === "string" && json.error
            ? json.error
            : `Falha ao salvar classe (HTTP ${r.status})`,
        );
        return;
      }
      setErr("");
      setClassCounts((c) => {
        const copy = { ...c };
        if (prev && copy[prev] != null) copy[prev] = Math.max(0, copy[prev] - 1);
        if (json.class && typeof json.class === "string") {
          copy[json.class] = (copy[json.class] || 0) + 1;
        }
        return copy;
      });
      setStats((s) =>
        s
          ? {
              ...s,
              classifiedCount: Math.max(
                0,
                (s.classifiedCount || 0) +
                  (prev ? -1 : 0) +
                  (json.class ? 1 : 0),
              ),
            }
          : s,
      );
    } catch {
      setRows((list) =>
        list.map((row) => (row.id === itemId ? { ...row, class: prev } : row)),
      );
      setErr("Falha de rede ao salvar classe");
    } finally {
      setSavingId(null);
    }
  };

  const createClassForItem = async (itemId: number) => {
    const label = window.prompt("Nome da nova classe:");
    if (label == null) return;
    const trimmed = label.trim();
    if (!trimmed) return;
    setSavingId(itemId);
    try {
      const r = await fetch(apiUrl("items-catalog.php"), {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          action: "create_class",
          label: trimmed,
          itemId,
        }),
      });
      const json = await r.json().catch(() => ({}));
      if (!r.ok) {
        setErr(
          typeof json.error === "string" && json.error
            ? json.error
            : `Falha ao criar classe (HTTP ${r.status})`,
        );
        return;
      }
      setErr("");
      if (Array.isArray(json.classes)) setClasses(json.classes);
      if (json.classCounts && typeof json.classCounts === "object") {
        setClassCounts(json.classCounts);
      }
      if (typeof json.class === "string" && json.class) {
        setRows((list) =>
          list.map((row) =>
            row.id === itemId ? { ...row, class: json.class } : row,
          ),
        );
        setStats((s) =>
          s
            ? {
                ...s,
                classifiedCount: (s.classifiedCount || 0) + 1,
              }
            : s,
        );
      }
    } catch {
      setErr("Falha de rede ao criar classe");
    } finally {
      setSavingId(null);
    }
  };

  // BEGIN CHANGE: catalog clientId preview + resolve GIF by cid owner
  // IMPORTANT: never auto-borrow peer GIF just because two items share a CID.
  // That made the ORIGINAL cid owner look swapped (showed the remapped item's GIF).
  // Borrow only for: explicit gifOverride (this remapped item) OR pending preview CID.
  const peerGifSrc = (itemId: number, cid: number): number | null => {
    const owner = cidOwners[String(cid)];
    if (owner != null && owner > 0 && owner !== itemId) return owner;
    const peers = cidPeers[String(cid)];
    if (Array.isArray(peers)) {
      const withOther = peers.find((p) => p !== itemId && p > 0);
      if (withOther != null) return withOther;
    }
    return null;
  };

  const mergeGifSources = (
    sources: Record<string, number> | null | undefined,
  ) => {
    if (!sources || typeof sources !== "object") return;
    setGifOverride((prev) => {
      const next = { ...prev };
      for (const [k, v] of Object.entries(sources)) {
        const id = Number(k);
        const src = Number(v);
        if (id > 0 && src > 0 && src !== id) {
          next[id] = src;
        }
      }
      persistGifOverride(next);
      return next;
    });
  };

  const lockGifForItem = (itemId: number, gifItemId: number) => {
    if (itemId < 1 || gifItemId < 1) return;
    setGifOverride((prev) => {
      const next = { ...prev, [itemId]: gifItemId };
      persistGifOverride(next);
      return next;
    });
  };

  /** GIF file to show for itemId. Own server id unless override / pending / shared-cid borrower. */
  const resolveGifItemId = (
    displayCid: number | null | undefined,
    itemId: number,
    otbCid?: number | null,
  ) => {
    const locked = gifOverride[itemId];
    if (locked != null && locked > 0) {
      return locked;
    }
    const pending =
      displayCid != null &&
      displayCid >= 0 &&
      otbCid != null &&
      displayCid !== otbCid;
    if (pending) {
      const peer = peerGifSrc(itemId, displayCid);
      if (peer != null) return peer;
    }
    // After OTB write / reload: item that BORROWS another item's cid should still
    // show the owner's GIF. The cid owner always keeps its own {itemId}.gif
    // (avoids swap illusion and "itens antigos" after Recarregar).
    if (displayCid != null && displayCid >= 0) {
      const owner = cidOwners[String(displayCid)];
      if (owner != null && owner > 0 && owner !== itemId) {
        return owner;
      }
    }
    return itemId;
  };

  const catalogPendingMap = useMemo(() => {
    const map: Record<string, number> = {};
    for (const row of rows) {
      if (!(row.id in cidPreview)) continue;
      const n = cidPreview[row.id];
      if (!Number.isFinite(n) || n < 0 || n > 65535) continue;
      if (row.clientId != null && row.clientId === n) continue;
      map[String(row.id)] = n;
    }
    return map;
  }, [rows, cidPreview]);

  const catalogPendingCount = Object.keys(catalogPendingMap).length;

  const setCidDraft = (itemId: number, value: string) => {
    setCidDrafts((prev) => {
      const next = { ...prev };
      if (value === "") {
        delete next[itemId];
      } else {
        next[itemId] = value;
      }
      return next;
    });
  };

  const previewCatalogCid = (row: Row) => {
    const raw = (cidDrafts[row.id] ?? "").trim();
    const n = parseInt(raw, 10);
    if (!Number.isFinite(n) || n < 0 || n > 65535) {
      setCidMsg("client_id invalido (0-65535).");
      return;
    }
    setCidPreview((prev) => ({ ...prev, [row.id]: n }));
    const peer = peerGifSrc(row.id, n);
    const gifSrc = peer != null ? peer : row.id;
    // Always overwrite (2o remap nao pode ficar preso no GIF do 1o).
    lockGifForItem(row.id, gifSrc);
    setCidMsg(
      peer != null
        ? `Preview cid ${n} (GIF do item ${peer}). Ainda nao gravou no OTB.`
        : `Preview cid ${n} (sem outro item com este cid - GIF pode permanecer o antigo). Ainda nao gravou no OTB.`,
    );
  };

  const applyCatalogClientIds = async (
    map: Record<string, number>,
    dryRun: boolean,
  ) => {
    if (!canWriteOtb) {
      setCidMsg("Aplicar no items.otb exige page_access >= 6.");
      return;
    }
    const keys = Object.keys(map);
    if (keys.length < 1) {
      setCidMsg("Nenhum client_id em preview para gravar. Use Aplicar novo client_id primeiro.");
      return;
    }
    if (!dryRun) {
      const ok = window.confirm(
        `Gravar ${keys.length} client_id(s) no items.otb?\nBackup automatico. Depois reinicie o OT.`,
      );
      if (!ok) return;
    }
    setCidBusy(true);
    setCidMsg("");
    setErr("");
    try {
      // Sempre recalcular pelo cid NOVO (nao reusar override antigo do 1o remap).
      const preLock: Record<string, number> = {};
      for (const [sid, cid] of Object.entries(map)) {
        const id = Number(sid);
        const peer = peerGifSrc(id, Number(cid));
        if (peer != null) preLock[sid] = peer;
      }
      // Force replace overrides for these ids
      setGifOverride((prev) => {
        const next = { ...prev };
        for (const sid of keys) {
          const id = Number(sid);
          delete next[id];
          if (preLock[sid]) next[id] = preLock[sid];
        }
        persistGifOverride(next);
        return next;
      });

      const r = await fetch(apiUrl("items-catalog.php"), {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          action: "apply_client_id",
          map,
          dryRun,
        }),
      });
      const json = await r.json().catch(() => ({}));
      if (!r.ok) {
        setCidMsg(
          typeof json.error === "string" ? json.error : `HTTP ${r.status}`,
        );
        return;
      }
      const patched = json.apply?.patched ?? keys.length;
      if (!dryRun) {
        mergeGifSources(json.gifSources);
        if (Array.isArray(json.applied)) {
          const fromApplied: Record<string, number> = {};
          for (const a of json.applied as {
            itemId: number;
            gifItemId?: number | null;
          }[]) {
            if (a.gifItemId != null && a.gifItemId > 0 && a.gifItemId !== a.itemId) {
              fromApplied[String(a.itemId)] = a.gifItemId;
            }
          }
          mergeGifSources(fromApplied);
        }
        setCidDrafts((prev) => {
          const next = { ...prev };
          for (const sid of keys) delete next[Number(sid)];
          return next;
        });
        setCidPreview((prev) => {
          const next = { ...prev };
          for (const sid of keys) delete next[Number(sid)];
          return next;
        });
        await reloadCatalog({ quiet: true, applyGifSuggest: false });
        setCidMsg(
          typeof json.hint === "string"
            ? `${json.hint} (patched=${patched}). Pagina atualizada.`
            : `OTB atualizado (${patched}). Pagina atualizada. Reinicie o OT.`,
        );
      } else {
        setCidMsg(
          typeof json.hint === "string"
            ? `${json.hint} (patched=${patched})`
            : `Dry-run OK (${patched})`,
        );
      }
    } catch {
      setCidMsg("Falha de rede ao aplicar client_id");
    } finally {
      setCidBusy(false);
    }
  };
  // END CHANGE

  const createSet = async () => {
    const name = window.prompt("Nome do set:");
    if (name == null || !name.trim()) return;
    setSetsBusy(true);
    setSetsMsg("");
    try {
      const r = await fetch(apiUrl("items-sets.php"), {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ action: "create", name: name.trim() }),
      });
      const json = await r.json().catch(() => ({}));
      if (!r.ok) {
        setSetsMsg(
          typeof json.error === "string" ? json.error : `HTTP ${r.status}`,
        );
        return;
      }
      if (Array.isArray(json.sets)) setSets(json.sets);
      if (json.set) {
        setActiveSet(json.set);
        setSetSlots(
          Array.isArray(json.set.slots) ? json.set.slots : emptySlotsFromGrid(),
        );
      }
      setSetsMsg("Set criado.");
    } catch {
      setSetsMsg("Falha de rede ao criar set");
    } finally {
      setSetsBusy(false);
    }
  };

  const renameSet = async () => {
    if (!activeSet) return;
    const name = window.prompt("Novo nome do set:", activeSet.name);
    if (name == null || !name.trim()) return;
    setSetsBusy(true);
    try {
      const r = await fetch(apiUrl("items-sets.php"), {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          action: "rename",
          id: activeSet.id,
          name: name.trim(),
        }),
      });
      const json = await r.json().catch(() => ({}));
      if (!r.ok) {
        setSetsMsg(
          typeof json.error === "string" ? json.error : `HTTP ${r.status}`,
        );
        return;
      }
      if (Array.isArray(json.sets)) setSets(json.sets);
      if (json.set) setActiveSet(json.set);
    } finally {
      setSetsBusy(false);
    }
  };

  const deleteSet = async () => {
    if (!activeSet) return;
    if (!window.confirm(`Apagar set "${activeSet.name}"?`)) return;
    setSetsBusy(true);
    try {
      const r = await fetch(apiUrl("items-sets.php"), {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ action: "delete", id: activeSet.id }),
      });
      const json = await r.json().catch(() => ({}));
      if (!r.ok) {
        setSetsMsg(
          typeof json.error === "string" ? json.error : `HTTP ${r.status}`,
        );
        return;
      }
      setSets(Array.isArray(json.sets) ? json.sets : []);
      setActiveSet(null);
      setSetSlots(emptySlotsFromGrid());
      setSetsMsg("Set apagado.");
      if (Array.isArray(json.sets) && json.sets[0]) {
        await openSet(json.sets[0].id);
      }
    } finally {
      setSetsBusy(false);
    }
  };

  const saveSetSlots = async () => {
    if (!activeSet) return;
    setSetsBusy(true);
    setSetsMsg("");
    try {
      const r = await fetch(apiUrl("items-sets.php"), {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          action: "save",
          id: activeSet.id,
          slots: setSlots.map((s) => ({
            slot: s.slot,
            itemId: s.itemId,
            pendingClientId: s.pendingClientId,
          })),
        }),
      });
      const json = await r.json().catch(() => ({}));
      if (!r.ok) {
        setSetsMsg(
          typeof json.error === "string" ? json.error : `HTTP ${r.status}`,
        );
        return;
      }
      if (Array.isArray(json.sets)) setSets(json.sets);
      if (json.set) {
        setActiveSet(json.set);
        setSetSlots(json.set.slots);
      }
      setSetsMsg("Set salvo.");
    } catch {
      setSetsMsg("Falha de rede ao salvar set");
    } finally {
      setSetsBusy(false);
    }
  };

  const applyOtb = async (dryRun: boolean) => {
    if (!activeSet) return;
    if (!dryRun) {
      const ok = window.confirm(
        `Gravar ${pendingCount} client_id(s) no items.otb?\nBackup automatico. Depois reinicie o OT.`,
      );
      if (!ok) return;
    }
    setSetsBusy(true);
    setSetsMsg("");
    try {
      // Persist pending CIDs first so apply reads DB
      const saveRes = await fetch(apiUrl("items-sets.php"), {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          action: "save",
          id: activeSet.id,
          slots: setSlots.map((s) => ({
            slot: s.slot,
            itemId: s.itemId,
            pendingClientId: s.pendingClientId,
          })),
        }),
      });
      const saveJson = await saveRes.json().catch(() => ({}));
      if (!saveRes.ok) {
        setSetsMsg(
          typeof saveJson.error === "string"
            ? saveJson.error
            : `Falha ao salvar antes do apply (HTTP ${saveRes.status})`,
        );
        return;
      }

      const r = await fetch(apiUrl("items-sets.php"), {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          action: "apply_otb",
          id: activeSet.id,
          dryRun,
        }),
      });
      const json = await r.json().catch(() => ({}));
      if (!r.ok) {
        setSetsMsg(
          typeof json.error === "string" ? json.error : `HTTP ${r.status}`,
        );
        return;
      }
      if (Array.isArray(json.sets)) setSets(json.sets);
      if (json.set) {
        setActiveSet(json.set);
        setSetSlots(json.set.slots);
      }
      const patched = json.apply?.patched ?? pendingCount;
      if (!dryRun) {
        setSlotCidDrafts({});
        mergeGifSources(json.gifSources);
        // Refresh reverse map so catalog/set previews stay aligned with OTB
        try {
          const cr = await fetch(
            apiUrl("items-catalog.php?page=0&pageSize=24&filter=all"),
            { headers: { Authorization: `Bearer ${token}` } },
          );
          const cj = await cr.json().catch(() => ({}));
          if (cr.ok && cj.cidOwners && typeof cj.cidOwners === "object") {
            setCidOwners(cj.cidOwners as Record<string, number>);
          }
        } catch {
          /* ignore */
        }
        setSetsMsg(
          typeof json.hint === "string"
            ? `${json.hint} (patched=${patched}). Set atualizado.`
            : `OTB atualizado (${patched}). Set atualizado. Reinicie o OT.`,
        );
      } else {
        setSetsMsg(
          typeof json.hint === "string"
            ? `${json.hint} (patched=${patched})`
            : `Dry-run OK (${patched})`,
        );
      }
    } catch {
      setSetsMsg("Falha de rede ao aplicar OTB");
    } finally {
      setSetsBusy(false);
    }
  };

  const assignItemToSlot = (slot: number, row: Row) => {
    setSetSlots((list) =>
      list.map((s) =>
        s.slot === slot
          ? {
              ...s,
              itemId: row.id,
              name: row.name,
              clientId: row.clientId,
              hasGif: row.hasGif,
              pendingClientId: null,
            }
          : s,
      ),
    );
    setSlotCidDrafts((prev) => {
      const next = { ...prev };
      delete next[slot];
      return next;
    });
  };

  const clearSlot = (slot: number) => {
    setSetSlots((list) =>
      list.map((s) =>
        s.slot === slot
          ? {
              ...s,
              itemId: null,
              name: null,
              clientId: null,
              hasGif: false,
              pendingClientId: null,
            }
          : s,
      ),
    );
    setSlotCidDrafts((prev) => {
      const next = { ...prev };
      delete next[slot];
      return next;
    });
  };

  const setSlotCidDraft = (slot: number, raw: string) => {
    setSlotCidDrafts((prev) => {
      const next = { ...prev };
      if (raw.trim() === "") delete next[slot];
      else next[slot] = raw;
      return next;
    });
  };

  const previewSlotCid = (slot: number) => {
    const raw = (slotCidDrafts[slot] ?? "").trim();
    if (raw === "") {
      setSetSlots((list) =>
        list.map((s) =>
          s.slot === slot ? { ...s, pendingClientId: null } : s,
        ),
      );
      setSetsMsg("Preview limpo neste slot.");
      return;
    }
    const n = parseInt(raw, 10);
    if (!Number.isFinite(n) || n < 0 || n > 65535) {
      setSetsMsg("client_id invalido (0-65535).");
      return;
    }
    const itemId = slotById.get(slot)?.itemId;
    setSetSlots((list) =>
      list.map((s) =>
        s.slot === slot ? { ...s, pendingClientId: n } : s,
      ),
    );
    const peer = itemId != null ? peerGifSrc(itemId, n) : null;
    if (itemId != null) {
      lockGifForItem(itemId, peer != null ? peer : itemId);
    }
    setSetsMsg(
      peer != null
        ? `Preview cid ${n} no slot (GIF do item ${peer}). Ainda nao gravou no OTB.`
        : `Preview cid ${n} no slot (sem outro item com este cid - GIF pode permanecer o antigo). Ainda nao gravou no OTB.`,
    );
  };

  const setPendingCid = (slot: number, raw: string) => {
    setSlotCidDraft(slot, raw);
  };

  const sourceFilters: { id: SourceFilter; label: string }[] = [
    { id: "all", label: "Todos" },
    { id: "named", label: "Com nome (XML+GIF)" },
    { id: "unnamed", label: "GIF sem nome" },
    { id: "xml_only", label: "XML sem GIF" },
  ];

  const grid = activeSet?.grid ?? EMPTY_GRID;
  const selected = selectedSlot != null ? slotById.get(selectedSlot) : null;

  return (
    <div className="space-y-4">
      <div>
        <h2 className="text-xl font-semibold">Items</h2>
        <p className="mt-1 text-sm text-muted">
          Catalogo / Sets: digite o cid, clique Aplicar novo client_id para ver
          o GIF na hora, depois Gravar no items.otb (page_access &gt;= 6).
        </p>
      </div>

      <div className="flex flex-wrap gap-2">
        {(
          [
            { id: "catalog" as const, label: "Catalogo" },
            { id: "sets" as const, label: "Sets (inventario)" },
          ] as const
        ).map((t) => (
          <button
            key={t.id}
            type="button"
            onClick={() => setTab(t.id)}
            className={
              tab === t.id
                ? "rounded-md bg-brand px-3 py-1.5 text-sm text-white"
                : "rounded-md border border-border px-3 py-1.5 text-sm hover:bg-muted/40"
            }
          >
            {t.label}
          </button>
        ))}
        <button
          type="button"
          disabled={cidBusy || setsBusy}
          onClick={() => void reloadOtbFromDisk()}
          className="rounded-md border border-border px-3 py-1.5 text-sm hover:bg-muted/40 disabled:opacity-40"
          title="Ler de novo o items.otb do disco e atualizar cids na pagina"
        >
          Recarregar OTB
        </button>
      </div>

      {tab === "catalog" && (
        <>
          {stats && (
            <p className="text-xs text-muted">
              GIFs: {stats.gifCount} | XML: {stats.namedCount}
              {typeof stats.clientIdCount === "number"
                ? ` | OTB cids: ${stats.clientIdCount}`
                : ""}
              {typeof stats.classifiedCount === "number"
                ? ` | classificados: ${stats.classifiedCount}`
                : ""}
              {/* BEGIN CHANGE: OTB container count in admin items stats */}
              {typeof stats.containerCount === "number"
                ? ` | OTB containers: ${stats.containerCount}`
                : ""}
              {/* END CHANGE */}
            </p>
          )}

          <div className="flex flex-wrap items-center gap-2">
            {sourceFilters.map((f) => (
              <button
                key={f.id}
                type="button"
                onClick={() => {
                  setFilter(f.id);
                }}
                className={
                  filter === f.id
                    ? "rounded-md bg-brand px-3 py-1.5 text-sm text-white"
                    : "rounded-md border border-border px-3 py-1.5 text-sm hover:bg-muted/40"
                }
              >
                {f.label}
              </button>
            ))}
            <form
              className="flex min-w-[220px] flex-1 gap-2"
              onSubmit={(e) => {
                e.preventDefault();
                setQ(qDraft);
              }}
            >
              <input
                className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
                placeholder="Filtrar por item_id, cid ou nome..."
                value={qDraft}
                onChange={(e) => setQDraft(e.target.value)}
              />
              <button
                type="submit"
                className="rounded-md border border-border px-3 py-2 text-sm hover:bg-muted/40"
              >
                Buscar
              </button>
            </form>
          </div>

          <div className="flex flex-wrap items-center gap-2">
            <span className="text-sm text-muted">Classe:</span>
            <select
              className="rounded-md border border-border bg-background px-3 py-1.5 text-sm"
              value={classFilter}
              onChange={(e) => {
                const v = e.target.value;
                setClassFilter(v);
                if (v === "__none__") {
                  setRows([]);
                  setTotal(0);
                  setStatus("idle");
                }
              }}
              aria-label="Filtrar por classe"
            >
              <option value="__none__">Escolha uma classe...</option>
              <option value="">Todas (pesado)</option>
              <option value="unclassified">Sem classe</option>
              {classes.map((c) => (
                <option key={c.value} value={c.value}>
                  {c.label}
                  {classCounts[c.value] != null
                    ? ` (${classCounts[c.value]})`
                    : ""}
                </option>
              ))}
            </select>
          </div>

          {status === "idle" && classFilter === "__none__" && (
            <p className="text-sm text-muted">
              Selecione uma classe acima para carregar o catalogo. Use
              &quot;Todas&quot; so se precisar da lista completa.
            </p>
          )}

          {status === "loading" && (
            <p className="text-sm text-muted">Carregando...</p>
          )}
          {status === "error" && <p className="text-sm text-red-600">{err}</p>}
          {status === "ok" && err && (
            <p className="text-sm text-red-600">{err}</p>
          )}
          {/* BEGIN CHANGE: catalog cid batch bar */}
          {status === "ok" && canWriteOtb && (
            <div className="flex flex-wrap items-center gap-2 rounded-md border border-border bg-background/60 px-3 py-2 text-sm">
              <span className="text-muted">
                Em preview nesta pagina: {catalogPendingCount} (ainda nao no
                OTB)
              </span>
              <button
                type="button"
                className="rounded border border-border px-2 py-1 hover:bg-muted/40 disabled:opacity-40"
                disabled={cidBusy || catalogPendingCount < 1}
                onClick={() =>
                  void applyCatalogClientIds(catalogPendingMap, true)
                }
              >
                Dry-run (sem gravar)
              </button>
              <button
                type="button"
                className="rounded border border-amber-600 px-2 py-1 text-amber-800 hover:bg-amber-50 disabled:opacity-40"
                disabled={cidBusy || catalogPendingCount < 1}
                onClick={() =>
                  void applyCatalogClientIds(catalogPendingMap, false)
                }
              >
                Gravar no items.otb
              </button>
              {cidMsg && <span className="text-muted">{cidMsg}</span>}
            </div>
          )}
          {status === "ok" && !canWriteOtb && (
            <p className="text-xs text-muted">
              Use &quot;Aplicar novo client_id&quot; para ver o GIF. Gravar no OTB
              exige page_access &gt;= 6.
              {cidMsg ? ` ${cidMsg}` : ""}
            </p>
          )}
          {status === "ok" && canWriteOtb && cidMsg && catalogPendingCount < 1 && (
            <p className="text-xs text-muted">{cidMsg}</p>
          )}
          {/* END CHANGE */}

          {status === "ok" && (
            <div className="space-y-3">
              {/* BEGIN CHANGE: full list with scroll; larger pixelated icons */}
              <p className="text-sm text-muted">{total} itens nesta lista (scroll)</p>
              <div className="max-h-[75vh] overflow-auto rounded-md border border-border p-2">
              <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
                {rows.map((row) => {
                  const previewCid =
                    row.id in cidPreview ? cidPreview[row.id] : null;
                  const displayCid =
                    previewCid != null
                      ? previewCid
                      : (row.clientId ?? null);
                  const gifId = resolveGifItemId(
                    displayCid,
                    row.id,
                    row.clientId,
                  );
                  const showPreviewBadge =
                    previewCid != null && previewCid !== row.clientId;
                  return (
                  <div
                    key={row.id}
                    className="flex flex-col items-center gap-1 rounded-md border border-border bg-background/60 p-2 text-center"
                  >
                    <ItemIcon
                      key={`gif-${row.id}-${gifId}-${displayCid ?? "x"}`}
                      id={gifId}
                      alt={row.name}
                      size={64}
                      pixelated
                    />
                    <div className="font-mono text-sm font-semibold text-brand">
                      {row.id}
                    </div>
                    {/* BEGIN CHANGE: preview cid before OTB write */}
                    <div className="flex w-full flex-col items-center gap-1">
                      <label className="flex w-full items-center justify-center gap-1 font-mono text-[11px] text-muted">
                        <span>cid</span>
                        <input
                          type="number"
                          min={0}
                          max={65535}
                          className="w-16 rounded border border-border bg-background px-1 py-0.5 text-center font-mono text-[11px]"
                          value={
                            row.id in cidDrafts
                              ? cidDrafts[row.id]
                              : row.clientId != null
                                ? String(row.clientId)
                                : ""
                          }
                          disabled={cidBusy}
                          onChange={(e) =>
                            setCidDraft(row.id, e.target.value)
                          }
                          onKeyDown={(e) => {
                            if (e.key === "Enter") {
                              e.preventDefault();
                              previewCatalogCid(row);
                            }
                          }}
                          aria-label={`client_id do item ${row.id}`}
                        />
                      </label>
                      {showPreviewBadge && (
                        <span className="text-[10px] text-amber-700">
                          preview {row.clientId ?? "-"} -&gt; {previewCid}
                        </span>
                      )}
                      <button
                        type="button"
                        className="rounded border border-border px-1.5 py-0.5 text-[10px] hover:bg-muted/40 disabled:opacity-40"
                        disabled={cidBusy}
                        onClick={() => previewCatalogCid(row)}
                      >
                        Aplicar novo client_id
                      </button>
                    </div>
                    {/* END CHANGE */}
                    <div className="line-clamp-2 text-xs leading-tight">
                      {row.name}
                    </div>
                    <select
                      className="mt-1 w-full rounded border border-border bg-background px-1 py-1 text-[11px]"
                      value={row.class || ""}
                      disabled={savingId === row.id}
                      onChange={(e) => {
                        const v = e.target.value;
                        if (v === "__new__") {
                          e.target.value = row.class || "";
                          void createClassForItem(row.id);
                          return;
                        }
                        void saveClass(row.id, v);
                      }}
                      aria-label={`Classe do item ${row.id}`}
                    >
                      <option value="">Sem classe</option>
                      {classes.map((c) => (
                        <option key={c.value} value={c.value}>
                          {c.label}
                        </option>
                      ))}
                      <option value="__new__">+ Criar classe...</option>
                    </select>
                  </div>
                  );
                })}
              </div>
              </div>
              {/* END CHANGE */}

              {rows.length === 0 && (
                <p className="text-sm text-muted">
                  Nenhum resultado para o filtro.
                </p>
              )}
            </div>
          )}
        </>
      )}

      {tab === "sets" && (
        <div className="space-y-4">
          <p className="text-sm text-muted">
            Layout = inventario do player (10 slots). Escolha o item no slot,
            informe o novo client_id e aplique no items.otb (so CID; item_id
            fica fixo). Acao de gravar exige page_access &gt;= 6.
          </p>

          <div className="flex flex-wrap items-center gap-2">
            <select
              className="min-w-[180px] rounded-md border border-border bg-background px-3 py-1.5 text-sm"
              value={activeSet?.id ?? ""}
              disabled={setsBusy}
              onChange={(e) => {
                const id = parseInt(e.target.value, 10);
                if (Number.isFinite(id)) void openSet(id);
              }}
            >
              {sets.length === 0 && <option value="">Nenhum set</option>}
              {sets.map((s) => (
                <option key={s.id} value={s.id}>
                  {s.name} ({s.filled}/10
                  {s.pending ? `, ${s.pending} pend.` : ""})
                </option>
              ))}
            </select>
            <button
              type="button"
              className="rounded-md border border-border px-3 py-1.5 text-sm hover:bg-muted/40"
              disabled={setsBusy}
              onClick={() => void createSet()}
            >
              Novo set
            </button>
            <button
              type="button"
              className="rounded-md border border-border px-3 py-1.5 text-sm hover:bg-muted/40 disabled:opacity-40"
              disabled={setsBusy || !activeSet}
              onClick={() => void renameSet()}
            >
              Renomear
            </button>
            <button
              type="button"
              className="rounded-md border border-border px-3 py-1.5 text-sm text-red-700 hover:bg-muted/40 disabled:opacity-40"
              disabled={setsBusy || !activeSet}
              onClick={() => void deleteSet()}
            >
              Apagar
            </button>
            <button
              type="button"
              className="rounded-md bg-brand px-3 py-1.5 text-sm text-white disabled:opacity-40"
              disabled={setsBusy || !activeSet}
              onClick={() => void saveSetSlots()}
            >
              Salvar set
            </button>
            <button
              type="button"
              className="rounded-md border border-border px-3 py-1.5 text-sm hover:bg-muted/40 disabled:opacity-40"
              disabled={setsBusy || !activeSet || pendingCount < 1}
              onClick={() => void applyOtb(true)}
            >
              Dry-run (sem gravar)
            </button>
            <button
              type="button"
              className="rounded-md border border-amber-600 px-3 py-1.5 text-sm text-amber-800 hover:bg-amber-50 disabled:opacity-40"
              disabled={setsBusy || !activeSet || pendingCount < 1}
              onClick={() => void applyOtb(false)}
            >
              Gravar no items.otb ({pendingCount})
            </button>
          </div>

          {setsMsg && <p className="text-sm text-muted">{setsMsg}</p>}

          {!activeSet && (
            <p className="text-sm text-muted">
              Crie um set para montar o inventario.
            </p>
          )}

          {activeSet && (
            <div className="grid gap-4 lg:grid-cols-[auto_1fr]">
              <div className="space-y-2">
                <div className="inline-grid grid-cols-3 gap-2 rounded-md border border-border bg-background/60 p-3">
                  {grid.map((row, ri) =>
                    row.map((slotId, ci) => {
                      if (slotId == null) {
                        return (
                          <div
                            key={`empty-${ri}-${ci}`}
                            className="h-[88px] w-[88px]"
                          />
                        );
                      }
                      const slot = slotById.get(slotId);
                      const active = selectedSlot === slotId;
                      const displayCid =
                        slot?.pendingClientId != null
                          ? slot.pendingClientId
                          : (slot?.clientId ?? null);
                      const gifId =
                        slot?.itemId != null
                          ? resolveGifItemId(
                              displayCid,
                              slot.itemId,
                              slot.clientId,
                            )
                          : null;
                      return (
                        <button
                          key={slotId}
                          type="button"
                          onClick={() => setSelectedSlot(slotId)}
                          className={
                            active
                              ? "flex h-[88px] w-[88px] flex-col items-center justify-center gap-0.5 rounded border-2 border-brand bg-brand/10 p-1 text-center"
                              : "flex h-[88px] w-[88px] flex-col items-center justify-center gap-0.5 rounded border border-border bg-background p-1 text-center hover:bg-muted/30"
                          }
                          title={slot?.label || `Slot ${slotId}`}
                        >
                          <span className="text-[10px] text-muted">
                            {slot?.label || slotId}
                          </span>
                          {gifId != null ? (
                            <ItemIcon
                              key={`slot-gif-${slotId}-${gifId}-${displayCid ?? "x"}`}
                              id={gifId}
                              alt={slot?.name || ""}
                              size={48}
                              pixelated
                            />
                          ) : (
                            // eslint-disable-next-line @next/next/no-img-element
                            <img
                              src={equipmentSlotUrl(slotId)}
                              alt=""
                              width={32}
                              height={32}
                              className="object-contain opacity-70"
                            />
                          )}
                          {slot?.itemId != null && (
                            <span className="font-mono text-[10px] text-muted">
                              cid{" "}
                              {slot.pendingClientId != null
                                ? `${slot.clientId ?? "-"}->${slot.pendingClientId}`
                                : (slot.clientId ?? "-")}
                            </span>
                          )}
                        </button>
                      );
                    }),
                  )}
                </div>
                <p className="max-w-[280px] text-xs text-muted">
                  Layout legado: ring / legs / ammo; boots embaixo no centro.
                  Clique no slot, informe o cid e use Aplicar novo client_id
                  para ver o GIF antes de gravar no OTB.
                </p>
              </div>

              <div className="space-y-3 rounded-md border border-border p-3">
                {!selected ? (
                  <p className="text-sm text-muted">Nenhum slot selecionado.</p>
                ) : (
                  <>
                    <div className="flex flex-wrap items-start justify-between gap-2">
                      <div>
                        <h3 className="font-semibold">{selected.label}</h3>
                        {selected.itemId != null ? (
                          <p className="text-sm text-muted">
                            item_id {selected.itemId}
                            {selected.name ? ` - ${selected.name}` : ""} | cid
                            atual {selected.clientId ?? "-"}
                          </p>
                        ) : (
                          <p className="text-sm text-muted">Slot vazio</p>
                        )}
                      </div>
                      {selected.itemId != null && (
                        <button
                          type="button"
                          className="rounded border border-border px-2 py-1 text-xs hover:bg-muted/40"
                          onClick={() => clearSlot(selected.slot)}
                        >
                          Limpar slot
                        </button>
                      )}
                    </div>

                    {selected.itemId != null && (
                      <div className="space-y-2">
                        <div className="flex items-center gap-3">
                          <ItemIcon
                            key={`sel-${selected.itemId}-${selected.pendingClientId ?? selected.clientId ?? "x"}`}
                            id={resolveGifItemId(
                              selected.pendingClientId != null
                                ? selected.pendingClientId
                                : selected.clientId,
                              selected.itemId,
                              selected.clientId,
                            )}
                            alt={selected.name || ""}
                            size={48}
                            pixelated
                          />
                          <label className="block flex-1 text-sm">
                            <span className="text-muted">
                              Novo client_id (preview antes do OTB)
                            </span>
                            <input
                              type="number"
                              min={0}
                              max={65535}
                              className="mt-1 w-40 rounded-md border border-border bg-background px-3 py-1.5 font-mono text-sm"
                              value={
                                selected.slot in slotCidDrafts
                                  ? slotCidDrafts[selected.slot]
                                  : selected.pendingClientId != null
                                    ? String(selected.pendingClientId)
                                    : selected.clientId != null
                                      ? String(selected.clientId)
                                      : ""
                              }
                              onChange={(e) =>
                                setPendingCid(selected.slot, e.target.value)
                              }
                              onKeyDown={(e) => {
                                if (e.key === "Enter") {
                                  e.preventDefault();
                                  previewSlotCid(selected.slot);
                                }
                              }}
                              placeholder={
                                selected.clientId != null
                                  ? String(selected.clientId)
                                  : "cid"
                              }
                            />
                          </label>
                        </div>
                        <button
                          type="button"
                          className="rounded-md border border-border px-3 py-1.5 text-sm hover:bg-muted/40"
                          onClick={() => previewSlotCid(selected.slot)}
                        >
                          Aplicar novo client_id
                        </button>
                        {selected.pendingClientId != null &&
                          selected.pendingClientId !== selected.clientId && (
                            <p className="text-xs text-amber-700">
                              Preview ativo: {selected.clientId ?? "-"} -&gt;{" "}
                              {selected.pendingClientId}. Use Gravar no
                              items.otb para confirmar.
                            </p>
                          )}
                      </div>
                    )}

                    <div className="flex flex-wrap items-center gap-2">
                      <input
                        className="min-w-[160px] flex-1 rounded-md border border-border bg-background px-3 py-1.5 text-sm"
                        placeholder="Buscar item (id, cid, nome)..."
                        value={pickerQ}
                        onChange={(e) => setPickerQ(e.target.value)}
                      />
                      <select
                        className="rounded-md border border-border bg-background px-2 py-1.5 text-sm"
                        value={pickerClass}
                        onChange={(e) => setPickerClass(e.target.value)}
                      >
                        <option value="">Todas classes</option>
                        {classes.map((c) => (
                          <option key={c.value} value={c.value}>
                            {c.label}
                          </option>
                        ))}
                      </select>
                    </div>

                    <div className="grid max-h-[50vh] grid-cols-2 gap-2 overflow-y-auto sm:grid-cols-3 md:grid-cols-4">
                      {pickerRows.map((row) => (
                        <button
                          key={row.id}
                          type="button"
                          onClick={() => assignItemToSlot(selected.slot, row)}
                          className="flex flex-col items-center gap-1 rounded border border-border p-2 text-center hover:border-brand hover:bg-brand/5"
                        >
                          {row.hasGif ? (
                            <ItemIcon id={row.id} alt={row.name} size={48} pixelated />
                          ) : (
                            <div
                              className="flex items-center justify-center rounded bg-muted/40 text-[10px] text-muted"
                              style={{ width: 48, height: 48 }}
                            >
                              -
                            </div>
                          )}
                          <span className="font-mono text-xs text-brand">
                            {row.id}
                          </span>
                          <span className="line-clamp-2 text-[11px]">
                            {row.name}
                          </span>
                        </button>
                      ))}
                    </div>
                  </>
                )}
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}
// END CHANGE
