<script setup lang="ts">
// Carte O - mindmap force-directed D3 (V1 zoom + pan + drag + click).
// Pattern adapté de nav-carte/components/codev/CodevGraph.vue (sans coupling Nuxt).
// Drill-down V1 = zoom + pan seul. V2 récursif backlog (cf MP-PAGE-CERVEAU.md).

import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
import * as d3 from 'd3'

interface CarteNode {
  id: string
  label: string
  family: string
  niveau?: number
  nature?: 'essai' | 'projet'
  statut?: 'gestation' | 'edite'
  resume?: string | null
  radius?: number
  intention?: string
  slug?: string
  theme?: string
  path?: string
  domain?: string  // V1.2-O : domaine plateforme source (logo Brandfetch CDN)
}

// V1.2-O : logos plateforme via Brandfetch CDN (visible zoom > 1.5x seulement)
const BRANDFETCH_CLIENT_ID = '4ae58bd85c8140eab0cee72f40656120'
const LOGO_ZOOM_THRESHOLD = 1.5

// V1.3-D : dimensions rectangle central (phrase manifeste 3 lignes)
const CENTRAL_W = 300
const CENTRAL_H = 64
const CENTRAL_COLLIDE_RADIUS = 160  // demi-largeur + marge
const logoUrl = (domain: string) =>
  `https://cdn.brandfetch.io/${domain}/w/64/h/64?c=${BRANDFETCH_CLIENT_ID}`

interface CarteEdge {
  source: string | CarteNode
  target: string | CarteNode
  central?: boolean  // V1.3-D : link au noeud central (court/fort)
}

const props = withDefaults(defineProps<{
  nodes: CarteNode[]
  edges: CarteEdge[]
  familyColors?: Record<string, string>
}>(), {
  familyColors: () => ({
    penseur:   '#3b82f6',
    concept:   '#10b981',
    methode:   '#f59e0b',
    collectif: '#ef4444',
    ressource: '#8b5cf6',
  }),
})

const emit = defineEmits<{
  'node-click': [payload: { node: CarteNode; x: number; y: number }]
}>()

// Refs
const container = ref<HTMLDivElement | null>(null)
const svgEl = ref<SVGSVGElement | null>(null)
const width = ref(800)
const height = ref(600)

// State interne
type SimNode = d3.SimulationNodeDatum & CarteNode
type SimLink = d3.SimulationLinkDatum<SimNode>

let simulation: d3.Simulation<SimNode, SimLink> | null = null
let svgRoot: d3.Selection<SVGSVGElement, unknown, null, undefined> | null = null
let gZoom: d3.Selection<SVGGElement, unknown, null, undefined> | null = null
let gLinks: d3.Selection<SVGGElement, unknown, null, undefined> | null = null
let gNodes: d3.Selection<SVGGElement, unknown, null, undefined> | null = null
let zoomBehavior: d3.ZoomBehavior<SVGSVGElement, unknown> | null = null

const isMobile = computed(() => width.value < 600)
const nodeRadius = computed(() => isMobile.value ? 10 : 14)

// V1.2-N palette : encre / papier / ocre
// - central niveau 0 : fill encre #0F172A, label blanc inscrit dans le cercle
// - essais (niveaux 1-2) : fill papier #FFFFFF, stroke encre, label encre a droite
// - projets : fill ocre #B45309, label blanc inscrit dans le cercle
// - statut "edite" : stroke epaissi (l'epaisseur conserve l'info, pas la couleur)
function colorFor(d: SimNode): string {
  if (d.nature === 'projet') return '#B45309'
  if (d.niveau === 0) return '#0F172A'
  return '#FFFFFF'
}

function getRadius(d: SimNode): number {
  if (d.radius != null) return d.radius
  if (d.niveau === 0) return 30
  if (d.nature === 'projet') return 18
  return nodeRadius.value
}

function strokeFor(d: SimNode): string {
  if (d.nature === 'projet') return '#B45309'
  if (d.niveau === 0) return '#0F172A'
  return '#0F172A'
}

function strokeWidthFor(d: SimNode): number {
  return d.statut === 'edite' ? 2.5 : 1.5
}

function labelWeightFor(d: SimNode): string {
  return d.statut === 'edite' ? 'bold' : 'normal'
}

// Label inscrit dans le cercle (fond fonce) pour central + projets
function labelInsideFor(d: SimNode): boolean {
  return d.niveau === 0 || d.nature === 'projet'
}

function labelColorFor(d: SimNode): string {
  if (labelInsideFor(d)) return '#FFFFFF'
  return d.statut === 'edite' ? '#0F172A' : '#475569'
}

function truncate(str: string, max: number): string {
  if (!str) return ''
  return str.length > max ? str.slice(0, max - 1) + '…' : str
}

// V1.3-D : Wrap intelligent du label central sur 3 lignes (rectangle 300x64).
// Phrase cible : "Une medecine du corps social pour ecrire un nouveau contrat social"
// Heuristique : decoupe en mots, repartit sur ~3 lignes de longueur equilibree.
function splitCentralLabel(label: string, maxLines = 3, maxCharsPerLine = 30): string[] {
  if (!label) return ['']
  // Fallback : pivot legacy " + " (V1.2 backward compat)
  if (label.includes(' + ')) {
    const parts = label.split(' + ')
    return [parts[0].trim(), '+', parts.slice(1).join(' + ').trim()]
  }
  const words = label.split(/\s+/).filter(Boolean)
  if (words.length <= 1) return [label]

  // Repartit en lignes en respectant maxCharsPerLine, max maxLines lignes
  const lines: string[] = []
  let current = ''
  for (const w of words) {
    const next = current ? `${current} ${w}` : w
    if (next.length > maxCharsPerLine && current && lines.length < maxLines - 1) {
      lines.push(current)
      current = w
    } else {
      current = next
    }
  }
  if (current) lines.push(current)
  // Si plus de lignes que prevu (mots tres longs), concatene les dernieres
  while (lines.length > maxLines) {
    const last = lines.pop()!
    lines[lines.length - 1] = `${lines[lines.length - 1]} ${last}`
  }
  return lines
}

function buildSimNodes(): SimNode[] {
  return props.nodes.map(n => ({ ...n }))
}

function buildSimLinks(simNodes: SimNode[]): SimLink[] {
  const byId = new Map(simNodes.map(n => [n.id, n]))
  return props.edges
    .map(e => {
      const s = typeof e.source === 'string' ? byId.get(e.source) : byId.get(e.source.id)
      const t = typeof e.target === 'string' ? byId.get(e.target) : byId.get(e.target.id)
      if (!s || !t) return null
      return { source: s, target: t } as SimLink
    })
    .filter((x): x is SimLink => x !== null)
}

function makeDrag(sim: d3.Simulation<SimNode, SimLink>) {
  return d3.drag<SVGGElement, SimNode>()
    .on('start', (event, d) => {
      if (!event.active) sim.alphaTarget(0.3).restart()
      d.fx = d.x
      d.fy = d.y
    })
    .on('drag', (event, d) => {
      d.fx = event.x
      d.fy = event.y
    })
    .on('end', (event, d) => {
      if (!event.active) sim.alphaTarget(0)
      d.fx = null
      d.fy = null
    })
}

function initSvg() {
  if (!svgEl.value) return

  svgRoot = d3.select(svgEl.value)
    .attr('width', width.value)
    .attr('height', height.value)
    .attr('role', 'img')
    .attr('aria-label', 'Carte O — mindmap de la pensée AEP')

  svgRoot.selectAll('*').remove()

  // Wrapper group for zoom/pan transform.
  gZoom = svgRoot.append('g').attr('class', 'zoom-layer')
  gLinks = gZoom.append('g').attr('class', 'links')
  gNodes = gZoom.append('g').attr('class', 'nodes')

  // Zoom + pan behavior.
  zoomBehavior = d3.zoom<SVGSVGElement, unknown>()
    .scaleExtent([0.3, 4])
    .on('zoom', (event) => {
      gZoom!.attr('transform', event.transform.toString())
      // V1.2-O : toggle visibilite logos plateforme selon le niveau de zoom
      // (evite la surcharge visuelle au niveau d'ensemble, montre detail au drill-down)
      const scale = event.transform.k
      gZoom!.selectAll('.logo-overlay')
        .style('opacity', scale > LOGO_ZOOM_THRESHOLD ? 1 : 0)
    })

  svgRoot.call(zoomBehavior as any)
}

function render() {
  if (!svgEl.value || props.nodes.length === 0) return

  initSvg()

  const simNodes = buildSimNodes()
  const simLinks = buildSimLinks(simNodes)

  const fontSize = isMobile.value ? 9 : 11

  // Liens — gris doux papier
  gLinks!
    .selectAll<SVGLineElement, SimLink>('line')
    .data(simLinks)
    .join('line')
    .attr('stroke', '#94A3B8')
    .attr('stroke-opacity', 0.4)
    .attr('stroke-width', 1)

  // Noeuds = groupe <g> par node
  const nodeGroups = gNodes!
    .selectAll<SVGGElement, SimNode>('g.node')
    .data(simNodes, d => d.id)
    .join('g')
    .attr('class', 'node')
    .style('cursor', 'pointer')
    .on('click', (_event, d) => emit('node-click', { node: d as CarteNode, x: (d as SimNode).x || 0, y: (d as SimNode).y || 0 }))
    .on('mouseover', function (_event, d) {
      // V1.3-D : selecteur etendu rect|circle (rect pour central, circle pour autres)
      d3.select(this).select('rect, circle')
        .transition().duration(120)
        .attr('stroke-width', strokeWidthFor(d) + 1.5)
    })
    .on('mouseout', function (_event, d) {
      d3.select(this).select('rect, circle')
        .transition().duration(120)
        .attr('stroke-width', strokeWidthFor(d))
    })

  // V1.3-D : nœud central = rectangle 300x64 (phrase 3 lignes), autres = cercle
  const isCentral = (d: SimNode) => d.niveau === 0

  nodeGroups.filter(isCentral)
    .append('rect')
    .attr('x', -CENTRAL_W / 2)
    .attr('y', -CENTRAL_H / 2)
    .attr('width', CENTRAL_W)
    .attr('height', CENTRAL_H)
    .attr('rx', 6)
    .attr('fill', d => colorFor(d))
    .attr('stroke', d => strokeFor(d))
    .attr('stroke-width', d => strokeWidthFor(d))

  nodeGroups.filter(d => !isCentral(d))
    .append('circle')
    .attr('r', d => getRadius(d))
    .attr('fill', d => colorFor(d))
    .attr('stroke', d => strokeFor(d))
    .attr('stroke-width', d => strokeWidthFor(d))

  // Label : inscrit dans le cercle pour central + projets, a droite sinon
  // Pour le noeud central (label long), on passe en multi-lignes
  nodeGroups.each(function (d) {
    const g = d3.select(this)
    const inside = labelInsideFor(d)

    if (inside && d.niveau === 0) {
      // V1.3-D : noeud central = rectangle, label 3 lignes 13px line-height 1.35
      const fs = isMobile.value ? 11 : 13
      const lineHeight = Math.round(fs * 1.35)
      const parts = splitCentralLabel(d.label, 3, isMobile.value ? 22 : 30)
      const startY = -((parts.length - 1) * lineHeight) / 2
      parts.forEach((line, i) => {
        g.append('text')
          .attr('text-anchor', 'middle')
          .attr('dominant-baseline', 'central')
          .attr('x', 0)
          .attr('y', startY + i * lineHeight)
          .attr('font-size', fs)
          .attr('font-family', 'system-ui, sans-serif')
          .attr('font-weight', '500')
          .attr('fill', '#FFFFFF')
          .attr('pointer-events', 'none')
          .text(line)
      })
    } else if (inside) {
      // Projets : label centre dans le cercle (court)
      g.append('text')
        .attr('text-anchor', 'middle')
        .attr('dominant-baseline', 'central')
        .attr('x', 0)
        .attr('y', 0)
        .attr('font-size', fontSize)
        .attr('font-family', 'system-ui, sans-serif')
        .attr('font-weight', labelWeightFor(d))
        .attr('fill', '#FFFFFF')
        .attr('pointer-events', 'none')
        .text(truncate(d.label, 6))
    } else {
      // Essais : label a droite du cercle
      g.append('text')
        .attr('text-anchor', 'start')
        .attr('dominant-baseline', 'central')
        .attr('dx', getRadius(d) + 4)
        .attr('font-size', fontSize)
        .attr('font-family', 'system-ui, sans-serif')
        .attr('font-weight', labelWeightFor(d))
        .attr('fill', labelColorFor(d))
        .attr('pointer-events', 'none')
        .text(truncate(d.label, isMobile.value ? 18 : 30))
    }
  })

  // V1.2-O : Logo plateforme (visible zoom > 1.5x seulement)
  // Cercle blanc 14px en bas-droite du node (offset +60% x/+60% y du radius)
  // Image clip-path circle pour avatar style. Hidden par defaut (opacity 0).
  const nodeGroupsWithLogo = nodeGroups.filter(d => !!d.domain)

  const logoOverlay = nodeGroupsWithLogo.append('g')
    .attr('class', 'logo-overlay')
    .style('opacity', 0)
    .attr('pointer-events', 'none')

  logoOverlay.append('circle')
    .attr('cx', d => getRadius(d) * 0.6)
    .attr('cy', d => getRadius(d) * 0.6)
    .attr('r', 9)
    .attr('fill', '#FFFFFF')
    .attr('stroke', '#0F172A')
    .attr('stroke-width', 0.5)

  logoOverlay.append('image')
    .attr('href', d => logoUrl(d.domain!))
    .attr('x', d => getRadius(d) * 0.6 - 7)
    .attr('y', d => getRadius(d) * 0.6 - 7)
    .attr('width', 14)
    .attr('height', 14)
    .attr('clip-path', d => `circle(7px at ${getRadius(d) * 0.6}px ${getRadius(d) * 0.6}px)`)

  // Tooltip <title>
  nodeGroups.append('title')
    .text(d => `${d.label}\n[${d.family}]\n${truncate(d.resume || d.intention || '', 200)}`)

  // V1.3-D : Simulation force-directed fit-cadre + animation continue subtile
  // - link distance/strength differencies : TMIP (central) court/fort, autres essais souples
  // - collide +12 zero overlap labels
  // - forceX/Y faibles rappel cadre
  // - alphaDecay 0.025 (anime sans vertige)
  simulation = d3.forceSimulation<SimNode, SimLink>(simNodes)
    .force('link', d3.forceLink<SimNode, SimLink>(simLinks)
      .id(d => d.id)
      .distance(l => {
        const link = l as SimLink & { central?: boolean }
        // Lien central -> TMIP (projet) = court (90), autres centraux (essais) = standard (200)
        if (link.central) {
          const tgt = link.target as SimNode
          const src = link.source as SimNode
          const other = (tgt.niveau === 0 ? src : tgt) as SimNode
          if (other.nature === 'projet') return 90
          return 200
        }
        return 180
      })
      .strength(l => {
        const link = l as SimLink & { central?: boolean }
        if (link.central) {
          const tgt = link.target as SimNode
          const src = link.source as SimNode
          const other = (tgt.niveau === 0 ? src : tgt) as SimNode
          if (other.nature === 'projet') return 0.6
          return 0.3
        }
        return 0.3
      }))
    .force('charge', d3.forceManyBody<SimNode>().strength(d => {
      if (d.niveau === 0) return -1200
      if (d.niveau === 1) return -400
      if (d.niveau === 2) return -180
      return -220
    }))
    .force('center', d3.forceCenter(width.value / 2, height.value / 2))
    .force('x', d3.forceX<SimNode>(width.value / 2).strength(0.05))
    .force('y', d3.forceY<SimNode>(height.value / 2).strength(0.05))
    .force('collide', d3.forceCollide<SimNode>().radius(d => {
      // V1.3-D : central traite comme zone large (rect 300x64 -> rayon equivalent 160)
      if (d.niveau === 0) return CENTRAL_COLLIDE_RADIUS
      return getRadius(d) + 12
    }))
    .alphaDecay(0.025)
    .velocityDecay(0.4)
    .alphaMin(0.001)
    .on('tick', tick)

  // Bind drag once simulation exists.
  nodeGroups.call(makeDrag(simulation) as any)
}

function tick() {
  if (!gLinks || !gNodes) return

  gLinks.selectAll<SVGLineElement, SimLink>('line')
    .attr('x1', d => (d.source as SimNode).x ?? 0)
    .attr('y1', d => (d.source as SimNode).y ?? 0)
    .attr('x2', d => (d.target as SimNode).x ?? 0)
    .attr('y2', d => (d.target as SimNode).y ?? 0)

  gNodes.selectAll<SVGGElement, SimNode>('g.node')
    .attr('transform', d => `translate(${d.x ?? 0},${d.y ?? 0})`)
}

// Public methods exposed (zoom controls).
function zoomIn() {
  if (!svgRoot || !zoomBehavior) return
  svgRoot.transition().duration(250).call(zoomBehavior.scaleBy as any, 1.3)
}
function zoomOut() {
  if (!svgRoot || !zoomBehavior) return
  svgRoot.transition().duration(250).call(zoomBehavior.scaleBy as any, 0.77)
}
function zoomReset() {
  if (!svgRoot || !zoomBehavior) return
  svgRoot.transition().duration(350).call(zoomBehavior.transform as any, d3.zoomIdentity)
}
defineExpose({ zoomIn, zoomOut, zoomReset })

// Re-render on data changes.
watch(() => [props.nodes, props.edges], () => {
  if (simulation) {
    simulation.stop()
    simulation = null
  }
  render()
}, { deep: true })

// ResizeObserver
let ro: ResizeObserver | null = null

onMounted(() => {
  if (!container.value) return
  width.value = container.value.clientWidth || 800
  height.value = container.value.clientHeight || 600

  render()

  ro = new ResizeObserver(() => {
    if (!container.value) return
    width.value = container.value.clientWidth || 800
    height.value = container.value.clientHeight || 600
    if (svgRoot) {
      svgRoot.attr('width', width.value).attr('height', height.value)
    }
    if (simulation) {
      simulation.force('center', d3.forceCenter(width.value / 2, height.value / 2))
      simulation.force('x', d3.forceX<SimNode>(width.value / 2).strength(0.05))
      simulation.force('y', d3.forceY<SimNode>(height.value / 2).strength(0.05))
      simulation.alpha(0.3).restart()
    }
  })
  ro.observe(container.value)
})

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

<template>
  <div ref="container" class="carte-o-wrap">
    <svg ref="svgEl" class="carte-o-svg" />

    <!-- Zoom controls overlay -->
    <div class="zoom-controls">
      <button type="button" @click="zoomIn" aria-label="Zoom avant">+</button>
      <button type="button" @click="zoomReset" aria-label="Réinitialiser le zoom">⟲</button>
      <button type="button" @click="zoomOut" aria-label="Zoom arrière">−</button>
    </div>
  </div>
</template>

<style scoped>
.carte-o-wrap {
  width: 100%;
  height: 100%;
  position: relative;
  background: #FAFAF7;
  overflow: hidden;
}

.carte-o-svg {
  width: 100%;
  height: 100%;
  display: block;
  cursor: grab;
}

.carte-o-svg:active {
  cursor: grabbing;
}

.zoom-controls {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 4px;
  background: rgba(255, 255, 255, 0.85);
  border: 1px solid rgba(0, 0, 0, 0.06);
  border-radius: 8px;
  padding: 4px;
  backdrop-filter: blur(4px);
}

.zoom-controls button {
  width: 28px;
  height: 28px;
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 16px;
  font-weight: 600;
  color: #374151;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.zoom-controls button:hover {
  background: rgba(0, 0, 0, 0.05);
}
</style>
