<!--
  FlowNode.vue — Recursive node component for the FlowEditor tree.

  Renders a single step as a clickable card + its children (branches and
  nested nodes). The component is fully generic: behaviour depends on the
  `kindCatalog` prop, never on the kind name directly.

  Props:
    - step:        the step object to render (has id, kind, label, payload, ...)
    - allSteps:    full steps array (for looking up children)
    - kindCatalog: PROJECT_KINDS or AGENT_KINDS (see kind-catalogs.js)
    - depth:       nesting depth (for indentation)
    - readonly:    disable reorder + delete controls

  Events:
    - edit(step):    user clicked a node to edit
    - delete(id):    user wants to delete step
    - add(parentId, branch): user wants to add a child under this step
    - move(id, dir): reorder within the same (parent_id, branch) scope;
                     dir is 'up' or 'down'
    - reorder(parentId, branch, oldIdx, newIdx): DnD reorder, bubbled up
                     through the recursive tree so FlowEditor can handle it.

  Drag-and-drop
  -------------
  Each peer group gets its own <draggable> with a unique `group` name derived
  from `step.id + branch`. This prevents Sortable.js from moving items across
  scopes (which would require recomputing parent_id/branch semantics).

  Each draggable binds to a LOCAL MIRROR ref, kept in sync via watchers on the
  authoritative `allSteps` prop. Sortable mutates those mirrors during the
  drag; on drag end we emit `reorder` and let FlowEditor rebuild the flat
  steps array, which triggers a re-render that resyncs the mirrors.

  Performance:
    - Children are computed once via a cached filter (Vue memoizes computed).
    - Mirror watchers run only when the ID-order of a scope actually changes.
    - Recursion bounded by tree depth (linear in number of steps).
-->
<template>
  <div class="flow-node-wrap" :style="{ marginLeft: depth ? '16px' : '0' }">
    <div class="flow-node" :class="`flow-node-${step.kind}`"
      :style="{ borderLeftColor: kindDef.color }"
      @click.stop="$emit('edit', step)">
      <div class="flow-node-head">
        <!-- Drag handle — Sortable.js only starts a drag when the user grabs
             this exact element (see handle=".flow-drag-handle" on the
             <draggable> wrapper in FlowEditor / the recursive branches below). -->
        <q-icon v-if="!readonly" name="drag_indicator"
          size="16px" color="grey-5" class="flow-drag-handle"
          @click.stop>
          <q-tooltip>Glisser pour réordonner</q-tooltip>
        </q-icon>
        <q-icon :name="kindDef.icon" size="16px" :style="{ color: kindDef.color }" class="flow-node-icon" />
        <span class="flow-node-label">{{ step.label || step.id }}</span>
        <span class="flow-node-type">{{ kindDef.label }}</span>
        <span v-if="triggerBadge" class="flow-node-badge">{{ triggerBadge }}</span>
        <div v-if="!readonly" class="flow-node-actions">
          <q-btn flat round dense size="xs" icon="arrow_upward" color="grey-6"
            :disable="!canMoveUp"
            @click.stop="$emit('move', step.id, 'up')">
            <q-tooltip>Déplacer vers le haut</q-tooltip>
          </q-btn>
          <q-btn flat round dense size="xs" icon="arrow_downward" color="grey-6"
            :disable="!canMoveDown"
            @click.stop="$emit('move', step.id, 'down')">
            <q-tooltip>Déplacer vers le bas</q-tooltip>
          </q-btn>
          <q-btn flat round dense size="xs" icon="close" color="grey-5"
            @click.stop="$emit('delete', step.id)">
            <q-tooltip>Supprimer</q-tooltip>
          </q-btn>
        </div>
      </div>
      <div v-if="summaryLines.length" class="flow-node-body">
        <div v-for="(line, i) in summaryLines" :key="i" class="flow-node-detail">{{ line }}</div>
      </div>
    </div>

    <!-- Branches (for condition/switch-like kinds) -->
    <div v-if="hasBranches" class="flow-node-branches">
      <div v-for="branch in branchNames" :key="branch" class="flow-branch">
        <div class="flow-branch-label" :class="`flow-branch-${branch}`">
          {{ branchLabel(branch) }}
        </div>
        <draggable
          :list="branchMirrors[branch] || []"
          item-key="id"
          :group="{ name: `flow-${step.id}-${branch}`, pull: false, put: false }"
          handle=".flow-drag-handle"
          :animation="150"
          ghost-class="flow-drag-ghost"
          chosen-class="flow-drag-chosen"
          drag-class="flow-drag-dragging"
          :disabled="readonly"
          @end="(evt) => onBranchDragEnd(branch, evt)"
        >
          <template #item="{ element: child }">
            <FlowNode
              :step="child" :all-steps="allSteps" :kind-catalog="kindCatalog" :depth="depth + 1"
              :readonly="readonly"
              @edit="(s) => $emit('edit', s)"
              @delete="(id) => $emit('delete', id)"
              @add="(p, b) => $emit('add', p, b)"
              @move="(id, d) => $emit('move', id, d)"
              @reorder="(p, b, o, n) => $emit('reorder', p, b, o, n)"
            />
          </template>
        </draggable>
        <button v-if="!readonly" class="flow-add-child"
          @click.stop="$emit('add', step.id, branch)">+</button>
      </div>
      <button v-if="dynamicBranches && !readonly" class="flow-add-branch"
        @click.stop="onAddBranch">+ Branche</button>
    </div>

    <!-- Non-branching children (flat children without a branch name) -->
    <div v-else-if="flatChildren.length" class="flow-node-children">
      <draggable
        :list="flatMirror"
        item-key="id"
        :group="{ name: `flow-${step.id}-flat`, pull: false, put: false }"
        handle=".flow-drag-handle"
        :animation="150"
        ghost-class="flow-drag-ghost"
        chosen-class="flow-drag-chosen"
        drag-class="flow-drag-dragging"
        :disabled="readonly"
        @end="onFlatDragEnd"
      >
        <template #item="{ element: child }">
          <FlowNode
            :step="child" :all-steps="allSteps" :kind-catalog="kindCatalog" :depth="depth + 1"
            :readonly="readonly"
            @edit="(s) => $emit('edit', s)"
            @delete="(id) => $emit('delete', id)"
            @add="(p, b) => $emit('add', p, b)"
            @move="(id, d) => $emit('move', id, d)"
            @reorder="(p, b, o, n) => $emit('reorder', p, b, o, n)"
          />
        </template>
      </draggable>
    </div>
  </div>
</template>

<script setup>
import { computed, ref, watch } from 'vue'
import draggable from 'vuedraggable'
import { getKind, getTrigger } from './kind-catalogs'

const props = defineProps({
  step:        { type: Object,  required: true },
  allSteps:    { type: Array,   default: () => [] },
  kindCatalog: { type: Object,  required: true },
  depth:       { type: Number,  default: 0 },
  readonly:    { type: Boolean, default: false },
})

const emit = defineEmits(['edit', 'delete', 'add', 'move', 'reorder'])

/**
 * A step can only move within its (parent_id, branch) scope — that keeps
 * branch semantics intact. We compute the peers ordered by their index in
 * the global `allSteps` array (which is the source of truth for ordering).
 */
const peers = computed(() =>
  props.allSteps.filter(
    s => s.parent_id === props.step.parent_id && s.branch === props.step.branch,
  ),
)
const peerIndex = computed(() => peers.value.findIndex(s => s.id === props.step.id))
const canMoveUp   = computed(() => peerIndex.value > 0)
const canMoveDown = computed(() => peerIndex.value >= 0 && peerIndex.value < peers.value.length - 1)

/** Resolved kind descriptor from the catalog (icon, color, branchLabels, …). */
const kindDef = computed(() => getKind(props.kindCatalog, props.step.kind))

/** True when this kind supports branches (yes/no for condition, dynamic for switch). */
const hasBranches = computed(() => !!kindDef.value.hasBranches)

/** True when the kind supports adding arbitrary branch names (e.g. switch). */
const dynamicBranches = computed(() => kindDef.value.hasBranches === 'dynamic')

/** Branch names: either fixed (yes/no) or all distinct branches of children. */
const branchNames = computed(() => {
  if (kindDef.value.branchLabels) return Object.keys(kindDef.value.branchLabels)
  const set = new Set()
  for (const s of props.allSteps) {
    if (s.parent_id === props.step.id && s.branch) set.add(s.branch)
  }
  return [...set]
})

/** Human label for a branch (from catalog definitions or raw key). */
function branchLabel (branch) {
  return kindDef.value.branchLabels?.[branch] || branch
}

/** Children in a specific branch (e.g. 'yes' branch of a condition). */
function childrenOfBranch (branch) {
  return props.allSteps.filter(s => s.parent_id === props.step.id && s.branch === branch)
}

/** Children without a branch (flat nesting, rarely used). */
const flatChildren = computed(() =>
  props.allSteps.filter(s => s.parent_id === props.step.id && !s.branch),
)

// ── Local mirrors for vuedraggable ─────────────────────────────────────────
//
// vuedraggable mutates the list it's bound to. We can't give it a computed
// ref (read-only), so we keep local mirrors and resync them whenever the
// authoritative source changes. The watchers key off stringified ID-order so
// they don't fire on unrelated edits (e.g. a peer's payload change).

/** Per-branch mirrors: { yes: [step,...], no: [step,...], ... } */
const branchMirrors = ref({})
/** Flat-children mirror. */
const flatMirror = ref([])

watch(
  () => branchNames.value.map(b => `${b}:${childrenOfBranch(b).map(s => s.id).join(',')}`).join('|'),
  () => {
    const next = {}
    for (const b of branchNames.value) next[b] = [...childrenOfBranch(b)]
    branchMirrors.value = next
  },
  { immediate: true },
)

watch(
  () => flatChildren.value.map(s => s.id).join('|'),
  () => { flatMirror.value = [...flatChildren.value] },
  { immediate: true },
)

/** DnD ended inside a branch — bubble the reorder up with scope info. */
function onBranchDragEnd (branch, evt) {
  if (typeof evt.oldIndex !== 'number' || typeof evt.newIndex !== 'number') return
  if (evt.oldIndex === evt.newIndex) return
  emit('reorder', props.step.id, branch, evt.oldIndex, evt.newIndex)
}

/** DnD ended inside flatChildren — bubble up (branch = null). */
function onFlatDragEnd (evt) {
  if (typeof evt.oldIndex !== 'number' || typeof evt.newIndex !== 'number') return
  if (evt.oldIndex === evt.newIndex) return
  emit('reorder', props.step.id, null, evt.oldIndex, evt.newIndex)
}

/** Prompt user for a new branch name (used for switch dynamic branches). */
function onAddBranch () {
  // eslint-disable-next-line no-alert
  const branch = prompt('Nom de la branche (ex: dying_gasp)')
  if (branch) emit('add', props.step.id, branch)
}

/** Short badge text describing the trigger (shown on the node card). */
const triggerBadge = computed(() => {
  const t = props.step.trigger?.type
  if (!t || t === 'on_prev_complete') return null
  const td = getTrigger(t)
  if (t === 'after_delay') {
    const h = props.step.trigger.delay_hours
    const d = props.step.trigger.delay_days
    if (d) return `+ ${d}j`
    if (h) return `+ ${h}h`
    return td.label
  }
  return td.label
})

/**
 * 1-2 lines of payload summary shown under the label (compact UX).
 * Skip empty payloads to keep the node card tight.
 */
const summaryLines = computed(() => {
  const lines = []
  const p = props.step.payload || {}
  switch (props.step.kind) {
    case 'dispatch_job':
      if (p.subject) lines.push(p.subject)
      if (p.assigned_group) lines.push(`${p.job_type || 'Job'} · ${p.assigned_group} · ${p.duration_h || 1}h`)
      break
    case 'issue':
      if (p.subject) lines.push(p.subject)
      break
    case 'notify':
      if (p.channel && p.template_id) lines.push(`${p.channel.toUpperCase()} · ${p.template_id}`)
      else if (p.channel) lines.push(p.channel.toUpperCase())
      break
    case 'webhook':
      if (p.url) lines.push(`${p.method || 'POST'} ${p.url.slice(0, 50)}${p.url.length > 50 ? '…' : ''}`)
      break
    case 'erp_update':
      if (p.doctype) lines.push(`${p.doctype}${p.docname_ref ? ` · ${p.docname_ref}` : ''}`)
      break
    case 'condition':
      if (p.field) lines.push(`${p.field} ${p.op || '=='} ${p.value ?? ''}`)
      break
    case 'subscription_activate':
      if (p.subscription_ref) lines.push(p.subscription_ref)
      break
    case 'respond':
      if (p.message) lines.push(p.message.length > 70 ? p.message.slice(0, 70) + '…' : p.message)
      break
    case 'action':
      if (p.action) lines.push(p.action)
      break
    case 'tool':
      if (p.tool) lines.push(p.tool)
      break
  }
  if (props.step.depends_on?.length) {
    // Resolve dep IDs → human labels so the user sees step NAMES, not cryptic
    // "s4" tokens that no longer match position after reorder. If a referenced
    // step is missing (orphan ref), we keep the raw id and mark it with a
    // question mark so it's obvious something's stale.
    const labels = props.step.depends_on.map(id => {
      const dep = props.allSteps.find(s => s.id === id)
      return dep?.label || dep?.id || `${id} ?`
    })
    lines.push(`← ${labels.join(', ')}`)
  }
  return lines
})
</script>

<style scoped>
.flow-node-wrap { margin-bottom: 4px; }

.flow-node {
  background: #fff;
  border: 1px solid #e2e8f0;
  border-left: 3px solid #94a3b8;
  border-radius: 6px;
  padding: 8px 10px;
  cursor: pointer;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.flow-node:hover {
  border-color: #6366f1;
  box-shadow: 0 2px 8px rgba(99, 102, 241, 0.12);
}

.flow-node-head {
  display: flex;
  align-items: center;
  gap: 6px;
}
.flow-node-icon { flex-shrink: 0; }

/* Drag handle: cursor changes on hover; visually muted until the card is hovered. */
.flow-drag-handle {
  cursor: grab;
  opacity: 0;
  transition: opacity 0.15s;
  flex-shrink: 0;
}
.flow-node:hover .flow-drag-handle { opacity: 0.8; }
.flow-drag-handle:active { cursor: grabbing; }

.flow-node-label {
  flex: 1;
  font-size: 0.85rem;
  font-weight: 600;
  color: #1e293b;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.flow-node-type {
  font-size: 0.65rem;
  padding: 1px 6px;
  border-radius: 3px;
  background: #f1f5f9;
  color: #64748b;
  flex-shrink: 0;
}
.flow-node-badge {
  font-size: 0.65rem;
  padding: 1px 6px;
  border-radius: 3px;
  background: #fef3c7;
  color: #a16207;
  flex-shrink: 0;
}
.flow-node-actions {
  display: flex;
  align-items: center;
  gap: 2px;
  opacity: 0;
  transition: opacity 0.15s;
  flex-shrink: 0;
}
.flow-node:hover .flow-node-actions { opacity: 1; }
/* Disabled arrows keep the nav's layout stable but stay dim. */
.flow-node-actions .q-btn--disable { opacity: 0.35 !important; }

.flow-node-body {
  margin-top: 4px;
  padding-left: 22px;
}
.flow-node-detail {
  font-size: 0.7rem;
  color: #64748b;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.flow-node-children { margin-top: 4px; padding-left: 20px; border-left: 1px dashed #cbd5e1; }

.flow-node-branches {
  margin-top: 6px;
  padding-left: 12px;
  border-left: 2px dashed #eab308;
}
.flow-branch { margin-bottom: 8px; }
.flow-branch-label {
  font-size: 0.65rem;
  font-weight: 700;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 3px;
  display: inline-block;
  margin-bottom: 4px;
}
.flow-branch-yes { background: #dcfce7; color: #166534; }
.flow-branch-no  { background: #fee2e2; color: #991b1b; }
.flow-branch-label:not(.flow-branch-yes):not(.flow-branch-no) {
  background: #e0e7ff; color: #3730a3;
}

.flow-add-child, .flow-add-branch {
  background: #f8fafc;
  border: 1px dashed #cbd5e1;
  border-radius: 4px;
  padding: 3px 10px;
  font-size: 0.7rem;
  color: #64748b;
  cursor: pointer;
  transition: background 0.15s;
}
.flow-add-child:hover, .flow-add-branch:hover { background: #eef2ff; color: #4338ca; }
</style>
