<template>
  <div class="mod-weather__prediction" v-loading="state.loading">
    <div class="page-header">
      <div>
        <h2 class="page-title">中尺度模式回波预报</h2>
      </div>
      <div class="page-header-actions">
        <el-tag type="success">文件 {{ state.fileItems.length }}</el-tag>
        <el-button type="primary" plain icon="Refresh" @click="loadData">刷新</el-button>
      </div>
    </div>

    <el-alert v-if="state.errorMessage" :title="state.errorMessage" type="error" :closable="false" show-icon class="mb-16" />

    <div class="page-body" v-if="state.fileItems.length">
      <aside class="file-list-panel">
        <div v-for="(item, index) in state.fileItems" :key="item.anchorId" class="file-list-item" :class="{ 'is-active': state.selectedFileIndex === index }" @click="selectFile(index)">
          <div class="file-list-item__thumb">
            <img v-if="blobUrlCache[item.fileId]" :src="blobUrlCache[item.fileId]" class="file-list-item__img" :alt="item.displayName" />
            <span v-else-if="isImageFile(item.type)" class="file-list-item__icon file-list-item__icon--img">GIF</span>
            <span v-else class="file-list-item__icon file-list-item__icon--other">—</span>
          </div>
          <span class="file-list-item__name">{{ item.displayName || `文件-${item.fileId}` }}</span>
        </div>
      </aside>

      <section class="preview-panel" v-if="previewItem">
        <div class="preview-panel__header">
          <h3 class="preview-panel__title">{{ previewItem.displayName || `文件-${previewItem.fileId}` }}</h3>
          <div class="preview-panel__actions">
            <el-button type="primary" plain size="small" :loading="state.copyingId === previewItem.anchorId" @click="copyImage(previewItem)"> 复制图片 </el-button>
            <el-button type="success" plain size="small" @click="downloadFile(previewItem)"> 下载原图 </el-button>
            <el-button type="warning" plain size="small" @click="openFullscreen"> 全屏预览 </el-button>
          </div>
        </div>

        <div class="preview-panel__body">
          <div v-if="!previewBlobUrl && selectedFileId" class="preview-panel__loading">加载中...</div>
          <div v-show="previewBlobUrl" class="preview-zoom-area">
            <img :src="previewBlobUrl" :alt="previewItem.displayName" class="preview-panel__img" draggable="false" @click="openFullscreen" />
          </div>
        </div>
      </section>

      <div class="preview-panel preview-panel--empty" v-else>
        <el-empty description="请在左侧选择要预览的文件" :image-size="120" />
      </div>
    </div>

    <el-empty v-else-if="!state.loading && !state.errorMessage" description="暂无可供展示的文件" />

    <!-- 全屏预览 -->
    <teleport to="body">
      <div v-if="fullscreenVisible" class="fullscreen-overlay" @click="closeFullscreen">
        <div class="fullscreen-overlay__toolbar">
          <span class="fullscreen-overlay__hint">滚轮缩放 · 拖拽移动 · 双击重置</span>
          <el-button circle size="large" @click="closeFullscreen">✕</el-button>
        </div>
        <img
          :src="previewBlobUrl"
          :alt="previewItem?.displayName"
          :style="fsZoomStyle"
          class="fullscreen-overlay__img"
          @click.stop
          @wheel.prevent="fsOnWheel"
          @mousedown="fsOnPanStart"
          @dblclick="fsResetZoom"
          draggable="false"
        />
      </div>
    </teleport>
  </div>
</template>

<script lang="ts" setup>
import app from "@/constants/app";
import { getToken } from "@/utils/cache";
import baseService from "@/service/baseService";
import { ElMessage } from "element-plus";
import axios from "axios";
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";

interface ModelFileItemVO {
  type: string;
  display_name: string;
  file_name: string;
  file_id: string;
}

interface ModelFileGroupVO {
  type: string;
  file_list: ModelFileItemVO[];
}

interface FileItem {
  type: string;
  displayName: string;
  fileId: string;
  anchorId: string;
}

const state = reactive({
  loading: true,
  errorMessage: "",
  fileItems: [] as FileItem[],
  copyingId: "",
  selectedFileIndex: 0
});

const blobUrlCache = {} as Record<string, string>;
const blobPromises = {} as Record<string, Promise<string> | undefined>;
const previewBlobUrl = ref("");
const selectedFileId = ref("");

const IMAGE_EXTS = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg"];

const isImageFile = (type: string) => {
  if (!type) return true;
  const lower = type.toLowerCase();
  return IMAGE_EXTS.some((ext) => lower.includes(ext));
};

const previewItem = computed(() => {
  if (state.selectedFileIndex >= 0 && state.selectedFileIndex < state.fileItems.length) {
    return state.fileItems[state.selectedFileIndex];
  }
  return state.fileItems.length > 0 ? state.fileItems[0] : null;
});

const loadBlobUrl = (fileId: string): Promise<string> => {
  if (blobUrlCache[fileId]) return Promise.resolve(blobUrlCache[fileId]);
  if (blobPromises[fileId]) return blobPromises[fileId];

  const base = (app.api || "").replace(/\/$/, "");
  const url = `${base}/filescan/file/display/${fileId}`;
  const token = getToken();
  const headers: Record<string, string> = {};
  if (token) headers.token = token;

  const promise = axios
    .get(url, { responseType: "blob", headers })
    .then((res) => {
      const objectUrl = URL.createObjectURL(res.data as Blob);
      blobUrlCache[fileId] = objectUrl;
      return objectUrl;
    })
    .finally(() => {
      delete blobPromises[fileId];
    });

  blobPromises[fileId] = promise;
  return promise;
};

const selectFile = async (index: number) => {
  state.selectedFileIndex = index;
  const item = state.fileItems[index];
  if (!item) return;

  selectedFileId.value = item.fileId;
  previewBlobUrl.value = "";

  previewBlobUrl.value = await loadBlobUrl(item.fileId);
};

// 全屏预览
const fullscreenVisible = ref(false);

const fsZoomScale = ref(1);
const fsPanX = ref(0);
const fsPanY = ref(0);
const fsIsPanning = ref(false);
let fsPanStartX = 0;
let fsPanStartY = 0;
let fsPanOriginX = 0;
let fsPanOriginY = 0;

const fsZoomStyle = computed(() => ({
  transform: `translate(${fsPanX.value}px, ${fsPanY.value}px) scale(${fsZoomScale.value})`,
  cursor: fsIsPanning.value ? "grabbing" : fsZoomScale.value > 1 ? "grab" : "default",
  transition: fsIsPanning.value ? "none" : "transform 0.15s ease"
}));

const fsResetZoom = () => {
  fsZoomScale.value = 1;
  fsPanX.value = 0;
  fsPanY.value = 0;
};

const openFullscreen = () => {
  fsResetZoom();
  fullscreenVisible.value = true;
};

const closeFullscreen = () => {
  fullscreenVisible.value = false;
};

const fsOnWheel = (e: WheelEvent) => {
  e.preventDefault();
  const delta = e.deltaY > 0 ? -0.15 : 0.15;
  const newScale = Math.max(0.2, Math.min(8, fsZoomScale.value + delta));
  fsZoomScale.value = newScale;
  if (newScale <= 1) {
    fsPanX.value = 0;
    fsPanY.value = 0;
  }
};

const fsOnPanStart = (e: MouseEvent) => {
  if (fsZoomScale.value <= 1) return;
  fsIsPanning.value = true;
  fsPanStartX = e.clientX;
  fsPanStartY = e.clientY;
  fsPanOriginX = fsPanX.value;
  fsPanOriginY = fsPanY.value;
  document.addEventListener("mousemove", fsOnPanMove);
  document.addEventListener("mouseup", fsOnPanEnd);
};

const fsOnPanMove = (e: MouseEvent) => {
  if (!fsIsPanning.value) return;
  fsPanX.value = fsPanOriginX + (e.clientX - fsPanStartX);
  fsPanY.value = fsPanOriginY + (e.clientY - fsPanStartY);
};

const fsOnPanEnd = () => {
  fsIsPanning.value = false;
  document.removeEventListener("mousemove", fsOnPanMove);
  document.removeEventListener("mouseup", fsOnPanEnd);
};

const downloadFile = async (item: FileItem) => {
  const filename = item.displayName || `file-${item.fileId}`;
  await loadBlobUrl(item.fileId);
  const a = document.createElement("a");
  a.href = blobUrlCache[item.fileId];
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
};

const copyImage = async (item: FileItem) => {
  if (!isImageFile(item.type)) {
    ElMessage.warning("当前文件不是图片类型，无法复制");
    return;
  }
  state.copyingId = item.anchorId;
  try {
    await loadBlobUrl(item.fileId);
    const blobUrl = blobUrlCache[item.fileId];
    const res = await fetch(blobUrl);
    const blob = await res.blob();
    try {
      await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
    } catch (e: any) {
      if (e.name === "NotAllowedError" || e.message?.includes?.("not supported")) {
        await convertToPngAndCopy(blob);
      } else {
        throw e;
      }
    }
    ElMessage.success("图片已复制成功");
  } catch (error: any) {
    ElMessage.error(error?.message || "复制图片失败");
  } finally {
    state.copyingId = "";
  }
};

const convertToPngAndCopy = (blob: Blob): Promise<void> => {
  return new Promise((resolve, reject) => {
    const url = URL.createObjectURL(blob);
    const img = new Image();
    img.onload = () => {
      URL.revokeObjectURL(url);
      const canvas = document.createElement("canvas");
      canvas.width = img.naturalWidth;
      canvas.height = img.naturalHeight;
      const ctx = canvas.getContext("2d");
      if (!ctx) return reject(new Error("Canvas 不可用"));
      ctx.drawImage(img, 0, 0);
      canvas.toBlob((pngBlob) => {
        if (pngBlob) {
          navigator.clipboard
            .write([new ClipboardItem({ "image/png": pngBlob })])
            .then(resolve)
            .catch(reject);
        } else {
          reject(new Error("转 PNG 失败"));
        }
      }, "image/png");
    };
    img.onerror = () => {
      URL.revokeObjectURL(url);
      reject(new Error("图片加载失败"));
    };
    img.src = url;
  });
};

const loadData = async () => {
  state.loading = true;
  state.errorMessage = "";
  try {
    const res = await baseService.get("/filescan/record/model");
    const data = res.data as ModelFileGroupVO;
    const items: FileItem[] = (data.file_list || []).map((file) => ({
      type: file.type || "",
      displayName: file.display_name || file.file_name || "",
      fileId: String(file.file_id),
      anchorId: `pred-${file.file_id}`
    }));

    state.fileItems = items;
    state.selectedFileIndex = 0;

    const preloads = items.filter((item) => isImageFile(item.type)).map((item) => loadBlobUrl(item.fileId));
    if (items.length > 0) {
      selectFile(0);
    }
    await Promise.allSettled(preloads);
  } catch (error: any) {
    state.errorMessage = error?.message || "加载模式预报文件失败";
    state.fileItems = [];
  } finally {
    state.loading = false;
  }
};

onUnmounted(() => {
  Object.values(blobUrlCache).forEach((url) => URL.revokeObjectURL(url));
});

onMounted(() => {
  loadData();
});
</script>

<style scoped>
.mod-weather__prediction {
  min-height: calc(100vh - 140px);
}

.page-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 16px;
  margin-bottom: 16px;
}

.page-title {
  margin: 0;
  font-size: 24px;
  color: #303133;
}

.page-header-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.mb-16 {
  margin-bottom: 16px;
}

/* ==== 主体两栏 ==== */
.page-body {
  display: grid;
  grid-template-columns: 25% minmax(0, 1fr);
  gap: 12px;
}

/* ==== 左侧文件列表 ==== */
.file-list-panel {
  position: sticky;
  top: 16px;
  background: #fff;
  border: 1px solid #ebeef5;
  border-radius: 8px;
  padding: 4px;
  max-height: calc(100vh - 220px);
  overflow-y: auto;
}

.file-list-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  font-size: 13px;
  color: #606266;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}

.file-list-item:hover {
  background: #f5f7fa;
}

.file-list-item.is-active {
  background: var(--el-color-primary-light-9);
  color: var(--el-color-primary);
  font-weight: 500;
}

.file-list-item__thumb {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 6px;
  overflow: hidden;
  background: #f0f2f5;
  display: flex;
  align-items: center;
  justify-content: center;
}

.file-list-item__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.file-list-item__icon {
  font-size: 12px;
  font-weight: 600;
  color: #fff;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.file-list-item__icon--img {
  background: var(--el-color-success);
}

.file-list-item__icon--other {
  background: #c0c4cc;
}

.file-list-item__name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: 1.5;
}

/* ==== 右侧预览面板 ==== */
.preview-panel {
  background: #fff;
  border: 1px solid #ebeef5;
  border-radius: 10px;
  padding: 20px;
}

.preview-panel--empty {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 400px;
}

.preview-panel__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  margin-bottom: 16px;
}

.preview-panel__actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

.preview-panel__title {
  margin: 0;
  font-size: 20px;
  color: #303133;
  word-break: break-all;
}

.preview-panel__body {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.preview-panel__loading {
  text-align: center;
  color: #909399;
  padding: 60px 0;
  font-size: 14px;
}

.preview-zoom-area {
  overflow: hidden;
  border-radius: 8px;
  background: #f5f7fa;
  min-height: 200px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.preview-panel__img {
  display: block;
  max-width: 100%;
  max-height: 70vh;
  border-radius: 8px;
  cursor: pointer;
}

/* ==== 全屏预览遮罩 ==== */
.fullscreen-overlay {
  position: fixed;
  inset: 0;
  z-index: 3000;
  background: rgba(0, 0, 0, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
}

.fullscreen-overlay__toolbar {
  position: absolute;
  top: 20px;
  right: 20px;
  z-index: 3001;
  display: flex;
  align-items: center;
  gap: 16px;
}

.fullscreen-overlay__hint {
  color: rgba(255, 255, 255, 0.55);
  font-size: 13px;
  user-select: none;
}

.fullscreen-overlay__img {
  display: block;
  max-width: 95vw;
  max-height: 95vh;
  object-fit: contain;
  border-radius: 4px;
  user-select: none;
}

/* ==== 响应式 ==== */
@media (max-width: 992px) {
  .page-body {
    grid-template-columns: 1fr;
  }

  .file-list-panel {
    position: static;
    max-height: 200px;
  }
}

@media (max-width: 768px) {
  .page-header,
  .preview-panel__header,
  .preview-panel__actions {
    flex-direction: column;
    align-items: stretch;
  }
}
</style>
