<!--
  FlowEditor.vue — Visual editor for a flow_definition tree.

  A domain-agnostic editor for step trees. Host components pass a kind catalog
  (PROJECT_KINDS, AGENT_KINDS, …) and the editor renders the tree, opens the
  step editor modal, and emits updates.

  Props:
    - modelValue:   the full flow_definition object (v-model). Shape:
                    { version, trigger, variables, steps: [Step, ...] }
    - kindCatalog:  which kinds are allowed (controls the "add step" menu)
    - readonly:     disable all editing (useful for preview mode)
    - appliesTo:    domain context for the variable picker
                    ('Customer' | 'Quotation' | 'Service Contract' | 'Issue' | 'Subscription')

  Events:
    - update:modelValue(newDef):  emitted whenever the tree changes (add/edit/delete)
    - step-click(step):           for hosts that want to intercept clicks

  Reordering
  ==========
  Supports two equivalent ways to change step order:
    1. Up/down arrows on each FlowNode (touch-friendly, discrete)
    2. Drag-and-drop via vuedraggable (mouse-friendly, continuous)

  Both paths converge on `reorderPeers(scopePred, oldIdx, newIdx)` which does
  the heavy lifting:
    - Rebuilds the flat `steps` array with the peer subset in the new order,
      keeping non-peers and their flat positions untouched.
    - Detects a "clean sequential chain" (each peer_i depends only on peer_{i-1})
      and regenerates `depends_on` in the new order so that visual order =
      execution order. If the chain is non-sequential (parallel fork, explicit
      multi-deps), `depends_on` is left intact — we don't silently rewrite a
      hand-authored DAG.

  DnD scope isolation
  -------------------
  Each peer group (same parent_id + same branch) renders inside its own
  <draggable> with a unique `group` name, so Sortable.js can't move a step
  across scopes. Cross-scope reordering is intentionally unsupported because
  it would require recomputing parent_id / branch, which the arrows UI can't
  express either — keeps the mental model consistent.

  Performance notes
  -----------------
    - Uses shallow filter + computed for root-level steps (O(n) per render,
      n is small for typical flows < 50 steps).
    - Reorder is O(n) flat-array rebuild + O(peers) chain detection, both cheap.
    - Emits a complete def on each change — host should debounce save to API.
-->
<template>
  <div class="flow-editor">
    <div v-if="!steps.length" class="flow-editor-empty">
      <q-icon name="account_tree" size="32px" color="grey-5" />
      <div class="text-caption text-grey-7 q-mt-sm">Flow vide</div>
      <q-btn v-if="!readonly" color="indigo-6" label="Ajouter une étape" no-caps size="sm"
        icon="add" @click="openNew()" class="q-mt-sm" />
    </div>

    <template v-else>
      <!-- Root-level draggable list: one sortable per scope. -->
      <draggable
        :list="rootPeersMirror"
        item-key="id"
        :group="{ name: 'flow-root', 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="onRootDragEnd"
      >
        <template #item="{ element }">
          <FlowNode
            :step="element" :all-steps="steps" :kind-catalog="kindCatalog" :depth="0"
            :readonly="readonly"
            @edit="onEdit"
            @delete="onDelete"
            @add="onAddChild"
            @move="onMove"
            @reorder="onReorderScope"
          />
        </template>
      </draggable>

      <button v-if="!readonly" class="flow-add-root" @click="openNew()">
        <q-icon name="add" size="16px" /> Ajouter une étape
      </button>
    </template>

    <StepEditorModal v-model="modalOpen"
      :step="editingStep"
      :kind-catalog="kindCatalog"
      :all-step-ids="allStepIds"
      :all-steps="steps"
      :applies-to="appliesTo"
      @save="onSave" />

    <!-- Kind picker popover for "add new" -->
    <q-dialog v-model="kindPickerOpen">
      <q-card style="min-width: 320px; max-width: 95vw">
        <q-card-section>
          <div class="text-subtitle2 text-weight-bold">Quel type d'étape ?</div>
        </q-card-section>
        <q-list separator>
          <q-item v-for="(def, key) in kindCatalog" :key="key" clickable
            @click="addStepOfKind(key)">
            <q-item-section avatar>
              <q-icon :name="def.icon" :style="{ color: def.color }" />
            </q-item-section>
            <q-item-section>
              <q-item-label>{{ def.label }}</q-item-label>
              <q-item-label v-if="def.help" caption>{{ def.help }}</q-item-label>
            </q-item-section>
          </q-item>
        </q-list>
      </q-card>
    </q-dialog>
  </div>
</template>

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

const props = defineProps({
  modelValue:  { type: Object, required: true },
  kindCatalog: { type: Object, required: true },
  readonly:    { type: Boolean, default: false },
  appliesTo:   { type: String, default: null },
})

const emit = defineEmits(['update:modelValue', 'step-click'])

/** All steps in the tree. Defaults to [] if flow def is empty. */
const steps = computed(() => props.modelValue?.steps || [])

/** Top-level steps (no parent). */
const rootSteps = computed(() => steps.value.filter(s => !s.parent_id))

/**
 * Local mirror of the root peer group for vuedraggable.
 *
 * vuedraggable mutates the array it's bound to (Sortable.js moves the DOM first,
 * then the component splices the underlying list to match). We can't feed it a
 * computed ref directly because that's read-only. So we maintain a local ref
 * and sync it from the authoritative `rootSteps` whenever the source changes.
 *
 * The watcher uses the stringified ID-order as the dependency so we only
 * re-mirror when the set/order actually changes — not on every step edit.
 */
const rootPeersMirror = ref([])
watch(
  () => rootSteps.value.map(s => s.id).join('|'),
  () => { rootPeersMirror.value = [...rootSteps.value] },
  { immediate: true },
)

/** All step IDs — used by the editor modal for depends_on selection. */
const allStepIds = computed(() => steps.value.map(s => s.id))

// --- Editor modal state -----------------------------------------------------

const modalOpen = ref(false)
const editingStep = ref(null)

// --- Kind picker popover state ---------------------------------------------

const kindPickerOpen = ref(false)
/** Pending add context: when we open the picker, remember where to attach. */
const pendingParent = ref(null)
const pendingBranch = ref(null)

/** Emit a full def update with the steps array replaced. */
function emitSteps (newSteps) {
  emit('update:modelValue', { ...props.modelValue, steps: newSteps })
}

/** Open the step editor for an existing step. */
function onEdit (step) {
  if (props.readonly) return emit('step-click', step)
  editingStep.value = step
  modalOpen.value = true
}

/** Save an edited step back into the tree (in place by id). */
function onSave (edited) {
  const newSteps = steps.value.map(s => s.id === edited.id ? edited : s)
  emitSteps(newSteps)
}

// ─── Reordering ────────────────────────────────────────────────────────────

/**
 * Core reorder primitive — used by both the arrow buttons and the drag-and-drop
 * path. Moves a peer from `oldPeerIdx` to `newPeerIdx` within the scope defined
 * by `scopePred`, rewrites `depends_on` if the chain was sequential, and emits.
 *
 * @param {(step) => boolean} scopePred  identifies the peer group
 * @param {number}            oldPeerIdx 0-based index within the peer group
 * @param {number}            newPeerIdx 0-based target index within the peer group
 */
function reorderPeers (scopePred, oldPeerIdx, newPeerIdx) {
  if (props.readonly) return
  if (oldPeerIdx === newPeerIdx) return

  const arr = steps.value.map(s => ({ ...s }))

  // Record flat-array positions of peers so we can reinsert them in place.
  const peerFlatSlots = []
  arr.forEach((s, i) => { if (scopePred(s)) peerFlatSlots.push(i) })
  if (oldPeerIdx < 0 || oldPeerIdx >= peerFlatSlots.length) return
  if (newPeerIdx < 0 || newPeerIdx >= peerFlatSlots.length) return

  const peersBefore = peerFlatSlots.map(i => arr[i])
  const peerIdSet = new Set(peersBefore.map(s => s.id))

  // Detect a clean sequential chain: each peer_i depends on exactly peer_{i-1}
  // (and nothing else within the group). External deps don't count.
  const isSequential = peersBefore.every((p, i) => {
    const inGroup = (p.depends_on || []).filter(d => peerIdSet.has(d))
    if (i === 0) return inGroup.length === 0
    return inGroup.length === 1 && inGroup[0] === peersBefore[i - 1].id
  })

  // Build the new peer order.
  const peersAfter = [...peersBefore]
  const [moved] = peersAfter.splice(oldPeerIdx, 1)
  peersAfter.splice(newPeerIdx, 0, moved)

  // Regenerate sequential chain if that's what we detected.
  if (isSequential) {
    peersAfter.forEach((p, i) => {
      // Keep dependencies on steps outside the peer group (cross-scope refs).
      // Also filter out any self-ref that might have slipped in — this can't
      // happen from a clean reorder, but guards against stale data.
      const external = (p.depends_on || [])
        .filter(d => !peerIdSet.has(d) && d !== p.id)
      const prevId = i === 0 ? null : peersAfter[i - 1].id
      const chained = prevId && prevId !== p.id ? [...external, prevId] : external
      // Dedupe in case external already contained prevId for some reason.
      p.depends_on = [...new Set(chained)]
    })
  }

  // Reinsert peers at their original flat positions, now in the new order.
  peerFlatSlots.forEach((flatIdx, peerIdx) => {
    arr[flatIdx] = peersAfter[peerIdx]
  })

  emitSteps(arr)
}

/**
 * ± 1 arrow-button reorder.
 * Translates to a reorderPeers call on the peer group of the given step.
 */
function onMove (id, dir) {
  const self = steps.value.find(s => s.id === id)
  if (!self) return
  const scopePred = (x) => x.parent_id === self.parent_id && x.branch === self.branch
  const peers = steps.value.filter(scopePred)
  const peerIdx = peers.findIndex(s => s.id === id)
  const target = dir === 'up' ? peerIdx - 1 : peerIdx + 1
  if (target < 0 || target >= peers.length) return
  reorderPeers(scopePred, peerIdx, target)
}

/**
 * DnD reorder event — bubbled up from any FlowNode at any depth.
 * Arguments describe the scope that was reordered + the new indices.
 */
function onReorderScope (parentId, branch, oldIdx, newIdx) {
  const scopePred = (x) =>
    (x.parent_id || null) === (parentId || null) &&
    (x.branch || null) === (branch || null)
  reorderPeers(scopePred, oldIdx, newIdx)
}

/** DnD on the root scope — call Sortable's oldIndex/newIndex. */
function onRootDragEnd (evt) {
  if (typeof evt.oldIndex !== 'number' || typeof evt.newIndex !== 'number') return
  // Sortable has already visually moved the DOM inside rootPeersMirror. That's
  // fine: after emitSteps + Vue re-render, the watcher resyncs the mirror from
  // the authoritative order, so there's no double-apply.
  reorderPeers(s => !s.parent_id, evt.oldIndex, evt.newIndex)
}

// ─── Delete ────────────────────────────────────────────────────────────────

/** Delete a step and all its descendants (recursive cleanup). */
function onDelete (stepId) {
  const toRemove = new Set([stepId])
  // Find transitive descendants by parent_id
  let changed = true
  while (changed) {
    changed = false
    for (const s of steps.value) {
      if (s.parent_id && toRemove.has(s.parent_id) && !toRemove.has(s.id)) {
        toRemove.add(s.id)
        changed = true
      }
    }
  }
  const newSteps = steps.value
    .filter(s => !toRemove.has(s.id))
    // Also strip depends_on references to deleted steps
    .map(s => ({
      ...s,
      depends_on: (s.depends_on || []).filter(d => !toRemove.has(d)),
    }))
  emitSteps(newSteps)
}

// ─── Add step ──────────────────────────────────────────────────────────────

/** Open the kind picker for a new root-level step. */
function openNew () {
  pendingParent.value = null
  pendingBranch.value = null
  kindPickerOpen.value = true
}

/** Open the kind picker for a child step (under a condition/switch branch). */
function onAddChild (parentId, branch) {
  pendingParent.value = parentId
  pendingBranch.value = branch
  kindPickerOpen.value = true
}

/** After user picks a kind, create the step and open the editor. */
function addStepOfKind (kindName) {
  const step = buildEmptyStep(kindName, props.kindCatalog)
  if (pendingParent.value) {
    step.parent_id = pendingParent.value
    step.branch = pendingBranch.value
  }
  const newSteps = [...steps.value, step]
  emitSteps(newSteps)
  kindPickerOpen.value = false
  // Open editor for the new step (next tick so the prop updates)
  setTimeout(() => {
    editingStep.value = step
    modalOpen.value = true
  }, 50)
}
</script>

<style scoped>
.flow-editor { display: flex; flex-direction: column; gap: 4px; }
.flow-editor-empty {
  text-align: center;
  padding: 32px 16px;
  background: #f8fafc;
  border: 1px dashed #cbd5e1;
  border-radius: 6px;
}
.flow-add-root {
  margin-top: 8px;
  padding: 6px 12px;
  background: #f8fafc;
  border: 1px dashed #cbd5e1;
  border-radius: 6px;
  color: #64748b;
  font-size: 0.8rem;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: all 0.15s;
}
.flow-add-root:hover { background: #eef2ff; color: #4338ca; border-color: #6366f1; }
</style>

<!--
  Global drag-state styles — deliberately unscoped because Sortable.js assigns
  these classes to nodes that live inside recursive FlowNode children, and
  scoped <style> wouldn't reach them.
-->
<style>
.flow-drag-ghost {
  opacity: 0.35;
  background: #e0e7ff !important;
  border: 2px dashed #6366f1 !important;
}
.flow-drag-chosen { cursor: grabbing; }
.flow-drag-dragging {
  transform: rotate(0.5deg);
  box-shadow: 0 8px 24px rgba(99, 102, 241, 0.25) !important;
  border-color: #6366f1 !important;
}
</style>
