/* shared.jsx — common dashboard chrome: Sidebar, TopBar, KpiTile,
   MiniSpark, AreaChart, BarChart, Donut. */

const { useState: useStateS, useRef: useRefS, useEffect: useEffS, useMemo: useMemoS } = React;

/* ===================================================================
   SIDEBAR
   =================================================================== */

/* Map between platform key and the sidebar item it renders. Order here is
   the order the items appear under Workspace. A platform shows up only when
   it is enabled in client.platforms AND has an account_id set.

   Shopify additionally requires client.type === "ecom" (lead-gen clients
   never have a Shopify tab even if the platform is connected). */
const PLATFORM_NAV = [
  { id: "meta",      key: "meta",      icon: "meta",        label_key: "nav.meta",      requires_ecom: false },
  { id: "google",    key: "google",    icon: "google",      label_key: "nav.google",    requires_ecom: false },
  { id: "tiktok",    key: "tiktok",    icon: "tiktok",      label_key: "nav.tiktok",    requires_ecom: false },
  { id: "analytics", key: "analytics", icon: "bar-chart",   label_key: "nav.analytics", requires_ecom: false },
  { id: "shopify",   key: "shopify",   icon: "shopify-bag", label_key: "nav.shopify",   requires_ecom: true  }
];

function visiblePlatformItems(client, lang) {
  const platforms = client.platforms || {};
  return PLATFORM_NAV
    .filter(p => {
      const pl = platforms[p.key];
      if (!pl || !pl.enabled || !pl.account_id) return false;
      if (p.requires_ecom && client.type !== "ecom") return false;
      return true;
    })
    .map(p => ({ id: p.id, icon: p.icon, label: t(p.label_key, lang) }));
}
window.visiblePlatformItems = visiblePlatformItems;

function Sidebar({ active, onNavigate, client, lang, role, onLogout, mobileOpen, onMobileClose }) {
  const initials = client.name.split(" ").map(w => w[0]).join("").slice(0,2).toUpperCase();
  const isAdmin = role === "admin";

  // On mobile the sidebar is a slide-in panel; tapping any nav item should
  // dismiss it so the user lands on the new page with the menu out of the way.
  const navigate = (id) => {
    onNavigate(id);
    onMobileClose && onMobileClose();
  };

  const platformItems = visiblePlatformItems(client, lang);
  const groups = [
    {
      label: t("nav.workspace", lang),
      items: [
        { id: "overview", icon: "home", label: t("nav.overview", lang) },
        ...platformItems
      ]
    },
    // Account section only for admins
    ...(isAdmin ? [{
      label: t("nav.account", lang),
      items: [
        { id: "settings", icon: "settings", label: t("nav.settings", lang) }
      ]
    }] : [])
  ];

  return (
    <aside className={"side" + (mobileOpen ? " is-mobile-open" : "")}>
      <div className="side__brand">
        <img src="assets/logo-horizontal-color.png" alt="Makali"/>
      </div>

      {/* Static client card — name + category */}
      <div className="side__client side__client--static">
        <span className="side__client-mark">
          {client.logo_url
            ? <img src={client.logo_url} alt={client.name} style={{width:"100%", height:"100%", objectFit:"cover", borderRadius:"inherit"}}/>
            : initials}
        </span>
        <div style={{minWidth:0}}>
          <div className="side__client-name">{client.name}</div>
          <div className="side__client-meta">
            {client.category}
          </div>
        </div>
      </div>

      {groups.map((g, gi) => (
        <div key={gi} className="side__group">
          <div className="side__label">{g.label}</div>
          {g.items.map(it => (
            <button
              key={it.id}
              className={"side__item" + (active === it.id ? " is-active" : "")}
              onClick={() => navigate(it.id)}
            >
              <Icon name={it.icon} size={17}/>
              <span>{it.label}</span>
              {it.count ? <span className="side__item-count">{it.count}</span> : null}
            </button>
          ))}
        </div>
      ))}

      <div className="side__foot">
        <div className="side__user" onClick={onLogout}>
          <div className="side__avatar">{isAdmin ? "MK" : "RM"}</div>
          <div style={{minWidth:0}}>
            <div className="side__uname">{isAdmin ? (lang === "pt" ? "Equipe Makali" : "Makali Team") : "Renata Marçal"}</div>
            <div className={"side__urole" + (isAdmin ? " side__urole--admin" : "")}>
              {t(isAdmin ? "nav.user.role.admin" : "nav.user.role.client", lang)}
            </div>
          </div>
          <Icon name="logout" size={14} style={{color:"var(--fg-2)"}}/>
        </div>
      </div>
    </aside>
  );
}
window.Sidebar = Sidebar;

/* TOP BAR — sticky filter row with breadcrumbs + date range only.
   On mobile the hamburger is the only entry point to the sidebar.
   `onSync` is wired to the "synced … ago" chip — clicking it bumps a tick
   in app state that every page subscribes to via usePlatformData. */
function TopBar({ crumb, lang, range, onRange, onMenuClick, onSync }) {
  const ranges = ["top.last7", "top.last30", "top.last90", "top.custom"];
  const [spinning, setSpinning] = useStateS(false);
  const handleSync = () => {
    if (spinning) return;
    setSpinning(true);
    onSync && onSync();
    // Visual feedback only — actual fetch lifecycle is per-page.
    setTimeout(() => setSpinning(false), 900);
  };
  return (
    <header className="top">
      <button
        type="button"
        className="hamburger"
        onClick={onMenuClick}
        aria-label={lang === "pt" ? "Abrir menu" : "Open menu"}
      >
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
          <line x1="4" y1="7"  x2="20" y2="7"/>
          <line x1="4" y1="12" x2="20" y2="12"/>
          <line x1="4" y1="17" x2="20" y2="17"/>
        </svg>
      </button>
      <div className="top__crumbs">
        <span>{t("crumbs.performance", lang)}</span>
        <span className="slash">/</span>
        <b>{crumb}</b>
      </div>

      <div className="top__filters" style={{marginLeft:18}}>
        <div className="filter-group">
          <span className="filter-eyebrow">{t("top.reporting", lang)}</span>
          <button className="pill" onClick={() => onRange((ranges.indexOf(range)+1) % ranges.length)}>
            <Icon name="calendar" size={13}/>
            <span>{t(range, lang)}</span>
            <span className="pill__mute">· {t("top.compare", lang)}</span>
            <Icon name="chevron-down" size={12}/>
          </button>
        </div>
      </div>

      <div className="top__right">
        <button
          type="button"
          className={"sync-chip sync-chip--btn" + (spinning ? " is-syncing" : "")}
          onClick={handleSync}
          title={t("top.sync-now", lang)}
          aria-label={t("top.sync-now", lang)}
        >
          <span className={"sync-dot" + (spinning ? " sync-dot--spin" : "")}/>
          <span className="sync-chip__label">{spinning ? t("top.sync-now", lang) + "…" : t("top.synced", lang)}</span>
        </button>
        <button className="icon-btn" title="Info"><Icon name="info" size={15}/></button>
        <button className="icon-btn" title="Notifications">
          <Icon name="bell" size={15}/>
          <span className="dot"/>
        </button>
        <button className="icon-btn" title="User">
          <span style={{
            width:24, height:24, borderRadius:999,
            background:"var(--mk-gradient-diag)",
            display:"grid", placeItems:"center",
            fontFamily:"var(--font-display)", fontWeight:900,
            fontSize:9, color:"#000"
          }}>RM</span>
        </button>
      </div>
    </header>
  );
}
window.TopBar = TopBar;

/* ===================================================================
   KPI TILE
   =================================================================== */
function MiniSpark({ points, color = "#00E7FF", down = false, width = 70, height = 24, fill = false }) {
  if (!points || !points.length) return null;
  const min = Math.min(...points), max = Math.max(...points);
  const span = max - min || 1;
  const step = width / (points.length - 1);
  const pts = points.map((p,i) => `${i*step},${height - ((p-min)/span) * (height-4) - 2}`).join(" ");
  const stroke = down ? "#EF1166" : color;
  const id = "spk-" + Math.random().toString(36).slice(2, 9);
  const lastY = height - ((points[points.length-1]-min)/span) * (height-4) - 2;
  const firstY = height - ((points[0]-min)/span) * (height-4) - 2;
  return (
    <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>
      {fill ? (
        <>
          <defs>
            <linearGradient id={id} x1="0" x2="0" y1="0" y2="1">
              <stop offset="0%" stopColor={stroke} stopOpacity=".35"/>
              <stop offset="100%" stopColor={stroke} stopOpacity="0"/>
            </linearGradient>
          </defs>
          <polygon
            points={`0,${height} ${pts} ${width},${height}`}
            fill={`url(#${id})`}
          />
        </>
      ) : null}
      <polyline points={pts} fill="none" stroke={stroke} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  );
}
window.MiniSpark = MiniSpark;

function KpiTile({ label, icon, value, delta, deltaDir = "up", deltaCtx, spark, sparkColor }) {
  const dirClass = deltaDir === "neutral" ? "neutral" : (deltaDir === "up" ? "up" : "down");
  return (
    <div className="kpi">
      <div className="kpi__label">
        {icon ? <Icon name={icon} size={12}/> : null}
        <span>{label}</span>
      </div>
      <div className="kpi__val">{value}</div>
      <div className="kpi__delta-row">
        {delta ? (
          <span className={"kpi__delta " + dirClass}>
            {deltaDir === "up" && <Icon name="arrow-up-right" size={10}/>}
            {deltaDir === "down" && <Icon name="arrow-down-right" size={10}/>}
            {delta}
          </span>
        ) : null}
        {deltaCtx ? <span className="kpi__delta-ctx">{deltaCtx}</span> : null}
      </div>
      {spark ? (
        <div className="kpi__spark">
          <MiniSpark points={spark} down={deltaDir === "down"} color={sparkColor}/>
        </div>
      ) : null}
    </div>
  );
}
window.KpiTile = KpiTile;

/* ===================================================================
   AREA + BAR CHART — daily performance composite
   =================================================================== */
function StackedBarLine({
  bars,         // [{name, color, data:[number per day]}]  — stacked
  line,         // {name, color, data:[number per day]}
  labels,       // [string per day]
  yFormatLeft,  // (v) => string  (for bars axis, left)
  yFormatRight, // (v) => string  (for line axis, right)
  height = 280
}) {
  const wrapRef = useRefS(null);
  const [width, setWidth] = useStateS(640);
  const [hover, setHover] = useStateS(null);
  const padL = 50, padR = 50, padT = 16, padB = 30;

  useEffS(() => {
    const m = () => wrapRef.current && setWidth(wrapRef.current.clientWidth);
    m();
    window.addEventListener("resize", m);
    return () => window.removeEventListener("resize", m);
  }, []);

  const innerW = Math.max(120, width - padL - padR);
  const innerH = height - padT - padB;
  const n = labels.length;
  const colW = innerW / n;
  const barW = Math.max(4, colW * 0.55);

  /* axis scales */
  const stackedMaxes = labels.map((_,i) =>
    bars.reduce((s,b) => s + (b.data[i] || 0), 0)
  );
  const barMax = Math.max(1, ...stackedMaxes) * 1.15;
  const lineMax = line ? Math.max(1, ...line.data) * 1.15 : 1;

  const yBar = (v) => padT + innerH - (v / barMax) * innerH;
  const yLine = (v) => padT + innerH - (v / lineMax) * innerH;
  const xCol = (i) => padL + colW * (i + 0.5);

  /* line path */
  const linePath = line ? line.data.map((v,i) => `${i===0?"M":"L"}${xCol(i)},${yLine(v)}`).join(" ") : "";

  /* gridlines */
  const ticks = 4;
  const tickVals = Array.from({length: ticks + 1}, (_,i) => (barMax / ticks) * i);

  const onMove = (e) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const i = Math.floor((x - padL) / colW);
    if (i >= 0 && i < n) setHover(i); else setHover(null);
  };

  return (
    <div className="chart-wrap" ref={wrapRef} onMouseMove={onMove} onMouseLeave={() => setHover(null)}>
      <svg viewBox={`0 0 ${width} ${height}`} height={height}>
        {/* gridlines */}
        <g>{tickVals.map((v,i) => (
          <line key={i} x1={padL} x2={width-padR} y1={yBar(v)} y2={yBar(v)} stroke="#181818"/>
        ))}</g>
        {/* y-left labels */}
        <g style={{font: "10px monospace", fill: "#6B6B6B"}}>
          {tickVals.map((v,i) => (
            <text key={i} x={padL - 8} y={yBar(v) + 3.5} textAnchor="end">
              {yFormatLeft(v)}
            </text>
          ))}
        </g>
        {/* stacked bars */}
        <g>{labels.map((_, i) => {
          let acc = 0;
          return bars.map((b, bi) => {
            const v = b.data[i] || 0;
            const y = yBar(v + acc);
            const h = yBar(acc) - y;
            const x = xCol(i) - barW/2;
            const seg = (
              <rect key={bi} x={x} y={y} width={barW} height={Math.max(0,h)} rx={2}
                fill={b.color}
                style={{opacity: hover != null && hover !== i ? 0.55 : 1, transition: "opacity .15s"}}
              />
            );
            acc += v;
            return seg;
          });
        })}</g>
        {/* line */}
        {line ? (
          <>
            <path d={linePath} fill="none" stroke={line.color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
            {line.data.map((v,i) => (
              <circle key={i} cx={xCol(i)} cy={yLine(v)} r={hover === i ? 5 : 2.5}
                fill={hover === i ? line.color : "#000"} stroke={line.color} strokeWidth="1.5"
                style={{transition: "r .15s"}}
              />
            ))}
            {/* right axis */}
            <g style={{font: "10px monospace", fill: "#6B6B6B"}}>
              {tickVals.map((v,i) => {
                const yv = (lineMax / ticks) * i;
                return <text key={i} x={width - padR + 8} y={yBar(v) + 3.5} textAnchor="start">{yFormatRight(yv)}</text>;
              })}
            </g>
          </>
        ) : null}
        {/* x labels (every Nth) */}
        <g style={{font: "10px monospace", fill: "#6B6B6B"}}>
          {labels.map((l,i) => {
            if (i % Math.ceil(n/7) !== 0 && i !== n-1) return null;
            return <text key={i} x={xCol(i)} y={height - 10} textAnchor="middle">{l}</text>;
          })}
        </g>
        {/* hover guide */}
        {hover != null ? (
          <line x1={xCol(hover)} x2={xCol(hover)} y1={padT} y2={padT + innerH} stroke="#2E2E2E" strokeDasharray="2 3"/>
        ) : null}
      </svg>

      {hover != null ? (
        <div style={{
          position:"absolute",
          left: Math.min(width - 200, Math.max(0, xCol(hover) - 90)),
          top: padT + 6,
          background:"#0a0a0a", border: "1px solid #2E2E2E", borderRadius: 10,
          padding: "10px 12px",
          fontSize: 11, color: "#fff",
          pointerEvents: "none",
          minWidth: 180
        }}>
          <div style={{
            fontFamily:"var(--font-mono)", fontSize: 9.5,
            letterSpacing: ".12em", color: "var(--fg-mute)", textTransform: "uppercase",
            marginBottom: 8
          }}>{labels[hover]}</div>
          {bars.map((b,bi) => (
            <div key={bi} style={{display:"flex", justifyContent:"space-between", gap: 14, padding: "2px 0"}}>
              <span style={{display:"inline-flex", alignItems:"center", gap: 6, color: "var(--fg-2)"}}>
                <span style={{width: 8, height: 8, borderRadius: 2, background: b.color}}/>
                {b.name}
              </span>
              <span style={{fontFamily:"var(--font-display)", fontWeight: 700}}>
                {yFormatLeft(b.data[hover] || 0)}
              </span>
            </div>
          ))}
          {line ? (
            <div style={{display:"flex", justifyContent:"space-between", gap: 14, padding: "6px 0 2px", marginTop: 4, borderTop: "1px solid #1a1a1a"}}>
              <span style={{display:"inline-flex", alignItems:"center", gap: 6, color: "var(--fg-2)"}}>
                <span style={{width: 14, height: 2, background: line.color, borderRadius: 999}}/>
                {line.name}
              </span>
              <span style={{fontFamily:"var(--font-display)", fontWeight: 900}}>
                {yFormatRight(line.data[hover] || 0)}
              </span>
            </div>
          ) : null}
        </div>
      ) : null}
    </div>
  );
}
window.StackedBarLine = StackedBarLine;

/* ===================================================================
   AREA CHART — single series, line + area fill
   =================================================================== */
function AreaChart({ data, labels, color = "#00E7FF", height = 220, yFormat = v => v }) {
  const wrapRef = useRefS(null);
  const [width, setWidth] = useStateS(640);
  const [hover, setHover] = useStateS(null);
  const padL = 44, padR = 16, padT = 16, padB = 28;

  useEffS(() => {
    const m = () => wrapRef.current && setWidth(wrapRef.current.clientWidth);
    m();
    window.addEventListener("resize", m);
    return () => window.removeEventListener("resize", m);
  }, []);

  const innerW = Math.max(120, width - padL - padR);
  const innerH = height - padT - padB;
  const max = Math.max(...data) * 1.1;
  const step = innerW / (data.length - 1);
  const xAt = i => padL + i * step;
  const yAt = v => padT + innerH - (v / max) * innerH;
  const ticks = 4;
  const tickVals = Array.from({length: ticks+1}, (_,i) => (max/ticks)*i);

  const pathD = data.map((v,i) => `${i===0?"M":"L"}${xAt(i)},${yAt(v)}`).join(" ");
  const areaD = pathD + ` L${xAt(data.length-1)},${padT+innerH} L${xAt(0)},${padT+innerH} Z`;
  const gid = "ag-" + Math.random().toString(36).slice(2,9);

  const onMove = (e) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const i = Math.round((x - padL) / step);
    if (i >= 0 && i < data.length) setHover(i); else setHover(null);
  };

  return (
    <div className="chart-wrap" ref={wrapRef} onMouseMove={onMove} onMouseLeave={() => setHover(null)}>
      <svg viewBox={`0 0 ${width} ${height}`} height={height}>
        <defs>
          <linearGradient id={gid} x1="0" x2="0" y1="0" y2="1">
            <stop offset="0%"  stopColor={color} stopOpacity=".35"/>
            <stop offset="100%" stopColor={color} stopOpacity="0"/>
          </linearGradient>
        </defs>
        <g>{tickVals.map((v,i) => <line key={i} x1={padL} x2={width-padR} y1={yAt(v)} y2={yAt(v)} stroke="#181818"/>)}</g>
        <g style={{font:"10px monospace", fill:"#6B6B6B"}}>
          {tickVals.map((v,i) => <text key={i} x={padL-8} y={yAt(v)+3.5} textAnchor="end">{yFormat(v)}</text>)}
        </g>
        <path d={areaD} fill={`url(#${gid})`}/>
        <path d={pathD} fill="none" stroke={color} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round"/>
        {hover != null ? (
          <>
            <line x1={xAt(hover)} x2={xAt(hover)} y1={padT} y2={padT+innerH} stroke="#2E2E2E" strokeDasharray="2 3"/>
            <circle cx={xAt(hover)} cy={yAt(data[hover])} r={9} fill={color} opacity=".18"/>
            <circle cx={xAt(hover)} cy={yAt(data[hover])} r={4} fill={color}/>
          </>
        ) : null}
        <g style={{font:"10px monospace", fill:"#6B6B6B"}}>
          {labels.map((l,i) => {
            if (i % Math.ceil(labels.length/7) !== 0 && i !== labels.length-1) return null;
            return <text key={i} x={xAt(i)} y={height-8} textAnchor="middle">{l}</text>;
          })}
        </g>
      </svg>
      {hover != null ? (
        <div style={{
          position:"absolute",
          left: xAt(hover), top: yAt(data[hover]),
          transform: "translate(-50%, -120%)",
          background:"#0a0a0a", border:"1px solid #2E2E2E", borderRadius: 10,
          padding: "7px 10px",
          fontSize: 11, color: "#fff",
          pointerEvents: "none",
          whiteSpace: "nowrap"
        }}>
          <div style={{fontFamily:"var(--font-display)", fontWeight: 900, fontSize: 13, color: "#fff"}}>{yFormat(data[hover])}</div>
          <div style={{fontFamily:"var(--font-mono)", fontSize: 9.5, color: "var(--fg-2)", letterSpacing: ".08em"}}>{labels[hover]}</div>
        </div>
      ) : null}
    </div>
  );
}
window.AreaChart = AreaChart;

/* ===================================================================
   DONUT — generic
   =================================================================== */
function Donut({ data, totalLabel, centerVal }) {
  const total = data.reduce((s,d) => s + d.value, 0);
  const r = 60, stroke = 16, c = 80;
  const circ = 2 * Math.PI * r;
  let acc = 0;
  return (
    <div className="donut-wrap">
      <svg className="donut-svg" viewBox="0 0 160 160">
        <circle cx={c} cy={c} r={r} fill="none" stroke="#1a1a1a" strokeWidth={stroke}/>
        {data.map((d,i) => {
          const frac = d.value / total;
          const len = frac * circ;
          const seg = (
            <circle key={i}
              cx={c} cy={c} r={r} fill="none"
              stroke={d.color} strokeWidth={stroke}
              strokeDasharray={`${len} ${circ-len}`}
              strokeDashoffset={-acc}
              transform={`rotate(-90 ${c} ${c})`}
            />
          );
          acc += len;
          return seg;
        })}
        <text x={c} y={c-2} textAnchor="middle" fill="#fff"
          style={{fontFamily:"var(--font-display)", fontWeight:900, fontSize:"24px", letterSpacing:"-.02em"}}>
          {centerVal || (total >= 1000 ? `${(total/1000).toFixed(1)}k` : total)}
        </text>
        <text x={c} y={c+16} textAnchor="middle" fill="#A8A8A8"
          style={{fontFamily:"var(--font-display)", fontWeight:700, fontSize:"9.5px", letterSpacing:".18em", textTransform:"uppercase"}}>
          {totalLabel}
        </text>
      </svg>
      <div className="donut-rows">
        {data.map(d => (
          <div className="donut-row" key={d.name}>
            <span className="sw" style={{background: d.color}}/>
            <span className="nm">{d.name}</span>
            <span className="v">{d.valLabel || d.value.toLocaleString()}</span>
            <span className="p">{Math.round((d.value/total)*100)}%</span>
          </div>
        ))}
      </div>
    </div>
  );
}
window.Donut = Donut;

/* ===================================================================
   FUNNEL
   =================================================================== */
function FunnelChart({ stages, lang, valueFormat }) {
  const max = stages[0].value;
  return (
    <div className="funnel">
      {stages.map((s,i) => {
        const pct = (s.value / max) * 100;
        const label = lang === "pt" ? s.stage_pt : s.stage_en;
        const rate = lang === "en" && s.rate_en ? s.rate_en : s.rate;
        const ratGood = i > 0 && parseFloat(rate) >= 30;
        return (
          <div className="funnel-row" key={i}>
            <div className="funnel-bar">
              <div className="funnel-bar__fill" style={{width: `${pct}%`}}>
                {pct > 38 && <span className="funnel-bar__label">{label}</span>}
              </div>
              {pct <= 38 && <span className="funnel-bar__label out">{label}</span>}
            </div>
            <div className="funnel-val">{valueFormat ? valueFormat(s.value) : s.value.toLocaleString()}</div>
            <div className={"funnel-rate" + (i === 0 ? "" : (parseFloat(rate) >= 30 ? " good" : ""))}>{rate}</div>
          </div>
        );
      })}
    </div>
  );
}
window.FunnelChart = FunnelChart;

/* ===================================================================
   TAG PICKER — chip strip + add-tag popover.
   Used for manual campaign + creative concept tagging.

   tags:    string[] — current tag IDs (e.g. ["prospecting","brand"])
   options: [{id, key, color}] — suggested tags (key is an i18n key for display)
   onChange(newTags): called with the new tag IDs
   lang:    "pt" | "en"
   size:    "sm" | "md" (default "md")
   placement: "below" | "above" (where popover opens)
   =================================================================== */
const { useRef: useRefTP, useState: useStateTP, useEffect: useEffTP } = React;

function TagPicker({ tags = [], options = [], onChange, lang = "pt", size = "md", placement = "below" }) {
  const [open, setOpen] = useStateTP(false);
  const [custom, setCustom] = useStateTP("");
  const wrapRef = useRefTP(null);

  useEffTP(() => {
    if (!open) return;
    const onClick = (e) => {
      if (!wrapRef.current?.contains(e.target)) setOpen(false);
    };
    document.addEventListener("mousedown", onClick);
    return () => document.removeEventListener("mousedown", onClick);
  }, [open]);

  // Look up display label + color for an id.
  // - If it's an option, use its color and translated label.
  // - Otherwise treat as a custom tag — show as-is, neutral colour.
  const labelFor = (id) => {
    const o = options.find(o => o.id === id);
    if (o) return t(o.key, lang);
    return id;
  };
  const colorFor = (id) => {
    const o = options.find(o => o.id === id);
    return o ? o.color : "#6B6B6B";
  };

  const addTag = (id) => {
    if (!id || tags.includes(id)) return;
    onChange([...tags, id]);
    setCustom("");
  };
  const removeTag = (id) => onChange(tags.filter(t => t !== id));

  const submitCustom = (e) => {
    e?.preventDefault();
    const trimmed = custom.trim();
    if (!trimmed) return;
    addTag(trimmed);
  };

  const suggested = options.filter(o => !tags.includes(o.id));

  return (
    <div className={"tagpkr " + (size === "sm" ? "tagpkr--sm" : "")} ref={wrapRef}>
      {tags.length === 0 ? (
        <span className="tagpkr__empty">{t("tag.untagged", lang)}</span>
      ) : (
        tags.map(id => (
          <span key={id} className="tagpkr__chip" style={{
            "--tc": colorFor(id),
            background: "rgba(255,255,255,.04)"
          }}>
            <span className="tagpkr__dot"/>
            {labelFor(id)}
            <button className="tagpkr__x" onClick={() => removeTag(id)} aria-label={t("tag.remove", lang)}>
              <Icon name="x" size={9}/>
            </button>
          </span>
        ))
      )}

      <button className="tagpkr__add" onClick={() => setOpen(!open)} aria-label={t("tag.add", lang)}>
        <Icon name="sparkles" size={11}/>
        <span>{t("tag.add", lang)}</span>
      </button>

      {open && (
        <div className={"tagpkr__pop " + (placement === "above" ? "tagpkr__pop--above" : "")}>
          <div className="tagpkr__pop-head">{t("tag.suggested", lang)}</div>
          <div className="tagpkr__pop-grid">
            {suggested.length === 0 ? (
              <span className="tagpkr__pop-empty">{t("all", lang)} ✓</span>
            ) : suggested.map(o => (
              <button key={o.id} className="tagpkr__pop-opt" style={{ "--tc": o.color }}
                onClick={() => addTag(o.id)}>
                <span className="tagpkr__dot"/>
                {t(o.key, lang)}
              </button>
            ))}
          </div>
          <div className="tagpkr__pop-head" style={{marginTop: 10}}>{t("tag.custom", lang)}</div>
          <form className="tagpkr__pop-form" onSubmit={submitCustom}>
            <input
              value={custom}
              onChange={e => setCustom(e.target.value)}
              placeholder={t("tag.placeholder", lang)}
              maxLength={28}
              autoFocus
            />
            <button type="submit" className="tagpkr__pop-create">
              {t("tag.create", lang)}
            </button>
          </form>
        </div>
      )}
    </div>
  );
}
window.TagPicker = TagPicker;

/* Filter chip strip that mirrors a tag set.
   - Shows "Todas" + each unique tag found across items + an "Untagged" filter.
   - Active value: null (all), a tag id, or "__untagged__"
*/
function TagFilterStrip({ value, onChange, options, items, lang, leadingChip }) {
  // Build the set of tags actually used by items + suggested options.
  const usedSet = new Set();
  items.forEach(it => (it.tags || []).forEach(t => usedSet.add(t)));
  options.forEach(o => usedSet.add(o.id));
  const all = Array.from(usedSet);
  // Sort: suggested options first in their declared order, then custom tags alpha.
  const optionOrder = new Map(options.map((o, i) => [o.id, i]));
  all.sort((a, b) => {
    const ai = optionOrder.has(a) ? optionOrder.get(a) : 999;
    const bi = optionOrder.has(b) ? optionOrder.get(b) : 999;
    if (ai !== bi) return ai - bi;
    return a.localeCompare(b);
  });
  const untaggedCount = items.filter(it => !it.tags || it.tags.length === 0).length;

  const labelFor = (id) => {
    const o = options.find(o => o.id === id);
    return o ? t(o.key, lang) : id;
  };

  return (
    <div className="tag-strip">
      <button className={"chip" + (value == null ? " is-on" : "")} onClick={() => onChange(null)}>
        {leadingChip || t("tag.all", lang)}
      </button>
      {all.map(id => (
        <button key={id}
          className={"chip" + (value === id ? " is-on" : "")}
          onClick={() => onChange(id)}>
          {labelFor(id)}
        </button>
      ))}
      {untaggedCount > 0 && (
        <button
          className={"chip chip--untagged" + (value === "__untagged__" ? " is-on" : "")}
          onClick={() => onChange("__untagged__")}>
          <Icon name="alert" size={10}/>
          {t("tag.untagged", lang)} <span className="chip__count">{untaggedCount}</span>
        </button>
      )}
    </div>
  );
}
window.TagFilterStrip = TagFilterStrip;

function applyTagFilter(items, value) {
  if (value == null) return items;
  if (value === "__untagged__") return items.filter(it => !it.tags || it.tags.length === 0);
  return items.filter(it => (it.tags || []).includes(value));
}
window.applyTagFilter = applyTagFilter;

/* ===================================================================
   LOADING / ERROR / EMPTY STATES
   Branded placeholders rendered when a page is fetching Supabase data,
   the fetch failed, or the table returned zero rows.
   =================================================================== */
function LoadingState({ lang, label }) {
  return (
    <div className="state state--loading" role="status" aria-live="polite">
      <span className="state__spinner" aria-hidden="true"/>
      <div className="state__msg">
        {label || (lang === "pt" ? "Carregando dados…" : "Loading data…")}
      </div>
    </div>
  );
}
window.LoadingState = LoadingState;

function EmptyState({ lang, label, sublabel }) {
  return (
    <div className="state state--empty">
      <div className="state__mark"><Icon name="sparkles" size={26}/></div>
      <div className="state__title">
        {label || (lang === "pt" ? "Sem dados ainda" : "No data yet")}
      </div>
      <div className="state__msg">
        {sublabel || (lang === "pt"
          ? "Conecte sua conta e execute a primeira sincronização."
          : "Connect your account and run the first sync.")}
      </div>
    </div>
  );
}
window.EmptyState = EmptyState;

function ErrorState({ lang, error, onRetry }) {
  const msg = error && error.message ? error.message : (typeof error === "string" ? error : "");
  return (
    <div className="state state--error">
      <div className="state__mark"><Icon name="alert" size={24}/></div>
      <div className="state__title">
        {lang === "pt" ? "Erro ao carregar dados" : "Couldn't load data"}
      </div>
      {msg && <div className="state__msg state__msg--mono">{msg}</div>}
      {onRetry && (
        <button className="btn" onClick={onRetry} style={{marginTop: 6}}>
          <Icon name="sync" size={12}/>
          {lang === "pt" ? "Tentar novamente" : "Try again"}
        </button>
      )}
    </div>
  );
}
window.ErrorState = ErrorState;
