// ============================================================
// Pages - Logs Serveur & Logs Boutique (Connectés API Backend RBAC)
// ============================================================

const { useState, useMemo, useEffect, useRef, useCallback } = React;

// ----- helpers -----
function useDebouncedFilters(values, delay = 280) {
  const [applied, setApplied] = useState(values);
  const [loading, setLoading] = useState(false);
  const firstRef = useRef(true);
  useEffect(() => {
    if (firstRef.current) { firstRef.current = false; setApplied(values); return; }
    setLoading(true);
    const t = setTimeout(() => {
      setApplied(values);
      setLoading(false);
    }, delay);
    return () => clearTimeout(t);
  }, [JSON.stringify(values)]); // eslint-disable-line
  return [applied, loading];
}

const SEARCH_SCOPE_OPTIONS = [
  { key: "all", label: "Partout", icon: "globe", title: "Recherche globale sur tous les champs (de base)" },
  { key: "unique_id", label: "ID Unique", icon: "terminal", title: "ID unique du joueur en jeu (SCM ID, ID #...)" },
  { key: "ids", label: "Identifiants", icon: "link", title: "Licences Rockstar, Discord, CitizenID, Steam, FiveM, IP" },
  { key: "message", label: "Messages", icon: "logs", title: "Contenu du message, notes ou objets" },
  { key: "player", label: "Pseudos", icon: "user", title: "Pseudos RP et FiveM" },
  { key: "date", label: "Date & Heure", icon: "calendar", title: "Date et horodatage (jour, mois, heure...)" },
];

function SearchField({
  value,
  onChange,
  applied,
  onSubmit,
  placeholder,
  loading,
  scopes = ["all"],
  onScopesChange,
  appliedScopes = ["all"],
}) {
  const dirty = value !== applied || JSON.stringify(scopes) !== JSON.stringify(appliedScopes);

  const handleToggleScope = (scopeKey) => {
    if (!onScopesChange) return;

    if (scopeKey === "all") {
      onScopesChange(["all"]);
      return;
    }

    // Si "all" était actif, basculer vers le scope cliqué uniquement
    if (scopes.includes("all")) {
      onScopesChange([scopeKey]);
      return;
    }

    let next;
    if (scopes.includes(scopeKey)) {
      next = scopes.filter((k) => k !== scopeKey);
    } else {
      next = [...scopes, scopeKey];
    }

    const specificKeys = SEARCH_SCOPE_OPTIONS.filter((s) => s.key !== "all").map((s) => s.key);
    const hasAllSpecific = specificKeys.every((k) => next.includes(k));

    if (next.length === 0 || hasAllSpecific) {
      onScopesChange(["all"]);
    } else {
      onScopesChange(next);
    }
  };

  const isScopeSelected = (key) => {
    if (key === "all") {
      return scopes.length === 0 || scopes.includes("all");
    }
    return scopes.includes(key) && !scopes.includes("all");
  };

  return (
    <div className="field search-field">
      <label>Recherche</label>
      <div className="search-row">
        <div className="search-input-wrap">
          <input
            className="input search"
            placeholder={placeholder}
            value={value}
            onChange={(e) => onChange(e.target.value)}
            onKeyDown={(e) => { if (e.key === "Enter") onSubmit(); }}
          />
          {loading && <div className="search-spin"></div>}
          {dirty && !loading && <span className="search-dirty" title="Appuyez sur Entrée ou cliquez sur Rechercher">•</span>}
        </div>
        <button className={`btn ${dirty ? "btn-primary" : ""} search-btn`} onClick={onSubmit} disabled={!dirty && !loading}>
          Rechercher
        </button>
      </div>

      {onScopesChange && (
        <div className="search-scopes-row">
          <span className="search-scopes-label">Cible :</span>
          <div className="search-scopes-list">
            {SEARCH_SCOPE_OPTIONS.map((s) => {
              const active = isScopeSelected(s.key);
              return (
                <button
                  key={s.key}
                  type="button"
                  className={`search-scope-pill ${active ? "active" : ""}`}
                  onClick={() => handleToggleScope(s.key)}
                  title={s.title}
                >
                  <Icon name={s.icon} size={11} />
                  <span>{s.label}</span>
                </button>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
}

function LoadingOverlay({ label = "Chargement en cours…" }) {
  return (
    <div className="loading-overlay">
      <div className="spinner"></div>
      <div className="lbl">{label}</div>
    </div>
  );
}

function PerPageSelect({ value, onChange }) {
  return (
    <div className="pp-wrap">
      <span style={{ fontSize: 12 }}>Lignes</span>
      <select className="select" value={value} onChange={(e) => onChange(parseInt(e.target.value, 10))}>
        <option value={10}>10</option>
        <option value={25}>25</option>
        <option value={50}>50</option>
        <option value={100}>100</option>
      </select>
    </div>
  );
}

function fmtDate(ts) {
  if (!ts) return "-";
  try {
    if (ts instanceof Date) {
      const day = String(ts.getDate()).padStart(2, "0");
      const month = String(ts.getMonth() + 1).padStart(2, "0");
      const year = ts.getFullYear();
      const hours = String(ts.getHours()).padStart(2, "0");
      const minutes = String(ts.getMinutes()).padStart(2, "0");
      return `${day}/${month}/${year} à ${hours}:${minutes}`;
    }

    const s = String(ts).trim();

    // Si format ISO ou MySQL YYYY-MM-DDTHH:mm ou YYYY-MM-DD HH:mm ou nom de fichier 1_YYYY-MM-DDTHH-mm-ss
    const matchIso = s.match(/(?:^|_)(\d{4})-(\d{2})-(\d{2})[T\s](\d{2})[:-](\d{2})/);
    if (matchIso) {
      // Si la date se termine par Z ou fuseau horaire, on la convertit via new Date pour le fuseau local
      if (s.endsWith("Z") || s.includes("+") || s.includes("T")) {
        const d = new Date(s.replace(/T(\d{2})-(\d{2})-(\d{2})/, "T$1:$2:$3"));
        if (!isNaN(d.getTime())) {
          const day = String(d.getDate()).padStart(2, "0");
          const month = String(d.getMonth() + 1).padStart(2, "0");
          const year = d.getFullYear();
          const hours = String(d.getHours()).padStart(2, "0");
          const minutes = String(d.getMinutes()).padStart(2, "0");
          return `${day}/${month}/${year} à ${hours}:${minutes}`;
        }
      }
      const [, year, month, day, hour, min] = matchIso;
      return `${day}/${month}/${year} à ${hour}:${min}`;
    }

    const d = new Date(s);
    if (!isNaN(d.getTime())) {
      const day = String(d.getDate()).padStart(2, "0");
      const month = String(d.getMonth() + 1).padStart(2, "0");
      const year = d.getFullYear();
      const hours = String(d.getHours()).padStart(2, "0");
      const minutes = String(d.getMinutes()).padStart(2, "0");
      return `${day}/${month}/${year} à ${hours}:${minutes}`;
    }

    return s;
  } catch (e) {
    return String(ts);
  }
}

function MaskedValue({ visible, value, mask = "**********" }) {
  if (visible) return <span>{value ?? "-"}</span>;
  return <span className="masked" title="Grade insuffisant pour afficher cette information">{mask}</span>;
}

function CopyableValue({ value, copyKey, copiedKey, onCopy, title = "Copier", hideValue = false }) {
  if (!value || value === "-") return <span>-</span>;
  const isCopied = copiedKey === copyKey;
  return (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 7, maxWidth: "100%" }}>
      {!hideValue && <span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>{value}</span>}
      <button
        className="icon-btn"
        style={{
          display: "inline-flex",
          alignItems: "center",
          justifyContent: "center",
          gap: 4,
          flexShrink: 0,
          width: isCopied ? "auto" : 24,
          height: 22,
          padding: isCopied ? "2px 7px" : 0,
          color: isCopied ? "var(--ok)" : "var(--text-dim)",
          borderColor: isCopied ? "var(--ok)" : "var(--border)",
          background: isCopied ? "oklch(0.35 0.14 155 / 0.15)" : "transparent",
          transition: "all 0.15s ease",
        }}
        title={isCopied ? "Copié dans le presse-papier !" : title}
        onClick={(e) => {
          e.stopPropagation();
          onCopy(value, copyKey);
        }}
      >
        <Icon name={isCopied ? "check" : "copy"} size={11} />
        {isCopied && <span style={{ fontSize: 10, fontWeight: 600 }}>Copié !</span>}
      </button>
    </span>
  );
}

function InferredValue({ inferred, copyKey, copiedKey, onCopy, title, icon }) {
  if (!inferred || !inferred.value) return <span>-</span>;
  const isCopied = copiedKey === copyKey;
  return (
    <span
      style={{
        display: "inline-flex",
        alignItems: "center",
        gap: 6,
        color: "var(--accent)",
        fontSize: "inherit",
        flexWrap: "nowrap",
      }}
      title={`Déduit par recoupement (${inferred.confidence || "probable"}) via ${inferred.source || "autre log"}`}
    >
      {icon && <Icon name={icon} size={12} style={{ color: "var(--accent)", flexShrink: 0, opacity: 0.9 }} />}
      <span style={{ fontStyle: "italic", whiteSpace: "nowrap" }}>
        - (Possiblement : <span style={{ fontWeight: 600, fontStyle: "normal" }}>{inferred.value}</span>)
      </span>
      <button
        className="icon-btn"
        style={{
          display: "inline-flex",
          alignItems: "center",
          justifyContent: "center",
          gap: 4,
          flexShrink: 0,
          width: isCopied ? "auto" : 24,
          height: 22,
          padding: isCopied ? "2px 7px" : 0,
          color: isCopied ? "var(--ok)" : "var(--accent)",
          borderColor: isCopied ? "var(--ok)" : "oklch(0.5 0.15 275 / 0.4)",
          background: isCopied ? "oklch(0.35 0.14 155 / 0.15)" : "transparent",
          transition: "all 0.15s ease",
        }}
        title={isCopied ? "Copié dans le presse-papier !" : (title || "Copier la valeur déduite")}
        onClick={(e) => {
          e.stopPropagation();
          onCopy(inferred.value, copyKey);
        }}
      >
        <Icon name={isCopied ? "check" : "copy"} size={11} />
        {isCopied && <span style={{ fontSize: 10, fontWeight: 600 }}>Copié !</span>}
      </button>
    </span>
  );
}

function PlayerIdentityModal({ targetLog, onClose, onFilterPlayer, canSeeSensitive, canSeeShop, canSeeLogs }) {
  const [loading, setLoading] = useState(true);
  const [profile, setProfile] = useState(null);
  const [error, setError] = useState(null);
  const [copiedKey, setCopiedKey] = useState(null);

  const copyText = (text, key) => {
    if (!text) return;
    const cleanText = String(text).replace(/[\u2014\u2013]/g, "-");
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(cleanText);
    } else {
      const el = document.createElement("textarea");
      el.value = cleanText;
      document.body.appendChild(el);
      el.select();
      document.execCommand("copy");
      document.body.removeChild(el);
    }
    setCopiedKey(key);
    setTimeout(() => setCopiedKey((curr) => (curr === key ? null : curr)), 1800);
  };

  useEffect(() => {
    if (!targetLog) return;
    let isMounted = true;
    setLoading(true);
    setError(null);

    const cleanLic = (targetLog.license || targetLog.license2 || targetLog.inferred?.license?.value || "").replace(/^license:/, "").trim();
    const scmId = targetLog.scm_id || targetLog.inferred?.scm_id?.value || "";
    const citizenid = targetLog.citizenid || targetLog.inferred?.citizenid?.value || "";
    const discord = targetLog.discord || targetLog.inferred?.discord?.value || "";

    const params = new URLSearchParams();
    if (scmId) params.append("scm_id", scmId);
    if (cleanLic) params.append("license", cleanLic);
    if (citizenid) params.append("citizenid", citizenid);
    if (discord) params.append("discord", discord);

    fetch(`/api/player/identity?${params.toString()}`)
      .then((res) => {
        if (!res.ok) throw new Error("Impossible de récupérer la fiche croisée de ce joueur.");
        return res.json();
      })
      .then((data) => {
        if (isMounted) {
          setProfile(data.profile);
          setLoading(false);
        }
      })
      .catch((err) => {
        if (isMounted) {
          setError(err.message);
          setLoading(false);
        }
      });

    return () => { isMounted = false; };
  }, [targetLog]);

  if (!targetLog) return null;

  const primaryName = profile?.primary?.pseudo_rp || profile?.primary?.pseudo_fivem || targetLog.pseudo_rp || targetLog.pseudo_fivem || "Joueur FiveM";
  const scmId = profile?.primary?.scm_id || targetLog?.scm_id || (profile?.identifiers?.scm_ids && profile.identifiers.scm_ids[0]) || null;
  const filterTargetId = scmId || profile?.primary?.citizenid || profile?.primary?.license || profile?.primary?.discord;
  const isScmId = scmId !== null && scmId !== undefined && scmId !== "";

  return (
    <div className="modal-bg" onClick={onClose}>
      <div className="modal identity-modal" onClick={(e) => e.stopPropagation()}>
        {/* Header */}
        <div className="identity-modal-header">
          <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
            <div
              style={{
                width: 42,
                height: 42,
                borderRadius: "50%",
                background: "oklch(0.35 0.14 275 / 0.25)",
                border: "1px solid var(--accent)",
                display: "grid",
                placeItems: "center",
                color: "var(--accent)",
                fontWeight: 700,
                fontSize: 16,
              }}
            >
              {profile?.primary?.discord ? (
                <Icon name="discord" size={20} style={{ color: "#5865F2" }} />
              ) : (
                <Icon name="user" size={18} />
              )}
            </div>
            <div>
              <div style={{ fontSize: 16, fontWeight: 700, color: "var(--text)", display: "flex", alignItems: "center", gap: 8 }}>
                <span>{primaryName}</span>
                {profile?.primary?.scm_id && (
                  <span className="tag tag-muted" style={{ fontSize: 11 }}>SCM #{profile.primary.scm_id}</span>
                )}
              </div>
              <div style={{ fontSize: 11, color: "var(--muted)" }}>
                Fiche d'identité consolidée - Découverte croisée multi-tables (Serveur & Boutique)
              </div>
            </div>
          </div>

          <button className="icon-btn" onClick={onClose} title="Fermer la fiche">
            <Icon name="x" size={16} />
          </button>
        </div>

        {/* Body */}
        <div className="identity-modal-body">
          {loading && (
            <div style={{ padding: "40px 20px", textAlign: "center" }}>
              <div className="spinner" style={{ margin: "0 auto 12px" }}></div>
              <div style={{ color: "var(--text-dim)", fontSize: 13 }}>Recoupement en cours entre logs serveur et boutique...</div>
            </div>
          )}

          {error && !loading && (
            <div style={{ padding: "16px 20px", background: "oklch(0.35 0.15 25 / 0.15)", border: "1px solid var(--danger)", borderRadius: 8, color: "var(--danger)", fontSize: 13 }}>
              {error}
            </div>
          )}

          {profile && !loading && (
            <>
              {/* Bannière de corrélation */}
              <div className="identity-banner">
                <Icon name="check" size={14} />
                <span>
                  <strong>Identité reliée avec succès :</strong>{" "}
                  {profile.stats.totalServerLogs !== null
                    ? `${profile.stats.totalServerLogs} logs serveur`
                    : "logs serveur masqués"}{" "}
                  et{" "}
                  {profile.stats.totalBoutiqueTransactions !== null
                    ? `${profile.stats.totalBoutiqueTransactions} transactions boutique`
                    : "données boutique masquées"}{" "}
                  associés à ce profil.
                </span>
              </div>

              {/* Cartes de statistiques */}
              <div className="identity-stats-grid">
                <div className="stat" style={{ padding: "10px 12px" }}>
                  <div className="l">Logs Serveur</div>
                  {profile.stats.totalServerLogs !== null ? (
                    <div className="v" style={{ fontSize: 18, color: "var(--text)" }}>{profile.stats.totalServerLogs.toLocaleString("fr-FR")}</div>
                  ) : (
                    <div className="v" style={{ fontSize: 13, color: "var(--muted)", display: "flex", alignItems: "center", gap: 5 }}>
                      <Icon name="lock" size={13} />
                      <span>Masqué</span>
                    </div>
                  )}
                  <div className="d">{profile.stats.totalServerLogs !== null ? "Événements en jeu" : "Permission logs requise"}</div>
                </div>
                <div className="stat" style={{ padding: "10px 12px" }}>
                  <div className="l">Boutique</div>
                  {profile.stats.totalBoutiquePurchases !== null ? (
                    <div className="v" style={{ fontSize: 18, color: "var(--accent)" }}>{profile.stats.totalBoutiquePurchases.toLocaleString("fr-FR")}</div>
                  ) : (
                    <div className="v" style={{ fontSize: 13, color: "var(--muted)", display: "flex", alignItems: "center", gap: 5 }}>
                      <Icon name="lock" size={13} />
                      <span>Masqué</span>
                    </div>
                  )}
                  <div className="d">{profile.stats.totalBoutiquePurchases !== null ? "Achats réalisés" : "Permission boutique requise"}</div>
                </div>
                <div className="stat" style={{ padding: "10px 12px" }}>
                  <div className="l">Jetons Dépensés</div>
                  {profile.stats.totalTokensSpent !== null ? (
                    <div className="v" style={{ fontSize: 18, color: "oklch(0.84 0.18 335)" }}>{profile.stats.totalTokensSpent.toLocaleString("fr-FR")}</div>
                  ) : (
                    <div className="v" style={{ fontSize: 13, color: "var(--muted)", display: "flex", alignItems: "center", gap: 5 }}>
                      <Icon name="lock" size={13} />
                      <span>Masqué</span>
                    </div>
                  )}
                  <div className="d">{profile.stats.totalTokensSpent !== null ? "Total débité" : "Permission boutique requise"}</div>
                </div>
                <div className="stat" style={{ padding: "10px 12px" }}>
                  <div className="l">Dernière activité</div>
                  <div style={{ fontSize: 11, fontWeight: 600, marginTop: 4, color: "var(--text)" }}>
                    {fmtDate(profile.stats.lastSeen)}
                  </div>
                  <div className="d" style={{ fontSize: 10 }}>1ère : {fmtDate(profile.stats.firstSeen)}</div>
                </div>
              </div>

              {/* Grille d'identifiants */}
              <div className="identity-grid">
                {/* Discords */}
                <div className="identity-card">
                  <div className="lbl">
                    <Icon name="discord" size={13} style={{ color: "#5865F2" }} />
                    <span>Identifiant(s) Discord</span>
                  </div>
                  <div className="vals">
                    {profile.identifiers.discords.length > 0 ? (
                      profile.identifiers.discords.map((disc, idx) => (
                        <div key={idx} style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                          <span className="mono" style={{ fontSize: 12, color: "var(--text)" }}>{disc}</span>
                          <CopyableValue value={disc} copyKey={`modal-disc-${idx}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Discord ID" hideValue={true} />
                        </div>
                      ))
                    ) : (
                      <span style={{ color: "var(--muted)", fontSize: 12 }}>Aucun Discord enregistré</span>
                    )}
                  </div>
                </div>

                {/* CitizenIDs */}
                <div className="identity-card">
                  <div className="lbl">
                    <Icon name="user" size={13} style={{ color: "var(--accent)" }} />
                    <span>CitizenID</span>
                  </div>
                  <div className="vals">
                    {profile.identifiers.citizenids.length > 0 ? (
                      profile.identifiers.citizenids.map((cid, idx) => (
                        <div key={idx} style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                          <span className="mono" style={{ fontSize: 12, fontWeight: 600, color: "var(--text)" }}>{cid}</span>
                          <CopyableValue value={cid} copyKey={`modal-cid-${idx}`} copiedKey={copiedKey} onCopy={copyText} title="Copier CitizenID" hideValue={true} />
                        </div>
                      ))
                    ) : (
                      <span style={{ color: "var(--muted)", fontSize: 12 }}>Aucun CitizenID</span>
                    )}
                  </div>
                </div>

                {/* Licences Rockstar */}
                <div className="identity-card" style={{ gridColumn: "1 / -1" }}>
                  <div className="lbl">
                    <Icon name="lock" size={13} style={{ color: "var(--info)" }} />
                    <span>Licence(s) Rockstar</span>
                  </div>
                  <div className="vals">
                    {profile.identifiers.licenses.length > 0 ? (
                      profile.identifiers.licenses.map((lic, idx) => (
                        <div key={idx} style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                          <span className="mono" style={{ fontSize: 12, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis" }}>{lic}</span>
                          <CopyableValue value={lic} copyKey={`modal-lic-${idx}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Licence" hideValue={true} />
                        </div>
                      ))
                    ) : (
                      <span style={{ color: "var(--muted)", fontSize: 12 }}>Aucune licence enregistrée</span>
                    )}
                  </div>
                </div>

                {/* Steam & FiveM */}
                <div className="identity-card">
                  <div className="lbl">
                    <Icon name="terminal" size={13} style={{ color: "var(--warn)" }} />
                    <span>Identifiant Steam</span>
                  </div>
                  <div className="vals">
                    {profile.identifiers.steams.length > 0 ? (
                      profile.identifiers.steams.map((stm, idx) => (
                        <div key={idx} style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                          <span className="mono" style={{ fontSize: 12, color: "var(--text)" }}>{stm}</span>
                          <CopyableValue value={stm} copyKey={`modal-stm-${idx}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Steam ID" hideValue={true} />
                        </div>
                      ))
                    ) : (
                      <span style={{ color: "var(--muted)", fontSize: 12 }}>-</span>
                    )}
                  </div>
                </div>

                <div className="identity-card">
                  <div className="lbl">
                    <Icon name="terminal" size={13} style={{ color: "var(--ok)" }} />
                    <span>Identifiant FiveM</span>
                  </div>
                  <div className="vals">
                    {profile.identifiers.fivems.length > 0 ? (
                      profile.identifiers.fivems.map((fiv, idx) => (
                        <div key={idx} style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                          <span className="mono" style={{ fontSize: 12, color: "var(--text)" }}>{fiv}</span>
                          <CopyableValue value={fiv} copyKey={`modal-fiv-${idx}`} copiedKey={copiedKey} onCopy={copyText} title="Copier FiveM ID" hideValue={true} />
                        </div>
                      ))
                    ) : (
                      <span style={{ color: "var(--muted)", fontSize: 12 }}>-</span>
                    )}
                  </div>
                </div>

                {/* Adresses IP */}
                <div className="identity-card">
                  <div className="lbl">
                    <Icon name="lock" size={13} />
                    <span>Adresse(s) IP</span>
                  </div>
                  <div className="vals">
                    {canSeeSensitive ? (
                      profile.identifiers.ips.length > 0 ? (
                        profile.identifiers.ips.map((ip, idx) => (
                          <div key={idx} style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                            <span className="mono" style={{ fontSize: 12, color: "var(--text)" }}>{ip}</span>
                            <CopyableValue value={ip} copyKey={`modal-ip-${idx}`} copiedKey={copiedKey} onCopy={copyText} title="Copier IP" hideValue={true} />
                          </div>
                        ))
                      ) : (
                        <span style={{ color: "var(--muted)", fontSize: 12 }}>Aucune IP</span>
                      )
                    ) : (
                      <span className="masked" style={{ fontSize: 11 }} title="Grade Discord requis pour voir les IP">
                        [Masqué - Rôle requis]
                      </span>
                    )}
                  </div>
                </div>

                {/* Pseudos RP et FiveM */}
                <div className="identity-card">
                  <div className="lbl">
                    <Icon name="user" size={13} />
                    <span>Pseudos connus</span>
                  </div>
                  <div className="vals" style={{ fontSize: 12 }}>
                    <div>
                      <strong style={{ color: "var(--text)" }}>RP :</strong>{" "}
                      {profile.identifiers.pseudos_rp.length > 0 ? profile.identifiers.pseudos_rp.join(", ") : "-"}
                    </div>
                    {profile.identifiers.pseudos_fivem.length > 0 && (
                      <div style={{ color: "var(--muted)", marginTop: 2 }}>
                        FiveM : {profile.identifiers.pseudos_fivem.join(", ")}
                      </div>
                    )}
                  </div>
                </div>
              </div>
            </>
          )}
        </div>

        {/* Footer */}
        <div className="identity-modal-footer">
          <div>
            {filterTargetId && onFilterPlayer && (
              <button
                className="btn btn-primary btn-sm"
                style={{ display: "inline-flex", alignItems: "center", gap: 7 }}
                onClick={() => {
                  onFilterPlayer(String(filterTargetId), isScmId ? "unique_id" : "ids");
                  onClose();
                }}
              >
                <Icon name="search" size={13} />
                <span>
                  {isScmId
                    ? `Voir tous les logs de ce joueur (ID #${filterTargetId})`
                    : `Voir tous les logs de ce joueur (${filterTargetId})`}
                </span>
              </button>
            )}
          </div>
          <button className="btn btn-sm" onClick={onClose}>
            Fermer
          </button>
        </div>
      </div>
    </div>
  );
}

// =============================================================================
// SNAPVAULT - Preuves d'écran & Métadonnées Joueur
// =============================================================================

function isEvidenceCandidate(l) {
  if (!l) return false;
  const t = String(l.type || "").toLowerCase().trim();
  const m = String(l.message || "");
  const mLower = m.toLowerCase();

  // 1. Exclure formellement TOUS les débannissements (Unban) : aucun screenshot n'est pris au déban
  if (t === "unban" || mLower.includes("unban") || mLower.includes("déban") || mLower.includes("deban")) {
    return false;
  }

  // 2. Exclure les logs anticheat de routine (tokenizer, whitelist, heartbeat)
  if (mLower.includes("tokenizer") || mLower.includes("temp_whitelist") || mLower.includes("heartbeat")) {
    return false;
  }

  // 3. Captures d'écran staff explicites
  if (t === "screenshot" || mLower.includes("screenshot") || mLower.includes("admin/screenshots") || m.includes("Capture d'écran de")) {
    return true;
  }

  // 4. Bans Anticheat explicites (avec bannissement effectif)
  if (m.includes("[BAN ANTICHEAT #") || m.includes("BAN PERMANENT") || m.includes("SnapVault")) {
    return true;
  }

  // 5. Ban standard avec numéro de ban (#ID)
  if ((t === "ban" || /\bban\s*#\d+/i.test(m)) && !mLower.includes("unban")) {
    return true;
  }

  return false;
}

function SnapVaultLightbox({ image, title, onClose }) {
  if (!image) return null;

  return (
    <div className="snapvault-lightbox-backdrop" onClick={onClose}>
      <div className="snapvault-lightbox-modal" onClick={(e) => e.stopPropagation()}>
        <div className="snapvault-lightbox-header">
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <Icon name="camera" size={16} style={{ color: "var(--cyan, #06b6d4)" }} />
            <span style={{ fontWeight: 600, fontSize: 13, color: "var(--text)" }}>
              {title || "Capture d'écran SnapVault"}
            </span>
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <a
              href={image}
              target="_blank"
              rel="noopener noreferrer"
              className="btn btn-sm"
              style={{ display: "inline-flex", alignItems: "center", gap: 6 }}
            >
              <Icon name="download" size={12} />
              <span>Ouvrir dans un onglet</span>
            </a>
            <button className="btn btn-sm" onClick={onClose} title="Fermer la vue plein écran">
              <Icon name="x" size={14} />
            </button>
          </div>
        </div>
        <div className="snapvault-lightbox-body">
          <img src={image} alt={title || "Capture SnapVault"} className="snapvault-lightbox-img" />
        </div>
      </div>
    </div>
  );
}

function SnapVaultEvidencePanel({ evidence, loading, onZoomImage, copiedKey, onCopy }) {
  const [jsonExpanded, setJsonExpanded] = useState(false);

  if (loading) {
    return (
      <div className="snapvault-card snapvault-loading">
        <div className="spinner" style={{ width: 18, height: 18, borderWidth: 2 }} />
        <span>Récupération de la capture et des données en direct depuis SnapVault…</span>
      </div>
    );
  }

  if (evidence && !evidence.hasEvidence) {
    return (
      <div
        className="snapvault-card"
        style={{
          padding: "12px 16px",
          display: "flex",
          alignItems: "center",
          gap: 10,
          color: "var(--text-dim)",
          fontSize: 12,
          background: "var(--surface)",
          border: "1px dashed var(--border)",
          borderRadius: "var(--r)",
          margin: "10px 0 4px",
        }}
      >
        <Icon name="camera" size={16} style={{ color: "var(--muted)" }} />
        <span>
          Aucune capture d'écran SnapVault enregistrée pour cet événement (joueur hors ligne ou sanction sans transmission d'image).
        </span>
      </div>
    );
  }

  if (!evidence || !evidence.hasEvidence) {
    return null;
  }

  const { imageUrl, meta, rawMeta, evidenceType, category, name, hasImage, hasMeta } = evidence;
  const isBan = evidenceType === "ban";
  const badgeTitle = isBan ? "Preuve Bannissement" : "Capture Staff";
  const badgeColor = isBan ? "tag-err" : "tag-cyan";

  const fmtCurrency = (n) => {
    if (n === null || n === undefined || isNaN(n)) return "-";
    return Number(n).toLocaleString("fr-FR") + " $";
  };

  const copyJsonKey = `snapvault-json-${name}`;

  return (
    <div className="snapvault-card">
      <div className="snapvault-card-header">
        <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
          <div className="snapvault-icon-badge">
            <Icon name="camera" size={15} />
          </div>
          <span style={{ fontWeight: 600, fontSize: 13, color: "var(--text)" }}>
            Preuve SnapVault &amp; Données Joueur
          </span>
          <span className={`tag ${badgeColor}`} style={{ fontSize: 10, padding: "2px 7px" }}>
            {badgeTitle}
          </span>
          <span className="mono" style={{ fontSize: 10, color: "var(--muted)" }}>
            ({category}/{name})
          </span>
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
          {hasImage && imageUrl && (
            <button
              className="btn btn-sm"
              onClick={() => onZoomImage(imageUrl, `${badgeTitle} (${name})`)}
              title="Agrandir la capture en haute définition"
              style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: 11 }}
            >
              <Icon name="zoom" size={12} />
              <span>Agrandir</span>
            </button>
          )}
          {rawMeta && (
            <button
              className="btn btn-sm"
              onClick={() => onCopy(JSON.stringify(rawMeta, null, 2), copyJsonKey)}
              title="Copier les métadonnées JSON complètes"
              style={{
                display: "inline-flex",
                alignItems: "center",
                gap: 6,
                fontSize: 11,
                color: copiedKey === copyJsonKey ? "var(--ok)" : undefined,
                borderColor: copiedKey === copyJsonKey ? "var(--ok)" : undefined,
              }}
            >
              <Icon name={copiedKey === copyJsonKey ? "check" : "copy"} size={12} />
              <span>{copiedKey === copyJsonKey ? "JSON copié !" : "Copier JSON"}</span>
            </button>
          )}
        </div>
      </div>

      <div className="snapvault-card-body">
        {/* Colonne gauche : Capture d'écran */}
        {hasImage && imageUrl ? (
          <div className="snapvault-img-col">
            <div
              className="snapvault-img-frame"
              onClick={() => onZoomImage(imageUrl, `${badgeTitle} (${name})`)}
              title="Cliquer pour afficher la capture en plein écran"
            >
              <img
                src={imageUrl}
                alt={`Capture ${name}`}
                className="snapvault-img"
                loading="lazy"
              />
              <div className="snapvault-img-overlay">
                <Icon name="zoom" size={16} />
                <span>Plein écran</span>
              </div>
            </div>
            <div className="snapvault-img-caption">
              <span>{name}.jpg</span>
              <span className="mono" style={{ color: "var(--muted)" }}>SnapVault</span>
            </div>
          </div>
        ) : (
          <div className="snapvault-no-image">
            <Icon name="image" size={24} style={{ color: "var(--muted)" }} />
            <span>Aucune capture d'écran disponible</span>
          </div>
        )}

        {/* Colonne droite : Métadonnées du joueur formatées (HUD) */}
        {hasMeta && meta ? (
          <div className="snapvault-hud-col">
            {/* Barre de survie (Santé, Armure, Faim, Soif, Stress) */}
            <div className="snapvault-hud-section">
              <div className="snapvault-section-title">
                <Icon name="heart" size={12} style={{ color: "var(--danger)" }} />
                <span>État &amp; Constantes Vitales</span>
                <span
                  className="tag"
                  style={{
                    marginLeft: "auto",
                    fontSize: 10,
                    background: meta.statusColor ? `${meta.statusColor}22` : undefined,
                    color: meta.statusColor || "var(--text)",
                    borderColor: meta.statusColor || "var(--border)",
                  }}
                >
                  {meta.statusLabel || "Vivant"}
                </span>
              </div>

              <div className="snapvault-gauges-grid">
                {/* Santé (HP) */}
                <div className="snapvault-gauge-card">
                  <div className="snapvault-gauge-label">
                    <span>Vie (Santé)</span>
                    <strong className="mono">{meta.health !== undefined && meta.health !== null ? `${meta.health} HP` : "-"}</strong>
                  </div>
                  <div className="snapvault-progress-bg">
                    <div
                      className="snapvault-progress-fill snapvault-health"
                      style={{ width: `${Math.max(0, Math.min(100, ((meta.health || 0) / 200) * 100))}%` }}
                    />
                  </div>
                </div>

                {/* Armure */}
                <div className="snapvault-gauge-card">
                  <div className="snapvault-gauge-label">
                    <span>Armure</span>
                    <strong className="mono">{meta.armor !== null ? `${meta.armor} %` : "-"}</strong>
                  </div>
                  <div className="snapvault-progress-bg">
                    <div
                      className="snapvault-progress-fill snapvault-armor"
                      style={{ width: `${Math.max(0, Math.min(100, meta.armor || 0))}%` }}
                    />
                  </div>
                </div>

                {/* Faim */}
                <div className="snapvault-gauge-card">
                  <div className="snapvault-gauge-label">
                    <span>Faim</span>
                    <strong className="mono">{meta.hunger !== null ? `${meta.hunger} %` : "-"}</strong>
                  </div>
                  <div className="snapvault-progress-bg">
                    <div
                      className="snapvault-progress-fill snapvault-hunger"
                      style={{ width: `${Math.max(0, Math.min(100, meta.hunger || 0))}%` }}
                    />
                  </div>
                </div>

                {/* Soif */}
                <div className="snapvault-gauge-card">
                  <div className="snapvault-gauge-label">
                    <span>Soif</span>
                    <strong className="mono">{meta.thirst !== null ? `${meta.thirst} %` : "-"}</strong>
                  </div>
                  <div className="snapvault-progress-bg">
                    <div
                      className="snapvault-progress-fill snapvault-thirst"
                      style={{ width: `${Math.max(0, Math.min(100, meta.thirst || 0))}%` }}
                    />
                  </div>
                </div>

                {/* Stress */}
                <div className="snapvault-gauge-card" style={{ gridColumn: "1 / -1" }}>
                  <div className="snapvault-gauge-label">
                    <span>Niveau de Stress</span>
                    <strong className="mono">{meta.stress !== null ? `${meta.stress} %` : "-"}</strong>
                  </div>
                  <div className="snapvault-progress-bg">
                    <div
                      className="snapvault-progress-fill snapvault-stress"
                      style={{ width: `${Math.max(0, Math.min(100, meta.stress || 0))}%` }}
                    />
                  </div>
                </div>
              </div>
            </div>

            {/* Infos Identité & Situation */}
            <div className="snapvault-meta-grid">
              <div className="snapvault-kv">
                <span className="k">Nom RP</span>
                <span className="v"><strong>{meta.rpName || "-"}</strong></span>
              </div>
              <div className="snapvault-kv">
                <span className="k">CitizenID</span>
                <span className="v mono">{meta.citizenid || "-"}</span>
              </div>
              <div className="snapvault-kv">
                <span className="k">ID Serveur / SCM</span>
                <span className="v mono">
                  {meta.serverId ? `Src #${meta.serverId}` : "-"}
                  {meta.scmId ? ` · U #${meta.scmId}` : ""}
                </span>
              </div>
              <div className="snapvault-kv">
                <span className="k">Métier (Job)</span>
                <span className="v">{meta.job || "Aucun"}</span>
              </div>
              <div className="snapvault-kv">
                <span className="k">Gang</span>
                <span className="v">{meta.gang || "Aucun"}</span>
              </div>
              <div className="snapvault-kv">
                <span className="k">Argent liquide (Cash)</span>
                <span className="v mono" style={{ color: "var(--ok)" }}>
                  {meta.money ? fmtCurrency(meta.money.cash) : "-"}
                </span>
              </div>
              <div className="snapvault-kv">
                <span className="k">Compte Bancaire</span>
                <span className="v mono" style={{ color: "oklch(0.75 0.14 220)" }}>
                  {meta.money ? fmtCurrency(meta.money.bank) : "-"}
                </span>
              </div>
              <div className="snapvault-kv">
                <span className="k">Crypto-monnaie</span>
                <span className="v mono" style={{ color: "oklch(0.75 0.14 85)" }}>
                  {meta.money && meta.money.crypto !== undefined ? `${meta.money.crypto} ₿` : "-"}
                </span>
              </div>
            </div>

            {/* Position & Coordonnées */}
            {meta.coords && (
              <div className="snapvault-coords-box">
                <div style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 11, color: "var(--muted)" }}>
                  <Icon name="mapPin" size={13} style={{ color: "var(--accent)" }} />
                  <span>Position X: {meta.coords.x}, Y: {meta.coords.y}, Z: {meta.coords.z}</span>
                  {meta.heading !== undefined && <span>· Cap: {meta.heading}°</span>}
                </div>
                {meta.tpCommand && (
                  <button
                    className="btn btn-sm"
                    onClick={() => onCopy(meta.tpCommand, `tp-${name}`)}
                    title="Copier la commande de téléportation FiveM"
                    style={{
                      display: "inline-flex",
                      alignItems: "center",
                      gap: 6,
                      fontSize: 11,
                      color: copiedKey === `tp-${name}` ? "var(--ok)" : undefined,
                    }}
                  >
                    <Icon name={copiedKey === `tp-${name}` ? "check" : "copy"} size={11} />
                    <span>{copiedKey === `tp-${name}` ? "Copié !" : meta.tpCommand}</span>
                  </button>
                )}
              </div>
            )}

            {/* Date de capture (formatée en français JJ/MM/AAAA à HH:mm comme les logs classiques) */}
            {(meta.capturedAt || name) && (
              <div style={{ fontSize: 11, color: "var(--muted)", display: "flex", alignItems: "center", gap: 6, marginTop: 4 }}>
                <Icon name="calendar" size={11} />
                <span>Horodatage capture : <span className="mono" style={{ color: "var(--text-dim)" }}>{fmtDate(meta.capturedAt || name)}</span></span>
              </div>
            )}
          </div>
        ) : (
          <div className="snapvault-no-meta">
            <span>Aucune métadonnée NUI enregistrée pour ce fichier.</span>
          </div>
        )}
      </div>

      {/* Accordéon déroulant pour le JSON brut formaté */}
      {rawMeta && (
        <div className="snapvault-json-accordion">
          <button
            className="snapvault-json-toggle"
            onClick={() => setJsonExpanded((prev) => !prev)}
          >
            <Icon name={jsonExpanded ? "chevD" : "chevR"} size={12} />
            <span>Données JSON brutes (SnapVault Sidecar)</span>
            <span className="tag tag-muted" style={{ fontSize: 9 }}>
              {Object.keys(rawMeta).length} propriétés
            </span>
          </button>
          {jsonExpanded && (
            <div className="snapvault-json-wrap">
              <pre className="mono snapvault-json-pre">
                {JSON.stringify(rawMeta, null, 2)}
              </pre>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// Dictionnaire de correspondance des types de logs FiveM en français clair et compréhensible
const LOG_TYPE_LABELS = {
  admin: "Administration",
  afk: "Inactivité (AFK)",
  ambulance: "Ambulance / SAMU",
  antichead: "Anticheat",
  anticheat: "Anticheat",
  armor: "Gilet pare-balles",
  ban: "Bannissement",
  banking: "Banque",
  boss: "Menu Patron",
  boutique: "Boutique",
  business: "Entreprise",
  carwash: "Station de lavage",
  character: "Personnage",
  connect: "Connexion",
  coords: "Coordonnées",
  death: "Mort",
  disconnect: "Déconnexion",
  drop: "Déconnexion forcée",
  duty: "Prise / Fin de service",
  economy: "Économie",
  explosion: "Explosion",
  farm: "Récolte / Farm",
  feed: "Nourriture / Faim",
  freeze: "Immobilisation",
  giveitem: "Don d'objet",
  glovebox: "Boîte à gants",
  godmode: "Mode Invincible (Godmode)",
  heal: "Soin",
  inventory: "Inventaire",
  jail: "Prison (Jail)",
  joinleave: "Connexion / Déconnexion",
  kick: "Expulsion (Kick)",
  kill: "Meurtre (Kill)",
  ltd: "Supérette LTD",
  mechanic: "Mécano",
  message: "Message",
  message_player: "Message privé joueur",
  noclip: "Noclip",
  pawnshop: "Prêteur sur gages",
  pharmacie: "Pharmacie",
  "phone:calls": "Téléphone : Appels",
  "phone:instapic": "Téléphone : InstaPic",
  "phone:messages": "Téléphone : Messages",
  "phone:services": "Téléphone : Services",
  "phone:uploads": "Téléphone : Médias",
  playermoney: "Argent joueur",
  police: "Police / LSPD",
  recyclejob: "Recyclage",
  reporter: "Journaliste / Reporter",
  report_close: "Ticket fermé",
  report_create: "Ticket créé",
  report_take: "Ticket pris en charge",
  revive: "Réanimation (Revive)",
  role_assign: "Attribution de rôle",
  role_color: "Couleur de rôle",
  role_create: "Création de rôle",
  role_outfit: "Tenue de rôle",
  role_perms: "Permissions de rôle",
  rstaff: "Remboursement Staff",
  screenshot: "Capture d'écran",
  security: "Sécurité",
  setjob: "Attribution de métier",
  shop: "Magasin / Shop",
  spectate: "Spectateur",
  staffchat: "Chat Staff",
  stash: "Coffre / Stockage",
  taxi: "Taxi",
  teleport: "Téléportation",
  trunk: "Coffre véhicule",
  unban: "Débannissement",
  unjail: "Sortie de prison (Unjail)",
  vanish: "Mode Invisible (Vanish)",
  vehicle: "Véhicule",
  vehicle_clean: "Nettoyage véhicule",
  vehicle_delete: "Suppression véhicule",
  vehicle_delete_all: "Suppression de tous les véhicules",
  vehicle_delete_zone: "Suppression véhicules (zone)",
  vehicle_flip: "Retournement véhicule",
  vehicle_freeze: "Immobilisation véhicule",
  vehicle_repair: "Réparation véhicule",
  vehicle_repair_zone: "Réparation véhicules (zone)",
  vehicle_spawn: "Apparition véhicule",
  vineyard: "Vigneron",
  vip: "VIP",
  warn: "Avertissement (Warn)",
  weapon: "Arme",
  weather: "Météo",
  zone_clear_objects: "Zone : Nettoyage objets",
  zone_clear_peds: "Zone : Nettoyage PNJ",
  zone_clear_props: "Zone : Nettoyage props",
  zone_heal: "Zone : Soin global",
  zone_message: "Zone : Message global",
  zone_revive: "Zone : Réanimation globale",
};

// Helper: formatage du type de log FiveM en français compréhensible
// Les types non reconnus sont conservés tels quels pour être mappés au fur et à mesure
function formatLogType(type) {
  if (!type) return "Événement";
  const s = String(type).trim();
  if (s.toLowerCase().startsWith("table:") || s.toLowerCase().startsWith("table :")) {
    return "Événement";
  }
  if (LOG_TYPE_LABELS[s]) return LOG_TYPE_LABELS[s];
  const lower = s.toLowerCase();
  if (LOG_TYPE_LABELS[lower]) return LOG_TYPE_LABELS[lower];
  return s;
}

const DYNAMIC_TAG_PALETTE = [
  "tag-blue",
  "tag-purple",
  "tag-cyan",
  "tag-orange",
  "tag-teal",
  "tag-lime",
  "tag-indigo",
  "tag-violet",
  "tag-rose",
  "tag-sky",
  "tag-amber",
];

// Helper: badge de catégorie FiveM riche en couleurs et thématiques
function getTypeBadgeClass(type) {
  if (!type) return "tag-muted";
  const t = String(type).toLowerCase().trim();
  if (t.startsWith("table:") || t.startsWith("table :")) return "tag-muted";

  // Preuves visuelles et captures d'écran staff -> Cyan
  if (t.includes("screenshot") || t.includes("capture")) {
    return "tag-cyan";
  }

  // 1. Anticheat, Ban, Kill, Sanctions lourdes -> Rouge / Danger
  if (
    t.includes("anticheat") ||
    t.includes("ban") ||
    t.includes("kick") ||
    t.includes("kill") ||
    t.includes("meurtre") ||
    t.includes("expulsion") ||
    t.includes("unban")
  ) {
    return "tag-err";
  }

  // 2. Avertissements, Armes, Dégâts, Explosions, Sécurité -> Orange alerte / Warn
  if (
    t.includes("warn") ||
    t.includes("avertissement") ||
    t.includes("weapon") ||
    t.includes("arme") ||
    t.includes("explosion") ||
    t.includes("damage") ||
    t.includes("sécurité") ||
    t.includes("security") ||
    t.includes("armor") ||
    t.includes("gilet")
  ) {
    return "tag-warn";
  }

  // 3. Staff, Administration, Tickets -> Indigo / Blurple
  if (
    t.includes("admin") ||
    t.includes("staff") ||
    t.includes("report") ||
    t.includes("ticket")
  ) {
    return "tag-indigo";
  }

  // 4. Police, Prison -> Bleu autorité
  if (
    t.includes("police") ||
    t.includes("lspd") ||
    t.includes("jail") ||
    t.includes("prison") ||
    t.includes("unjail")
  ) {
    return "tag-blue";
  }

  // 5. Santé, SAMU, Mort, Réanimation -> Rose médical
  if (
    t.includes("ambulance") ||
    t.includes("samu") ||
    t.includes("death") ||
    t.includes("mort") ||
    t.includes("heal") ||
    t.includes("soin") ||
    t.includes("revive") ||
    t.includes("réanimation") ||
    t.includes("hospital") ||
    t.includes("pharmacie")
  ) {
    return "tag-rose";
  }

  // 6. Économie, Banque, Argent -> Vert Émeraude
  if (
    t.includes("money") ||
    t.includes("argent") ||
    t.includes("bank") ||
    t.includes("banque") ||
    t.includes("economy") ||
    t.includes("économi") ||
    t.includes("salary") ||
    t.includes("pay")
  ) {
    return "tag-ok";
  }

  // 7. Boutique, VIP, Patron, Entreprise -> Violet Royal
  if (
    t.includes("boutique") ||
    t.includes("vip") ||
    t.includes("boss") ||
    t.includes("patron") ||
    t.includes("business") ||
    t.includes("entreprise")
  ) {
    return "tag-violet";
  }

  // 8. Téléportation, Modificateurs joueurs (Vanish, Godmode, Noclip) -> Pourpre
  if (
    t.includes("teleport") ||
    t.includes("téléportation") ||
    t.includes("coords") ||
    t.includes("noclip") ||
    t.includes("spectat") ||
    t.includes("vanish") ||
    t.includes("godmode") ||
    t.includes("freeze")
  ) {
    return "tag-purple";
  }

  // 9. Véhicules, Dépannage, Garages -> Orange Mécanique
  if (
    t.includes("vehicle") ||
    t.includes("véhicule") ||
    t.includes("mechanic") ||
    t.includes("mécano") ||
    t.includes("car")
  ) {
    return "tag-orange";
  }

  // 10. Téléphone, Réseaux, Messages privés -> Fuchsia
  if (
    t.includes("phone") ||
    t.includes("téléphone") ||
    t.includes("instapic") ||
    t.includes("message") ||
    t.includes("sms")
  ) {
    return "tag-fuchsia";
  }

  // 11. Farm, Nature, Métiers artisanaux, Inventaire -> Lime / Vert clair
  if (
    t.includes("farm") ||
    t.includes("récolte") ||
    t.includes("vineyard") ||
    t.includes("vigneron") ||
    t.includes("recycle") ||
    t.includes("recyclage") ||
    t.includes("inventory") ||
    t.includes("inventaire") ||
    t.includes("giveitem") ||
    t.includes("craft") ||
    t.includes("trunk") ||
    t.includes("glovebox") ||
    t.includes("coffre") ||
    t.includes("stash")
  ) {
    return "tag-lime";
  }

  // 12. Commerces & Services civils -> Cyan / Turquoise
  if (
    t.includes("taxi") ||
    t.includes("reporter") ||
    t.includes("journaliste") ||
    t.includes("carwash") ||
    t.includes("lavage") ||
    t.includes("pawnshop") ||
    t.includes("ltd") ||
    t.includes("shop") ||
    t.includes("magasin") ||
    t.includes("feed") ||
    t.includes("faim") ||
    t.includes("nourriture")
  ) {
    return "tag-cyan";
  }

  // 13. Rôles, Personnage, Métiers -> Bleu Ciel
  if (
    t.includes("role") ||
    t.includes("rôle") ||
    t.includes("job") ||
    t.includes("métier") ||
    t.includes("character") ||
    t.includes("personnage") ||
    t.includes("duty") ||
    t.includes("service")
  ) {
    return "tag-sky";
  }

  // 14. Nettoyage de zone & Événements de zone -> Ambre doré
  if (t.includes("zone")) {
    return "tag-amber";
  }

  // 15. Connexions, Déconnexions, AFK, Météo -> Teal / Menthe
  if (
    t.includes("join") ||
    t.includes("leave") ||
    t.includes("connect") ||
    t.includes("disconnect") ||
    t.includes("connexion") ||
    t.includes("déconnexion") ||
    t.includes("afk") ||
    t.includes("inactivité") ||
    t.includes("weather") ||
    t.includes("météo")
  ) {
    return "tag-teal";
  }

  // 16. Fallback dynamique pour tout nouveau type : couleur pastel déterminée par son nom (jamais gris fade)
  let hash = 0;
  for (let i = 0; i < t.length; i++) hash = (hash << 5) - hash + t.charCodeAt(i);
  return DYNAMIC_TAG_PALETTE[Math.abs(hash) % DYNAMIC_TAG_PALETTE.length];
}

// Helper: détection de coordonnées FiveM dans le message
function extractCoords(msg) {
  if (!msg) return null;
  const m = msg.match(/(?:vector3|vec3|\/tp)?\s*\(?\s*(-?\d+\.?\d*)[,\s]+(-?\d+\.?\d*)[,\s]+(-?\d+\.?\d*)\)?/i);
  if (m && Math.abs(parseFloat(m[1])) <= 10000 && Math.abs(parseFloat(m[2])) <= 10000) {
    return `/tp ${parseFloat(m[1]).toFixed(1)} ${parseFloat(m[2]).toFixed(1)} ${parseFloat(m[3]).toFixed(1)}`;
  }
  return null;
}

// =================================================================
// Composant sélecteur multiple avec recherche intégrée pour les types
// =================================================================
function TypeMultiSelect({
  types = [],
  selected = [],
  onChange,
  formatType = formatLogType,
  getBadgeClass = getTypeBadgeClass,
  placeholder = "Tous les types",
  searchPlaceholder = "Rechercher un type (ex: ban, mort, ticket...)",
}) {
  const [open, setOpen] = useState(false);
  const [search, setSearch] = useState("");
  const wrapRef = useRef(null);

  // Fermer quand on clique en dehors
  useEffect(() => {
    function handleClickOutside(e) {
      if (wrapRef.current && !wrapRef.current.contains(e.target)) {
        setOpen(false);
      }
    }
    if (open) {
      document.addEventListener("mousedown", handleClickOutside);
      return () => document.removeEventListener("mousedown", handleClickOutside);
    }
  }, [open]);

  const filteredTypes = useMemo(() => {
    if (!search.trim()) return types;
    const s = search.trim().toLowerCase();
    return types.filter((t) => {
      const label = formatType(t).toLowerCase();
      const raw = t.toLowerCase();
      return label.includes(s) || raw.includes(s);
    });
  }, [types, search, formatType]);

  const toggleType = (typeKey) => {
    const isSelected = selected.includes(typeKey);
    let next;
    if (isSelected) {
      next = selected.filter((t) => t !== typeKey);
    } else {
      next = [...selected, typeKey];
    }
    onChange(next);
  };

  const handleSelectAllVisible = () => {
    const newItems = new Set([...selected, ...filteredTypes]);
    onChange(Array.from(newItems));
  };

  const handleClear = (e) => {
    if (e) e.stopPropagation();
    onChange([]);
  };

  return (
    <div className="multiselect" ref={wrapRef}>
      <div
        className={`multiselect-trigger ${open ? "open" : ""}`}
        onClick={() => setOpen((prev) => !prev)}
      >
        <div className="val">
          {selected.length === 0 ? (
            <span style={{ color: "var(--text-dim)" }}>{placeholder} ({types.length})</span>
          ) : selected.length === 1 ? (
            <span className={`tag ${getBadgeClass(selected[0])}`} style={{ fontSize: 10 }}>
              {formatType(selected[0])}
            </span>
          ) : selected.length === 2 ? (
            <>
              <span className={`tag ${getBadgeClass(selected[0])}`} style={{ fontSize: 10 }}>
                {formatType(selected[0])}
              </span>
              <span className={`tag ${getBadgeClass(selected[1])}`} style={{ fontSize: 10 }}>
                {formatType(selected[1])}
              </span>
            </>
          ) : (
            <>
              <span className="multiselect-count-badge">{selected.length} types</span>
              <span className={`tag ${getBadgeClass(selected[0])}`} style={{ fontSize: 10 }}>
                {formatType(selected[0])}
              </span>
            </>
          )}
        </div>

        <div style={{ display: "flex", alignItems: "center", gap: 4 }}>
          {selected.length > 0 && (
            <button
              className="icon-btn"
              style={{ width: 18, height: 18, border: "none" }}
              title="Réinitialiser les types"
              onClick={handleClear}
            >
              <Icon name="x" size={12} />
            </button>
          )}
          <Icon name={open ? "chevD" : "chevR"} size={13} style={{ color: "var(--muted)" }} />
        </div>
      </div>

      {open && (
        <div className="multiselect-popover">
          <div className="multiselect-search-wrap">
            <input
              className="input"
              placeholder={searchPlaceholder}
              value={search}
              onChange={(e) => setSearch(e.target.value)}
              autoFocus
            />
          </div>

          <div className="multiselect-actions">
            <button className="multiselect-action-btn" onClick={handleSelectAllVisible}>
              Tout sélectionner {search.trim() ? "visibles" : ""}
            </button>
            {selected.length > 0 && (
              <button className="multiselect-action-btn" onClick={() => onChange([])}>
                Tout désélectionner ({selected.length})
              </button>
            )}
          </div>

          <div className="multiselect-list">
            {filteredTypes.length === 0 ? (
              <div style={{ padding: "12px 8px", textAlign: "center", color: "var(--muted)", fontSize: 11 }}>
                Aucun type ne correspond à « {search} »
              </div>
            ) : (
              filteredTypes.map((t) => {
                const isSelected = selected.includes(t);
                const label = formatType(t);
                return (
                  <div
                    key={t}
                    className={`multiselect-item ${isSelected ? "selected" : ""}`}
                    onClick={() => toggleType(t)}
                  >
                    <input
                      type="checkbox"
                      checked={isSelected}
                      onChange={() => {}}
                    />
                    <span className={`tag ${getBadgeClass(t)}`} style={{ fontSize: 11 }}>
                      {label}
                    </span>
                    {label !== t && (
                      <span className="mono" style={{ fontSize: 10, color: "var(--muted)", marginLeft: "auto" }}>
                        {t}
                      </span>
                    )}
                  </div>
                );
              })
            )}
          </div>
        </div>
      )}
    </div>
  );
}

// =================================================================
// LOGS PAGE - Logs serveur FiveM (Table scm_logs) & BDD MySQL/SQL
// =================================================================
function LogsPage({ user, perms, serverName: propServerName, serverLogo: propServerLogo }) {
  const [logs, setLogs] = useState([]);
  const [total, setTotal] = useState(0);
  const [types, setTypes] = useState([]);
  const [dbStatus, setDbStatus] = useState(null);
  const [fetchLoading, setFetchLoading] = useState(true);
  const [fetchError, setFetchError] = useState(null);
  const [canSeeSensitive, setCanSeeSensitive] = useState(perms.logsSensitive || false);
  const [apiServerInfo, setApiServerInfo] = useState({ name: "", logo: "" });

  const [q, setQ] = useState("");
  const [qApplied, setQApplied] = useState("");
  const [qScopes, setQScopes] = useState(["all"]);
  const [qScopesApplied, setQScopesApplied] = useState(["all"]);
  const [selectedTypes, setSelectedTypes] = useState([]);
  const [range, setRange] = useState("all");
  const [open, setOpen] = useState(new Set());
  const [page, setPage] = useState(0);
  const [perPage, setPerPage] = useState(25);
  const [copiedKey, setCopiedKey] = useState(null);
  const [identityModalLog, setIdentityModalLog] = useState(null);

  // SnapVault on-demand evidence cache
  const [evidenceMap, setEvidenceMap] = useState({});
  const [loadingEvidence, setLoadingEvidence] = useState({});
  const [lightbox, setLightbox] = useState(null);

  const fetchEvidence = useCallback(async (logId) => {
    if (evidenceMap[logId] !== undefined || loadingEvidence[logId]) return;
    setLoadingEvidence((prev) => ({ ...prev, [logId]: true }));
    try {
      const resp = await fetch(`/api/logs/${logId}/evidence`);
      if (resp.ok) {
        const data = await resp.json();
        setEvidenceMap((prev) => ({ ...prev, [logId]: data }));
      } else {
        setEvidenceMap((prev) => ({ ...prev, [logId]: { hasEvidence: false } }));
      }
    } catch (_) {
      setEvidenceMap((prev) => ({ ...prev, [logId]: { hasEvidence: false } }));
    } finally {
      setLoadingEvidence((prev) => ({ ...prev, [logId]: false }));
    }
  }, [evidenceMap, loadingEvidence]);

  const handleFilterPlayer = (searchStr, scope = "unique_id") => {
    setQ(searchStr);
    setQApplied(searchStr);
    setQScopes([scope]);
    setQScopesApplied([scope]);
    setPage(0);
  };

  const handleScopesChange = (nextScopes) => {
    setQScopes(nextScopes);
    if (qApplied || q.trim()) {
      setPage(0);
      setQApplied(q.trim());
      setQScopesApplied(nextScopes);
    }
  };

  // Filtrer les types valides (exclure les adresses de tables Lua)
  const cleanTypes = useMemo(() => {
    return types.filter((t) => t && !t.toLowerCase().startsWith("table:") && !t.toLowerCase().startsWith("table :"));
  }, [types]);

  const fetchLogs = useCallback(async (params) => {
    setFetchLoading(true);
    setFetchError(null);
    try {
      const qParams = new URLSearchParams({
        page: params.page ?? 0,
        perPage: params.perPage ?? 25,
        type: params.selectedTypes && params.selectedTypes.length > 0 ? params.selectedTypes.join(",") : "all",
        q: params.qApplied ?? "",
        qFields: (params.qScopesApplied || ["all"]).join(","),
        range: params.range ?? "all",
      });
      const res = await fetch(`/api/logs?${qParams.toString()}`);
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        if (res.status === 403) throw new Error("Accès refusé : votre grade Discord ne permet pas de voir les logs serveur.");
        throw new Error(data.error || "Erreur de connexion à la base de données MySQL.");
      }
      setLogs(data.logs || []);
      setTotal(data.total || 0);
      if (Array.isArray(data.types)) setTypes(data.types);
      setCanSeeSensitive(data.canSeeSensitive);
      if (data.dbStatus) setDbStatus(data.dbStatus);
      if (data.serverName) setApiServerInfo({ name: data.serverName, logo: data.serverLogo || "" });
    } catch (err) {
      setFetchError(err.message);
    } finally {
      setFetchLoading(false);
    }
  }, []);

  useEffect(() => {
    fetchLogs({ page, perPage, selectedTypes, qApplied, qScopesApplied, range });
  }, [page, perPage, selectedTypes, qApplied, qScopesApplied, range, fetchLogs]);

  const copyText = (text, key) => {
    if (!text) return;
    const cleanText = text.replace(/[\u2014\u2013]/g, "-");
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(cleanText);
    } else {
      const el = document.createElement("textarea");
      el.value = cleanText;
      document.body.appendChild(el);
      el.select();
      document.execCommand("copy");
      document.body.removeChild(el);
    }
    setCopiedKey(key);
    setTimeout(() => setCopiedKey((curr) => (curr === key ? null : curr)), 1800);
  };

  const currentServerName = propServerName || apiServerInfo.name || "Mon Serveur RP";
  const currentServerLogo = propServerLogo || apiServerInfo.logo || "";

  const totalPages = Math.max(1, Math.ceil(total / perPage));

  const toggle = (id, logObj) => {
    const n = new Set(open);
    const willOpen = !n.has(id);
    if (willOpen) {
      n.add(id);
      // Récupération à la demande quand le joueur clique sur un log avec preuve
      if (logObj && (logObj.has_evidence || isEvidenceCandidate(logObj))) {
        fetchEvidence(id);
      }
    } else {
      n.delete(id);
    }
    setOpen(n);
  };

  const handleSearchSubmit = () => {
    setPage(0);
    setQApplied(q.trim());
    setQScopesApplied([...qScopes]);
  };

  const handleReset = () => {
    setQ("");
    setQApplied("");
    setSelectedTypes([]);
    setRange("all");
    setQScopes(["all"]);
    setQScopesApplied(["all"]);
    setPage(0);
  };

  if (fetchError) {
    const isConnErr = fetchError.toLowerCase().includes("connexion") || fetchError.toLowerCase().includes("mysql") || fetchError.toLowerCase().includes("base de données") || fetchError.toLowerCase().includes("database");
    return (
      <div className="card" style={{ padding: "40px 30px", textAlign: "center", maxWidth: 640, margin: "24px auto" }}>
        <div style={{ width: 56, height: 56, borderRadius: "50%", background: "oklch(0.35 0.15 25 / 0.2)", border: "1px solid oklch(0.5 0.15 25 / 0.4)", display: "grid", placeItems: "center", margin: "0 auto 16px", color: "var(--danger)" }}>
          <Icon name={isConnErr ? "database" : "warn"} size={26} />
        </div>
        <h2 style={{ fontSize: 18, marginBottom: 8, color: "var(--text)" }}>
          {isConnErr ? "Erreur de connexion à la base de données" : "Accès refusé aux logs"}
        </h2>
        <p style={{ color: "var(--text-dim)", fontSize: 13, lineHeight: 1.6, marginBottom: 20 }}>
          {fetchError}
        </p>

        {isConnErr && (
          <div style={{ background: "var(--bg-2)", border: "1px solid var(--border)", borderRadius: 8, padding: "14px 16px", textAlign: "left", fontSize: 12, marginBottom: 20 }}>
            <div style={{ fontWeight: 600, marginBottom: 6, color: "var(--warn)" }}>Configuration MySQL requise dans votre fichier .env :</div>
            <div className="mono" style={{ color: "var(--text-dim)", lineHeight: 1.7 }}>
              DATABASE_URL=mysql://utilisateur:motdepasse@hote:3306/QBCore<br />
              <span style={{ color: "var(--muted)" }}># ou paramètres séparés :</span><br />
              DB_HOST={dbStatus?.host || "localhost"} | DB_PORT={dbStatus?.port || 3306} | DB_NAME={dbStatus?.database || "QBCore"} | DB_TABLE={dbStatus?.table || "scm_logs"}
            </div>
          </div>
        )}

        <button
          className="btn btn-primary"
          style={{ display: "inline-flex", alignItems: "center", gap: 8 }}
          onClick={() => fetchLogs({ page, perPage, selectedTypes, qApplied, range })}
        >
          <Icon name="refresh" size={14} />
          <span>Réessayer la connexion</span>
        </button>
      </div>
    );
  }

  return (
    <>
      {/* Bannière personnalisée du serveur avec état de la BDD */}
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "12px 18px", background: "var(--surface)", border: "1px solid var(--border)", borderRadius: "var(--r)", marginBottom: 14 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
          {currentServerLogo ? (
            <img src={currentServerLogo} alt="" style={{ width: 38, height: 38, borderRadius: 8, objectFit: "cover" }} />
          ) : (
            <div className="logo" style={{ width: 38, height: 38, borderRadius: 8, fontSize: 16, fontWeight: 700 }}>
              {currentServerName[0].toUpperCase()}
            </div>
          )}
          <div>
            <div style={{ fontSize: 15, fontWeight: 600 }}>{currentServerName}</div>
            <div style={{ fontSize: 11, color: "var(--muted)" }}>Journaux d'activité et événements FiveM en direct</div>
          </div>
        </div>

        {/* Statut de la Base de Données */}
        <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
          <span
            className="dev-pill"
            style={{ color: "var(--ok)", borderColor: "oklch(0.5 0.14 155 / 0.4)", display: "inline-flex", alignItems: "center", gap: 6, textTransform: "none", fontSize: 11 }}
            title={`Hôte MySQL: ${dbStatus?.host}:${dbStatus?.port} - Base: ${dbStatus?.database} - Table: ${dbStatus?.table}`}
          >
            <Icon name="database" size={13} />
            <span>Connecté à la BDD</span>
          </span>
        </div>
      </div>

      {/* Filtres de recherche */}
      <div className="filters">
        <SearchField
          value={q}
          onChange={setQ}
          applied={qApplied}
          onSubmit={handleSearchSubmit}
          loading={fetchLoading}
          scopes={qScopes}
          onScopesChange={handleScopesChange}
          appliedScopes={qScopesApplied}
          placeholder="Recherche selon les cibles sélectionnées... - Entrée pour rechercher"
        />
        <div className="field multiselect-field">
          <label>Type / Événement</label>
          <TypeMultiSelect
            types={cleanTypes}
            selected={selectedTypes}
            onChange={(newTypes) => {
              setSelectedTypes(newTypes);
              setPage(0);
            }}
          />
        </div>
        <div className="field">
          <label>Période</label>
          <select
            className="select"
            value={range}
            onChange={(e) => {
              setRange(e.target.value);
              setPage(0);
            }}
          >
            <option value="all">Tout l'historique</option>
            <option value="24h">Dernières 24 heures</option>
            <option value="7d">7 derniers jours</option>
            <option value="30d">30 derniers jours</option>
          </select>
        </div>
        <button className="btn" onClick={handleReset}>
          Réinitialiser
        </button>
      </div>

      {/* Tableau des logs */}
      <div className="tbl-wrap">
        {fetchLoading && <LoadingOverlay label="Chargement des logs FiveM…" />}
        <table className="tbl">
          <thead>
            <tr>
              <th style={{ width: 32 }}></th>
              <th style={{ width: 165 }}>Date & Heure</th>
              <th style={{ width: 125 }}>Type</th>
              <th style={{ width: 220 }}>Joueur</th>
              <th>Message</th>
              <th style={{ width: 180 }}>Discord</th>
            </tr>
          </thead>
          <tbody>
            {logs.length === 0 && !fetchLoading && (
              <tr><td colSpan={6}><div className="empty">Aucun log FiveM ne correspond aux critères.</div></td></tr>
            )}
            {logs.map((l) => {
              const isOpen = open.has(l.id);
              const displayType = formatLogType(l.type);
              const tagClass = getTypeBadgeClass(displayType);
              const playerName = l.pseudo_rp || l.pseudo_fivem || "Système";
              const coords = extractCoords(l.message);

              return (
                <React.Fragment key={l.id}>
                  <tr className={`row ${isOpen ? "open" : ""}`} onClick={() => toggle(l.id, l)}>
                    <td><span className={`caret ${isOpen ? "open" : ""}`}><Icon name="chevR" /></span></td>
                    <td className="mono" style={{ fontSize: 12, color: "var(--text-dim)", whiteSpace: "nowrap" }}>
                      {fmtDate(l.timestamp)}
                    </td>
                    <td>
                      <span className={`tag ${tagClass}`}>{displayType}</span>
                      {l.has_evidence && (
                        <span
                          title="Capture d'écran disponible dans SnapVault (cliquer pour consulter)"
                          style={{
                            display: "inline-flex",
                            alignItems: "center",
                            color: "oklch(0.78 0.14 210)",
                            marginLeft: 6,
                            verticalAlign: "middle",
                          }}
                        >
                          <Icon name="camera" size={13} />
                        </span>
                      )}
                    </td>
                    <td>
                      <div>
                        <div style={{ fontWeight: 600, color: "var(--text)" }}>{playerName}</div>
                        <div style={{ fontSize: 11, color: "var(--muted)", display: "flex", alignItems: "center", gap: 6 }}>
                          <span>ID #{l.scm_id !== null && l.scm_id !== undefined ? l.scm_id : "-"}</span>
                          {l.pseudo_fivem && <span>• {l.pseudo_fivem}</span>}
                        </div>
                      </div>
                    </td>
                    <td>
                      <div
                        style={{ color: "var(--text-dim)", maxWidth: 440, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
                        title={l.message}
                      >
                        {l.message ? l.message.replace(/[\u2014\u2013]/g, "-") : "-"}
                      </div>
                    </td>
                    <td className="mono" style={{ fontSize: 12, color: "var(--text-dim)" }}>
                      {l.discord ? (
                        <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
                          <Icon name="discord" size={13} style={{ color: "#5865F2", flexShrink: 0 }} />
                          <span>{l.discord}</span>
                        </span>
                      ) : (
                        "-"
                      )}
                    </td>
                  </tr>

                  {/* Panneau de développement dépliant */}
                  {isOpen && (
                    <tr className="detail">
                      <td colSpan={6}>
                        <div className="detail-header">
                          <div style={{ display: "flex", alignItems: "center", gap: 8, fontWeight: 600 }}>
                            <Icon name="terminal" size={14} style={{ color: "var(--accent)" }} />
                            <span>Détails techniques du log FiveM</span>
                            <span className="tag tag-muted" style={{ fontSize: 10 }}>#{l.id}</span>
                          </div>
                          <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
                            <button
                              className="btn btn-sm btn-primary"
                              style={{
                                display: "inline-flex",
                                alignItems: "center",
                                gap: 7,
                                fontWeight: 600,
                              }}
                              onClick={(e) => {
                                e.stopPropagation();
                                setIdentityModalLog(l);
                              }}
                              title="Ouvrir la fiche croisée consolidée de ce joueur"
                            >
                              <Icon name="link" size={12} />
                              <span>Fiche croisée</span>
                            </button>
                            {coords && (
                              <button
                                className="btn btn-sm"
                                style={{
                                  display: "inline-flex",
                                  alignItems: "center",
                                  gap: 7,
                                  color: copiedKey === `coords-${l.id}` ? "var(--ok)" : undefined,
                                  borderColor: copiedKey === `coords-${l.id}` ? "var(--ok)" : undefined,
                                  background: copiedKey === `coords-${l.id}` ? "oklch(0.35 0.14 155 / 0.15)" : undefined,
                                  transition: "all 0.15s ease",
                                }}
                                onClick={(e) => { e.stopPropagation(); copyText(coords, `coords-${l.id}`); }}
                                title="Copier la commande de téléportation FiveM"
                              >
                                <Icon name={copiedKey === `coords-${l.id}` ? "check" : "copy"} size={12} />
                                <span>{copiedKey === `coords-${l.id}` ? "Commande /tp copiée !" : coords}</span>
                              </button>
                            )}
                            <button
                              className="btn btn-sm"
                              style={{
                                display: "inline-flex",
                                alignItems: "center",
                                gap: 7,
                                color: copiedKey === `msg-${l.id}` ? "var(--ok)" : undefined,
                                borderColor: copiedKey === `msg-${l.id}` ? "var(--ok)" : undefined,
                                background: copiedKey === `msg-${l.id}` ? "oklch(0.35 0.14 155 / 0.15)" : undefined,
                                transition: "all 0.15s ease",
                              }}
                              onClick={(e) => { e.stopPropagation(); copyText(l.message, `msg-${l.id}`); }}
                            >
                              <Icon name={copiedKey === `msg-${l.id}` ? "check" : "copy"} size={12} />
                              <span>{copiedKey === `msg-${l.id}` ? "Message copié !" : "Copier le message"}</span>
                            </button>
                          </div>
                        </div>

                        <div className="detail-grid">
                          <div className="kv">
                            <div className="k">ID en jeu (scm_id)</div>
                            <div className="v mono">
                              {l.scm_id !== null && l.scm_id !== undefined ? (
                                `#${l.scm_id}`
                              ) : l.inferred?.scm_id ? (
                                <InferredValue inferred={{ value: `#${l.inferred.scm_id.value}`, source: l.inferred.scm_id.source, confidence: l.inferred.scm_id.confidence }} copyKey={`inf-scm-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier SCM ID déduit" />
                              ) : (
                                "-"
                              )}
                            </div>
                          </div>
                          <div className="kv">
                            <div className="k">Pseudo RP</div>
                            <div className="v">
                              {l.pseudo_rp ? (
                                <strong>{l.pseudo_rp}</strong>
                              ) : l.inferred?.pseudo_rp ? (
                                <InferredValue inferred={l.inferred.pseudo_rp} copyKey={`inf-rp-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Pseudo RP déduit" />
                              ) : (
                                "-"
                              )}
                            </div>
                          </div>
                          <div className="kv">
                            <div className="k">Pseudo FiveM</div>
                            <div className="v mono">
                              {l.pseudo_fivem ? (
                                l.pseudo_fivem
                              ) : l.inferred?.pseudo_fivem ? (
                                <InferredValue inferred={l.inferred.pseudo_fivem} copyKey={`inf-pfm-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Pseudo FiveM déduit" />
                              ) : (
                                "-"
                              )}
                            </div>
                          </div>
                          <div className="kv">
                            <div className="k">Identifiant Discord</div>
                            <div className="v mono" style={{ display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
                              {l.discord ? (
                                <>
                                  <Icon name="discord" size={13} style={{ color: "#5865F2", flexShrink: 0 }} />
                                  <CopyableValue value={l.discord} copyKey={`disc-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Discord ID" />
                                </>
                              ) : l.inferred?.discord ? (
                                <InferredValue inferred={l.inferred.discord} copyKey={`disc-inf-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Discord ID déduit" icon="discord" />
                              ) : (
                                "-"
                              )}
                            </div>
                          </div>
                          <div className="kv">
                            <div className="k">Licence Rockstar 1</div>
                            <div className="v mono">
                              {l.license ? (
                                <CopyableValue value={l.license} copyKey={`lic1-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Licence Rockstar 1" />
                              ) : l.inferred?.license ? (
                                <InferredValue inferred={l.inferred.license} copyKey={`inf-lic1-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Licence déduite" />
                              ) : (
                                "-"
                              )}
                            </div>
                          </div>
                          <div className="kv">
                            <div className="k">Licence Rockstar 2</div>
                            <div className="v mono">
                              <CopyableValue value={l.license2} copyKey={`lic2-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Licence Rockstar 2" />
                            </div>
                          </div>
                          <div className="kv">
                            <div className="k">Identifiant Steam</div>
                            <div className="v mono">
                              {l.steam ? (
                                <CopyableValue value={l.steam} copyKey={`steam-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Steam ID" />
                              ) : l.inferred?.steam ? (
                                <InferredValue inferred={l.inferred.steam} copyKey={`inf-stm-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Steam ID déduit" />
                              ) : (
                                "-"
                              )}
                            </div>
                          </div>
                          <div className="kv">
                            <div className="k">Identifiant FiveM</div>
                            <div className="v mono">
                              {l.fivem ? (
                                <CopyableValue value={l.fivem} copyKey={`fivem-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier FiveM ID" />
                              ) : l.inferred?.fivem ? (
                                <InferredValue inferred={l.inferred.fivem} copyKey={`inf-fiv-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier FiveM ID déduit" />
                              ) : (
                                "-"
                              )}
                            </div>
                          </div>
                          <div className="kv">
                            <div className="k">Adresse IP</div>
                            <div className="v mono">
                              {canSeeSensitive ? (
                                l.ip ? (
                                  <CopyableValue value={l.ip} copyKey={`ip-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Adresse IP" />
                                ) : l.inferred?.ip ? (
                                  <InferredValue inferred={l.inferred.ip} copyKey={`inf-ip-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier IP déduite" />
                                ) : (
                                  "-"
                                )
                              ) : (
                                <span className="masked" title="Grade Discord insuffisant pour afficher l'IP (strippée côté serveur)">
                                  [Masqué - Rôle requis]
                                </span>
                              )}
                            </div>
                          </div>
                          <div className="kv">
                            <div className="k">Catégorie / Type</div>
                            <div className="v" style={{ display: "flex", alignItems: "center", gap: 6 }}>
                              <span className={`tag ${tagClass}`}>{displayType}</span>
                              {displayType !== l.type && (
                                <span className="mono" style={{ fontSize: 11, color: "var(--muted)" }}>({l.type})</span>
                              )}
                            </div>
                          </div>
                          <div className="kv"><div className="k">Date & Heure</div><div className="v mono">{fmtDate(l.timestamp)}</div></div>
                          <div className="kv" style={{ gridColumn: "1 / -1" }}>
                            <div className="k">Message complet</div>
                            <div className="v" style={{ background: "var(--surface)", padding: "10px 14px", borderRadius: 6, border: "1px solid var(--border)", fontFamily: "inherit", whiteSpace: "pre-wrap", lineHeight: 1.5 }}>
                              {l.message ? l.message.replace(/[\u2014\u2013]/g, "-") : "-"}
                            </div>
                          </div>
                        </div>

                        {/* Preuve SnapVault (Capture d'écran & métadonnées) */}
                        {(l.has_evidence || isEvidenceCandidate(l)) && (
                          <SnapVaultEvidencePanel
                            evidence={evidenceMap[l.id]}
                            loading={loadingEvidence[l.id]}
                            onZoomImage={(url, title) => setLightbox({ url, title })}
                            copiedKey={copiedKey}
                            onCopy={copyText}
                          />
                        )}
                      </td>
                    </tr>
                  )}
                </React.Fragment>
              );
            })}
          </tbody>
        </table>

        {/* Pagination */}
        <div className="pagination">
          <PerPageSelect value={perPage} onChange={(val) => { setPerPage(val); setPage(0); }} />
          <div>{total.toLocaleString("fr-FR")} log{total > 1 ? "s" : ""} FiveM - page {page + 1}/{totalPages}</div>
          <div className="pages">
            <button className="btn btn-sm" disabled={page === 0} onClick={() => setPage(0)}>«</button>
            <button className="btn btn-sm" disabled={page === 0} onClick={() => setPage(page - 1)}>‹</button>
            {Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
              const start = Math.max(0, Math.min(page - 2, totalPages - 5));
              const n = start + i;
              if (n >= totalPages) return null;
              return (
                <button
                  key={n}
                  className={`btn btn-sm ${n === page ? "cur" : ""}`}
                  onClick={() => setPage(n)}
                >
                  {n + 1}
                </button>
              );
            })}
            <button className="btn btn-sm" disabled={page >= totalPages - 1} onClick={() => setPage(page + 1)}>›</button>
            <button className="btn btn-sm" disabled={page >= totalPages - 1} onClick={() => setPage(totalPages - 1)}>»</button>
          </div>
        </div>
      </div>

      {/* Notice grade données sensibles */}
      <div style={{ marginTop: 14, padding: "10px 14px", background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 8, fontSize: 12, color: "var(--text-dim)", display: "flex", alignItems: "center", gap: 8 }}>
        <Icon name={canSeeSensitive ? "check" : "lock"} />
        {canSeeSensitive ? (
          <span style={{ color: "var(--ok)" }}>Grade Discord avec accès complet aux adresses IP des joueurs.</span>
        ) : (
          <span>Protection des données : les adresses IP sont strictement filtrées et non transmises par l'API (rôle Discord requis).</span>
        )}
      </div>

      {/* Modal Fiche croisée joueur */}
      {identityModalLog && (
        <PlayerIdentityModal
          targetLog={identityModalLog}
          onClose={() => setIdentityModalLog(null)}
          onFilterPlayer={handleFilterPlayer}
          canSeeSensitive={canSeeSensitive}
          canSeeShop={perms.shopView}
          canSeeLogs={perms.logsView}
        />
      )}

      {/* Lightbox / Zoom SnapVault */}
      {lightbox && (
        <SnapVaultLightbox
          image={lightbox.url}
          title={lightbox.title}
          onClose={() => setLightbox(null)}
        />
      )}
    </>
  );
}
window.LogsPage = LogsPage;

// =================================================================
// SHOP PAGE - Logs boutique FiveM (Table scm_boutique_ig) connectés à la BDD MySQL
// =================================================================
const BOUTIQUE_TYPE_LABELS = {
  purchase: "Achat boutique",
  admin_give: "Give Staff",
  console_give: "Achat Boutique (Console)",
  lootbox: "Gain Lootbox",
};

function formatBoutiqueType(type, log) {
  if (log && log.is_console_give) return "Achat Boutique (Console)";
  if (!type) return "Transaction";
  const s = String(type).trim();
  if (BOUTIQUE_TYPE_LABELS[s]) return BOUTIQUE_TYPE_LABELS[s];
  const lower = s.toLowerCase();
  if (BOUTIQUE_TYPE_LABELS[lower]) return BOUTIQUE_TYPE_LABELS[lower];
  return s;
}

function getBoutiqueBadgeClass(type, log) {
  if (log && log.is_console_give) return "tag-ok";
  if (!type) return "tag-muted";
  const t = String(type).toLowerCase();
  if (t === "purchase" || t.includes("achat")) return "tag-violet";
  if (t === "admin_give" || t.includes("give") || t.includes("admin")) return "tag-indigo";
  if (t === "lootbox" || t.includes("box") || t.includes("caisse")) return "tag-fuchsia";
  return getTypeBadgeClass(type);
}

function ShopPage({ user, perms, serverName: propServerName, serverLogo: propServerLogo }) {
  const [logs, setLogs] = useState([]);
  const [total, setTotal] = useState(0);
  const [types, setTypes] = useState([]);
  const [stats, setStats] = useState({});
  const [tokenRate, setTokenRate] = useState(100);
  const [dbStatus, setDbStatus] = useState(null);
  const [fetchLoading, setFetchLoading] = useState(true);
  const [fetchError, setFetchError] = useState(null);
  const [canSeeEuro, setCanSeeEuro] = useState(perms.shopEuroEquiv || false);
  const [canSeeSensitive, setCanSeeSensitive] = useState(perms.logsSensitive || false);
  const [apiServerInfo, setApiServerInfo] = useState({ name: "", logo: "" });

  const [q, setQ] = useState("");
  const [qApplied, setQApplied] = useState("");
  const [qScopes, setQScopes] = useState(["all"]);
  const [qScopesApplied, setQScopesApplied] = useState(["all"]);
  const [selectedTypes, setSelectedTypes] = useState([]);
  const [range, setRange] = useState("all");
  const [open, setOpen] = useState(new Set());
  const [page, setPage] = useState(0);
  const [perPage, setPerPage] = useState(25);
  const [copiedKey, setCopiedKey] = useState(null);
  const [identityModalLog, setIdentityModalLog] = useState(null);

  const handleFilterPlayer = (searchStr, scope = "unique_id") => {
    setQ(searchStr);
    setQApplied(searchStr);
    setQScopes([scope]);
    setQScopesApplied([scope]);
    setPage(0);
  };

  const handleScopesChange = (nextScopes) => {
    setQScopes(nextScopes);
    if (qApplied || q.trim()) {
      setPage(0);
      setQApplied(q.trim());
      setQScopesApplied(nextScopes);
    }
  };

  const cleanTypes = useMemo(() => {
    return types.filter((t) => t && !t.toLowerCase().startsWith("table:") && !t.toLowerCase().startsWith("table :"));
  }, [types]);

  const fetchShopLogs = useCallback(async (params) => {
    setFetchLoading(true);
    setFetchError(null);
    try {
      const qParams = new URLSearchParams({
        page: params.page ?? 0,
        perPage: params.perPage ?? 25,
        type: params.selectedTypes && params.selectedTypes.length > 0 ? params.selectedTypes.join(",") : "all",
        q: params.qApplied ?? "",
        qFields: (params.qScopesApplied || ["all"]).join(","),
        range: params.range ?? "all",
      });
      const res = await fetch(`/api/shop?${qParams.toString()}`);
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        if (res.status === 403) throw new Error("Accès refusé : votre grade Discord ne permet pas de voir les logs boutique.");
        throw new Error(data.error || "Erreur de connexion à la base de données MySQL.");
      }
      setLogs(data.logs || []);
      setTotal(data.total || 0);
      setStats(data.stats || {});
      setTokenRate(data.tokenRate || 100);
      setCanSeeEuro(data.canSeeEuro || false);
      setCanSeeSensitive(data.canSeeSensitive || false);
      if (Array.isArray(data.types)) setTypes(data.types);
      if (data.dbStatus) setDbStatus(data.dbStatus);
      if (data.serverName) setApiServerInfo({ name: data.serverName, logo: data.serverLogo || "" });
    } catch (err) {
      setFetchError(err.message);
    } finally {
      setFetchLoading(false);
    }
  }, []);

  useEffect(() => {
    fetchShopLogs({ page, perPage, selectedTypes, qApplied, qScopesApplied, range });
  }, [page, perPage, selectedTypes, qApplied, qScopesApplied, range, fetchShopLogs]);

  const copyText = (text, key) => {
    if (!text) return;
    const cleanText = String(text).replace(/[\u2014\u2013]/g, "-");
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(cleanText);
    } else {
      const el = document.createElement("textarea");
      el.value = cleanText;
      document.body.appendChild(el);
      el.select();
      document.execCommand("copy");
      document.body.removeChild(el);
    }
    setCopiedKey(key);
    setTimeout(() => setCopiedKey((curr) => (curr === key ? null : curr)), 1800);
  };

  const currentServerName = propServerName || apiServerInfo.name || "Mon Serveur RP";
  const currentServerLogo = propServerLogo || apiServerInfo.logo || "";
  const totalPages = Math.max(1, Math.ceil(total / perPage));

  const fmtTok = (n) => (n || 0).toLocaleString("fr-FR");
  const fmtEur = (n) => (n || 0).toLocaleString("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + " €";

  const toggle = (id) => {
    const n = new Set(open);
    n.has(id) ? n.delete(id) : n.add(id);
    setOpen(n);
  };

  const handleSearchSubmit = () => {
    setPage(0);
    setQApplied(q.trim());
    setQScopesApplied([...qScopes]);
  };

  const handleReset = () => {
    setQ("");
    setQApplied("");
    setSelectedTypes([]);
    setRange("all");
    setQScopes(["all"]);
    setQScopesApplied(["all"]);
    setPage(0);
  };

  if (fetchError) {
    const isConnErr = fetchError.toLowerCase().includes("connexion") || fetchError.toLowerCase().includes("mysql") || fetchError.toLowerCase().includes("base de données") || fetchError.toLowerCase().includes("database");
    return (
      <div className="card" style={{ padding: "40px 30px", textAlign: "center", maxWidth: 640, margin: "24px auto" }}>
        <div style={{ width: 56, height: 56, borderRadius: "50%", background: "oklch(0.35 0.15 25 / 0.2)", border: "1px solid oklch(0.5 0.15 25 / 0.4)", display: "grid", placeItems: "center", margin: "0 auto 16px", color: "var(--danger)" }}>
          <Icon name={isConnErr ? "database" : "warn"} size={26} />
        </div>
        <h2 style={{ fontSize: 18, marginBottom: 8, color: "var(--text)" }}>
          {isConnErr ? "Erreur de connexion à la base de données" : "Accès refusé aux logs boutique"}
        </h2>
        <p style={{ color: "var(--text-dim)", fontSize: 13, lineHeight: 1.6, marginBottom: 20 }}>
          {fetchError}
        </p>

        {isConnErr && (
          <div style={{ background: "var(--bg-2)", border: "1px solid var(--border)", borderRadius: 8, padding: "14px 16px", textAlign: "left", fontSize: 12, marginBottom: 20 }}>
            <div style={{ fontWeight: 600, marginBottom: 6, color: "var(--warn)" }}>Configuration MySQL requise dans votre fichier .env :</div>
            <div className="mono" style={{ color: "var(--text-dim)", lineHeight: 1.7 }}>
              DB_HOST={dbStatus?.host || "localhost"} | DB_PORT={dbStatus?.port || 3306} | DB_NAME={dbStatus?.database || "QBCore"} | DB_TABLE_BOUTIQUE={dbStatus?.table || "scm_boutique_ig"}
            </div>
          </div>
        )}

        <button
          className="btn btn-primary"
          style={{ display: "inline-flex", alignItems: "center", gap: 8 }}
          onClick={() => fetchShopLogs({ page, perPage, selectedTypes, qApplied, range })}
        >
          <Icon name="refresh" size={14} />
          <span>Réessayer la connexion</span>
        </button>
      </div>
    );
  }

  return (
    <>
      {/* Bannière personnalisée du serveur avec état de la BDD */}
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "12px 18px", background: "var(--surface)", border: "1px solid var(--border)", borderRadius: "var(--r)", marginBottom: 14 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
          {currentServerLogo ? (
            <img src={currentServerLogo} alt="" style={{ width: 38, height: 38, borderRadius: 8, objectFit: "cover" }} />
          ) : (
            <div className="logo" style={{ width: 38, height: 38, borderRadius: 8, fontSize: 16, fontWeight: 700 }}>
              {currentServerName[0].toUpperCase()}
            </div>
          )}
          <div>
            <div style={{ fontSize: 15, fontWeight: 600 }}>{currentServerName}</div>
            <div style={{ fontSize: 11, color: "var(--muted)" }}>Logs de la boutique en jeu en direct</div>
          </div>
        </div>

        {/* Statut de la Base de Données */}
        <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
          <span
            className="dev-pill"
            style={{ color: "var(--ok)", borderColor: "oklch(0.5 0.14 155 / 0.4)", display: "inline-flex", alignItems: "center", gap: 6, textTransform: "none", fontSize: 11 }}
            title="Connecté à la base de données MySQL"
          >
            <Icon name="database" size={13} />
            <span>Connecté à la BDD</span>
          </span>
        </div>
      </div>

      {/* Cartes statistiques globales */}
      <div className="stats">
        <div className="stat">
          <div className="l">Jetons dépensés - Achats</div>
          <div className="v" style={{ color: "var(--accent)" }}>
            {fmtTok(stats.purchaseTokens)} <span style={{ fontSize: 13, color: "var(--muted)", fontWeight: 500 }}>jet.</span>
          </div>
          <div className="d">
            {canSeeEuro ? (
              <>≈ <strong style={{ color: "var(--accent)" }}>{fmtEur(stats.purchaseRevenue)}</strong> équivalent dépensé</>
            ) : (
              <span style={{ color: "var(--muted)" }}><Icon name="lock" size={11} /> Montant € masqué (grade requis)</span>
            )}
          </div>
        </div>

        <div className="stat">
          <div className="l">Jetons achetés - Boutique web</div>
          <div className="v" style={{ color: "var(--ok)" }}>
            {fmtTok(stats.consoleGiveTokens || 0)} <span style={{ fontSize: 13, color: "var(--muted)", fontWeight: 500 }}>jet.</span>
          </div>
          <div className="d">
            {canSeeEuro ? (
              <>≈ <strong style={{ color: "var(--ok)" }}>{fmtEur(stats.consoleGiveRevenue || 0)}</strong> encaissés (give console)</>
            ) : (
              <span style={{ color: "var(--muted)" }}><Icon name="lock" size={11} /> Montant € masqué (grade requis)</span>
            )}
          </div>
        </div>

        <div className="stat">
          <div className="l">Jetons offerts - Staff & Gains</div>
          <div className="v" style={{ color: "var(--info)" }}>
            {fmtTok((stats.staffGiveTokens != null ? stats.staffGiveTokens : stats.giveTokens || 0) + (stats.lootboxTokens || 0))} <span style={{ fontSize: 13, color: "var(--muted)", fontWeight: 500 }}>jet.</span>
          </div>
          <div className="d">
            Staff ({fmtTok(stats.staffGiveTokens != null ? stats.staffGiveTokens : stats.giveTokens || 0)}) & Lootbox ({fmtTok(stats.lootboxTokens || 0)})
          </div>
        </div>

        <div className="stat">
          <div className="l">Total transactions boutique</div>
          <div className="v">{fmtTok(stats.totalTransactions || total)}</div>
          <div className="d">
            Taux officiel : 1,00 € = {tokenRate} jetons
          </div>
        </div>
      </div>

      {/* Filtres de recherche */}
      <div className="filters">
        <SearchField
          value={q}
          onChange={setQ}
          applied={qApplied}
          onSubmit={handleSearchSubmit}
          loading={fetchLoading}
          scopes={qScopes}
          onScopesChange={handleScopesChange}
          appliedScopes={qScopesApplied}
          placeholder="Recherche selon les cibles sélectionnées... - Entrée pour rechercher"
        />
        <div className="field multiselect-field">
          <label>Type de transaction</label>
          <TypeMultiSelect
            types={cleanTypes}
            selected={selectedTypes}
            formatType={formatBoutiqueType}
            getBadgeClass={getBoutiqueBadgeClass}
            placeholder="Tous les types boutique"
            searchPlaceholder="Rechercher un type (ex: achat, give, lootbox...)"
            onChange={(newTypes) => {
              setSelectedTypes(newTypes);
              setPage(0);
            }}
          />
        </div>
        <div className="field">
          <label>Période</label>
          <select
            className="select"
            value={range}
            onChange={(e) => {
              setRange(e.target.value);
              setPage(0);
            }}
          >
            <option value="all">Tout l'historique</option>
            <option value="24h">Dernières 24 heures</option>
            <option value="7d">7 derniers jours</option>
            <option value="30d">30 derniers jours</option>
          </select>
        </div>
        <button className="btn" onClick={handleReset}>
          Réinitialiser
        </button>
      </div>

      {/* Tableau des logs boutique */}
      <div className="tbl-wrap">
        {fetchLoading && <LoadingOverlay label="Chargement des logs boutique…" />}
        <table className="tbl">
          <thead>
            <tr>
              <th style={{ width: 32 }}></th>
              <th style={{ width: 165 }}>Date & Heure</th>
              <th style={{ width: 135 }}>Type</th>
              <th style={{ width: 220 }}>Joueur</th>
              <th>Article / Détail</th>
              <th style={{ width: 130 }}>Jetons</th>
              <th style={{ width: 120 }}>
                {canSeeEuro ? "Équiv. €" : <span><Icon name="lock" size={11} style={{ marginRight: 4 }} />Équiv. €</span>}
              </th>
              <th style={{ width: 180 }}>Discord</th>
            </tr>
          </thead>
          <tbody>
            {logs.length === 0 && !fetchLoading && (
              <tr><td colSpan={8}><div className="empty">Aucune transaction boutique ne correspond aux critères.</div></td></tr>
            )}
            {logs.map((l) => {
              const isOpen = open.has(l.id);
              const displayType = formatBoutiqueType(l.type, l);
              const tagClass = getBoutiqueBadgeClass(l.type, l);
              const playerName = l.pseudo_rp || l.pseudo_fivem || (l.citizenid ? `Citoyen #${l.citizenid}` : "Joueur");
              const isPurchase = l.type === "purchase";
              const isConsoleGive = l.is_console_give;
              const isAdminGive = (l.type === "admin_give" || l.type === "give") && !isConsoleGive;
              const isLootbox = l.type === "lootbox";

              return (
                <React.Fragment key={l.id}>
                  <tr className={`row ${isOpen ? "open" : ""}`} onClick={() => toggle(l.id)}>
                    <td><span className={`caret ${isOpen ? "open" : ""}`}><Icon name="chevR" /></span></td>
                    <td className="mono" style={{ fontSize: 12, color: "var(--text-dim)", whiteSpace: "nowrap" }}>
                      {fmtDate(l.created_at || l.timestamp)}
                    </td>
                    <td><span className={`tag ${tagClass}`}>{displayType}</span></td>
                    <td>
                      <div>
                        <div style={{ fontWeight: 600, color: "var(--text)" }}>{playerName}</div>
                        <div style={{ fontSize: 11, color: "var(--muted)", display: "flex", alignItems: "center", gap: 6 }}>
                          <span>ID #{l.scm_id !== null && l.scm_id !== undefined ? l.scm_id : "-"}</span>
                          {l.citizenid && <span>• CID: {l.citizenid}</span>}
                        </div>
                      </div>
                    </td>
                    <td>
                      {isPurchase ? (
                        <div style={{ fontWeight: 500, color: "var(--text)" }}>
                          {l.item_name || l.item_id || "Article boutique"}
                          {l.item_qty > 1 && <span className="tag tag-muted" style={{ fontSize: 10, marginLeft: 6 }}>x{l.item_qty}</span>}
                        </div>
                      ) : isConsoleGive ? (
                        <div style={{ color: "var(--ok)", fontWeight: 500 }}>
                          Achat Boutique Web (Give Console)
                          {l.note && <span style={{ color: "var(--muted)", fontSize: 11, marginLeft: 6 }}>({l.note})</span>}
                        </div>
                      ) : isAdminGive ? (
                        <div style={{ color: "var(--text-dim)" }}>
                          Give par <strong>{l.admin_pseudo_rp || "Staff"}</strong>
                          {l.note && <span style={{ color: "var(--muted)", fontSize: 11, marginLeft: 6 }}>({l.note})</span>}
                        </div>
                      ) : (
                        <div style={{ color: "var(--text-dim)" }}>
                          {l.item_name || "Gain Lootbox"}
                          {l.note && <span style={{ color: "var(--muted)", fontSize: 11, marginLeft: 6 }}>({l.note})</span>}
                        </div>
                      )}
                    </td>
                    <td className="mono" style={{ whiteSpace: "nowrap" }}>
                      {isPurchase ? (
                        <span style={{ color: "var(--accent)", fontWeight: 600 }}>
                          - {fmtTok(l.amount || Math.abs(l.tokens_delta))} jet.
                        </span>
                      ) : isConsoleGive ? (
                        <span style={{ color: "var(--ok)", fontWeight: 600 }}>
                          + {fmtTok(l.tokens_delta || l.amount)} jet.
                        </span>
                      ) : isAdminGive ? (
                        <span style={{ color: "var(--info)", fontWeight: 600 }}>
                          + {fmtTok(l.tokens_delta || l.amount)} jet.
                        </span>
                      ) : (
                        <span style={{ color: "oklch(0.84 0.18 335)", fontWeight: 600 }}>
                          + {fmtTok(l.amount || l.tokens_delta)} jet.
                        </span>
                      )}
                    </td>
                    <td className="mono" style={{ fontSize: 12, whiteSpace: "nowrap" }}>
                      {canSeeEuro ? (
                        l.priceEur !== null && l.priceEur !== undefined ? (
                          <span style={{ color: isPurchase ? "var(--accent)" : "var(--ok)", fontWeight: 600 }}>
                            {isPurchase ? "- " : "+ "}{fmtEur(l.priceEur)}
                          </span>
                        ) : (
                          <span style={{ color: "var(--muted)" }}>-</span>
                        )
                      ) : (
                        <span className="masked" title="Grade Discord insuffisant pour voir l'équivalent en euros">🔒 Masqué</span>
                      )}
                    </td>
                    <td className="mono" style={{ fontSize: 12, color: "var(--text-dim)" }}>
                      {l.discord ? (
                        <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
                          <Icon name="discord" size={13} style={{ color: "#5865F2", flexShrink: 0 }} />
                          <span>{l.discord}</span>
                        </span>
                      ) : (
                        "-"
                      )}
                    </td>
                  </tr>

                  {/* Tiroir de détails techniques dépliant */}
                  {isOpen && (
                    <tr className="detail">
                      <td colSpan={8}>
                        <div className="detail-header">
                          <div style={{ display: "flex", alignItems: "center", gap: 8, fontWeight: 600 }}>
                            <Icon name="cart" size={14} style={{ color: "var(--accent)" }} />
                            <span>Détails de la transaction boutique</span>
                            <span className="tag tag-muted" style={{ fontSize: 10 }}>#{l.id}</span>
                          </div>
                          <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
                            <button
                              className="btn btn-sm btn-primary"
                              style={{
                                display: "inline-flex",
                                alignItems: "center",
                                gap: 7,
                                fontWeight: 600,
                              }}
                              onClick={(e) => {
                                e.stopPropagation();
                                setIdentityModalLog(l);
                              }}
                              title="Ouvrir la fiche croisée consolidée de ce joueur"
                            >
                              <Icon name="link" size={12} />
                              <span>Fiche croisée</span>
                            </button>
                            {(l.item_name || l.item_id) && (
                              <button
                                className="btn btn-sm"
                                style={{
                                  display: "inline-flex",
                                  alignItems: "center",
                                  gap: 7,
                                  color: copiedKey === `item-${l.id}` ? "var(--ok)" : undefined,
                                  borderColor: copiedKey === `item-${l.id}` ? "var(--ok)" : undefined,
                                  background: copiedKey === `item-${l.id}` ? "oklch(0.35 0.14 155 / 0.15)" : undefined,
                                  transition: "all 0.15s ease",
                                }}
                                onClick={(e) => { e.stopPropagation(); copyText(l.item_name || l.item_id, `item-${l.id}`); }}
                                title="Copier le nom de l'article"
                              >
                                <Icon name={copiedKey === `item-${l.id}` ? "check" : "copy"} size={12} />
                                <span>{copiedKey === `item-${l.id}` ? "Article copié !" : (l.item_name || l.item_id)}</span>
                              </button>
                            )}
                            {l.citizenid && (
                              <button
                                className="btn btn-sm"
                                style={{
                                  display: "inline-flex",
                                  alignItems: "center",
                                  gap: 7,
                                  color: copiedKey === `cid-btn-${l.id}` ? "var(--ok)" : undefined,
                                  borderColor: copiedKey === `cid-btn-${l.id}` ? "var(--ok)" : undefined,
                                  background: copiedKey === `cid-btn-${l.id}` ? "oklch(0.35 0.14 155 / 0.15)" : undefined,
                                  transition: "all 0.15s ease",
                                }}
                                onClick={(e) => { e.stopPropagation(); copyText(l.citizenid, `cid-btn-${l.id}`); }}
                                title="Copier Citizen ID"
                              >
                                <Icon name={copiedKey === `cid-btn-${l.id}` ? "check" : "copy"} size={12} />
                                <span>{copiedKey === `cid-btn-${l.id}` ? "Citizen ID copié !" : `CID: ${l.citizenid}`}</span>
                              </button>
                            )}
                          </div>
                        </div>

                        <div className="detail-grid">
                          <div className="kv"><div className="k">ID transaction</div><div className="v mono">#{l.id}</div></div>
                          <div className="kv">
                            <div className="k">Type d'opération</div>
                            <div className="v" style={{ display: "flex", alignItems: "center", gap: 6 }}>
                              <span className={`tag ${tagClass}`}>{displayType}</span>
                              <span className="mono" style={{ fontSize: 11, color: "var(--muted)" }}>({l.type})</span>
                            </div>
                          </div>
                          <div className="kv"><div className="k">Date & Heure</div><div className="v mono">{fmtDate(l.created_at || l.timestamp)}</div></div>
                          <div className="kv">
                            <div className="k">ID joueur (scm_id)</div>
                            <div className="v mono">
                              {l.scm_id !== null && l.scm_id !== undefined ? (
                                `#${l.scm_id}`
                              ) : l.inferred?.scm_id ? (
                                <InferredValue inferred={{ value: `#${l.inferred.scm_id.value}`, source: l.inferred.scm_id.source, confidence: l.inferred.scm_id.confidence }} copyKey={`inf-scm-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier SCM ID déduit" />
                              ) : (
                                "-"
                              )}
                            </div>
                          </div>
                          <div className="kv">
                            <div className="k">Pseudo RP</div>
                            <div className="v">
                              {l.pseudo_rp ? (
                                <strong>{l.pseudo_rp}</strong>
                              ) : l.inferred?.pseudo_rp ? (
                                <InferredValue inferred={l.inferred.pseudo_rp} copyKey={`inf-rp-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Pseudo RP déduit" />
                              ) : (
                                "-"
                              )}
                            </div>
                          </div>
                          <div className="kv">
                            <div className="k">Pseudo FiveM</div>
                            <div className="v mono">
                              {l.pseudo_fivem ? (
                                l.pseudo_fivem
                              ) : l.inferred?.pseudo_fivem ? (
                                <InferredValue inferred={l.inferred.pseudo_fivem} copyKey={`inf-pfm-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Pseudo FiveM déduit" />
                              ) : (
                                "-"
                              )}
                            </div>
                          </div>
                          <div className="kv">
                            <div className="k">Identifiant Citoyen</div>
                            <div className="v mono">
                              {l.citizenid ? (
                                <CopyableValue value={l.citizenid} copyKey={`cid-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Citizen ID" />
                              ) : l.inferred?.citizenid ? (
                                <InferredValue inferred={l.inferred.citizenid} copyKey={`inf-cid-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Citizen ID déduit" />
                              ) : (
                                "-"
                              )}
                            </div>
                          </div>
                          <div className="kv">
                            <div className="k">Identifiant Discord</div>
                            <div className="v mono" style={{ display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
                              {l.discord ? (
                                <>
                                  <Icon name="discord" size={13} style={{ color: "#5865F2", flexShrink: 0 }} />
                                  <CopyableValue value={l.discord} copyKey={`disc-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Discord ID" />
                                </>
                              ) : l.inferred?.discord ? (
                                <InferredValue inferred={l.inferred.discord} copyKey={`disc-inf-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Discord ID déduit" icon="discord" />
                              ) : (
                                "-"
                              )}
                            </div>
                          </div>
                          <div className="kv">
                            <div className="k">Licence Rockstar</div>
                            <div className="v mono">
                              {l.license ? (
                                <CopyableValue value={l.license} copyKey={`lic-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Licence Rockstar" />
                              ) : l.inferred?.license ? (
                                <InferredValue inferred={l.inferred.license} copyKey={`inf-lic-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Licence déduite" />
                              ) : (
                                "-"
                              )}
                            </div>
                          </div>
                          <div className="kv">
                            <div className="k">Adresse IP</div>
                            <div className="v mono">
                              {canSeeSensitive && l.ip ? (
                                <CopyableValue value={l.ip} copyKey={`ip-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Adresse IP" />
                              ) : l.inferred?.ip ? (
                                <InferredValue inferred={l.inferred.ip} copyKey={`inf-ip-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier IP déduite" />
                              ) : (
                                <span className="masked" title="Grade Discord insuffisant pour afficher l'IP (strippée côté serveur)">
                                  [Masqué - Rôle requis]
                                </span>
                              )}
                            </div>
                          </div>
                          <div className="kv"><div className="k">Article / Objet</div><div className="v">{l.item_name || "-"}</div></div>
                          <div className="kv"><div className="k">Identifiant Article</div><div className="v mono">{l.item_id || "-"}</div></div>
                          <div className="kv"><div className="k">Quantité</div><div className="v mono">{l.item_qty || 1}</div></div>
                          <div className="kv"><div className="k">Devise utilisée</div><div className="v mono">{l.currency || "-"}</div></div>
                          <div className="kv">
                            <div className="k">Montant</div>
                            <div className="v mono" style={{ fontWeight: 600 }}>
                              {l.amount !== null && l.amount !== undefined ? `${fmtTok(l.amount)} jetons` : "-"}
                            </div>
                          </div>
                          <div className="kv">
                            <div className="k">
                              {isPurchase ? "Équivalent dépensé (€)" : isConsoleGive ? "Montant encaissé (€)" : "Équivalent en €"}
                            </div>
                            <div className="v mono">
                              {canSeeEuro ? (
                                l.priceEur !== null && l.priceEur !== undefined ? (
                                  <strong style={{ color: isPurchase ? "var(--accent)" : "var(--ok)" }}>
                                    {isPurchase ? "- " : "+ "}{fmtEur(l.priceEur)}
                                  </strong>
                                ) : "-"
                              ) : (
                                <span className="masked">🔒 Masqué (grade Discord requis)</span>
                              )}
                            </div>
                          </div>
                          <div className="kv"><div className="k">Solde jetons avant</div><div className="v mono">{l.tokens_before !== null && l.tokens_before !== undefined ? `${fmtTok(l.tokens_before)} jet.` : "-"}</div></div>
                          <div className="kv"><div className="k">Solde jetons après</div><div className="v mono" style={{ fontWeight: 600, color: "var(--accent)" }}>{l.tokens_after !== null && l.tokens_after !== undefined ? `${fmtTok(l.tokens_after)} jet.` : "-"}</div></div>

                          {(l.admin_pseudo_rp || l.admin_scm_id || l.admin_license || isConsoleGive) && (
                            <>
                              <div className="kv">
                                <div className="k">Émetteur du Give</div>
                                <div className="v">
                                  <strong>{isConsoleGive ? "Console Serveur (Achat Boutique Web)" : (l.admin_pseudo_rp || "Staff")}</strong>
                                </div>
                              </div>
                              <div className="kv">
                                <div className="k">ID FiveM émetteur</div>
                                <div className="v mono">
                                  {isConsoleGive ? "Console (RCON/Tebex)" : (l.admin_scm_id !== null && l.admin_scm_id !== undefined ? `#${l.admin_scm_id}` : "-")}
                                </div>
                              </div>
                              <div className="kv" style={{ gridColumn: "1 / -1" }}>
                                <div className="k">Licence émetteur</div>
                                <div className="v mono">
                                  {isConsoleGive ? (
                                    <span style={{ color: "var(--muted)" }}>Console (aucune licence joueur)</span>
                                  ) : (
                                    <CopyableValue value={l.admin_license} copyKey={`stf-lic-${l.id}`} copiedKey={copiedKey} onCopy={copyText} title="Copier Licence Staff" />
                                  )}
                                </div>
                              </div>
                            </>
                          )}

                          <div className="kv" style={{ gridColumn: "1 / -1" }}>
                            <div className="k">Note / Justification</div>
                            <div className="v" style={{ background: "var(--surface)", padding: "10px 14px", borderRadius: 6, border: "1px solid var(--border)", fontFamily: "inherit", whiteSpace: "pre-wrap", lineHeight: 1.5 }}>
                              {l.note ? l.note.replace(/[\u2014\u2013]/g, "-") : "Aucune note enregistrée pour cette transaction."}
                            </div>
                          </div>
                        </div>
                      </td>
                    </tr>
                  )}
                </React.Fragment>
              );
            })}
          </tbody>
        </table>

        {/* Pagination */}
        <div className="pagination">
          <PerPageSelect value={perPage} onChange={(val) => { setPerPage(val); setPage(0); }} />
          <div>{total.toLocaleString("fr-FR")} transaction{total > 1 ? "s" : ""} boutique - page {page + 1}/{totalPages}</div>
          <div className="pages">
            <button className="btn btn-sm" disabled={page === 0} onClick={() => setPage(0)}>«</button>
            <button className="btn btn-sm" disabled={page === 0} onClick={() => setPage(page - 1)}>‹</button>
            {Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
              const start = Math.max(0, Math.min(page - 2, totalPages - 5));
              const n = start + i;
              if (n >= totalPages) return null;
              return (
                <button
                  key={n}
                  className={`btn btn-sm ${n === page ? "cur" : ""}`}
                  onClick={() => setPage(n)}
                >
                  {n + 1}
                </button>
              );
            })}
            <button className="btn btn-sm" disabled={page >= totalPages - 1} onClick={() => setPage(page + 1)}>›</button>
            <button className="btn btn-sm" disabled={page >= totalPages - 1} onClick={() => setPage(totalPages - 1)}>»</button>
          </div>
        </div>
      </div>

      {/* Notice sur les permissions boutique */}
      <div style={{ marginTop: 14, padding: "10px 14px", background: "var(--surface)", border: "1px solid var(--border)", borderRadius: 8, fontSize: 12, color: "var(--text-dim)", display: "flex", alignItems: "center", gap: 8 }}>
        <Icon name={canSeeEuro ? "check" : "lock"} />
        {canSeeEuro ? (
          <span style={{ color: "var(--ok)" }}>Grade Discord avec accès complet aux données financières et conversions en euros.</span>
        ) : (
          <span>L'équivalent en euros est masqué : votre rôle Discord ne possède pas le grade financier requis (ROLE_SHOP_EURO_EQUIV).</span>
        )}
      </div>

      {/* Modal Fiche croisée joueur */}
      {identityModalLog && (
        <PlayerIdentityModal
          targetLog={identityModalLog}
          onClose={() => setIdentityModalLog(null)}
          onFilterPlayer={handleFilterPlayer}
          canSeeSensitive={canSeeSensitive}
          canSeeShop={perms.shopView}
          canSeeLogs={perms.logsView}
        />
      )}
    </>
  );
}
window.ShopPage = ShopPage;
