// Rôle : fournit l'outil de planification d'équipements, sertissages, résumé et échange texte.
import { useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { Icon } from "../../../components/Icon.jsx";
import { Tooltip } from "../../../components/Tooltip.jsx";
import { applyGroupedReorderOperation, moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
import { TextExchangeActions } from "./TextExchangeActions.jsx";

const SOCKET_SHAPES = ["ball", "jewel"];
const SOCKET_COLORS = ["red", "orange", "amber", "yellow", "lime", "green", "teal", "cyan", "blue", "indigo", "violet", "purple", "pink", "rose", "white", "gray", "black"];
const SOCKET_COLOR_VALUES = {
  red: "#ef4444",
  orange: "#f97316",
  amber: "#f59e0b",
  yellow: "var(--color-accent-gold)",
  lime: "#84cc16",
  green: "#22c55e",
  teal: "#14b8a6",
  cyan: "var(--color-accent-cyan)",
  blue: "#3b82f6",
  indigo: "var(--color-accent-indigo)",
  violet: "#7c3aed",
  purple: "var(--color-primary)",
  pink: "var(--color-accent-pink)",
  rose: "#f43f5e",
  white: "#f8fafc",
  gray: "#64748b",
  black: "#020617"
};
const EQUIPMENT_ICONS = [
  { icon: "helmet", label: "Casque" },
  { icon: "chest-armor", label: "Chest" },
  { icon: "glove", label: "Gants" },
  { icon: "boot", label: "Bottes" },
  { icon: "necklace", label: "Collier" },
  { icon: "earring", label: "Earring" },
  { icon: "ring", label: "Anneau" },
  { icon: "sword", label: "Épée" },
  { icon: "gun", label: "Gun" },
  { icon: "shield", label: "Bouclier" }
];
const TRAIT_ICONS = ["sword", "shield"];
const DETAIL_TABS = ["stats", "sockets", "craft"];

function getEquipmentIconLabel(icon) {
  return EQUIPMENT_ICONS.find((item) => item.icon === icon)?.label || "Équipement";
}

function getCharacteristicCategories(equipment) {
  const categories = equipment.characteristics.map((trait) => trait.category).filter(Boolean);
  return [
    ...equipment.categoryOrder.filter((category) => categories.includes(category)),
    ...categories.filter((category, index) => categories.indexOf(category) === index && !equipment.categoryOrder.includes(category))
  ];
}

function setItemCategory(item, category) {
  const nextItem = { ...item };
  if (category) nextItem.category = category;
  else delete nextItem.category;
  return nextItem;
}

function formatValue(value) {
  if (typeof value === "number") return Number.isInteger(value) ? String(value) : String(Math.round(value * 1000) / 1000);
  return String(value || "");
}

function createLocalId(prefix) {
  return globalThis.crypto?.randomUUID?.() || `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}

function getSocketItemName(equipment, socketItemId) {
  return equipment.socketItems.find((socketItem) => socketItem.id === socketItemId)?.name || "";
}

function SocketShapeIcon({ shape, color = "gray", className = "" }) {
  return (
    <span className={`equipment-planner-socket-icon color-${color} ${className}`.trim()} style={{ "--equipment-socket-icon-color": SOCKET_COLOR_VALUES[color] || SOCKET_COLOR_VALUES.gray }} aria-hidden="true">
      <Icon name={shape} />
    </span>
  );
}

function SummaryTotalIcon({ total }) {
  if (SOCKET_SHAPES.includes(total.icon)) return <SocketShapeIcon shape={total.icon} color={total.color || "gray"} />;
  return <Icon name={total.icon || "sword"} />;
}

export function EquipmentPlannerModule({ toolboxId, moduleId, context, editing }) {
  const data = context.normalizeEquipmentPlannerData(context.getModuleData(toolboxId, moduleId, { types: [], equipments: [] }));
  const summary = useMemo(() => context.summarizeEquipmentPlannerData(data), [context, data]);
  const textContent = context.moduleText?.equipmentPlanner || {};
  const [equipmentIcon, setEquipmentIcon] = useState("helmet");
  const [equipmentName, setEquipmentName] = useState("");
  const {
    itemReorder,
    groupReorder,
    getItemProps,
    getGroupProps,
    getGroupBoundaryProps,
    isItemDragging,
    isItemDropTarget,
    isGroupDragging,
    isGroupDropTarget,
    isBoundaryDropTarget,
    getDropPlacement,
    shouldShowGroupBoundaries
  } = useGroupedReorder({
    namespace: `equipment-planner-${moduleId}`,
    items: data.equipments,
    getItemId: (equipment) => equipment.id,
    onItemMove: (operation) => save({ ...data, equipments: moveItem(data.equipments, operation.sourceId, operation.targetId, operation.placement) })
  });
  const reorder = {
    itemReorder,
    groupReorder,
    getItemProps,
    getGroupProps,
    getGroupBoundaryProps,
    isItemDragging,
    isItemDropTarget,
    isGroupDragging,
    isGroupDropTarget,
    isBoundaryDropTarget,
    getDropPlacement,
    shouldShowGroupBoundaries
  };

  function save(nextData) {
    context.setModuleData(toolboxId, moduleId, nextData, "equipmentPlanner");
  }

  function addEquipment(event) {
    event.preventDefault();
    const cleanName = equipmentName.trim();
    if (!cleanName) return;
    const type = data.types[0] || { id: context.uid("equipmentType"), title: "Équipements", icon: "shield", collapsed: false };
    const nextData = {
      ...data,
      types: data.types.length ? data.types : [type],
      typeOrder: data.typeOrder.length ? data.typeOrder : [type.id],
      equipments: [
        ...data.equipments,
        {
          id: context.uid("equipment"),
          typeId: type.id,
          name: cleanName,
          icon: equipmentIcon,
          active: true,
          obtain: "",
          characteristics: [],
          socketItems: [],
          socketLinks: [],
          materials: [],
          categoryOrder: [],
          collapsedCategories: []
        }
      ]
    };
    save(nextData);
    setEquipmentName("");
  }

  function updateEquipment(equipmentId, updater) {
    save({ ...data, equipments: data.equipments.map((equipment) => equipment.id === equipmentId ? updater(equipment) : equipment) });
  }

  function deleteEquipment(equipmentId) {
    const nextEquipments = data.equipments.filter((equipment) => equipment.id !== equipmentId);
    const usedTypeIds = new Set(nextEquipments.map((equipment) => equipment.typeId));
    save({
      ...data,
      equipments: nextEquipments,
      types: data.types.filter((type) => usedTypeIds.has(type.id)),
      typeOrder: data.typeOrder.filter((typeId) => usedTypeIds.has(typeId))
    });
  }

  return (
    <div className="equipment-planner-module">
      {editing && (
        <div className="module-add-panel">
          <form className="inline-form equipment-planner-add-form" onSubmit={addEquipment}>
            <IconChoiceDropdown
              className="equipment-planner-equipment-icon-picker"
              label={textContent.equipmentIconLabel || "Type"}
              value={equipmentIcon}
              options={EQUIPMENT_ICONS}
              onChange={setEquipmentIcon}
            />
            <input value={equipmentName} onChange={(event) => setEquipmentName(event.target.value)} placeholder={textContent.equipmentPlaceholder || "Nouvel équipement"} />
            <button className="primary">{textContent.addEquipmentButton || "Ajouter"}</button>
          </form>
          <TextExchangeActions type="equipmentPlanner" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
        </div>
      )}

      <EquipmentPlannerSummary summary={summary} textContent={textContent} />

      <ul className="equipment-planner-list">
        {data.equipments.length ? data.equipments.map((equipment) => (
          <EquipmentItem
            key={equipment.id}
            equipment={equipment}
            reorder={reorder}
            textContent={textContent}
            onUpdate={(updater) => updateEquipment(equipment.id, updater)}
            onDelete={() => deleteEquipment(equipment.id)}
          />
        )) : (
          <li className="equipment-planner-empty">{textContent.emptyEquipments || "Aucun équipement planifié."}</li>
        )}
      </ul>
    </div>
  );
}

function EquipmentPlannerSummary({ summary, textContent }) {
  const hasSourceTotals = summary.equipmentTotals.length || summary.socketTotals.length;
  const sourceColumnCount = [summary.equipmentTotals.length, summary.socketTotals.length].filter(Boolean).length;
  return (
    <section className="equipment-planner-summary" aria-label={textContent.summaryTitle || "Résumé"}>
      <div className="equipment-planner-summary-header">
        <h3>{textContent.summaryTitle || "Résumé"}</h3>
        <span>{summary.activeEquipmentCount} {summary.activeEquipmentCount > 1 ? textContent.activeEquipmentsPlural || "équipements actifs" : textContent.activeEquipmentsSingular || "équipement actif"}</span>
      </div>
      <div className="equipment-planner-summary-grid">
        {hasSourceTotals ? (
          <div className={`equipment-planner-summary-source-grid columns-${sourceColumnCount}`}>
            {summary.equipmentTotals.length ? <SummaryTotalsPanel title={textContent.equipmentTotalsTitle || "Statistiques de l'équipement"} totals={summary.equipmentTotals} textContent={textContent} /> : null}
            {summary.socketTotals.length ? <SummaryTotalsPanel title={textContent.socketTotalsTitle || "Objets sertis"} totals={summary.socketTotals} textContent={textContent} /> : null}
          </div>
        ) : null}
        {summary.mixedTotals.length ? <SummaryTotalsPanel title={textContent.mixedTotalsTitle || "Totaux fusionnés"} totals={summary.mixedTotals} textContent={textContent} /> : null}
        {!summary.totals.length ? (
          <div className="equipment-planner-summary-panel">
            <strong>{textContent.numericTotalsTitle || "Totaux"}</strong>
            <p>{textContent.emptyTotals || "Aucun total numérique."}</p>
          </div>
        ) : null}
        {summary.textBonuses.length ? (
          <div className="equipment-planner-summary-panel equipment-planner-summary-text-bonuses">
            <strong>{textContent.textBonusesTitle || "Bonus"}</strong>
            <ul>
              {summary.textBonuses.map((bonus) => (
                <li key={`${bonus.source}:${bonus.id}`}>
                  <span>{bonus.category ? `${bonus.category} · ` : ""}{bonus.name}</span>
                  <small>{bonus.source}{bonus.value ? ` · ${bonus.value}` : ""}</small>
                </li>
              ))}
            </ul>
          </div>
        ) : null}
      </div>
    </section>
  );
}

function SummaryTotalsPanel({ title, totals, textContent }) {
  return (
    <div className="equipment-planner-summary-panel">
      <strong>{title}</strong>
      <ul>
        {totals.map((total) => (
          <Tooltip
            as="li"
            className="equipment-planner-summary-total"
            content={<SummaryTotalTooltip total={total} textContent={textContent} />}
            key={total.name}
            position="top-start"
          >
            <SummaryTotalIcon total={total} />
            <span>{total.name}</span>
            <b>{formatValue(total.value)}</b>
          </Tooltip>
        ))}
      </ul>
    </div>
  );
}

function SummaryTotalTooltip({ total, textContent }) {
  return (
    <span className="equipment-planner-summary-tooltip">
      <strong>{textContent.summaryTooltipTitle || "Provenance"}</strong>
      <span>
        {(total.contributions || []).map((contribution) => (
          <span className="equipment-planner-summary-tooltip-row" key={`${contribution.sourceKind}:${contribution.id}:${contribution.sourceName}`}>
            <Icon name={contribution.equipmentIcon || "shield"} />
            <span>{contribution.sourceName || contribution.equipmentName}</span>
            <b>{formatValue(contribution.value)}</b>
          </span>
        ))}
      </span>
    </span>
  );
}

function IconChoiceDropdown({ className = "", label, value, options, onChange }) {
  const selected = options.find((option) => option.icon === value) || options[0];
  return (
    <FloatingPicker
      className={className}
      label={label}
      columns={Math.min(options.length, 5)}
      renderTrigger={({ open, toggle }) => (
        <button
          className={`equipment-planner-picker-trigger ${open ? "active" : ""}`}
          type="button"
          onClick={toggle}
          aria-label={label}
          aria-expanded={open}
          title={selected.label}
        >
          <Icon name={selected.icon} />
          <Icon name="dropdown" />
        </button>
      )}
    >
      {({ close }) => (
        <div className="equipment-planner-picker-grid">
          {options.map((option) => (
            <button
              key={option.icon}
              className={`equipment-planner-picker-option ${option.icon === value ? "active" : ""}`}
              type="button"
              onClick={() => {
                onChange(option.icon);
                close();
              }}
              aria-label={option.label}
              title={option.label}
            >
              <Icon name={option.icon} />
            </button>
          ))}
        </div>
      )}
    </FloatingPicker>
  );
}

function ShapeChoiceDropdown({ label, value, onChange }) {
  return (
    <FloatingPicker
      className="equipment-planner-shape-picker"
      label={label}
      columns={SOCKET_SHAPES.length}
      renderTrigger={({ open, toggle }) => (
        <button className={`equipment-planner-picker-trigger ${open ? "active" : ""}`} type="button" onClick={toggle} aria-label={label} aria-expanded={open} title={label}>
          <SocketShapeIcon shape={value} />
          <Icon name="dropdown" />
        </button>
      )}
    >
      {({ close }) => (
        <div className="equipment-planner-picker-grid">
          {SOCKET_SHAPES.map((shape) => (
            <button
              key={shape}
              className={`equipment-planner-picker-option ${shape === value ? "active" : ""}`}
              type="button"
              onClick={() => {
                onChange(shape);
                close();
              }}
              aria-label={shape}
              title={shape}
            >
              <SocketShapeIcon shape={shape} />
            </button>
          ))}
        </div>
      )}
    </FloatingPicker>
  );
}

function ColorChoiceDropdown({ label, value, onChange }) {
  return (
    <FloatingPicker
      label={label}
      columns={6}
      renderTrigger={({ open, toggle }) => (
        <button className={`equipment-planner-picker-trigger ${open ? "active" : ""}`} type="button" onClick={toggle} aria-label={label} aria-expanded={open} title={value}>
          <span className={`equipment-planner-color-swatch color-${value}`} aria-hidden="true" />
          <Icon name="dropdown" />
        </button>
      )}
    >
      {({ close }) => (
        <div className="equipment-planner-color-grid">
          {SOCKET_COLORS.map((color) => (
            <button
              key={color}
              className={`equipment-planner-color-option ${color === value ? "active" : ""}`}
              type="button"
              onClick={() => {
                onChange(color);
                close();
              }}
              aria-label={color}
              title={color}
            >
              <span className={`equipment-planner-color-swatch color-${color}`} aria-hidden="true" />
            </button>
          ))}
        </div>
      )}
    </FloatingPicker>
  );
}

function FloatingPicker({ className = "", label, columns = 5, renderTrigger, children }) {
  const [open, setOpen] = useState(false);
  const [menuStyle, setMenuStyle] = useState({});
  const rootRef = useRef(null);
  const menuRef = useRef(null);

  useEffect(() => {
    if (!open) return undefined;

    function placeMenu() {
      const trigger = rootRef.current;
      if (!trigger) return;
      const rect = trigger.getBoundingClientRect();
      const width = Math.max(0, Math.min(220, window.innerWidth - 16));
      const left = Math.min(Math.max(8, rect.left), Math.max(8, window.innerWidth - width - 8));
      setMenuStyle({
        top: `${Math.min(rect.bottom + 6, window.innerHeight - 8)}px`,
        left: `${left}px`,
        maxWidth: `${width}px`
      });
    }

    function handlePointerDown(event) {
      if (rootRef.current?.contains(event.target) || menuRef.current?.contains(event.target)) return;
      setOpen(false);
    }

    function handleKeyDown(event) {
      if (event.key === "Escape") setOpen(false);
    }

    placeMenu();
    window.addEventListener("resize", placeMenu);
    window.addEventListener("scroll", placeMenu, true);
    document.addEventListener("pointerdown", handlePointerDown);
    document.addEventListener("keydown", handleKeyDown);
    return () => {
      window.removeEventListener("resize", placeMenu);
      window.removeEventListener("scroll", placeMenu, true);
      document.removeEventListener("pointerdown", handlePointerDown);
      document.removeEventListener("keydown", handleKeyDown);
    };
  }, [open]);

  function close() {
    setOpen(false);
  }

  function toggle() {
    setOpen((value) => !value);
  }

  return (
    <div className={`equipment-planner-picker ${className} ${open ? "is-open" : ""}`.trim()} ref={rootRef} aria-label={label}>
      {renderTrigger({ open, toggle, close })}
      {open && createPortal(
        <div className="equipment-planner-picker-menu is-open" ref={menuRef} style={{ ...menuStyle, "--picker-columns": columns }} aria-label={label}>
          {children({ close })}
        </div>,
        document.body
      )}
    </div>
  );
}

function EquipmentItem({ equipment, reorder, textContent, onUpdate, onDelete }) {
  const [activeTab, setActiveTab] = useState("stats");
  const [detailEditing, setDetailEditing] = useState(false);
  const nameEdit = useInlineEdit({
    value: equipment.name,
    onCommit: (name) => onUpdate((current) => ({ ...current, name: name || current.name })),
    blurOnEscape: true
  });
  const obtainEdit = useInlineEdit({
    value: equipment.obtain || "",
    onCommit: (obtain) => onUpdate((current) => ({ ...current, obtain })),
    commitOnEnter: false
  });
  const className = [
    "equipment-planner-item",
    equipment.active ? "is-active" : "is-inactive",
    reorder.isItemDragging(equipment.id) ? "is-dragging" : "",
    reorder.isItemDropTarget(equipment.id) ? "is-drop-target" : "",
    reorder.getDropPlacement("item", equipment.id) === "after" ? "drop-after" : ""
  ].filter(Boolean).join(" ");

  return (
    <li className={className} {...reorder.getItemProps({ itemId: equipment.id })}>
      <div className="equipment-planner-line">
        <button
          className="task-planner-drag-handle"
          type="button"
          onPointerDown={(event) => reorder.itemReorder.startDrag(event, equipment.id)}
          aria-label={`${textContent.reorderEquipmentTitle || "Déplacer"} ${equipment.name}`}
          title={textContent.reorderEquipmentTitle || "Déplacer"}
        >
          <Icon name="drag" />
        </button>
        <div className="equipment-planner-identity">
          <span className="equipment-planner-type-icon" aria-hidden="true">
            <Icon name={equipment.icon || "shield"} />
          </span>
          <strong {...nameEdit.getContentEditableProps({ className: "equipment-planner-name", title: textContent.renameEquipmentTitle || "Renommer" })}>{equipment.name}</strong>
        </div>
        <label className="equipment-planner-include-switch">
          <input
            type="checkbox"
            checked={equipment.active}
            onChange={(event) => onUpdate((current) => ({ ...current, active: event.target.checked }))}
          />
          <span aria-hidden="true" />
          <b>{textContent.includeInSummaryLabel || "Inclure dans le résumé"}</b>
        </label>
        <button
          type="button"
          className={`task-planner-settings-button equipment-planner-row-edit-button ${detailEditing ? "active" : ""}`}
          onClick={() => setDetailEditing((value) => !value)}
          aria-pressed={detailEditing}
          title={detailEditing ? textContent.viewModeTitle || "Mode affichage" : textContent.editModeTitle || "Mode édition"}
        >
          <Icon name="edit" />
        </button>
        <button type="button" className="task-planner-delete-button danger" onClick={onDelete} aria-label={`${textContent.deleteEquipmentTitle || "Supprimer"} ${equipment.name}`} title={textContent.deleteEquipmentTitle || "Supprimer"}>
          <Icon name="trash" />
        </button>
      </div>
      <div className="equipment-planner-details">
        {detailEditing ? (
          <>
          <div className="equipment-planner-tabs" role="tablist" aria-label={textContent.tabsLabel || "Ajout de caractéristiques"}>
            {DETAIL_TABS.map((tab) => (
              <button
                key={tab}
                className={`equipment-planner-tab ${activeTab === tab ? "active" : ""}`}
                type="button"
                role="tab"
                aria-selected={activeTab === tab}
                onClick={() => setActiveTab(tab)}
                title={getTabLabel(tab, textContent)}
              >
                <TabIcon tab={tab} />
                <span>{getTabLabel(tab, textContent)}</span>
              </button>
            ))}
          </div>
          {activeTab === "stats" && <CharacteristicsEditor equipment={equipment} textContent={textContent} editing onUpdate={onUpdate} />}
          {activeTab === "sockets" && <SocketItemsEditor equipment={equipment} textContent={textContent} editing onUpdate={onUpdate} />}
          {activeTab === "craft" && (
            <section className="equipment-planner-craft-tab">
              <label className="equipment-planner-field">
                <span>{textContent.obtainLabel || "Obtention"}</span>
                <textarea {...obtainEdit.getInputProps({ placeholder: textContent.obtainPlaceholder || "Comment obtenir ou créer cet équipement" })} />
              </label>
              <MaterialsEditor equipment={equipment} textContent={textContent} editing onUpdate={onUpdate} />
            </section>
          )}
          </>
        ) : (
          <EquipmentDisplay equipment={equipment} textContent={textContent} />
        )}
      </div>
    </li>
  );
}

function EquipmentDisplay({ equipment, textContent }) {
  const hasStats = equipment.characteristics.length > 0;
  const hasSockets = equipment.socketItems.length > 0;
  const hasCraft = Boolean(equipment.obtain || equipment.materials.length);
  if (!hasStats && !hasSockets && !hasCraft) {
    return <p className="equipment-planner-muted">{textContent.emptyEquipmentDetails || "Aucun détail renseigné."}</p>;
  }

  return (
    <div className="equipment-planner-display-grid">
      {hasStats && (
        <section className="equipment-planner-display-panel">
          <h4>{textContent.displayStatsTitle || "Statistiques"}</h4>
          <ul>
            {equipment.characteristics.map((trait) => (
              <li className="equipment-planner-display-stat" key={trait.id}>
                <Icon name={trait.icon || "sword"} />
                <span>{trait.name}</span>
                {trait.value !== "" && <b>{formatValue(trait.value)}</b>}
              </li>
            ))}
          </ul>
        </section>
      )}
      {hasSockets && (
        <section className="equipment-planner-display-panel">
          <h4>{textContent.displaySocketTitle || "Objets sertis"}</h4>
          <ul>
            {equipment.socketItems.map((socketItem) => (
              <li className="equipment-planner-display-socket" key={socketItem.id}>
                <SocketShapeIcon shape={socketItem.shape} color={socketItem.color} />
                <div>
                  <strong>{socketItem.name}</strong>
                  {socketItem.bonuses.map((bonus) => (
                    <small key={bonus.id}>{bonus.name}{bonus.value !== "" ? ` +${formatValue(bonus.value)}` : ""}</small>
                  ))}
                </div>
              </li>
            ))}
          </ul>
        </section>
      )}
      {hasCraft && (
        <section className="equipment-planner-display-panel">
          <h4>{textContent.craftTab || "Craft"}</h4>
          {equipment.obtain ? <p>{equipment.obtain}</p> : null}
          {equipment.materials.length ? (
          <ul>
            {equipment.materials.map((material) => <li key={material.id}><span>{material.name}</span><b>x{material.qty}</b></li>)}
          </ul>
          ) : null}
        </section>
      )}
    </div>
  );
}

function getTabLabel(tab, textContent) {
  if (tab === "stats") return textContent.statsTab || "Stats / affixe";
  if (tab === "sockets") return textContent.socketsTab || "Objets à sertir";
  return textContent.craftTab || "Craft";
}

function TabIcon({ tab }) {
  if (tab === "stats") return <span className="equipment-planner-percent-icon" aria-hidden="true">%</span>;
  if (tab === "sockets") return <Icon name="jewel" />;
  return <Icon name="hammer" />;
}

function CharacteristicsEditor({ equipment, textContent, editing, onUpdate }) {
  const [draft, setDraft] = useState({ icon: "sword", category: "", name: "", value: "" });
  const categories = getCharacteristicCategories(equipment);
  const {
    itemReorder,
    groupReorder,
    getItemProps,
    getGroupProps,
    isItemDragging,
    isItemDropTarget,
    isGroupDragging,
    isGroupDropTarget,
    getDropPlacement
  } = useGroupedReorder({
    namespace: `equipment-characteristics-${equipment.id}`,
    items: equipment.characteristics,
    getItemId: (trait) => trait.id,
    getItemGroup: (trait) => trait.category,
    getEffectiveGroup: (trait) => trait.category,
    reorderFeatures: {
      item: { groupChange: true },
      group: { reorder: true, itemDrop: true }
    },
    onItemMove: (operation) => onUpdate((current) => applyGroupedReorderOperation(current, {
      operation,
      itemsKey: "characteristics",
      groupOrderKey: "categoryOrder",
      collapsedGroupsKey: "collapsedCategories",
      getItemGroup: (trait) => trait.category,
      setItemGroup: setItemCategory
    })),
    onGroupMove: (operation) => onUpdate((current) => applyGroupedReorderOperation(current, {
      operation,
      itemsKey: "characteristics",
      groupOrderKey: "categoryOrder",
      collapsedGroupsKey: "collapsedCategories",
      getItemGroup: (trait) => trait.category,
      setItemGroup: setItemCategory
    }))
  });
  const reorder = { itemReorder, groupReorder, getItemProps, getGroupProps, isItemDragging, isItemDropTarget, isGroupDragging, isGroupDropTarget, getDropPlacement };

  function addTrait(event) {
    event.preventDefault();
    const cleanName = draft.name.trim();
    if (!cleanName) return;
    onUpdate((current) => ({
      ...current,
      characteristics: [...current.characteristics, { id: createLocalId("trait"), icon: draft.icon, category: draft.category.trim(), name: cleanName, value: draft.value.trim() }],
      categoryOrder: draft.category.trim() && !current.categoryOrder.includes(draft.category.trim()) ? [...current.categoryOrder, draft.category.trim()] : current.categoryOrder
    }));
    setDraft({ icon: draft.icon, category: draft.category, name: "", value: "" });
  }

  function updateTrait(traitId, updater) {
    onUpdate((current) => ({ ...current, characteristics: current.characteristics.map((trait) => trait.id === traitId ? updater(trait) : trait).filter((trait) => trait.name) }));
  }

  return (
    <section className="equipment-planner-panel">
      <h4>{textContent.characteristicsTitle || "Caractéristiques"}</h4>
      {editing && (
        <form className="inline-form equipment-planner-trait-form" onSubmit={addTrait}>
          <IconChoiceDropdown
            label={textContent.traitIconLabel || "Icône"}
            value={draft.icon}
            options={TRAIT_ICONS.map((icon) => ({ icon, label: icon }))}
            onChange={(icon) => setDraft((current) => ({ ...current, icon }))}
          />
          <input value={draft.category} onChange={(event) => setDraft((current) => ({ ...current, category: event.target.value }))} placeholder={textContent.categoryPlaceholder || "Catégorie"} />
          <input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.traitNamePlaceholder || "Nom"} />
          <input value={draft.value} onChange={(event) => setDraft((current) => ({ ...current, value: event.target.value }))} placeholder={textContent.traitValuePlaceholder || "Valeur"} />
          <button>{textContent.addTraitButton || "Ajouter"}</button>
        </form>
      )}
      <TraitCategoryList
        traits={equipment.characteristics}
        categories={categories}
        collapsedCategories={equipment.collapsedCategories}
        reorder={reorder}
        textContent={textContent}
        onToggleCategory={(category) => onUpdate((current) => ({
          ...current,
          collapsedCategories: current.collapsedCategories.includes(category)
            ? current.collapsedCategories.filter((item) => item !== category)
            : [...current.collapsedCategories, category]
        }))}
        onUpdateTrait={updateTrait}
        editing={editing}
      />
    </section>
  );
}

function TraitCategoryList({ traits, categories, collapsedCategories, reorder, textContent, onToggleCategory, onUpdateTrait, editing }) {
  const ungrouped = traits.filter((trait) => !trait.category);

  function renderTrait(trait) {
    const traitClassName = [
      "equipment-planner-trait",
      reorder.isItemDragging(trait.id) ? "is-dragging" : "",
      reorder.isItemDropTarget(trait.id) ? "is-drop-target" : "",
      reorder.getDropPlacement("item", trait.id) === "after" ? "drop-after" : ""
    ].filter(Boolean).join(" ");
    return (
      <EquipmentTraitRow
        key={trait.id}
        trait={trait}
        className={traitClassName}
        itemProps={reorder.getItemProps({ itemId: trait.id, groupId: trait.category })}
        editing={editing}
        textContent={textContent}
        onStartDrag={(event) => reorder.itemReorder.startDrag(event, trait.id)}
        onUpdateTrait={onUpdateTrait}
      />
    );
  }

  return (
    <div className="equipment-planner-trait-list">
      {ungrouped.length ? <ul>{ungrouped.map(renderTrait)}</ul> : null}
      {categories.map((category) => {
        const categoryTraits = traits.filter((trait) => trait.category === category);
        const collapsed = collapsedCategories.includes(category);
        const className = [
          "equipment-planner-trait-category",
          collapsed ? "is-collapsed" : "",
          reorder.isGroupDragging(category) ? "is-dragging" : "",
          reorder.isGroupDropTarget(category) ? "is-drop-target" : ""
        ].filter(Boolean).join(" ");
        return (
          <section className={className} key={category} {...reorder.getGroupProps({ groupId: category })}>
            <div className="equipment-planner-trait-category-header">
              <button type="button" className="task-planner-category-drag-handle" onPointerDown={(event) => reorder.groupReorder.startDrag(event, { groupId: category })} title={textContent.reorderCategoryTitle || "Déplacer la catégorie"}>
                <Icon name="drag" />
              </button>
              <strong>{category}</strong>
              <span>{categoryTraits.length}</span>
              <button type="button" className="checklist-section-collapse-button" onClick={() => onToggleCategory(category)} aria-expanded={!collapsed}>
                <Icon name={collapsed ? "chevron-down" : "chevron-up"} />
              </button>
            </div>
            {!collapsed && <ul>{categoryTraits.map(renderTrait)}</ul>}
          </section>
        );
      })}
      {!traits.length && <p className="equipment-planner-muted">{textContent.emptyCharacteristics || "Aucune caractéristique."}</p>}
    </div>
  );
}

function EquipmentTraitRow({ trait, className, itemProps, editing, textContent, onStartDrag, onUpdateTrait }) {
  const categoryEdit = useInlineEdit({
    value: trait.category || "",
    onCommit: (category) => onUpdateTrait(trait.id, (current) => ({ ...current, category }))
  });
  const nameEdit = useInlineEdit({
    value: trait.name,
    transform: (draft) => String(draft || "").trim() || trait.name,
    onCommit: (name) => onUpdateTrait(trait.id, (current) => ({ ...current, name }))
  });
  const valueEdit = useInlineEdit({
    value: formatValue(trait.value),
    onCommit: (value) => onUpdateTrait(trait.id, (current) => ({ ...current, value }))
  });

  return (
    <li className={className} {...itemProps}>
      {editing ? (
        <>
          <button type="button" className="task-planner-drag-handle" onPointerDown={onStartDrag} title={textContent.reorderTraitTitle || "Déplacer"}>
            <Icon name="drag" />
          </button>
          <IconChoiceDropdown
            label={textContent.traitIconLabel || "Icône"}
            value={trait.icon || "sword"}
            options={TRAIT_ICONS.map((icon) => ({ icon, label: icon }))}
            onChange={(icon) => onUpdateTrait(trait.id, (current) => ({ ...current, icon }))}
          />
          <input {...categoryEdit.getInputProps({ placeholder: textContent.categoryPlaceholder || "Catégorie" })} />
          <input {...nameEdit.getInputProps({ placeholder: textContent.traitNamePlaceholder || "Nom" })} />
          <input {...valueEdit.getInputProps({ placeholder: textContent.traitValuePlaceholder || "Valeur" })} />
          <button type="button" className="task-planner-delete-button danger" onClick={() => onUpdateTrait(trait.id, () => ({ ...trait, name: "" }))} title={textContent.deleteTraitTitle || "Supprimer"}>
            <Icon name="trash" />
          </button>
        </>
      ) : (
        <div className="equipment-planner-trait-readonly">
          <Icon name={trait.icon || "sword"} />
          <span>{trait.name}</span>
          {trait.value !== "" && <b>{formatValue(trait.value)}</b>}
        </div>
      )}
    </li>
  );
}

function MaterialsEditor({ equipment, textContent, editing, onUpdate }) {
  const [draft, setDraft] = useState({ name: "", qty: 1 });

  function addMaterial(event) {
    event.preventDefault();
    const cleanName = draft.name.trim();
    if (!cleanName) return;
    onUpdate((current) => ({
      ...current,
      materials: [...current.materials, { id: createLocalId("material"), name: cleanName, qty: Math.max(1, Number.parseInt(draft.qty, 10) || 1) }]
    }));
    setDraft({ name: "", qty: 1 });
  }

  return (
    <section className="equipment-planner-panel">
      <h4>{textContent.materialsTitle || "Matériaux"}</h4>
      {editing && (
        <form className="inline-form equipment-planner-material-form" onSubmit={addMaterial}>
          <input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.materialNamePlaceholder || "Matériau"} />
          <input type="number" min="1" value={draft.qty} onChange={(event) => setDraft((current) => ({ ...current, qty: event.target.value }))} aria-label={textContent.materialQtyLabel || "Quantité"} />
          <button>{textContent.addMaterialButton || "Ajouter"}</button>
        </form>
      )}
      <ul className="equipment-planner-materials">
        {equipment.materials.map((material) => (
          <MaterialRow
            key={material.id}
            material={material}
            editing={editing}
            textContent={textContent}
            onUpdate={onUpdate}
          />
        ))}
      </ul>
    </section>
  );
}

function MaterialRow({ material, editing, textContent, onUpdate }) {
  const nameEdit = useInlineEdit({
    value: material.name,
    transform: (draft) => String(draft || "").trim() || material.name,
    onCommit: (name) => onUpdate((current) => ({ ...current, materials: current.materials.map((item) => item.id === material.id ? { ...item, name } : item) }))
  });

  return (
    <li>
      {editing ? (
        <>
          <input {...nameEdit.getInputProps()} />
          <input type="number" min="1" value={material.qty} onChange={(event) => onUpdate((current) => ({ ...current, materials: current.materials.map((item) => item.id === material.id ? { ...item, qty: Math.max(1, Number.parseInt(event.target.value, 10) || 1) } : item) }))} />
          <button type="button" className="task-planner-delete-button danger" onClick={() => onUpdate((current) => ({ ...current, materials: current.materials.filter((item) => item.id !== material.id) }))} title={textContent.deleteMaterialTitle || "Supprimer"}>
            <Icon name="trash" />
          </button>
        </>
      ) : (
        <div className="equipment-planner-material-readonly">
          <span>{material.name}</span>
          <b>x{material.qty}</b>
        </div>
      )}
    </li>
  );
}

function SocketItemsEditor({ equipment, textContent, editing, onUpdate }) {
  const [draft, setDraft] = useState({ name: "", shape: "jewel", color: "yellow" });

  function addSocketItem(event) {
    event.preventDefault();
    const cleanName = draft.name.trim();
    if (!cleanName) return;
    onUpdate((current) => ({
      ...current,
      socketItems: [...current.socketItems, { id: createLocalId("socket"), name: cleanName, shape: draft.shape, color: draft.color, bonuses: [] }]
    }));
    setDraft({ ...draft, name: "" });
  }

  function updateSocketItem(socketItemId, updater) {
    onUpdate((current) => ({
      ...current,
      socketItems: current.socketItems.map((socketItem) => socketItem.id === socketItemId ? updater(socketItem) : socketItem).filter((socketItem) => socketItem.name)
    }));
  }

  function deleteSocketItem(socketItemId) {
    onUpdate((current) => ({
      ...current,
      socketItems: current.socketItems.filter((socketItem) => socketItem.id !== socketItemId),
      socketLinks: current.socketLinks.filter((link) => link.fromSocketItemId !== socketItemId && link.toSocketItemId !== socketItemId)
    }));
  }

  function addSocketLink(fromSocketItemId, toSocketItemId) {
    if (!toSocketItemId || fromSocketItemId === toSocketItemId) return;
    const pair = [fromSocketItemId, toSocketItemId].sort().join(":");
    if (equipment.socketLinks.some((link) => [link.fromSocketItemId, link.toSocketItemId].sort().join(":") === pair)) return;
    onUpdate((current) => ({
      ...current,
      socketLinks: [...current.socketLinks, { id: createLocalId("socket-link"), fromSocketItemId, toSocketItemId }]
    }));
  }

  return (
    <section className="equipment-planner-panel">
      <h4>{textContent.socketItemsTitle || "Objets sertis"}</h4>
      {editing && (
        <form className="inline-form equipment-planner-socket-form" onSubmit={addSocketItem}>
          <input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.socketNamePlaceholder || "Joyau, materia..."} />
          <ShapeChoiceDropdown label={textContent.socketShapeLabel || "Forme"} value={draft.shape} onChange={(shape) => setDraft((current) => ({ ...current, shape }))} />
          <ColorChoiceDropdown label={textContent.socketColorLabel || "Couleur"} value={draft.color} onChange={(color) => setDraft((current) => ({ ...current, color }))} />
          <button>{textContent.addSocketButton || "Ajouter"}</button>
        </form>
      )}
      <div className="equipment-planner-socket-grid">
        {equipment.socketItems.map((socketItem) => (
          <SocketItemEditor
            key={socketItem.id}
            equipment={equipment}
            socketItem={socketItem}
            textContent={textContent}
            editing={editing}
            onUpdate={(updater) => updateSocketItem(socketItem.id, updater)}
            onDelete={() => deleteSocketItem(socketItem.id)}
            onAddLink={(toSocketItemId) => addSocketLink(socketItem.id, toSocketItemId)}
            onDeleteLink={(linkId) => onUpdate((current) => ({ ...current, socketLinks: current.socketLinks.filter((link) => link.id !== linkId) }))}
          />
        ))}
      </div>
    </section>
  );
}

function SocketItemEditor({ equipment, socketItem, textContent, editing, onUpdate, onDelete, onAddLink, onDeleteLink }) {
  const [bonusDraft, setBonusDraft] = useState({ name: "", value: "" });
  const links = equipment.socketLinks.filter((link) => link.fromSocketItemId === socketItem.id || link.toSocketItemId === socketItem.id);
  const nameEdit = useInlineEdit({
    value: socketItem.name,
    transform: (draft) => String(draft || "").trim() || socketItem.name,
    onCommit: (name) => onUpdate((current) => ({ ...current, name }))
  });

  function addBonus(event) {
    event.preventDefault();
    const cleanName = bonusDraft.name.trim();
    if (!cleanName) return;
    onUpdate((current) => ({
      ...current,
      bonuses: [...current.bonuses, { id: createLocalId("bonus"), category: "", name: cleanName, value: bonusDraft.value.trim() }]
    }));
    setBonusDraft({ name: "", value: "" });
  }

  return (
    <article className="equipment-planner-socket-card">
      <div className="equipment-planner-socket-heading">
        <SocketShapeIcon shape={socketItem.shape} color={socketItem.color} />
        {editing ? (
          <>
            <input {...nameEdit.getInputProps()} />
            <button type="button" className="task-planner-delete-button danger" onClick={onDelete} title={textContent.deleteSocketTitle || "Supprimer"}>
              <Icon name="trash" />
            </button>
          </>
        ) : (
          <strong>{socketItem.name}</strong>
        )}
      </div>
      {editing && (
        <>
          <div className="equipment-planner-socket-controls">
            <ShapeChoiceDropdown label={textContent.socketShapeLabel || "Forme"} value={socketItem.shape} onChange={(shape) => onUpdate((current) => ({ ...current, shape }))} />
            <ColorChoiceDropdown label={textContent.socketColorLabel || "Couleur"} value={socketItem.color} onChange={(color) => onUpdate((current) => ({ ...current, color }))} />
            {equipment.socketItems.length > 1 && (
              <select defaultValue="" onChange={(event) => { onAddLink(event.target.value); event.target.value = ""; }} aria-label={textContent.addSocketLinkLabel || "Lier à"}>
                <option value="">{textContent.addSocketLinkLabel || "Lier à"}</option>
                {equipment.socketItems.filter((item) => item.id !== socketItem.id).map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
              </select>
            )}
          </div>
          <form className="inline-form equipment-planner-bonus-form" onSubmit={addBonus}>
            <input value={bonusDraft.name} onChange={(event) => setBonusDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.bonusNamePlaceholder || "Bonus"} />
            <input value={bonusDraft.value} onChange={(event) => setBonusDraft((current) => ({ ...current, value: event.target.value }))} placeholder={textContent.traitValuePlaceholder || "Valeur"} />
            <button aria-label={textContent.addBonusButton || "Ajouter"} title={textContent.addBonusButton || "Ajouter"}>+</button>
          </form>
        </>
      )}
      <ul className="equipment-planner-socket-bonuses">
        {socketItem.bonuses.map((bonus) => (
          <li key={bonus.id}>
            <span>{bonus.category ? `${bonus.category} · ` : ""}{bonus.name}</span>
            <b>{formatValue(bonus.value)}</b>
            {editing && (
              <button type="button" className="task-planner-delete-button danger" onClick={() => onUpdate((current) => ({ ...current, bonuses: current.bonuses.filter((item) => item.id !== bonus.id) }))} title={textContent.deleteBonusTitle || "Supprimer"}>
                <Icon name="trash" />
              </button>
            )}
          </li>
        ))}
      </ul>
      {links.length ? (
        <div className={`equipment-planner-socket-links ${editing ? "is-editing" : ""}`}>
          {links.map((link) => {
            const otherSocketItemId = link.fromSocketItemId === socketItem.id ? link.toSocketItemId : link.fromSocketItemId;
            return (
              <button key={link.id} type="button" className="equipment-planner-link-chip" onClick={editing ? () => onDeleteLink(link.id) : undefined} title={editing ? textContent.deleteSocketLinkTitle || "Retirer le lien" : getSocketItemName(equipment, otherSocketItemId)}>
                <span>{getSocketItemName(equipment, otherSocketItemId)}</span>
                {editing && <Icon name="close" />}
              </button>
            );
          })}
        </div>
      ) : null}
    </article>
  );
}
