"use client";


import React, { useCallback, useMemo, useRef, useState } from "react";
import { createPortal } from 'react-dom';
import AccessDeniedCard from "@/components/AccessDeniedCard";
import { usePageTitle } from "@/hooks/usePageTitle";

// --- Utils: debounce ---------------------------------------------------------
function generateReference() {
  const letters = "ABCDEFGHIKLMNPQRSTUVWXYZ"; // sans O
  const digits = "123456789";               // sans 0
  const pool = letters + digits;
  const pick = (s: string) => s[Math.floor(Math.random() * s.length)];
  while (true) {
    let ref = "";
    for (let i = 0; i < 8; i++) ref += pick(pool);
    if (ref.startsWith("RG")) continue;     // ne pas commencer par RG
    if (!/[A-Z]/.test(ref)) continue;       // au moins une lettre
    if (!/[1-9]/.test(ref)) continue;       // au moins un chiffre
    return ref;
  }
}

function useDebouncedValue<T>(value: T, delay = 300) {
  const [debounced, setDebounced] = React.useState(value);
  React.useEffect(() => {
    const t = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(t);
  }, [value, delay]);
  return debounced;
}

// --- API helpers (alignés avec NouveauCDDUForm.tsx) ------------------------
type SalarieOption = { matricule: string; nom: string; email?: string | null };
type SpectacleOption = { id?: string; nom: string; numero_objet?: string | null };
type ClientInfo = { id: string; name: string; api_name?: string } | null;

// Import de la fonction api depuis lib/fetcher
import { api } from '@/lib/fetcher';
import { PROFESSIONS_ARTISTE, type ProfessionOption } from '@/components/constants/ProfessionsArtiste';

// Hook pour récupérer les infos client
function useClientInfo() {
  const [clientInfo, setClientInfo] = React.useState<ClientInfo>(null);
  const [loaded, setLoaded] = React.useState(false);

  React.useEffect(() => {
    if (loaded) return;
    console.log('🔍 [CLIENT INFO DEBUG] Loading client info...');
    
    (async () => {
      try {
        const res = await fetch("/api/me", { 
          cache: "no-store", 
          headers: { Accept: "application/json" }, 
          credentials: "include" 
        });
        
        console.log('🔍 [CLIENT INFO DEBUG] /api/me response status:', res.status);
        
        if (res.ok) {
          const me = await res.json();
          console.log('🔍 [CLIENT INFO DEBUG] /api/me response data:', me);
          
          const info = { 
            id: me.active_org_id || null, 
            name: me.active_org_name || "Organisation", 
            api_name: me.active_org_api_name 
          };
          
          console.log('🔍 [CLIENT INFO DEBUG] Client info constructed:', info);
          setClientInfo(info);
        } else {
          console.log('🔍 [CLIENT INFO DEBUG] /api/me response not ok');
        }
      } catch (e) {
        console.warn('🔍 [CLIENT INFO DEBUG] Could not load client info:', e);
        setClientInfo(null);
      }
      setLoaded(true);
    })();
  }, [loaded]);

  return clientInfo;
}

// Recherche de productions (alignée avec useSearchSpectacles)
async function searchProductions(q: string, clientInfo: ClientInfo): Promise<SpectacleOption[]> {
  if (!q || q.trim().length < 2 || !clientInfo) return [];
  
  try {
    const params = new URLSearchParams();
    params.set("page", "1");
    params.set("limit", "10");
    params.set("q", q.trim());

    const result = await api<{ items: SpectacleOption[] }>(`/spectacles?${params.toString()}`, {}, clientInfo);
    
    return (result.items || []).filter(s => s.nom && s.nom.length > 0);
  } catch (e) {
    console.warn('Search productions failed:', e);
    return [];
  }
}

// Recherche de salariés (alignée avec useSearchSalaries) — renvoie des objets complets
async function searchSalaries(q: string, clientInfo: ClientInfo): Promise<SalarieOption[]> {
  console.log('🔍 [SEARCH DEBUG] searchSalaries called with:', { q, clientInfo });
  
  if (!q || q.trim().length < 2 || !clientInfo) {
    console.log('🔍 [SEARCH DEBUG] Search conditions not met');
    return [];
  }
  
  try {
    const params = new URLSearchParams();
    params.set("page", "1");
    params.set("limit", "10");
    params.set("q", q.trim());
    
    const url = `/salaries?${params.toString()}`;
    console.log('🔍 [SEARCH DEBUG] Search URL:', url);
    
    const result = await api<{ items: SalarieOption[] }>(url, {}, clientInfo);
    console.log('🔍 [SEARCH DEBUG] API result:', result);
    
    const items = result.items || [];
    console.log('🔍 [SEARCH DEBUG] Final items:', items);
    
    return items;
  } catch (e) {
    console.warn('🔍 [SEARCH DEBUG] Search salaries failed:', e);
    return [];
  }
}

// Recherche locale dans la liste des professions ARTISTE
function searchProfessionsArtiste(q: string): Promise<string[]> {
  if (!q || q.trim().length < 2) return Promise.resolve([]);
  const s = q.trim().toLowerCase();
  const items = PROFESSIONS_ARTISTE.filter((p: ProfessionOption) =>
    p.label.toLowerCase().includes(s) || p.code.toLowerCase().includes(s)
  )
    .slice(0, 20)
    .map((p) => `${p.label} — ${p.code}`);
  return Promise.resolve(items);
}

// Recherche locale dans la liste des professions TECHNICIEN (chargée depuis /public)
let _TECH_LIST: ProfessionOption[] | null = null;
async function loadProfessionsTechnicien(): Promise<ProfessionOption[]> {
  if (_TECH_LIST) return _TECH_LIST;
  try {
    const res = await fetch('/data/professions-techniciens.json', { cache: 'force-cache' });
    if (!res.ok) throw new Error('Failed to load technicien professions');
    const json = (await res.json()) as ProfessionOption[];
    _TECH_LIST = json;
    return json;
  } catch (e) {
    console.warn('Unable to load professions-techniciens.json', e);
    _TECH_LIST = [];
    return _TECH_LIST;
  }
}
async function searchProfessionsTechnicien(q: string): Promise<string[]> {
  if (!q || q.trim().length < 2) return [];
  const list = await loadProfessionsTechnicien();
  const s = q.trim().toLowerCase();
  return list
    .filter((p) => p.label.toLowerCase().includes(s) || p.code.toLowerCase().includes(s))
    .slice(0, 20)
    .map((p) => `${p.label} — ${p.code}`);
}

export type RoleType = "ARTISTE" | "TECHNICIEN";
export type SalaryType = "BRUT" | "NET_AVT_PAS" | "CTE" | "MINIMA";

export type ContractRow = {
  id: string;
  salarie: string;
  salarieMatricule?: string; // Stocker le matricule séparément
  role: RoleType;
  profession: string;
  dateDebut: string; // YYYY-MM-DD
  dateFin: string;   // YYYY-MM-DD
  typeSalaire: SalaryType;
  salaire: number | "";
  nbreCachetsRepresentation: number | "";
  nbreServiceRepet: number | "";
  heuresSiTechnicien: number | "";
  note: string;
  selected?: boolean;
};

const uid = () => Math.random().toString(36).slice(2, 9);
const emptyRow = (defaults?: Partial<ContractRow>): ContractRow => {
  const { id: _omitId, selected: _omitSel, ...rest } = defaults || {};
  return {
    salarie: "",
    role: "ARTISTE",
    profession: "",
    dateDebut: "",
    dateFin: "",
    typeSalaire: "BRUT",
    salaire: "",
    nbreCachetsRepresentation: "",
    nbreServiceRepet: "",
    heuresSiTechnicien: "",
    note: "",
    // on positionne l'id et selected à la fin pour ne JAMAIS reprendre ceux d'une autre ligne
    ...rest,
    id: uid(),
    selected: false,
  };
};

function validateRow(r: ContractRow) {
  const errors: Record<string, string> = {};
  if (!r.salarie) errors.salarie = "Requis";
  if (!r.profession) errors.profession = "Requis";
  if (!r.dateDebut) errors.dateDebut = "Requis";
  if (!r.dateFin) errors.dateFin = "Requis";
  if (r.dateDebut && r.dateFin && r.dateFin < r.dateDebut) errors.dateFin = "Fin < Début";
  // Nouvelle règle : interdire les contrats multi-mois
  if (r.dateDebut && r.dateFin) {
    const m1 = r.dateDebut.slice(0, 7); // YYYY-MM
    const m2 = r.dateFin.slice(0, 7);
    if (m1 !== m2) {
      errors.dateFin = "MULTI_MOIS"; // signal dédié pour l'UI
    }
  }
  if (r.typeSalaire !== "MINIMA" && (r.salaire === "" || Number(r.salaire) <= 0)) errors.salaire = "> 0";
  if (r.role === "ARTISTE") {
    const hasCachets = r.nbreCachetsRepresentation !== "" && Number(r.nbreCachetsRepresentation) >= 0;
    const hasServices = r.nbreServiceRepet !== "" && Number(r.nbreServiceRepet) >= 0;

    if (!hasCachets && !hasServices) {
      errors.nbreCachetsRepresentation = "Requis (cachets ou services)";
      errors.nbreServiceRepet = "Requis (cachets ou services)";
    } else {
      if (r.nbreCachetsRepresentation !== "" && Number(r.nbreCachetsRepresentation) < 0) {
        errors.nbreCachetsRepresentation = ">= 0";
      }
      if (r.nbreServiceRepet !== "" && Number(r.nbreServiceRepet) < 0) {
        errors.nbreServiceRepet = ">= 0";
      }
    }
  }
  if (r.role === "TECHNICIEN") {
    if (r.heuresSiTechnicien === "" || Number(r.heuresSiTechnicien) < 0) errors.heuresSiTechnicien = ">= 0";
  }
  return errors;
}

function parsePasted(text: string): string[][] {
  return text.trim().split(/\r?\n/).map((line) => line.split(/\t|;/));
}

const PASTE_ORDER = [
  "salarie",
  "role",
  "profession",
  "dateDebut",
  "dateFin",
  "typeSalaire",
  "salaire",
  "nbreCachetsRepresentation",
  "nbreServiceRepet",
  "heuresSiTechnicien",
  "note",
] as const;

// --- CSV utils ---------------------------------------------------------------
function detectDelimiter(text: string): string {
  const comma = (text.match(/,/g) || []).length;
  const semicolon = (text.match(/;/g) || []).length;
  return semicolon > comma ? ';' : ',';
}


function parseCSV(text: string): string[][] {
  const delim = detectDelimiter(text);
  const rows: string[][] = [];
  let row: string[] = [];
  let field = '';
  let inQuotes = false;
  for (let i = 0; i < text.length; i++) {
    const c = text[i];
    const next = text[i + 1];
    if (c === '"') {
      if (inQuotes && next === '"') { field += '"'; i++; continue; }
      inQuotes = !inQuotes; continue;
    }
    if (!inQuotes && (c === '\n' || c === '\r')) {
      if (c === '\r' && next === '\n') i++; // handle CRLF
      row.push(field); field = '';
      if (row.length > 1 || row[0] !== '') rows.push(row);
      row = []; continue;
    }
    if (!inQuotes && c === delim) { row.push(field); field = ''; continue; }
    field += c;
  }
  // flush last field/row
  row.push(field);
  if (row.length > 1 || row[0] !== '') rows.push(row);
  return rows;
}

function normalizeDate(val: string): string {
  if (!val) return "";
  const s = String(val).trim();
  // DD/MM/YYYY
  const m1 = s.match(/^([0-3]?\d)\/([0-1]?\d)\/(\d{4})$/);
  if (m1) {
    const dd = m1[1].padStart(2, '0');
    const mm = m1[2].padStart(2, '0');
    const yyyy = m1[3];
    return `${yyyy}-${mm}-${dd}`;
  }
  // YYYY-MM-DD (déjà OK)
  const m2 = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
  if (m2) return s;
  return s; // fallback: on laisse tel quel
}

function rowsFromMatrix(parsed: string[][]): ContractRow[] {
  if (parsed.length === 0) return [];
  return parsed.map((cols) => {
    const base = emptyRow();
    const assigned: Partial<ContractRow> = {};
    PASTE_ORDER.forEach((key, i) => {
      const val = cols[i];
      if (val == null) return;
      switch (key) {
        case 'role':
          assigned.role = /^tech/i.test(val) ? 'TECHNICIEN' : 'ARTISTE';
          break;
        case 'typeSalaire':
          if (/^min/i.test(val)) assigned.typeSalaire = 'MINIMA';
          else if (/^net/i.test(val)) assigned.typeSalaire = 'NET_AVT_PAS';
          else if (/^cte/i.test(val)) assigned.typeSalaire = 'CTE';
          else assigned.typeSalaire = 'BRUT';
          break;
        case 'dateDebut':
          (assigned as any)[key] = normalizeDate(val);
          break;
        case 'dateFin':
          (assigned as any)[key] = normalizeDate(val);
          break;
        case 'salaire':
        case 'nbreCachetsRepresentation':
        case 'nbreServiceRepet':
        case 'heuresSiTechnicien':
          (assigned as any)[key] = val === '' ? '' : Number(String(val).replace(',', '.'));
          break;
        default:
          (assigned as any)[key] = val;
      }
    });
    return { ...base, ...assigned } as ContractRow;
  });
}

// Ordre de navigation des colonnes (pour Tab/Shift+Tab)
const COLUMN_ORDER = [
  "salarie",
  "role",
  "profession",
  "dateDebut",
  "dateFin",
  "typeSalaire",
  "salaire",
  "nbreCachetsRepresentation",
  "nbreServiceRepet",
  "heuresSiTechnicien",
  "note",
] as const;

// --- Classes compactes réutilisables ---
const thCls = "text-left text-[11px] font-medium uppercase tracking-wide text-muted-foreground px-2 py-1";
const tdCls = "px-1.5 py-1 align-middle";
const inputCls = (error?: boolean) =>
  `w-full rounded-md border px-2 py-1 h-8 text-[13px] ${error ? "border-red-500" : ""}`;
const selectCls = "w-full rounded-md border px-2 h-8 text-[13px] bg-white";
const numberCls = (error?: boolean, disabled?: boolean) =>
  `w-full rounded-md border px-2 py-1 h-8 text-right text-[13px] ${error ? "border-red-500" : ""} ${disabled ? "bg-muted/40" : ""}`;
const headerPad = "px-3 pt-2";
const mainPad = "px-3 pb-3";

// --- Combobox compact avec API intégrée -------------------------------------
function ComboBox({
  value,
  onChange,
  onSelect,
  placeholder,
  searchType,
  className = "",
  inputProps,
  error,
}: {
  value: string | SpectacleOption | null;
  onChange: (v: string | SpectacleOption | null) => void;
  onSelect: (v: string | SpectacleOption | null) => void;
  placeholder?: string;
  searchType: 'productions' | 'salaries' | 'professionsArtiste' | 'professionsTechnicien';
  className?: string;
  inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
  error?: boolean;
}) {
  const [open, setOpen] = React.useState(false);
  const [query, setQuery] = React.useState("");
  const [items, setItems] = React.useState<any[]>([]);
  const [active, setActive] = React.useState(0);
  const [loading, setLoading] = React.useState(false);
  const debounced = useDebouncedValue(query, 300);
  const clientInfo = useClientInfo();
  const listRef = React.useRef<HTMLUListElement | null>(null);
  const anchorRef = React.useRef<HTMLDivElement | null>(null);
  const [popupPos, setPopupPos] = React.useState<{ top: number; left: number; width: number } | null>(null);
  const updatePopupPos = React.useCallback(() => {
    const el = anchorRef.current;
    if (!el) return;
    const r = el.getBoundingClientRect();
    setPopupPos({ top: r.bottom + window.scrollY, left: r.left + window.scrollX, width: r.width });
  }, []);

  React.useEffect(() => {
    if (!open || !listRef.current) return;
    const el = listRef.current.children[active] as HTMLElement | undefined;
    if (el) {
      el.scrollIntoView({ block: 'nearest' });
    }
  }, [active, open]);

  React.useEffect(() => {
    if (!debounced || debounced.length < 2) {
      setItems([]);
      return;
    }
    let alive = true;
    setLoading(true);
    (async () => {
      try {
        updatePopupPos();
        if (searchType === 'productions') {
          const res = await searchProductions(debounced, clientInfo);
          if (alive) setItems(res);
        } else if (searchType === 'salaries') {
          const res = await searchSalaries(debounced, clientInfo); // objets complets
          if (alive) setItems(res);
        } else if (searchType === 'professionsArtiste') {
          const res = await searchProfessionsArtiste(debounced);
          if (alive) setItems(res);
        } else {
          const res = await searchProfessionsTechnicien(debounced);
          if (alive) setItems(res);
        }
      } catch (e) {
        console.warn('Search error:', e);
        if (alive) setItems([]);
      } finally {
        if (alive) setLoading(false);
      }
    })();
    return () => { alive = false; };
  }, [debounced, searchType, clientInfo, updatePopupPos]);

  React.useEffect(() => {
    if (!open) return;
    updatePopupPos();
    const onScroll = () => updatePopupPos();
    const onResize = () => updatePopupPos();
    window.addEventListener('scroll', onScroll, true);
    window.addEventListener('resize', onResize);
    return () => {
      window.removeEventListener('scroll', onScroll, true);
      window.removeEventListener('resize', onResize);
    };
  }, [open, updatePopupPos]);

  const commit = (v: any) => {
    console.log('🔍 [COMBOBOX DEBUG] Commit called with:', v);
    console.log('🔍 [COMBOBOX DEBUG] Search type:', searchType);
    
    if (searchType === 'salaries' && v && typeof v === 'object') {
      // Pour les salariés, on stocke "MATRICULE | Nom" pour pouvoir extraire le matricule plus tard
      const matricule = v.matricule || '';
      const nom = v.nom || '';
      const finalValue = matricule && nom ? `${matricule} | ${nom}` : nom;
      console.log('🔍 [COMBOBOX DEBUG] Salarie object:', v);
      console.log('🔍 [COMBOBOX DEBUG] Matricule:', matricule);
      console.log('🔍 [COMBOBOX DEBUG] Nom:', nom);
      console.log('🔍 [COMBOBOX DEBUG] Final value:', finalValue);
      onSelect(finalValue);
    } else if (searchType === 'productions' && v && typeof v === 'object') {
      // Pour les productions, on passe l'objet complet
      console.log('🔍 [COMBOBOX DEBUG] Production object:', v);
      onSelect(v);
    } else if (typeof v === 'string') {
      console.log('🔍 [COMBOBOX DEBUG] String value:', v);
      onSelect(v);
    } else {
      console.log('🔍 [COMBOBOX DEBUG] Other value (converted to string):', v);
      onSelect(String(v ?? ''));
    }
    setOpen(false);
  };

  // Convertir la valeur pour l'affichage dans l'input
  const inputValue = React.useMemo(() => {
    if (searchType === 'productions' && value && typeof value === 'object') {
      const nom = value.nom || '';
      const numero = value.numero_objet;
      return numero ? `${nom} — ${numero}` : nom;
    }
    return typeof value === 'string' ? value : '';
  }, [value, searchType]);

  return (
    <div ref={anchorRef} className={`relative ${className}`}>
      <input
        value={inputValue}
        onChange={(e) => {
          const newValue = e.target.value;
          if (searchType === 'productions') {
            // Pour les productions, on garde la valeur string temporairement pendant la frappe
            onChange(newValue);
          } else {
            onChange(newValue);
          }
          setQuery(newValue);
          setOpen(true);
        }}
        onFocus={() => setOpen(true)}
        onBlur={() => setTimeout(() => setOpen(false), 150)}
        onKeyDown={(e) => {
          if (!open) return;
          if (e.key === "ArrowDown") { e.preventDefault(); setActive((a) => Math.min(a + 1, Math.max(items.length - 1, 0))); }
          if (e.key === "ArrowUp") { e.preventDefault(); setActive((a) => Math.max(a - 1, 0)); }
          if (e.key === "Enter") { e.preventDefault(); commit(items[active] ?? value); }
          if (e.key === "Escape") { setOpen(false); }
        }}
        placeholder={placeholder}
        {...inputProps}
        className={`w-full rounded-md border px-2 py-1 h-8 text-[13px] ${error ? 'border-red-500' : ''}`}
        aria-invalid={error || undefined}
      />
      {open && (loading || items.length > 0) && popupPos && createPortal(
        (
          <ul
            ref={listRef}
            className="z-[1000] max-h-64 overflow-auto rounded-lg border bg-white shadow-lg text-[13px]"
            role="listbox"
            style={{ position: 'fixed', top: popupPos.top, left: popupPos.left, width: popupPos.width }}
          >
            {loading ? (
              <li className="px-3 py-2 text-gray-500">Recherche en cours…</li>
            ) : items.length === 0 ? (
              <li className="px-3 py-2 text-gray-500">Aucun résultat</li>
            ) : (
              items.map((it: any, i: number) => {
                const isActive = i === active;
                if (searchType === 'salaries') {
                  const nom = it?.nom ?? '';
                  const matricule = it?.matricule ?? '';
                  const email = it?.email ?? '';
                  return (
                    <li
                      key={matricule || nom + i}
                      role="option"
                      aria-selected={isActive}
                      className={`px-3 py-2 cursor-pointer transition border-l-4 ${
                        isActive
                          ? 'bg-emerald-50 border-emerald-500'
                          : 'hover:bg-slate-50 border-transparent'
                      }`}
                      onMouseEnter={() => setActive(i)}
                      onMouseDown={(e) => { e.preventDefault(); commit(it); }}
                    >
                      <div className="font-medium">{nom}</div>
                      <div className="text-xs text-slate-500">{matricule}{email ? ` • ${email}` : ''}</div>
                    </li>
                  );
                }
                // productions et professions
                return (
                  <li
                    key={typeof it === 'string' ? it : (it.id || it.nom || 'opt-' + i)}
                    role="option"
                    aria-selected={isActive}
                    className={`px-3 py-2 cursor-pointer transition border-l-4 ${
                      isActive
                        ? 'bg-emerald-50 border-emerald-500'
                        : 'hover:bg-slate-50 border-transparent'
                    }`}
                    onMouseEnter={() => setActive(i)}
                    onMouseDown={(e) => { e.preventDefault(); commit(it); }}
                  >
                    {(() => {
                      if (searchType === 'productions' && typeof it === 'object' && it.nom) {
                        return (
                          <>
                            <div className="font-medium">{it.nom}</div>
                            {it.numero_objet ? (
                              <div className="text-xs text-slate-500">{it.numero_objet}</div>
                            ) : null}
                          </>
                        );
                      }
                      // fallback pour professions et autres
                      const text = typeof it === 'string' ? it : String(it);
                      const [main, extra] = text.split(' — ');
                      return (
                        <>
                          <div className="font-medium">{main}</div>
                          {extra ? (
                            <div className="text-xs text-slate-500">{extra}</div>
                          ) : null}
                        </>
                      );
                    })()}
                  </li>
                );
              })
            )}
          </ul>
        ),
        document.body
      )}
    </div>
  );
}

export default function Page() {
  usePageTitle("Saisie en tableau - Nouveaux contrats");
  
  // Staff-only guard for this page
  const [authChecked, setAuthChecked] = React.useState(false);
  const [isStaff, setIsStaff] = React.useState<boolean>(false);
  
  // Tous les autres hooks DOIVENT être déclarés avant les conditions de retour
  const [production, setProduction] = useState<SpectacleOption | null>(null);
  const [rows, setRows] = useState<ContractRow[]>([emptyRow()]);
  const pasteTargetRef = useRef<HTMLTextAreaElement | null>(null);

  // State pour l'organisation sélectionnée (staff uniquement)
  const [selectedOrg, setSelectedOrg] = useState<{id: string; name: string} | null>(null);
  const [availableOrgs, setAvailableOrgs] = useState<{id: string; name: string; structure_api: string}[]>([]);
  
  // Focus helpers for row/column navigation
  const focusCell = useCallback((rowId: string, field: keyof ContractRow) => {
    const el = document.querySelector<HTMLElement>(`[data-row="${rowId}"][data-field="${field}"]`);
    el?.focus();
  }, []);

  // Tooltip custom pour le bouton Valider
  const validateWrapRef = useRef<HTMLDivElement | null>(null);
  const [validateTipOpen, setValidateTipOpen] = useState(false);
  const [validateTipPos, setValidateTipPos] = useState<{ top: number; left: number; width: number } | null>(null);
  const computeValidateTipPos = useCallback(() => {
    const el = validateWrapRef.current;
    if (!el) return;
    const r = el.getBoundingClientRect();
    setValidateTipPos({ top: r.top + window.scrollY, left: r.left + window.scrollX + r.width / 2, width: r.width });
  }, []);

  // Tooltip d'aide pour "Annexe" (icône ou select role)
  const annexeIconRef = useRef<HTMLSpanElement | null>(null);
  const annexeAnchorEl = useRef<HTMLElement | null>(null);
  const [annexeTipOpen, setAnnexeTipOpen] = useState(false);
  const [annexeTipPos, setAnnexeTipPos] = useState<{ top: number; left: number } | null>(null);
  const computeAnnexeTipPos = useCallback(() => {
    const el = annexeAnchorEl.current;
    if (!el) return;
    const r = el.getBoundingClientRect();
    setAnnexeTipPos({ top: r.top + window.scrollY, left: r.left + window.scrollX + r.width / 2 });
  }, []);
  const openAnnexeTipFromEl = useCallback((el: HTMLElement) => {
    annexeAnchorEl.current = el;
    const r = el.getBoundingClientRect();
    setAnnexeTipPos({ top: r.top + window.scrollY, left: r.left + window.scrollX + r.width / 2 });
    setAnnexeTipOpen(true);
  }, []);

  // États pour les tooltips et la soumission
  const [submitting, setSubmitting] = useState(false);
  const [submitError, setSubmitError] = useState<string | null>(null);
  const [submitSuccess, setSubmitSuccess] = useState(false);

  // Effect pour vérifier le statut staff
  React.useEffect(() => {
    let mounted = true;
    (async () => {
      try {
        const res = await fetch('/api/me', { cache: 'no-store', credentials: 'include' });
        if (res.ok) {
          const me = await res.json();
          if (mounted) setIsStaff(!!me?.is_staff);
        }
      } catch (e) {
        // ignore, default is false
      } finally {
        if (mounted) setAuthChecked(true);
      }
    })();
    return () => { mounted = false; };
  }, []);

  // Charger les organisations disponibles pour le staff
  React.useEffect(() => {
    if (!isStaff) return;
    
    (async () => {
      try {
        const res = await fetch("/api/organizations", { credentials: "include", cache: "no-store" });
        if (res.ok) {
          const json: any = await res.json();
          const items = json.items || [];
          setAvailableOrgs(items);
          
          // Auto-sélectionner l'organisation active si disponible
          const meRes = await fetch("/api/me", { credentials: "include", cache: "no-store" });
          if (meRes.ok) {
            const meData = await meRes.json();
            if (meData.active_org_id) {
              const activeOrg = items.find((org: any) => org.id === meData.active_org_id);
              if (activeOrg) {
                setSelectedOrg({ id: activeOrg.id, name: activeOrg.name });
              }
            }
          }
        }
      } catch (e) {
        console.warn('Could not load organizations:', e);
      }
    })();
  }, [isStaff]);

  React.useEffect(() => {
    if (!validateTipOpen) return;
    const onScroll = () => computeValidateTipPos();
    const onResize = () => computeValidateTipPos();
    window.addEventListener('scroll', onScroll, true);
    window.addEventListener('resize', onResize);
    return () => {
      window.removeEventListener('scroll', onScroll, true);
      window.removeEventListener('resize', onResize);
    };
  }, [validateTipOpen, computeValidateTipPos]);

  // Hooks pour CSV
  const csvInputRef = useRef<HTMLInputElement | null>(null);

  const openCSVDialog = useCallback(() => {
    csvInputRef.current?.click();
  }, []);

  const onCSVFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => {
      try {
        const text = String(reader.result || '');
        const matrix = parseCSV(text);
        // On ignore la première ligne (headers)
        const dataRows = matrix.slice(1);
        const newRows = rowsFromMatrix(dataRows);
        if (newRows.length) {
          setRows((prev) => {
            const isFirstRowEmpty = prev.length === 1 && !prev[0].salarie && !prev[0].profession && !prev[0].dateDebut && !prev[0].dateFin && prev[0].salaire === "" && prev[0].nbreCachetsRepresentation === "" && prev[0].nbreServiceRepet === "" && prev[0].heuresSiTechnicien === "" && !prev[0].note;
            if (isFirstRowEmpty) {
              // Remplacer la première ligne vide par la première ligne CSV, puis ajouter le reste
              const [first, ...rest] = newRows;
              return [first, ...rest];
            }
            return [...prev, ...newRows];
          });
        }
      } catch (err) {
        console.error('CSV parse error', err);
      } finally {
        e.target.value = '';
      }
    };
    reader.readAsText(file);
  }, []);

  React.useEffect(() => {
    if (!annexeTipOpen) return;
    const onScroll = () => computeAnnexeTipPos();
    const onResize = () => computeAnnexeTipPos();
    window.addEventListener('scroll', onScroll, true);
    window.addEventListener('resize', onResize);
    return () => {
      window.removeEventListener('scroll', onScroll, true);
      window.removeEventListener('resize', onResize);
    };
  }, [annexeTipOpen, computeAnnexeTipPos]);

  // ==============================================
  // TOUS LES AUTRES HOOKS (AVANT LES CONDITIONS DE RETOUR)
  // ==============================================
  
  const downloadCSVTemplate = useCallback(() => {
    const headers = Array.from(PASTE_ORDER).join(',');
    const blob = new Blob([headers + '\n'], { type: 'text/csv;charset=utf-8;' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'modele_saisie_cddu.csv';
    a.click();
    URL.revokeObjectURL(url);
  }, []);

  // Re-render tick for floating elements (tooltips) on scroll/resize
  const [scrollTick, setScrollTick] = useState(0);
  React.useEffect(() => {
    const onMove = () => setScrollTick((t) => t + 1);
    window.addEventListener('scroll', onMove, true);
    window.addEventListener('resize', onMove);
    return () => {
      window.removeEventListener('scroll', onMove, true);
      window.removeEventListener('resize', onMove);
    };
  }, []);

  const endDateRefs = useRef<Record<string, HTMLInputElement | null>>({});

  // --- Raccourcis clavier & ligne active ------------------------------------
  const [activeRowId, setActiveRowId] = useState<string | null>(null);

  // --- Éditeur de note latéral ------------------------------------------------
  const [noteEditorOpen, setNoteEditorOpen] = useState(false);
  const [noteEditorRowId, setNoteEditorRowId] = useState<string | null>(null);
  const [noteDraft, setNoteDraft] = useState("");
  const [noteAnchor, setNoteAnchor] = useState<{ top: number; left: number; width: number; height: number } | null>(null);
  const [panelPos, setPanelPos] = useState<{ top: number; left: number; width: number } | null>(null);
  const PANEL_W = 360; // largeur du panneau de note

  const openNote = useCallback((rowId: string, current: string, anchorEl: HTMLElement) => {
    const r = anchorEl.getBoundingClientRect();
    setNoteAnchor({ top: r.top + window.scrollY, left: r.left + window.scrollX, width: r.width, height: r.height });
    setPanelPos({ top: r.top + window.scrollY, left: Math.max(16, window.scrollX + window.innerWidth - PANEL_W - 16), width: PANEL_W });
    setNoteDraft(current || "");
    setNoteEditorRowId(rowId);
    setNoteEditorOpen(true);
  }, []);

  const closeNote = useCallback(() => {
    setNoteEditorOpen(false);
    setNoteEditorRowId(null);
    setNoteDraft("");
    setNoteAnchor(null);
    setPanelPos(null);
  }, []);

  // Maintenir la position du panneau et de la flèche au scroll/resize
  React.useEffect(() => {
    if (!noteEditorOpen || !noteAnchor) return;
    const onScroll = () => {
      setPanelPos((prev) => ({ top: noteAnchor.top - window.scrollY + window.scrollY, left: Math.max(16, window.scrollX + window.innerWidth - PANEL_W - 16), width: PANEL_W }));
    };
    const onResize = () => {
      setPanelPos({ top: noteAnchor.top, left: Math.max(16, window.scrollX + window.innerWidth - PANEL_W - 16), width: PANEL_W });
    };
    window.addEventListener('scroll', onScroll, true);
    window.addEventListener('resize', onResize);
    return () => {
      window.removeEventListener('scroll', onScroll, true);
      window.removeEventListener('resize', onResize);
    };
  }, [noteEditorOpen, noteAnchor]);

  const moveFocusVertical = useCallback((delta: -1 | 1) => {
    const activeEl = document.activeElement as HTMLElement | null;
    const field = (activeEl?.dataset?.field as keyof ContractRow | undefined) || undefined;
    if (!field) return;
    const idx = rows.findIndex(r => r.id === activeRowId);
    if (idx === -1) return;

    // Wrap intelligent (option 1) : si on est sur la dernière ligne et qu'on descend, créer une nouvelle ligne
    if (delta === 1 && idx === rows.length - 1) {
      const newRow = emptyRow();
      setRows(prev => [...prev, newRow]);
      setActiveRowId(newRow.id);
      setTimeout(() => focusCell(newRow.id, field), 0);
      return;
    }

    const targetIdx = Math.max(0, Math.min(rows.length - 1, idx + delta));
    const target = rows[targetIdx];
    if (target) {
      setActiveRowId(target.id);
      setTimeout(() => focusCell(target.id, field), 0);
    }
  }, [rows, activeRowId, focusCell]);

  const getRowIndexById = useCallback((id: string) => rows.findIndex(r => r.id === id), [rows]);

  const duplicateRowById = useCallback((id: string) => {
    setRows(prev => {
      const idx = prev.findIndex(r => r.id === id);
      if (idx === -1) return prev;
      const { id: _omit, selected: _s, ...rest } = prev[idx] as any;
      const dup = emptyRow(rest);
      const next = [...prev.slice(0, idx + 1), dup, ...prev.slice(idx + 1)];
      return next;
    });
  }, []);

  const addEmptyRowAfterId = useCallback((id: string | null) => {
    setRows(prev => {
      const idx = id ? prev.findIndex(r => r.id === id) : prev.length - 1;
      const insertAt = idx >= 0 ? idx + 1 : prev.length;
      const next = [...prev.slice(0, insertAt), emptyRow(), ...prev.slice(insertAt)];
      return next;
    });
  }, []);

  const deleteRowById = useCallback((id: string) => {
    setRows(prev => {
      const idx = prev.findIndex(r => r.id === id);
      if (idx === -1) return prev;
      const next = prev.filter(r => r.id !== id);
      // repositionner la ligne active sur la suivante si possible
      const nextIdx = Math.min(idx, next.length - 1);
      setActiveRowId(next[nextIdx]?.id ?? null);
      return next.length ? next : [emptyRow()];
    });
  }, []);

  // Raccourcis globaux
  React.useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      const tag = (e.target as HTMLElement)?.tagName?.toLowerCase();
      const isTyping = tag === 'input' || tag === 'select' || tag === 'textarea';
      if (!(e.ctrlKey && e.shiftKey) || e.metaKey) return;

      if (e.key.toLowerCase() === 'd') {
        e.preventDefault();
        if (activeRowId) duplicateRowById(activeRowId);
        return;
      }
      if (e.key.toLowerCase() === 'n') {
        e.preventDefault();
        addEmptyRowAfterId(activeRowId);
        return;
      }
      if (e.key.toLowerCase() === 'x') {
        e.preventDefault();
        if (activeRowId) deleteRowById(activeRowId);
        return;
      }
      if (e.key === 'ArrowUp') {
        e.preventDefault();
        moveFocusVertical(-1);
        return;
      }
      if (e.key === 'ArrowDown') {
        e.preventDefault();
        moveFocusVertical(1);
        return;
      }
    };
    window.addEventListener('keydown', onKey, { capture: true });
    return () => window.removeEventListener('keydown', onKey, { capture: true });
  }, [activeRowId, duplicateRowById, addEmptyRowAfterId, deleteRowById, moveFocusVertical]);

  // Validation globale
  const anyErrors = useMemo(() => rows.some(r => Object.keys(validateRow(r)).length > 0), [rows]);
  const hasAnyData = useMemo(() => rows.some(r => (
    !!r.salarie || !!r.profession || !!r.dateDebut || !!r.dateFin || r.salaire !== "" || r.nbreCachetsRepresentation !== "" || r.nbreServiceRepet !== "" || r.heuresSiTechnicien !== "" || !!r.note
  )), [rows]);
  const canSubmit = hasAnyData && !anyErrors;

  // handleSubmit comme useCallback
  const handleSubmit = useCallback(async () => {
    if (!canSubmit) return;
    
    // Validation pour staff : une organisation doit être sélectionnée
    if (isStaff && !selectedOrg) {
      setSubmitError("Veuillez sélectionner une organisation");
      return;
    }
    
    setSubmitting(true);
    setSubmitError(null);
    
    try {
      const validRows = rows.filter(r => 
        r.salarie && r.profession && r.dateDebut && r.dateFin && 
        (r.salaire !== "" || r.typeSalaire === "MINIMA")
      );
      
      if (validRows.length === 0) {
        throw new Error("Aucun contrat valide à soumettre");
      }
      
      // Créer chaque contrat individuellement
      const results = [];
      for (const row of validRows) {
        // Extraire le matricule depuis le format "MATRICULE | Nom" ou utiliser le nom complet
        const salarieData = row.salarie.includes(' | ')
          ? row.salarie.split(' | ')
          : [row.salarie, row.salarie];
        const [matriculeRaw = "", nomRaw = ""] = salarieData;
        const salarieMatricule = matriculeRaw.trim();
        const salarieNomComplet = (nomRaw || matriculeRaw).trim();
        
        console.log('🔍 [FRONTEND DEBUG] Row salarie:', row.salarie);
        console.log('🔍 [FRONTEND DEBUG] SalarieData split:', salarieData);
        console.log('🔍 [FRONTEND DEBUG] Matricule raw:', matriculeRaw);
        console.log('🔍 [FRONTEND DEBUG] Nom raw:', nomRaw);
        console.log('🔍 [FRONTEND DEBUG] Matricule final:', salarieMatricule);
        console.log('🔍 [FRONTEND DEBUG] Nom final:', salarieNomComplet);
        console.log('🔍 [FRONTEND DEBUG] Selected org:', selectedOrg);
        console.log('🔍 [FRONTEND DEBUG] Is staff:', isStaff);
        
        const contractData = {
          salarie_matricule: salarieMatricule || salarieNomComplet,
          salarie_nom: salarieNomComplet,
          salarie_email: null,
          send_email_confirmation: false,
          regime: "CDDU_MONO",
          production_id: production?.id || null,
          spectacle: production?.nom || "Production",
          numero_objet: production?.numero_objet || null,
          profession_label: row.profession.split(' — ')[0] || row.profession,
          profession_code: row.profession.split(' — ')[1] || null,
          categorie: row.role === "ARTISTE" ? "Artiste" : "Technicien",
          date_debut: row.dateDebut,
          date_fin: row.dateFin,
          nb_representations: row.role === "ARTISTE" ? (row.nbreCachetsRepresentation || 0) : 0,
          nb_services_repetition: row.role === "ARTISTE" ? (row.nbreServiceRepet || 0) : 0,
          heures_total: row.role === "TECHNICIEN" ? (row.heuresSiTechnicien || 0) : 0,
          minutes_total: 0,
          type_salaire: (() => {
            switch (row.typeSalaire) {
              case "NET_AVT_PAS": return "Net avant PAS";
              case "CTE": return "Coût total employeur";
              case "MINIMA": return "Minimum conventionnel";
              default: return "Brut";
            }
          })(),
          montant: row.typeSalaire !== "MINIMA" ? Number(row.salaire) : undefined,
          panier_repas: "Non",
          reference: generateReference(),
          notes: row.note || undefined,
          org_id: isStaff ? selectedOrg?.id : null,
        };
        
        console.log('🔍 [FRONTEND DEBUG] Contract data à envoyer:', contractData);
        
        const response = await fetch("/api/cddu-contracts", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(contractData)
        });
        
        console.log('🔍 [FRONTEND DEBUG] Response status:', response.status);
        console.log('🔍 [FRONTEND DEBUG] Response ok:', response.ok);
        
        if (!response.ok) {
          const errorData = await response.json().catch(() => ({}));
          console.log('🔍 [FRONTEND DEBUG] Error data:', errorData);
          throw new Error(errorData.error || `Erreur lors de la création du contrat pour ${row.salarie}`);
        }
        
        const result = await response.json();
        console.log('🔍 [FRONTEND DEBUG] Success result:', result);
        results.push(result);
      }
      
      setSubmitSuccess(true);
      setTimeout(() => {
        window.location.href = "/contrats";
      }, 2000);
      
    } catch (error: any) {
      setSubmitError(error.message || "Erreur lors de la soumission");
    } finally {
      setSubmitting(false);
    }
  }, [canSubmit, isStaff, selectedOrg, rows, production]);

  const totals = useMemo(() => {
    const totalSalaire = rows.reduce((acc, r) => acc + (Number(r.salaire) || 0), 0);
    const totalCachets = rows.reduce((acc, r) => acc + (r.role === "ARTISTE" ? Number(r.nbreCachetsRepresentation) || 0 : 0), 0);
    const totalServices = rows.reduce((acc, r) => acc + (r.role === "ARTISTE" ? Number(r.nbreServiceRepet) || 0 : 0), 0);
    const totalHeures = rows.reduce((acc, r) => acc + (r.role === "TECHNICIEN" ? Number(r.heuresSiTechnicien) || 0 : 0), 0);
    return { totalSalaire, totalCachets, totalServices, totalHeures };
  }, [rows]);

  // Tous les autres useCallback DOIVENT être ici AVANT les conditions de retour
  const addRow = useCallback((preset?: Partial<ContractRow>) => {
    setRows((prev) => [...prev, emptyRow(preset)]);
  }, []);

  const duplicateSelected = useCallback(() => {
    setRows((prev) => {
      const dups: ContractRow[] = [];
      prev.forEach((r) => {
        if (r.selected) {
          const { id: _omit, selected: _s, ...rest } = r as any;
          dups.push(emptyRow(rest));
        }
      });
      if (dups.length === 0) return prev;
      return [...prev, ...dups];
    });
  }, []);

  const deleteSelected = useCallback(() => {
    setRows((prev) => prev.filter((r) => !r.selected));
  }, []);

  const clearAll = useCallback(() => {
    setRows([emptyRow()]);
  }, []);

  const handlePasteBulk = useCallback(() => {
    pasteTargetRef.current?.focus();
    pasteTargetRef.current?.select();
  }, []);

  const onPaste = useCallback((e: React.ClipboardEvent<HTMLTextAreaElement>) => {
    const text = e.clipboardData.getData("text");
    if (!text) return;
    e.preventDefault();
    const parsed = parsePasted(text);
    if (parsed.length === 0) return;

    const newRows: ContractRow[] = parsed.map((cols) => {
      const base = emptyRow();
      const assigned: Partial<ContractRow> = {};
      PASTE_ORDER.forEach((key, i) => {
        const val = cols[i];
        if (val == null) return;
        switch (key) {
          case "role":
            assigned.role = /^tech/i.test(val) ? "TECHNICIEN" : "ARTISTE";
            break;
          case "typeSalaire":
            if (/^min/i.test(val)) assigned.typeSalaire = "MINIMA";
            else if (/^net/i.test(val)) assigned.typeSalaire = "NET_AVT_PAS";
            else if (/^cte/i.test(val)) assigned.typeSalaire = "CTE";
            else assigned.typeSalaire = "BRUT";
            break;
          case "dateDebut":
            (assigned as any)[key] = normalizeDate(val);
            break;
          case "dateFin":
            (assigned as any)[key] = normalizeDate(val);
            break;
          case "salaire":
          case "nbreCachetsRepresentation":
          case "nbreServiceRepet":
          case "heuresSiTechnicien":
            (assigned as any)[key] = val === "" ? "" : Number(val.replace(",", "."));
            break;
          default:
            (assigned as any)[key] = val;
        }
      });
      return { ...base, ...assigned } as ContractRow;
    });

    setRows((prev) => [...prev, ...newRows]);
  }, []);

  const updateCell = useCallback((id: string, field: keyof ContractRow, value: any) => {
    setRows((prev) =>
      prev.map((r) =>
        r.id === id
          ? (() => {
              const next: ContractRow = { ...r, [field]: value } as any;
              if (field === "role") {
                if (value === "ARTISTE") next.heuresSiTechnicien = "";
                if (value === "TECHNICIEN") {
                  next.nbreCachetsRepresentation = "";
                  next.nbreServiceRepet = "";
                }
              }
              if (field === "typeSalaire") {
                if (value === "MINIMA") next.salaire = "";
              }
              return next;
            })()
          : r
      )
    );
  }, []);

  const saveNote = useCallback(() => {
    if (!noteEditorRowId) return;
    updateCell(noteEditorRowId, 'note', noteDraft);
    closeNote();
  }, [noteEditorRowId, noteDraft, updateCell, closeNote]);

  const exportJSON = useCallback(() => {
    const payload = { production, contrats: rows };
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `saisie_cddu_${new Date().toISOString().slice(0, 10)}.json`;
    a.click();
    URL.revokeObjectURL(url);
  }, [production, rows]);

  // Conditions de retour APRÈS tous les hooks
  if (!authChecked) {
    return <div className="p-6 text-sm text-slate-500">Chargement…</div>;
  }
  if (!isStaff) {
    return (
      <div className="p-6">
        <AccessDeniedCard
          title="Accès réservé au staff"
          message="Cette page n'est pas encore disponible pour les clients."
          hint="Cette fonction sera bientôt disponible."
        />
      </div>
    );
  }

  // Fonctions d'aide (pas des hooks)

  const onKeyDownCell = (
    e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
    row: ContractRow,
    field: keyof ContractRow
  ) => {
    // Sauter les segments internes du <input type="date"> :
    if ((field === 'dateDebut' || field === 'dateFin') && e.key === 'Tab' && !e.ctrlKey && !e.metaKey && !e.altKey) {
      e.preventDefault();
      const idx = COLUMN_ORDER.indexOf(field as any);
      const nextIdx = e.shiftKey ? Math.max(0, idx - 1) : Math.min(COLUMN_ORDER.length - 1, idx + 1);
      const nextField = COLUMN_ORDER[nextIdx] as keyof ContractRow;
      setTimeout(() => focusCell(row.id, nextField), 0);
      return;
    }
  };

  return (
    <div className="space-y-3">
      <header className={`flex items-center justify-between ${headerPad}`}>
        <div className="space-y-1">
          <h1 className="text-[18px] font-semibold tracking-tight">Saisie CDDU en tableau</h1>
          <p className="text-[12px] text-muted-foreground">
            
          </p>
        </div>
        <div className="flex gap-1.5" ref={validateWrapRef}
             onMouseEnter={() => { if (!canSubmit) { setValidateTipOpen(true); computeValidateTipPos(); } }}
             onMouseLeave={() => setValidateTipOpen(false)}
        >
          <button
            className="inline-flex items-center gap-2 rounded-lg px-4 py-2 bg-emerald-600 text-white shadow hover:bg-emerald-700 transition text-[14px] h-10 text-base font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
            type="button"
            disabled={!canSubmit || submitting}
            onClick={handleSubmit}
          >
            {submitting ? (
              <>
                <svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
                  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"/>
                  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
                </svg>
                Création en cours...
              </>
            ) : (
              "Valider la saisie"
            )}
          </button>
        </div>
        
        {/* Messages d'erreur et succès */}
        {submitError && (
          <div className="mt-3 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
            {submitError}
          </div>
        )}
        {submitSuccess && (
          <div className="mt-3 p-3 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">
            ✅ Contrats créés avec succès ! Redirection vers la liste des contrats...
          </div>
        )}
        {validateTipOpen && !canSubmit && validateTipPos && createPortal(
          (() => {
            const top = validateTipPos.top - 10; // au-dessus
            const left = validateTipPos.left;
            return (
              <div className="z-[1200]" style={{ position: 'fixed', top, left, transform: 'translate(-50%, -100%)' }}>
                <div className="inline-block max-w-[300px] rounded-md border border-amber-400 bg-amber-50 text-amber-900 text-[12px] px-2 py-1 shadow">
                  Veuillez d'abord corriger les erreurs.
                </div>
                <div className="mx-auto w-0 h-0" style={{ borderLeft: '8px solid transparent', borderRight: '8px solid transparent', borderTop: '8px solid #f59e0b' }} />
              </div>
            );
          })(),
          document.body
        )}
      </header>

      {annexeTipOpen && annexeTipPos && createPortal(
        (() => {
          const top = annexeTipPos.top - 10;
          const left = annexeTipPos.left;
          return (
            <div className="z-[1200]" style={{ position: 'fixed', top, left, transform: 'translate(-50%, -100%)' }}>
              <div className="inline-block rounded-md border border-slate-300 bg-white text-slate-800 text-[12px] px-2 py-1 shadow">
                <div>Annexe 8 = Technicien</div>
                <div>Annexe 10 = Artiste</div>
              </div>
              <div className="mx-auto w-0 h-0" style={{ borderLeft: '8px solid transparent', borderRight: '8px solid transparent', borderTop: '8px solid #e5e7eb' }} />
            </div>
          );
        })(),
        document.body
      )}

      <section className={`grid grid-cols-1 md:grid-cols-3 gap-2 items-end ${headerPad}`}>
        {isStaff && (
          <div className="col-span-full mb-3">
            <label className="text-[12px] font-medium text-amber-700">
              Organisation (obligatoire pour les utilisateurs staff)
            </label>
            <select
              value={selectedOrg?.id || ""}
              onChange={(e) => {
                const org = availableOrgs.find(o => o.id === e.target.value);
                setSelectedOrg(org ? { id: org.id, name: org.name } : null);
              }}
              className="mt-1 w-full rounded-md border px-3 py-2 text-sm bg-white"
            >
              <option value="" disabled>Sélectionner une organisation…</option>
              {availableOrgs.map(org => (
                <option key={org.id} value={org.id}>
                  {org.name}
                </option>
              ))}
            </select>
            {isStaff && !selectedOrg && (
              <p className="text-xs text-amber-600 mt-1">
                Une organisation doit être sélectionnée avant de pouvoir valider la saisie.
              </p>
            )}
          </div>
        )}
        <div className="col-span-2">
          <label className="text-[12px] font-medium">Production (commune à toutes les lignes)</label>
          <ComboBox
            value={production}
            onChange={(v) => {
              if (typeof v === 'string') {
                // Quand l'utilisateur tape, on crée un objet temporaire avec le nom
                setProduction({ nom: v, numero_objet: null });
              } else {
                setProduction(v);
              }
            }}
            onSelect={(v) => {
              if (typeof v === 'object' && v !== null) {
                setProduction(v);
              } else if (typeof v === 'string' && v.trim()) {
                // Si l'utilisateur tape une nouvelle production, on crée un objet temporaire
                setProduction({ nom: v.trim(), numero_objet: null });
              } else {
                setProduction(null);
              }
            }}
            placeholder="Rechercher ou saisir une production…"
            searchType="productions"
            className="mt-1"
          />
        </div>
        <div className="flex gap-1.5">
          <button onClick={() => addRow()} className="rounded-lg px-3 py-1 border shadow-sm hover:shadow transition text-[13px] h-8">
            + Ligne
          </button>
          <button onClick={duplicateSelected} className="rounded-lg px-3 py-1 border shadow-sm hover:shadow transition text-[13px] h-8">
            Dupliquer
          </button>
          <button onClick={deleteSelected} className="rounded-lg px-3 py-1 border shadow-sm hover:shadow transition text-[13px] h-8">
            Supprimer
          </button>
        </div>
      </section>

      <section className={`flex flex-wrap items-center gap-2 text-[12px] ${headerPad}`}>
        <button onClick={openCSVDialog} className="rounded-lg px-2.5 py-1 border shadow-sm hover:shadow transition h-8">
          Import CSV
        </button>
        <button onClick={downloadCSVTemplate} className="rounded-lg px-2.5 py-1 border shadow-sm hover:shadow transition h-8">
          Télécharger le modèle CSV
        </button>
        <span className="text-muted-foreground">
          Vous pouvez exporter un CSV depuis Excel/Sheets et l'importer ici.
        </span>
        <input ref={csvInputRef} type="file" accept=".csv,text/csv" className="hidden" onChange={onCSVFileChange} />
      </section>

      <div className={`overflow-auto rounded-xl border ${mainPad}`}>
        <table className="min-w-[1000px] w-full table-fixed text-[13px]">
          <thead className="bg-muted/50">
            <tr>
              <Th w="20px">S</Th>
              <Th w="160px">Salarie</Th>
              <Th w="75px">
                <span className="inline-flex items-center gap-1">
                  Annexe
                  <span
                    ref={annexeIconRef}
                    className="ml-1 inline-flex items-center justify-center w-4 h-4 rounded-full border text-[10px] text-slate-600 bg-white cursor-help"
                    onMouseEnter={(e) => { if (e.currentTarget) openAnnexeTipFromEl(e.currentTarget); }}
                    onMouseLeave={() => setAnnexeTipOpen(false)}
                    aria-label="Aide sur l'annexe"
                  >
                    ?
                  </span>
                </span>
              </Th>
              <Th w="160px">Profession</Th>
              <Th w="120px">Début</Th>
              <Th w="120px">Fin</Th>
              <Th w="120px">Type salaire</Th>
              <Th w="80px">Salaire (€)</Th>
              <Th w="64px">Cachets repr.</Th>
              <Th w="64px">Services rép.</Th>
              <Th w="64px">Heures (tech.)</Th>
              <Th>Note</Th>
            </tr>
          </thead>
          <tbody>
            {rows.map((row) => {
              const errors = validateRow(row);
              scrollTick; // re-render anchor positions on scroll/resize
              return (
                <tr
                  key={row.id}
                  onFocusCapture={() => setActiveRowId(row.id)}
                  className={`relative transition-colors odd:bg-background even:bg-muted/20 ${row.id === activeRowId ? 'bg-emerald-50/30 ring-1 ring-emerald-400/40' : ''}`}
                >
                  <Td>
                    <div className="relative">
                      {row.id === activeRowId && (
                        <span className="absolute left-[-6px] top-[-6px] bottom-[-6px] w-[3px] rounded-full bg-emerald-500" aria-hidden />
                      )}
                      <input
                        type="checkbox"
                        checked={!!row.selected}
                        onChange={(e) => updateCell(row.id, "selected" as any, e.target.checked)}
                        className="mx-auto h-4 w-4"
                      />
                    </div>
                  </Td>

                  <Td>
                    <ComboBox
                      value={row.salarie.includes(' | ') ? row.salarie.split(' | ')[1] : row.salarie}
                      onChange={(v) => updateCell(row.id, "salarie", v)}
                      onSelect={(v) => updateCell(row.id, "salarie", v)}
                      placeholder="Rechercher un salarié…"
                      searchType="salaries"
                      inputProps={{ 'data-row': row.id, 'data-field': 'salarie' as any } as any}
                      error={!!errors.salarie}
                    />
                  </Td>

                  <Td>
                    <select
                      value={row.role}
                      onKeyDown={(e) => onKeyDownCell(e, row, "role")}
                      onChange={(e) => updateCell(row.id, "role", e.target.value as RoleType)}
                      className={selectCls}
                      data-row={row.id}
                      data-field="role"
                      onFocus={(e) => openAnnexeTipFromEl(e.currentTarget)}
                      onMouseEnter={(e) => openAnnexeTipFromEl(e.currentTarget)}
                      onBlur={() => setAnnexeTipOpen(false)}
                      onMouseLeave={() => setAnnexeTipOpen(false)}
                    >
                      <option value="ARTISTE">A10</option>
                      <option value="TECHNICIEN">A8</option>
                    </select>
                  </Td>

                  <Td>
                    {row.role === 'ARTISTE' ? (
                      <ComboBox
                        value={row.profession}
                        onChange={(v) => updateCell(row.id, 'profession', typeof v === 'string' ? v : String(v || ''))}
                        onSelect={(v) => {
                          const strValue = typeof v === 'string' ? v : String(v || '');
                          const label = strValue.includes(' — ') ? strValue.split(' — ')[0] : strValue;
                          updateCell(row.id, 'profession', label);
                        }}
                        placeholder="Rechercher une profession (Artiste)…"
                        searchType="professionsArtiste"
                        inputProps={{ 'data-row': row.id, 'data-field': 'profession' as any } as any}
                        error={!!errors.profession}
                      />
                    ) : (
                      <ComboBox
                        value={row.profession}
                        onChange={(v) => updateCell(row.id, 'profession', typeof v === 'string' ? v : String(v || ''))}
                        onSelect={(v) => {
                          const strValue = typeof v === 'string' ? v : String(v || '');
                          const label = strValue.includes(' — ') ? strValue.split(' — ')[0] : strValue;
                          updateCell(row.id, 'profession', label);
                        }}
                        placeholder="Rechercher une profession (Technicien)…"
                        searchType="professionsTechnicien"
                        inputProps={{ 'data-row': row.id, 'data-field': 'profession' as any } as any}
                        error={!!errors.profession}
                      />
                    )}
                  </Td>

                  <Td>
                    <input
                      type="date"
                      value={row.dateDebut}
                      onKeyDown={(e) => onKeyDownCell(e, row, "dateDebut")}
                      onChange={(e) => updateCell(row.id, "dateDebut", e.target.value)}
                      className={inputCls(!!errors.dateDebut)}
                      data-row={row.id}
                      data-field="dateDebut"
                    />
                  </Td>
                  <Td>
                    <div className="relative">
                      <input
                        ref={(el) => { endDateRefs.current[row.id] = el; }}
                        type="date"
                        value={row.dateFin}
                        onKeyDown={(e) => onKeyDownCell(e, row, "dateFin")}
                        onChange={(e) => updateCell(row.id, "dateFin", e.target.value)}
                        className={inputCls(!!errors.dateFin)}
                        data-row={row.id}
                        data-field="dateFin"
                      />
                      {errors.dateFin === 'MULTI_MOIS' && endDateRefs.current[row.id] && createPortal(
                        (() => {
                          const r = endDateRefs.current[row.id]!.getBoundingClientRect();
                          const top = r.top + window.scrollY - 8; // place au-dessus
                          const left = r.left + window.scrollX;
                          const width = Math.min(320, Math.max(220, r.width + 40));
                          return (
                            <div className="z-[1200]" style={{ position: 'fixed', top, left, width, transform: 'translateY(-100%)' }}>
                              <div className="inline-block w-full rounded-md border border-amber-400 bg-amber-50 text-amber-900 text-[12px] px-2 py-1 shadow">
                                La création d'un contrat multi-mois n'est pour l'instant pas possible en saisie tableau, merci d'utiliser le formulaire unique.
                              </div>
                              <div className="ml-4 w-0 h-0 border-l-8 border-l-transparent border-r-8 border-r-transparent" style={{ borderTopWidth: 8, borderTopColor: '#f59e0b' }} />
                            </div>
                          );
                        })(),
                        document.body
                      )}
                    </div>
                  </Td>

                  <Td>
                    <select
                      value={row.typeSalaire}
                      onKeyDown={(e) => onKeyDownCell(e, row, "typeSalaire")}
                      onChange={(e) => updateCell(row.id, "typeSalaire", e.target.value as SalaryType)}
                      className={selectCls}
                      data-row={row.id}
                      data-field="typeSalaire"
                    >
                      <option value="BRUT">Brut</option>
                      <option value="NET_AVT_PAS">Net avt PAS</option>
                      <option value="CTE">CTE</option>
                      <option value="MINIMA">Minima</option>
                    </select>
                  </Td>

                  <Td>
                    <input
                      type="number"
                      step="0.01"
                      placeholder="0.00"
                      value={row.salaire}
                      onKeyDown={(e) => onKeyDownCell(e, row, "salaire")}
                      onChange={(e) => updateCell(row.id, "salaire", e.target.value === "" ? "" : Number(e.target.value))}
                      className={numberCls(!!errors.salaire, row.typeSalaire === "MINIMA")}
                      disabled={row.typeSalaire === "MINIMA"}
                      data-row={row.id}
                      data-field="salaire"
                    />
                  </Td>

                  <Td>
                    <input
                      type="number"
                      min={0}
                      max={99}
                      placeholder="0"
                      value={row.nbreCachetsRepresentation}
                      inputMode="numeric"
                      onKeyDown={(e) => onKeyDownCell(e, row, "nbreCachetsRepresentation")}
                      onChange={(e) => {
                        const v = e.target.value;
                        if (v === "") return updateCell(row.id, "nbreCachetsRepresentation", "");
                        const n = Math.min(99, Math.max(0, Number(v)));
                        updateCell(row.id, "nbreCachetsRepresentation", n);
                      }}
                      disabled={row.role !== "ARTISTE"}
                      className={numberCls(!!errors.nbreCachetsRepresentation, row.role !== "ARTISTE") + " text-center"}
                      data-row={row.id}
                      data-field="nbreCachetsRepresentation"
                    />
                  </Td>
                  <Td>
                    <input
                      type="number"
                      min={0}
                      max={99}
                      placeholder="0"
                      value={row.nbreServiceRepet}
                      inputMode="numeric"
                      onKeyDown={(e) => onKeyDownCell(e, row, "nbreServiceRepet")}
                      onChange={(e) => {
                        const v = e.target.value;
                        if (v === "") return updateCell(row.id, "nbreServiceRepet", "");
                        const n = Math.min(99, Math.max(0, Number(v)));
                        updateCell(row.id, "nbreServiceRepet", n);
                      }}
                      disabled={row.role !== "ARTISTE"}
                      className={numberCls(!!errors.nbreServiceRepet, row.role !== "ARTISTE") + " text-center"}
                      data-row={row.id}
                      data-field="nbreServiceRepet"
                    />
                  </Td>

                  <Td>
                    <input
                      type="number"
                      min={0}
                      max={99}
                      placeholder="0"
                      value={row.heuresSiTechnicien}
                      inputMode="numeric"
                      onKeyDown={(e) => onKeyDownCell(e, row, "heuresSiTechnicien")}
                      onChange={(e) => {
                        const v = e.target.value;
                        if (v === "") return updateCell(row.id, "heuresSiTechnicien", "");
                        const n = Math.min(99, Math.max(0, Number(v)));
                        updateCell(row.id, "heuresSiTechnicien", n);
                      }}
                      disabled={row.role !== "TECHNICIEN"}
                      className={numberCls(!!errors.heuresSiTechnicien, row.role !== "TECHNICIEN") + " text-center"}
                      data-row={row.id}
                      data-field="heuresSiTechnicien"
                    />
                  </Td>

                  <Td>
                    <button
                      onClick={(e) => openNote(row.id, row.note, e.currentTarget as HTMLElement)}
                      className="inline-flex items-center gap-2 px-2 py-1 h-8 rounded-md border shadow-sm hover:shadow transition text-[13px] bg-white"
                      data-row={row.id}
                      data-field="note"
                    >
                      <span className="inline-flex items-center justify-center w-6 h-6 rounded-full border bg-white">
                        {row.note ? <span className="text-[11px] font-medium">1</span> : <span aria-hidden>📝</span>}
                      </span>
                    </button>
                  </Td>
                </tr>
              );
            })}
          </tbody>
          <tfoot>
            <tr className="bg-muted/30">
              <Td colSpan={7}>
                <div className="text-[12px] text-muted-foreground">
                  <div className="flex flex-wrap gap-x-4 gap-y-1">
                    <span>
                      Raccourcis ligne : <kbd className="px-1 border rounded">Ctrl</kbd> + <kbd className="px-1 border rounded">Shift</kbd> + <kbd className="px-1 border rounded">D</kbd> = Dupliquer
                    </span>
                    <span>
                      <kbd className="px-1 border rounded">Ctrl</kbd> + <kbd className="px-1 border rounded">Shift</kbd> + <kbd className="px-1 border rounded">N</kbd> = Nouvelle ligne
                    </span>
                    <span>
                      <kbd className="px-1 border rounded">Ctrl</kbd> + <kbd className="px-1 border rounded">Shift</kbd> + <kbd className="px-1 border rounded">X</kbd> = Supprimer
                    </span>
                    <span>
                      <kbd className="px-1 border rounded">Ctrl</kbd> + <kbd className="px-1 border rounded">Shift</kbd> + <kbd className="px-1 border rounded">↑/↓</kbd> = Déplacer le focus ligne (même colonne)
                    </span>
                  </div>
                </div>
              </Td>
              <Td className="text-right font-medium">{totals.totalSalaire.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</Td>
              <Td className="text-right font-medium">{totals.totalCachets}</Td>
              <Td className="text-right font-medium">{totals.totalServices}</Td>
              <Td className="text-right font-medium">{totals.totalHeures}</Td>
              <Td>{/* empty cell */}</Td>
            </tr>
          </tfoot>
        </table>
      </div>

      <div className={`flex items-center justify-between text-[12px] text-muted-foreground ${mainPad}`}>
        <div>
          Lignes : <span className="font-medium text-foreground">{rows.length}</span>
        </div>
        <div className="flex gap-1.5">
          <button
            onClick={() => addRow(rows[rows.length - 1])}
            className="rounded-lg px-2.5 py-1 border shadow-sm hover:shadow transition text-[13px] h-8"
          >
            + Copier la dernière
          </button>
          <button onClick={() => addRow()} className="rounded-lg px-2.5 py-1 border shadow-sm hover:shadow transition text-[13px] h-8">
            + Ligne vide
          </button>
        </div>
      </div>
      {noteEditorOpen && panelPos && createPortal(
        <div className="z-[1100]" style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
          {/* Flèche (ligne + pointe) */}
          {noteAnchor ? (
            <svg width="100%" height="100%" style={{ position: 'absolute', top: 0, left: 0 }}>
              {(() => {
                const startX = noteAnchor.left + noteAnchor.width + 6;
                const startY = noteAnchor.top + noteAnchor.height / 2;
                const endX = panelPos.left;
                const endY = panelPos.top + 16; // pointe vers le header du panneau
                return (
                  <g stroke="#10b981" strokeWidth="2" fill="none">
                    <path d={`M ${startX} ${startY} C ${startX + 40} ${startY}, ${endX - 40} ${endY}, ${endX} ${endY}`} />
                    <polygon points={`${endX},${endY} ${endX - 8},${endY - 6} ${endX - 8},${endY + 6}`} fill="#10b981" />
                  </g>
                );
              })()}
            </svg>
          ) : null}

          {/* Panneau latéral */}
          <div
            className="rounded-l-xl border border-slate-200 bg-white shadow-xl"
            style={{ position: 'fixed', top: panelPos.top, left: panelPos.left, width: panelPos.width, pointerEvents: 'auto' }}
            tabIndex={-1}
            onKeyDown={(e) => { if (e.key === 'Escape') { e.preventDefault(); closeNote(); } }}
          >
            <div className="flex items-center justify-between px-3 py-2 border-b bg-emerald-50">
              <div className="text-sm font-medium">Note de la ligne</div>
              <button onClick={closeNote} className="text-sm text-slate-600 hover:underline">Fermer</button>
            </div>
            <div className="p-3">
              <p className="text-xs text-slate-500 mb-2">Ajoutez une note pour ce contrat, si vous souhaitez nous apporter toute précision que vous jugez utile.</p>
              <textarea
                value={noteDraft}
                onChange={(e) => setNoteDraft(e.target.value)}
                onKeyDown={(e) => {
                  if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); saveNote(); }
                  if (e.key === 'Escape') { e.preventDefault(); closeNote(); }
                }}
                className="w-full h-40 text-[13px] border rounded-md p-2"
                placeholder="Votre note…"
                autoFocus
              />
              <div className="mt-3 flex items-center justify-between gap-2">
                <div className="text-[11px] text-slate-500">Astuce : <kbd className="px-1 border rounded">Shift</kbd> + <kbd className="px-1 border rounded">Entrée</kbd> pour valider · <kbd className="px-1 border rounded">Esc</kbd> pour fermer et annuler</div>
                <div className="flex gap-2">
                  <button onClick={closeNote} className="px-3 py-1 h-8 rounded-md border">Annuler</button>
                  <button onClick={saveNote} className="px-3 py-1 h-8 rounded-md bg-emerald-600 text-white hover:bg-emerald-700">Valider</button>
                </div>
              </div>
            </div>
          </div>
        </div>,
        document.body
      )}
    </div>
  );
}

function Th({ children, w }: { children: React.ReactNode; w?: string }) {
  return (
    <th style={{ width: w }} className={thCls}>
      {children}
    </th>
  );
}

function Td({
  children,
  colSpan,
  className = "",
}: {
  children?: React.ReactNode;
  colSpan?: number;
  className?: string;
}) {
  return (
    <td colSpan={colSpan} className={`${tdCls} ${className}`}>
      {children}
    </td>
  );
}