<script setup lang="ts">
import { computed, onBeforeUnmount, reactive, ref } from 'vue';
import { ArrowDown, Delete, Document, Loading, Picture, QuestionFilled, Upload } from '@element-plus/icons-vue';
import { buildFileProxyUrl, deleteFile, downloadFile, uploadFile } from '@/service/api/file';

defineOptions({ name: 'BusinessAttachmentUploader' });

interface Props {
  /** 上传目录，传给后端 directory 字段 */
  directory?: string;
  /** 数量上限，默认 20（与后端 AttachmentValidator 一致） */
  max?: number;
  /** 单文件大小上限 MB（前端兜底；最终由 /system/file/upload 拦截） */
  maxFileSizeMB?: number;
  disabled?: boolean;
  /**
   * 平铺模式：所有附件直接逐项渲染，不再做"首项 + 折叠浮层"。
   * 用于本身已经在 popover / 详情卡片里展示，避免嵌套浮层。
   */
  flat?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  directory: undefined,
  max: 20,
  maxFileSizeMB: 50,
  disabled: false,
  flat: false
});

const model = defineModel<Api.Project.AttachmentItem[]>({ default: () => [] });

/** 给用户看的简短分类（hint 行展示） */
const ALLOWED_EXTENSIONS_HINT = '支持 PDF、Word、Excel、PPT、TXT/MD/CSV、图片、ZIP/RAR/7Z、MP3/MP4、SQL/JSON/XML';

// 与后端 AttachmentValidator 白/黑名单保持一致（5.16）
const ALLOWED_EXTENSIONS = new Set([
  'pdf',
  'doc',
  'docx',
  'xls',
  'xlsx',
  'ppt',
  'pptx',
  'txt',
  'md',
  'csv',
  'jpg',
  'jpeg',
  'png',
  'gif',
  'webp',
  'bmp',
  'zip',
  'rar',
  '7z',
  'mp4',
  'mp3',
  'sql',
  'xml',
  'json'
]);

const FORBIDDEN_EXTENSIONS = new Set([
  'exe',
  'bat',
  'cmd',
  'sh',
  'ps1',
  'msi',
  'dll',
  'jar',
  'war',
  'php',
  'jsp',
  'asp',
  'aspx',
  'py',
  'rb',
  'pl',
  'com',
  'scr',
  'vbs',
  'js'
]);

interface PendingItem {
  id: string;
  name: string;
}

const pending = ref<PendingItem[]>([]);
const inputRef = ref<HTMLInputElement>();
const isUnmounting = ref(false);

/**
 * 会话级清理账本：
 * - originalIds: 弹层打开时已存在的 fileId（编辑模式下来自 rowData.attachments）。
 *   当前未在 commit/rollback 中直接读取（清理逻辑靠 addedIds 自己判定）；
 *   保留是为了让会话模型完整、便于后续扩展（如"撤销删除""仅删原有附件"等差异行为）。
 * - addedIds: 本次会话内上传成功的 fileId
 * - pendingDeleteIds: 用户在 UI 上点过"删除"的 fileId（含 original 和 added 两类）
 * - committed: commit() 调用后置 true，阻止后续 rollback 误删
 *
 * UI 显示 = model（已减去 pendingDelete 项）
 * 真删时机：commit() 删 pendingDelete；rollback() 删 addedIds（除非 committed）
 */
interface UploadSession {
  originalIds: Set<string>;
  addedIds: Set<string>;
  pendingDeleteIds: Set<string>;
  committed: boolean;
}

const session = reactive<UploadSession>({
  originalIds: new Set<string>(),
  addedIds: new Set<string>(),
  pendingDeleteIds: new Set<string>(),
  committed: false
});

const totalCount = computed(() => model.value.length + pending.value.length);
const isFull = computed(() => totalCount.value >= props.max);
const hasUploading = computed(() => pending.value.length > 0);

const acceptExtensionsList = computed(() => Array.from(ALLOWED_EXTENSIONS).join(', '));

/**
 * 列表区拆成"直接展示"和"折叠浮层"两组：
 * - flat：全部直接展示（适合本身已在 popover 里）
 * - 默认：首项直接展示，>1 时其余进入悬浮浮层
 */
const displayedAttachments = computed(() => (props.flat ? model.value : model.value.slice(0, 1)));
const popoverAttachments = computed(() => (props.flat || model.value.length <= 1 ? [] : model.value.slice(1)));

const IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg']);

function isImage(item: Api.Project.AttachmentItem) {
  if (item.contentType?.startsWith('image/')) {
    return true;
  }
  return IMAGE_EXTENSIONS.has(getExtension(item.name));
}

interface ImagePreviewState {
  visible: boolean;
  urls: string[];
}

const imagePreview = reactive<ImagePreviewState>({
  visible: false,
  urls: []
});

function getExtension(name: string) {
  const idx = name.lastIndexOf('.');
  return idx > 0 ? name.slice(idx + 1).toLowerCase() : '';
}

function validateFile(file: File): string | null {
  if (!file.name) {
    return '文件名为空';
  }
  if (file.name.length > 255) {
    return '文件名超过 255 字符';
  }

  const ext = getExtension(file.name);
  if (!ext) {
    return '文件缺少扩展名';
  }
  if (FORBIDDEN_EXTENSIONS.has(ext)) {
    return `不允许上传 .${ext} 文件`;
  }
  if (!ALLOWED_EXTENSIONS.has(ext)) {
    return `暂不支持 .${ext} 文件`;
  }

  if (file.size > props.maxFileSizeMB * 1024 * 1024) {
    return `单文件不能超过 ${props.maxFileSizeMB}MB`;
  }

  return null;
}

function triggerSelect() {
  if (props.disabled || isFull.value) {
    return;
  }
  inputRef.value?.click();
}

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

  if (files.length === 0) {
    return;
  }

  const remaining = props.max - totalCount.value;
  if (files.length > remaining) {
    window.$message?.warning(`最多还能上传 ${remaining} 个附件`);
    return;
  }

  const validFiles: File[] = [];
  files.forEach(file => {
    const err = validateFile(file);
    if (err) {
      window.$message?.error(`${file.name}：${err}`);
      return;
    }
    validFiles.push(file);
  });

  if (validFiles.length === 0) {
    return;
  }

  await Promise.all(validFiles.map(uploadOne));
}

async function uploadOne(file: File) {
  const tempId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
  pending.value = [...pending.value, { id: tempId, name: file.name }];

  try {
    const result = await uploadFile(file, props.directory);
    if (result.error || !result.data) {
      window.$message?.error(`${file.name}：上传失败`);
      return;
    }

    const { id, configId, path, url } = result.data;

    // 组件已卸载（用户上传过程中关弹层）：onBeforeUnmount 已跑过且看不到这个 id，
    // 这里立刻调删除，避免孤儿文件
    if (isUnmounting.value) {
      deleteFile(id).catch(() => {
        // 已卸载场景下 console.warn 也访问不到 component scope，这里静默吞掉
      });
      return;
    }

    model.value = [
      ...model.value,
      {
        fileId: id,
        url,
        name: file.name,
        size: file.size,
        contentType: file.type || undefined,
        configId,
        path
      }
    ];
    session.addedIds.add(id);
  } finally {
    pending.value = pending.value.filter(item => item.id !== tempId);
  }
}

function handleRemove(item: Api.Project.AttachmentItem) {
  removeAttachmentByFileId(item.fileId);
}

async function fetchAsBlobUrl(item: Api.Project.AttachmentItem) {
  const { data, error } = await downloadFile(item.fileId);
  if (error || !data) {
    window.$message?.error(`${item.name}：加载失败`);
    return null;
  }
  return URL.createObjectURL(data);
}

async function handleDownload(item: Api.Project.AttachmentItem) {
  const blobUrl = await fetchAsBlobUrl(item);
  if (!blobUrl) {
    return;
  }

  const link = document.createElement('a');
  link.href = blobUrl;
  link.download = item.name;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
  URL.revokeObjectURL(blobUrl);
}

async function handlePreviewImage(item: Api.Project.AttachmentItem) {
  const blobUrl = await fetchAsBlobUrl(item);
  if (!blobUrl) {
    return;
  }
  imagePreview.urls = [blobUrl];
  imagePreview.visible = true;
}

function handleClosePreview() {
  imagePreview.urls.forEach(url => URL.revokeObjectURL(url));
  imagePreview.urls = [];
  imagePreview.visible = false;
}

/** 文件名点击的统一入口：图片走预览，其余走下载 */
function handleOpen(item: Api.Project.AttachmentItem) {
  if (isImage(item)) {
    handlePreviewImage(item);
  } else {
    handleDownload(item);
  }
}

/** 把 model 里的某项移除（折叠浮层里也用，不依赖索引） */
function removeAttachmentByFileId(fileId: string) {
  if (props.disabled) {
    return;
  }
  const idx = model.value.findIndex(item => item.fileId === fileId);
  if (idx === -1) {
    return;
  }
  session.pendingDeleteIds.add(fileId);
  model.value = model.value.filter((_, i) => i !== idx);
}

function formatSize(size?: number) {
  if (!size && size !== 0) {
    return '';
  }
  if (size < 1024) {
    return `${size}B`;
  }
  if (size < 1024 * 1024) {
    return `${(size / 1024).toFixed(1)}KB`;
  }
  if (size < 1024 * 1024 * 1024) {
    return `${(size / 1024 / 1024).toFixed(1)}MB`;
  }
  return `${(size / 1024 / 1024 / 1024).toFixed(2)}GB`;
}

/**
 * 删除一批 fileId。fire-and-forget：
 * - 不阻塞 UI；任何失败仅 console.warn
 * - 后端返回 1001003001（文件不存在）视为成功
 */
async function deleteMany(ids: string[]) {
  if (ids.length === 0) {
    return;
  }
  await Promise.allSettled(
    ids.map(async id => {
      const { error } = await deleteFile(id);
      if (error) {
        // eslint-disable-next-line no-console
        console.warn('[BusinessAttachmentUploader] 删除失败（已忽略）', id, error);
      }
    })
  );
}

/** 等关闭弹层时先等再清理。设上限 5s，避免极端网络下 commit/rollback 永久挂起。 */
async function waitForPending(maxWaitMs = 5000) {
  const start = Date.now();
  while (pending.value.length > 0) {
    if (Date.now() - start >= maxWaitMs) {
      // eslint-disable-next-line no-console
      console.warn('[BusinessAttachmentUploader] 等待 pending 上传超时，继续后续清理');
      return;
    }
    // polling: 需要在循环里 await，suppress 即可
    // eslint-disable-next-line no-await-in-loop
    await new Promise<void>(resolve => {
      setTimeout(resolve, 50);
    });
  }
}

defineExpose({
  /**
   * 父组件在【打开弹层并填充 model 之后】调用。
   * 把当前 model 视为 original，清空 added / pendingDelete，重置 committed。
   */
  initSession() {
    session.originalIds = new Set(model.value.map(item => item.fileId));
    session.addedIds.clear();
    session.pendingDeleteIds.clear();
    session.committed = false;
  },

  /**
   * 父组件在【业务保存成功后】调用。
   * 真删 pendingDelete（含 original 和 added 两类）；置 committed 阻止后续 rollback。
   */
  async commit() {
    await waitForPending();
    const ids = Array.from(session.pendingDeleteIds);
    session.pendingDeleteIds.clear();
    session.addedIds.clear();
    session.committed = true;
    await deleteMany(ids);
  },

  /**
   * 父组件取消/关闭时调用；onBeforeUnmount 也会兜底调一次。
   * 真删 addedIds（保留 original）；committed=true 时跳过。
   */
  async rollback() {
    if (session.committed) {
      return;
    }
    await waitForPending();
    const ids = Array.from(session.addedIds);
    session.addedIds.clear();
    session.pendingDeleteIds.clear();
    session.committed = true;
    await deleteMany(ids);
  },

  /** 父组件在提交前可读此值判断是否还有 pending 上传 */
  get hasUploading() {
    return hasUploading.value;
  },

  /**
   * 返回当前已选附件的「永久代理 URL」数组（私有/公开桶都不过期）。
   * 缺 configId/path 时回退 item.url。供反馈等"按 URL 字符串存储"的场景使用。
   */
  getPermanentUrls(): string[] {
    return model.value.map(item =>
      item.configId && item.path ? buildFileProxyUrl(item.configId, item.path) : item.url
    );
  }
});

onBeforeUnmount(() => {
  // 标记卸载中：让正在 flight 的 uploadOne 完成时知道要立刻删除自己
  isUnmounting.value = true;
  // 兜底：用户没显式 rollback 就直接关弹层 / 切路由 / unmount
  // deleteMany 内部已 swallow 单项失败，这里不再 await，fire-and-forget
  if (!session.committed) {
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    deleteMany(Array.from(session.addedIds));
  }
});
</script>

<template>
  <div class="business-attachment-uploader">
    <div v-if="!disabled" class="business-attachment-uploader__trigger">
      <ElButton :icon="Upload" :disabled="isFull" :loading="hasUploading" @click="triggerSelect">点击上传</ElButton>
      <span class="business-attachment-uploader__hint">
        最多 {{ max }} 个，已选 {{ totalCount }} 个；单文件 ≤ {{ maxFileSizeMB }}MB
        <ElTooltip placement="top">
          <template #content>
            <div class="business-attachment-uploader__hint-tooltip">
              <div>{{ ALLOWED_EXTENSIONS_HINT }}</div>
              <div class="business-attachment-uploader__hint-tooltip-ext">允许扩展名：{{ acceptExtensionsList }}</div>
            </div>
          </template>
          <ElIcon class="business-attachment-uploader__hint-icon"><QuestionFilled /></ElIcon>
        </ElTooltip>
      </span>
      <input
        ref="inputRef"
        type="file"
        multiple
        class="business-attachment-uploader__input"
        @change="handleFileChange"
      />
    </div>

    <div v-else-if="totalCount === 0" class="business-attachment-uploader__empty">暂无附件</div>

    <ul v-if="totalCount > 0" class="business-attachment-uploader__list">
      <!-- 直接展示：默认仅首项；flat 模式全部 -->
      <li v-for="item in displayedAttachments" :key="`done-${item.fileId}`" class="business-attachment-uploader__item">
        <ElIcon class="business-attachment-uploader__icon">
          <Picture v-if="isImage(item)" />
          <Document v-else />
        </ElIcon>
        <ElLink
          type="primary"
          underline="never"
          class="business-attachment-uploader__name"
          :title="item.name"
          @click="handleOpen(item)"
        >
          {{ item.name }}
        </ElLink>
        <span v-if="item.size" class="business-attachment-uploader__size">{{ formatSize(item.size) }}</span>
        <ElLink type="primary" underline="never" @click="handleDownload(item)">下载</ElLink>
        <ElButton v-if="!disabled" link type="danger" :icon="Delete" @click="handleRemove(item)" />
      </li>

      <!-- 折叠提示：>1 个时显示，hover 弹完整列表（flat 模式下永不出现） -->
      <li v-if="popoverAttachments.length > 0" class="business-attachment-uploader__more-row">
        <ElPopover
          trigger="hover"
          placement="bottom-start"
          :width="380"
          :show-after="200"
          popper-class="business-attachment-uploader__popover"
        >
          <template #reference>
            <span class="business-attachment-uploader__more">
              还有 {{ popoverAttachments.length }} 个附件
              <ElIcon><ArrowDown /></ElIcon>
            </span>
          </template>
          <ul class="business-attachment-uploader__popover-list">
            <li
              v-for="item in popoverAttachments"
              :key="`popover-${item.fileId}`"
              class="business-attachment-uploader__item"
            >
              <ElIcon class="business-attachment-uploader__icon">
                <Picture v-if="isImage(item)" />
                <Document v-else />
              </ElIcon>
              <ElLink
                type="primary"
                underline="never"
                class="business-attachment-uploader__name"
                :title="item.name"
                @click="handleOpen(item)"
              >
                {{ item.name }}
              </ElLink>
              <span v-if="item.size" class="business-attachment-uploader__size">{{ formatSize(item.size) }}</span>
              <ElLink type="primary" underline="never" @click="handleDownload(item)">下载</ElLink>
              <ElButton v-if="!disabled" link type="danger" :icon="Delete" @click="handleRemove(item)" />
            </li>
          </ul>
        </ElPopover>
      </li>

      <!-- pending 项不折叠：让用户能持续看到上传进度 -->
      <li
        v-for="item in pending"
        :key="`pending-${item.id}`"
        class="business-attachment-uploader__item business-attachment-uploader__item--pending"
      >
        <ElIcon class="business-attachment-uploader__icon is-loading"><Loading /></ElIcon>
        <span class="business-attachment-uploader__name" :title="item.name">{{ item.name }}</span>
        <span class="business-attachment-uploader__status">上传中…</span>
      </li>
    </ul>

    <ElImageViewer
      v-if="imagePreview.visible"
      :url-list="imagePreview.urls"
      hide-on-click-modal
      @close="handleClosePreview"
    />
  </div>
</template>

<style scoped lang="scss">
.business-attachment-uploader {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.business-attachment-uploader__trigger {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

.business-attachment-uploader__hint {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: rgb(100 116 139 / 88%);
  font-size: 12px;
}

.business-attachment-uploader__hint-icon {
  color: rgb(100 116 139 / 88%);
  cursor: help;
  font-size: 14px;
}

.business-attachment-uploader__hint-tooltip {
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-width: 320px;
  line-height: 1.5;
}

.business-attachment-uploader__hint-tooltip-ext {
  word-break: break-all;
  opacity: 0.85;
}

.business-attachment-uploader__empty {
  color: rgb(100 116 139 / 88%);
  font-size: 13px;
}

.business-attachment-uploader__input {
  display: none;
}

.business-attachment-uploader__list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.business-attachment-uploader__item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border: 1px solid var(--el-border-color-lighter);
  border-radius: 4px;
  background: var(--el-fill-color-blank);
  font-size: 13px;

  &--pending {
    background: var(--el-fill-color-light);
    color: rgb(100 116 139 / 88%);
  }
}

.business-attachment-uploader__icon {
  flex: 0 0 auto;
  color: var(--el-color-primary);
}

.business-attachment-uploader__name {
  flex: 1 1 auto;
  min-width: 0;
  justify-content: flex-start;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.business-attachment-uploader__size {
  flex: 0 0 auto;
  color: rgb(100 116 139 / 88%);
  font-size: 12px;
}

.business-attachment-uploader__status {
  flex: 0 0 auto;
  color: rgb(100 116 139 / 88%);
  font-size: 12px;
}

.business-attachment-uploader__more-row {
  display: flex;
  align-items: center;
  padding: 2px 0;
}

.business-attachment-uploader__more {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  color: var(--el-color-primary);
  font-size: 13px;
  cursor: pointer;
  user-select: none;

  &:hover {
    text-decoration: underline;
  }
}
</style>

<style lang="scss">
// 浮层非 scoped：popper 渲染到 body
.business-attachment-uploader__popover {
  padding: 8px 4px !important;

  .business-attachment-uploader__popover-list {
    display: flex;
    flex-direction: column;
    gap: 4px;
    max-height: 280px;
    margin: 0;
    padding: 0;
    list-style: none;
    overflow-y: auto;
  }

  .business-attachment-uploader__item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 8px;
    border-radius: 4px;
    font-size: 13px;

    &:hover {
      background: var(--el-fill-color-light);
    }
  }

  .business-attachment-uploader__icon {
    flex: 0 0 auto;
    color: var(--el-color-primary);
  }

  .business-attachment-uploader__name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .business-attachment-uploader__size {
    flex: 0 0 auto;
    color: rgb(100 116 139 / 88%);
    font-size: 12px;
  }
}
</style>
