<template>
  <div class="graph-view" style="width: 100%; height: 100%; position: relative; background: var(--nav-bg);">
    <!-- Canvas SVG pour D3 (zone centrale, marge droite pour sidebar) -->
    <svg
      ref="svgRef"
      :style="{
        width: sidebarOpen ? 'calc(100% - 200px)' : 'calc(100% - 40px)',
        height: '100%',
        transition: 'width 0.2s ease',
      }"
    ></svg>

    <!-- Sidebar droite (repliable) - 3 sections : AFFICHER / HASHTAGS / MODE D'EMPLOI -->
    <aside
      :style="{
        position: 'absolute', top: '0', right: '0', bottom: '0',
        width: sidebarOpen ? '200px' : '40px',
        background: 'var(--nav-surface)',
        borderLeft: '1px solid var(--nav-bg-alt)',
        display: 'flex', flexDirection: 'column',
        transition: 'width 0.2s ease',
        zIndex: 10,
      }"
    >
      <!-- Toggle (toujours visible) -->
      <button
        @click="sidebarOpen = !sidebarOpen"
        :title="sidebarOpen ? 'Replier la sidebar' : 'Deplier la sidebar'"
        style="
          width: 100%; height: 36px; flex-shrink: 0;
          display: flex; align-items: center; justify-content: center;
          background: var(--nav-bg-alt); border: none; cursor: pointer;
          color: var(--nav-text-muted); font-size: 0.78rem; font-weight: 700;
          border-bottom: 1px solid var(--nav-bg-alt);
        "
      >{{ sidebarOpen ? '>>' : '<<' }}</button>

      <!-- Mode replie : label vertical -->
      <div
        v-if="!sidebarOpen"
        style="
          flex: 1; display: flex; align-items: center; justify-content: center;
          writing-mode: vertical-rl; transform: rotate(180deg);
          font-size: 0.7rem; font-weight: 700; color: var(--nav-text-muted);
          letter-spacing: 0.12em; text-transform: uppercase;
        "
      >HASHTAGS ({{ activeHashtags.length }}/{{ props.allHashtags.length }})</div>

      <!-- Mode deplie : 3 sections empilees -->
      <template v-if="sidebarOpen">
        <div style="flex: 1; overflow-y: auto; display: flex; flex-direction: column;">

          <!-- SECTION 1 : AFFICHER (toggles familles / pratiques) -->
          <div style="padding: 10px 12px; flex-shrink: 0;">
            <div style="font-size: 0.72rem; font-weight: 700; color: var(--nav-text); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 8px;">Afficher</div>
            <label
              :style="{
                display: 'flex', alignItems: 'center', gap: '8px',
                padding: '7px 10px', marginBottom: '4px',
                borderRadius: '6px', cursor: 'pointer',
                fontSize: '0.82rem', fontWeight: 600,
                background: showFamilles ? 'var(--nav-bg-alt)' : 'transparent',
                color: showFamilles ? 'var(--nav-text)' : 'var(--nav-text-muted)',
                transition: 'all 0.12s',
              }"
            >
              <input type="checkbox" v-model="showFamilles" style="cursor: pointer; width: 14px; height: 14px;" />
              <span>Familles</span>
            </label>
            <label
              :style="{
                display: 'flex', alignItems: 'center', gap: '8px',
                padding: '7px 10px',
                borderRadius: '6px', cursor: 'pointer',
                fontSize: '0.82rem', fontWeight: 600,
                background: showPratiques ? 'var(--nav-bg-alt)' : 'transparent',
                color: showPratiques ? 'var(--nav-text)' : 'var(--nav-text-muted)',
                transition: 'all 0.12s',
              }"
            >
              <input type="checkbox" v-model="showPratiques" style="cursor: pointer; width: 14px; height: 14px;" />
              <span>Pratiques</span>
            </label>
          </div>

          <!-- SECTION 2 : HASHTAGS (chips groupees) -->
          <div style="border-top: 1px solid var(--nav-bg-alt); margin-top: 0; padding: 10px 12px 8px; flex-shrink: 0;">
            <div style="display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px;">
              <span style="font-size: 0.72rem; font-weight: 700; color: var(--nav-text); text-transform: uppercase; letter-spacing: 0.08em;">Hashtags</span>
              <span style="font-size: 0.68rem; color: var(--nav-text-muted);">{{ activeHashtags.length }} actif{{ activeHashtags.length > 1 ? 's' : '' }}</span>
            </div>
            <button
              v-if="activeHashtags.length"
              @click="activeHashtags = []"
              style="margin-bottom: 6px; font-size: 0.68rem; color: var(--nav-text-muted); background: none; border: none; cursor: pointer; text-decoration: underline; padding: 0;"
            >Tout effacer</button>
          </div>

          <div style="flex: 1; overflow-y: auto; padding: 0 10px 10px;">
            <div
              v-for="group in hashtagsByFamille"
              :key="group.famille"
              style="margin-bottom: 10px;"
            >
              <div
                :style="{
                  fontSize: '0.65rem', fontWeight: 700,
                  color: group.color, textTransform: 'uppercase',
                  letterSpacing: '0.06em', marginBottom: '4px',
                  paddingLeft: '2px',
                }"
              >{{ group.label }}</div>
              <div style="display: flex; flex-wrap: wrap; gap: 3px;">
                <span
                  v-for="tag in group.tags"
                  :key="tag"
                  style="padding: 2px 7px; border-radius: 9999px; font-size: 0.66rem; cursor: pointer; transition: all 0.12s;"
                  :style="activeHashtags.includes(tag)
                    ? `background: ${group.color}; color: white; font-weight: 600;`
                    : 'background: var(--nav-bg-alt); color: var(--nav-text-muted);'"
                  @click="toggleHashtag(tag)"
                >{{ tag }}</span>
              </div>
            </div>
          </div>

          <!-- SECTION 3 : MODE D'EMPLOI -->
          <div style="border-top: 1px solid var(--nav-bg-alt); padding: 10px 12px; flex-shrink: 0;">
            <div style="font-size: 0.72rem; font-weight: 700; color: var(--nav-text); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 6px;">Mode d'emploi</div>
            <div style="font-size: 0.7rem; color: var(--nav-text-muted); line-height: 1.5;">
              La carte croise des familles editoriales avec des pratiques (hashtags). Coche les couches a afficher, filtre par hashtag, clique sur un noeud pour en savoir plus.
            </div>
          </div>

        </div>
      </template>
    </aside>

    <!-- Tooltip -->
    <div ref="tooltipRef" style="
      position: absolute; pointer-events: none;
      background: var(--nav-surface); border: 1px solid var(--nav-bg-alt);
      border-radius: 6px; padding: 8px 12px; font-size: 0.78rem;
      color: var(--nav-text); max-width: 220px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
      opacity: 0; transition: opacity 0.15s; z-index: 100;
    "></div>

    <!-- Popover unifie (famille OU hashtag) -->
    <div
      v-if="popover.open"
      :style="{
        position: 'absolute',
        left: popover.x + 'px',
        top: popover.y + 'px',
        background: 'var(--nav-surface)',
        border: '1px solid var(--nav-bg-alt)',
        borderRadius: '8px',
        padding: '12px 14px',
        maxWidth: '280px',
        boxShadow: '0 6px 18px rgba(0,0,0,0.18)',
        zIndex: 50,
      }"
      @click.stop
    >
      <button
        @click="closePopover"
        style="
          position: absolute; top: 4px; right: 6px;
          background: none; border: none; cursor: pointer;
          font-size: 1rem; color: var(--nav-text-muted); padding: 2px 6px;
          line-height: 1;
        "
        title="Fermer"
      >x</button>
      <div
        :style="{
          fontWeight: 700, fontSize: '0.92rem',
          color: popover.color, marginBottom: '6px',
          paddingRight: '14px',
        }"
      >{{ popover.title }}</div>

      <!-- Body famille : description + compteur + 6 structures + bouton "Voir toutes" -->
      <div v-if="popover.kind === 'famille'">
        <div style="font-size: 0.78rem; line-height: 1.45; color: var(--nav-text); margin-bottom: 10px;">
          {{ popover.body }}
        </div>
        <div style="font-size: 0.72rem; color: var(--nav-text-muted); margin-bottom: 6px;">
          {{ popover.structures.length }} structure{{ popover.structures.length > 1 ? 's' : '' }} dans cette famille
        </div>
        <ul style="list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 3px;">
          <li
            v-for="s in popover.structures.slice(0, 6)"
            :key="s.id"
            @click="selectStructureFromPopover(s.id)"
            style="
              font-size: 0.78rem; color: var(--nav-text);
              padding: 4px 6px; border-radius: 4px;
              cursor: pointer; transition: background 0.1s;
              display: flex; align-items: center; gap: 6px;
            "
            @mouseenter="(e: any) => e.currentTarget.style.background = 'var(--nav-bg-alt)'"
            @mouseleave="(e: any) => e.currentTarget.style.background = 'transparent'"
          >
            <span
              style="width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;"
              :style="`background: ${popover.color};`"
            />
            <span>{{ s.nom }}</span>
          </li>
        </ul>
        <button
          v-if="popover.familleId != null"
          @click="openFicheFamilleFromPopover"
          style="
            margin-top: 10px; width: 100%;
            padding: 7px 10px; border-radius: 6px;
            background: var(--nav-bg-alt); border: none;
            font-size: 0.75rem; font-weight: 600; cursor: pointer;
            color: var(--nav-text); transition: opacity 0.12s;
            text-align: left;
          "
          @mouseenter="(e: any) => e.currentTarget.style.opacity = '0.7'"
          @mouseleave="(e: any) => e.currentTarget.style.opacity = '1'"
        >Voir toutes les {{ popover.structures.length }} pratiques -&gt;</button>
      </div>

      <!-- Body hashtag : ligne generique + compteur + liste structures cliquables -->
      <div v-else-if="popover.kind === 'hashtag'">
        <div
          style="
            font-size: 0.72rem; color: var(--nav-text-muted);
            font-style: italic; margin-bottom: 8px; line-height: 1.4;
          "
        >Pratique transversale - portee par {{ popover.structures.length }} structure{{ popover.structures.length > 1 ? 's' : '' }} de {{ popover.famillesCount }} famille{{ popover.famillesCount > 1 ? 's' : '' }}</div>
        <ul style="list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 3px;">
          <li
            v-for="s in popover.structures.slice(0, 6)"
            :key="s.id"
            @click="selectStructureFromPopover(s.id)"
            style="
              font-size: 0.78rem; color: var(--nav-text);
              padding: 4px 6px; border-radius: 4px;
              cursor: pointer; transition: background 0.1s;
            "
            @mouseenter="(e: any) => e.currentTarget.style.background = 'var(--nav-bg-alt)'"
            @mouseleave="(e: any) => e.currentTarget.style.background = 'transparent'"
          >{{ s.nom }}</li>
        </ul>
        <div
          v-if="popover.structures.length > 6"
          style="font-size: 0.7rem; color: var(--nav-text-muted); margin-top: 6px; padding-left: 6px;"
        >+ {{ popover.structures.length - 6 }} autre{{ popover.structures.length - 6 > 1 ? 's' : '' }}</div>
      </div>
    </div>

    <!-- Fiche famille modale -->
    <FicheFamilleModal
      v-model="ficheFamilleOpen"
      :famille-id="ficheFamilleId"
      :data="props.data"
      @select-structure="(id) => emit('select-structure', id)"
    />
  </div>
</template>

<script setup lang="ts">
import type { ReseauxBifurcationData } from '~/types/structure-v2'

const props = defineProps<{
  data: ReseauxBifurcationData | null
  allHashtags: string[]
  active?: boolean
}>()

const emit = defineEmits<{
  'select-structure': [id: string]
}>()

const svgRef = ref<SVGElement | null>(null)
const tooltipRef = ref<HTMLElement | null>(null)

// Hashtag filter
const activeHashtags = ref<string[]>([])
const sidebarOpen = ref(true)

// Layers superposables (remplace viewMode exclusif PV2-5e)
const showFamilles = ref(true)
const showPratiques = ref(false)

function toggleHashtag(tag: string) {
  activeHashtags.value = activeHashtags.value.includes(tag)
    ? activeHashtags.value.filter(t => t !== tag)
    : [...activeHashtags.value, tag]
}

// Popover unifie (famille | hashtag)
type PopoverState = {
  open: boolean
  kind: 'famille' | 'hashtag' | null
  x: number
  y: number
  title: string
  body: string
  color: string
  structures: { id: string; nom: string }[]
  familleId: number | null
  famillesCount: number
}

const popover = ref<PopoverState>({
  open: false,
  kind: null,
  x: 0,
  y: 0,
  title: '',
  body: '',
  color: '#000',
  structures: [],
  familleId: null,
  famillesCount: 0,
})

// Fiche famille modale
const ficheFamilleOpen = ref(false)
const ficheFamilleId = ref<number | null>(null)

function closePopover() {
  popover.value.open = false
  popover.value.kind = null
}

function selectStructureFromPopover(id: string) {
  closePopover()
  emit('select-structure', id)
}

function openFicheFamilleFromPopover() {
  if (popover.value.familleId == null) return
  ficheFamilleId.value = popover.value.familleId
  ficheFamilleOpen.value = true
  closePopover()
}

// Mapping hashtag -> famille majoritaire
// En cas d'egalite : prendre la famille la plus petite (visibilite minoritaires)
const tagToFamille = computed<Record<string, number>>(() => {
  if (!props.data) return {}
  const counts: Record<string, Record<number, number>> = {}
  props.data.structures.forEach(s => {
    s.hashtags.forEach(tag => {
      if (!counts[tag]) counts[tag] = {}
      counts[tag][s.famille_principale] = (counts[tag][s.famille_principale] ?? 0) + 1
    })
  })
  const familleSize: Record<number, number> = {}
  props.data.structures.forEach(s => {
    familleSize[s.famille_principale] = (familleSize[s.famille_principale] ?? 0) + 1
  })
  const out: Record<string, number> = {}
  for (const tag in counts) {
    const entries = Object.entries(counts[tag])
    entries.sort((a, b) => {
      const diff = (b[1] as number) - (a[1] as number)
      if (diff !== 0) return diff
      return (familleSize[Number(a[0])] ?? 0) - (familleSize[Number(b[0])] ?? 0)
    })
    out[tag] = Number(entries[0][0])
  }
  return out
})

const hashtagsByFamille = computed(() => {
  if (!props.data) return []
  const map = tagToFamille.value
  const groups: Record<number, string[]> = {}
  props.allHashtags.forEach(tag => {
    const fam = map[tag]
    if (fam == null) return
    if (!groups[fam]) groups[fam] = []
    groups[fam].push(tag)
  })
  return [1, 2, 3, 4, 5, 6]
    .filter(famId => groups[famId]?.length)
    .map(famId => ({
      famille: famId,
      label: FAMILLE_LABELS[famId],
      color: FAMILLE_COLORS[famId],
      tags: groups[famId].sort(),
    }))
})

// Structures portant un hashtag donne (pour popover)
function structuresForHashtag(tag: string): { id: string; nom: string }[] {
  if (!props.data) return []
  return props.data.structures
    .filter(s => s.hashtags.includes(tag))
    .map(s => ({ id: s.id, nom: s.nom }))
}

// IDs de structures correspondant aux hashtags actifs
const filteredStructureIds = computed(() => {
  if (!props.data || !activeHashtags.value.length) return null
  const ids = new Set(
    props.data.structures
      .filter(s => activeHashtags.value.every(h => s.hashtags.includes(h)))
      .map(s => s.id)
  )
  return ids
})

const FAMILLE_COLORS: Record<number, string> = {
  1: '#a85d3e',
  2: '#c4a472',
  3: '#d4a017',
  4: '#5a7a4a',
  5: '#3d6a8c',
  6: '#6b3fa0',
}

const FAMILLE_LABELS: Record<number, string> = {
  1: 'Reemploi',
  2: 'Frugalite',
  3: 'Social',
  4: 'Collectifs',
  5: 'Urbanisme',
  6: 'Recherche',
}

const FAMILLE_DESCRIPTIONS: Record<number, string> = {
  1: "Structures dont le geste premier est de travailler avec la matiere existante : deconstruction selective, plateformes de redistribution, filieres biosourcees et geosourcees.",
  2: "Pratiques qui partent du principe qu'on peut faire mieux avec moins. Renovation profonde, materiaux locaux, sobriete choisie.",
  3: "Structures dont le terrain premier est le mal-logement, la precarite, l'hospitalite. Architecture comme reponse a l'urgence sociale.",
  4: "Structures qui accompagnent les projets collectifs : cooperatives d'habitat, ecovillages, accompagnement vers l'autogestion ou la renovation.",
  5: "Demarches a l'echelle du territoire : villes en transition, PLU alternatifs, coalitions territoriales.",
  6: "Recherche-action et production de contre-savoirs (Forensic Architecture, Rural Studio, PEROU, Centrala). Badge transversal aux familles.",
}

let simulation: any = null
let d3NodeSelection: any = null
let d3LinkSelection: any = null

async function initGraph() {
  if (!svgRef.value || !props.data) return

  const d3 = await import('d3')

  const svgEl = svgRef.value
  const width = svgEl.clientWidth || 800
  const height = svgEl.clientHeight || 600

  // Nettoyer
  d3.select(svgEl).selectAll('*').remove()
  closePopover()

  const svg = d3.select(svgEl)
    .attr('viewBox', `0 0 ${width} ${height}`)

  // Click sur le SVG vide -> fermer popover
  svg.on('click', (event: any) => {
    if (event.target === svgEl) closePopover()
  })

  // Groupe principal avec zoom
  const g = svg.append('g')
  const zoomBehavior = d3.zoom<SVGElement, unknown>()
    .scaleExtent([0.2, 4])
    .on('zoom', (event) => {
      g.attr('transform', event.transform)
      closePopover()
    })

  svg.call(zoomBehavior as any)

  const { allNodes, links } = buildNodesLinks(width, height)

  // Simulation force-directed
  if (simulation) simulation.stop()

  // Adapter la charge selon le nombre de noeuds (mode "tout coche" = plus de repulsion)
  const heavyMode = showPratiques.value && allNodes.length > 150

  simulation = d3.forceSimulation(allNodes)
    .force('link', d3.forceLink(links).id((d: any) => d.id)
      .distance((d: any) => {
        if (d.type === 'practice') return 90
        return d.type === 'primary' ? 80 : 120
      })
      .strength((d: any) => d.strength ?? 0.5))
    .force('charge', d3.forceManyBody().strength(heavyMode ? -80 : -120))
    .force('center', d3.forceCenter(width / 2, height / 2))
    .force('collision', d3.forceCollide().radius((d: any) => d.r + 4))

  // Rendu liens
  d3LinkSelection = g.append('g').selectAll('line')
    .data(links)
    .join('line')
    .attr('stroke', (d: any) => {
      if (d.type === 'practice') return 'rgba(150,150,150,0.25)'
      return d.type === 'primary' ? 'rgba(150,150,150,0.45)' : 'rgba(150,150,150,0.35)'
    })
    .attr('stroke-width', 1.5)
    .attr('stroke-dasharray', null)

  // Rendu noeuds (groupes g)
  d3NodeSelection = g.append('g').selectAll('g')
    .data(allNodes)
    .join('g')
    .style('cursor', (d: any) => {
      if (d.type === 'structure') return 'pointer'
      if (d.type === 'family') return 'pointer'
      if (d.type === 'hashtag') return 'pointer'
      return 'default'
    })
    .call(
      d3.drag<any, any>()
        .on('start', (event: any, d: any) => {
          if (!event.active) simulation.alphaTarget(0.3).restart()
          d.fx = d.x
          d.fy = d.y
          closePopover()
        })
        .on('drag', (event: any, d: any) => {
          d.fx = event.x
          d.fy = event.y
        })
        .on('end', (event: any, d: any) => {
          if (!event.active) simulation.alphaTarget(0)
          if (d.type !== 'family') {
            d.fx = null
            d.fy = null
          }
        })
    )
    .on('click', (event: any, d: any) => {
      event.stopPropagation()
      if (d.type === 'structure') {
        emit('select-structure', d.id)
      } else if (d.type === 'family') {
        openFamillePopover(d, event, svgEl)
      } else if (d.type === 'hashtag') {
        openHashtagPopover(d, event, svgEl)
      }
    })

  // Cercles
  d3NodeSelection.append('circle')
    .attr('r', (d: any) => d.r)
    .attr('fill', (d: any) => {
      if (d.type === 'family') return d.color
      if (d.type === 'hashtag') return d.fill
      return d.color + 'cc'
    })
    .attr('stroke', (d: any) => {
      if (d.type === 'family') return 'white'
      if (d.type === 'hashtag') return d.stroke
      return d.color
    })
    .attr('stroke-width', (d: any) => {
      if (d.type === 'family') return 3
      if (d.type === 'hashtag') return 2
      return 1.5
    })

  // Labels familles
  d3NodeSelection.filter((d: any) => d.type === 'family')
    .append('text')
    .text((d: any) => d.label)
    .attr('text-anchor', 'middle')
    .attr('dy', '0.35em')
    .attr('font-size', '11px')
    .attr('font-weight', '700')
    .attr('fill', 'white')
    .style('pointer-events', 'none')

  // Labels hashtags : texte noir sur fond clair, tronque a 12 caracteres
  d3NodeSelection.filter((d: any) => d.type === 'hashtag')
    .append('text')
    .text((d: any) => {
      const raw = d.label as string
      return raw.length > 12 ? raw.slice(0, 12) + '...' : raw
    })
    .attr('text-anchor', 'middle')
    .attr('dy', '0.35em')
    .attr('font-size', '9px')
    .attr('font-weight', '600')
    .attr('fill', '#2a2a2a')
    .style('pointer-events', 'none')

  // Labels structures : nom au-dessus du cercle, halo pour lisibilite
  d3NodeSelection.filter((d: any) => d.type === 'structure')
    .append('text')
    .attr('class', 'graph-struct-label')
    .text((d: any) => {
      const raw = d.label as string
      return raw.length > 22 ? raw.slice(0, 20) + '…' : raw
    })
    .attr('text-anchor', 'middle')
    .attr('dy', (d: any) => -(d.r + 5))
    .attr('font-size', '9.5px')
    .attr('font-weight', '500')
    .style('pointer-events', 'none')

  // Tooltip hover pour structures
  d3NodeSelection.filter((d: any) => d.type === 'structure')
    .on('mouseenter', (_event: any, d: any) => {
      if (!tooltipRef.value) return
      tooltipRef.value.style.opacity = '1'
      tooltipRef.value.innerHTML = `<strong>${d.label}</strong><br><span style="opacity:0.6;font-size:0.7rem;">${FAMILLE_LABELS[d.famille] ?? ''}</span>`
    })
    .on('mousemove', (event: any) => {
      if (!tooltipRef.value || !svgEl) return
      const rect = (svgEl as HTMLElement).getBoundingClientRect()
      tooltipRef.value.style.left = (event.clientX - rect.left + 12) + 'px'
      tooltipRef.value.style.top = (event.clientY - rect.top - 10) + 'px'
    })
    .on('mouseleave', () => {
      if (tooltipRef.value) tooltipRef.value.style.opacity = '0'
    })

  // Tick - mise a jour positions
  simulation.on('tick', () => {
    d3LinkSelection
      .attr('x1', (d: any) => d.source.x)
      .attr('y1', (d: any) => d.source.y)
      .attr('x2', (d: any) => d.target.x)
      .attr('y2', (d: any) => d.target.y)

    d3NodeSelection.attr('transform', (d: any) => `translate(${d.x},${d.y})`)

    // Surlignage selon hashtags actifs
    applyHashtagFilter()
  })
}

function buildNodesLinks(width: number, height: number) {
  const allNodes: any[] = []
  const links: any[] = []

  if (!props.data) return { allNodes, links }

  const tagFamilleMap = tagToFamille.value

  // Noeuds structures (toujours presents)
  const structureNodes = props.data.structures.map(s => ({
    id: s.id,
    type: 'structure',
    label: s.nom,
    famille: s.famille_principale,
    familles_secondaires: s.familles_secondaires ?? [],
    hashtags: s.hashtags,
    color: FAMILLE_COLORS[s.famille_principale] ?? '#888',
    r: 8,
  }))
  allNodes.push(...structureNodes)

  // Layer Familles : 6 noeuds famille fixes en etoile + liens primaires/secondaires
  if (showFamilles.value) {
    const familyNodes = [1, 2, 3, 4, 5, 6].map(id => ({
      id: `family-${id}`,
      type: 'family',
      familleId: id,
      label: FAMILLE_LABELS[id],
      color: FAMILLE_COLORS[id],
      r: 32,
      x: width / 2 + Math.cos((id - 1) * Math.PI * 2 / 6) * 180,
      y: height / 2 + Math.sin((id - 1) * Math.PI * 2 / 6) * 180,
      fx: width / 2 + Math.cos((id - 1) * Math.PI * 2 / 6) * 180,
      fy: height / 2 + Math.sin((id - 1) * Math.PI * 2 / 6) * 180,
    }))
    allNodes.push(...familyNodes)

    structureNodes.forEach(s => {
      links.push({
        source: s.id,
        target: `family-${s.famille}`,
        type: 'primary',
        strength: 0.55,
      })
      ;(s.familles_secondaires as number[]).forEach((f: number) => {
        links.push({
          source: s.id,
          target: `family-${f}`,
          type: 'secondary',
          strength: 0.45,
        })
      })
    })
  }

  // Layer Pratiques : noeuds hashtag + liens structure -> hashtag
  if (showPratiques.value) {
    const uniqueTags = new Set<string>()
    props.data.structures.forEach(s => s.hashtags.forEach(t => uniqueTags.add(t)))
    const tagsArr = Array.from(uniqueTags).sort()

    // Si seul layer Pratiques actif : disposition radiale comme reference
    // Si superpose avec Familles : laisser la simulation placer
    const radius = Math.min(width, height) * 0.32
    const hashtagNodes = tagsArr.map((tag, i) => {
      const famId = tagFamilleMap[tag]
      const strokeColor = famId != null ? FAMILLE_COLORS[famId] : '#888'
      const node: any = {
        id: `hashtag-${tag}`,
        type: 'hashtag',
        label: tag.startsWith('#') ? tag.slice(1) : tag,
        tag,
        fill: 'var(--nav-bg-alt)',
        stroke: strokeColor,
        color: strokeColor,
        r: 22,
      }
      if (!showFamilles.value) {
        const angle = (i / tagsArr.length) * Math.PI * 2
        node.x = width / 2 + Math.cos(angle) * radius
        node.y = height / 2 + Math.sin(angle) * radius
      }
      return node
    })
    allNodes.push(...hashtagNodes)

    structureNodes.forEach(s => {
      s.hashtags.forEach(tag => {
        if (uniqueTags.has(tag)) {
          links.push({
            source: s.id,
            target: `hashtag-${tag}`,
            type: 'practice',
            strength: 0.3,
          })
        }
      })
    })
  }

  return { allNodes, links }
}

function clampPopoverPosition(rect: DOMRect, evtX: number, evtY: number, w = 280, h = 180) {
  const margin = 12
  let x = evtX - rect.left + 14
  let y = evtY - rect.top + 10
  if (x + w > rect.width - margin) {
    x = Math.max(margin, rect.width - w - margin)
  }
  if (y + h > rect.height - margin) {
    y = Math.max(margin, rect.height - h - margin)
  }
  return { x, y }
}

function structuresForFamille(famId: number): { id: string; nom: string }[] {
  if (!props.data) return []
  return props.data.structures
    .filter(s =>
      s.famille_principale === famId
      || (s.familles_secondaires ?? []).includes(famId)
    )
    .sort((a, b) => a.nom.localeCompare(b.nom, 'fr'))
    .map(s => ({ id: s.id, nom: s.nom }))
}

function openFamillePopover(d: any, event: any, svgEl: SVGElement) {
  const rect = (svgEl as HTMLElement).getBoundingClientRect()
  const famId = d.familleId as number
  const desc = FAMILLE_DESCRIPTIONS[famId] ?? ''
  const structures = structuresForFamille(famId)
  const { x, y } = clampPopoverPosition(rect, event.clientX, event.clientY, 280, 280)
  popover.value = {
    open: true,
    kind: 'famille',
    x,
    y,
    title: FAMILLE_LABELS[famId] ?? '',
    body: desc,
    color: FAMILLE_COLORS[famId] ?? '#000',
    structures,
    familleId: famId,
    famillesCount: 0,
  }
}

function openHashtagPopover(d: any, event: any, svgEl: SVGElement) {
  const rect = (svgEl as HTMLElement).getBoundingClientRect()
  const tag = d.tag as string
  const structures = structuresForHashtag(tag)
  const famId = tagToFamille.value[tag]
  const color = famId != null ? FAMILLE_COLORS[famId] : '#444'
  // Compter les familles distinctes parmi les porteuses (famille_principale)
  const famSet = new Set<number>()
  if (props.data) {
    props.data.structures
      .filter(s => s.hashtags.includes(tag))
      .forEach(s => famSet.add(s.famille_principale))
  }
  const { x, y } = clampPopoverPosition(rect, event.clientX, event.clientY, 280, 220)
  popover.value = {
    open: true,
    kind: 'hashtag',
    x,
    y,
    title: tag.startsWith('#') ? tag : '#' + tag,
    body: '',
    color,
    structures,
    familleId: null,
    famillesCount: famSet.size,
  }
}

function applyHashtagFilter() {
  if (!d3NodeSelection || !d3LinkSelection) return
  if (filteredStructureIds.value) {
    const ids = filteredStructureIds.value
    d3NodeSelection.filter((d: any) => d.type === 'structure').select('circle')
      .attr('opacity', (d: any) => ids.has(d.id) ? 1 : 0.1)
    d3LinkSelection.attr('opacity', (d: any) => {
      const srcId = typeof d.source === 'object' ? d.source.id : d.source
      const tgtId = typeof d.target === 'object' ? d.target.id : d.target
      return ids.has(srcId) || ids.has(tgtId) ? 1 : 0.05
    })
  } else {
    d3NodeSelection.select('circle').attr('opacity', 1)
    d3LinkSelection.attr('opacity', 1)
  }
}

// Declencher quand l'onglet devient visible
watch(() => props.active, (val) => {
  if (val && import.meta.client && props.data) {
    requestAnimationFrame(() => requestAnimationFrame(() => initGraph()))
  }
})

// Relancer si les donnees arrivent apres l'activation
watch(() => props.data, (val) => {
  if (val && props.active && import.meta.client) {
    requestAnimationFrame(() => requestAnimationFrame(() => initGraph()))
  }
})

// Re-appliquer le filtre visuel sans rebuild complet
watch(activeHashtags, () => {
  applyHashtagFilter()
  if (simulation) simulation.alpha(0.01).restart()
}, { deep: true })

// Watchers layers : rebuild simulation
watch([showFamilles, showPratiques], () => {
  closePopover()
  if (import.meta.client && props.data && props.active) {
    requestAnimationFrame(() => requestAnimationFrame(() => initGraph()))
  }
})

// Toggle sidebar : largeur SVG change -> reinit graphe apres transition CSS
watch(sidebarOpen, () => {
  if (!import.meta.client || !props.active || !props.data) return
  setTimeout(() => {
    requestAnimationFrame(() => requestAnimationFrame(() => initGraph()))
  }, 220)
})

onMounted(async () => {
  if (import.meta.client && props.data && props.active) {
    await nextTick()
    initGraph()
  }
})

onUnmounted(() => {
  if (simulation) simulation.stop()
})
</script>

<style>
/* Labels des structures dans le graphe (D3 injecte les <text>, donc style global) */
.graph-view .graph-struct-label {
  fill: var(--nav-text);
  opacity: 0.7;
  paint-order: stroke;
  stroke: var(--nav-bg);
  stroke-width: 3px;
  stroke-linejoin: round;
  user-select: none;
}
</style>
