<script setup>
import { Icon } from "@iconify/vue";
import { Effect } from "effect";
import { Fetch } from "fx-fetch";
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import { toast } from "vue-sonner";
import { cel as _rawCel } from "../../../../../utils/cel";
import { celBindings, injectCELContext } from "../../../../../utils/cel-context";
import { getLocalizedText } from "../../../../../share/locale";
import { Button } from "../../../../ui/button";
import { Field, FieldLabel, FieldMessages } from "../../../../ui/field";
import { Markdown } from "../../../../ui/markdown";
import { DEFAULT_FIELD_ORIENTATION } from "../../../utils/common";
import { useFieldValidation } from "../../../utils/validation";
import { useFormReadonly } from "../../../utils/readonly";
import { useFormState } from "../../../utils/state";
const ICONS = {
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "vscode-icons:file-type-excel",
  "application/vnd.ms-excel": "vscode-icons:file-type-excel",
  "application/vnd.openxmlformats-officedocument.presentationml.presentation": "vscode-icons:file-type-powerpoint",
  "application/vnd.ms-powerpoint": "vscode-icons:file-type-powerpoint",
  "application/zip": "vscode-icons:file-type-zip",
  "application/x-zip-compressed": "vscode-icons:file-type-zip",
  "application/vnd.rar": "vscode-icons:file-type-zip",
  "application/x-rar-compressed": "vscode-icons:file-type-zip",
  "application/pdf": "vscode-icons:file-type-pdf2",
  "application/ofd": "vscode-icons:file-type-ofd",
  "application/xml": "vscode-icons:file-type-xml",
  "image/png": "vscode-icons:file-type-image",
  "image/jpg": "vscode-icons:file-type-image",
  "image/jpeg": "vscode-icons:file-type-image",
  "application/msword": "vscode-icons:file-type-word",
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "vscode-icons:file-type-word"
};
const SHORT_NAMES = {
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "XLSX",
  "application/vnd.ms-excel": "XLS",
  "application/vnd.openxmlformats-officedocument.presentationml.presentation": "PPTX",
  "application/vnd.ms-powerpoint": "PPT",
  "application/zip": "ZIP",
  "application/x-zip-compressed": "ZIP",
  "application/vnd.rar": "RAR",
  "application/x-rar-compressed": "RAR",
  "application/pdf": "PDF",
  "application/ofd": "OFD",
  "application/xml": "XML",
  "image/png": "PNG",
  "image/jpg": "JPG",
  "image/jpeg": "JPEG",
  "application/msword": "DOC",
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "DOCX"
};
defineOptions({ name: "ShwfedUploadFieldRuntime" });
const props = defineProps({
  fieldId: { type: String, required: true },
  config: { type: null, required: true }
});
const { locale, t } = useI18n({
  inheritLocale: true,
  messages: {
    zh: {
      "upload-add-file": "\u6DFB\u52A0\u6587\u4EF6",
      "upload-accept-hint": "\u4EC5\u63A5\u53D7 {types}",
      "upload-rejected-mime": "\u300C{name}\u300D\u7C7B\u578B\u4E0D\u88AB\u5141\u8BB8",
      "upload-rejected-size": "\u300C{name}\u300D\u8D85\u51FA\u5355\u6587\u4EF6\u5927\u5C0F\u9650\u5236",
      "upload-rejected-total": "\u300C{name}\u300D\u52A0\u5165\u540E\u5C06\u8D85\u51FA\u603B\u5927\u5C0F\u9650\u5236",
      "upload-rejected-count": "\u5DF2\u8FBE\u5230\u6587\u4EF6\u6570\u91CF\u4E0A\u9650\uFF0C\u300C{name}\u300D\u672A\u6DFB\u52A0",
      "upload-uploading": "\u4E0A\u4F20\u4E2D\u2026",
      "upload-failed": "\u6587\u4EF6\u4E0A\u4F20\u5931\u8D25"
    },
    en: {
      "upload-add-file": "Add file",
      "upload-accept-hint": "Only {types} are accepted",
      "upload-rejected-mime": "{name} is not an allowed file type",
      "upload-rejected-size": "{name} exceeds the maximum file size",
      "upload-rejected-total": "{name} would exceed the total size limit",
      "upload-rejected-count": "File count limit reached; {name} was not added",
      "upload-uploading": "Uploading\u2026",
      "upload-failed": "File upload failed"
    },
    ja: {
      "upload-add-file": "\u30D5\u30A1\u30A4\u30EB\u3092\u8FFD\u52A0",
      "upload-accept-hint": "{types} \u306E\u307F\u53D7\u3051\u4ED8\u3051\u307E\u3059",
      "upload-rejected-mime": "\u300C{name}\u300D\u306F\u8A31\u53EF\u3055\u308C\u305F\u30D5\u30A1\u30A4\u30EB\u5F62\u5F0F\u3067\u306F\u3042\u308A\u307E\u305B\u3093",
      "upload-rejected-size": "\u300C{name}\u300D\u306F\u30D5\u30A1\u30A4\u30EB\u30B5\u30A4\u30BA\u306E\u4E0A\u9650\u3092\u8D85\u3048\u3066\u3044\u307E\u3059",
      "upload-rejected-total": "\u300C{name}\u300D\u3092\u8FFD\u52A0\u3059\u308B\u3068\u5408\u8A08\u30B5\u30A4\u30BA\u306E\u4E0A\u9650\u3092\u8D85\u3048\u307E\u3059",
      "upload-rejected-count": "\u30D5\u30A1\u30A4\u30EB\u6570\u306E\u4E0A\u9650\u306B\u9054\u3057\u305F\u305F\u3081\u300C{name}\u300D\u306F\u8FFD\u52A0\u3055\u308C\u307E\u305B\u3093",
      "upload-uploading": "\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u4E2D\u2026",
      "upload-failed": "\u30D5\u30A1\u30A4\u30EB\u306E\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u306B\u5931\u6557\u3057\u307E\u3057\u305F"
    }
  }
});
const { state, getAt, setAt } = useFormState();
const inherited = injectCELContext();
const $cel = (expression, context) => _rawCel(expression, { ...celBindings(inherited), ...context });
const labelText = computed(
  () => props.config.label ? getLocalizedText(props.config.label, locale.value) : void 0
);
const descriptionText = computed(
  () => props.config.description ? getLocalizedText(props.config.description, locale.value) : void 0
);
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 { isRequired, errors, warnings, visible } = useFieldValidation({
  fieldId: () => props.fieldId,
  config: () => props.config,
  $cel,
  locale: () => locale.value
});
const uncontrolled = ref([]);
const entries = computed({
  get: () => {
    const path = props.config.binding;
    const raw = path == null ? uncontrolled.value : getAt(path);
    return Array.isArray(raw) ? raw : [];
  },
  set: (next) => {
    const path = props.config.binding;
    if (path == null) {
      uncontrolled.value = next;
      return;
    }
    setAt(path, next.length === 0 ? null : next);
  }
});
const immediate = computed(() => props.config.upload != null);
const uploading = ref(false);
function entryName(entry) {
  if (entry instanceof File) return entry.name;
  const expr = props.config.upload?.filename;
  if (expr) {
    try {
      const name = Effect.runSync($cel(expr, { form: state.value ?? {}, file: entry }));
      if (typeof name === "string" && name.length > 0) return name;
    } catch (err) {
      console.error(`[shwfed-form] failed to evaluate upload filename for ${props.fieldId}:`, err);
    }
  }
  if (entry != null && typeof entry === "object") {
    const rec = entry;
    if (typeof rec.filename === "string") return rec.filename;
    if (typeof rec.name === "string") return rec.name;
  }
  return "(\u6587\u4EF6)";
}
function entryIcon(entry) {
  if (entry instanceof File) return ICONS[entry.type] ?? "fluent:document-20-regular";
  return "fluent:document-20-regular";
}
const inputRef = ref();
const acceptAttr = computed(() => {
  const list = props.config.accept;
  return list && list.length > 0 ? list.join(",") : void 0;
});
const acceptHint = computed(() => {
  const list = props.config.accept;
  if (!list || list.length === 0) return void 0;
  return Array.from(new Set(list.map((m) => SHORT_NAMES[m] ?? m))).join("\u3001");
});
const isFull = computed(
  () => props.config.maxFiles !== void 0 && entries.value.length >= props.config.maxFiles
);
const canAdd = computed(() => !isDisabled.value && !isReadonly.value && !isFull.value && !uploading.value);
function isMimeAllowed(file) {
  const list = props.config.accept;
  if (!list || list.length === 0) return true;
  return list.includes(file.type);
}
function partition(incoming) {
  const accepted = [];
  const rejected = [];
  let runningTotal = entries.value.reduce((acc, f) => acc + (f instanceof File ? f.size : 0), 0);
  let count = entries.value.length;
  for (const file of incoming) {
    if (!isMimeAllowed(file)) {
      rejected.push({ file, reason: "mime" });
      continue;
    }
    if (props.config.maxFileSize !== void 0 && file.size > props.config.maxFileSize) {
      rejected.push({ file, reason: "size" });
      continue;
    }
    if (props.config.maxFiles !== void 0 && count >= props.config.maxFiles) {
      rejected.push({ file, reason: "count" });
      continue;
    }
    if (props.config.maxTotalSize !== void 0 && runningTotal + file.size > props.config.maxTotalSize) {
      rejected.push({ file, reason: "total" });
      continue;
    }
    accepted.push(file);
    runningTotal += file.size;
    count += 1;
  }
  return { accepted, rejected };
}
async function uploadFiles(accepted) {
  const upload = props.config.upload;
  if (!upload) return [];
  const program = Effect.gen(function* () {
    const builder = yield* $cel(upload.request, {
      form: state.value ?? {},
      files: accepted
    });
    const json = yield* builder.json();
    return yield* $cel(upload.handle, { form: state.value ?? {}, json });
  });
  try {
    const items = await Effect.runPromise(Effect.provide(program, Fetch.layer));
    if (!Array.isArray(items)) {
      console.error(`[shwfed-form] upload handle for ${props.fieldId} did not return a list`);
      toast.error(t("upload-failed"));
      return [];
    }
    return items;
  } catch (err) {
    console.error(`[shwfed-form] failed to upload files for ${props.fieldId}:`, err);
    toast.error(t("upload-failed"));
    return [];
  }
}
async function ingest(list) {
  if (!list || list.length === 0) return;
  const multi = props.config.multiple ?? false;
  const incoming = multi ? Array.from(list) : Array.from(list).slice(0, 1);
  const { accepted, rejected } = partition(incoming);
  for (const r of rejected) {
    toast.error(t(`upload-rejected-${r.reason}`, { name: r.file.name }));
  }
  if (inputRef.value) inputRef.value.value = "";
  if (accepted.length === 0) return;
  if (!immediate.value) {
    entries.value = multi ? [...entries.value, ...accepted] : accepted;
    return;
  }
  uploading.value = true;
  try {
    const items = await uploadFiles(accepted);
    if (items.length > 0) {
      entries.value = multi ? [...entries.value, ...items] : items;
    }
  } finally {
    uploading.value = false;
  }
}
function onPickerChange(event) {
  void ingest(event.target.files);
}
function onAddClick() {
  if (!canAdd.value) return;
  inputRef.value?.click();
}
function onKeydown(event) {
  if (!canAdd.value) return;
  if (event.key === "Enter" || event.key === " ") {
    event.preventDefault();
    inputRef.value?.click();
  }
}
function onDelete(index) {
  if (isDisabled.value || isReadonly.value || uploading.value) return;
  const next = [...entries.value];
  next.splice(index, 1);
  entries.value = next;
}
const templates = computed(() => props.config.templates ?? []);
function templateIcon(template) {
  return template.icon ?? "fluent:arrow-download-20-regular";
}
function templateLabelText(template) {
  return (template.label ? getLocalizedText(template.label, locale.value) : void 0) ?? "\u4E0B\u8F7D\u6A21\u677F";
}
async function onDownloadTemplate(template) {
  const ctx = { form: state.value ?? {} };
  const program = Effect.gen(function* () {
    const requestBuilder = yield* $cel(template.request, ctx);
    if (!template.download) {
      return yield* requestBuilder.file();
    }
    const json = yield* requestBuilder.json();
    const downloadBuilder = yield* $cel(template.download, { ...ctx, json });
    return yield* downloadBuilder.file();
  });
  try {
    const file = await Effect.runPromise(Effect.provide(program, Fetch.layer));
    if (!(file instanceof File)) {
      console.error(`[shwfed-form] template request for ${props.fieldId} did not produce a File`);
      return;
    }
    const url = URL.createObjectURL(file);
    const a = document.createElement("a");
    a.href = url;
    a.download = file.name;
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
  } catch (err) {
    console.error(`[shwfed-form] failed to download template for ${props.fieldId}:`, err);
  }
}
</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
      :id="fieldId"
      class="flex flex-col gap-2"
      :data-disabled="isDisabled || void 0"
      :data-readonly="isReadonly || void 0"
    >
      <div
        v-if="entries.length === 0"
        role="button"
        :tabindex="canAdd ? 0 : -1"
        data-slot="upload-zone"
        :data-disabled="isDisabled || void 0"
        :data-readonly="isReadonly || void 0"
        :aria-disabled="isDisabled || void 0"
        class="flex min-h-40 w-full flex-col gap-3 rounded border-2 border-dashed border-zinc-200 bg-zinc-50 p-4 outline-none transition-colors duration-180 ease-out focus-visible:border-(--primary) data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-60"
        :class="[
  canAdd && 'cursor-pointer hover:border-zinc-400 hover:bg-zinc-100',
  isReadonly && 'cursor-default'
]"
        @click="onAddClick"
        @keydown="onKeydown"
      >
        <div class="flex flex-1 flex-col items-center justify-center gap-2">
          <Icon
            :icon="uploading ? 'fluent:spinner-ios-20-regular' : 'fluent:cloud-arrow-up-20-regular'"
            class="size-8 text-zinc-400"
            :class="uploading && 'animate-spin'"
          />
          <p
            v-if="uploading"
            class="text-xs text-zinc-500"
          >
            {{ t("upload-uploading") }}
          </p>
          <Markdown
            v-if="descriptionText"
            :source="descriptionText"
            block
            class="prose prose-sm prose-zinc max-w-none text-center text-zinc-500"
          />
          <p
            v-if="acceptHint"
            class="text-xs text-zinc-500"
          >
            {{ t("upload-accept-hint", { types: acceptHint }) }}
          </p>
        </div>
      </div>

      <div
        v-else
        class="flex w-full flex-col gap-2"
      >
        <ul
          data-slot="upload-zone-list"
          class="flex flex-col gap-1"
        >
          <li
            v-for="(entry, index) in entries"
            :key="`${entryName(entry)}-${index}`"
            data-slot="upload-zone-item"
            class="flex items-center justify-between gap-2 rounded px-2 py-1.5 text-sm leading-snug transition-[background-color] duration-180 ease-out hover:bg-zinc-50"
          >
            <span class="flex min-w-0 items-center gap-2">
              <Icon
                :icon="entryIcon(entry)"
                class="size-4 shrink-0"
              />
              <span class="truncate text-zinc-700">{{ entryName(entry) }}</span>
            </span>
            <Button
              v-if="!isDisabled && !isReadonly"
              type="button"
              variant="ghost"
              size="sm"
              data-slot="upload-zone-delete"
              class="size-6 p-0 text-zinc-500 hover:bg-red-50 hover:text-red-600"
              :disabled="uploading"
              :aria-label="`\u5220\u9664\uFF1A${entryName(entry)}`"
              @click="onDelete(index)"
            >
              <Icon icon="fluent:delete-20-regular" />
            </Button>
          </li>
        </ul>

        <Button
          v-if="canAdd"
          type="button"
          variant="default"
          data-slot="upload-zone-add"
          class="w-full shrink-0"
          @click="onAddClick"
        >
          <Icon
            :icon="uploading ? 'fluent:spinner-ios-20-regular' : 'fluent:add-20-regular'"
            :class="uploading && 'animate-spin'"
          />
          {{ uploading ? t("upload-uploading") : t("upload-add-file") }}
        </Button>
      </div>

      <input
        ref="inputRef"
        type="file"
        class="hidden"
        :accept="acceptAttr"
        :multiple="config.multiple"
        :disabled="isDisabled || isReadonly"
        @change="onPickerChange"
      >

      <div
        v-if="templates.length > 0 && !isReadonly"
        class="flex flex-wrap justify-end gap-1"
      >
        <Button
          v-for="(template, index) in templates"
          :key="index"
          type="button"
          variant="link"
          size="sm"
          data-slot="upload-zone-template"
          @click="onDownloadTemplate(template)"
        >
          <Icon :icon="templateIcon(template)" />
          {{ templateLabelText(template) }}
        </Button>
      </div>
    </div>
    <FieldMessages
      v-if="visible"
      :errors="errors"
      :warnings="warnings"
    />
  </Field>
</template>
