<script setup>
import { Icon } from "@iconify/vue";
import { Effect, Fiber, Option } from "effect";
import { Fetch } from "fx-fetch";
import { computed, ref, shallowRef, watch } from "vue";
import { useI18n } from "vue-i18n";
import { fetchJsonOption } from "../../../../../share/request";
import { cel as _rawCel } from "../../../../../utils/cel";
import { celBindings, injectCELContext } from "../../../../../utils/cel-context";
import { publishSelection } from "../../../../../utils/selections-registry";
import { getLocalizedText } from "../../../../../share/locale";
import { Field, FieldLabel, FieldMessages } from "../../../../ui/field";
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "../../../../ui/input-group";
import { Markdown } from "../../../../ui/markdown";
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../../../../ui/popover";
import { Skeleton } from "../../../../ui/skeleton";
import { Tree as UiTree } from "../../../../ui/tree";
import { interpolateMarkdown } from "../../../../table/utils/runtime";
import { DEFAULT_FIELD_ORIENTATION } from "../../../utils/common";
import { useFormReadonly } from "../../../utils/readonly";
import { useFormState } from "../../../utils/state";
import { useFieldValidation } from "../../../utils/validation";
import {
  findWrappedPath,
  getWrappedChildren,
  wrapStructuralAll
} from "../tree-combobox-shared";
defineOptions({ name: "ShwfedTreeComboboxMultiFieldRuntime" });
const props = defineProps({
  fieldId: { type: String, required: true },
  config: { type: null, required: true }
});
const { locale, t } = useI18n({
  inheritLocale: true,
  messages: {
    zh: {
      "tree-combobox-multi-placeholder": "\u8BF7\u9009\u62E9",
      "tree-combobox-multi-search-placeholder": "\u641C\u7D22\u2026",
      "tree-combobox-multi-search-clear": "\u6E05\u9664\u641C\u7D22",
      "tree-combobox-multi-empty": "\u65E0\u53EF\u9009\u9879",
      "tree-combobox-multi-load-error": "\u52A0\u8F7D\u5931\u8D25",
      "tree-combobox-multi-retry": "\u91CD\u8BD5",
      "tree-combobox-multi-readonly-empty": "\u2014"
    },
    en: {
      "tree-combobox-multi-placeholder": "Select\u2026",
      "tree-combobox-multi-search-placeholder": "Search\u2026",
      "tree-combobox-multi-search-clear": "Clear search",
      "tree-combobox-multi-empty": "No options",
      "tree-combobox-multi-load-error": "Failed to load",
      "tree-combobox-multi-retry": "Retry",
      "tree-combobox-multi-readonly-empty": "\u2014"
    },
    ja: {
      "tree-combobox-multi-placeholder": "\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044",
      "tree-combobox-multi-search-placeholder": "\u691C\u7D22\u2026",
      "tree-combobox-multi-search-clear": "\u691C\u7D22\u3092\u30AF\u30EA\u30A2",
      "tree-combobox-multi-empty": "\u9078\u629E\u80A2\u306A\u3057",
      "tree-combobox-multi-load-error": "\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
      "tree-combobox-multi-retry": "\u518D\u8A66\u884C",
      "tree-combobox-multi-readonly-empty": "\u2014"
    }
  }
});
const { state, getAt, setAt } = useFormState();
const inherited = injectCELContext();
const $cel = (expression, context) => _rawCel(expression, { ...celBindings(inherited), ...context });
const labelText = computed(() => getLocalizedText(props.config.label, locale.value));
const tooltipText = computed(
  () => props.config.tooltip ? getLocalizedText(props.config.tooltip, locale.value) : void 0
);
const placeholderText = computed(
  () => props.config.placeholder ? getLocalizedText(props.config.placeholder, locale.value) : t("tree-combobox-multi-placeholder")
);
function evalBool(expression, label) {
  if (!expression) return false;
  try {
    return Effect.runSync($cel(expression, { form: state.value ?? {} }));
  } catch (err) {
    console.error(`[shwfed-form] failed to evaluate ${label} for ${props.fieldId}:`, err);
    return false;
  }
}
const isDisabled = computed(() => evalBool(props.config.disabled, "disabled"));
const formReadonly = useFormReadonly();
const isReadonly = computed(
  () => props.config.readonly != null ? evalBool(props.config.readonly, "readonly") : formReadonly.value
);
const effectiveReadonly = computed(() => isReadonly.value || props.config.derived?.mode === "formula");
const { isRequired, errors, warnings, visible } = useFieldValidation({
  fieldId: () => props.fieldId,
  config: () => props.config,
  $cel,
  locale: () => locale.value
});
const uncontrolled = ref([]);
const model = computed({
  get: () => {
    const path = props.config.binding;
    const raw = path == null ? uncontrolled.value : getAt(path);
    return Array.isArray(raw) ? raw.map((v) => String(v)) : [];
  },
  set: (next) => {
    const coerced = next.map((v) => String(v));
    const path = props.config.binding;
    if (path == null) {
      uncontrolled.value = coerced;
      return;
    }
    setAt(path, coerced);
  }
});
function nodeCtx(raw) {
  return { form: state.value ?? {}, node: raw };
}
function nodeKey(raw) {
  try {
    return String(Effect.runSync($cel(props.config.nodeKey, nodeCtx(raw))));
  } catch (err) {
    console.error(`[shwfed-form] failed to evaluate nodeKey for ${props.fieldId}:`, err);
    return "";
  }
}
function nodeChildrenList(raw) {
  let v;
  try {
    v = Effect.runSync($cel(props.config.nodeChildren, nodeCtx(raw)));
  } catch (err) {
    console.error(`[shwfed-form] failed to evaluate nodeChildren for ${props.fieldId}:`, err);
    return void 0;
  }
  if (v == null) return void 0;
  if (!Array.isArray(v)) return void 0;
  return v;
}
function safeInterpolate(tpl, raw) {
  try {
    return interpolateMarkdown(tpl, $cel, nodeCtx(raw));
  } catch (err) {
    console.error(`[shwfed-form] failed to interpolate node text for ${props.fieldId}:`, err);
    return tpl;
  }
}
function wrappedLabelText(w) {
  const tpl = getLocalizedText(props.config.nodeLabel, locale.value) ?? "";
  return tpl ? safeInterpolate(tpl, w.raw) : "";
}
function wrappedTooltipText(w) {
  if (!props.config.nodeTooltip) return void 0;
  const tpl = getLocalizedText(props.config.nodeTooltip, locale.value);
  return tpl ? safeInterpolate(tpl, w.raw) : void 0;
}
function wrappedSelectable(w, _depth) {
  if (isDisabled.value) return false;
  const expr = props.config.nodeSelectable;
  if (!expr) return true;
  try {
    return Effect.runSync($cel(expr, nodeCtx(w.raw)));
  } catch {
    return false;
  }
}
function evalKeywords(raw) {
  const expr = props.config.keywords;
  if (!expr) return [];
  try {
    const v = Effect.runSync($cel(expr, nodeCtx(raw)));
    if (!Array.isArray(v)) return [];
    return v.map((k) => String(k));
  } catch {
    return [];
  }
}
const rawRoots = shallowRef([]);
const loading = ref(false);
const fetchError = ref(null);
async function fetchTree() {
  const dataSource = props.config.dataSource;
  if (!dataSource) {
    rawRoots.value = [];
    return;
  }
  const effect = Effect.gen(function* () {
    let jsonOpt = Option.none();
    if (dataSource.request) {
      jsonOpt = yield* fetchJsonOption(yield* $cel(dataSource.request, {
        form: state.value ?? {}
      }));
    }
    const dataRaw = yield* $cel(dataSource.data, {
      form: state.value ?? {},
      json: jsonOpt
    });
    return Array.isArray(dataRaw) ? dataRaw : [];
  });
  const program = Effect.gen(function* () {
    const showMaskFiber = yield* Effect.fork(
      Effect.sync(() => {
        loading.value = true;
      }).pipe(Effect.delay("30 millis"))
    );
    return yield* Effect.ensuring(effect, Fiber.interrupt(showMaskFiber));
  }).pipe(
    Effect.ensuring(Effect.sync(() => {
      loading.value = false;
    }))
  );
  try {
    const result = await Effect.runPromise(Effect.provide(program, Fetch.layer));
    rawRoots.value = result;
    fetchError.value = null;
  } catch (e) {
    console.warn("[shwfed-form] tree-combobox dataSource fetch failed", e);
    fetchError.value = e;
  }
}
watch(
  () => [props.config.dataSource?.request, props.config.dataSource?.data],
  () => {
    void fetchTree();
  },
  { immediate: true }
);
const roots = computed(
  () => wrapStructuralAll(rawRoots.value, {
    nodeKey,
    nodeChildren: nodeChildrenList
  })
);
function labelForKey(k) {
  const path = findWrappedPath(roots.value, k);
  if (!path || path.length === 0) return null;
  const leaf = path[path.length - 1];
  return wrappedLabelText(leaf);
}
const triggerLabel = computed(() => {
  const arr = model.value;
  if (arr.length === 0) return "";
  const head = labelForKey(arr[0]);
  if (head === null || head.length === 0) return "";
  if (arr.length === 1) return head;
  return `${head} + ${arr.length - 1}`;
});
const selectedLeaves = computed(
  () => model.value.map((k) => {
    const path = findWrappedPath(roots.value, k);
    if (!path || path.length === 0) return void 0;
    return path[path.length - 1];
  }).filter((node) => node !== void 0)
);
publishSelection(
  props.fieldId,
  () => selectedLeaves.value.length > 0 ? selectedLeaves.value : void 0,
  {
    identity: (list) => list.map((w) => w.key).join(" "),
    raw: (list) => list.map((w) => w.raw)
  }
);
const initialExpanded = computed(() => {
  if (!props.config.expandAll) return [];
  const out = [];
  const stack = [...roots.value];
  while (stack.length) {
    const n = stack.pop();
    if (n.kind === "branch") {
      out.push(n.key);
      stack.push(...n.children);
    }
  }
  return out;
});
const filterQuery = ref("");
const searchCorpus = shallowRef(/* @__PURE__ */ new Map());
watch(
  () => [
    roots.value,
    locale.value,
    props.config.nodeLabel,
    props.config.keywords
  ],
  () => {
    const map = /* @__PURE__ */ new Map();
    const stack = [...roots.value];
    while (stack.length) {
      const n = stack.pop();
      const label = wrappedLabelText(n).trim().toLowerCase();
      const kw = evalKeywords(n.raw).map((k) => k.trim().toLowerCase()).join("\n");
      map.set(n.key, kw ? `${label}
${kw}` : label);
      if (n.kind === "branch") stack.push(...n.children);
    }
    searchCorpus.value = map;
  },
  { immediate: true }
);
function filterPredicate(w, query) {
  const q = query.trim().toLowerCase();
  if (!q) return true;
  const hay = searchCorpus.value.get(w.key);
  if (hay !== void 0) return hay.includes(q);
  return wrappedLabelText(w).trim().toLowerCase().includes(q);
}
const open = ref(false);
function handleUpdate(next) {
  const arr = Array.isArray(next) ? next : [];
  model.value = arr;
}
const showClear = computed(
  () => !isDisabled.value && !loading.value && model.value.length > 0
);
function handleClear() {
  model.value = [];
  open.value = false;
}
const hoveredKey = ref(null);
watch(open, (isOpen) => {
  if (!isOpen) hoveredKey.value = null;
});
function collectTooltipNodes(out, siblings) {
  for (const n of siblings) {
    if (wrappedTooltipText(n)) out.push(n.key);
    if (n.kind === "branch") collectTooltipNodes(out, n.children);
  }
}
const anyHasTooltip = computed(() => {
  if (!props.config.nodeTooltip) return false;
  const out = [];
  collectTooltipNodes(out, roots.value);
  return out.length > 0;
});
const hoveredTooltip = computed(() => {
  const k = hoveredKey.value;
  if (!k) return void 0;
  const path = findWrappedPath(roots.value, k);
  if (!path || path.length === 0) return void 0;
  return wrappedTooltipText(path[path.length - 1]);
});
</script>

<template>
  <Field
    :orientation="config.orientation ?? DEFAULT_FIELD_ORIENTATION"
    :data-disabled="isDisabled || void 0"
  >
    <FieldLabel
      v-if="labelText"
      :for="fieldId"
      class="text-xs text-zinc-500"
    >
      <template
        v-if="tooltipText"
        #tooltip
      >
        <Markdown
          :source="tooltipText"
          class="prose prose-xs prose-zinc"
        />
      </template>
      {{ labelText }}<span
        v-if="isRequired"
        aria-hidden="true"
        class="text-red-600"
      > *</span>
    </FieldLabel>

    <div
      v-if="effectiveReadonly"
      class="min-h-9 py-1.5 text-sm text-zinc-700"
    >
      {{ triggerLabel || t("tree-combobox-multi-readonly-empty") }}
    </div>

    <Popover
      v-else
      v-model:open="open"
    >
      <PopoverAnchor as-child>
        <InputGroup
          class="group/tree-combobox-multi"
          :data-disabled="isDisabled ? 'true' : void 0"
        >
          <PopoverTrigger as-child>
            <InputGroupInput
              :id="fieldId"
              :model-value="triggerLabel"
              :disabled="isDisabled"
              :placeholder="placeholderText"
              class="cursor-pointer text-left"
              readonly
            />
          </PopoverTrigger>
          <InputGroupAddon
            v-if="loading"
            align="inline-end"
            data-slot="tree-combobox-multi-loading"
            class="text-zinc-400"
          >
            <Icon
              icon="fluent:spinner-ios-20-regular"
              class="animate-spin"
            />
          </InputGroupAddon>
          <InputGroupAddon
            v-else-if="showClear"
            align="inline-end"
            class="[@media(hover:hover)]:opacity-0 transition-opacity group-hover/tree-combobox-multi:opacity-100 focus-within:opacity-100"
          >
            <InputGroupButton
              size="icon-xs"
              data-slot="tree-combobox-multi-clear"
              class="text-zinc-500 hover:text-zinc-700"
              tabindex="-1"
              @mousedown.prevent
              @click.stop="handleClear"
            >
              <Icon icon="fluent:dismiss-20-regular" />
            </InputGroupButton>
          </InputGroupAddon>
        </InputGroup>
      </PopoverAnchor>
      <PopoverContent
        class="p-0"
        :style="{ width: 'var(--reka-popover-trigger-width)' }"
      >
        <div
          data-slot="tree-combobox-multi-search"
          class="flex h-9 items-center gap-2 border-b border-zinc-200 px-3"
        >
          <Icon
            icon="fluent:search-20-filled"
            class="size-3.5 shrink-0 opacity-50"
          />
          <input
            v-model="filterQuery"
            :disabled="isDisabled"
            :placeholder="t('tree-combobox-multi-search-placeholder')"
            class="flex h-9 w-full bg-transparent text-sm outline-hidden placeholder:text-zinc-500 disabled:cursor-not-allowed disabled:opacity-50"
          >
        </div>
        <div class="flex flex-col p-1">
          <div
            v-if="fetchError && roots.length === 0"
            class="min-h-9 px-2 py-1.5 text-sm text-red-500"
          >
            {{ t("tree-combobox-multi-load-error") }}
          </div>
          <div
            v-else-if="loading && roots.length === 0"
            class="flex flex-col gap-1.5 px-2 py-1"
            aria-busy="true"
          >
            <Skeleton class="h-5 w-2/3" />
            <Skeleton class="h-5 w-1/2" />
            <Skeleton class="h-5 w-3/5" />
          </div>

          <UiTree
            v-else
            :roots="roots"
            :get-key="(w) => w.key"
            :get-children="getWrappedChildren"
            selection-mode="multi"
            :cascade="config.cascade ?? 'independent'"
            :model-value="model"
            :selectable="wrappedSelectable"
            :filter-predicate="filterPredicate"
            :filter-query="filterQuery"
            :initial-expanded="initialExpanded"
            :show-focused-row="false"
            :class="isDisabled ? 'pointer-events-none opacity-60' : void 0"
            class="max-h-72 overflow-y-auto"
            @update:model-value="handleUpdate"
          >
            <template #node="{ node, selected }">
              <span
                class="flex flex-1 min-w-0 items-center justify-between gap-2"
                @mouseenter="hoveredKey = node.key"
                @focusin="hoveredKey = node.key"
              >
                <Markdown
                  :source="wrappedLabelText(node)"
                  class="prose prose-sm prose-zinc text-zinc-700 min-w-0"
                />
                <Icon
                  v-if="selected"
                  icon="fluent:checkmark-20-regular"
                  class="size-3 shrink-0 text-zinc-700"
                />
              </span>
            </template>

            <template #empty>
              <div class="min-h-9 px-2 py-1.5 text-sm text-zinc-400">
                {{ t("tree-combobox-multi-empty") }}
              </div>
            </template>

            <template #error="{ error, retry }">
              <span class="inline-flex items-center gap-1 text-xs text-red-500">
                {{ t("tree-combobox-multi-load-error") }}
                <button
                  type="button"
                  class="underline hover:text-red-600"
                  @click.stop="retry"
                >
                  {{ t("tree-combobox-multi-retry") }}
                </button>
                <span class="sr-only">{{ String(error) }}</span>
              </span>
            </template>
          </UiTree>
        </div>
        <div
          v-if="anyHasTooltip && hoveredTooltip"
          data-slot="tree-combobox-multi-tooltip"
          class="border-t border-zinc-200 px-3 py-2"
        >
          <Markdown
            :source="hoveredTooltip"
            block
            class="prose prose-sm prose-zinc"
          />
        </div>
      </PopoverContent>
    </Popover>
    <FieldMessages
      v-if="visible"
      :errors="errors"
      :warnings="warnings"
    />
  </Field>
</template>
