<script setup>
import { computed, ref } from "vue";
import { Icon } from "@iconify/vue";
import { useEventAncestry } from "../../../share/event-bus";
import { findOperation } from "../../operations/utils/resolve";
import TriggerRowScope from "./trigger-row-scope.vue";
import {
  useTreeDnd
} from "../../../composables/useTreeDnd";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupCombobox,
  InputGroupText
} from "../../ui/input-group";
import { CommandGroup, CommandItem } from "../../ui/command";
defineOptions({ name: "ShwfedTriggersField" });
const props = defineProps({
  triggers: { type: Array, required: true }
});
const emit = defineEmits(["update:triggers"]);
function keyOf(target, operation) {
  return `${target}::${operation}`;
}
const ancestry = useEventAncestry();
const allOptions = computed(
  () => ancestry.value.flatMap(
    (ancestor) => ancestor.operations.map((operation) => ({
      key: keyOf(ancestor.instanceId, operation.id),
      target: ancestor.instanceId,
      operation: operation.id,
      compatibilityDate: operation.compatibilityDate,
      typeName: ancestor.typeName,
      typeIcon: ancestor.typeIcon,
      instanceName: ancestor.name,
      opName: operation.name,
      opIcon: operation.icon
    }))
  )
);
const optionByKey = computed(() => {
  const map = /* @__PURE__ */ new Map();
  for (const option of allOptions.value) map.set(option.key, option);
  return map;
});
const rows = computed(
  () => props.triggers.map((trigger, index) => ({
    key: String(index),
    trigger,
    option: optionByKey.value.get(keyOf(trigger.target, trigger.operation))
  }))
);
const available = computed(() => allOptions.value);
function searchValue(option) {
  return [option.typeName, option.instanceName, option.opName].filter((part) => !!part).join(" ");
}
function addTrigger(option) {
  if (option.compatibilityDate) {
    const entry = findOperation(option.operation, option.compatibilityDate);
    emit("update:triggers", [
      ...props.triggers,
      {
        id: crypto.randomUUID(),
        target: option.target,
        operation: option.operation,
        compatibilityDate: option.compatibilityDate,
        params: entry?.defaults?.() ?? {}
      }
    ]);
    return;
  }
  emit("update:triggers", [
    ...props.triggers,
    { id: crypto.randomUUID(), target: option.target, operation: option.operation }
  ]);
}
function updateParams(index, params) {
  emit("update:triggers", props.triggers.map((t, i) => i === index ? { ...t, params } : t));
}
const collapsed = ref(/* @__PURE__ */ new Set());
function toggleCollapsed(key) {
  if (collapsed.value.has(key)) collapsed.value.delete(key);
  else collapsed.value.add(key);
}
function opConfigFor(trigger) {
  if (!trigger.compatibilityDate) return void 0;
  return findOperation(trigger.operation, trigger.compatibilityDate)?.config;
}
function removeTrigger(index) {
  emit("update:triggers", props.triggers.filter((_, i) => i !== index));
}
const publishingSteps = computed(
  () => props.triggers.flatMap((trigger, index) => {
    if (!trigger.id || !trigger.compatibilityDate) return [];
    const output = findOperation(trigger.operation, trigger.compatibilityDate)?.metadata.output;
    if (!output) return [];
    return [{ id: trigger.id, index, output }];
  })
);
function stepRosterFor(index) {
  return publishingSteps.value.map((step) => ({
    id: step.id,
    label: `\u7B2C ${step.index + 1} \u6B65`,
    valid: step.index < index,
    description: step.output
  }));
}
const ROW_KIND = "shwfed-trigger-row";
function moveTrigger(from, to) {
  if (from === to) return;
  if (from < 0 || to < 0 || from >= props.triggers.length || to >= props.triggers.length) return;
  const next = [...props.triggers];
  const [item] = next.splice(from, 1);
  if (!item) return;
  next.splice(to, 0, item);
  emit("update:triggers", next);
}
function onRowDrop(e) {
  if (e.source.kind !== ROW_KIND || e.target.kind !== ROW_KIND) return;
  const from = rows.value.findIndex((r) => r.key === e.source.id);
  const to = rows.value.findIndex((r) => r.key === e.target.id);
  if (from < 0 || to < 0) return;
  if (e.instruction === "reorder-above") moveTrigger(from, from < to ? to - 1 : to);
  else if (e.instruction === "reorder-below") moveTrigger(from, from <= to ? to : to + 1);
}
const dnd = useTreeDnd({ onRowDrop });
const pickDragHandle = (el) => el.querySelector(".drag-handle");
function rowConfig(rowKey) {
  return {
    kind: ROW_KIND,
    canDrop: (src) => src.kind === ROW_KIND,
    blockInstructions: (src) => {
      const block = ["make-child", "reparent"];
      if (src.id === rowKey) block.push("reorder-above", "reorder-below");
      return block;
    },
    dragHandle: pickDragHandle
  };
}
</script>

<template>
  <div class="flex flex-col gap-2">
    <div
      v-for="(row, index) in rows"
      :key="row.key"
      :ref="dnd.rowRef(row.key, rowConfig(row.key))"
      class="relative"
      :data-instruction="dnd.instructionFor(row.key) ?? void 0"
    >
      <InputGroup>
        <InputGroupAddon align="inline-start">
          <Icon
            icon="fluent:re-order-dots-vertical-20-regular"
            class="drag-handle size-4 shrink-0 cursor-grab text-zinc-400"
            aria-label="拖动排序"
          />
        </InputGroupAddon>

        <InputGroupText class="min-w-0 pl-1 flex-1">
          <template v-if="row.option">
            <Icon
              v-if="row.option.typeIcon"
              :icon="row.option.typeIcon"
              class="size-4 shrink-0 text-zinc-500"
            />
            <span class="shrink-0 text-zinc-500">{{ row.option.typeName }}</span>
            <span
              v-if="row.option.instanceName"
              class="truncate font-medium text-zinc-700"
            >「{{ row.option.instanceName }}」</span>
            <Icon icon="fluent:arrow-step-in-right-20-regular" />
            <Icon
              v-if="row.option.opIcon"
              :icon="row.option.opIcon"
              class="size-4 shrink-0 text-zinc-500"
            />
            <span class="truncate text-zinc-700">{{ row.option.opName }}</span>
          </template>
          <template v-else>
            <Icon
              icon="fluent:warning-20-regular"
              class="size-4 shrink-0 text-amber-500"
            />
            <span class="shrink-0 text-amber-600">目标已失效</span>
            <span class="truncate text-xs tabular-nums text-zinc-400">
              {{ row.trigger.target.slice(0, 8) }} · {{ row.trigger.operation }}
            </span>
          </template>
        </InputGroupText>

        <InputGroupAddon align="inline-end">
          <!-- Collapse toggle — only for rows with a param editor; param-less
               and dangling rows have nothing to fold, so it is omitted. -->
          <InputGroupButton
            v-if="opConfigFor(row.trigger)"
            size="icon-xs"
            as-child
          >
            <button
              type="button"
              :aria-expanded="!collapsed.has(row.key)"
              :aria-label="collapsed.has(row.key) ? '\u5C55\u5F00\u53C2\u6570' : '\u6298\u53E0\u53C2\u6570'"
              @click="toggleCollapsed(row.key)"
            >
              <Icon
                :icon="collapsed.has(row.key) ? 'fluent:chevron-right-20-regular' : 'fluent:chevron-down-20-regular'"
              />
            </button>
          </InputGroupButton>
          <InputGroupButton
            variant="destructive"
            size="icon-xs"
            as-child
          >
            <button
              type="button"
              aria-label="移除"
              @click="removeTrigger(index)"
            >
              <Icon icon="fluent:delete-20-regular" />
            </button>
          </InputGroupButton>
        </InputGroupAddon>
      </InputGroup>

      <!-- Parameterized op: its own `config.vue`, bound to the trigger's
           `params`. Param-less ops and dangling rows render nothing here.
           `v-show` (not `v-if`) so the editor stays mounted while collapsed —
           its draft state and continuous validation keep running. The
           row-scope wrapper provides this row's step roster (earlier rows'
           outputs) to the param editor's ExpressionEditors. -->
      <div
        v-if="opConfigFor(row.trigger)"
        v-show="!collapsed.has(row.key)"
        class="mt-2 rounded-md border border-zinc-200 bg-zinc-50/50 p-3"
      >
        <TriggerRowScope :roster="stepRosterFor(index)">
          <component
            :is="opConfigFor(row.trigger)"
            :model-value="row.trigger.params ?? {}"
            @update:model-value="(v) => updateParams(index, v)"
          />
        </TriggerRowScope>
      </div>
    </div>

    <InputGroup v-if="available.length > 0">
      <InputGroupAddon align="inline-start">
        <Icon icon="fluent:add-20-regular" />
      </InputGroupAddon>
      <InputGroupCombobox>
        <CommandGroup>
          <CommandItem
            v-for="option in available"
            :key="option.key"
            :value="searchValue(option)"
            @select="addTrigger(option)"
          >
            <Icon
              v-if="option.typeIcon"
              :icon="option.typeIcon"
              class="size-4 shrink-0 text-zinc-500"
            />
            <span class="text-zinc-500">{{ option.typeName }}</span>
            <span
              v-if="option.instanceName"
              class="font-medium"
            >「{{ option.instanceName }}」</span>
            <span class="text-zinc-400">：</span>
            <span class="flex-1">{{ option.opName }}</span>
          </CommandItem>
        </CommandGroup>
      </InputGroupCombobox>
    </InputGroup>

    <p
      v-else-if="rows.length === 0"
      class="text-xs text-zinc-400"
    >
      当前位置没有可触发的祖先组件。
    </p>
  </div>
</template>

<style scoped>
[data-instruction=reorder-above]:before,[data-instruction=reorder-below]:after{background:var(--primary,#2563eb);content:"";height:2px;left:0;pointer-events:none;position:absolute;right:0;z-index:1}[data-instruction=reorder-above]:before{top:-5px}[data-instruction=reorder-below]:after{bottom:-5px}
</style>
