<script setup>
import { cel as rawCel } from "../../utils/cel";
import { Icon } from "@iconify/vue";
import { Effect } from "effect";
import { computed, inject, onBeforeUnmount, provide, ref, useTemplateRef, watch } from "vue";
import { toast } from "vue-sonner";
import {
  celBindings,
  injectCELContext,
  provideCELContext,
  provideSelectionRoster
} from "../../utils/cel-context";
import { findFreePlacement, normalizeLayoutSet, placeGroupAt } from "../../share/layout";
import { readClip, reidFragment, writeClip } from "../../share/clipboard";
import { getLocalizedText } from "../../share/locale";
import BlockLayoutEditor from "../block-layout-editor/index.vue";
import LayoutsSidebar from "../block-layout-editor/sidebar.vue";
import LayoutMetaStrip from "../block-layout-editor/meta-strip.vue";
import { BREADCRUMB_EXTENSION_KEY } from "../config/breadcrumb-extension";
import { SIDEBAR_TAKEOVER_KEY } from "../sidebar-takeover";
import { FORM_FIELD_LAYOUT_KEY } from "./field-layout";
import { Button } from "../ui/button";
import { ScrollArea } from "../ui/scroll-area";
import { Separator } from "../ui/separator";
import { FIELDS, findField } from "./utils/resolve";
defineOptions({ name: "ShwfedFormUnitConfig" });
const unit = defineModel({ type: Object, ...{ required: true } });
const selection = defineModel("selection", { type: Object, ...{
  default: () => ({ kind: "layout" })
} });
const props = defineProps({
  extras: { type: Array, required: false },
  configure: { type: Function, required: false },
  fieldCelScope: { type: Object, required: false }
});
defineSlots();
const activeLayoutIndex = ref(0);
const stack = ref([{ kind: "top" }]);
const top = computed(() => stack.value[stack.value.length - 1]);
const isDrilled = computed(() => stack.value.length > 1);
function popOne() {
  if (stack.value.length <= 1) return;
  stack.value = stack.value.slice(0, -1);
}
const breadcrumbExt = inject(BREADCRUMB_EXTENSION_KEY, null);
const fullPane = ref(false);
provide(FORM_FIELD_LAYOUT_KEY, { fullPane });
provideCELContext(props.fieldCelScope ?? {});
const selectionRoster = computed(
  () => unit.value.fields.filter((f) => findField(f.type, f.compatibilityDate)?.metadata?.selection).map((f) => ({ id: f.id, name: fieldLabel(f) }))
);
provideSelectionRoster(selectionRoster);
const takeover = inject(SIDEBAR_TAKEOVER_KEY, null);
const takeoverTarget = computed(() => takeover?.target.value ?? null);
const activeFieldId = computed(() => {
  return top.value.kind === "field-drill" ? top.value.id : null;
});
const activeField = computed(() => {
  const id = activeFieldId.value;
  if (!id) return null;
  return unit.value.fields.find((f) => f.id === id) ?? null;
});
const activeFieldEntry = computed(() => {
  const f = activeField.value;
  return f ? findField(f.type, f.compatibilityDate) ?? null : null;
});
function selectExtra(id) {
  selection.value = { kind: "extras", id };
}
function selectLayout() {
  selection.value = { kind: "layout" };
}
function drillField(id) {
  stack.value = [...stack.value, { kind: "field-drill", id }];
}
function switchDrillField(id) {
  if (!stack.value.length) return;
  if (id === activeFieldId.value) return;
  if (top.value.kind !== "field-drill") return;
  stack.value = [...stack.value.slice(0, -1), { kind: "field-drill", id }];
}
function fieldIcon(f) {
  return findField(f.type, f.compatibilityDate)?.metadata?.icon ?? "fluent:square-20-regular";
}
const layoutsModel = computed({
  get: () => [...unit.value.layouts],
  set: (next) => {
    unit.value = { ...unit.value, layouts: next };
  }
});
const sidebarRef = useTemplateRef("sidebarRef");
function addLayout() {
  sidebarRef.value?.addLayout();
}
const FIELD_PICKER_ENTRIES = computed(() => {
  const byType = /* @__PURE__ */ new Map();
  for (const f of FIELDS) {
    if (f.deprecated) continue;
    const prev = byType.get(f.type);
    if (!prev || f.compatibilityDate > prev.compatibilityDate) {
      byType.set(f.type, {
        type: f.type,
        compatibilityDate: f.compatibilityDate,
        name: f.metadata.name,
        icon: f.metadata.icon
      });
    }
  }
  return [...byType.values()].sort((a, b) => a.name.localeCompare(b.name, "zh"));
});
function makeId() {
  if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
    const r = Math.random() * 16 | 0;
    return (c === "x" ? r : r & 3 | 8).toString(16);
  });
}
function defaultField(entry) {
  const fieldEntry = findField(entry.type, entry.compatibilityDate);
  return {
    ...fieldEntry?.defaults?.() ?? {},
    id: makeId(),
    type: entry.type,
    compatibilityDate: entry.compatibilityDate
  };
}
function addField(entry, options) {
  const field = defaultField(entry);
  const meta = findField(entry.type, entry.compatibilityDate)?.metadata;
  const w = meta?.w.initial ?? 1;
  const h = meta?.h.initial ?? 1;
  const i = activeLayoutIndex.value;
  const nextLayouts = unit.value.layouts.map((ls, idx) => {
    if (idx !== i) return ls;
    const placement = findFreePlacement(ls.layout.placements, w, h);
    const placements = { ...ls.layout.placements, [field.id]: placement };
    let cols = 0;
    let rows = 0;
    for (const p of Object.values(placements)) {
      const [, [x2, y2]] = p.area;
      if (x2 - 1 > cols) cols = x2 - 1;
      if (y2 - 1 > rows) rows = y2 - 1;
    }
    return {
      ...ls,
      layout: {
        ...ls.layout,
        placements,
        columns: Math.max(1, cols),
        rows: Math.max(1, rows)
      }
    };
  });
  unit.value = {
    ...unit.value,
    fields: [...unit.value.fields, field],
    layouts: nextLayouts
  };
  if (!options?.silent) {
    drillField(field.id);
  }
}
function removeField(id) {
  removeFields([id]);
}
function removeFields(ids) {
  if (ids.length === 0) return;
  const drop = new Set(ids);
  unit.value = {
    ...unit.value,
    fields: unit.value.fields.filter((f) => !drop.has(f.id)),
    layouts: unit.value.layouts.map((ls) => {
      const entries = Object.entries(ls.layout.placements).filter(([pid]) => !drop.has(pid));
      if (entries.length === Object.keys(ls.layout.placements).length) return ls;
      return normalizeLayoutSet({
        ...ls,
        layout: { ...ls.layout, placements: Object.fromEntries(entries) }
      });
    })
  };
  if (top.value.kind === "field-drill" && drop.has(top.value.id)) {
    stack.value = stack.value.slice(0, -1);
  }
}
async function copyFields(ids) {
  const want = new Set(ids);
  const items = unit.value.fields.filter((f) => want.has(f.id));
  if (items.length === 0) return;
  const ls = unit.value.layouts[activeLayoutIndex.value];
  const placements = {};
  if (ls) {
    for (const id of ids) {
      const p = ls.layout.placements[id];
      if (p) placements[id] = p;
    }
  }
  if (!await writeClip("form", items, placements)) {
    toast.error("\u590D\u5236\u5931\u8D25");
    return;
  }
  toast.success(`\u5DF2\u590D\u5236 ${items.length} \u4E2A\u5B57\u6BB5`);
}
async function pasteFields(target) {
  const payload = await readClip();
  if (!payload || payload.surface !== "form") return;
  const { items, placements } = reidFragment(payload.items, payload.placements);
  const pasted = items;
  if (pasted.length === 0) return;
  const place = placements;
  if (place && Object.keys(place).length > 0) {
    const i = activeLayoutIndex.value;
    const nextLayouts = unit.value.layouts.map((ls, idx) => {
      if (idx !== i) return ls;
      const shifted = placeGroupAt(ls.layout.placements, place, target ?? { x: 1, y: 1 });
      return normalizeLayoutSet({
        ...ls,
        layout: { ...ls.layout, placements: { ...ls.layout.placements, ...shifted } }
      });
    });
    unit.value = { ...unit.value, fields: [...unit.value.fields, ...pasted], layouts: nextLayouts };
  } else {
    unit.value = { ...unit.value, fields: [...unit.value.fields, ...pasted] };
  }
  toast.success(`\u5DF2\u7C98\u8D34 ${pasted.length} \u4E2A\u5B57\u6BB5`);
}
function updateField(id, next) {
  const i = unit.value.fields.findIndex((f) => f.id === id);
  if (i < 0) return;
  const fields = [...unit.value.fields];
  fields[i] = next;
  unit.value = { ...unit.value, fields };
}
function updateActiveField(next) {
  const id = activeFieldId.value;
  if (!id) return;
  updateField(id, next);
}
function fieldById(id) {
  return unit.value.fields.find((f) => f.id === id) ?? null;
}
function fieldEntryById(id) {
  const f = fieldById(id);
  return f ? findField(f.type, f.compatibilityDate) ?? null : null;
}
const inheritedCEL = injectCELContext();
function celForMigration(expression, context) {
  return rawCel(expression, { ...celBindings(inheritedCEL), ...context });
}
const migrationContext = { $cel: celForMigration };
const latestEntry = computed(() => {
  const e = activeFieldEntry.value;
  if (!e?.deprecated || !e.supersededBy) return null;
  return findField(e.supersededBy.type, e.supersededBy.compatibilityDate) ?? null;
});
const canMigrate = computed(() => !!latestEntry.value?.migrate);
async function migrateActiveField() {
  const sel = activeField.value;
  const target = latestEntry.value;
  if (!sel || !target?.migrate) return;
  try {
    const next = await Effect.runPromise(target.migrate(sel, migrationContext));
    updateActiveField(next);
    const isSameType = target.type === sel.type;
    toast.success(
      isSameType ? `\u5DF2\u8FC1\u79FB\u5230 ${target.compatibilityDate} \u7248\u672C` : `\u5DF2\u8FC1\u79FB\u5230 ${target.type}@${target.compatibilityDate}`
    );
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    toast.error("\u8FC1\u79FB\u5931\u8D25", { description: message });
  }
}
function fieldLabel(f) {
  const fa = f;
  if (typeof fa.displayName === "string" && fa.displayName.length > 0) {
    return fa.displayName;
  }
  const label = fa.label;
  return getLocalizedText(label, "zh") ?? "\u672A\u547D\u540D\u5B57\u6BB5";
}
function getFieldLabel(field) {
  return fieldLabel(field);
}
if (breadcrumbExt) {
  const crumb = breadcrumbExt.add();
  watch(
    [isDrilled, activeField],
    ([drilled, field]) => {
      if (drilled && field) {
        crumb.label.value = `\u7F16\u8F91${fieldLabel(field)}`;
        crumb.back.value = popOne;
      } else {
        crumb.label.value = null;
        crumb.back.value = null;
      }
    },
    { immediate: true }
  );
  onBeforeUnmount(() => {
    crumb.dispose();
  });
}
function isExtraActive(id) {
  return selection.value.kind === "extras" && selection.value.id === id;
}
</script>

<template>
  <!-- Sidebar takeover: when a host provides a takeover target, teleport our
       left rail into it. This flattens nested sidebars when the form is
       drilled into from the actions modal-layout button. -->
  <Teleport
    v-if="takeoverTarget && !fullPane"
    :to="takeoverTarget"
  >
    <template v-if="!isDrilled">
      <ScrollArea class="flex-1">
        <template v-if="extras && extras.length > 0">
          <div
            v-for="extra in extras"
            :key="extra.id"
            class="row general-row pl-2"
            :class="[
  isExtraActive(extra.id) ? 'bg-[color-mix(in_srgb,var(--primary)_10%,white)] text-(--primary)' : 'text-zinc-600 hover:bg-zinc-50'
]"
            @click.stop="selectExtra(extra.id)"
          >
            <Icon
              :icon="extra.icon"
              class="size-4 shrink-0"
            />
            <span class="flex-1 truncate">{{ extra.label }}</span>
          </div>

          <Separator class="my-1" />
        </template>

        <LayoutsSidebar
          ref="sidebarRef"
          v-model="layoutsModel"
          v-model:active-index="activeLayoutIndex"
          :active="selection.kind === 'layout'"
          @select="selectLayout()"
        />
      </ScrollArea>

      <Button
        variant="ghost"
        size="sm"
        @click="addLayout()"
      >
        <Icon
          icon="fluent:add-20-regular"
          class="size-4"
        />
        <span>新增布局</span>
      </Button>
    </template>
    <template v-else>
      <ScrollArea class="flex-1">
        <div class="flex flex-col gap-0.5">
          <button
            v-for="f in unit.fields"
            :key="f.id"
            type="button"
            class="row w-full pl-2 text-left"
            :class="[
  activeFieldId === f.id ? 'bg-[color-mix(in_srgb,var(--primary)_10%,white)] text-(--primary)' : 'text-zinc-600 hover:bg-zinc-50'
]"
            @click="switchDrillField(f.id)"
          >
            <Icon
              :icon="fieldIcon(f)"
              class="size-4 shrink-0"
            />
            <span class="flex-1 truncate">{{ getFieldLabel(f) }}</span>
          </button>
        </div>
      </ScrollArea>
    </template>
  </Teleport>

  <div class="flex gap-2 min-h-128">
    <!-- Inline sidebar: top-level shows extras + layouts; drilled view swaps
         to a field switcher so users can hop between fields without going
         back. Suppressed when the host claims our rail via
         SIDEBAR_TAKEOVER_KEY, or when a drilled field config claims the right
         pane via FORM_FIELD_LAYOUT_KEY (fullPane). -->
    <div
      v-if="!fullPane && !takeoverTarget"
      class="flex w-64 shrink-0 flex-col"
    >
      <template v-if="!isDrilled">
        <ScrollArea class="flex-1">
          <template v-if="extras && extras.length > 0">
            <div
              v-for="extra in extras"
              :key="extra.id"
              class="row general-row pl-2"
              :class="[
  isExtraActive(extra.id) ? 'bg-[color-mix(in_srgb,var(--primary)_10%,white)] text-(--primary)' : 'text-zinc-600 hover:bg-zinc-50'
]"
              @click.stop="selectExtra(extra.id)"
            >
              <Icon
                :icon="extra.icon"
                class="size-4 shrink-0"
              />
              <span class="flex-1 truncate">{{ extra.label }}</span>
            </div>

            <Separator class="my-1" />
          </template>

          <LayoutsSidebar
            ref="sidebarRef"
            v-model="layoutsModel"
            v-model:active-index="activeLayoutIndex"
            :active="selection.kind === 'layout'"
            @select="selectLayout()"
          />
        </ScrollArea>

        <Button
          variant="ghost"
          size="sm"
          @click="addLayout()"
        >
          <Icon
            icon="fluent:add-20-regular"
            class="size-4"
          />
          <span>新增布局</span>
        </Button>
      </template>
      <template v-else>
        <ScrollArea class="flex-1">
          <div class="flex flex-col gap-0.5">
            <button
              v-for="f in unit.fields"
              :key="f.id"
              type="button"
              class="row w-full pl-2 text-left"
              :class="[
  activeFieldId === f.id ? 'bg-[color-mix(in_srgb,var(--primary)_10%,white)] text-(--primary)' : 'text-zinc-600 hover:bg-zinc-50'
]"
              @click="switchDrillField(f.id)"
            >
              <Icon
                :icon="fieldIcon(f)"
                class="size-4 shrink-0"
              />
              <span class="flex-1 truncate">{{ getFieldLabel(f) }}</span>
            </button>
          </div>
        </ScrollArea>
      </template>
    </div>

    <!-- Right pane -->
    <div class="flex flex-1 flex-col">
      <!-- Extras pane -->
      <div
        v-if="!isDrilled && selection.kind === 'extras'"
      >
        <slot
          :id="selection.id"
          name="extras-pane"
        />
      </div>

      <!-- 布局 view -->
      <div
        v-else-if="!isDrilled && selection.kind === 'layout'"
        class="flex flex-1 flex-col gap-2"
      >
        <LayoutMetaStrip
          v-model="layoutsModel"
          :index="activeLayoutIndex"
        />
        <div class="flex-1">
          <BlockLayoutEditor
            v-model="layoutsModel"
            v-model:active-index="activeLayoutIndex"
            :items="unit.fields"
            :find-entry="findField"
            :get-item-label="getFieldLabel"
            :picker-entries="FIELD_PICKER_ENTRIES"
            :can-delete="true"
            item-noun="字段"
            @add-new="addField"
            @drill-down="drillField"
            @delete-item="removeField"
            @delete-items="removeFields"
            @copy-items="copyFields"
            @paste="pasteFields"
          >
            <!-- Fullscreen inline-config pane body: render the field's own
                 config editor for a simple field opened inline, without
                 pushing the drill stack. -->
            <template #inline-config="{ itemId }">
              <component
                :is="fieldEntryById(itemId).config"
                v-if="fieldEntryById(itemId)?.config"
                :model-value="fieldById(itemId)"
                @update:model-value="(next) => updateField(itemId, next)"
              />
              <div
                v-else
                class="text-sm text-red-500"
              >
                未注册的字段类型
              </div>
            </template>
          </BlockLayoutEditor>
        </div>
      </div>

      <!-- 字段 view (drill) -->
      <div
        v-else-if="activeField"
        :class="fullPane ? '' : 'p-6'"
      >
        <div
          v-if="activeFieldEntry"
        >
          <div
            v-if="activeFieldEntry.deprecated && activeFieldEntry.supersededBy"
            class="mb-4 flex items-center gap-2 rounded border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800"
            role="alert"
          >
            <Icon
              icon="fluent:warning-20-regular"
              class="size-4 shrink-0"
            />
            <span class="flex-1">
              此字段类型已弃用，建议迁移至 <span class="tabular-nums font-medium">{{ activeFieldEntry.supersededBy.type === activeFieldEntry.type ? activeFieldEntry.supersededBy.compatibilityDate : `${activeFieldEntry.supersededBy.type}@${activeFieldEntry.supersededBy.compatibilityDate}` }}</span>。
            </span>
            <Button
              v-if="canMigrate"
              variant="ghost"
              size="sm"
              class="text-amber-800 hover:bg-amber-100 hover:text-amber-900"
              data-slot="field-migrate"
              @click="migrateActiveField"
            >
              <Icon icon="fluent:arrow-sync-20-regular" />
              迁移
            </Button>
          </div>
          <component
            :is="activeFieldEntry.config"
            :model-value="activeField"
            @update:model-value="updateActiveField"
          />
        </div>
        <div
          v-else
          class="text-sm text-red-500"
        >
          未注册的字段类型：{{ activeField.type }}@{{ activeField.compatibilityDate }}
        </div>
      </div>

      <!-- Empty state -->
      <div
        v-else
        class="flex min-h-112 items-center justify-center p-8 text-sm text-zinc-400"
      >
        选择左侧项目以查看和编辑
      </div>
    </div>
  </div>
</template>

<style scoped>
.row{align-items:center;border-radius:.25rem;cursor:pointer;display:flex;font-size:.875rem;gap:.375rem;padding-block:.375rem;padding-right:.5rem;position:relative;transition:background-color .1s ease;-webkit-user-select:none;-moz-user-select:none;user-select:none}.general-row{font-weight:500}
</style>
