<template>
  <div class="attachment-list-panel unified-shell">
    <template v-if="displayMode === 'table'">
      <div v-if="showHeader" class="header">
        <span>{{ headerTitle }}</span>
        <div v-if="canShowUploader && tableUploadGroup" class="immediate-upload">
          <el-upload
            :http-request="uploadImmediate"
            :show-file-list="false"
            :limit="1"
            :disabled="isImmediateUploading"
            :auto-upload="true"
          >
            <el-button type="primary" :loading="isImmediateUploading">{{ TEXT.common.actions.upload }}</el-button>
          </el-upload>
          <el-progress v-if="tableProgress > 0 && tableProgress < 100" :percentage="tableProgress" :stroke-width="6" />
        </div>
      </div>
      <el-table :data="attachments" v-loading="loading" style="width: 100%" class="attachment-table" table-layout="fixed">
        <el-table-column v-if="hasEntityGroups" label="附件类型" min-width="180" show-overflow-tooltip>
          <template #default="scope">{{ scope.row.attachment_type_label || TEXT.common.fallback }}</template>
        </el-table-column>
        <el-table-column prop="filename" :label="TEXT.common.labels.filename" min-width="360" show-overflow-tooltip />
        <el-table-column :label="TEXT.common.labels.size" min-width="180">
          <template #default="scope">{{ formatFileSize(scope.row.file_size ?? scope.row.size) }}</template>
        </el-table-column>
        <el-table-column :label="TEXT.common.labels.uploader" min-width="180" show-overflow-tooltip>
          <template #default="scope">
            {{ uploaderLabel(scope.row) }}
          </template>
        </el-table-column>
        <el-table-column prop="uploaded_at" :label="TEXT.common.labels.uploadedAt" min-width="180" show-overflow-tooltip>
          <template #default="scope">
            {{ displayDateTime(scope.row.uploaded_at) }}
          </template>
        </el-table-column>
        <el-table-column :label="TEXT.common.labels.actions" min-width="180">
          <template #default="scope">
            <div class="attachment-actions">
              <el-button link type="primary" size="small" @click="preview(scope.row)">
                {{ TEXT.common.labels.preview }}
              </el-button>
              <el-button link type="primary" size="small" @click="download(scope.row)">{{ TEXT.common.actions.download }}</el-button>
              <el-button
                v-if="canDelete(scope.row)"
                link
                type="danger"
                size="small"
                @click="remove(scope.row)"
              >
                {{ TEXT.common.actions.delete }}
              </el-button>
            </div>
          </template>
        </el-table-column>
      </el-table>
    </template>
    <div v-else class="upload-grid">
      <div v-for="group in uploadGroups" :key="group.key" class="attachment-upload-card">
        <div v-if="showUploadCardLabels" class="upload-card-label">{{ group.label }}</div>
        <el-upload
          :show-file-list="false"
          multiple
          :disabled="!canUploadGroup(group) || isUploading(group.key)"
          :auto-upload="false"
          :on-change="(file: any) => queuePendingUpload(group.key, file)"
        >
          <div class="upload-trigger" :class="{ 'is-disabled': !canUploadGroup(group) }">
            <el-icon class="upload-icon"><Document /></el-icon>
            <span class="upload-text">点击上传文件</span>
          </div>
        </el-upload>
        <div v-if="pendingMap[group.key]?.length" class="pending-list">
          <div
            v-for="item in pendingMap[group.key]"
            :key="pendingFileKey(item.file)"
            class="pending-file"
            :class="{ 'is-failed': item.status === 'failed' }"
          >
            <span class="pending-file-name">{{ item.file.name }}</span>
            <span v-if="item.status === 'failed'" class="pending-file-status">
              {{ item.error || TEXT.common.messages.uploadFailed }}
            </span>
            <el-button link type="danger" size="small" @click="removePendingUpload(group.key, item)">
              {{ TEXT.common.actions.remove }}
            </el-button>
          </div>
        </div>
        <el-progress
          v-if="progressMap[group.key] && progressMap[group.key] < 100"
          :percentage="progressMap[group.key]"
          :stroke-width="6"
        />
      </div>
    </div>
  </div>

  <el-dialog v-if="previewVisible" append-to-body v-model="previewVisible" :title="previewTitle" width="720px" :close-on-click-modal="false">
    <div v-if="previewError" class="preview-error">
      {{ previewError }}
    </div>
    <template v-else>
      <img v-if="previewType === 'image'" :src="previewUrl" class="preview-media" />
      <iframe v-else-if="previewType === 'pdf'" :src="previewUrl" class="preview-frame" />
      <div v-else class="preview-error">
        {{ TEXT.common.messages.previewNotSupported }}
      </div>
    </template>
  </el-dialog>
</template>

<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Document } from "@element-plus/icons-vue";
import { fetchAttachments, deleteAttachment, uploadAttachment } from "../../api/attachments";
import { formatFileSize } from "./attachmentUtils";
import { useAuthStore } from "../../store/auth";
import { useStudyStore } from "../../store/study";
import { isSystemAdmin } from "../../utils/roles";
import { displayDateTime, getUserDisplayName } from "../../utils/display";
import { getAttachmentPermissionKey } from "../../utils/attachmentPermissions";
import { isApiPermissionAllowed } from "../../utils/apiPermissionValue";
import { TEXT } from "../../locales";

type AttachmentEntityGroup = {
  entityType: string;
  label: string;
};

type UploadGroup = {
  key: string;
  label: string;
  entityType: string;
};

type PendingUploadStatus = "pending" | "uploading" | "failed";

type PendingUploadItem = {
  file: File;
  status: PendingUploadStatus;
  error?: string;
};

const props = defineProps<{
  studyId: string;
  entityType: string;
  entityId: string;
  entityGroups?: AttachmentEntityGroup[];
  title?: string;
  readonly?: boolean;
  hideUploader?: boolean;
  mode?: "table" | "upload";
  maxSizeMb?: number;
  refreshKey?: number;
}>();

const attachments = ref<any[]>([]);
const loading = ref(false);
const progressMap = reactive<Record<string, number>>({});
const pendingMap = reactive<Record<string, PendingUploadItem[]>>({});
const auth = useAuthStore();
const study = useStudyStore();
const previewVisible = ref(false);
const previewUrl = ref("");
const previewObjectUrl = ref("");
const previewType = ref<"image" | "pdf" | "other">("other");
const previewTitle = ref(TEXT.common.labels.preview);
const previewError = ref("");
const previewLoading = ref(false);
const headerTitle = computed(() => props.title || TEXT.common.labels.attachments);
const displayMode = computed(() => props.mode || "table");
const maxSize = computed(() => props.maxSizeMb ?? 50);
const hasEntityGroups = computed(() => !!props.entityGroups?.length);
const uploadGroups = computed<UploadGroup[]>(() => {
  if (props.entityGroups?.length) {
    return props.entityGroups.map((group) => ({
      key: group.entityType,
      label: group.label,
      entityType: group.entityType,
    }));
  }
  return [{ key: props.entityType, label: headerTitle.value, entityType: props.entityType }];
});
const showUploadCardLabels = computed(() => uploadGroups.value.length > 1);

const currentRolePermissions = computed(() => {
  const role = study.currentStudyRole || (study.currentStudy as any)?.role_in_study;
  return role ? study.currentPermissions?.[role] : null;
});

const canUseAttachmentPermission = (action: "create" | "read" | "delete", entityType = props.entityType) => {
  if (isSystemAdmin(auth.user)) return true;
  const operationKey = getAttachmentPermissionKey(entityType, action);
  if (!operationKey) return false;
  return isApiPermissionAllowed(currentRolePermissions.value?.[operationKey]);
};

const canCreate = computed(() => {
  if (props.readonly) return false;
  if (displayMode.value === "upload") {
    return uploadGroups.value.some((group) => canUseAttachmentPermission("create", group.entityType));
  }
  return !hasEntityGroups.value && canUseAttachmentPermission("create");
});
const canShowUploader = computed(() => !props.hideUploader && canCreate.value);
const showHeader = computed(() => !!props.title || canShowUploader.value);
const canUploadGroup = (group: UploadGroup) => canShowUploader.value && canUseAttachmentPermission("create", group.entityType);
const isUploading = (key: string) => (progressMap[key] || 0) > 0 && (progressMap[key] || 0) < 100;
const tableUploadGroup = computed(() => uploadGroups.value[0] || null);
const tableProgress = computed(() => {
  const group = tableUploadGroup.value;
  return group ? progressMap[group.key] || 0 : 0;
});
const isImmediateUploading = computed(() => tableProgress.value > 0 && tableProgress.value < 100);

const validateFile = (file: File) => {
  if (file.size > maxSize.value * 1024 * 1024) {
    ElMessage.error(`${TEXT.common.messages.fileTooLarge} ${maxSize.value}MB`);
    return false;
  }
  return true;
};

const pendingFileKey = (file: File) => `${file.name}-${file.size}-${file.lastModified}`;

const queuePendingUpload = (fileType: string, uploadFile: any) => {
  const group = uploadGroups.value.find((item) => item.key === fileType);
  if (!group || !canUploadGroup(group)) return;
  const file = uploadFile?.raw as File | undefined;
  if (!file || !validateFile(file)) return;
  const next = pendingMap[fileType] || [];
  if (!next.some((item) => pendingFileKey(item.file) === pendingFileKey(file))) {
    pendingMap[fileType] = [...next, { file, status: "pending" }];
  }
};

const removePendingUpload = (fileType: string, item: PendingUploadItem) => {
  pendingMap[fileType] = (pendingMap[fileType] || []).filter((current) => pendingFileKey(current.file) !== pendingFileKey(item.file));
};

const pendingSnapshot = () =>
  uploadGroups.value.flatMap((group) => (pendingMap[group.key] || []).map((item) => `${group.key}:${pendingFileKey(item.file)}`));
const hasPendingUploads = computed(() => uploadGroups.value.some((group) => (pendingMap[group.key] || []).length > 0));

const uploadFile = async (group: UploadGroup, file: File, targetEntityId: string) => {
  if (!props.studyId || !targetEntityId) return;
  if (!validateFile(file)) throw new Error("invalid file");
  progressMap[group.key] = 0;
  await uploadAttachment(props.studyId, group.entityType, targetEntityId, file, {
    onUploadProgress: (evt: ProgressEvent) => {
      if (evt.total) {
        progressMap[group.key] = Math.round((evt.loaded / evt.total) * 100);
      }
    },
  });
};

const uploadPending = async (entityId?: string) => {
  const targetEntityId = entityId || props.entityId;
  if (!targetEntityId && hasPendingUploads.value) {
    throw new Error(TEXT.common.messages.uploadFailed);
  }
  if (!targetEntityId) return;
  let hasFailure = false;
  for (const group of uploadGroups.value) {
    const items = [...(pendingMap[group.key] || [])];
    const remainItems: PendingUploadItem[] = [];
    for (const item of items) {
      try {
        item.status = "uploading";
        item.error = "";
        await uploadFile(group, item.file, targetEntityId);
      } catch (e: any) {
        hasFailure = true;
        item.status = "failed";
        item.error = e?.response?.data?.message || TEXT.common.messages.uploadFailed;
        remainItems.push(item);
      } finally {
        progressMap[group.key] = 0;
      }
    }
    pendingMap[group.key] = remainItems;
  }
  if (hasFailure) throw new Error(TEXT.common.messages.uploadFailed);
};

const uploadImmediate = async (options: any) => {
  const file = options?.file as File | undefined;
  const group = tableUploadGroup.value;
  if (!file || !group || !props.entityId) return;
  if (!validateFile(file)) return;
  try {
    await uploadFile(group, file, props.entityId);
    ElMessage.success(TEXT.common.messages.uploadSuccess);
    load();
  } catch (e: any) {
    ElMessage.error(e?.response?.data?.message || e?.message || TEXT.common.messages.uploadFailed);
  } finally {
    progressMap[group.key] = 0;
  }
};

const load = async () => {
  if (!props.studyId || !props.entityId) return;
  loading.value = true;
  try {
    if (hasEntityGroups.value && props.entityGroups) {
      const results = await Promise.all(
        props.entityGroups.map(async (group) => {
          const { data } = await fetchAttachments(props.studyId, group.entityType, props.entityId);
          const items = data.items || data || [];
          return (Array.isArray(items) ? items : []).map((item: any) => ({
            ...item,
            attachment_type_label: group.label,
            attachment_entity_type: group.entityType,
          }));
        })
      );
      attachments.value = results.flat();
    } else {
      const { data } = await fetchAttachments(props.studyId, props.entityType, props.entityId);
      attachments.value = data.items || data || [];
    }
  } catch (e: any) {
    ElMessage.error(e?.response?.data?.message || TEXT.common.messages.loadFailed);
  } finally {
    loading.value = false;
  }
};

const uploaderLabel = (row: any) => {
  if (row?.uploaded_by && typeof row.uploaded_by === "object") {
    return getUserDisplayName(row.uploaded_by) || row.uploaded_by.id || TEXT.common.fallback;
  }
  return row.uploaded_by_id || row.uploaded_by || TEXT.common.fallback;
};

const getDownloadUrl = (row: any) => {
  const token = localStorage.getItem("ctms_token");
  return row?.url || (token ? `/api/v1/attachments/${row.id}/download?token=${token}` : `/api/v1/attachments/${row.id}/download`);
};

const getPreviewUrl = (row: any) => {
  const token = localStorage.getItem("ctms_token");
  return token ? `/api/v1/attachments/${row.id}/preview?token=${token}` : `/api/v1/attachments/${row.id}/preview`;
};

const download = (row: any) => {
  const url = getDownloadUrl(row);
  const link = document.createElement("a");
  link.href = url;
  link.download = row?.filename || "download";
  link.rel = "noopener";
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
};

const getFileName = (row: any) => (row?.filename || "").toLowerCase();
const getMimeType = (row: any) => (row?.mime_type || row?.content_type || "").toLowerCase();

const detectPreviewType = (row: any) => {
  const mime = getMimeType(row);
  if (mime.startsWith("image/")) return "image";
  if (mime === "application/pdf") return "pdf";
  const name = getFileName(row);
  if ([".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg"].some((ext) => name.endsWith(ext))) return "image";
  if (name.endsWith(".pdf")) return "pdf";
  return "other";
};

const previewImage = async (downloadUrl: string) => {
  previewLoading.value = true;
  try {
    const token = localStorage.getItem("ctms_token");
    const headers: Record<string, string> = {};
    if (token) headers.Authorization = `Bearer ${token}`;
    const response = await fetch(downloadUrl, { headers });
    if (!response.ok) throw new Error("preview failed");
    const blob = await response.blob();
    if (previewObjectUrl.value) URL.revokeObjectURL(previewObjectUrl.value);
    previewObjectUrl.value = URL.createObjectURL(blob);
    previewUrl.value = previewObjectUrl.value;
  } catch {
    previewUrl.value = downloadUrl;
  } finally {
    previewLoading.value = false;
  }
};

const preview = (row: any) => {
  previewError.value = "";
  previewType.value = detectPreviewType(row);
  previewTitle.value = row?.filename || TEXT.common.labels.preview;
  const downloadUrl = getDownloadUrl(row);
  if (previewType.value === "image") {
    previewUrl.value = "";
    previewImage(downloadUrl);
  } else if (previewType.value === "pdf") {
    previewUrl.value = getPreviewUrl(row);
  } else {
    previewUrl.value = downloadUrl;
  }
  if (previewType.value === "other") {
    previewError.value = TEXT.common.messages.previewNotSupported;
  }
  previewVisible.value = true;
};

const canDelete = (row: any) => {
  if (props.readonly) return false;
  return canUseAttachmentPermission("delete", row?.attachment_entity_type || props.entityType);
};

const remove = async (row: any) => {
  const ok = await ElMessageBox.confirm(TEXT.common.confirm.delete, TEXT.common.labels.tips).catch(() => null);
  if (!ok) return;
  try {
    await deleteAttachment(row.id);
    ElMessage.success(TEXT.common.messages.deleteSuccess);
    load();
  } catch (e: any) {
    ElMessage.error(e?.response?.data?.message || TEXT.common.messages.deleteFailed);
  }
};

onMounted(() => {
  load();
});

watch(previewVisible, (visible) => {
  if (!visible && previewObjectUrl.value) {
    URL.revokeObjectURL(previewObjectUrl.value);
    previewObjectUrl.value = "";
  }
});

watch(
  () => props.refreshKey,
  () => {
    load();
  }
);

defineExpose({
  pendingSnapshot,
  uploadPending,
});
</script>

<style scoped>
.attachment-list-panel {
  background: #ffffff;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 16px;
  border-bottom: 1px solid #eaf0f8;
}

.attachment-table :deep(.el-table__inner-wrapper::before) {
  display: none;
}

.attachment-actions {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  gap: 4px;
  white-space: nowrap;
}

.attachment-actions :deep(.el-button) {
  margin-left: 0;
  padding: 0 2px;
}

.immediate-upload {
  display: flex;
  gap: 8px;
  align-items: center;
}

.upload-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
}

.attachment-upload-card {
  min-height: 72px;
  padding: 14px 16px;
  border: 1px dashed #d0dced;
  border-radius: 8px;
  background: #ffffff;
  transition: border-color 0.2s ease, background-color 0.2s ease;
}

.attachment-upload-card:hover {
  border-color: var(--ctms-primary);
  background: #f8faff;
}

.upload-card-label {
  margin-bottom: 10px;
  color: #4a6283;
  font-size: 13px;
  font-weight: 600;
}

.upload-trigger {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 34px;
  cursor: pointer;
}

.upload-trigger.is-disabled {
  cursor: not-allowed;
  opacity: 0.62;
}

.upload-icon {
  font-size: 18px;
  line-height: 1;
}

.upload-text {
  color: var(--ctms-text-secondary);
  font-size: 13px;
}

.upload-trigger:not(.is-disabled):hover .upload-text {
  color: var(--ctms-primary);
}

.pending-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 8px;
}

.pending-file {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  color: var(--ctms-text-regular);
  font-size: 12px;
}

.pending-file.is-failed {
  color: var(--ctms-danger, #f56c6c);
}

.pending-file-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.pending-file-status {
  flex: 0 0 auto;
  color: var(--ctms-danger, #f56c6c);
  font-size: 12px;
  white-space: nowrap;
}

@media (max-width: 640px) {
  .upload-grid {
    grid-template-columns: 1fr;
  }
}

.preview-frame {
  width: 100%;
  height: 520px;
  border: none;
}

.preview-media {
  max-width: 100%;
  max-height: 520px;
  display: block;
  margin: 0 auto;
}

.preview-error {
  color: var(--ctms-text-secondary);
  font-size: 13px;
  padding: 12px 0;
  text-align: center;
}
</style>
