<script setup lang="ts">
import type { ImageSelectorEmits, ImageSelectorFile, ImageSelectorProps } from './types';

import { computed, ref, watch } from 'vue';

import {
  Check,
  CloudUploadOutlined,
  DeleteOutlined,
  EyeOutlined,
  FileImageOutlined,
} from '@vben/icons';
import { $t } from '@vben/locales';

import { ElButton, ElDialog, ElImageViewer, ElMessage, ElProgress } from 'element-plus';

import {
  getFilesInfo,
  getRecentImages,
  uploadFile as uploadFileApi,
} from '#/api/core/file';
import { getFileUrl } from '#/composables/useFileUrl';

defineOptions({
  name: 'ImageSelector',
  inheritAttrs: false,
});

interface Props extends ImageSelectorProps {}

type UploadingImage = ImageSelectorFile & {
  failed?: boolean;
  progress?: number;
  uploading?: boolean;
};

const props = withDefaults(defineProps<Props>(), {
  multiple: false,
  placeholder: () => $t('ui.placeholder.select') || '?????',
  disabled: false,
  clearable: true,
  showImageInfo: true,
  maxSize: 10,
  maxWidth: 0,
  maxHeight: 0,
  minWidth: 0,
  minHeight: 0,
  accept: () => ['image/*'],
  gridColumns: 4,
  sortable: false,
  enableCrop: false,
  cropAspectRatio: undefined,
  cropShape: 'rect',
  aspectRatioPreset: '',
  aspectRatioWidth: 16,
  aspectRatioHeight: 9,
  aspectRatioMode: 'validate',
  aspectRatioTolerance: 0.02,
  size: undefined,
  width: undefined,
  height: undefined,
  source: 'form',
});

const emit = defineEmits<ImageSelectorEmits>();

const modalVisible = ref(false);
const uploadInputRef = ref<HTMLInputElement>();
const confirmedImages = ref<UploadingImage[]>([]);
const uploadedImages = ref<UploadingImage[]>([]);
const recentImages = ref<UploadingImage[]>([]);
const selectedImages = ref<Set<string>>(new Set());
const recentLoading = ref(false);
const isDragging = ref(false);
const previewVisible = ref(false);
const previewImageUrls = ref<string[]>([]);
const previewInitialIndex = ref(0);

const acceptText = computed(() => props.accept?.join(',') || 'image/*');
const triggerWidth = computed(() => props.width ?? props.size ?? undefined);
const triggerHeight = computed(() => props.height ?? props.size ?? undefined);
const triggerStyle = computed(() => ({
  '--image-selector-width': triggerWidth.value ? `${triggerWidth.value}px` : undefined,
  '--image-selector-height': triggerHeight.value ? `${triggerHeight.value}px` : undefined,
}));

const selectedCount = computed(() => selectedImages.value.size);
const hasSelection = computed(() => selectedCount.value > 0);
const allImages = computed(() => [...uploadedImages.value, ...recentImages.value]);

watch(
  () => props.modelValue,
  async (value) => {
    const ids = normalizeModelValue(value);
    if (ids.length === 0) {
      confirmedImages.value = [];
      return;
    }

    confirmedImages.value = ids.map((id) => ({ id, name: '', path: '', url: '' }));

    try {
      const infos = await getFilesInfo(ids);
      const infoMap = new Map(infos.filter(Boolean).map((item: any) => [String(item.id), item]));
      confirmedImages.value = await Promise.all(
        ids.map(async (id) => {
          const info = infoMap.get(id) as any;
          return {
            id,
            name: info?.name || info?.file_name || '',
            path: info?.path || '',
            size: info?.file_size ?? info?.size,
            mime_type: info?.mime_type,
            sys_create_datetime: info?.sys_create_datetime,
            url: await resolveImageUrl(id),
          };
        }),
      );
    } catch {
      confirmedImages.value = await Promise.all(
        ids.map(async (id) => ({ id, name: '', path: '', url: await resolveImageUrl(id) })),
      );
    }
  },
  { immediate: true },
);

function normalizeModelValue(value: Props['modelValue']): string[] {
  if (!value) return [];
  return (Array.isArray(value) ? value : [value]).filter(Boolean).map(String);
}

async function resolveImageUrl(id: string) {
  try {
    return await getFileUrl(id);
  } catch {
    return '';
  }
}

function openModal() {
  if (props.disabled) return;
  modalVisible.value = true;
  selectedImages.value = new Set(confirmedImages.value.map((item) => item.id));
  loadRecentImages();
}

async function loadRecentImages() {
  recentLoading.value = true;
  try {
    const items = await getRecentImages(20);
    recentImages.value = await Promise.all(
      (items || []).map(async (item: any) => ({
        id: String(item.id),
        name: item.name || item.file_name || '',
        path: item.path || '',
        size: item.file_size ?? item.size,
        mime_type: item.mime_type,
        sys_create_datetime: item.sys_create_datetime,
        url: await resolveImageUrl(String(item.id)),
      })),
    );
  } finally {
    recentLoading.value = false;
  }
}

function toggleImage(id: string) {
  const next = new Set(selectedImages.value);
  if (props.multiple) {
    if (next.has(id)) next.delete(id);
    else next.add(id);
  } else {
    next.clear();
    next.add(id);
  }
  selectedImages.value = next;
}

function removeConfirmed(id: string) {
  const next = confirmedImages.value.filter((item) => item.id !== id);
  confirmedImages.value = next;
  emitValue(next.map((item) => item.id));
}

function clearValue() {
  confirmedImages.value = [];
  emitValue([]);
}

function emitValue(ids: string[]) {
  const value = props.multiple ? ids : (ids[0] ?? null);
  emit('update:modelValue', value);
  emit('change', value);
}

function confirmSelection() {
  const selectedIds = [...selectedImages.value];
  const imageMap = new Map(allImages.value.map((item) => [item.id, item]));
  confirmedImages.value = selectedIds.map(
    (id) => imageMap.get(id) || confirmedImages.value.find((item) => item.id === id) || { id, name: '', path: '' },
  );
  emitValue(selectedIds);
  modalVisible.value = false;
}

function handleUploadClick() {
  uploadInputRef.value?.click();
}

async function handleFileInput(event: Event) {
  const input = event.target as HTMLInputElement;
  await uploadFiles(Array.from(input.files || []));
  input.value = '';
}

async function uploadFiles(files: File[]) {
  const validFiles = await filterValidFiles(files);
  if (validFiles.length === 0) return;

  for (const file of validFiles) {
    const tempId = `local-${crypto.randomUUID?.() || Date.now()}`;
    const item: UploadingImage = {
      id: tempId,
      name: file.name,
      path: '',
      previewUrl: URL.createObjectURL(file),
      url: URL.createObjectURL(file),
      progress: 0,
      uploading: true,
    };
    uploadedImages.value.unshift(item);

    try {
      const result: any = await uploadFileApi(file, {
        isPublic: false,
        source: props.source,
        onProgress: ({ percentage }) => {
          item.progress = percentage;
        },
      });
      const data = result?.data ?? result;
      const id = String(data?.id ?? data?.file_id ?? data?.fileId);
      if (!id || id === 'undefined') throw new Error('upload response missing id');
      item.id = id;
      item.path = data?.path || '';
      item.size = data?.file_size ?? data?.size ?? file.size;
      item.mime_type = data?.mime_type || file.type;
      item.url = await resolveImageUrl(id);
      item.uploading = false;
      item.progress = 100;
      selectedImages.value = props.multiple
        ? new Set([...selectedImages.value, id])
        : new Set([id]);
    } catch (error: any) {
      item.uploading = false;
      item.failed = true;
      ElMessage.error(error?.message || '??????');
    }
  }
}

async function filterValidFiles(files: File[]) {
  const result: File[] = [];
  for (const file of files) {
    if (!file.type.startsWith('image/')) {
      ElMessage.warning(`${file.name} ??????`);
      continue;
    }
    if (props.maxSize && file.size > props.maxSize * 1024 * 1024) {
      ElMessage.warning(`${file.name} ?? ${props.maxSize}MB`);
      continue;
    }
    if (!(await validateImageSize(file))) continue;
    result.push(file);
  }
  return result;
}

function validateImageSize(file: File) {
  if (!props.maxWidth && !props.maxHeight && !props.minWidth && !props.minHeight) {
    return Promise.resolve(true);
  }

  return new Promise<boolean>((resolve) => {
    const url = URL.createObjectURL(file);
    const image = new Image();
    image.onload = () => {
      URL.revokeObjectURL(url);
      const { width, height } = image;
      if (props.maxWidth && width > props.maxWidth) {
        ElMessage.warning(`${file.name} ?????? ${props.maxWidth}px`);
        resolve(false);
        return;
      }
      if (props.maxHeight && height > props.maxHeight) {
        ElMessage.warning(`${file.name} ?????? ${props.maxHeight}px`);
        resolve(false);
        return;
      }
      if (props.minWidth && width < props.minWidth) {
        ElMessage.warning(`${file.name} ?????? ${props.minWidth}px`);
        resolve(false);
        return;
      }
      if (props.minHeight && height < props.minHeight) {
        ElMessage.warning(`${file.name} ?????? ${props.minHeight}px`);
        resolve(false);
        return;
      }
      resolve(true);
    };
    image.onerror = () => {
      URL.revokeObjectURL(url);
      resolve(false);
    };
    image.src = url;
  });
}

function onDragOver(event: DragEvent) {
  event.preventDefault();
  if (!props.disabled) isDragging.value = true;
}

function onDragLeave() {
  isDragging.value = false;
}

async function onDrop(event: DragEvent) {
  event.preventDefault();
  isDragging.value = false;
  if (props.disabled) return;
  await uploadFiles(Array.from(event.dataTransfer?.files || []));
}

function openPreview(images: UploadingImage[], index: number) {
  const urls = images.map((item) => item.url || item.previewUrl || '').filter(Boolean);
  if (urls.length === 0) return;
  previewImageUrls.value = urls;
  previewInitialIndex.value = index;
  previewVisible.value = true;
}
</script>

<template>
  <div class="image-selector" :style="triggerStyle">
    <div
      class="image-selector-trigger-grid"
      :class="{ disabled: disabled, 'has-size': !!(size || width || height) }"
    >
      <div
        v-for="(image, index) in confirmedImages"
        :key="image.id"
        class="image-grid-item image-preview"
        :class="{ 'is-circle': cropShape === 'circle' }"
        @click="openPreview(confirmedImages, index)"
      >
        <img v-if="image.url || image.previewUrl" class="preview-image" :src="image.url || image.previewUrl" />
        <FileImageOutlined v-else class="placeholder-icon" />
        <button
          v-if="clearable && !disabled"
          class="image-remove-btn"
          type="button"
          @click.stop="removeConfirmed(image.id)"
        >
          <DeleteOutlined class="h-3 w-3" />
        </button>
      </div>

      <button
        v-if="!disabled && (multiple || confirmedImages.length === 0)"
        class="image-grid-item upload-placeholder"
        :class="{ 'is-circle': cropShape === 'circle' }"
        type="button"
        @click="openModal"
      >
        <CloudUploadOutlined class="placeholder-icon" />
        <span class="placeholder-text">{{ placeholder }}</span>
      </button>

      <button
        v-if="clearable && !disabled && confirmedImages.length > 0"
        class="clear-btn"
        type="button"
        @click="clearValue"
      >
        <DeleteOutlined class="h-3.5 w-3.5" />
      </button>
    </div>

    <ElDialog
      v-model="modalVisible"
      append-to-body
      class="image-selector-dialog"
      title="????"
      width="860px"
    >
      <div class="image-selector-layout" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
        <div class="upload-area" :class="{ 'is-dragging': isDragging }" @click="handleUploadClick">
          <CloudUploadOutlined class="upload-area-icon" />
          <div class="upload-area-title">????</div>
          <div class="upload-area-hint">???????????? {{ maxSize }}MB</div>
          <input
            ref="uploadInputRef"
            class="hidden-input"
            :accept="acceptText"
            :multiple="multiple"
            type="file"
            @change="handleFileInput"
          />
        </div>

        <div class="image-selector-content">
          <div class="toolbar">
            <span>??? {{ selectedCount }} ?</span>
            <ElButton size="small" :loading="recentLoading" @click="loadRecentImages">??</ElButton>
          </div>

          <div class="image-grid">
            <div
              v-for="(image, index) in allImages"
              :key="image.id"
              class="image-card"
              :class="{ selected: selectedImages.has(image.id), uploading: image.uploading, failed: image.failed }"
              @click="!image.uploading && !image.failed && toggleImage(image.id)"
            >
              <div class="image-card-preview">
                <img v-if="image.url || image.previewUrl" class="image-card-img" :src="image.url || image.previewUrl" />
                <FileImageOutlined v-else class="image-card-empty" />
                <div v-if="selectedImages.has(image.id)" class="image-card-check">
                  <Check class="h-3.5 w-3.5" />
                </div>
                <div v-if="image.uploading" class="image-card-mask">
                  <ElProgress type="circle" :percentage="image.progress || 0" :width="56" />
                </div>
              </div>
              <div class="image-card-info">
                <span class="image-card-name">{{ image.name || image.id }}</span>
                <ElButton text size="small" @click.stop="openPreview(allImages, index)">
                  <EyeOutlined class="h-4 w-4" />
                </ElButton>
              </div>
            </div>
          </div>
        </div>
      </div>

      <template #footer>
        <ElButton @click="modalVisible = false">??</ElButton>
        <ElButton type="primary" :disabled="!hasSelection" @click="confirmSelection">??</ElButton>
      </template>
    </ElDialog>

    <ElImageViewer
      v-if="previewVisible"
      :initial-index="previewInitialIndex"
      :url-list="previewImageUrls"
      @close="previewVisible = false"
    />
  </div>
</template>

<style scoped>
.image-selector {
  width: 100%;
}

.image-selector-trigger-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 10px;
  position: relative;
}

.image-selector-trigger-grid.has-size {
  display: flex;
  flex-wrap: wrap;
}

.image-selector-trigger-grid.disabled {
  opacity: 0.6;
  pointer-events: none;
}

.image-grid-item {
  position: relative;
  width: var(--image-selector-width, 100%);
  height: var(--image-selector-height, auto);
  min-width: var(--image-selector-width, 96px);
  min-height: var(--image-selector-height, 76px);
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border: 1px dashed hsl(var(--border));
  border-radius: 8px;
  background: hsl(var(--background));
}

.image-grid-item.is-circle,
.image-grid-item.is-circle .preview-image {
  border-radius: 999px;
}

.upload-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 6px;
  color: hsl(var(--muted-foreground));
  cursor: pointer;
}

.upload-placeholder:hover {
  border-color: hsl(var(--primary));
  color: hsl(var(--primary));
}

.placeholder-icon {
  width: 28px;
  height: 28px;
}

.placeholder-text {
  max-width: 90%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
}

.preview-image,
.image-card-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.image-preview {
  cursor: zoom-in;
}

.image-remove-btn,
.clear-btn {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  cursor: pointer;
}

.image-remove-btn {
  top: 4px;
  right: 4px;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  color: #fff;
  background: rgb(0 0 0 / 55%);
}

.clear-btn {
  right: 4px;
  top: 4px;
  width: 24px;
  height: 24px;
  border-radius: 6px;
  color: hsl(var(--muted-foreground));
  background: hsl(var(--background));
  border: 1px solid hsl(var(--border));
}

.image-selector-layout {
  display: grid;
  grid-template-columns: 240px minmax(0, 1fr);
  gap: 16px;
  min-height: 520px;
}

.upload-area {
  display: flex;
  min-height: 320px;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 10px;
  padding: 24px;
  border: 1px dashed hsl(var(--border));
  border-radius: 8px;
  background: hsl(var(--muted) / 40%);
  cursor: pointer;
  text-align: center;
}

.upload-area.is-dragging,
.upload-area:hover {
  border-color: hsl(var(--primary));
  background: hsl(var(--primary) / 8%);
}

.upload-area-icon {
  width: 46px;
  height: 46px;
  color: hsl(var(--primary));
}

.upload-area-title {
  font-size: 15px;
  font-weight: 600;
  color: hsl(var(--foreground));
}

.upload-area-hint {
  font-size: 12px;
  color: hsl(var(--muted-foreground));
}

.hidden-input {
  display: none;
}

.image-selector-content {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 13px;
  color: hsl(var(--muted-foreground));
}

.image-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  gap: 12px;
  overflow: auto;
  max-height: 472px;
  padding-right: 4px;
}

.image-card {
  overflow: hidden;
  border: 1px solid hsl(var(--border));
  border-radius: 8px;
  background: hsl(var(--background));
  cursor: pointer;
}

.image-card.selected {
  border-color: hsl(var(--primary));
  box-shadow: 0 0 0 2px hsl(var(--primary) / 18%);
}

.image-card-preview {
  position: relative;
  aspect-ratio: 1 / 1;
  background: hsl(var(--muted));
}

.image-card-empty {
  width: 32px;
  height: 32px;
  margin: calc(50% - 16px);
  color: hsl(var(--muted-foreground));
}

.image-card-check {
  position: absolute;
  top: 6px;
  right: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  color: #fff;
  background: hsl(var(--primary));
}

.image-card-mask {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgb(0 0 0 / 55%);
}

.image-card-info {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 10px;
}

.image-card-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
}

@media (max-width: 768px) {
  .image-selector-layout {
    grid-template-columns: 1fr;
  }
}
</style>
