import { useState, useRef, useEffect, useCallback, useMemo, memo, Fragment } from 'react'
import { Terminal as XTerm } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import { WebLinksAddon } from '@xterm/addon-web-links'
import { WebglAddon } from '@xterm/addon-webgl'
import { SearchAddon } from '@xterm/addon-search'
import { Unicode11Addon } from '@xterm/addon-unicode11'
import { ImageAddon } from '@xterm/addon-image'
import { Plus, X, Monitor, Globe, ChevronDown, Pencil, Trash2, Search, Copy, Send, Eye, Bot } from 'lucide-react'
import '@xterm/xterm/css/xterm.css'
import { useI18n } from '../i18n'
import mermaid from 'mermaid'

mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose', fontFamily: 'var(--font-mono)' })

const AI_TAB_ID = 0
const MAX_TABS = 7
const SHELL_MAX_TOKENS = 100000
const SHELL_AI_COMMANDS = ['/clear', '/help', '/model', '/model change']
const TABS_STORAGE_KEY = 'muyue_shell_tabs'
const TERMINAL_BUFFER_KEY = 'muyue_terminal_buffers'

function renderContent(text) {
  const parts = []
  const codeBlockRegex = /(```[\s\S]*?```)/g
  let match
  let lastIndex = 0
  while ((match = codeBlockRegex.exec(text)) !== null) {
    if (match.index > lastIndex) {
      parts.push({ type: 'text', content: text.slice(lastIndex, match.index) })
    }
    const full = match[1]
    const firstNewline = full.indexOf('\n')
    const lang = firstNewline > -1 ? full.slice(3, firstNewline).trim() : ''
    const code = firstNewline > -1 ? full.slice(firstNewline + 1, -3) : full.slice(3, -3)
    parts.push({ type: 'code', lang, content: code })
    lastIndex = match.index + full.length
  }
  if (lastIndex < text.length) {
    const remaining = text.slice(lastIndex)
    const openBlock = remaining.match(/```(\w*)\n?([\s\S]*)$/)
    if (openBlock) {
      if (openBlock.index > 0) {
        parts.push({ type: 'text', content: remaining.slice(0, openBlock.index) })
      }
      parts.push({ type: 'code', lang: openBlock[1] || '', content: openBlock[2] || '' })
    } else {
      parts.push({ type: 'text', content: remaining })
    }
  }
  return parts
}

function formatText(text) {
  let html = text
    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')

  html = html.replace(/^(\|.+\|)\n(\|[\s\-:|]+\|)\n((?:\|.+\|\n?)+)/gm, (match, headerRow, sepRow, bodyRows) => {
    const headers = headerRow.split('|').filter(c => c.trim() !== '').map(c => `<th>${c.trim()}</th>`).join('')
    const rows = bodyRows.trim().split('\n').map(row => {
      const cells = row.split('|').filter(c => c.trim() !== '').map(c => `<td>${c.trim()}</td>`).join('')
      return `<tr>${cells}</tr>`
    }).join('')
    return `<table><thead><tr>${headers}</tr></thead><tbody>${rows}</tbody></table>`
  })

  html = html
    .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
    .replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
    .replace(/^### (.+)$/gm, '<h4 class="msg-h4">$1</h4>')
    .replace(/^## (.+)$/gm, '<h3 class="msg-h3">$1</h3>')
    .replace(/^# (.+)$/gm, '<h2 class="msg-h2">$1</h2>')
    .replace(/^---+$/gm, '<hr>')
    .replace(/^\s*[-*] (.+)$/gm, '<div class="msg-bullet">\u2022 $1</div>')
    .replace(/^\s*(\d+)[.)] (.+)$/gm, '<div class="msg-step"><span class="msg-step-num">$1</span> $2</div>')
    .replace(/\n/g, '<br/>')

  html = html
    .replace(/<br\/>\s*<br\/>/g, '<br/>')
    .replace(/<br\/>\s*(<h[234]|<div class="msg-|<table|<hr)/g, '$1')
    .replace(/(<\/h[234]|<\/div>|<\/table>|<hr>)\s*<br\/>/g, '$1')
    .replace(/\s+on\w+=["'][^"']*["']/gi, '')
    .replace(/javascript:/gi, '')
    .replace(/data:/gi, '')

  return html
}

// === Style thème système pour xterm ===
function getCSSVariable(varName) {
  if (typeof document === 'undefined') return null;
  return getComputedStyle(document.documentElement).getPropertyValue(varName).trim() || null;
}

function parseHexColor(hex) {
  if (!hex || hex.startsWith('var(')) return null;
  hex = hex.replace('#', '');
  if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
  if (hex.length !== 6) return null;
  const r = parseInt(hex.slice(0, 2), 16);
  const g = parseInt(hex.slice(2, 4), 16);
  const b = parseInt(hex.slice(4, 6), 16);
  return { r, g, b };
}

function toRgbString(hex) {
  const c = parseHexColor(hex);
  if (!c) return '#000000';
  return `#${c.r.toString(16).padStart(2, '0')}${c.g.toString(16).padStart(2, '0')}${c.b.toString(16).padStart(2, '0')}`;
}

function buildSystemTheme() {
  const bg = getCSSVariable('--bg-base') || '#0F0D10';
  const fg = getCSSVariable('--text-primary') || '#EAE0E2';
  const accent = getCSSVariable('--accent-light') || '#FF1A5E';
  const accentDim = getCSSVariable('--accent-dim') || '#6B2033';
  const success = '#00E676';
  const warning = '#FFD740';
  const error = getCSSVariable('--accent-bright') || '#FF1744';
  const bgSurface = getCSSVariable('--bg-surface') || bg;
  const bgElevated = getCSSVariable('--bg-elevated') || bgSurface;
  const textSecondary = getCSSVariable('--text-secondary') || fg;
  const textTertiary = getCSSVariable('--text-tertiary') || textSecondary;
  
  return {
    background: toRgbString(bg),
    foreground: toRgbString(fg),
    cursor: toRgbString(accent),
    cursorAccent: toRgbString(bg),
    selectionBackground: `${toRgbString(accentDim)}44`,
    selectionForeground: '#FFFFFF',
    black: toRgbString(bgElevated),
    red: toRgbString(error),
    green: toRgbString(success),
    yellow: toRgbString(warning),
    blue: toRgbString(getCSSVariable('--accent') || '#448AFF'),
    magenta: toRgbString(accent),
    cyan: '#00BCD4',
    white: toRgbString(fg),
    brightBlack: toRgbString(bgSurface),
    brightRed: toRgbString(accent),
    brightGreen: toRgbString(success),
    brightYellow: toRgbString(warning),
    brightBlue: toRgbString(getCSSVariable('--accent-muted') || '#82B1FF'),
    brightMagenta: toRgbString(getCSSVariable('--accent-soft') || '#FF80AB'),
    brightCyan: '#84FFFF',
    brightWhite: '#FFFFFF',
  };
}

const THEMES = {
  system: buildSystemTheme(),
  default: {
    background: '#0A0A0C', foreground: '#EAE0E2', cursor: '#FF0033',
    cursorAccent: '#0A0A0C', selectionBackground: '#FF003344', selectionForeground: '#ffffff',
    black: '#0A0A0C', red: '#FF0033', green: '#00E676', yellow: '#FFD740',
    blue: '#448AFF', magenta: '#FF1A5E', cyan: '#00BCD4', white: '#EAE0E2',
    brightBlack: '#5A4F52', brightRed: '#FF5252', brightGreen: '#69F0AE',
    brightYellow: '#FFFF00', brightBlue: '#82B1FF', brightMagenta: '#FF80AB',
    brightCyan: '#84FFFF', brightWhite: '#FFFFFF',
  },
  monokai: {
    background: '#272822', foreground: '#F8F8F2', cursor: '#F8F8F0',
    cursorAccent: '#272822', selectionBackground: '#F8F8F244', selectionForeground: '#ffffff',
    black: '#272822', red: '#F92672', green: '#A6E22E', yellow: '#E6DB74',
    blue: '#66D9EF', magenta: '#AE81FF', cyan: '#A1EFE4', white: '#F8F8F2',
    brightBlack: '#75715E', brightRed: '#F92672', brightGreen: '#A6E22E',
    brightYellow: '#E6DB74', brightBlue: '#66D9EF', brightMagenta: '#AE81FF',
    brightCyan: '#A1EFE4', brightWhite: '#F8F8F2',
  },
  gruvbox: {
    background: '#282828', foreground: '#EBDBB2', cursor: '#FB4934',
    cursorAccent: '#282828', selectionBackground: '#EBDBB244', selectionForeground: '#ffffff',
    black: '#282828', red: '#CC241D', green: '#98971A', yellow: '#D79921',
    blue: '#458588', magenta: '#B16286', cyan: '#689D6A', white: '#EBDBB2',
    brightBlack: '#928374', brightRed: '#FB4934', brightGreen: '#B8BB26',
    brightYellow: '#FABC2A', brightBlue: '#83A598', brightMagenta: '#D3869B',
    brightCyan: '#8EC07C', brightWhite: '#EBDBB2',
  },
  nord: {
    background: '#2E3440', foreground: '#D8DEE9', cursor: '#D8DEE9',
    cursorAccent: '#2E3440', selectionBackground: '#D8DEE944', selectionForeground: '#ffffff',
    black: '#2E3440', red: '#BF616A', green: '#A3BE8C', yellow: '#EBCB8B',
    blue: '#81A1C1', magenta: '#B48EAD', cyan: '#88C0D0', white: '#D8DEE9',
    brightBlack: '#4C566A', brightRed: '#BF616A', brightGreen: '#A3BE8C',
    brightYellow: '#EBCB8B', brightBlue: '#81A1C1', brightMagenta: '#B48EAD',
    brightCyan: '#8FBCBB', brightWhite: '#ECEFF4',
  },
  'solarized-dark': {
    background: '#002B36', foreground: '#839496', cursor: '#D33682',
    cursorAccent: '#002B36', selectionBackground: '#83949644', selectionForeground: '#ffffff',
    black: '#002B36', red: '#DC322F', green: '#859900', yellow: '#B58900',
    blue: '#268BD2', magenta: '#D33682', cyan: '#2AA198', white: '#FDF6E3',
    brightBlack: '#073642', brightRed: '#CB4B16', brightGreen: '#586E75',
    brightYellow: '#657B83', brightBlue: '#6C71C4', brightMagenta: '#6C71C4',
    brightCyan: '#93A1A1', brightWhite: '#FDF6E3',
  },
  dracula: {
    background: '#282A36', foreground: '#F8F8F2', cursor: '#F8F8F2',
    cursorAccent: '#282A36', selectionBackground: '#F8F8F244', selectionForeground: '#ffffff',
    black: '#282A36', red: '#FF5555', green: '#50FA7B', yellow: '#F1FA8C',
    blue: '#BD93F9', magenta: '#FF79C6', cyan: '#8BE9FD', white: '#F8F8F2',
    brightBlack: '#6272A4', brightRed: '#FF6E6E', brightGreen: '#69FF94',
    brightYellow: '#FFFFA5', brightBlue: '#D6ACFF', brightMagenta: '#FF92DF',
    brightCyan: '#A4FFFF', brightWhite: '#FFFFFF',
  },
}

function getTheme(themeName) {
  if (themeName === 'system' || themeName === 'default') {
    return buildSystemTheme()
  }
  return THEMES[themeName] || buildSystemTheme()
}

function createTerminal(container, settings = {}) {
  const theme = getTheme(settings.theme || 'system')
  const actualFontSize = settings.fontSize || 14
  const term = new XTerm({
    cursorBlink: true,
    allowProposedApi: true,
    fontSize: actualFontSize,
    fontFamily: settings.fontFamily || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
    theme,
    allowTransparency: false,
    scrollback: 5000,
    bracketedPaste: false,
  })

  const fitAddon = new FitAddon()
  const webLinksAddon = new WebLinksAddon()
  const searchAddon = new SearchAddon()
  const unicode11Addon = new Unicode11Addon()
  const imageAddon = new ImageAddon()

  term.loadAddon(fitAddon)
  term.loadAddon(webLinksAddon)
  term.loadAddon(searchAddon)
  term.loadAddon(unicode11Addon)
  term.loadAddon(imageAddon)

  term.unicode.activeVersion = '11'

  try {
    const webglAddon = new WebglAddon()
    webglAddon.onContextLoss(() => { webglAddon.dispose() })
    term.loadAddon(webglAddon)
  } catch (e) {
    console.warn('[Shell] WebGL renderer not available, using DOM fallback:', e)
  }

  term.attachCustomKeyEventHandler((e) => {
    if (e.type !== 'keydown') return true
    const ctrl = e.ctrlKey || e.metaKey
    const shift = e.shiftKey

    if (ctrl && shift && e.key === 'C') {
      e.preventDefault()
      e.stopPropagation()
      const selection = term.getSelection()
      if (selection) navigator.clipboard.writeText(selection)
      return false
    }

    if (ctrl && shift && e.key === 'V') {
      e.preventDefault()
      e.stopPropagation()
      navigator.clipboard.readText().then(text => {
        if (text) term.paste(text)
      }).catch(() => {})
      return false
    }

    if (ctrl && (e.key === '=' || e.key === '+')) {
      e.preventDefault()
      e.stopPropagation()
      window.dispatchEvent(new CustomEvent('shell-zoom', { detail: 1 }))
      return false
    }

    if (ctrl && e.key === '-') {
      e.preventDefault()
      e.stopPropagation()
      window.dispatchEvent(new CustomEvent('shell-zoom', { detail: -1 }))
      return false
    }

    if (ctrl && e.key === '0') {
      e.preventDefault()
      e.stopPropagation()
      window.dispatchEvent(new CustomEvent('shell-zoom', { detail: 0 }))
      return false
    }

    return true
  })

  term.open(container)
  fitAddon.fit()

  return { term, fitAddon, searchAddon }
}

function connectWebSocket(term, fitAddon, initPayload, onStateChange, onFirstMessage) {
  const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
  const ws = new WebSocket(`${proto}//${window.location.host}/api/ws/terminal`)

  ws.addEventListener('open', () => {
    ws.send(JSON.stringify(initPayload))
    const dims = fitAddon.proposeDimensions()
    // Envoyer resize avec dimensions minimales garanties (24x80)
    const rows = dims?.rows || 24
    const cols = dims?.cols || 80
    ws.send(JSON.stringify({ type: 'resize', rows, cols }))
    // Forcer un fit après l'ouverture
    setTimeout(() => {
      try {
        fitAddon.fit()
        const newDims = fitAddon.proposeDimensions()
        if (newDims && newDims.rows > 0 && newDims.cols > 0) {
          ws.send(JSON.stringify({ type: 'resize', rows: newDims.rows, cols: newDims.cols }))
        }
      } catch (e) { console.warn('[Shell] fit failed:', e) }
    }, 50)
    if (onStateChange) onStateChange(true)
  })

  let firstMessage = true
  ws.addEventListener('message', (event) => {
    if (firstMessage) {
      firstMessage = false
      if (onFirstMessage) onFirstMessage()
    }
    try {
      const msg = JSON.parse(event.data)
      if (msg.type === 'output') {
        term.write(msg.data)
      } else if (msg.type === 'error') {
        term.write(`\r\n\x1b[31mError: ${msg.data}\x1b[0m\r\n`)
      }
    } catch {
      term.write(event.data)
    }
  })

  ws.addEventListener('close', () => {
    term.write('\r\n\x1b[33m— Connection closed —\x1b[0m\r\n')
    if (onStateChange) onStateChange(false)
  })

  ws.addEventListener('error', () => {
    term.write('\r\n\x1b[31m— Connection error —\x1b[0m\r\n')
    if (onStateChange) onStateChange(false)
  })

  term.onResize(({ rows, cols }) => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({ type: 'resize', rows, cols }))
    }
  })

  return ws
}

export default function Shell({ api, isSudo }) {
  const { t } = useI18n()
  const tabsRef = useRef({})
  const nextIdRef = useRef(1)
  const settingsRef = useRef({ fontSize: 14, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'system' })
  const pendingCommandsRef = useRef({})

  const [tabs, setTabs] = useState(() => {
    try {
      const raw = localStorage.getItem(TABS_STORAGE_KEY)
      if (raw) {
        const parsed = JSON.parse(raw)
        if (Array.isArray(parsed) && parsed.length > 0 && parsed.length <= MAX_TABS) {
          return parsed.map((t, i) => ({ 
            id: t.id || i + 1, 
            name: t.name || `Tab ${i + 1}`, 
            type: t.type || 'local', 
            shell: t.shell || '',
            host: t.host,
            port: t.port,
            user: t.user,
            key_path: t.key_path,
            connected: false 
          }))
        }
      }
    } catch (e) { 
      console.warn('[Shell] Failed to parse saved tabs:', e)
      localStorage.removeItem(TABS_STORAGE_KEY)
    }
    return [
      { id: 1, name: 'Local Shell', type: 'local', shell: '', connected: false },
    ]
  })
  const [activeTab, setActiveTab] = useState(() => {
    try {
      const raw = localStorage.getItem(TABS_STORAGE_KEY)
      if (raw) {
        const parsed = JSON.parse(raw)
        if (Array.isArray(parsed) && parsed.length > 0) return parsed[0]?.id || 1
      }
    } catch {}
    return 1
  })
  const activeTabRef = useRef(activeTab)
  useEffect(() => { activeTabRef.current = activeTab }, [activeTab])
  const tabIdsKey = useMemo(() => tabs.map(t => t.id).join(','), [tabs])
  const [sshConnections, setSshConnections] = useState([])
  const [systemTerminals, setSystemTerminals] = useState([])
  const [showMenu, setShowMenu] = useState(false)
  const [showSshModal, setShowSshModal] = useState(false)
  const [editingTab, setEditingTab] = useState(null)
  const [editName, setEditName] = useState('')
  const [terminalSettings, setTerminalSettings] = useState({
    fontSize: 14,
    fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
    theme: 'system',
  })

  const [configLoaded, setConfigLoaded] = useState(false)
  const [showSearch, setShowSearch] = useState(false)
  const [searchText, setSearchText] = useState('')
  const searchInputRef = useRef(null)
  const searchDecorationsRef = useRef(null)
  const [zoomLevel, setZoomLevel] = useState(0)
  const baseFontSizeRef = useRef(14)

  useEffect(() => { settingsRef.current = terminalSettings }, [terminalSettings])

  useEffect(() => {
    baseFontSizeRef.current = terminalSettings.fontSize || 14
  }, [terminalSettings.fontSize])

  useEffect(() => {
    const handler = (e) => {
      const direction = e.detail
      setZoomLevel(prev => {
        let next
        if (direction === 0) next = 0
        else next = Math.max(-8, Math.min(10, prev + direction))
        const newSize = baseFontSizeRef.current + next * 2
        for (const entry of Object.values(tabsRef.current)) {
          if (entry.term && !entry.term._disposed) {
            entry.term.options.fontSize = newSize
            try { entry.fitAddon.fit() } catch {}
          }
        }
        return next
      })
    }
    window.addEventListener('shell-zoom', handler)
    return () => window.removeEventListener('shell-zoom', handler)
  }, [])

  const [sshForm, setSshForm] = useState({
    name: '', host: '', port: 22, user: '', key_path: '', password: '',
  })
  const [sshEditing, setSshEditing] = useState(null)

  const [aiMessages, setAiMessages] = useState([])
  const [aiInput, setAiInput] = useState('')
  const [aiLoading, setAiLoading] = useState(false)
  const [aiTokens, setAiTokens] = useState(0)
  const [aiAtLimit, setAiAtLimit] = useState(false)
  const [analyzing, setAnalyzing] = useState(false)
  const [showAnalysis, setShowAnalysis] = useState(false)
  const [analysisContent, setAnalysisContent] = useState('')
  const [sudoModal, setSudoModal] = useState(null)
  const aiMessagesRef = useRef(null)
  const aiLoadedRef = useRef(false)
  const aiLoadingRef = useRef(false)
  const analysisSavingRef = useRef(false)
  const _streamRafRef = useRef(null)
  const _streamPendingRef = useRef(null)

  const _flushStreamUpdate = useCallback(() => {
    _streamRafRef.current = null
    const pending = _streamPendingRef.current
    if (!pending) return
    _streamPendingRef.current = null
    setAiMessages(pending)
  }, [])

  useEffect(() => {
    if (_streamRafRef.current) {
      cancelAnimationFrame(_streamRafRef.current)
      _streamRafRef.current = null
    }
    aiMessagesRef.current?.scrollTo(0, aiMessagesRef.current.scrollHeight)
  }, [aiMessages])

  useEffect(() => {
    api.getShellAnalysis?.().then(d => {
      if (d?.analysis) setAnalysisContent(d.analysis)
    }).catch(() => {
      const stored = localStorage.getItem('shell_analysis')
      if (stored) setAnalysisContent(stored)
    })
    const stored = localStorage.getItem('shell_analysis')
    if (stored && !analysisContent) setAnalysisContent(stored)
  }, [])

  useEffect(() => {
    if (aiLoadedRef.current) return
    aiLoadedRef.current = true
    api.getShellChatHistory().then(d => {
      if (d.messages && d.messages.length > 0) {
        setAiMessages(d.messages)
      }
      setAiTokens(d.tokens || 0)
      setAiAtLimit(d.at_limit || false)
    }).catch(() => {})
  }, [])

  useEffect(() => {
    const maxId = tabs.reduce((max, t) => Math.max(max, t.id), 0)
    nextIdRef.current = maxId + 1
  }, [])

  useEffect(() => {
    api.getTerminalSessions().then(d => {
      setSshConnections(d.ssh || [])
      setSystemTerminals(d.system || [])
    }).catch(() => {})
    api.getConfig().then(d => {
      if (d.terminal) {
        const fontSize = d.terminal.font_size || 14
        const fontFamily = d.terminal.font_family || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace"
        const theme = d.terminal.theme || 'system'
        setTerminalSettings({ fontSize, fontFamily, theme })
        settingsRef.current = { fontSize, fontFamily, theme }
        baseFontSizeRef.current = fontSize
      }
      setConfigLoaded(true)
    }).catch((err) => { console.warn('[Shell] getConfig failed:', err); setConfigLoaded(true) })
  }, [])

  const initTerminal = useCallback((tabId, tab) => {
    if (tabsRef.current[tabId]) return

    const container = document.getElementById(`terminal-${tabId}`)
    if (!container) return

    const s = settingsRef.current
    const effectiveFontSize = s.fontSize + zoomLevel * 2
    const { term, fitAddon, searchAddon } = createTerminal(container, {
      fontSize: effectiveFontSize,
      fontFamily: s.fontFamily,
      theme: s.theme,
    })

    let initPayload
    if (tab.type === 'ssh') {
      initPayload = {
        type: 'ssh',
        data: JSON.stringify({
          host: tab.host,
          port: tab.port || 22,
          user: tab.user || 'root',
          key_path: tab.key_path || '',
          password: tab.password || '',
        }),
      }
    } else {
      initPayload = {
        type: 'shell',
        data: tab.shell || '',
      }
    }

    let disposed = false

    const saveBuffer = () => {
      try {
        const buf = term.buffer.active
        const lines = []
        for (let i = 0; i < buf.length; i++) {
          const line = buf.getLine(i)
          if (line) lines.push(line.translateToString(true))
        }
        const savedBuffers = JSON.parse(sessionStorage.getItem(TERMINAL_BUFFER_KEY) || '{}')
        savedBuffers[tabId] = lines.join('\n')
        sessionStorage.setItem(TERMINAL_BUFFER_KEY, JSON.stringify(savedBuffers))
      } catch (e) { console.warn('[Shell] Buffer save failed:', e) }
    }

    const onWsState = (connected) => {
      if (disposed) return
      if (!connected) saveBuffer()
      setTabs(prev => prev.map(t => t.id === tabId ? { ...t, connected } : t))
    }

    const restoreBuffer = () => {
      try {
        const savedBuffers = JSON.parse(sessionStorage.getItem(TERMINAL_BUFFER_KEY) || '{}')
        if (savedBuffers[tabId]) {
          term.write('\x1b[90m— session restaurée —\x1b[0m\r\n')
          term.write(savedBuffers[tabId])
        }
      } catch (e) { console.warn('[Shell] Buffer restore failed:', e) }
    }

    const ws = connectWebSocket(term, fitAddon, initPayload, onWsState, restoreBuffer)

    const clearBufferOnClear = () => {
      try {
        const buf = term.buffer.active
        const lineY = buf.length - 1
        const line = buf.getLine(lineY)
        if (line) {
          const text = line.translateToString(true).trim().toLowerCase()
          if (text === 'clear' || text === '$ clear' || text.endsWith(' clear')) {
            const savedBuffers = JSON.parse(sessionStorage.getItem(TERMINAL_BUFFER_KEY) || '{}')
            delete savedBuffers[tabId]
            sessionStorage.setItem(TERMINAL_BUFFER_KEY, JSON.stringify(savedBuffers))
          }
        }
      } catch (e) { console.warn('[Shell] Clear detection failed:', e) }
    }

    term.onData((data) => {
      if (data === '\r') clearBufferOnClear()
      if (ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify({ type: 'input', data }))
      }
    })

    const onResize = () => {
      fitAddon.fit()
    }

    const resizeObserver = new ResizeObserver(onResize)
    resizeObserver.observe(container)
    window.addEventListener('resize', onResize)

    const bufferSaveInterval = setInterval(() => { if (!disposed) saveBuffer() }, 5000)

    tabsRef.current[tabId] = { term, fitAddon, searchAddon, ws, resizeObserver, onResize, bufferSaveInterval, saveBuffer, disposed: () => disposed }
    tabsRef.current[tabId]._markDisposed = () => { disposed = true }

    const pending = pendingCommandsRef.current[tabId]
    if (pending && pending.length > 0) {
      for (const cmd of pending) {
        if (ws.readyState === WebSocket.OPEN) {
          ws.send(JSON.stringify({ type: 'input', data: cmd + '\r' }))
        }
      }
      delete pendingCommandsRef.current[tabId]
    }
  }, [])

  const initPendingTabs = useCallback(() => {
    if (!configLoaded) return
    for (const tab of tabsRef.current._tabList || []) {
      if (!tabsRef.current[tab.id]) {
        const container = document.getElementById(`terminal-${tab.id}`)
        if (container && container.offsetHeight > 0) {
          initTerminal(tab.id, tab)
        }
      }
    }
    requestAnimationFrame(() => {
      for (const tab of tabsRef.current._tabList || []) {
        const entry = tabsRef.current[tab.id]
        if (entry) entry.fitAddon.fit()
      }
      setTimeout(() => {
        for (const tab of tabsRef.current._tabList || []) {
          const entry = tabsRef.current[tab.id]
          if (entry) entry.fitAddon.fit()
        }
      }, 150)
    })
  }, [initTerminal, configLoaded])

  useEffect(() => {
    tabsRef.current._tabList = tabs
  }, [tabs])

  useEffect(() => {
    let cancelled = false
    const pending = []

    // Forcer le layout à se calculer
    const forceLayout = () => {
      const el = document.querySelector('.shell-terminal-col')
      if (el) {
        el.style.height = ''
        el.style.minHeight = ''
        // Forcer reflow
        void el.offsetHeight
      }
    }

    const tryInitTab = (tab, attempt) => {
      if (cancelled) return
      if (attempt > 20) {
        return
      }
      
      forceLayout()
      const shellCol = document.querySelector('.shell-terminal-col')
      if (!shellCol) {
        pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 150))
        return
      }
      
      const container = document.getElementById(`terminal-${tab.id}`)
      if (!container) {
        pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 100))
        return
      }
      
      const rect = container.getBoundingClientRect()
      if (rect.height < 10 || rect.width < 10) {
        pending.push(setTimeout(() => tryInitTab(tab, attempt + 1), 100))
        return
      }
      
      if (!tabsRef.current[tab.id]) {
        initTerminal(tab.id, tab)
      }
      
      // Multiple fit attempts avec délais croissants
      const fitAttempts = [0, 50, 100, 200, 400]
      fitAttempts.forEach(delay => {
        setTimeout(() => {
          if (cancelled) return
          const entry = tabsRef.current[tab.id]
          if (entry && entry.fitAddon) {
            try {
              entry.fitAddon.fit()
            } catch (e) { console.warn(`[Shell] fit attempt ${delay}ms failed:`, e) }
          }
        }, delay)
      })
    }

    for (const tab of tabs) {
      if (configLoaded && !tabsRef.current[tab.id]) {
        tryInitTab(tab, 0)
      }
    }

    const wrapper = document.querySelector('.shell-layout')?.parentElement
    let observer
    if (wrapper) {
      observer = new MutationObserver(() => {
        if (!wrapper.classList.contains('tab-hidden') && wrapper.offsetParent !== null) {
          initPendingTabs()
        }
      })
      observer.observe(wrapper, { attributes: true, attributeFilter: ['class'] })
    }

    return () => {
      cancelled = true
      pending.forEach(clearTimeout)
      observer?.disconnect()
    }
  }, [tabIdsKey, initTerminal, initPendingTabs, configLoaded])

  useEffect(() => {
    const entry = tabsRef.current[activeTab]
    if (entry) {
      requestAnimationFrame(() => {
        if (activeTabRef.current === activeTab) {
          entry.fitAddon.fit()
        }
      })
    }
  }, [activeTab])

  useEffect(() => {
    const iv = setInterval(() => {
      const wrapper = document.querySelector('.shell-layout')?.parentElement
      if (wrapper && wrapper.classList.contains('tab-hidden')) return
      const entry = tabsRef.current[activeTabRef.current]
      if (entry && entry.fitAddon && entry.term) {
        const container = document.getElementById(`terminal-${activeTabRef.current}`)
        if (!container) return
        const rect = container.getBoundingClientRect()
        const dims = entry.fitAddon.proposeDimensions()
        if (dims && (entry.term.cols !== dims.cols || entry.term.rows !== dims.rows)) {
          entry.fitAddon.fit()
        }
      }
    }, 2000)
    return () => clearInterval(iv)
  }, [tabIdsKey])

  useEffect(() => {
    return () => {
      for (const [tabId, entry] of Object.entries(tabsRef.current)) {
        entry._markDisposed?.()
        if (entry.bufferSaveInterval) clearInterval(entry.bufferSaveInterval)
        window.removeEventListener('resize', entry.onResize)
        entry.resizeObserver?.disconnect()
        entry.ws?.close()
        entry.term?.dispose()
      }
      tabsRef.current = {}
    }
  }, [])

  useEffect(() => {
    const onKey = (e) => {
      const ctrl = e.ctrlKey || e.metaKey
      if (ctrl && e.shiftKey && e.key === 'F') {
        const shellTab = document.querySelector('.shell-layout')
        if (!shellTab || shellTab.closest('.tab-hidden')) return
        e.preventDefault()
        setShowSearch(prev => !prev)
        return
      }

      if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
      if (!e.altKey && !(e.key === 'Tab' && e.shiftKey)) return

      const currentTabs = tabsRef.current._tabList || []
      if (e.key === 'Tab' && e.shiftKey) {
        const shellTab = document.querySelector('.shell-layout')
        if (!shellTab || shellTab.closest('.tab-hidden')) return
        e.preventDefault()
        const idx = currentTabs.findIndex(t => t.id === activeTabRef.current)
        const next = (idx + 1) % currentTabs.length
        setActiveTab(currentTabs[next].id)
        return
      }

      const num = parseInt(e.key)
      if (num >= 1 && num <= currentTabs.length) {
        e.preventDefault()
        setActiveTab(currentTabs[num - 1].id)
      }
    }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [])

  useEffect(() => {
    if (showSearch && searchInputRef.current) {
      searchInputRef.current.focus()
    }
  }, [showSearch])

  const handleSearchChange = useCallback((value) => {
    setSearchText(value)
    const entry = tabsRef.current[activeTabRef.current]
    if (!entry?.searchAddon) return
    if (!value) {
      entry.searchAddon.clearDecorations()
      entry.searchAddon.clearActiveDecoration()
      return
    }
    try {
      searchDecorationsRef.current = entry.searchAddon.findNext(value)
    } catch {}
  }, [])

  const handleSearchNext = useCallback(() => {
    const entry = tabsRef.current[activeTabRef.current]
    if (!entry?.searchAddon || !searchText) return
    try { entry.searchAddon.findNext(searchText) } catch {}
  }, [searchText])

  const handleSearchPrev = useCallback(() => {
    const entry = tabsRef.current[activeTabRef.current]
    if (!entry?.searchAddon || !searchText) return
    try { entry.searchAddon.findPrevious(searchText) } catch {}
  }, [searchText])

  const handleCloseSearch = useCallback(() => {
    setShowSearch(false)
    setSearchText('')
    const entry = tabsRef.current[activeTabRef.current]
    if (entry?.searchAddon) {
      entry.searchAddon.clearDecorations()
      entry.searchAddon.clearActiveDecoration()
    }
    if (entry?.term) entry.term.focus()
  }, [])

  const addLocalTab = (shell, name) => {
    if (tabs.length >= MAX_TABS) return
    const id = nextIdRef.current++
    const newTab = { id, name: name || `${t('shell.localShell')} ${tabs.length}`, type: 'local', shell: shell || '', connected: false }
    setTabs(prev => { const next = [...prev, newTab]; localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(next.map(t => ({ id: t.id, name: t.name, type: t.type, shell: t.shell, host: t.host, port: t.port, user: t.user, key_path: t.key_path })))); return next })
    setActiveTab(id)
    setShowMenu(false)
  }

  const addSSHTab = (conn) => {
    if (tabs.length >= MAX_TABS) return
    const id = nextIdRef.current++
    const newTab = {
      id,
      name: conn.name || `${conn.user}@${conn.host}`,
      type: 'ssh',
      host: conn.host,
      port: conn.port || 22,
      user: conn.user || 'root',
      key_path: conn.key_path || '',
      password: conn.password || '',
      connected: false,
    }
    setTabs(prev => { const next = [...prev, newTab]; localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(next.map(t => ({ id: t.id, name: t.name, type: t.type, shell: t.shell, host: t.host, port: t.port, user: t.user, key_path: t.key_path })))); return next })
    setActiveTab(id)
    setShowMenu(false)
  }

  const closeTab = (tabId, e) => {
    if (e) e.stopPropagation()

    const entry = tabsRef.current[tabId]
    if (entry) {
      entry._markDisposed?.()
      entry.saveBuffer?.()
      if (entry.bufferSaveInterval) clearInterval(entry.bufferSaveInterval)
      window.removeEventListener('resize', entry.onResize)
      entry.resizeObserver.disconnect()
      entry.ws.close()
      entry.term.dispose()
      delete tabsRef.current[tabId]
    }

    try {
      const savedBuffers = JSON.parse(sessionStorage.getItem(TERMINAL_BUFFER_KEY) || '{}')
      delete savedBuffers[tabId]
      sessionStorage.setItem(TERMINAL_BUFFER_KEY, JSON.stringify(savedBuffers))
    } catch (e) { console.warn('[Shell] Buffer cleanup failed:', e) }

    setTabs(prev => {
      if (prev.length <= 1) return prev
      const next = prev.filter(t => t.id !== tabId)
      if (activeTab === tabId && next.length > 0) {
        setActiveTab(next[next.length - 1].id)
      }
      return next
    })

    // Redimensionner le nouveau tab actif
    setTimeout(() => {
      const newActiveTabId = next.length > 0 ? next[next.length - 1].id : null
      if (newActiveTabId) {
        const entry = tabsRef.current[newActiveTabId]
        if (entry && entry.fitAddon) {
          try {
            entry.fitAddon.fit()
          } catch (e) { console.warn('[Shell] fit after close failed:', e) }
        }
      }
    }, 100)
  }

  const startRename = (tabId, e) => {
    if (e) e.stopPropagation()
    const tab = tabs.find(t => t.id === tabId)
    setEditingTab(tabId)
    setEditName(tab.name)
  }

  const finishRename = () => {
    if (editName.trim() && editingTab) {
      setTabs(prev => prev.map(t => t.id === editingTab ? { ...t, name: editName.trim() } : t))
    }
    setEditingTab(null)
    setEditName('')
  }

  const saveSSHConnection = async () => {
    if (!sshForm.name.trim() || !sshForm.host.trim()) return
    try {
      await api.addSSHConnection(sshForm)
      if (sshEditing) {
        setSshConnections(prev => prev.map(c => c.name === sshEditing ? { ...sshForm } : c))
      } else {
        setSshConnections(prev => [...prev, { ...sshForm }])
      }
      setSshForm({ name: '', host: '', port: 22, user: '', key_path: '', password: '' })
      setSshEditing(null)
      setShowSshModal(false)
    } catch (err) {
      console.error(err)
    }
  }

  const editSSHConnection = (conn) => {
    setSshForm({ name: conn.name, host: conn.host, port: conn.port || 22, user: conn.user || '', key_path: conn.key_path || '', password: conn.password || '' })
    setSshEditing(conn.name)
    setShowSshModal(true)
    setShowMenu(false)
  }

  const deleteSSHConnection = async (name) => {
    try {
      await api.deleteSSHConnection(name)
      setSshConnections(prev => prev.filter(c => c.name !== name))
    } catch (err) {
      console.error(err)
    }
  }

  const sendToTerminal = useCallback((code, tabId) => {
    const targetId = tabId || activeTabRef.current
    const entry = tabsRef.current[targetId]
    if (!entry) {
      console.warn(`[Shell] sendToTerminal: tab ${targetId} not ready. Queueing. tabsRef:`, Object.keys(tabsRef.current), 'activeTab:', activeTabRef.current, 'requested:', tabId)
      if (!pendingCommandsRef.current[targetId]) pendingCommandsRef.current[targetId] = []
      pendingCommandsRef.current[targetId].push(code)
      return
    }
    if (!entry.ws || entry.ws.readyState !== WebSocket.OPEN) {
      console.warn(`[Shell] sendToTerminal: WS not open for tab ${targetId} (state=${entry.ws?.readyState}). Queueing.`)
      if (!pendingCommandsRef.current[targetId]) pendingCommandsRef.current[targetId] = []
      pendingCommandsRef.current[targetId].push(code)
      return
    }
    entry.ws.send(JSON.stringify({ type: 'input', data: code + '\r' }))
  }, [])

  const focusAiTerminal = useCallback(() => {
    const entry = tabsRef.current[activeTabRef.current]
    if (entry) entry.term.focus()
  }, [])

  const _sendAiMessage = useCallback(async (text, fromEvent = false, isAnalysis = false) => {
    if (!text || !text.trim() || aiLoadingRef.current) return
    const trimmed = text.trim()
    if (aiAtLimit && !trimmed.startsWith('/')) return
    aiLoadingRef.current = true

    if (!fromEvent) {
      setAiInput('')
      setTimeout(() => focusAiTerminal(), 0)
    }

    const isSlashCommand = (t) => /^\/(clear|help|model(?:\s+\S+)?)$/.test(t)

    if (trimmed.startsWith('/') && !isSlashCommand(trimmed)) {
      setAiMessages(prev => [...prev,
        { role: 'user', content: trimmed },
        { role: 'assistant', content: 'Commande inconnue. Tapez `/help` pour la liste des commandes.' }
      ])
      aiLoadingRef.current = false
      return
    }

    if (trimmed === '/clear') {
      try {
        await api.clearShellChat()
        setAiMessages([])
        setAiTokens(0)
        setAiAtLimit(false)
      } catch {}
      aiLoadingRef.current = false
      return
    }

    if (trimmed === '/help') {
      setAiMessages(prev => [...prev,
        { role: 'user', content: trimmed },
        { role: 'assistant', content: '## Commandes Terminal\n\n- `/clear` — Effacer la conversation\n- `/help` — Afficher cette aide\n- `/model` — Afficher le provider et modèle actifs\n- `/model change` — Basculer entre les providers disponibles' }
      ])
      aiLoadingRef.current = false
      return
    }

    if (trimmed === '/model' || trimmed === '/model change') {
      if (trimmed === '/model change') {
        api.getProviders().then(data => {
          const providers = data.providers || []
          const minimax = providers.find(p => p.name.toUpperCase() === 'MINIMAX')
          const mimo = providers.find(p => p.name.toUpperCase() === 'MIMO')
          if (!minimax || !mimo) {
            setAiMessages(prev => [...prev, { role: 'assistant', content: 'MiniMax et MiMo doivent être configurés pour utiliser `/model change`.' }])
            return
          }
          const active = providers.find(p => p.active)
          const activeName = active ? active.name.toUpperCase() : ''
          const switchTo = activeName === 'MINIMAX' ? 'MIMO' : 'MINIMAX'
          const target = switchTo === 'MINIMAX' ? minimax : mimo
          api.saveProvider({ name: target.name, active: true }).then(() => {
            setAiMessages(prev => [...prev, { role: 'assistant', content: `✓ Provider changé: **${target.name}** (${target.model})` }])
          }).catch(() => {
            setAiMessages(prev => [...prev, { role: 'assistant', content: 'Erreur lors du changement de provider.' }])
          })
        }).catch(() => {
          setAiMessages(prev => [...prev, { role: 'assistant', content: 'Erreur: impossible de récupérer les providers' }])
        })
      } else {
        api.getProviders().then(data => {
          const active = data.providers?.find(p => p.active)
          const modelMsg = active ? `**${active.name}** — ${active.model}` : 'Aucun provider actif configuré'
          setAiMessages(prev => [...prev, { role: 'assistant', content: modelMsg }])
        }).catch(() => {
          setAiMessages(prev => [...prev, { role: 'assistant', content: 'Erreur: impossible de récupérer les providers' }])
        })
      }
      aiLoadingRef.current = false
      return
    }

    const currentTab = activeTabRef.current
    setAiMessages(prev => [...prev, { role: 'user', content: trimmed, _tabId: currentTab, _analysis: isAnalysis || undefined }])
    setAiLoading(true)

    try {
      let segments = []
      let textStartIdx = 0
      const controller = new AbortController()

      const _updateLastText = (text) => {
        if (!text) return
        const last = segments.length > 0 ? segments[segments.length - 1] : null
        if (last && last.type === 'text') {
          last.content = text
        } else {
          segments.push({ type: 'text', content: text })
        }
      }

      await api.sendShellChat(trimmed, {}, true, (partial, event) => {
        if (event && event.tool_call) {
          _updateLastText(partial.slice(textStartIdx))
          textStartIdx = partial.length
          segments.push({ type: 'tool', call: event.tool_call, result: null })
          const snap = segments.map(s => ({ ...s, ...(s.type === 'tool' ? { call: s.call, result: s.result } : {}) }))
          if (_streamRafRef.current) { cancelAnimationFrame(_streamRafRef.current); _streamRafRef.current = null }
          _streamPendingRef.current = null
          setAiMessages(prev => {
            const filtered = prev.filter(m => !m._streaming)
            return [...filtered, { role: 'assistant', _streaming: true, _tabId: currentTab, _segments: snap }]
          })
          return
        }
        if (event && event.tool_result) {
          if (event.tool_result.sudo_blocked) {
            setSudoModal({ command: event.tool_result.command || event.tool_result.content })
          }
          const segIdx = segments.findIndex(s => s.type === 'tool' && s.call && s.call.tool_call_id === event.tool_result.tool_call_id)
          if (segIdx >= 0) {
            segments[segIdx].result = event.tool_result
            const snap = segments.map(s => ({ ...s, ...(s.type === 'tool' ? { call: s.call, result: s.result } : {}) }))
            if (_streamRafRef.current) { cancelAnimationFrame(_streamRafRef.current); _streamRafRef.current = null }
            _streamPendingRef.current = null
            setAiMessages(prev => {
              const filtered = prev.filter(m => !m._streaming)
              return [...filtered, { role: 'assistant', _streaming: true, _tabId: currentTab, _segments: snap }]
            })
          }
          return
        }
        if (event && (event.thinking !== undefined || event.thinking_end)) {
          return
        }
        _updateLastText(partial.slice(textStartIdx))
        const nextMsgs = prev => {
          const filtered = prev.filter(m => !m._streaming)
          const snap = segments.map(s => ({ ...s, ...(s.type === 'tool' ? { call: s.call, result: s.result } : {}) }))
          return [...filtered, { role: 'assistant', _streaming: true, _tabId: currentTab, _segments: snap }]
        }
        _streamPendingRef.current = nextMsgs
        if (!_streamRafRef.current) {
          _streamRafRef.current = requestAnimationFrame(_flushStreamUpdate)
        }
      }, controller.signal)

      if (_streamRafRef.current) { cancelAnimationFrame(_streamRafRef.current); _streamRafRef.current = null }
      _streamPendingRef.current = null

      const allText = segments.filter(s => s.type === 'text').map(s => s.content).join('')
      const toolSegs = segments.filter(s => s.type === 'tool')

      const finalMsg = { role: 'assistant', content: allText, _tabId: currentTab }
      if (toolSegs.length > 0 || segments.length > 1) {
        finalMsg.content = JSON.stringify({
          segments: segments.map(s => s.type === 'text'
            ? { type: 'text', content: s.content }
            : { type: 'tool', call: s.call, result: { content: s.result?.content || '', is_error: s.result?.is_error || false, tool_call_id: s.call?.tool_call_id } }
          ),
          content: allText,
          tool_calls: toolSegs.map(s => s.call),
          tool_results: toolSegs.map(s => ({
            tool_call_id: s.call?.tool_call_id,
            result: s.result?.content || '',
            is_error: s.result?.is_error || false,
          })),
        })
      }

      setAiMessages(prev => {
        const filtered = prev.filter(m => !m._streaming)
        return [...filtered, finalMsg]
      })

      if (analysisSavingRef.current && allText) {
        analysisSavingRef.current = false
        setAnalysisContent(allText)
        try { localStorage.setItem('shell_analysis', allText) } catch {}
        setAnalyzing(false)
      }

      api.getShellChatHistory().then(d => {
        setAiTokens(d.tokens || 0)
        setAiAtLimit(d.at_limit || false)
      }).catch(() => {})
    } catch (err) {
      if (err.message?.includes('context limit')) {
        setAiAtLimit(true)
      }
      setAiMessages(prev => [...prev.filter(m => !m._streaming), { role: 'assistant', content: `Erreur: ${err.message}` }])
      if (analysisSavingRef.current) {
        analysisSavingRef.current = false
        setAnalyzing(false)
      }
    }
    setAiLoading(false)
    aiLoadingRef.current = false
  }, [api, t, aiAtLimit, focusAiTerminal, _flushStreamUpdate])

  const handleAiSend = () => _sendAiMessage(aiInput, false)

  useEffect(() => {
    const handler = (e) => {
      const msg = e.detail?.message
      if (!msg) return
      setAiInput('')
      setTimeout(() => _sendAiMessage(msg, true), 100)
    }
    window.addEventListener('ask-ai-terminal', handler)
    return () => window.removeEventListener('ask-ai-terminal', handler)
  }, [_sendAiMessage])

  const handleClearChat = async () => {
    try {
      await api.clearShellChat()
      setAiMessages([])
      setAiTokens(0)
      setAiAtLimit(false)
    } catch {}
  }

  const handleAnalyze = async () => {
    if (analyzing) return
    setAnalyzing(true)
    try {
      await api.clearShellChat()
      setAiMessages([])
      setAiTokens(0)
      setAiAtLimit(false)
    } catch {}
    analysisSavingRef.current = true
    _sendAiMessage(`Fais une analyse complète du système. Utilise l'outil terminal pour explorer et rédige un rapport structuré en markdown. Couvre:

1. **OS & Matériel** — distrib, kernel, CPU, RAM, GPU, hostname
2. **Disque & Partitions** — occupation, points de montage, inodes
3. **Réseau** — interfaces, IP, ports en écoute, DNS, pare-feu, SSH actif ?
4. **Utilisateurs & Sécurité** — utilisateurs, groupes sudo, last logins, fail2ban, règles sudo
5. **Services & Processus** — systemd actifs, top processus par CPU/RAM, load average, uptime
6. **Outils installés** — IDE (VSCode, JetBrains...), langages (go, python, node, rust...), gestionnaire de paquets, conteneurs (docker, podman), git, outils CLI
7. **Environnement utilisateur** — shell, $HOME (arborescence top-level, nombre de fichiers/projects), dotfiles, config notable (.bashrc, .zshrc, starship)
8. **Paquets & Mises à jour** — paquets obsolètes, kernel à jour, security patches
9. **Recommandations** — outils manquants, optimisations, points d'attention

Sois concret : cite les vraies versions, les vrais chemins, les vrais nombres. Le rapport fera office de carte d'identité du système (max ~40k tokens).`, true, true)
  }

  return (
    <div className="shell-layout">
      <div className="shell-terminal-col">
        <div className="shell-tabs-bar">
          <div className="shell-tabs">
            {tabs.map((tab, i) => (
              <div
                key={tab.id}
                className={`shell-tab ${activeTab === tab.id ? 'active' : ''}`}
                onClick={() => setActiveTab(tab.id)}
                onDoubleClick={(e) => startRename(tab.id, e)}
              >
                <span className={`connection-dot ${tab.connected ? 'on' : 'off'}`} />
                {tab.type === 'ssh' && <Globe size={12} />}
                {tab.type === 'local' && <Monitor size={12} />}
                {editingTab === tab.id ? (
                  <input
                    className="shell-tab-rename"
                    value={editName}
                    onChange={e => setEditName(e.target.value)}
                    onBlur={finishRename}
                    onKeyDown={e => { if (e.key === 'Enter') finishRename(); if (e.key === 'Escape') setEditingTab(null) }}
                    autoFocus
                    onClick={e => e.stopPropagation()}
                  />
                ) : (
                  <span className="shell-tab-name">{tab.name}</span>
                )}
                <span className="shell-tab-index">{i + 1}</span>
                {tabs.length > 1 && (
                  <button
                    className="shell-tab-close"
                    onClick={(e) => closeTab(tab.id, e)}
                    title={t('shell.closeTab')}
                  >
                    <X size={12} />
                  </button>
                )}
              </div>
            ))}
          </div>

          <div className="shell-tab-actions">
            {zoomLevel !== 0 && (
              <span className="shell-zoom-badge">
                {zoomLevel > 0 ? '+' : ''}{zoomLevel > 0 ? zoomLevel * 2 : zoomLevel * 2}px
              </span>
            )}
            {tabs.length < MAX_TABS && (
              <div className="shell-new-tab-wrapper">
                <button className="shell-new-tab-btn" onClick={() => setShowMenu(!showMenu)} title={t('shell.newTab')}>
                  <Plus size={16} />
                  <ChevronDown size={12} />
                </button>
                {showMenu && (
                  <>
                    <div className="shell-menu-overlay" onClick={() => setShowMenu(false)} />
                    <div className="shell-new-tab-menu">
                      <div className="shell-menu-label">{t('shell.systemTerminals')}</div>
                      {systemTerminals.map(st => (
                        <button
                          key={st.name}
                          className="shell-menu-item"
                          onClick={() => addLocalTab(st.shell, st.name)}
                        >
                          <Monitor size={14} />
                          <span>{st.name}</span>
                          <span className="shell-menu-item-sub">{st.shell}</span>
                        </button>
                      ))}
                      <div className="shell-menu-divider" />
                      <div className="shell-menu-label">{t('shell.savedConnections')}</div>
                      {sshConnections.length === 0 && (
                        <div className="shell-menu-empty">{t('shell.noConnections')}</div>
                      )}
                      {sshConnections.map(conn => (
                        <div key={conn.name} className="shell-menu-item-row">
                          <button
                            className="shell-menu-item"
                            onClick={() => addSSHTab(conn)}
                          >
                            <Globe size={14} />
                            <span>{conn.name}</span>
                            <span className="shell-menu-item-sub">{conn.user}@{conn.host}:{conn.port}</span>
                          </button>
                          <button
                            className="shell-menu-item-icon"
                            onClick={(e) => { e.stopPropagation(); editSSHConnection(conn) }}
                            title={t('shell.editConnection')}
                          >
                            <Pencil size={11} />
                          </button>
                          <button
                            className="shell-menu-item-icon"
                            onClick={(e) => { e.stopPropagation(); deleteSSHConnection(conn.name) }}
                            title={t('shell.deleteConnection')}
                          >
                            <Trash2 size={12} />
                          </button>
                        </div>
                      ))}
                      <div className="shell-menu-divider" />
                      <button className="shell-menu-item accent" onClick={() => { setShowSshModal(true); setShowMenu(false) }}>
                        <Plus size={14} />
                        <span>{t('shell.addConnection')}</span>
                      </button>
                    </div>
                  </>
                )}
              </div>
            )}
          </div>
        </div>

        <div className="shell-xterm-wrapper">
          {showSearch && (
            <div className="shell-search-bar">
              <Search size={14} className="shell-search-icon" />
              <input
                ref={searchInputRef}
                className="shell-search-input"
                value={searchText}
                onChange={e => handleSearchChange(e.target.value)}
                onKeyDown={e => {
                  if (e.key === 'Enter') { e.shiftKey ? handleSearchPrev() : handleSearchNext() }
                  if (e.key === 'Escape') handleCloseSearch()
                  e.stopPropagation()
                }}
                placeholder="Rechercher..."
              />
              <button className="shell-search-nav" onClick={handleSearchPrev} title="Précédent (Shift+Entrée)">↑</button>
              <button className="shell-search-nav" onClick={handleSearchNext} title="Suivant (Entrée)">↓</button>
              <button className="shell-search-close" onClick={handleCloseSearch}><X size={14} /></button>
            </div>
          )}
          {tabs.map(tab => (
            <div
              key={tab.id}
              id={`terminal-${tab.id}`}
              className={`shell-xterm-instance${activeTab === tab.id ? ' active' : ''}`}
            />
          ))}
        </div>
      </div>

      <div className="shell-ai-col">
        <div className="ai-panel-header">
          <span style={{ flex: 1 }}>Analyste Système</span>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexShrink: 0 }}>
            <span className={`sudo-indicator ${isSudo ? 'sudo-ok' : 'sudo-blocked'}`} title={isSudo ? 'Sudo sans mot de passe disponible' : 'Sudo bloqué — mot de passe requis'} />
            <button
              className="shell-analyze-btn"
              onClick={() => setShowAnalysis(true)}
              disabled={!analysisContent}
              title="Voir l'analyse"
            >
              <Eye size={13} />
              Analyse
            </button>
            <button
              className="shell-analyze-btn"
              onClick={handleAnalyze}
              disabled={analyzing}
              title="Analyser le système"
            >
              <Search size={13} />
              {analyzing ? '...' : 'Analyser'}
            </button>
          </div>
        </div>
        <div className="shell-ai-token-bar">
          <div className="shell-ai-token-track">
            <div
              className={`shell-ai-token-fill ${aiTokens >= SHELL_MAX_TOKENS * 0.8 ? 'warn' : ''}`}
              style={{ width: `${Math.min(100, (aiTokens / SHELL_MAX_TOKENS) * 100)}%` }}
            />
          </div>
          <span className="shell-ai-token-text">{Math.round(aiTokens / 1000)}k/{Math.round(SHELL_MAX_TOKENS / 1000)}k</span>
        </div>
        <div className="ai-panel-messages" ref={aiMessagesRef}>
          {aiMessages.map((msg, i) => (
            <ShellAIMessage key={i} msg={msg} sendToTerminal={sendToTerminal} terminalTabId={msg._tabId || activeTab} />
          ))}
          {aiLoading && <div style={{ textAlign: 'center', padding: 8 }}><span className="spinner" /></div>}
        </div>
        <div className="ai-panel-input">
          {aiAtLimit ? (
            <button className="ai-clear-btn" onClick={handleClearChat}>Nettoyer la conversation</button>
          ) : (
            <>
              <input
                value={aiInput}
                onChange={e => setAiInput(e.target.value)}
                onKeyDown={e => {
                  if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); handleAiSend(); return }
                  if (e.key === 'Tab') {
                    e.preventDefault()
                    const val = aiInput
                    const pos = e.target.selectionStart
                    const before = val.slice(0, pos)
                    const afterSlash = before.match(/\/[\w ]*$/)
                    if (afterSlash) {
                      const partial = afterSlash[0]
                      const matches = SHELL_AI_COMMANDS.filter(c => c.startsWith(partial) && c !== partial)
                      if (matches.length >= 1) {
                        let completed = matches[0]
                        for (const m of matches) {
                          while (!m.startsWith(completed)) completed = completed.slice(0, -1)
                        }
                        if (completed === partial && matches.length === 1) completed = matches[0]
                        if (completed.length > partial.length) {
                          const suffix = completed[completed.length - 1] === ' ' ? '' : (matches.length === 1 ? ' ' : '')
                          completed += suffix
                          const newText = val.slice(0, pos - afterSlash[0].length) + completed + val.slice(pos)
                          setAiInput(newText)
                          requestAnimationFrame(() => {
                            e.target.selectionStart = e.target.selectionEnd = pos - afterSlash[0].length + completed.length
                          })
                        }
                      }
                    }
                    return
                  }
                }}
                placeholder={t('shell.askAi')}
              />
              <button className="sm" onClick={handleAiSend} disabled={!aiInput.trim()}>{t('shell.send')}</button>
            </>
          )}
        </div>
      </div>

      {showAnalysis && analysisContent && (
        <div className="shell-modal-overlay" onClick={() => setShowAnalysis(false)}>
          <div className="shell-analysis-modal" onClick={e => e.stopPropagation()}>
            <div className="shell-analysis-modal-header">
              <span>Analyse Système</span>
              <button className="shell-tab-close" onClick={() => setShowAnalysis(false)}><X size={16} /></button>
            </div>
            <div className="shell-analysis-modal-body">
              {renderContent(analysisContent).map((part, i) =>
                part.type === 'code' && part.lang === 'mermaid' ? (
                  <div key={i} className="shell-code-block">
                    <div className="shell-code-lang">mermaid</div>
                    <MermaidBlock code={part.content} />
                  </div>
                ) : part.type === 'code' ? (
                  <div key={i} className="shell-code-block">
                    {part.lang && <div className="shell-code-lang">{part.lang}</div>}
                    <pre><code>{part.content}</code></pre>
                  </div>
                ) : (
                  <span key={i} dangerouslySetInnerHTML={{ __html: formatText(part.content) }} />
                )
              )}
            </div>
          </div>
        </div>
      )}

      {showSshModal && (
        <div className="shell-modal-overlay" onClick={() => { setShowSshModal(false); setSshEditing(null) }}>
          <div className="shell-modal" onClick={e => e.stopPropagation()}>
            <div className="shell-modal-header">{sshEditing ? t('shell.editConnection') : t('shell.addConnection')}</div>
            <div className="shell-modal-body">
              <label className="shell-modal-label">{t('shell.connectionName')}</label>
              <input
                value={sshForm.name}
                onChange={e => setSshForm(f => ({ ...f, name: e.target.value }))}
                placeholder="prod-server"
                disabled={!!sshEditing}
              />
              <label className="shell-modal-label">{t('shell.host')}</label>
              <input
                value={sshForm.host}
                onChange={e => setSshForm(f => ({ ...f, host: e.target.value }))}
                placeholder="192.168.1.100"
              />
              <div className="shell-modal-row">
                <div className="shell-modal-field">
                  <label className="shell-modal-label">{t('shell.port')}</label>
                  <input
                    type="number"
                    value={sshForm.port}
                    onChange={e => setSshForm(f => ({ ...f, port: parseInt(e.target.value) || 22 }))}
                  />
                </div>
                <div className="shell-modal-field">
                  <label className="shell-modal-label">{t('shell.user')}</label>
                  <input
                    value={sshForm.user}
                    onChange={e => setSshForm(f => ({ ...f, user: e.target.value }))}
                    placeholder="root"
                  />
                </div>
              </div>
              <label className="shell-modal-label">{t('shell.keyPath')} ({t('shell.local')})</label>
              <input
                value={sshForm.key_path}
                onChange={e => setSshForm(f => ({ ...f, key_path: e.target.value }))}
                placeholder="~/.ssh/id_rsa"
              />
              <label className="shell-modal-label">{t('shell.password')} <span style={{ fontWeight: 400, fontSize: 10, color: 'var(--text-disabled)' }}>({t('shell.passwordHint')})</span></label>
              <input
                type="password"
                value={sshForm.password}
                onChange={e => setSshForm(f => ({ ...f, password: e.target.value }))}
                placeholder="••••••"
              />
            </div>
            <div className="shell-modal-footer">
              <button className="ghost" onClick={() => { setShowSshModal(false); setSshEditing(null) }}>{t('shell.cancel')}</button>
              <button className="primary" onClick={saveSSHConnection}>{t('shell.save')}</button>
            </div>
          </div>
        </div>
      )}

      {sudoModal && (
        <div className="shell-modal-overlay" onClick={() => setSudoModal(null)}>
          <div className="shell-modal" onClick={e => e.stopPropagation()}>
            <div className="shell-modal-header">Commande bloquée</div>
            <div className="shell-modal-body">
              <p style={{ color: 'var(--accent-bright)', fontWeight: 600, marginBottom: 8 }}>L'IA a tenté d'exécuter une commande nécessitant des privilèges administrateur :</p>
              <pre style={{ background: 'var(--bg)', padding: '10px 12px', borderRadius: 'var(--radius)', fontSize: 12, overflow: 'auto', fontFamily: 'var(--font-mono)' }}>{sudoModal.command}</pre>
              <p style={{ color: 'var(--text-secondary)', fontSize: 12, marginTop: 12 }}>La commande a été bloquée. L'IA en a été informée et cherchera une alternative.</p>
            </div>
            <div className="shell-modal-footer">
              <button className="primary" onClick={() => setSudoModal(null)}>Compris</button>
            </div>
          </div>
        </div>
      )}
    </div>
  )
}

function ShellToolBlock({ call, result }) {
  const icon = '⌨'
  const label = call.name === 'terminal' ? 'Terminal' : call.name
  const isErr = result && result.is_error

  let argsPreview = ''
  try {
    const args = typeof call.args === 'string' ? JSON.parse(call.args) : call.args
    if (args.command) argsPreview = args.command
    else argsPreview = JSON.stringify(args).slice(0, 80)
  } catch {
    argsPreview = String(call.args).slice(0, 80)
  }

  const truncatedResult = result ? (result.content || '').slice(0, 1500) : null

  return (
    <div className={`studio-tool-block ${isErr ? 'error' : ''} ${result ? 'done' : 'running'}`}>
      <div className="studio-tool-header">
        <span className="studio-tool-icon">{icon}</span>
        <span className="studio-tool-name">{label}</span>
        {!result && <span className="studio-tool-spinner"><span/><span/><span/></span>}
        {result && <span className={`studio-tool-status ${isErr ? 'error' : 'ok'}`}>{isErr ? '✗' : '✓'}</span>}
      </div>
      <div className="studio-tool-args" title={argsPreview}>{argsPreview}</div>
      {truncatedResult && (
        <div className="studio-tool-result">
          <pre>{truncatedResult}</pre>
        </div>
      )}
    </div>
  )
}

let mermaidIdCounter = 0

function MermaidBlock({ code }) {
  const ref = useRef(null)
  const [svg, setSvg] = useState('')
  const [error, setError] = useState(false)

  useEffect(() => {
    let cancelled = false
    const id = `mermaid-${++mermaidIdCounter}`
    mermaid.render(id, code).then(({ svg }) => {
      if (!cancelled) setSvg(svg)
    }).catch(() => {
      if (!cancelled) setError(true)
    })
    return () => { cancelled = true }
  }, [code])

  if (error) return <pre className="shell-mermaid-error">{code}</pre>
  if (!svg) return <div className="shell-mermaid-loading">Chargement du diagramme...</div>
  return <div className="shell-mermaid-container" ref={ref} dangerouslySetInnerHTML={{ __html: svg }} />
}

const _renderParts = (parts, copiedIdx, setCopiedIdx, sendToTerminal, terminalTabId) => parts.map((part, i) => {
  if (part.type === 'code' && part.lang === 'mermaid') {
    return (
      <div key={i} className="shell-code-block">
        <div className="shell-code-lang">mermaid</div>
        <MermaidBlock code={part.content} />
      </div>
    )
  }
  if (part.type === 'code') {
    return (
      <div key={i} className="shell-code-block">
        {part.lang && <div className="shell-code-lang">{part.lang}</div>}
        <pre><code>{part.content}</code></pre>
        <div className="shell-code-actions">
          <button className={copiedIdx === i ? 'copied' : ''} onClick={() => {
            navigator.clipboard.writeText(part.content)
            setCopiedIdx(i)
            setTimeout(() => setCopiedIdx(null), 1500)
          }} title="Copier">
            <Copy size={12} /> {copiedIdx === i ? 'Copié !' : 'Copier'}
          </button>
          <button onClick={() => sendToTerminal(part.content, terminalTabId)} title="Envoyer au terminal">
            <Send size={12} /> Terminal
          </button>
        </div>
      </div>
    )
  }
  return <span key={i} dangerouslySetInnerHTML={{ __html: formatText(part.content) }} />
})

const ShellAIMessage = memo(function ShellAIMessage({ msg, sendToTerminal, terminalTabId }) {
  const role = msg.role === 'user' ? 'user' : msg.role === 'system' ? 'system' : 'assistant'
  const content = msg.content || ''
  const [copiedIdx, setCopiedIdx] = useState(null)

  if (role === 'user') {
    return <div className={`ai-message user${msg._analysis ? ' analysis' : ''}`} dangerouslySetInnerHTML={{ __html: formatText(content) }} />
  }

  if (role === 'system') {
    return <div className={`ai-message system`}>{content}</div>
  }

  // Ordered segments (streaming or final with segments)
  let segments = msg._segments || null
  if (!segments) {
    try {
      const parsed = JSON.parse(content)
      if (parsed && Array.isArray(parsed.segments)) {
        segments = parsed.segments
      }
    } catch {}
  }

  if (segments && segments.length > 0) {
    const hasTools = segments.some(s => s.type === 'tool')
    if (hasTools) {
      return (
        <div className="ai-message assistant">
          {segments.map((seg, i) => {
            if (seg.type === 'text') {
              if (!seg.content) return null
              return <Fragment key={`t${i}`}>{_renderParts(renderContent(seg.content), copiedIdx, setCopiedIdx, sendToTerminal, terminalTabId)}</Fragment>
            }
            if (seg.type === 'tool') {
              const r = seg.result
              const result = r && (r.content !== undefined || r.is_error !== undefined)
                ? { content: r.content, is_error: r.is_error }
                : null
              return <ShellToolBlock key={`tc${i}`} call={seg.call} result={result} />
            }
            return null
          })}
        </div>
      )
    }
  }

  // Fallback: old format (all tools then all text)
  let parsedToolCalls = null
  let parsedToolResults = null
  let displayContent = content

  try {
    const parsed = JSON.parse(content)
    if (parsed && Array.isArray(parsed.tool_calls)) {
      parsedToolCalls = parsed.tool_calls
      parsedToolResults = parsed.tool_results || null
      displayContent = parsed.content || ''
    }
  } catch {}

  const parts = renderContent(displayContent)

  return (
    <div className={`ai-message assistant`}>
      {parsedToolCalls && parsedToolCalls.map((tc, i) => {
        const resultData = parsedToolResults
          ? parsedToolResults.find(r => r.tool_call_id === tc.tool_call_id)
          : null
        const result = resultData
          ? { content: resultData.result, is_error: resultData.is_error }
          : null
        return <ShellToolBlock key={tc.tool_call_id || i} call={tc} result={result} />
      })}
      {_renderParts(parts, copiedIdx, setCopiedIdx, sendToTerminal, terminalTabId)}
    </div>
  )
})
