<script setup>
import { computed, onBeforeUnmount, provide, reactive, ref, watch } from "vue";
import { Icon } from "@iconify/vue";
import { Effect } from "effect";
import { useI18n } from "vue-i18n";
import { ActionContext, useOverlay } from "../../../composables/useOverlay";
import { getLocalizedText, hasVisibleLocaleValue } from "../../../share/locale";
import { cn } from "../../../utils/cn";
import { Markdown } from "../../ui/markdown";
import { Button } from "../../ui/button";
import { ButtonGroup } from "../../ui/button-group";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip";
import { cel as _rawCel } from "../../../utils/cel";
import { celBindings, injectCELContext } from "../../../utils/cel-context";
import { buttonRegistryKey } from "../utils/button-registry";
import { findButton, findItem } from "../utils/resolve";
import { Fetch } from "fx-fetch";
import { toast } from "vue-sonner";
defineOptions({
  name: "ShwfedActions",
  inheritAttrs: false
});
defineProps({
  configure: { type: Function, required: false }
});
const config = defineModel("config", { type: null, ...{ required: true } });
const isDevelopment = import.meta.dev;
const duplicateActionWarnings = /* @__PURE__ */ new Set();
const unknownActionWarnings = /* @__PURE__ */ new Set();
const missingRuntimeWarnings = /* @__PURE__ */ new Set();
const { locale, t } = useI18n({
  messages: {
    zh: {
      "untitled-button": "\u672A\u547D\u540D\u6309\u94AE",
      "untitled-dropdown": "\u672A\u547D\u540D\u4E0B\u62C9\u6309\u94AE",
      "action-network-error-title": "\u7F51\u7EDC\u9519\u8BEF",
      "action-network-error-description": "\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u540E\u91CD\u8BD5\u3002",
      "action-server-error-title": "\u8BF7\u6C42\u5931\u8D25",
      "action-server-error-description": "\u670D\u52A1\u5668\u8FD4\u56DE {status}\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
      "action-bad-response-title": "\u54CD\u5E94\u6709\u70B9\u95EE\u9898",
      "action-bad-response-description": "\u6CA1\u80FD\u8BFB\u61C2\u670D\u52A1\u5668\u8FD4\u56DE\u7684\u5185\u5BB9\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002"
    },
    en: {
      "untitled-button": "Untitled button",
      "untitled-dropdown": "Untitled dropdown",
      "action-network-error-title": "Network error",
      "action-network-error-description": "Please check your connection and try again.",
      "action-server-error-title": "Request failed",
      "action-server-error-description": "The server responded with {status}. Please try again later.",
      "action-bad-response-title": "Something came back wrong",
      "action-bad-response-description": "We couldn't read the server's reply. Please try again."
    },
    ja: {
      "untitled-button": "\u672A\u547D\u540D\u30DC\u30BF\u30F3",
      "untitled-dropdown": "\u672A\u547D\u540D\u30C9\u30ED\u30C3\u30D7\u30C0\u30A6\u30F3",
      "action-network-error-title": "\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u30A8\u30E9\u30FC",
      "action-network-error-description": "\u63A5\u7D9A\u3092\u78BA\u8A8D\u3057\u3066\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
      "action-server-error-title": "\u30EA\u30AF\u30A8\u30B9\u30C8\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
      "action-server-error-description": "\u30B5\u30FC\u30D0\u30FC\u304C {status} \u3092\u8FD4\u3057\u307E\u3057\u305F\u3002\u3057\u3070\u3089\u304F\u3057\u3066\u304B\u3089\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
      "action-bad-response-title": "\u5FDC\u7B54\u304C\u3046\u307E\u304F\u8AAD\u307F\u53D6\u308C\u307E\u305B\u3093\u3067\u3057\u305F",
      "action-bad-response-description": "\u30B5\u30FC\u30D0\u30FC\u304B\u3089\u306E\u8FD4\u7B54\u3092\u51E6\u7406\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u3057\u3070\u3089\u304F\u3057\u3066\u304B\u3089\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
    },
    ko: {
      "untitled-button": "\uC774\uB984 \uC5C6\uB294 \uBC84\uD2BC",
      "untitled-dropdown": "\uC774\uB984 \uC5C6\uB294 \uB4DC\uB86D\uB2E4\uC6B4",
      "action-network-error-title": "\uB124\uD2B8\uC6CC\uD06C \uC624\uB958",
      "action-network-error-description": "\uC5F0\uACB0 \uC0C1\uD0DC\uB97C \uD655\uC778\uD55C \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.",
      "action-server-error-title": "\uC694\uCCAD \uC2E4\uD328",
      "action-server-error-description": "\uC11C\uBC84\uAC00 {status}\uC744(\uB97C) \uBC18\uD658\uD588\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694.",
      "action-bad-response-title": "\uC751\uB2F5\uC774 \uC870\uAE08 \uC774\uC0C1\uD574\uC694",
      "action-bad-response-description": "\uC11C\uBC84\uC758 \uC751\uB2F5\uC744 \uC77D\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694."
    }
  }
});
const _inheritedCelContext = injectCELContext();
const $cel = (expression, context) => _rawCel(expression, { ...celBindings(_inheritedCelContext), ...context });
const overlay = useOverlay();
const pendingIds = ref([]);
const overlayOwnerId = crypto.randomUUID();
const declaredActions = reactive({});
const actionRuntimes = computed(() => {
  const list = [];
  for (const item of config.value.items) {
    if (isRegistryItem(item)) continue;
    if (isDropdownItem(item)) {
      for (const child of item.items) {
        if (isRegistrySubItem(child)) continue;
        if (!child.action) continue;
        const entry2 = findButton(child.action.type, child.action.compatibilityDate);
        if (!entry2) {
          warnMissingRuntime(child.action.type, child.action.compatibilityDate);
          continue;
        }
        list.push({
          buttonId: child.id,
          component: entry2.runtime,
          config: child.action,
          buttonTitle: child.title
        });
      }
      continue;
    }
    if (!item.action) continue;
    const entry = findButton(item.action.type, item.action.compatibilityDate);
    if (!entry) {
      warnMissingRuntime(item.action.type, item.action.compatibilityDate);
      continue;
    }
    list.push({
      buttonId: item.id,
      component: entry.runtime,
      config: item.action,
      buttonTitle: item.title
    });
  }
  return list;
});
const actionIds = computed(() => config.value.items.flatMap((item) => {
  if (isRegistryItem(item)) return [];
  if (isDropdownItem(item)) {
    return item.items.filter((child) => !isRegistrySubItem(child)).map((child) => child.id);
  }
  return [item.id];
}));
const actionIdSet = computed(() => new Set(actionIds.value));
function warnMissingRuntime(type, date) {
  const key = `${type}@${date}`;
  if (!isDevelopment || missingRuntimeWarnings.has(key)) return;
  missingRuntimeWarnings.add(key);
  console.warn(`[shwfed-actions] no runtime registered for action "${type}" (${date}).`);
}
function warnDuplicateAction(actionId) {
  if (!isDevelopment || duplicateActionWarnings.has(actionId)) {
    return;
  }
  duplicateActionWarnings.add(actionId);
  console.warn(`[shwfed-actions] duplicate action declaration for "${actionId}". The latest declaration wins.`);
}
function warnUnknownAction(actionId) {
  if (!isDevelopment || unknownActionWarnings.has(actionId)) {
    return;
  }
  unknownActionWarnings.add(actionId);
  console.warn(`[shwfed-action-definition] action "${actionId}" is not present in the current button config.`);
}
function registerAction(definition) {
  const existing = declaredActions[definition.actionId];
  if (existing && existing.ownerId !== definition.ownerId) {
    warnDuplicateAction(definition.actionId);
  }
  declaredActions[definition.actionId] = definition;
}
function unregisterAction(actionId, ownerId) {
  const existing = declaredActions[actionId];
  if (!existing) {
    return;
  }
  if (ownerId !== void 0 && existing.ownerId !== ownerId) {
    return;
  }
  Reflect.deleteProperty(declaredActions, actionId);
}
const registry = {
  registerAction,
  unregisterAction,
  hasAction: (actionId) => actionIdSet.value.has(actionId)
};
provide(buttonRegistryKey, registry);
function validateDeclaredButtons() {
  for (const actionId of Object.keys(declaredActions)) {
    if (!actionIdSet.value.has(actionId)) {
      warnUnknownAction(actionId);
    }
  }
}
watch(actionIdSet, () => {
  validateDeclaredButtons();
}, { immediate: true });
const modalDefinitions = computed(() => {
  const nextDefinitions = [];
  for (const item of config.value.items) {
    if (isRegistryItem(item)) continue;
    if (isDropdownItem(item)) {
      for (const child of item.items) {
        if (isRegistrySubItem(child)) continue;
        const declaration2 = declaredActions[child.id];
        if (!declaration2?.render) continue;
        nextDefinitions.push({
          // Per-instance unique id (declaration ownerId), not the shared
          // buttonId — otherwise every row registering the same button id
          // overwrites the prior slot in the global overlay registry, and
          // every modal renders the last-mounted row's closure.
          definitionId: declaration2.ownerId,
          ownerId: overlayOwnerId,
          render: declaration2.render
        });
      }
      continue;
    }
    const declaration = declaredActions[item.id];
    if (!declaration?.render) continue;
    nextDefinitions.push({
      definitionId: declaration.ownerId,
      ownerId: overlayOwnerId,
      render: declaration.render
    });
  }
  return nextDefinitions;
});
watch(modalDefinitions, (definitions) => {
  overlay.syncDefinitions(overlayOwnerId, definitions);
}, { immediate: true });
onBeforeUnmount(() => {
  overlay.syncDefinitions(overlayOwnerId, []);
});
const groupedItems = computed(() => {
  return config.value.groups.map((group) => ({
    ...group,
    items: config.value.items.filter((item) => item.groupId === group.id)
  }));
});
const rootStyle = computed(() => ({
  gap: `calc(${config.value.gap ?? 4} * 0.25rem)`
}));
function getButtonLabel(button) {
  return getLocalizedText(button.title, locale.value) ?? t("untitled-button");
}
function getDropdownLabel(dropdown) {
  return getLocalizedText(dropdown.title, locale.value) ?? t("untitled-dropdown");
}
function getButtonTooltip(button) {
  return getLocalizedText(button.tooltip, locale.value);
}
function hasButtonTooltip(button) {
  return hasVisibleLocaleValue(button.tooltip);
}
function getButtonEffect(buttonId) {
  return declaredActions[buttonId]?.effect;
}
function isEffectValue(value) {
  return typeof value === "object" && value !== null && "_op" in value;
}
function isEffectFactory(value) {
  return typeof value === "function";
}
function isPending(buttonId) {
  return pendingIds.value.includes(buttonId);
}
function evaluateExpression(source, fallback) {
  if (!source) {
    return fallback;
  }
  try {
    return Effect.runSync($cel(source));
  } catch {
    return fallback;
  }
}
function isButtonHidden(button) {
  return evaluateExpression(button.hidden, false);
}
function isButtonDisabled(button) {
  const effect = getButtonEffect(button.id);
  return evaluateExpression(button.disabled, false) || isPending(button.id) || !(isEffectValue(effect) || isEffectFactory(effect));
}
function isDropdownPending(dropdown) {
  return dropdown.items.some(
    (item) => !isRegistrySubItem(item) && !isButtonHidden(item) && isPending(item.id)
  );
}
function hasVisibleDropdownItems(dropdown) {
  return dropdown.items.some(
    (item) => isRegistrySubItem(item) || !isButtonHidden(item)
  );
}
function isDropdownItem(item) {
  return "items" in item;
}
function isRegistryItem(item) {
  return "type" in item && "compatibilityDate" in item;
}
function isRegistrySubItem(item) {
  return "type" in item && "compatibilityDate" in item;
}
function getItemRuntime(item) {
  return findItem(item.type, item.compatibilityDate)?.runtime;
}
function isButtonDropdownChild(item) {
  return !isRegistrySubItem(item);
}
function getResolvedButtonVariant(group, button) {
  return button.variant ?? group.variant ?? "default";
}
function getResolvedSize() {
  return config.value.size ?? "default";
}
function getResolvedDropdownVariant(group) {
  return group.variant ?? "default";
}
function hasLeadingPrimaryGap(group, itemIndex) {
  if (itemIndex === 0) {
    return false;
  }
  const item = group.items[itemIndex];
  const previousItem = group.items[itemIndex - 1];
  if (!item || !previousItem) return false;
  if (isDropdownItem(item) || isDropdownItem(previousItem)) return false;
  if (isRegistryItem(item) || isRegistryItem(previousItem)) return false;
  return getResolvedButtonVariant(group, item) === "primary" && getResolvedButtonVariant(group, previousItem) === "primary";
}
function handleBadResponse(error) {
  return Effect.sync(() => {
    toast.error(t("action-bad-response-title"), {
      description: t("action-bad-response-description")
    });
    if (isDevelopment) console.warn("[shwfed-actions] bad response body", error);
  });
}
async function runButton(buttonId) {
  if (isPending(buttonId)) {
    return;
  }
  const action = declaredActions[buttonId];
  if (!action?.effect) {
    return;
  }
  pendingIds.value = [...pendingIds.value, buttonId];
  try {
    await action.effect.pipe(
      Effect.scoped,
      Effect.provideService(ActionContext, { definitionId: action.ownerId }),
      Effect.provide(Fetch.layer),
      Effect.catchTags({
        FetchError: (error) => Effect.sync(() => {
          toast.error(t("action-network-error-title"), {
            description: t("action-network-error-description")
          });
          if (isDevelopment) console.warn("[shwfed-actions] network error", error);
        }),
        NotOkError: (error) => Effect.sync(() => {
          toast.error(t("action-server-error-title"), {
            description: t("action-server-error-description", { status: error.response.status })
          });
          if (isDevelopment) console.warn("[shwfed-actions] non-ok response", error);
        }),
        MalformedJsonError: handleBadResponse,
        MalformedTextError: handleBadResponse,
        MalformedBlobError: handleBadResponse,
        MalformedBytesError: handleBadResponse,
        MalformedArrayBufferError: handleBadResponse,
        MalformedFormDataError: handleBadResponse,
        MalformedReadableStreamError: handleBadResponse,
        BodyIsNotPresentError: handleBadResponse
      }),
      Effect.runPromise
    );
  } catch (error) {
    if (isDevelopment) console.error("[shwfed-actions] action failed", error);
  } finally {
    pendingIds.value = pendingIds.value.filter((id) => id !== buttonId);
  }
}
</script>

<template>
  <ClientOnly>
    <div
      data-slot="buttons"
      :class="cn('relative flex flex-wrap items-baseline')"
      :style="[
  rootStyle,
  config.style
]"
    >
      <template
        v-for="runtime in actionRuntimes"
        :key="runtime.buttonId"
      >
        <component
          :is="runtime.component"
          :button-id="runtime.buttonId"
          :config="runtime.config"
          :button-title="runtime.buttonTitle"
        />
      </template>

      <ButtonGroup
        v-for="group in groupedItems"
        :key="group.id"
        data-slot="buttons-group"
        orientation="horizontal"
      >
        <template
          v-for="(item, itemIndex) in group.items"
          :key="item.id"
        >
          <component
            :is="getItemRuntime(item)"
            v-if="isRegistryItem(item) && getItemRuntime(item)"
            :config="item"
          />

          <Button
            v-else-if="!isRegistryItem(item) && !isDropdownItem(item) && !isButtonHidden(item) && !hasButtonTooltip(item)"
            data-slot="buttons-item"
            :variant="getResolvedButtonVariant(group, item)"
            :size="getResolvedSize()"
            :disabled="isButtonDisabled(item)"
            :class="hasLeadingPrimaryGap(group, itemIndex) ? 'ml-px' : void 0"
            @click="void runButton(item.id)"
          >
            <Icon
              v-if="isPending(item.id)"
              icon="line-md:loading-twotone-loop"
            />
            <Icon
              v-else-if="item.icon"
              :icon="item.icon"
            />
            <span v-if="!item.hideTitle">{{ getButtonLabel(item) }}</span>
          </Button>

          <Tooltip
            v-else-if="!isRegistryItem(item) && !isDropdownItem(item) && !isButtonHidden(item)"
            :delay-duration="180"
          >
            <TooltipTrigger as-child>
              <Button
                data-slot="buttons-item"
                :variant="getResolvedButtonVariant(group, item)"
                :size="getResolvedSize()"
                :disabled="isButtonDisabled(item)"
                :class="hasLeadingPrimaryGap(group, itemIndex) ? 'ml-px' : void 0"
                @click="void runButton(item.id)"
              >
                <Icon
                  v-if="isPending(item.id)"
                  icon="line-md:loading-twotone-loop"
                />
                <Icon
                  v-else-if="item.icon"
                  :icon="item.icon"
                />
                <span v-if="!item.hideTitle">{{ getButtonLabel(item) }}</span>
              </Button>
            </TooltipTrigger>
            <TooltipContent
              data-slot="buttons-tooltip-content"
              align="center"
              side="top"
            >
              <Markdown
                :source="getButtonTooltip(item) ?? ''"
                class="prose prose-xs prose-zinc"
              />
            </TooltipContent>
          </Tooltip>

          <DropdownMenu
            v-else-if="isDropdownItem(item) && hasVisibleDropdownItems(item)"
          >
            <DropdownMenuTrigger as-child>
              <Button
                data-slot="buttons-dropdown-trigger"
                :variant="getResolvedDropdownVariant(group)"
                :size="getResolvedSize()"
                :disabled="isDropdownPending(item)"
              >
                <Icon
                  v-if="isDropdownPending(item)"
                  icon="line-md:loading-twotone-loop"
                />
                <Icon
                  v-else-if="item.icon"
                  :icon="item.icon"
                />
                <span v-if="!item.hideTitle">{{ getDropdownLabel(item) }}</span>
              </Button>
            </DropdownMenuTrigger>

            <DropdownMenuContent align="start">
              <template
                v-for="child in item.items"
                :key="child.id"
              >
                <component
                  :is="getItemRuntime(child)"
                  v-if="isRegistrySubItem(child) && getItemRuntime(child)"
                  data-slot="buttons-dropdown-item-registry"
                  :config="child"
                />

                <DropdownMenuItem
                  v-else-if="isButtonDropdownChild(child) && !isButtonHidden(child) && !hasButtonTooltip(child)"
                  data-slot="buttons-dropdown-item"
                  :disabled="isButtonDisabled(child)"
                  @select="void runButton(child.id)"
                >
                  <Icon
                    v-if="isPending(child.id)"
                    icon="line-md:loading-twotone-loop"
                  />
                  <Icon
                    v-else-if="child.icon"
                    :icon="child.icon"
                  />
                  <span>{{ getButtonLabel(child) }}</span>
                </DropdownMenuItem>

                <Tooltip
                  v-else-if="isButtonDropdownChild(child) && !isButtonHidden(child)"
                  :delay-duration="180"
                >
                  <TooltipTrigger as-child>
                    <DropdownMenuItem
                      data-slot="buttons-dropdown-item"
                      :disabled="isButtonDisabled(child)"
                      @select="void runButton(child.id)"
                    >
                      <Icon
                        v-if="isPending(child.id)"
                        icon="line-md:loading-twotone-loop"
                      />
                      <Icon
                        v-else-if="child.icon"
                        :icon="child.icon"
                      />
                      <span>{{ getButtonLabel(child) }}</span>
                    </DropdownMenuItem>
                  </TooltipTrigger>
                  <TooltipContent
                    data-slot="buttons-tooltip-content"
                    align="center"
                    side="top"
                  >
                    <Markdown
                      :source="getButtonTooltip(child) ?? ''"
                      class="prose prose-xs prose-zinc"
                    />
                  </TooltipContent>
                </Tooltip>
              </template>
            </DropdownMenuContent>
          </DropdownMenu>
        </template>
      </ButtonGroup>

      <slot />
    </div>
  </ClientOnly>
</template>
