<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 { Checkbox } from "../../../../ui/checkbox";
import { Field, FieldLabel, FieldMessages } from "../../../../ui/field";
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "../../../../ui/input-group";
import { Markdown } from "../../../../ui/markdown";
import { Skeleton } from "../../../../ui/skeleton";
import { Tree as UiTree } from "../../../../ui/tree";
import { interpolateMarkdown } from "../../../../table/utils/runtime";
import { useFormReadonly } from "../../../utils/readonly";
import { useFormState } from "../../../utils/state";
import { useFieldValidation } from "../../../utils/validation";
defineOptions({ name: "ShwfedTreeMultiFieldRuntime" });
const props = defineProps({
  fieldId: { type: String, required: true },
  config: { type: null, required: true }
});
const { locale, t } = useI18n({
  inheritLocale: true,
  messages: {
    zh: {
      "tree-multi-search-label": "\u641C\u7D22",
      "tree-multi-search-clear": "\u6E05\u9664\u641C\u7D22",
      "tree-multi-load-error": "\u52A0\u8F7D\u5931\u8D25",
      "tree-multi-empty": "\u65E0\u53EF\u9009\u9879",
      "tree-multi-readonly-empty": "\u2014",
      "tree-multi-readonly-separator": "\u3001"
    },
    en: {
      "tree-multi-search-label": "Search",
      "tree-multi-search-clear": "Clear search",
      "tree-multi-load-error": "Failed to load",
      "tree-multi-empty": "No options",
      "tree-multi-readonly-empty": "\u2014",
      "tree-multi-readonly-separator": ", "
    },
    ja: {
      "tree-multi-search-label": "\u691C\u7D22",
      "tree-multi-search-clear": "\u691C\u7D22\u3092\u30AF\u30EA\u30A2",
      "tree-multi-load-error": "\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
      "tree-multi-empty": "\u9078\u629E\u80A2\u306A\u3057",
      "tree-multi-readonly-empty": "\u2014",
      "tree-multi-readonly-separator": "\u3001"
    }
  }
});
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
);
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);
  }
});
const roots = ref([]);
const loading = ref(false);
const fetchError = ref(null);
async function fetchTree() {
  const dataSource = props.config.dataSource;
  if (!dataSource) {
    roots.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));
    roots.value = result;
    fetchError.value = null;
  } catch (e) {
    console.warn("[shwfed-form] tree dataSource fetch failed", e);
    fetchError.value = e;
  }
}
watch(
  () => [props.config.dataSource?.request, props.config.dataSource?.data],
  () => {
    void fetchTree();
  },
  { immediate: true }
);
function nodeCtx(node) {
  return { form: state.value ?? {}, node };
}
function getKey(node) {
  try {
    const v = Effect.runSync($cel(props.config.nodeKey, nodeCtx(node)));
    return String(v);
  } catch (err) {
    console.error(`[shwfed-form] failed to evaluate nodeKey for ${props.fieldId}:`, err);
    return "";
  }
}
function getChildren(node) {
  let raw;
  try {
    raw = Effect.runSync($cel(props.config.nodeChildren, nodeCtx(node)));
  } catch (err) {
    console.error(`[shwfed-form] failed to evaluate nodeChildren for ${props.fieldId}:`, err);
    return void 0;
  }
  if (raw == null) return void 0;
  if (!Array.isArray(raw)) return void 0;
  if (raw.length === 0) return void 0;
  return raw;
}
function nodeSelectable(node, _depth) {
  if (isDisabled.value) return false;
  const expr = props.config.nodeSelectable;
  if (!expr) return true;
  try {
    return Effect.runSync($cel(expr, nodeCtx(node)));
  } catch (err) {
    console.error(`[shwfed-form] failed to evaluate nodeSelectable for ${props.fieldId}:`, err);
    return false;
  }
}
function safeInterpolate(tpl, node) {
  try {
    return interpolateMarkdown(tpl, $cel, { form: state.value ?? {}, node });
  } catch (err) {
    console.error(`[shwfed-form] failed to interpolate node text for ${props.fieldId}:`, err);
    return tpl;
  }
}
function nodeLabelText(node) {
  const tpl = getLocalizedText(props.config.nodeLabel, locale.value) ?? "";
  return tpl ? safeInterpolate(tpl, node) : "";
}
function nodeTooltipText(node) {
  if (!props.config.nodeTooltip) return void 0;
  const tpl = getLocalizedText(props.config.nodeTooltip, locale.value);
  if (!tpl) return void 0;
  return safeInterpolate(tpl, node);
}
const initialExpanded = computed(() => {
  if (!props.config.expandAll) return [];
  const out = [];
  const stack = [...roots.value];
  while (stack.length) {
    const n = stack.pop();
    const kids = getChildren(n);
    if (kids && kids.length) {
      out.push(getKey(n));
      stack.push(...kids);
    }
  }
  return out;
});
const filterQuery = ref("");
function evalKeywords(node) {
  const expr = props.config.filter?.keywords;
  if (!expr) return [];
  try {
    const raw = Effect.runSync($cel(expr, nodeCtx(node)));
    if (!Array.isArray(raw)) return [];
    return raw.map((v) => String(v));
  } catch {
    return [];
  }
}
const searchCorpus = shallowRef(/* @__PURE__ */ new Map());
watch(
  () => [
    roots.value,
    locale.value,
    props.config.nodeLabel,
    props.config.filter?.keywords
  ],
  () => {
    const map = /* @__PURE__ */ new Map();
    const stack = [...roots.value];
    while (stack.length) {
      const n = stack.pop();
      const label = nodeLabelText(n).trim().toLowerCase();
      const kw = evalKeywords(n).map((k) => k.trim().toLowerCase()).join("\n");
      map.set(getKey(n), kw ? `${label}
${kw}` : label);
      const kids = getChildren(n);
      if (kids && kids.length) stack.push(...kids);
    }
    searchCorpus.value = map;
  },
  { immediate: true }
);
function filterPredicate(node, query) {
  const q = query.trim().toLowerCase();
  if (!q) return true;
  const hay = searchCorpus.value.get(getKey(node));
  if (hay !== void 0) return hay.includes(q);
  return nodeLabelText(node).trim().toLowerCase().includes(q);
}
const filterLabelText = computed(() => {
  const localized = props.config.filter?.label ? getLocalizedText(props.config.filter.label, locale.value) : void 0;
  return localized && localized.length > 0 ? localized : t("tree-multi-search-label");
});
const resolvedByKey = computed(() => {
  const selected = new Set(model.value);
  const found = /* @__PURE__ */ new Map();
  if (selected.size === 0) return found;
  const stack = [...roots.value];
  while (stack.length) {
    const n = stack.pop();
    const k = getKey(n);
    if (selected.has(k)) found.set(k, n);
    const kids = getChildren(n);
    if (kids && kids.length) stack.push(...kids);
  }
  return found;
});
const selectedNodes = computed(
  () => model.value.map((id) => resolvedByKey.value.get(id)).filter((n) => n !== void 0)
);
publishSelection(
  props.fieldId,
  () => selectedNodes.value.length > 0 ? selectedNodes.value : void 0,
  {
    identity: (list) => list.map((n) => getKey(n)).join(" "),
    raw: (list) => list
  }
);
const readonlyText = computed(() => {
  const empty = t("tree-multi-readonly-empty");
  if (model.value.length === 0) return empty;
  const found = resolvedByKey.value;
  const labels = model.value.map((id) => {
    const n = found.get(id);
    return n !== void 0 ? nodeLabelText(n) : id;
  }).filter((l) => l.length > 0);
  return labels.length > 0 ? labels.join(t("tree-multi-readonly-separator")) : empty;
});
</script>

<template>
  <Field
    orientation="vertical"
    :data-disabled="isDisabled || void 0"
  >
    <FieldLabel
      v-if="labelText"
      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"
    >
      {{ readonlyText }}
    </div>

    <template v-else>
      <Field
        v-if="config.filter?.enabled"
        orientation="floating"
      >
        <FieldLabel>{{ filterLabelText }}</FieldLabel>
        <InputGroup>
          <InputGroupAddon align="inline-start">
            <Icon icon="lucide:search" />
          </InputGroupAddon>
          <InputGroupInput
            v-model="filterQuery"
            :disabled="isDisabled"
          />
          <InputGroupAddon
            v-if="filterQuery.length > 0"
            align="inline-end"
          >
            <InputGroupButton
              size="icon-xs"
              :aria-label="t('tree-multi-search-clear')"
              @click="filterQuery = ''"
            >
              <Icon icon="lucide:x" />
            </InputGroupButton>
          </InputGroupAddon>
        </InputGroup>
      </Field>

      <div
        v-if="fetchError && roots.length === 0"
        class="min-h-9 py-1.5 text-sm text-red-500"
      >
        {{ t("tree-multi-load-error") }}
      </div>
      <div
        v-else-if="loading && roots.length === 0"
        class="flex flex-col gap-1.5 py-1"
        aria-busy="true"
      >
        <Skeleton class="h-6 w-2/3" />
        <Skeleton class="h-6 w-1/2" />
        <Skeleton class="h-6 w-3/5" />
        <Skeleton class="h-6 w-2/5" />
      </div>

      <UiTree
        v-else
        v-model="model"
        :roots="roots"
        :get-key="getKey"
        :get-children="getChildren"
        selection-mode="multi"
        :cascade="config.cascade ?? 'cascade-both'"
        :selectable="nodeSelectable"
        :filter-predicate="filterPredicate"
        :filter-query="filterQuery"
        :initial-expanded="initialExpanded"
        :show-focused-row="false"
        :class="isDisabled ? 'pointer-events-none opacity-60' : void 0"
      >
        <template #node="{ node, selected, indeterminate }">
          <span class="flex items-start gap-2">
            <!-- Presentational checkbox: mirrors the row's selection state but
                 never owns it. `pointer-events-none` + `tabindex=-1` keep the
                 existing row-click / keyboard toggle in `<UiTree>` the single
                 source of truth — the control is a visual reflection only. -->
            <Checkbox
              :model-value="indeterminate ? 'indeterminate' : selected"
              class="pointer-events-none mt-0.5 shrink-0"
              tabindex="-1"
              aria-hidden="true"
            />
            <span class="flex flex-col gap-0.5">
              <Markdown
                :source="nodeLabelText(node)"
                class="prose prose-sm prose-zinc text-zinc-700"
              />
              <Markdown
                v-if="nodeTooltipText(node)"
                :source="nodeTooltipText(node)"
                block
                class="prose prose-xs prose-zinc text-zinc-400"
              />
            </span>
          </span>
        </template>

        <template #empty>
          <div class="min-h-9 py-1.5 text-sm text-zinc-400">
            {{ t("tree-multi-empty") }}
          </div>
        </template>
      </UiTree>
    </template>
    <FieldMessages
      v-if="visible"
      :errors="errors"
      :warnings="warnings"
    />
  </Field>
</template>
