// DsiOpex.tsx — OPEX DSI : vignettes par catégorie, CRUD lignes, persistance BDD via tRPC
// Migré depuis localStorage vers tRPC + MySQL

import React, { useState, useMemo, useEffect, useCallback } from 'react';
import {
  TrendingDown,
  TrendingUp,
  Search,
  ChevronDown,
  ChevronUp,
  Info,
  BarChart3,
  Building2,
  Euro,
  Filter,
  Eye,
  EyeOff,
  ArrowUpDown,
  Lock,
  CheckCircle2,
  PencilLine,
  RotateCcw,
  Plus,
  Pencil,
  Trash2,
  Loader2,
  Key,
  Upload,
  AlertCircle,
} from 'lucide-react';
import { AppSidebar } from '../components/AppSidebar';
import { AnneeSelectorBar } from '../components/AnneeSelectorBar';
import { useAnnee } from '../contexts/AnneeContext';
import opexRaw from '../data_opex.json';
import opex2025Raw from '../data_opex_2025.json';
import { formatEuros } from '../lib/format';
import { calculateCategoryTrends } from '../lib/opexTrends';
import { isOpexEtablissementCode, normalizeOpexEtablissementCode } from '@shared/opexValidation';
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogFooter,
} from '@/components/ui/dialog';
import { toast } from 'sonner';
import { trpc } from '@/lib/trpc';

// ─── Types ────────────────────────────────────────────────────────────────────

interface Poste {
  col_idx: number;
  fournisseur: string | null;
  libelle: string;
  detail: string | null;
  facturation: string | null;
  mode_ventilation: string | null;
  categorie: string | null;
  type: string | null;
  compte: string | null;
  budget_n1: number | null;
  montant_previsionnel_2026?: number | null;
  montant_previsionnel_2025?: number | null;
  libelle_court?: string;
  libelle_detail?: string;
}

interface EtabSource {
  code: string;
  nom: string;
  base_repartition: number;
}

interface OpexData {
  annee: number;
  total_global: number;
  postes: Poste[];
  etablissements: EtabSource[];
  categories_totaux: Record<string, number>;
  meta: { nb_postes: number; nb_etablissements?: number };
}

// Ligne en mémoire (mappée depuis la BDD)
interface LigneOpex {
  id: number;           // id BDD
  libelle: string;
  fournisseur: string;
  categorie: string;
  type: string;
  facturation: string;
  compte: string;
  detail: string;
  budget_n1: number;
  montant: number;
  isCustom: boolean;
  mode_ventilation: string | null;
  libelleCourt?: string | null;
  libelleDetail?: string | null;
  colIdx: number;
}

/** Sous-ensemble commun aux lignes brutes OPEX retournées par la BDD. */
interface OpexPosteRow {
  id: number;
  libelle: string;
  fournisseur: string | null;
  categorie: string | null;
  type: string | null;
  facturation: string | null;
  compte: string | null;
  detail: string | null;
  budgetN1: string | null;
  montant: string | null;
  isCustom: boolean | null;
  modeVentilation: string | null;
  libelleCourt: string | null;
  libelleDetail: string | null;
  colIdx: number;
}

// ─── Constantes ───────────────────────────────────────────────────────────────

const opexData2026 = opexRaw as OpexData;
const opexData2025 = opex2025Raw as OpexData;

function getOpexDataForAnnee(annee: number): OpexData {
  if (annee === 2025) return opexData2025;
  return opexData2026;
}

const CATEGORIES_FIXES = ['App global', 'Infogérance', 'Sécurité', 'Téléphonie', 'App HEP', 'App SMR'];

const CATEGORIE_COLORS: Record<string, { badge: string; bar: string; kpi: string; icon: string }> = {
  'App global':   { badge: 'bg-blue-100 text-blue-700',    bar: 'bg-blue-500',    kpi: 'text-blue-600',    icon: 'text-blue-500' },
  'Infogérance':  { badge: 'bg-purple-100 text-purple-700', bar: 'bg-purple-500',  kpi: 'text-purple-600',  icon: 'text-purple-500' },
  'Sécurité':     { badge: 'bg-red-100 text-red-700',       bar: 'bg-red-500',     kpi: 'text-red-600',     icon: 'text-red-500' },
  'Téléphonie':   { badge: 'bg-green-100 text-green-700',   bar: 'bg-green-500',   kpi: 'text-green-600',   icon: 'text-green-500' },
  'App HEP':      { badge: 'bg-orange-100 text-orange-700', bar: 'bg-orange-500',  kpi: 'text-orange-600',  icon: 'text-orange-500' },
  'App SMR':      { badge: 'bg-teal-100 text-teal-700',     bar: 'bg-teal-500',    kpi: 'text-teal-600',    icon: 'text-teal-500' },
  'Autre':        { badge: 'bg-gray-100 text-gray-600',     bar: 'bg-gray-400',    kpi: 'text-gray-600',    icon: 'text-gray-400' },
};

function getCatColors(cat: string) {
  return CATEGORIE_COLORS[cat] ?? CATEGORIE_COLORS['Autre'];
}

function formatNum(v: number | null | undefined): string {
  if (v === null || v === undefined || v === 0) return '—';
  return new Intl.NumberFormat('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(v) + ' €';
}

// Libellés parasites (noms de catégories ou de synthèse qui ne sont pas de vrais postes)
const LIBELLES_PARASITES = new Set([
  'Ventilation cout', 'Ventilation coût',
  'Infogérance', 'Sécurité', 'Téléphonie',
  'App global', 'App HEP', 'App SMR',
  'Applicatifs communs', 'Applicatifs PA', 'Applicatifs HEP', 'Applicatifs SMR',
  'Nouveautés', 'TOTAL',
]);

function isLibelleParasite(libelle: string): boolean {
  const lib = libelle.trim();
  return LIBELLES_PARASITES.has(lib) || /^(total|ventilation|répartition)/i.test(lib);
}

// Construire les postes sources pour l'initialisation BDD
function buildPostesFromSource(annee: number) {
  const src = getOpexDataForAnnee(annee);
  return src.postes
    .filter(p => !isLibelleParasite(p.libelle))
    .map((p, idx) => ({
      colIdx: p.col_idx ?? idx,
      libelle: p.libelle,
      libelleCourt: p.libelle_court ?? null,
      libelleDetail: p.libelle_detail ?? null,
      fournisseur: p.fournisseur ?? null,
      categorie: p.categorie ?? 'Autre',
      type: p.type ?? null,
      facturation: p.facturation ?? null,
      modeVentilation: p.mode_ventilation ?? null,
      compte: p.compte ?? null,
      detail: p.detail ?? null,
      budgetN1: String(p.budget_n1 ?? 0),
      montant: String((annee === 2025 ? p.montant_previsionnel_2025 : p.montant_previsionnel_2026) ?? 0),
      isCustom: false,
    }));
}

/** Convertit les lignes Drizzle en modèle de vue, en écartant les synthèses importées. */
function mapDbPostesToLignes(postes: OpexPosteRow[]): LigneOpex[] {
  return postes
    .filter(p => !isLibelleParasite(p.libelle))
    .map(p => ({
      id: p.id,
      libelle: p.libelle,
      fournisseur: p.fournisseur ?? '',
      categorie: p.categorie ?? 'Autre',
      type: p.type ?? '',
      facturation: p.facturation ?? '',
      compte: p.compte ?? '',
      detail: p.detail ?? '',
      budget_n1: parseFloat(p.budgetN1 ?? '0') || 0,
      montant: parseFloat(p.montant ?? '0') || 0,
      isCustom: p.isCustom ?? false,
      mode_ventilation: p.modeVentilation ?? null,
      libelleCourt: p.libelleCourt ?? null,
      libelleDetail: p.libelleDetail ?? null,
      colIdx: p.colIdx,
    }));
}

const EMPTY_LIGNE_FORM = {
  libelle: '',
  fournisseur: '',
  categorie: 'App global',
  type: '',
  facturation: '',
  compte: '',
  detail: '',
  budget_n1: 0,
  montant: 0,
};

// ─── Composant champ numérique ─────────────────────────────────────────────

function MoneyInput({
  value,
  onChange,
  disabled = false,
}: {
  value: number;
  onChange: (v: number) => void;
  disabled?: boolean;
}) {
  const [raw, setRaw] = useState(value === 0 ? '' : String(value));

  useEffect(() => {
    setRaw(value === 0 ? '' : String(value));
  }, [value]);

  return (
    <div className="relative">
      <input
        type="number"
        min={0}
        step={100}
        value={raw}
        placeholder="0"
        disabled={disabled}
        onChange={e => {
          setRaw(e.target.value);
          const v = parseFloat(e.target.value);
          onChange(isNaN(v) || v < 0 ? 0 : Math.round(v));
        }}
        onBlur={() => setRaw(value === 0 ? '' : String(value))}
        className={`w-full pl-6 pr-2 py-1.5 text-xs border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary tabular-nums transition-colors text-right ${
          disabled
            ? 'bg-muted/30 text-muted-foreground cursor-not-allowed border-border/40'
            : 'bg-background border-border hover:border-primary/50'
        }`}
      />
      <span className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground text-xs">€</span>
    </div>
  );
}

// ─── Formulaire ajout/édition ──────────────────────────────────────────────

type LigneFormData = typeof EMPTY_LIGNE_FORM;

function LigneForm({
  initial,
  onSave,
  onClose,
}: {
  initial: LigneFormData;
  onSave: (data: LigneFormData) => void;
  onClose: () => void;
}) {
  const [form, setForm] = useState(initial);
  const set = (k: keyof typeof form, v: string | number) =>
    setForm(prev => ({ ...prev, [k]: v }));

  const allCats = [...CATEGORIES_FIXES, 'Autre'];

  return (
    <div className="space-y-4">
      <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
        <div className="sm:col-span-2">
          <label className="block text-xs font-medium text-muted-foreground mb-1">Libellé du poste *</label>
          <input
            type="text"
            value={form.libelle}
            onChange={e => set('libelle', e.target.value)}
            placeholder="Ex : Maintenance serveurs"
            className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30"
          />
        </div>
        <div>
          <label className="block text-xs font-medium text-muted-foreground mb-1">Fournisseur</label>
          <input
            type="text"
            value={form.fournisseur}
            onChange={e => set('fournisseur', e.target.value)}
            placeholder="Ex : Microsoft"
            className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30"
          />
        </div>
        <div>
          <label className="block text-xs font-medium text-muted-foreground mb-1">Catégorie *</label>
          <select
            value={form.categorie}
            onChange={e => set('categorie', e.target.value)}
            className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 bg-background"
          >
            {allCats.map(c => <option key={c} value={c}>{c}</option>)}
          </select>
        </div>
        <div>
          <label className="block text-xs font-medium text-muted-foreground mb-1">Type</label>
          <input
            type="text"
            value={form.type}
            onChange={e => set('type', e.target.value)}
            placeholder="Ex : Licence, Maintenance…"
            className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30"
          />
        </div>
        <div>
          <label className="block text-xs font-medium text-muted-foreground mb-1">Facturation</label>
          <input
            type="text"
            value={form.facturation}
            onChange={e => set('facturation', e.target.value)}
            placeholder="Ex : Annuelle, Mensuelle…"
            className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30"
          />
        </div>
        <div>
          <label className="block text-xs font-medium text-muted-foreground mb-1">Compte comptable</label>
          <input
            type="text"
            value={form.compte}
            onChange={e => set('compte', e.target.value)}
            placeholder="Ex : 6156"
            className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30"
          />
        </div>
        <div>
          <label className="block text-xs font-medium text-muted-foreground mb-1">Budget N-1 (€)</label>
          <input
            type="number"
            min={0}
            value={form.budget_n1 || ''}
            onChange={e => set('budget_n1', parseFloat(e.target.value) || 0)}
            placeholder="0"
            className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30"
          />
        </div>
        <div>
          <label className="block text-xs font-medium text-muted-foreground mb-1">Montant prévisionnel (€) *</label>
          <input
            type="number"
            min={0}
            value={form.montant || ''}
            onChange={e => set('montant', parseFloat(e.target.value) || 0)}
            placeholder="0"
            className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30"
          />
        </div>
        <div className="sm:col-span-2">
          <label className="block text-xs font-medium text-muted-foreground mb-1">Détail / Description</label>
          <textarea
            value={form.detail}
            onChange={e => set('detail', e.target.value)}
            rows={2}
            placeholder="Description complémentaire…"
            className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 resize-none"
          />
        </div>
      </div>
      <DialogFooter className="gap-2">
        <button
          onClick={onClose}
          className="px-4 py-2 text-sm rounded-lg border border-border bg-card hover:bg-muted transition-colors text-muted-foreground"
        >
          Annuler
        </button>
        <button
          onClick={() => {
            if (!form.libelle.trim()) { toast.error('Le libellé est obligatoire'); return; }
            onSave(form);
          }}
          className="px-4 py-2 text-sm rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 font-medium transition-all shadow-sm"
        >
          Enregistrer
        </button>
      </DialogFooter>
    </div>
  );
}

// ─── Composant Clés de répartition ──────────────────────────────────────────

interface BaseRow {
  annee: number;
  etablissementCode: string;
  etablissementNom: string | null;
  baseRepartition: string;
  baseRepartitionHep: string | null;
  modeManuel: boolean;
}

interface ClesRepartitionViewProps {
  annee: number;
  basesRepartitionRaw: BaseRow[];
  isLoadingBases: boolean;
  isErrorBases: boolean;
  setBaseRepartition: ReturnType<typeof trpc.opex.setBaseRepartition.useMutation>;
  importBasesRepartition: ReturnType<typeof trpc.opex.importBasesRepartition.useMutation>;
  batchSetMontantsEtab: ReturnType<typeof trpc.opex.batchSetMontantsEtab.useMutation>;
  etablissementsRecalcules: Array<{ code: string; nom: string; montants: Record<string, number>; total: number }>;
}

function ClesRepartitionView({ annee, basesRepartitionRaw, isLoadingBases, isErrorBases, setBaseRepartition, importBasesRepartition, batchSetMontantsEtab, etablissementsRecalcules }: ClesRepartitionViewProps) {
  const anneeBase = annee - 2; // La base de répartition correspond à l'année OPEX - 2
  const [editingCell, setEditingCell] = useState<{ code: string; col: 'base' | 'hep' } | null>(null);
  const [editValue, setEditValue] = useState('');
  const [searchBase, setSearchBase] = useState('');
  const [isDragging, setIsDragging] = useState(false);
  const fileInputRef = React.useRef<HTMLInputElement>(null);

  const rows = useMemo(() => {
    return basesRepartitionRaw
      .filter(r => {
        if (!searchBase) return true;
        const s = searchBase.toLowerCase();
        return r.etablissementCode.toLowerCase().includes(s) || (r.etablissementNom ?? '').toLowerCase().includes(s);
      })
      .sort((a, b) => a.etablissementCode.localeCompare(b.etablissementCode));
  }, [basesRepartitionRaw, searchBase]);

  const totalBase = useMemo(() => basesRepartitionRaw.reduce((s, r) => s + parseFloat(r.baseRepartition ?? '0'), 0), [basesRepartitionRaw]);
  const totalHep = useMemo(() => basesRepartitionRaw.reduce((s, r) => s + parseFloat(r.baseRepartitionHep ?? '0'), 0), [basesRepartitionRaw]);

  function startEdit(code: string, col: 'base' | 'hep', currentVal: string) {
    setEditingCell({ code, col });
    setEditValue(currentVal.replace(/[^0-9.]/g, ''));
  }

  function commitEdit(row: BaseRow) {
    if (!editingCell) return;
    const numVal = parseFloat(editValue.replace(',', '.')) || 0;
    const baseRepartition = editingCell.col === 'base' ? numVal : parseFloat(row.baseRepartition ?? '0');
    const baseRepartitionHep = editingCell.col === 'hep' ? numVal : parseFloat(row.baseRepartitionHep ?? '0');
    setBaseRepartition.mutate({
      annee,
      etablissementCode: row.etablissementCode,
      etablissementNom: row.etablissementNom,
      baseRepartition,
      baseRepartitionHep,
      modeManuel: row.modeManuel,
    });
    setEditingCell(null);
  }

  function toggleModeManuel(row: BaseRow) {
    const activating = !row.modeManuel;
    if (activating) {
      // Avant d'activer le mode manuel, on pré-remplit les montants avec les valeurs
      // calculées automatiquement (prorata) pour cet établissement
      const etabCalc = etablissementsRecalcules.find(e => e.code === row.etablissementCode);
      if (etabCalc && Object.keys(etabCalc.montants).length > 0) {
        const montantsASeeder = Object.entries(etabCalc.montants)
          .filter(([, v]) => v > 0)
          .map(([libellePoste, v]) => ({ libellePoste, montant: String(v) }));
        if (montantsASeeder.length > 0) {
          batchSetMontantsEtab.mutate(
            { annee, etablissementCode: row.etablissementCode, montants: montantsASeeder },
            {
              onSettled: () => {
                // Activer le flag modeManuel après la sauvegarde des montants
                setBaseRepartition.mutate({
                  annee,
                  etablissementCode: row.etablissementCode,
                  etablissementNom: row.etablissementNom,
                  baseRepartition: parseFloat(row.baseRepartition ?? '0'),
                  baseRepartitionHep: parseFloat(row.baseRepartitionHep ?? '0'),
                  modeManuel: true,
                });
              },
            }
          );
          return;
        }
      }
    }
    // Cas normal : désactivation ou activation sans montants à seeder
    setBaseRepartition.mutate({
      annee,
      etablissementCode: row.etablissementCode,
      etablissementNom: row.etablissementNom,
      baseRepartition: parseFloat(row.baseRepartition ?? '0'),
      baseRepartitionHep: parseFloat(row.baseRepartitionHep ?? '0'),
      modeManuel: activating,
    });
  }

  async function handleFileImport(file: File) {
    try {
      const XLSX = await import('xlsx');
      const buf = await file.arrayBuffer();
      const wb = XLSX.read(buf, { type: 'array' });

      // ── Sélection intelligente de la feuille ──────────────────────────────
      // Priorité : feuille dont le nom contient OPEX, DSI, répartition ou base
      // (insensible à la casse et aux accents). Sinon, première feuille.
      const SHEET_KEYWORDS = ['opex', 'dsi', 'repartition', 'répartition', 'base'];
      const selectedSheetName =
        wb.SheetNames.find(name => {
          const lower = name.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
          return SHEET_KEYWORDS.some(kw => lower.includes(kw));
        }) ?? wb.SheetNames[0];

      const ws = wb.Sheets[selectedSheetName];

      // Informer l'utilisateur de la feuille utilisée si le classeur en contient plusieurs
      if (wb.SheetNames.length > 1) {
        toast.info(`Feuille utilisée : « ${selectedSheetName} »`, {
          description: `${wb.SheetNames.length} feuilles disponibles dans le classeur.`,
        });
      }

      const data: string[][] = XLSX.utils.sheet_to_json(ws, { header: 1, defval: '' }) as string[][];

      // ── Détection de la ligne d'en-tête ──────────────────────────────────
      // Cherche dans les 15 premières lignes une ligne contenant un code établissement
      let headerIdx = -1;
      for (let i = 0; i < Math.min(data.length, 15); i++) {
        const row = data[i].map(c => String(c).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''));
        if (row.some(c =>
          c.includes('code') ||
          c.includes('etablissement') ||
          c.includes('etab') ||
          c.includes('structure')
        )) {
          headerIdx = i;
          break;
        }
      }
      if (headerIdx === -1) {
        toast.error('Format non reconnu', {
          description: `Impossible de trouver la ligne d'en-tête dans la feuille « ${selectedSheetName} ». Colonnes attendues : code établissement, base de répartition.`,
        });
        return;
      }

      // ── Détection des colonnes ────────────────────────────────────────────
      const headers = data[headerIdx].map(c =>
        String(c).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '')
      );
      const codeIdx = headers.findIndex(h => h.includes('code'));
      const nomIdx = headers.findIndex(h =>
        h.includes('nom') || h.includes('etablissement') || h.includes('etab') || h.includes('libelle')
      );
      // Colonne base standard : contient 'base' ou 'repartition' mais PAS 'hep'
      const baseIdx = headers.findIndex(h =>
        (h.includes('base') || h.includes('repartition')) && !h.includes('hep')
      );
      // Colonne base HEP : contient 'hep'
      const hepIdx = headers.findIndex(h => h.includes('hep'));

      if (codeIdx === -1 || baseIdx === -1) {
        toast.error('Colonnes manquantes', {
          description: `Le fichier doit contenir au minimum les colonnes : code établissement, base de répartition. Colonnes détectées : ${headers.filter(Boolean).join(', ') || '(aucune)'}`,
        });
        return;
      }

      // ── Construction des lignes à importer ───────────────────────────────
      const importRows = data.slice(headerIdx + 1)
        .filter(row => {
          const code = String(row[codeIdx] ?? '').trim();
          return isOpexEtablissementCode(code);
        })
        .map(row => ({
          etablissementCode: normalizeOpexEtablissementCode(String(row[codeIdx])),
          etablissementNom: nomIdx >= 0 ? String(row[nomIdx]).trim() || null : null,
          baseRepartition: parseFloat(String(row[baseIdx]).replace(/[^0-9.,]/g, '').replace(',', '.')) || 0,
          baseRepartitionHep: hepIdx >= 0
            ? parseFloat(String(row[hepIdx]).replace(/[^0-9.,]/g, '').replace(',', '.')) || 0
            : 0,
        }));

      if (importRows.length === 0) {
        toast.error('Aucune ligne importée', {
          description: 'Le fichier ne contient aucune ligne de données valide après la ligne d\'en-tête.',
        });
        return;
      }

      importBasesRepartition.mutate({ annee, rows: importRows });
    } catch (e) {
      toast.error('Erreur lecture fichier', { description: String(e) });
    }
  }

  function formatBaseNum(val: string | null | undefined): string {
    const n = parseFloat(val ?? '0');
    if (!n) return '—';
    return new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(n) + ' €';
  }

  if (isLoadingBases) {
    return (
      <div className="flex flex-col items-center justify-center py-20 gap-3 text-muted-foreground">
        <Loader2 className="w-8 h-8 animate-spin text-primary" />
        <p className="text-sm">Chargement des bases de répartition…</p>
      </div>
    );
  }

  if (isErrorBases) {
    return (
      <div className="flex flex-col items-center justify-center py-20 gap-3">
        <AlertCircle className="w-8 h-8 text-destructive" />
        <p className="text-sm font-semibold text-destructive">Erreur lors du chargement des bases de répartition</p>
        <p className="text-xs text-muted-foreground">Vérifiez votre connexion et rechargez la page.</p>
      </div>
    );
  }

  return (
    <div className="flex flex-col h-full overflow-hidden">
      {/* Bandeau d'information */}
      <div className="flex-shrink-0 mb-4 p-4 rounded-xl bg-blue-50 border border-blue-200 flex items-start gap-3">
        <AlertCircle className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
        <div className="text-sm text-blue-800">
          <p className="font-semibold mb-1">Base de répartition {anneeBase} — OPEX {annee}</p>
          <p>La base de répartition utilisée pour l'OPEX <strong>{annee}</strong> correspond aux charges de classe 6 de l'exercice <strong>{anneeBase}</strong> (année OPEX − 2).</p>
          <p className="mt-1">La colonne <strong>Base / HEP</strong> est utilisée exclusivement pour le poste <em>DUI pôle HEP</em>. Pour les établissements hors pôle HEP, cette valeur doit être 0.</p>
        </div>
      </div>

      {/* Barre d'outils */}
      <div className="flex-shrink-0 flex items-center gap-3 mb-3 flex-wrap">
        <div className="relative flex-1 min-w-48 max-w-xs">
          <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
          <input
            type="text"
            placeholder="Rechercher un établissement..."
            value={searchBase}
            onChange={e => setSearchBase(e.target.value)}
            className="w-full pl-9 pr-3 py-2 text-sm bg-muted/50 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors"
          />
        </div>
        <div className="flex items-center gap-2 text-sm text-muted-foreground">
          <span className="font-medium text-foreground">{basesRepartitionRaw.length}</span> établissements
        </div>
        <div className="ml-auto flex items-center gap-2">
          <input
            ref={fileInputRef}
            type="file"
            accept=".xlsx,.xls,.csv"
            className="hidden"
            onChange={e => { const f = e.target.files?.[0]; if (f) handleFileImport(f); e.target.value = ''; }}
          />
          <button
            onClick={() => fileInputRef.current?.click()}
            disabled={importBasesRepartition.isPending}
            className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-lg border border-border bg-card hover:bg-muted/50 transition-colors disabled:opacity-50"
          >
            {importBasesRepartition.isPending ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Upload className="w-3.5 h-3.5" />}
            Importer fichier
          </button>
        </div>
      </div>

      {/* Zone de drop */}
      <div
        className={`flex-shrink-0 mb-3 border-2 border-dashed rounded-xl p-3 text-center text-xs text-muted-foreground transition-colors ${
          isDragging ? 'border-primary bg-primary/5 text-primary' : 'border-border hover:border-primary/40'
        }`}
        onDragOver={e => { e.preventDefault(); setIsDragging(true); }}
        onDragLeave={() => setIsDragging(false)}
        onDrop={e => { e.preventDefault(); setIsDragging(false); const f = e.dataTransfer.files?.[0]; if (f) handleFileImport(f); }}
      >
        Glissez un fichier Excel (.xlsx) ou CSV ici pour importer les bases de répartition
        <span className="block mt-0.5 text-muted-foreground/60">Colonnes attendues : code établissement · base de répartition · base répartition HEP (optionnel)</span>
      </div>

      {/* Tableau */}
      <div className="flex-1 overflow-auto rounded-xl border border-border">
        <table className="w-full text-sm border-collapse">
          <thead>
            <tr className="bg-muted/40 border-b border-border sticky top-0 z-10">
              <th className="px-4 py-3 text-left font-semibold text-foreground text-xs uppercase tracking-wide w-28">Code</th>
              <th className="px-4 py-3 text-left font-semibold text-foreground text-xs uppercase tracking-wide">Établissement</th>
              <th className="px-4 py-3 text-right font-semibold text-foreground text-xs uppercase tracking-wide w-48">
                Base de répartition {anneeBase}
              </th>
              <th className="px-4 py-3 text-right font-semibold text-foreground text-xs uppercase tracking-wide w-48">
                Base répartition {anneeBase} / HEP
              </th>
              <th className="px-4 py-3 text-center font-semibold text-foreground text-xs uppercase tracking-wide w-36" title="Si activé, tous les montants OPEX de cet établissement sont saisis manuellement (pas de calcul prorata)">
                Tout manuel
              </th>
            </tr>
          </thead>
          <tbody>
            {rows.length === 0 && (
              <tr>
                <td colSpan={5} className="px-4 py-12 text-center text-muted-foreground">
                  {basesRepartitionRaw.length === 0
                    ? `Aucune base de répartition pour ${annee}. Importez un fichier ou saisissez les valeurs manuellement.`
                    : 'Aucun résultat pour cette recherche.'}
                </td>
              </tr>
            )}
            {rows.map((row, idx) => (
              <tr key={row.etablissementCode} className={`border-b border-border/50 hover:bg-muted/20 transition-colors ${row.modeManuel ? 'bg-amber-50/60' : idx % 2 === 0 ? '' : 'bg-muted/10'}`}>
                <td className="px-4 py-2.5 font-mono text-xs text-muted-foreground">{row.etablissementCode}</td>
                <td className="px-4 py-2.5 font-medium text-foreground">
                  {row.etablissementNom ?? row.etablissementCode}
                  {row.modeManuel && (
                    <span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-semibold bg-amber-100 text-amber-700 border border-amber-200">
                      Manuel
                    </span>
                  )}
                </td>
                {/* Colonne Base de répartition */}
                <td className="px-4 py-2.5 text-right">
                  {editingCell?.code === row.etablissementCode && editingCell.col === 'base' ? (
                    <input
                      type="number"
                      value={editValue}
                      onChange={e => setEditValue(e.target.value)}
                      onBlur={() => commitEdit(row)}
                      onKeyDown={e => { if (e.key === 'Enter') commitEdit(row); if (e.key === 'Escape') setEditingCell(null); }}
                      className="w-full text-right px-2 py-1 border border-primary rounded text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 tabular-nums"
                      autoFocus
                    />
                  ) : (
                    <button
                      onClick={() => startEdit(row.etablissementCode, 'base', row.baseRepartition)}
                      className="w-full text-right px-2 py-1 rounded hover:bg-primary/10 transition-colors tabular-nums font-medium text-foreground group"
                    >
                      {formatBaseNum(row.baseRepartition)}
                      <Pencil className="inline-block ml-1 w-3 h-3 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
                    </button>
                  )}
                </td>
                {/* Colonne Base / HEP */}
                <td className="px-4 py-2.5 text-right">
                  {editingCell?.code === row.etablissementCode && editingCell.col === 'hep' ? (
                    <input
                      type="number"
                      value={editValue}
                      onChange={e => setEditValue(e.target.value)}
                      onBlur={() => commitEdit(row)}
                      onKeyDown={e => { if (e.key === 'Enter') commitEdit(row); if (e.key === 'Escape') setEditingCell(null); }}
                      className="w-full text-right px-2 py-1 border border-primary rounded text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 tabular-nums"
                      autoFocus
                    />
                  ) : (
                    <button
                      onClick={() => startEdit(row.etablissementCode, 'hep', row.baseRepartitionHep ?? '0')}
                      className={`w-full text-right px-2 py-1 rounded hover:bg-primary/10 transition-colors tabular-nums group ${
                        parseFloat(row.baseRepartitionHep ?? '0') > 0 ? 'font-medium text-orange-600' : 'text-muted-foreground'
                      }`}
                    >
                      {parseFloat(row.baseRepartitionHep ?? '0') > 0 ? formatBaseNum(row.baseRepartitionHep) : '—'}
                      <Pencil className="inline-block ml-1 w-3 h-3 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
                    </button>
                  )}
                </td>
                {/* Colonne Mode Manuel */}
                <td className="px-4 py-2.5 text-center">
                  <button
                    onClick={() => toggleModeManuel(row)}
                    disabled={setBaseRepartition.isPending}
                    title={row.modeManuel
                      ? 'Mode manuel activé — cliquer pour revenir au calcul prorata automatique'
                      : 'Cliquer pour activer le mode tout manuel (montants OPEX saisis manuellement pour cet établissement)'}
                    className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary/30 disabled:opacity-50 ${
                      row.modeManuel ? 'bg-amber-500' : 'bg-muted-foreground/30'
                    }`}
                  >
                    <span
                      className={`inline-block h-3.5 w-3.5 rounded-full bg-white shadow transition-transform ${
                        row.modeManuel ? 'translate-x-4.5' : 'translate-x-0.5'
                      }`}
                    />
                  </button>
                </td>
              </tr>
            ))}
          </tbody>
          <tfoot>
            <tr className="bg-muted/40 border-t-2 border-border font-bold">
              <td colSpan={2} className="px-4 py-3 text-sm font-semibold text-foreground">TOTAL — {basesRepartitionRaw.length} établissements
                {basesRepartitionRaw.filter(r => r.modeManuel).length > 0 && (
                  <span className="ml-2 text-xs font-normal text-amber-600">
                    ({basesRepartitionRaw.filter(r => r.modeManuel).length} en mode manuel)
                  </span>
                )}
              </td>
              <td className="px-4 py-3 text-right text-sm font-bold text-foreground tabular-nums">
                {new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(totalBase)} €
              </td>
              <td className="px-4 py-3 text-right text-sm font-bold text-orange-600 tabular-nums">
                {totalHep > 0 ? new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(totalHep) + ' €' : '—'}
              </td>
              <td className="px-4 py-3 text-center text-xs text-muted-foreground">
                {basesRepartitionRaw.filter(r => r.modeManuel).length > 0
                  ? `${basesRepartitionRaw.filter(r => r.modeManuel).length} actif${basesRepartitionRaw.filter(r => r.modeManuel).length > 1 ? 's' : ''}`
                  : '—'}
              </td>
            </tr>
          </tfoot>
        </table>
      </div>
    </div>
  );
}

// ─── Composant principal ──────────────────────────────────────────────────────

type ViewMode = 'postes' | 'etablissements' | 'cles_repartition';
type SortDir = 'asc' | 'desc';

export default function DsiOpex() {
  const { annee } = useAnnee();
  const utils = trpc.useUtils();

  // ── Requêtes tRPC ──────────────────────────────────────────────────────────
  const { data: postesRaw, isLoading: loadingPostes } = trpc.opex.getPostes.useQuery({ annee });
  // Les vignettes doivent comparer l'exercice affiché avec ses données BDD N-1,
  // et non avec les tendances figées du fichier source 2026.
  const { data: postesN1Raw, isLoading: loadingPostesN1 } = trpc.opex.getPostes.useQuery({ annee: annee - 1 });
  const { data: montantsEtabRaw, isLoading: loadingMontants } = trpc.opex.getMontantsEtab.useQuery({ annee });
  const { data: validatedRow } = trpc.opex.getValidated.useQuery({ annee });
  // Bases de répartition depuis la BDD (charges classe 6 par établissement)
  const { data: basesRepartitionRaw, isLoading: isLoadingBases, isError: isErrorBases } = trpc.opex.getBasesRepartition.useQuery({ annee });

  // ── Mutations tRPC ─────────────────────────────────────────────────────────
  const upsertPoste = trpc.opex.upsertPoste.useMutation({
    onSuccess: () => utils.opex.getPostes.invalidate({ annee }),
    onError: (err) => toast.error('Erreur sauvegarde', { description: err.message }),
  });
  const deletePoste = trpc.opex.deletePoste.useMutation({
    onSuccess: () => {
      utils.opex.getPostes.invalidate({ annee });
      utils.opex.getMontantsEtab.invalidate({ annee });
      toast.success('Poste OPEX supprimé');
    },
    onError: (err) => toast.error('Erreur suppression', { description: err.message }),
  });
  const initFromSource = trpc.opex.initFromSource.useMutation({
    onSuccess: () => { utils.opex.getPostes.invalidate({ annee }); toast.success(`OPEX ${annee} initialisé`); },
    onError: (err) => toast.error('Erreur initialisation', { description: err.message }),
  });
  const deleteAll = trpc.opex.deleteAll.useMutation({
    onSuccess: () => { utils.opex.getPostes.invalidate({ annee }); utils.opex.getMontantsEtab.invalidate({ annee }); toast.success(`OPEX ${annee} supprimé`); },
    onError: (err) => toast.error('Erreur suppression', { description: err.message }),
  });
  const setMontantEtab = trpc.opex.setMontantEtab.useMutation({
    onSuccess: () => utils.opex.getMontantsEtab.invalidate({ annee }),
    onError: (err) => toast.error('Erreur sauvegarde montant', { description: err.message }),
  });
  const validateMutation = trpc.opex.validate.useMutation({
    onSuccess: () => { utils.opex.getValidated.invalidate({ annee }); toast.success(`OPEX ${annee} validé`, { description: 'Le prévisionnel est maintenant verrouillé.' }); },
    onError: (err) => toast.error('Erreur validation', { description: err.message }),
  });
  const setBaseRepartition = trpc.opex.setBaseRepartition.useMutation({
    onSuccess: () => utils.opex.getBasesRepartition.invalidate({ annee }),
    onError: (err) => toast.error('Erreur sauvegarde base', { description: err.message }),
  });
  const importBasesRepartition = trpc.opex.importBasesRepartition.useMutation({
    onSuccess: (data) => { utils.opex.getBasesRepartition.invalidate({ annee }); toast.success(`${data.count} bases de répartition importées`); },
    onError: (err) => toast.error('Erreur import', { description: err.message }),
  });
  const batchSetMontantsEtab = trpc.opex.batchSetMontantsEtab.useMutation({
    onSuccess: () => utils.opex.getMontantsEtab.invalidate({ annee }),
    onError: (err) => toast.error('Erreur sauvegarde montants', { description: err.message }),
  });

  // ── Mapper les données BDD vers le format interne ──────────────────────────
  const lignes: LigneOpex[] = useMemo(() => {
    if (!postesRaw) return [];
    return mapDbPostesToLignes(postesRaw);
  }, [postesRaw]);

  const lignesN1 = useMemo(() => {
    if (postesN1Raw && postesN1Raw.length > 0) {
      return mapDbPostesToLignes(postesN1Raw);
    }

    // Seuls les fichiers 2025 et 2026 sont des sources historiques fiables.
    // Pour les années sans source ni données BDD, l'interface indique simplement
    // qu'il n'existe pas de référence N-1 au lieu de réutiliser 2026 par défaut.
    if (annee - 1 === 2025 || annee - 1 === 2026) {
      return buildPostesFromSource(annee - 1).map((poste, index) => ({
        id: -(index + 1),
        libelle: poste.libelle,
        fournisseur: poste.fournisseur ?? '',
        categorie: poste.categorie ?? 'Autre',
        type: poste.type ?? '',
        facturation: poste.facturation ?? '',
        compte: poste.compte ?? '',
        detail: poste.detail ?? '',
        budget_n1: parseFloat(poste.budgetN1 ?? '0') || 0,
        montant: parseFloat(poste.montant ?? '0') || 0,
        isCustom: poste.isCustom ?? false,
        mode_ventilation: poste.modeVentilation ?? null,
        libelleCourt: poste.libelleCourt ?? null,
        libelleDetail: poste.libelleDetail ?? null,
        colIdx: poste.colIdx,
      }));
    }

    return [];
  }, [annee, postesN1Raw]);

  // Overrides par établissement : { codeEtab: { libellePoste: montant } }
  const montantsManuelEtab = useMemo(() => {
    if (!montantsEtabRaw) return {} as Record<string, Record<string, number>>;
    const result: Record<string, Record<string, number>> = {};
    for (const row of montantsEtabRaw) {
      if (!result[row.etablissementCode]) result[row.etablissementCode] = {};
      result[row.etablissementCode][row.libellePoste] = parseFloat(row.montant ?? '0') || 0;
    }
    return result;
  }, [montantsEtabRaw]);

  const isValidated = !!validatedRow;
  const isLoading = loadingPostes || loadingMontants;

  // ── UI state ───────────────────────────────────────────────────────────────
  const [viewMode, setViewMode] = useState<ViewMode>('postes');
  const [search, setSearch] = useState('');
  const [selectedCategorie, setSelectedCategorie] = useState('Toutes');
  const [sortCol, setSortCol] = useState<string | null>(null);
  const [sortDir, setSortDir] = useState<SortDir>('desc');
  const [expandedPoste, setExpandedPoste] = useState<number | null>(null);
  const [showDetails, setShowDetails] = useState(false);

  // CRUD
  const [dialogMode, setDialogMode] = useState<'add' | 'edit' | null>(null);
  const [editingLigne, setEditingLigne] = useState<LigneOpex | null>(null);
  const [deleteConfirmId, setDeleteConfirmId] = useState<number | null>(null);

  // Édition inline établissements
  const [inlineEditKey, setInlineEditKey] = useState<string | null>(null);
  const [inlineEditVal, setInlineEditVal] = useState<string>('');

  // Réinitialiser la recherche quand l'année change
  useEffect(() => {
    setSearch('');
    setExpandedPoste(null);
    setSelectedCategorie('Toutes');
  }, [annee]);

  // ── Handlers ───────────────────────────────────────────────────────────────

  const handleToggleVentilation = useCallback((ligne: LigneOpex) => {
    if (isValidated) return;
    const newMode = ligne.mode_ventilation === 'Manuel' ? 'Prorata C 6' : 'Manuel';
    upsertPoste.mutate({
      id: ligne.id,
      annee,
      colIdx: ligne.colIdx,
      libelle: ligne.libelle,
      libelleCourt: ligne.libelleCourt,
      libelleDetail: ligne.libelleDetail,
      fournisseur: ligne.fournisseur || null,
      categorie: ligne.categorie,
      type: ligne.type || null,
      facturation: ligne.facturation || null,
      modeVentilation: newMode,
      compte: ligne.compte || null,
      detail: ligne.detail || null,
      budgetN1: String(ligne.budget_n1),
      montant: String(ligne.montant),
      isCustom: ligne.isCustom,
    });
  }, [isValidated, upsertPoste, annee]);

  const handleMontantChange = useCallback((ligne: LigneOpex, value: number) => {
    if (isValidated) return;
    upsertPoste.mutate({
      id: ligne.id,
      annee,
      colIdx: ligne.colIdx,
      libelle: ligne.libelle,
      libelleCourt: ligne.libelleCourt,
      libelleDetail: ligne.libelleDetail,
      fournisseur: ligne.fournisseur || null,
      categorie: ligne.categorie,
      type: ligne.type || null,
      facturation: ligne.facturation || null,
      modeVentilation: ligne.mode_ventilation,
      compte: ligne.compte || null,
      detail: ligne.detail || null,
      budgetN1: String(ligne.budget_n1),
      montant: String(value),
      isCustom: ligne.isCustom,
    });
  }, [isValidated, upsertPoste, annee]);

  const handleAddLigne = useCallback((data: LigneFormData) => {
    const maxColIdx = lignes.length > 0 ? Math.max(...lignes.map(l => l.colIdx)) + 1 : 0;
    upsertPoste.mutate({
      annee,
      colIdx: maxColIdx,
      libelle: data.libelle,
      fournisseur: data.fournisseur || null,
      categorie: data.categorie,
      type: data.type || null,
      facturation: data.facturation || null,
      modeVentilation: 'Manuel',
      compte: data.compte || null,
      detail: data.detail || null,
      budgetN1: String(data.budget_n1),
      montant: String(data.montant),
      isCustom: true,
    }, {
      onSuccess: () => { setDialogMode(null); toast.success('Ligne ajoutée'); },
    });
  }, [lignes, upsertPoste, annee]);

  const handleEditLigne = useCallback((data: LigneFormData) => {
    if (!editingLigne) return;
    upsertPoste.mutate({
      id: editingLigne.id,
      annee,
      colIdx: editingLigne.colIdx,
      libelle: data.libelle,
      libelleCourt: editingLigne.libelleCourt,
      libelleDetail: editingLigne.libelleDetail,
      fournisseur: data.fournisseur || null,
      categorie: data.categorie,
      type: data.type || null,
      facturation: data.facturation || null,
      modeVentilation: editingLigne.mode_ventilation,
      compte: data.compte || null,
      detail: data.detail || null,
      budgetN1: String(data.budget_n1),
      montant: String(data.montant),
      isCustom: editingLigne.isCustom,
    }, {
      onSuccess: () => { setDialogMode(null); setEditingLigne(null); toast.success('Ligne modifiée'); },
    });
  }, [editingLigne, upsertPoste, annee]);

  const handleDeleteLigne = useCallback(() => {
    if (!deleteConfirmId) return;
    deletePoste.mutate({ annee, id: deleteConfirmId });
    setDeleteConfirmId(null);
  }, [annee, deleteConfirmId, deletePoste]);

  const handleInlineEditStart = useCallback((codeEtab: string, libelle: string, currentVal: number) => {
    if (isValidated) return;
    setInlineEditKey(`${codeEtab}|${libelle}`);
    setInlineEditVal(currentVal === 0 ? '' : String(currentVal));
  }, [isValidated]);

  const handleInlineEditCommit = useCallback((codeEtab: string, libelle: string) => {
    const v = parseFloat(inlineEditVal);
    const montant = isNaN(v) || v < 0 ? 0 : Math.round(v);
    setMontantEtab.mutate({
      annee,
      etablissementCode: codeEtab,
      libellePoste: libelle,
      montant: String(montant),
    });
    setInlineEditKey(null);
  }, [inlineEditVal, setMontantEtab, annee]);

  const handleInlineEditCancel = useCallback(() => {
    setInlineEditKey(null);
  }, []);

  const handleInitFromSource = useCallback(() => {
    const postes = buildPostesFromSource(annee);
    initFromSource.mutate({ annee, postes });
  }, [annee, initFromSource]);

  const handleCreateFromN1 = useCallback(async () => {
    const anneeN1 = annee - 1;
    // On récupère les postes N-1 depuis la BDD via une requête directe
    // Pour l'instant, on utilise les données JSON si disponibles
    const srcN1 = anneeN1 === 2025 || anneeN1 === 2026 ? buildPostesFromSource(anneeN1) : null;
    if (!srcN1 || srcN1.length === 0) {
      toast.error(`Aucune donnée disponible pour ${anneeN1}`);
      return;
    }
    const postes = srcN1.map((p, idx) => ({
      ...p,
      colIdx: idx,
      budgetN1: p.montant, // budget N-1 = montant de l'année précédente
    }));
    initFromSource.mutate({ annee, postes }, {
      onSuccess: () => toast.success(`OPEX ${annee} créé depuis ${anneeN1}`, { description: `${postes.length} postes copiés.` }),
    });
  }, [annee, initFromSource]);

  // ── Calculs ────────────────────────────────────────────────────────────────

  const categoriesPresentes = useMemo(() => {
    const cats = Array.from(new Set(lignes.map(l => l.categorie)));
    return CATEGORIES_FIXES.filter(c => cats.includes(c)).concat(
      cats.filter(c => !CATEGORIES_FIXES.includes(c) && c !== 'Autre')
    ).concat(cats.includes('Autre') ? ['Autre'] : []);
  }, [lignes]);

  const CATEGORIES_FILTER = useMemo(() => ['Toutes', ...categoriesPresentes], [categoriesPresentes]);

  const totalGlobal = useMemo(() =>
    lignes.reduce((s, l) => s + l.montant, 0),
  [lignes]);

  const totalParCategorie = useMemo(() => {
    const totaux: Record<string, number> = {};
    for (const ligne of lignes) {
      const cat = ligne.categorie || 'Autre';
      totaux[cat] = (totaux[cat] || 0) + ligne.montant;
    }
    return totaux;
  }, [lignes]);

  const tendancesParCategorie = useMemo(
    () => calculateCategoryTrends(lignes, lignesN1),
    [lignes, lignesN1],
  );

  const filteredLignes = useMemo(() => {
    return lignes.filter(l => {
      if (selectedCategorie !== 'Toutes' && l.categorie !== selectedCategorie) return false;
      if (search && !l.libelle.toLowerCase().includes(search.toLowerCase()) &&
          !l.fournisseur.toLowerCase().includes(search.toLowerCase())) return false;
      return true;
    });
  }, [lignes, selectedCategorie, search]);

  const totalFiltre = useMemo(() =>
    filteredLignes.reduce((s, l) => s + l.montant, 0),
  [filteredLignes]);

  const sortedLignes = useMemo(() => {
    const arr = [...filteredLignes];
    if (sortCol === 'montant') {
      arr.sort((a, b) => sortDir === 'asc' ? a.montant - b.montant : b.montant - a.montant);
    } else if (sortCol === 'libelle') {
      arr.sort((a, b) => sortDir === 'asc'
        ? a.libelle.localeCompare(b.libelle)
        : b.libelle.localeCompare(a.libelle));
    }
    return arr;
  }, [filteredLignes, sortCol, sortDir]);

  const handleSort = (col: string) => {
    if (sortCol === col) setSortDir(d => d === 'asc' ? 'desc' : 'asc');
    else { setSortCol(col); setSortDir('desc'); }
  };

  const SortBtn = ({ col, label }: { col: string; label: string }) => (
    <button
      onClick={() => handleSort(col)}
      className="flex items-center gap-1 hover:text-foreground transition-colors group"
    >
      {label}
      <ArrowUpDown className={`w-3 h-3 ${sortCol === col ? 'text-primary' : 'text-muted-foreground/50 group-hover:text-muted-foreground'}`} />
    </button>
  );

  // Vue établissements : recalcul dynamique depuis les postes éditables
  // Les bases de répartition viennent de la BDD (table opex_bases_repartition)
  // Fallback sur le JSON source si la BDD n'a pas de données pour cette année
  const etablissementsRecalcules = useMemo(() => {
    // Priorité : BDD > JSON source
    const basesFromBdd = basesRepartitionRaw && basesRepartitionRaw.length > 0
      ? basesRepartitionRaw.map(b => ({
          code: b.etablissementCode,
          nom: b.etablissementNom ?? b.etablissementCode,
          base_repartition: parseFloat(b.baseRepartition ?? '0') || 0,
          base_repartition_hep: parseFloat(b.baseRepartitionHep ?? '0') || 0,
          modeManuel: b.modeManuel ?? false,
        }))
      : getOpexDataForAnnee(annee).etablissements.map(e => ({
          code: e.code,
          nom: e.nom,
          base_repartition: e.base_repartition,
          base_repartition_hep: 0, // JSON source n'a pas de base HEP
          modeManuel: false,
        }));

    const totalBase = basesFromBdd.reduce((s, e) => s + e.base_repartition, 0);
    const totalBaseHep = basesFromBdd.reduce((s, e) => s + e.base_repartition_hep, 0);
    if (totalBase === 0) return basesFromBdd.map(e => ({ ...e, montants: {} as Record<string, number>, total: 0 }));

    return basesFromBdd.map(etab => {
      const ratio = etab.base_repartition / totalBase;
      // Ratio HEP : utiliser la base /HEP si disponible, sinon fallback sur base standard
      const ratioHep = totalBaseHep > 0 ? etab.base_repartition_hep / totalBaseHep : ratio;
      const etabOverrides: Record<string, number> = montantsManuelEtab[etab.code] ?? {};

      const montantsRecalcules: Record<string, number> = {};
      let totalEtab = 0;
      for (const ligne of lignes) {
        const isManuel = ligne.mode_ventilation === 'Manuel' || ligne.isCustom;
        // Le poste "SAAS DUI Pole HEP" utilise la base de répartition /HEP
        const isHepPoste = ligne.libelle.toLowerCase().includes('dui') &&
          (ligne.libelle.toLowerCase().includes('hep') ||
           (ligne.libelleDetail ?? '').toLowerCase().includes('hep'));
        const ratioApplique = isHepPoste ? ratioHep : ratio;
        let montant: number;
        if (etab.modeManuel) {
          // Mode tout manuel : utiliser uniquement les montants saisis manuellement,
          // sans aucun calcul prorata — si pas de saisie, le montant est 0
          montant = etabOverrides[ligne.libelle] ?? 0;
        } else if (isManuel && etabOverrides[ligne.libelle] !== undefined) {
          montant = etabOverrides[ligne.libelle];
        } else if (!isManuel && ligne.montant > 0) {
          montant = Math.round(ligne.montant * ratioApplique);
        } else {
          montant = 0;
        }
        if (montant > 0) montantsRecalcules[ligne.libelle] = montant;
        totalEtab += montant;
      }

      return { ...etab, montants: montantsRecalcules, total: totalEtab };
    });
  }, [lignes, montantsManuelEtab, annee, basesRepartitionRaw]);

  const filteredEtabs = useMemo(() => {
    return etablissementsRecalcules.filter(e => {
      if (search && !e.nom.toLowerCase().includes(search.toLowerCase()) &&
          !e.code.toLowerCase().includes(search.toLowerCase())) return false;
      return true;
    });
  }, [etablissementsRecalcules, search]);

  const sortedEtabs = useMemo(() => {
    const arr = [...filteredEtabs];
    if (sortCol === 'total') arr.sort((a, b) => sortDir === 'asc' ? a.total - b.total : b.total - a.total);
    else if (sortCol === 'nom') arr.sort((a, b) => sortDir === 'asc' ? a.nom.localeCompare(b.nom) : b.nom.localeCompare(a.nom));
    else if (sortCol === 'code') arr.sort((a, b) => sortDir === 'asc' ? a.code.localeCompare(b.code) : b.code.localeCompare(a.code));
    else arr.sort((a, b) => a.code.localeCompare(b.code)); // tri par défaut : code croissant
    return arr;
  }, [filteredEtabs, sortCol, sortDir]);

  // ── Rendu ──────────────────────────────────────────────────────────────────

  if (isLoading) {
    return (
      <div className="min-h-screen flex bg-background">
        <AppSidebar />
        <main className="flex-1 flex items-center justify-center">
          <div className="flex flex-col items-center gap-3">
            <Loader2 className="w-8 h-8 animate-spin text-primary" />
            <p className="text-sm text-muted-foreground">Chargement des données OPEX…</p>
          </div>
        </main>
      </div>
    );
  }

  return (
    <div className="min-h-screen flex bg-background">
      <AppSidebar />

      <main className="flex-1 flex flex-col min-w-0 overflow-hidden">
        {/* En-tête */}
        <header className="bg-card border-b border-border px-6 py-4 flex-shrink-0">
          <div className="flex items-center justify-between gap-4 flex-wrap">
            <div>
              <div className="flex items-center gap-3">
                <h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
                  OPEX DSI — Charges {annee}
                </h1>
                {isValidated && (
                  <span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold bg-emerald-100 text-emerald-700 border border-emerald-200">
                    <Lock className="w-3 h-3" />
                    Validé le {validatedRow?.validatedAt ? new Date(validatedRow.validatedAt).toLocaleDateString('fr-FR') : ''}
                  </span>
                )}
              </div>
              <p className="text-sm text-muted-foreground mt-0.5">
                {lignes.length} postes de charges · {getOpexDataForAnnee(annee).meta.nb_etablissements ?? opexData2026.meta.nb_etablissements} établissements
              </p>
            </div>
            <div className="flex items-center gap-3 flex-wrap">
              <AnneeSelectorBar />
              {!isValidated && lignes.length > 0 && (
                <>
                  <AlertDialog>
                    <AlertDialogTrigger asChild>
                      <button
                        className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-lg border border-destructive/40 bg-card hover:bg-destructive/10 transition-colors text-destructive"
                        title="Supprimer ce prévisionnel"
                      >
                        <Trash2 className="w-3.5 h-3.5" />
                        Supprimer
                      </button>
                    </AlertDialogTrigger>
                    <AlertDialogContent>
                      <AlertDialogHeader>
                        <AlertDialogTitle>Supprimer l'OPEX {annee} ?</AlertDialogTitle>
                        <AlertDialogDescription>
                          Toutes les données du prévisionnel OPEX {annee} seront <strong>définitivement supprimées</strong>. Cette action ne peut pas être annulée.
                        </AlertDialogDescription>
                      </AlertDialogHeader>
                      <AlertDialogFooter>
                        <AlertDialogCancel>Annuler</AlertDialogCancel>
                        <AlertDialogAction
                          onClick={() => deleteAll.mutate({ annee })}
                          className="bg-destructive hover:bg-destructive/90 text-destructive-foreground"
                        >
                          Supprimer définitivement
                        </AlertDialogAction>
                      </AlertDialogFooter>
                    </AlertDialogContent>
                  </AlertDialog>
                  <AlertDialog>
                    <AlertDialogTrigger asChild>
                      <button className="flex items-center gap-1.5 px-4 py-2 text-sm rounded-lg font-medium bg-emerald-600 text-white hover:bg-emerald-700 transition-all shadow-sm">
                        <CheckCircle2 className="w-3.5 h-3.5" />
                        Valider
                      </button>
                    </AlertDialogTrigger>
                    <AlertDialogContent>
                      <AlertDialogHeader>
                        <AlertDialogTitle>Valider l'OPEX {annee} ?</AlertDialogTitle>
                        <AlertDialogDescription>
                          Cette action est <strong>irréversible</strong>. Une fois validé, le prévisionnel OPEX {annee} sera verrouillé et ne pourra plus être modifié.
                        </AlertDialogDescription>
                      </AlertDialogHeader>
                      <AlertDialogFooter>
                        <AlertDialogCancel>Annuler</AlertDialogCancel>
                        <AlertDialogAction onClick={() => validateMutation.mutate({ annee })} className="bg-emerald-600 hover:bg-emerald-700 text-white">
                          Confirmer la validation
                        </AlertDialogAction>
                      </AlertDialogFooter>
                    </AlertDialogContent>
                  </AlertDialog>
                </>
              )}
              {isValidated && (
                <div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-emerald-50 border border-emerald-200 text-emerald-700 text-sm">
                  <Lock className="w-4 h-4" />
                  <span className="font-medium">Prévisionnel verrouillé</span>
                </div>
              )}
            </div>
          </div>
        </header>

        {/* KPIs */}
        {lignes.length > 0 && (
          <div className="px-6 py-4 border-b border-border bg-muted/20 flex-shrink-0">
            <div className="flex flex-wrap gap-3">
              <div className="bg-card border border-border rounded-xl p-4 min-w-[160px] flex-1">
                <div className="flex items-center gap-2 mb-1">
                  <Euro className="w-4 h-4 text-orange-500" />
                  <span className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Total OPEX {annee}</span>
                </div>
                <p className="text-2xl font-bold text-orange-600 tabular-nums" style={{ fontFamily: 'Sora, sans-serif' }}>
                  {formatEuros(totalGlobal)}
                </p>
                <p className="text-xs text-muted-foreground mt-0.5">{isValidated ? 'Validé' : 'Prévisionnel'}</p>
              </div>
              {categoriesPresentes.map(cat => {
                const montant = totalParCategorie[cat] || 0;
                const pct = totalGlobal > 0 ? (montant / totalGlobal * 100).toFixed(1) : '0';
                const colors = getCatColors(cat);
                const tendanceInfo = tendancesParCategorie[cat];
                // Tant que la requête N-1 est active, ne pas présenter de badge
                // temporairement erroné (par exemple « Nouveau »).
                const tendance = loadingPostesN1 ? undefined : tendanceInfo?.tendance;
                const variationPct = tendanceInfo?.variationPct;
                const montantN1 = tendanceInfo?.montantN1;
                return (
                  <div key={cat} className="bg-card border border-border rounded-xl p-4 min-w-[140px] flex-1">
                    <div className="flex items-center justify-between mb-1">
                      <div className="flex items-center gap-2">
                        <BarChart3 className={`w-4 h-4 ${colors.icon}`} />
                        <span className="text-xs text-muted-foreground uppercase tracking-wide font-medium truncate">{cat}</span>
                      </div>
                      {tendance && tendance !== 'stable' && tendance !== 'new' && variationPct !== null && variationPct !== undefined && (
                        <div className={`flex items-center gap-0.5 text-xs font-semibold px-1.5 py-0.5 rounded-full ${
                          tendance === 'hausse' ? 'bg-red-100 text-red-600' : 'bg-green-100 text-green-600'
                        }`}>
                          {tendance === 'hausse' ? <TrendingUp className="w-3 h-3" /> : <TrendingDown className="w-3 h-3" />}
                          <span>{variationPct > 0 ? '+' : ''}{variationPct}%</span>
                        </div>
                      )}
                      {tendance === 'stable' && (
                        <div className="flex items-center gap-0.5 text-xs font-semibold px-1.5 py-0.5 rounded-full bg-orange-100 text-orange-600">
                          <span>≈ stable</span>
                        </div>
                      )}
                      {tendance === 'new' && (
                        <div className="flex items-center gap-0.5 text-xs font-semibold px-1.5 py-0.5 rounded-full bg-blue-100 text-blue-600">
                          <span>Nouveau</span>
                        </div>
                      )}
                    </div>
                    <p className={`text-xl font-bold tabular-nums ${colors.kpi}`} style={{ fontFamily: 'Sora, sans-serif' }}>
                      {formatEuros(montant)}
                    </p>
                    <div className="flex items-center justify-between mt-0.5">
                      <p className="text-xs text-muted-foreground">{pct}% du total</p>
                      {montantN1 !== null && montantN1 !== undefined && montantN1 > 0 && (
                        <p className="text-xs text-muted-foreground">N-1 : {formatEuros(montantN1)}</p>
                      )}
                    </div>
                  </div>
                );
              })}
            </div>
          </div>
        )}

        {/* Barre d'outils */}
        {lignes.length > 0 && (
          <div className="px-6 py-3 border-b border-border bg-background flex-shrink-0 flex flex-wrap items-center gap-3">
            <div className="relative flex-1 min-w-48 max-w-72">
              <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
              <input
                type="text"
                placeholder={viewMode === 'postes' ? 'Rechercher un poste...' : 'Rechercher un établissement...'}
                value={search}
                onChange={e => setSearch(e.target.value)}
                className="w-full pl-9 pr-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary/30"
              />
            </div>

            {viewMode === 'postes' && (
              <div className="flex items-center gap-1.5 flex-wrap">
                <Filter className="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" />
                {CATEGORIES_FILTER.map(cat => (
                  <button
                    key={cat}
                    onClick={() => setSelectedCategorie(cat)}
                    className={`px-2.5 py-1 rounded-full text-xs font-medium transition-all ${
                      selectedCategorie === cat
                        ? 'bg-primary text-white'
                        : 'bg-muted text-muted-foreground hover:bg-muted/80'
                    }`}
                  >
                    {cat}
                  </button>
                ))}
              </div>
            )}

            <div className="ml-auto flex items-center gap-2">
              {viewMode === 'postes' && !isValidated && (
                <button
                  onClick={() => { setDialogMode('add'); setEditingLigne(null); }}
                  className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-primary text-primary-foreground text-xs font-medium hover:bg-primary/90 transition-all shadow-sm"
                >
                  <Plus className="w-3.5 h-3.5" />
                  Ajouter une ligne
                </button>
              )}
              <div className="flex items-center gap-1 bg-muted rounded-lg p-1">
                <button
                  onClick={() => { setViewMode('postes'); setSortCol(null); }}
                  className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all flex items-center gap-1.5 ${
                    viewMode === 'postes' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
                  }`}
                >
                  <BarChart3 className="w-3.5 h-3.5" />
                  Par poste
                </button>
                <button
                  onClick={() => { setViewMode('etablissements'); setSortCol(null); }}
                  className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all flex items-center gap-1.5 ${
                    viewMode === 'etablissements' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
                  }`}
                >
                  <Building2 className="w-3.5 h-3.5" />
                  Par établissement
                </button>
                <button
                  onClick={() => { setViewMode('cles_repartition'); setSortCol(null); }}
                  className={`px-3 py-1.5 rounded-md text-xs font-medium transition-all flex items-center gap-1.5 ${
                    viewMode === 'cles_repartition' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
                  }`}
                >
                  <Key className="w-3.5 h-3.5" />
                  Clés de répartition
                </button>
              </div>
              <button
                onClick={() => setShowDetails(d => !d)}
                className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-border text-xs text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
              >
                {showDetails ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
                {showDetails ? 'Masquer détails' : 'Voir détails'}
              </button>
            </div>
          </div>
        )}

        {/* Contenu principal */}
        <div className="flex-1 min-h-0 px-6 py-4 overflow-y-hidden">

          {/* État vide */}
          {lignes.length === 0 && (
            <div className="flex flex-col items-center justify-center py-24 text-center">
              <div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4">
                <BarChart3 className="w-8 h-8 text-muted-foreground" />
              </div>
              <h3 className="text-lg font-semibold text-foreground mb-1" style={{ fontFamily: 'Sora, sans-serif' }}>
                Aucune donnée pour {annee}
              </h3>
              <p className="text-sm text-muted-foreground mb-6 max-w-sm">
                L'OPEX {annee} n'a pas encore été créé. Vous pouvez le créer en copiant les charges de {annee - 1} ou en ajoutant les lignes manuellement.
              </p>
              <div className="flex items-center gap-3 flex-wrap justify-center">
                {(annee === 2025 || annee === 2026) && (
                  <AlertDialog>
                    <AlertDialogTrigger asChild>
                      <button className="flex items-center gap-2 px-5 py-2.5 rounded-lg bg-primary text-primary-foreground font-medium text-sm hover:bg-primary/90 transition-all shadow-sm">
                        <RotateCcw className="w-4 h-4" />
                        Initialiser depuis les données sources {annee}
                      </button>
                    </AlertDialogTrigger>
                    <AlertDialogContent>
                      <AlertDialogHeader>
                        <AlertDialogTitle>Initialiser l'OPEX {annee} ?</AlertDialogTitle>
                        <AlertDialogDescription>
                          Les données sources {annee} seront chargées en base de données. Vous pourrez ensuite les modifier.
                        </AlertDialogDescription>
                      </AlertDialogHeader>
                      <AlertDialogFooter>
                        <AlertDialogCancel>Annuler</AlertDialogCancel>
                        <AlertDialogAction onClick={handleInitFromSource} className="bg-primary hover:bg-primary/90">
                          Confirmer
                        </AlertDialogAction>
                      </AlertDialogFooter>
                    </AlertDialogContent>
                  </AlertDialog>
                )}
                <AlertDialog>
                  <AlertDialogTrigger asChild>
                    <button className="flex items-center gap-2 px-5 py-2.5 rounded-lg border border-border bg-card font-medium text-sm hover:bg-muted transition-colors">
                      <Plus className="w-4 h-4" />
                      Créer depuis {annee - 1}
                    </button>
                  </AlertDialogTrigger>
                  <AlertDialogContent>
                    <AlertDialogHeader>
                      <AlertDialogTitle>Créer l'OPEX {annee} depuis {annee - 1} ?</AlertDialogTitle>
                      <AlertDialogDescription>
                        Tous les postes de charges de {annee - 1} seront copiés comme base de travail pour {annee}.
                      </AlertDialogDescription>
                    </AlertDialogHeader>
                    <AlertDialogFooter>
                      <AlertDialogCancel>Annuler</AlertDialogCancel>
                      <AlertDialogAction onClick={handleCreateFromN1} className="bg-primary hover:bg-primary/90">
                        Confirmer
                      </AlertDialogAction>
                    </AlertDialogFooter>
                  </AlertDialogContent>
                </AlertDialog>
                <button
                  onClick={() => { setDialogMode('add'); setEditingLigne(null); }}
                  className="flex items-center gap-2 px-5 py-2.5 rounded-lg border border-border bg-card font-medium text-sm hover:bg-muted transition-colors"
                >
                  <Plus className="w-4 h-4" />
                  Ajouter une ligne manuellement
                </button>
              </div>
            </div>
          )}

          {/* Vue par poste */}
          {viewMode === 'postes' && lignes.length > 0 && (
            <div className="space-y-2">
              {selectedCategorie !== 'Toutes' && (
                <div className="flex items-center justify-between mb-3 px-1">
                  <span className="text-sm text-muted-foreground">
                    {sortedLignes.length} poste{sortedLignes.length > 1 ? 's' : ''} — catégorie <strong>{selectedCategorie}</strong>
                  </span>
                  <span className="text-sm font-semibold text-foreground">
                    Sous-total : <span className="text-primary">{formatEuros(totalFiltre)}</span>
                  </span>
                </div>
              )}

              {!isValidated && (
                <div className="flex items-center gap-2 mb-3 px-1 py-2 rounded-lg bg-amber-50 border border-amber-200 text-amber-700 text-xs">
                  <PencilLine className="w-3.5 h-3.5 flex-shrink-0" />
                  <span>Les montants sont modifiables. Chaque modification est sauvegardée automatiquement en base de données.</span>
                </div>
              )}

              <div className="bg-card border border-border rounded-xl" style={{ maxHeight: 'calc(100vh - 380px)', overflowY: 'auto', overflowX: 'auto' }}>
                <table className="w-full text-sm">
                  <thead>
                    <tr className="border-b border-border" style={{ position: 'sticky', top: 0, zIndex: 20 }}>
                      <th className="text-left px-4 py-3 font-medium text-muted-foreground w-8" style={{ background: 'oklch(0.94 0.005 80)' }}>#</th>
                      <th className="text-left px-4 py-3 font-medium text-muted-foreground" style={{ background: 'oklch(0.94 0.005 80)' }}>
                        <SortBtn col="libelle" label="Poste de charge" />
                      </th>
                      <th className="text-left px-4 py-3 font-medium text-muted-foreground hidden md:table-cell" style={{ background: 'oklch(0.94 0.005 80)' }}>Catégorie</th>
                      <th className="text-left px-4 py-3 font-medium text-muted-foreground hidden xl:table-cell" style={{ background: 'oklch(0.94 0.005 80)' }}>Facturation</th>
                      <th className="text-left px-4 py-3 font-medium text-muted-foreground hidden xl:table-cell" style={{ background: 'oklch(0.94 0.005 80)' }}>Ventilation</th>
                      {showDetails && <th className="text-left px-3 py-3 font-medium text-muted-foreground hidden lg:table-cell w-20 max-w-[80px]" style={{ background: 'oklch(0.94 0.005 80)' }}>Type</th>}
                      {showDetails && <th className="text-left px-4 py-3 font-medium text-muted-foreground hidden xl:table-cell" style={{ background: 'oklch(0.94 0.005 80)' }}>Compte</th>}
                      <th className="text-right px-4 py-3 font-medium text-muted-foreground" style={{ background: 'oklch(0.94 0.005 80)' }}>Budget N-1</th>
                      <th className="text-right px-4 py-3 font-medium text-muted-foreground" style={{ minWidth: '140px', background: 'oklch(0.94 0.005 80)' }}>
                        <SortBtn col="montant" label={`Prév. ${annee}`} />
                      </th>
                      <th className="text-center px-3 py-3 font-medium text-muted-foreground w-24" style={{ background: 'oklch(0.94 0.005 80)' }}>Tendance</th>
                      <th className="text-right px-4 py-3 font-medium text-muted-foreground w-24" style={{ background: 'oklch(0.94 0.005 80)' }}>Répartition</th>
                      {!isValidated && <th className="px-4 py-3 w-20" style={{ background: 'oklch(0.94 0.005 80)' }}></th>}
                    </tr>
                  </thead>
                  <tbody>
                    {sortedLignes.map((ligne, idx) => {
                      const pct = totalGlobal > 0 ? (ligne.montant / totalGlobal) * 100 : 0;
                      const isExpanded = expandedPoste === ligne.id;
                      const catColors = getCatColors(ligne.categorie);

                      return (
                        <React.Fragment key={ligne.id}>
                          <tr className={`border-b border-border/50 hover:bg-muted/30 transition-colors ${isExpanded ? 'bg-muted/20' : ''} ${ligne.isCustom ? 'bg-blue-50/30' : ''}`}>
                            <td className="px-4 py-3 text-muted-foreground text-xs">{idx + 1}</td>
                            <td
                              className="px-4 py-3 cursor-pointer"
                              onClick={() => setExpandedPoste(isExpanded ? null : ligne.id)}
                            >
                              <div className="flex items-center gap-2">
                                {isExpanded ? <ChevronUp className="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" /> : <ChevronDown className="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" />}
                                <div>
                                  <p className="font-medium text-foreground leading-tight">{ligne.libelle}</p>
                                  {ligne.fournisseur && ligne.fournisseur !== ligne.libelle && (
                                    <p className="text-xs text-muted-foreground mt-0.5">{ligne.fournisseur}</p>
                                  )}
                                  {ligne.isCustom && (
                                    <span className="text-xs text-blue-500 font-medium">Ajouté manuellement</span>
                                  )}
                                </div>
                              </div>
                            </td>
                            <td className="px-4 py-3 hidden md:table-cell">
                              <span className={`inline-flex px-2 py-0.5 rounded-full text-xs font-medium ${catColors.badge}`}>
                                {ligne.categorie}
                              </span>
                            </td>
                            <td className="px-4 py-3 text-sm text-muted-foreground hidden xl:table-cell">{ligne.facturation || '—'}</td>
                            <td className="px-4 py-3 text-xs hidden xl:table-cell">
                              {isValidated ? (
                                <span className={`inline-flex px-2 py-0.5 rounded-full text-xs font-medium ${
                                  ligne.mode_ventilation === 'Manuel' ? 'bg-blue-100 text-blue-700' : 'bg-emerald-100 text-emerald-700'
                                }`}>{ligne.mode_ventilation === 'Manuel' ? 'Manuel' : 'Prorata C 6'}</span>
                              ) : (
                                <button
                                  onClick={() => handleToggleVentilation(ligne)}
                                  title="Cliquer pour basculer Prorata C 6 ↔ Manuel"
                                  className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium border transition-colors hover:opacity-80 cursor-pointer ${
                                    ligne.mode_ventilation === 'Manuel'
                                      ? 'bg-blue-100 text-blue-700 border-blue-300 hover:bg-blue-200'
                                      : 'bg-emerald-100 text-emerald-700 border-emerald-300 hover:bg-emerald-200'
                                  }`}
                                >
                                  {ligne.mode_ventilation === 'Manuel' ? '🔒' : '🔓'}
                                  {ligne.mode_ventilation === 'Manuel' ? 'Manuel' : 'Prorata C 6'}
                                </button>
                              )}
                            </td>
                            {showDetails && <td className="px-3 py-3 text-xs text-muted-foreground hidden lg:table-cell w-20 max-w-[80px] truncate" title={ligne.type || ''}>{ligne.type || '—'}</td>}
                            {showDetails && <td className="px-4 py-3 text-xs font-mono text-muted-foreground hidden xl:table-cell">{ligne.compte || '—'}</td>}
                            <td className="px-4 py-3 text-right text-sm text-muted-foreground tabular-nums">
                              {formatNum(ligne.budget_n1)}
                            </td>
                            <td className="px-4 py-2 text-right" style={{ minWidth: '140px' }}>
                              {isValidated ? (
                                <span className={`font-semibold tabular-nums ${ligne.montant > 0 ? 'text-foreground' : 'text-muted-foreground'}`}>
                                  {formatNum(ligne.montant)}
                                </span>
                              ) : (
                                <MoneyInput
                                  value={ligne.montant}
                                  onChange={v => handleMontantChange(ligne, v)}
                                  disabled={isValidated}
                                />
                              )}
                            </td>
                            <td className="px-3 py-3 text-center">
                              {(() => {
                                const n1 = ligne.budget_n1;
                                const n = ligne.montant;
                                if (!n1 || n1 === 0) {
                                  return n > 0
                                    ? <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-600">Nouveau</span>
                                    : <span className="text-muted-foreground text-xs">—</span>;
                                }
                                const diff = n - n1;
                                const pctVar = (diff / n1) * 100;
                                if (Math.abs(pctVar) < 2) {
                                  return (
                                    <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-orange-100 text-orange-600">
                                      <span>≈ stable</span>
                                    </span>
                                  );
                                } else if (diff > 0) {
                                  return (
                                    <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-red-100 text-red-600">
                                      <TrendingUp className="w-3 h-3" />
                                      <span>+{pctVar.toFixed(1)}%</span>
                                    </span>
                                  );
                                } else {
                                  return (
                                    <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-600">
                                      <TrendingDown className="w-3 h-3" />
                                      <span>{pctVar.toFixed(1)}%</span>
                                    </span>
                                  );
                                }
                              })()}
                            </td>
                            <td className="px-4 py-3">
                              <div className="flex items-center gap-2 justify-end">
                                <div className="w-16 bg-muted rounded-full h-1.5 overflow-hidden">
                                  <div
                                    className={`h-1.5 rounded-full transition-all ${catColors.bar}`}
                                    style={{ width: `${Math.min(100, pct * 5)}%` }}
                                  />
                                </div>
                                <span className="text-xs text-muted-foreground tabular-nums w-10 text-right">
                                  {pct.toFixed(1)}%
                                </span>
                              </div>
                            </td>
                            {!isValidated && (
                              <td className="px-2 py-2">
                                <div className="flex items-center gap-1 justify-end">
                                  <button
                                    onClick={() => { setEditingLigne(ligne); setDialogMode('edit'); }}
                                    className="p-1.5 rounded-md text-muted-foreground hover:text-primary hover:bg-primary/10 transition-colors"
                                    title="Modifier"
                                  >
                                    <Pencil className="w-3.5 h-3.5" />
                                  </button>
                                  <button
                                    onClick={() => setDeleteConfirmId(ligne.id)}
                                    className="p-1.5 rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors"
                                    title="Supprimer"
                                  >
                                    <Trash2 className="w-3.5 h-3.5" />
                                  </button>
                                </div>
                              </td>
                            )}
                          </tr>
                          {isExpanded && ligne.detail && (
                            <tr key={`${ligne.id}-detail`} className="bg-blue-50/50 border-b border-border/50">
                              <td />
                              <td colSpan={!isValidated ? 9 : 8} className="px-8 py-3">
                                <div className="flex items-start gap-2 text-sm text-blue-700">
                                  <Info className="w-4 h-4 mt-0.5 flex-shrink-0" />
                                  <div>
                                    <p className="font-medium">Détail</p>
                                    <p className="text-blue-600 mt-0.5">{ligne.detail}</p>
                                  </div>
                                </div>
                              </td>
                            </tr>
                          )}
                        </React.Fragment>
                      );
                    })}
                  </tbody>
                  <tfoot>
                    <tr className="bg-muted/40 border-t-2 border-border">
                      <td colSpan={showDetails ? (isValidated ? 9 : 10) : (isValidated ? 6 : 7)} className="px-4 py-3 font-bold text-foreground">
                        TOTAL {selectedCategorie !== 'Toutes' ? selectedCategorie : 'OPEX DSI'} {annee}
                      </td>
                      <td className="px-4 py-3 text-right font-bold text-orange-600 text-base tabular-nums">
                        {formatEuros(totalFiltre)}
                      </td>
                      <td className="px-4 py-3 text-right text-sm text-muted-foreground">
                        {selectedCategorie !== 'Toutes'
                          ? `${totalGlobal > 0 ? ((totalFiltre / totalGlobal) * 100).toFixed(1) : '0'}%`
                          : '100%'}
                      </td>
                      {!isValidated && <td />}
                    </tr>
                  </tfoot>
                </table>
              </div>
            </div>
          )}

          {/* Vue par établissement */}
          {viewMode === 'etablissements' && lignes.length > 0 && (
            <div className="space-y-3">
              <div className="flex items-center gap-4 px-1 text-xs text-muted-foreground">
                <div className="flex items-center gap-1.5">
                  <span className="inline-block w-3 h-3 rounded border-2 border-emerald-500 bg-emerald-50"></span>
                  <span>Calcul automatique (prorata classe 6)</span>
                </div>
                <div className="flex items-center gap-1.5">
                  <span className="inline-block w-3 h-3 rounded border-2 border-blue-500 bg-blue-50"></span>
                  <span>Saisie manuelle</span>
                </div>
              </div>

              <div className="bg-card border border-border rounded-xl overflow-hidden">
                <div className="overflow-x-auto overflow-y-auto" style={{ isolation: 'isolate', maxHeight: 'calc(100vh - 320px)' }}>
                  <table className="text-xs border-separate border-spacing-0" style={{ minWidth: 'max-content', width: '100%' }}>
                    <thead>
                      <tr className="border-b border-border bg-muted/40">
                        <th className="text-left px-3 py-3 font-medium text-muted-foreground" style={{ position: 'sticky', left: 0, top: 0, zIndex: 40, background: 'oklch(0.94 0.005 80)', boxShadow: '2px 0 4px -1px rgba(0,0,0,0.15)', width: '110px', minWidth: '110px', maxWidth: '110px' }}>
                          <SortBtn col="code" label="Code" />
                        </th>
                        <th className="text-left px-3 py-3 font-medium text-muted-foreground" style={{ position: 'sticky', left: '110px', top: 0, zIndex: 40, background: 'oklch(0.94 0.005 80)', boxShadow: '2px 0 8px -2px rgba(0,0,0,0.2)', width: '220px', minWidth: '220px', maxWidth: '220px' }}>
                          <SortBtn col="nom" label="Établissement" />
                        </th>
                        {lignes.map(ligne => {
                          const isManuel = ligne.mode_ventilation === 'Manuel' || ligne.isCustom;
                          return (
                            <th
                              key={ligne.id}
                              style={{
                                position: 'sticky',
                                top: 0,
                                zIndex: 35,
                                background: isManuel ? 'rgb(239 246 255)' : 'rgb(240 253 244)',
                                borderBottom: isManuel ? '2px solid rgb(96 165 250)' : '2px solid rgb(52 211 153)'
                              }}
                              className={`text-right px-2 py-2 font-medium min-w-[90px] max-w-[110px] ${
                                isManuel ? 'text-blue-700' : 'text-emerald-700'
                              }`}
                              title={`${ligne.libelle} — ${isManuel ? 'Saisie manuelle' : 'Prorata C 6'} — ${formatEuros(ligne.montant)}`}
                            >
                              <span className="block font-semibold text-[11px] leading-tight">
                                {ligne.libelleCourt ?? ligne.libelle.split(' ')[0]}
                              </span>
                              {ligne.libelleDetail && (
                                <span className="block text-[10px] font-normal opacity-80 leading-tight mt-0.5">
                                  {ligne.libelleDetail}
                                </span>
                              )}
                              <span className="block text-[10px] font-normal opacity-60 mt-0.5">{formatEuros(ligne.montant)}</span>
                            </th>
                          );
                        })}
                        <th className="text-right px-3 py-3 font-medium text-muted-foreground" style={{ position: 'sticky', right: 0, top: 0, zIndex: 40, background: 'oklch(0.94 0.005 80)', boxShadow: '-2px 0 8px -2px rgba(0,0,0,0.2)', width: '120px', minWidth: '120px' }}>
                          <SortBtn col="total" label={`Total ${annee}`} />
                        </th>
                      </tr>
                    </thead>
                    <tbody>
                      {sortedEtabs.map((etab, idx) => {
                        const rowBg = etab.modeManuel ? 'oklch(0.98 0.02 80)' : (idx % 2 === 0 ? 'oklch(1 0 0)' : 'oklch(0.97 0.003 80)');
                        return (
                        <tr key={etab.code} className={`border-b border-border/50 hover:bg-muted/20 transition-colors ${etab.modeManuel ? 'bg-amber-50/40' : idx % 2 === 0 ? '' : 'bg-muted/5'}`}>
                          <td className="px-3 py-2 whitespace-nowrap" style={{ position: 'sticky', left: 0, zIndex: 20, background: rowBg, boxShadow: '2px 0 4px -1px rgba(0,0,0,0.12)', width: '110px', minWidth: '110px', maxWidth: '110px' }}>
                            <span className="font-mono bg-muted px-1.5 py-0.5 rounded text-muted-foreground">{etab.code}</span>
                          </td>
                          <td className="px-3 py-2 font-medium text-foreground whitespace-nowrap overflow-hidden text-ellipsis" style={{ position: 'sticky', left: '110px', zIndex: 20, background: rowBg, boxShadow: '2px 0 8px -2px rgba(0,0,0,0.2)', width: '220px', minWidth: '220px', maxWidth: '220px' }} title={etab.nom}>
                            {etab.nom}
                            {etab.modeManuel && (
                              <span className="ml-1.5 inline-flex items-center px-1 py-0.5 rounded text-[10px] font-semibold bg-amber-100 text-amber-700 border border-amber-200" title="Mode tout manuel activé — montants saisis manuellement, pas de calcul prorata">
                                Manuel
                              </span>
                            )}
                          </td>
                          {lignes.map(ligne => {
                            const isManuel = ligne.mode_ventilation === 'Manuel' || ligne.isCustom;
                            // En mode tout manuel pour cet établissement, toutes les cellules sont éditables
                            const isCellEditable = etab.modeManuel || isManuel;
                            const montant = etab.montants[ligne.libelle] ?? 0;
                            const cellKey = `${etab.code}|${ligne.libelle}`;
                            const isEditing = inlineEditKey === cellKey;
                            const hasOverride = montantsManuelEtab[etab.code]?.[ligne.libelle] !== undefined;

                            if (isCellEditable && isEditing) {
                              return (
                                <td key={ligne.id} className="px-1 py-1 border border-amber-400 bg-amber-50 min-w-[90px]">
                                  <input
                                    autoFocus
                                    type="number"
                                    min={0}
                                    step={1}
                                    value={inlineEditVal}
                                    placeholder="0"
                                    onChange={e => setInlineEditVal(e.target.value)}
                                    onBlur={() => handleInlineEditCommit(etab.code, ligne.libelle)}
                                    onKeyDown={e => {
                                      if (e.key === 'Enter') handleInlineEditCommit(etab.code, ligne.libelle);
                                      if (e.key === 'Escape') handleInlineEditCancel();
                                    }}
                                    className="w-full text-right text-xs tabular-nums px-1 py-0.5 border-0 bg-transparent focus:outline-none text-amber-800 font-medium"
                                    style={{ minWidth: '70px' }}
                                  />
                                </td>
                              );
                            }

                            return (
                              <td
                                key={ligne.id}
                                onClick={() => isCellEditable && !isValidated && handleInlineEditStart(etab.code, ligne.libelle, montant)}
                                className={`px-2 py-2 text-right tabular-nums transition-colors ${
                                  etab.modeManuel
                                    ? `border border-amber-200 bg-amber-50/30 text-amber-800 ${
                                        !isValidated ? 'cursor-pointer hover:bg-amber-100/60 hover:border-amber-400' : ''
                                      } ${hasOverride ? 'font-semibold' : 'opacity-60'}`
                                    : isManuel
                                      ? `border border-blue-200 bg-blue-50/30 text-blue-800 ${
                                          !isValidated ? 'cursor-pointer hover:bg-blue-100/60 hover:border-blue-400' : ''
                                        } ${hasOverride ? 'font-semibold' : ''}`
                                      : 'border border-emerald-200 bg-emerald-50/20 text-emerald-800'
                                }`}
                                title={
                                  etab.modeManuel && !isValidated
                                    ? 'Cliquer pour saisir le montant (mode tout manuel)'
                                    : isManuel && !isValidated
                                      ? 'Cliquer pour saisir le montant'
                                      : undefined
                                }
                              >
                                {montant > 0
                                  ? new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(montant) + ' €'
                                  : isCellEditable && !isValidated
                                    ? <span className={etab.modeManuel ? 'text-amber-300 text-[10px]' : 'text-blue-300 text-[10px]'}>cliquer</span>
                                    : <span className="text-muted-foreground">—</span>}
                              </td>
                            );
                          })}
                          <td className="px-3 py-2 text-right font-semibold tabular-nums" style={{ position: 'sticky', right: 0, zIndex: 20, background: idx % 2 === 0 ? 'oklch(1 0 0)' : 'oklch(0.97 0.003 80)', boxShadow: '-3px 0 8px -2px rgba(0,0,0,0.18)', width: '120px', minWidth: '120px' }}>
                            <span className={etab.total > 0 ? 'text-orange-600' : 'text-muted-foreground'}>
                              {etab.total > 0 ? formatEuros(etab.total) : '—'}
                            </span>
                          </td>
                        </tr>
                        );
                      })}
                    </tbody>
                    <tfoot>
                      <tr className="bg-muted/40 border-t-2 border-border">
                        <td colSpan={2} className="px-3 py-3 font-bold text-foreground" style={{ position: 'sticky', left: 0, zIndex: 20, background: 'oklch(0.94 0.005 80)', boxShadow: '2px 0 8px -2px rgba(0,0,0,0.2)', width: '330px', minWidth: '330px' }}>
                          TOTAL — {sortedEtabs.length} établissements
                        </td>
                        {lignes.map(ligne => {
                          const isManuel = ligne.mode_ventilation === 'Manuel' || ligne.isCustom;
                          // Utiliser le montant du poste (source BDD) pour éviter les écarts d'arrondis
                          const montantPoste = ligne.montant;
                          return (
                            <td
                              key={ligne.id}
                              className={`px-2 py-3 text-right font-semibold tabular-nums ${
                                isManuel ? 'text-blue-700 bg-blue-50/40' : 'text-emerald-700 bg-emerald-50/30'
                              }`}
                            >
                              {montantPoste > 0
                                ? new Intl.NumberFormat('fr-FR', { maximumFractionDigits: 0 }).format(montantPoste) + ' €'
                                : '—'}
                            </td>
                          );
                        })}
                        <td className="px-3 py-3 text-right font-bold text-orange-600 text-sm tabular-nums" style={{ position: 'sticky', right: 0, zIndex: 20, background: 'oklch(0.94 0.005 80)', boxShadow: '-3px 0 8px -2px rgba(0,0,0,0.18)', width: '120px', minWidth: '120px' }}>
                          {/* Total depuis les montants BDD — pas la somme des arrondis par établissement */}
                          {formatEuros(totalGlobal)}
                        </td>
                      </tr>
                    </tfoot>
                  </table>
                </div>
              </div>
            </div>
          )}
          {/* Vue Clés de répartition */}
          {viewMode === 'cles_repartition' && (
            <ClesRepartitionView
              annee={annee}
              basesRepartitionRaw={basesRepartitionRaw ?? []}
              isLoadingBases={isLoadingBases}
              isErrorBases={isErrorBases}
              setBaseRepartition={setBaseRepartition}
              importBasesRepartition={importBasesRepartition}
              batchSetMontantsEtab={batchSetMontantsEtab}
              etablissementsRecalcules={etablissementsRecalcules}
            />
          )}
        </div>
      </main>

      {/* Modale ajout / édition */}
      <Dialog open={dialogMode !== null} onOpenChange={open => { if (!open) { setDialogMode(null); setEditingLigne(null); } }}>
        <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
          <DialogHeader>
            <DialogTitle className="flex items-center gap-2">
              {dialogMode === 'add' ? <Plus className="w-4 h-4" /> : <Pencil className="w-4 h-4" />}
              {dialogMode === 'add' ? 'Ajouter une ligne OPEX' : 'Modifier la ligne'}
            </DialogTitle>
          </DialogHeader>
          {dialogMode === 'add' && (
            <LigneForm
              initial={EMPTY_LIGNE_FORM}
              onSave={handleAddLigne}
              onClose={() => setDialogMode(null)}
            />
          )}
          {dialogMode === 'edit' && editingLigne && (
            <LigneForm
              initial={{
                libelle: editingLigne.libelle,
                fournisseur: editingLigne.fournisseur,
                categorie: editingLigne.categorie,
                type: editingLigne.type,
                facturation: editingLigne.facturation,
                compte: editingLigne.compte,
                detail: editingLigne.detail,
                budget_n1: editingLigne.budget_n1,
                montant: editingLigne.montant,
              }}
              onSave={handleEditLigne}
              onClose={() => { setDialogMode(null); setEditingLigne(null); }}
            />
          )}
        </DialogContent>
      </Dialog>

      {/* Confirmation suppression */}
      <AlertDialog open={deleteConfirmId !== null} onOpenChange={open => { if (!open) setDeleteConfirmId(null); }}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Supprimer cette ligne ?</AlertDialogTitle>
            <AlertDialogDescription>
              Cette action est irréversible. La ligne sera définitivement retirée du prévisionnel OPEX {annee}.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Annuler</AlertDialogCancel>
            <AlertDialogAction
              onClick={handleDeleteLigne}
              className="bg-destructive hover:bg-destructive/90 text-white"
            >
              Supprimer
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </div>
  );
}
