<script setup>
import { Icon } from "@iconify/vue";
import { Effect, Option } from "effect";
import { Fetch } from "fx-fetch";
import { watchDebounced } from "@vueuse/core";
import { toast } from "vue-sonner";
import { computed, nextTick, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { asRequest, 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 { dispatchTriggers, useEventChannel } from "../../../../../share/event-bus";
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList
} from "../../../../ui/command";
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "../../../../ui/input-group";
import { Markdown } from "../../../../ui/markdown";
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../../../../ui/popover";
import { useFormState } from "../../../../form/utils/state";
import { interpolateMarkdown } from "../../../utils/runtime";
import { useSharedFetchLayer } from "../../../utils/shared-fetch";
defineOptions({ name: "ShwfedTableComboboxSingleRuntime" });
const props = defineProps({
  column: { type: null, required: true },
  ctx: { type: Object, required: true }
});
const { locale, t } = useI18n({
  inheritLocale: true,
  messages: {
    zh: {
      "combobox-single-placeholder": "\u8BF7\u9009\u62E9",
      "combobox-single-search-placeholder": "\u641C\u7D22\u2026",
      "combobox-single-empty": "\u65E0\u5339\u914D\u9879",
      "combobox-single-loading": "\u52A0\u8F7D\u4E2D\u2026"
    },
    en: {
      "combobox-single-placeholder": "Select\u2026",
      "combobox-single-search-placeholder": "Search\u2026",
      "combobox-single-empty": "No matches",
      "combobox-single-loading": "Loading\u2026"
    },
    ja: {
      "combobox-single-placeholder": "\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044",
      "combobox-single-search-placeholder": "\u691C\u7D22\u2026",
      "combobox-single-empty": "\u4E00\u81F4\u306A\u3057",
      "combobox-single-loading": "\u8AAD\u307F\u8FBC\u307F\u4E2D\u2026"
    }
  }
});
const formState = useFormState();
const inherited = injectCELContext();
const $cel = (expression, context) => _rawCel(expression, { ...celBindings(inherited), ...context });
const eventChannel = useEventChannel();
const sharedFetchLayer = useSharedFetchLayer();
const row = computed(() => props.ctx.row.original);
const rowIndex = computed(() => props.ctx.row.index);
const placeholderText = computed(
  () => props.column.placeholder ? getLocalizedText(props.column.placeholder, locale.value) : t("combobox-single-placeholder")
);
function evalBool(expression, label) {
  if (!expression) return false;
  try {
    return Effect.runSync($cel(expression, { row: row.value, index: rowIndex.value })) === true;
  } catch (e) {
    console.error(`[shwfed-table] combobox-single ${label} failed`, e);
    return false;
  }
}
const isHidden = computed(() => evalBool(props.column.hidden, "hidden"));
const isDisabledByConfig = computed(() => evalBool(props.column.disabled, "disabled"));
const isReadonly = computed(() => evalBool(props.column.readonly, "readonly"));
const isInteractive = computed(
  () => props.column.write.kind === "binding" || !!props.column.write.onChange
);
const pending = ref(false);
function safeInterpolate(tpl, context) {
  try {
    return interpolateMarkdown(tpl, $cel, context);
  } catch (e) {
    console.error("[shwfed-table] combobox-single label interpolation failed", e);
    return tpl;
  }
}
const staticEntries = computed(() => {
  if (props.column.options.kind !== "static") return [];
  return props.column.options.items.map((item) => {
    const labelTpl = getLocalizedText(item.label, locale.value) ?? "";
    const tooltipTpl = item.tooltip ? getLocalizedText(item.tooltip, locale.value) : void 0;
    return {
      key: item.id,
      value: item.value.value,
      label: labelTpl ? safeInterpolate(labelTpl, {}) : "",
      tooltip: tooltipTpl ? safeInterpolate(tooltipTpl, {}) : void 0,
      keywords: (item.keywords ?? []).join(" "),
      raw: item
    };
  });
});
const remoteJson = ref(null);
const remoteLoading = ref(false);
let fetchSeq = 0;
const requestSignature = computed(() => {
  if (props.column.options.kind !== "remote") return null;
  const expr = props.column.options.request;
  if (!expr) return null;
  try {
    const req = asRequest(Effect.runSync(
      $cel(expr, { row: row.value, index: rowIndex.value })
    ));
    return Option.isSome(req) ? JSON.stringify(req.value.describe()) : null;
  } catch {
    return null;
  }
});
async function fetchOptions() {
  const seq = ++fetchSeq;
  if (props.column.options.kind !== "remote" || requestSignature.value == null) {
    remoteJson.value = null;
    remoteLoading.value = false;
    return;
  }
  const expr = props.column.options.request;
  remoteLoading.value = true;
  const program = Effect.gen(function* () {
    const json = yield* fetchJsonOption(yield* $cel(expr, { row: row.value, index: rowIndex.value }));
    return Option.getOrNull(json);
  });
  try {
    const result = await Effect.runPromise(Effect.provide(program, sharedFetchLayer));
    if (seq === fetchSeq) remoteJson.value = result;
  } catch {
    if (seq === fetchSeq) remoteJson.value = null;
  } finally {
    if (seq === fetchSeq) remoteLoading.value = false;
  }
}
watchDebounced(
  requestSignature,
  () => {
    void fetchOptions();
  },
  { debounce: 200, immediate: true }
);
function valueOf(option) {
  if (props.column.options.kind !== "remote") return void 0;
  try {
    return Effect.runSync($cel(props.column.options.optionValue, {
      row: row.value,
      index: rowIndex.value,
      option
    }));
  } catch (e) {
    console.error("[shwfed-table] combobox-single optionValue failed", e);
    return void 0;
  }
}
function labelOf(option) {
  if (props.column.options.kind !== "remote") return "";
  const tpl = getLocalizedText(props.column.options.optionLabel, locale.value) ?? "";
  if (!tpl) return "";
  try {
    return interpolateMarkdown(tpl, $cel, { row: row.value, index: rowIndex.value, option });
  } catch (e) {
    console.error("[shwfed-table] combobox-single optionLabel failed", e);
    return "";
  }
}
function keywordsOf(option) {
  if (props.column.options.kind !== "remote") return "";
  const expr = props.column.options.optionKeywords;
  if (!expr) return "";
  try {
    const raw = Effect.runSync($cel(expr, {
      row: row.value,
      index: rowIndex.value,
      option
    }));
    if (!Array.isArray(raw)) return "";
    return raw.map((k) => String(k ?? "")).filter((k) => k.length > 0).join(" ");
  } catch (e) {
    console.error("[shwfed-table] combobox-single optionKeywords failed", e);
    return "";
  }
}
const remoteEntries = computed(() => {
  if (props.column.options.kind !== "remote") return [];
  let raw;
  try {
    raw = Effect.runSync($cel(props.column.options.options, {
      row: row.value,
      index: rowIndex.value,
      json: remoteJson.value
    }));
  } catch (e) {
    console.error("[shwfed-table] combobox-single options failed", e);
    return [];
  }
  if (!Array.isArray(raw)) return [];
  return raw.map((option, idx) => ({
    key: String(idx),
    label: labelOf(option),
    value: valueOf(option),
    keywords: keywordsOf(option),
    raw: option
  }));
});
const entries = computed(
  () => props.column.options.kind === "static" ? staticEntries.value : remoteEntries.value
);
const isLoading = computed(() => props.column.options.kind === "remote" && remoteLoading.value);
const cellValue = computed(
  () => props.column.write.kind === "binding" ? formState.getAt(props.column.write.binding) : props.ctx.cell.getValue()
);
function isEqual(a, b) {
  if (a === b) return true;
  if (a == null || b == null) return false;
  if (typeof a === "object" && typeof b === "object") {
    try {
      return JSON.stringify(a) === JSON.stringify(b);
    } catch {
      return false;
    }
  }
  return false;
}
const selectedEntry = computed(
  () => entries.value.find((entry) => isEqual(entry.value, cellValue.value))
);
const selectedKey = computed(() => selectedEntry.value?.key);
const selectedLabel = computed(() => selectedEntry.value?.label ?? "");
publishSelection(
  props.column.id,
  () => selectedEntry.value,
  { identity: (entry) => entry.key, raw: (entry) => entry.raw }
);
const open = ref(false);
const isDisabled = computed(
  () => !isInteractive.value || isDisabledByConfig.value || pending.value
);
async function submit(next) {
  const write = props.column.write;
  if (write.kind === "binding") {
    formState.setAt(write.binding, next);
    return;
  }
  const { onChange, successMessage, triggers } = write;
  if (!onChange) return;
  pending.value = true;
  const program = Effect.gen(function* () {
    const builder = yield* $cel(onChange, {
      row: row.value,
      index: rowIndex.value,
      value: next
    });
    return yield* builder.json();
  });
  try {
    const body = await Effect.runPromise(Effect.provide(program, Fetch.layer));
    if (successMessage) {
      try {
        const message = Effect.runSync($cel(successMessage, { json: body }));
        if (message) toast.success(message);
      } catch (e) {
        console.error("[shwfed-table] combobox-single successMessage failed", e);
      }
    }
    await Effect.runPromise(dispatchTriggers(eventChannel, triggers));
  } catch (e) {
    console.error("[shwfed-table] combobox-single onChange failed", e);
    toast.error("\u8BF7\u6C42\u5931\u8D25");
  } finally {
    pending.value = false;
  }
}
function handleSelect(key) {
  if (typeof key !== "string") return;
  const entry = entries.value.find((e) => e.key === key);
  if (!entry) return;
  void nextTick().then(() => {
    open.value = false;
  });
  void submit(entry.value);
}
const showClear = computed(
  () => !isDisabled.value && selectedEntry.value !== void 0
);
function handleClear() {
  open.value = false;
  void submit(null);
}
const hoveredKey = ref(null);
watch(open, (isOpen) => {
  hoveredKey.value = isOpen ? entries.value[0]?.key ?? null : null;
});
const hoveredEntry = computed(
  () => entries.value.find((e) => e.key === hoveredKey.value)
);
const hoveredTooltip = computed(() => hoveredEntry.value?.tooltip);
const anyHasTooltip = computed(
  () => entries.value.some((e) => e.tooltip != null && e.tooltip.length > 0)
);
</script>

<template>
  <div class="p-[0.125rem] w-full">
    <span
      v-if="isHidden"
      class="block h-7 w-full"
    />
    <span
      v-else-if="isReadonly || !isInteractive"
      class="flex items-center h-7 w-full px-2 text-[0.75rem] text-zinc-700 truncate"
    >
      {{ selectedLabel || "\u2014" }}
    </span>
    <Popover
      v-else
      v-model:open="open"
    >
      <PopoverAnchor as-child>
        <InputGroup
          class="group/combobox-single h-7 rounded border-zinc-200/30 hover:border-zinc-200 focus-within:border-zinc-200 transition-colors ease-out duration-180"
          :data-disabled="isDisabled ? 'true' : void 0"
        >
          <PopoverTrigger as-child>
            <InputGroupInput
              :model-value="selectedLabel"
              :disabled="isDisabled"
              :placeholder="placeholderText"
              class="h-7 text-[0.75rem] md:text-[0.75rem] px-2 cursor-pointer text-left"
              readonly
            />
          </PopoverTrigger>
          <InputGroupAddon
            v-if="pending || isLoading"
            align="inline-end"
            class="text-zinc-400"
          >
            <Icon
              icon="fluent:spinner-ios-20-regular"
              class="size-3 animate-spin"
            />
          </InputGroupAddon>
          <InputGroupAddon
            v-else-if="showClear"
            align="inline-end"
            class="[@media(hover:hover)]:opacity-0 transition-opacity group-hover/combobox-single:opacity-100 focus-within:opacity-100"
          >
            <InputGroupButton
              size="icon-xs"
              data-slot="combobox-single-clear"
              class="size-4 text-zinc-500 hover:text-zinc-700"
              tabindex="-1"
              @mousedown.prevent
              @click.stop="handleClear"
            >
              <Icon
                icon="fluent:dismiss-20-regular"
                class="size-3"
              />
            </InputGroupButton>
          </InputGroupAddon>
        </InputGroup>
      </PopoverAnchor>
      <PopoverContent
        class="w-auto p-0 [&_[data-slot=command-input-wrapper]]:h-7 [&_[data-slot=command-input-wrapper]]:px-2 [&_[data-slot=command-input-wrapper]_svg]:size-3 [&_[data-slot=command-input]]:h-7 [&_[data-slot=command-input]]:py-0 [&_[data-slot=command-input]]:text-[0.75rem] [&_[data-slot=command-item]]:px-2 [&_[data-slot=command-item]]:py-1 [&_[data-slot=command-item]]:text-[0.75rem]"
        :style="{ width: 'var(--reka-popover-trigger-width)' }"
      >
        <Command
          :model-value="selectedKey"
          :disabled="isDisabled"
          selection-behavior="toggle"
          @update:model-value="handleSelect"
        >
          <CommandInput :placeholder="t('combobox-single-search-placeholder')" />
          <CommandList>
            <div
              v-if="entries.length === 0"
              data-slot="combobox-single-empty"
              class="py-3 text-center text-[0.75rem] text-zinc-500"
            >
              {{ isLoading ? t("combobox-single-loading") : t("combobox-single-empty") }}
            </div>
            <template v-else>
              <CommandEmpty class="py-3 text-center text-[0.75rem] text-zinc-500">
                {{ t("combobox-single-empty") }}
              </CommandEmpty>
              <CommandGroup>
                <CommandItem
                  v-for="entry in entries"
                  :key="entry.key"
                  :value="entry.key"
                  :keywords="entry.keywords || void 0"
                  @mouseenter="hoveredKey = entry.key"
                  @focus="hoveredKey = entry.key"
                >
                  <span class="flex-1"><Markdown :source="entry.label" /></span>
                </CommandItem>
              </CommandGroup>
            </template>
          </CommandList>
          <div
            v-if="anyHasTooltip && hoveredTooltip"
            class="border-t border-zinc-200 px-2 py-1.5"
          >
            <Markdown
              :source="hoveredTooltip"
              block
              class="prose prose-xs prose-zinc"
            />
          </div>
        </Command>
      </PopoverContent>
    </Popover>
  </div>
</template>
