<template>
  <div class="table-box classify-management">
    <ProTable
      ref="proTable"
      :columns="columns"
      :request-api="getTableList"
      :init-param="initParam"
      :pagination="false"
      @drag-sort="onDragSortEnd"
    >
      <template #tableHeader>
        <div class="table-header">
          <div class="table-header-left">
            <el-button type="primary" @click="handleNew"> 新建 </el-button>
            <span v-if="sortSaving" class="sort-saving-tip">正在保存排序…</span>
            <span v-else class="sort-tip">拖动「排序」列图标可调整顺序，松手后自动保存</span>
          </div>
        </div>
      </template>
      <template #isDisplay="{ row }">
        <span :class="row.isDisplay === 1 ? 'status-show' : 'status-hide'">
          {{ row.isDisplay === 1 ? "显示" : "隐藏" }}
        </span>
      </template>
      <template #operation="scope">
        <el-button link type="primary" @click="handleEdit(scope.row)"> 编辑 </el-button>
        <el-button link type="primary" @click="handleDelete(scope.row)"> 删除 </el-button>
      </template>
    </ProTable>

    <!-- 新建/编辑弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      :title="editId == null ? '新建分类' : '编辑分类'"
      width="480px"
      class="classify-edit-dialog"
      append-to-body
      destroy-on-close
      @close="onDialogClose"
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="160px"
        require-asterisk-position="right"
        v-loading="detailLoading"
      >
        <el-form-item label="分类名称" prop="name" required>
          <el-input v-model="form.name" placeholder="请输入" clearable maxlength="20" show-word-limit />
        </el-form-item>
        <el-form-item label="平面图" prop="planeImageUrls" required>
          <div class="plane-upload-wrap">
            <el-upload
              v-model:file-list="form.planeImageFileList"
              list-type="picture-card"
              :limit="9"
              :http-request="handlePlaneImageUpload"
              :on-remove="onPlaneImageRemove"
              :on-preview="onPlaneImagePreview"
              accept="image/*"
              multiple
            >
              <el-icon><Plus /></el-icon>
            </el-upload>
            <div class="upload-tip">({{ form.planeImageUrls.length }}/9)</div>
          </div>
        </el-form-item>
        <el-form-item label="是否显示">
          <el-radio-group v-model="form.isDisplay">
            <el-radio :value="1"> 显示 </el-radio>
            <el-radio :value="0"> 隐藏 </el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="最长预订时间(分钟)" prop="maxBookingTime" required>
          <el-input
            :model-value="String(form.maxBookingTime || '')"
            placeholder="请输入"
            style="width: 100%"
            @update:model-value="
              (val: string) => {
                const n = parseInt(val, 10);
                form.maxBookingTime = isNaN(n) ? 0 : n;
              }
            "
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="dialogVisible = false"> 取消 </el-button>
        <el-button type="primary" :loading="submitLoading" @click="submitForm"> 确定 </el-button>
      </template>
    </el-dialog>

    <PcImagePreviewViewer
      v-model:visible="planeImageViewerVisible"
      :url-list="planeImageViewerUrlList"
      :initial-index="planeImageViewerInitialIndex"
    />
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { useDebounceFn } from "@vueuse/core";
import type { FormInstance, FormRules } from "element-plus";
import type { UploadFile, UploadRequestOptions, UploadUserFile } from "element-plus";
import { Plus } from "@element-plus/icons-vue";
import PcImagePreviewViewer from "@/components/pcMediaPreview/PcImagePreviewViewer.vue";
import ProTable from "@/components/ProTable/index.vue";
import type { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
import {
  scheduleList,
  scheduleDel,
  scheduleAdd,
  scheduleEdit,
  scheduleDetail,
  getHasReservation,
  scheduleSort
} from "@/api/modules/scheduledService";
import { uploadFileToOss } from "@/api/upload.js";
import { localGet } from "@/utils";

export interface CategoryRow {
  id: number | string;
  _seq?: number;
  name: string;
  maxBookingTime?: number;
  isDisplay: number;
  planeImageUrls?: string[];
}

const proTable = ref<ProTableInstance>();
const sortSaving = ref(false);

const columns: ColumnProps<CategoryRow>[] = [
  { type: "sort", label: "排序", width: 64, align: "center" },
  { prop: "_seq", label: "序号", width: 60, align: "center" },
  { prop: "name", label: "名称" },
  { prop: "maxBookingTime", label: "最长预订时间(分钟)", align: "center" },
  { prop: "isDisplay", label: "状态", align: "center" },
  { prop: "operation", label: "操作", fixed: "right", width: 140, align: "center" }
];

/** 关闭分页并一次拉全量，拖拽排序后提交的 categoryIds 与门店顺序一致 */
const initParam = reactive({
  storeId: localGet("geeker-user")?.userInfo?.storeId ?? localGet("createdId") ?? "",
  pageNum: 1,
  pageSize: 500
});

function mapCategoryItem(item: any): CategoryRow {
  const isDisplay = item.isDisplay !== undefined ? item.isDisplay : item.show ? 1 : 0;
  return {
    id: item.id,
    name: item.categoryName ?? item.name ?? "",
    maxBookingTime: item.maxBookingTime ?? item.bookingTime ?? 0,
    isDisplay: Number(isDisplay),
    planeImageUrls: item.planeImageUrls ?? item.floorPlanUrls ?? []
  };
}

/** ProTable / useTable 约定：返回 { data: { list, total } } */
async function getTableList(params: any) {
  const storeId = params.storeId ?? initParam.storeId;
  if (!storeId) {
    return { data: { list: [] as CategoryRow[], total: 0 } };
  }
  const pageNum = params.pageNum ?? 1;
  const pageSize = params.pageSize ?? 10;
  try {
    const res: any = await scheduleList({
      storeId: Number(storeId),
      pageNum,
      pageSize
    });
    const body = res?.data ?? res;
    const inner = body?.data ?? body;
    const raw = inner?.records ?? inner?.list ?? (Array.isArray(inner) ? inner : []);
    const arr = Array.isArray(raw) ? raw : [];
    const total = inner?.total ?? body?.total ?? inner?.totalCount ?? 0;
    const list = arr.map((item: any, i: number) => ({
      ...mapCategoryItem(item),
      _seq: (pageNum - 1) * pageSize + i + 1
    }));
    return { data: { list, total: Number(total) || 0 } };
  } catch (e: any) {
    ElMessage.error(e?.message || "加载失败");
    return { data: { list: [] as CategoryRow[], total: 0 } };
  }
}

const dialogVisible = ref(false);
const editId = ref<number | string | null>(null);
const submitLoading = ref(false);
const detailLoading = ref(false);
const formRef = ref<FormInstance>();

const form = reactive<{
  name: string;
  maxBookingTime: number;
  isDisplay: number;
  planeImageUrls: string[];
  planeImageFileList: UploadUserFile[];
}>({
  name: "",
  maxBookingTime: 0,
  isDisplay: 1,
  planeImageUrls: [],
  planeImageFileList: []
});

/** picture-card 放大镜：EP2 默认 onPreview 为空，需自行打开预览（挂 body，高于弹窗/表格） */
const planeImageViewerVisible = ref(false);
const planeImageViewerUrlList = ref<string[]>([]);
const planeImageViewerInitialIndex = ref(0);

const rules: FormRules = {
  name: [{ required: true, message: "请输入分类名称", trigger: "blur" }],
  maxBookingTime: [{ required: true, message: "请输入最长预订时间", trigger: "blur" }],
  planeImageUrls: [
    {
      required: true,
      validator: (_rule: any, _value: any, callback: (e?: Error) => void) => {
        if (!form.planeImageUrls || form.planeImageUrls.length < 1) {
          callback(new Error("请至少上传一张平面图（包含桌号）"));
        } else {
          callback();
        }
      },
      trigger: "change"
    }
  ]
};

async function handlePlaneImageUpload(options: UploadRequestOptions) {
  const uploadFileItem = options.file as UploadUserFile;
  const raw = uploadFileItem.raw || uploadFileItem;
  const file = raw instanceof File ? raw : null;
  if (!file) return;

  uploadFileItem.status = "uploading";
  try {
    const fileUrl = await uploadFileToOss(file, "image");
    if (fileUrl) {
      uploadFileItem.status = "success";
      uploadFileItem.url = fileUrl;
      uploadFileItem.response = { url: fileUrl };
      if (!form.planeImageUrls.includes(fileUrl)) form.planeImageUrls.push(fileUrl);
    } else {
      uploadFileItem.status = "fail";
      ElMessage.error("上传失败，未返回地址");
    }
  } catch {
    uploadFileItem.status = "fail";
    // OSS 签名/网络错误已在 upload.js 中提示
  }
  formRef.value?.validateField("planeImageUrls").catch(() => {});
}

function onPlaneImageRemove(_file: UploadUserFile, fileList: UploadUserFile[]) {
  const urls = fileList.map(f => (f as any).url).filter(Boolean);
  form.planeImageUrls = urls;
  formRef.value?.validateField("planeImageUrls").catch(() => {});
}

function onPlaneImagePreview(file: UploadFile) {
  const urls = form.planeImageFileList.map(f => String((f as UploadUserFile).url || "").trim()).filter(Boolean);
  if (!urls.length) {
    ElMessage.warning("暂无可预览的图片");
    return;
  }
  const u = String(file.url || "").trim();
  const idx = u ? urls.indexOf(u) : 0;
  planeImageViewerUrlList.value = urls;
  planeImageViewerInitialIndex.value = idx >= 0 ? idx : 0;
  planeImageViewerVisible.value = true;
}

function getStoreId(): number | string | null {
  return localGet("geeker-user")?.userInfo?.storeId ?? localGet("createdId") ?? null;
}

function getTableRowsFromProTable(): CategoryRow[] {
  const p = proTable.value as any;
  const td = p?.tableData;
  const list = td && typeof td === "object" && "value" in td ? td.value : td;
  return Array.isArray(list) ? list : [];
}

/** 拖拽结束后按当前表格顺序保存（防抖，避免连续误触） */
const persistSortOrder = useDebounceFn(async () => {
  const storeId = getStoreId();
  if (!storeId) return;
  const rows = getTableRowsFromProTable();
  const categoryIds = rows.map(r => r.id).filter(id => id != null && id !== "");
  if (!categoryIds.length) return;
  sortSaving.value = true;
  try {
    await scheduleSort({ categoryIds, storeId });
    rows.forEach((row, i) => {
      row._seq = i + 1;
    });
    ElMessage.success("排序已保存");
  } catch (e: any) {
    ElMessage.error(e?.message || "排序保存失败");
    proTable.value?.getTableList();
  } finally {
    sortSaving.value = false;
  }
}, 400);

function onDragSortEnd() {
  persistSortOrder();
}

/** 接口返回 true 表示该分类下桌号存在预定，不可编辑/删除 */
function parseHasReservationResponse(res: any): boolean {
  const body = res?.data ?? res;
  const val = body?.data !== undefined ? body.data : body;
  if (typeof val === "boolean") return val;
  if (val != null && typeof val === "object" && "hasReservation" in val) {
    return Boolean((val as { hasReservation?: boolean }).hasReservation);
  }
  return Boolean(val);
}

async function categoryHasReservation(categoryId: number | string): Promise<boolean> {
  const res: any = await getHasReservation({ categoryId: categoryId, storeId: getStoreId() });
  return parseHasReservationResponse(res);
}

function handleNew() {
  editId.value = null;
  form.name = "";
  form.maxBookingTime = 0;
  form.isDisplay = 1;
  form.planeImageUrls = [];
  form.planeImageFileList = [];
  dialogVisible.value = true;
}

async function handleEdit(row: CategoryRow) {
  try {
    const has = await categoryHasReservation(row.id);
    if (has) {
      ElMessage.warning("此分类所属桌号有预订信息，不可修改");
      return;
    }
  } catch (e: any) {
    ElMessage.error(e?.message || "校验失败，请稍后重试");
    return;
  }
  editId.value = row.id;
  dialogVisible.value = true;
  detailLoading.value = true;
  try {
    const res: any = await scheduleDetail({ id: row.id });
    const data = res?.data ?? res ?? {};
    form.name = data.categoryName ?? data.name ?? row.name ?? "";
    form.maxBookingTime = data.maxBookingTime ?? data.bookingTime ?? row.maxBookingTime ?? 0;
    form.isDisplay = data.isDisplay !== undefined ? Number(data.isDisplay) : data.show ? 1 : (row.isDisplay ?? 1);
    const raw = data.floorPlanImages ?? data.planeImageUrls ?? data.floorPlanUrls ?? data.planeImages ?? [];
    const urls = Array.isArray(raw)
      ? raw
      : typeof raw === "string"
        ? raw
            .split(",")
            .map((s: string) => s.trim())
            .filter(Boolean)
        : [];
    form.planeImageUrls = [...urls];
    form.planeImageFileList = form.planeImageUrls.map((url: string, i: number) => ({
      name: `plane-${i}`,
      url
    })) as UploadUserFile[];
  } catch (e: any) {
    ElMessage.error(e?.message || "获取详情失败");
    form.name = row.name;
    form.maxBookingTime = row.maxBookingTime ?? 0;
    form.isDisplay = row.isDisplay;
    const urls = (row as any).planeImageUrls ?? (row as any).floorPlanUrls ?? [];
    form.planeImageUrls = Array.isArray(urls) ? [...urls] : [];
    form.planeImageFileList = form.planeImageUrls.map((url, i) => ({
      name: `plane-${i}`,
      url
    })) as UploadUserFile[];
  } finally {
    detailLoading.value = false;
  }
}

async function handleDelete(row: CategoryRow) {
  try {
    const has = await categoryHasReservation(row.id);
    if (has) {
      ElMessage.warning("此分类所属桌号有预订信息，不可修改");
      return;
    }
  } catch (e: any) {
    ElMessage.error(e?.message || "校验失败，请稍后重试");
    return;
  }
  ElMessageBox.confirm("是否确认删除?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning"
  })
    .then(async () => {
      try {
        await scheduleDel({ id: row.id });
        ElMessage.success("删除成功");
        proTable.value?.getTableList();
      } catch (e: any) {
        ElMessage.error(e?.message || "删除失败");
      }
    })
    .catch(() => {});
}

function onDialogClose() {
  planeImageViewerVisible.value = false;
  editId.value = null;
  form.name = "";
  form.maxBookingTime = 0;
  form.isDisplay = 1;
  form.planeImageUrls = [];
  form.planeImageFileList = [];
}

async function submitForm() {
  if (!formRef.value) return;
  try {
    await formRef.value.validate();
  } catch {
    return;
  }
  const storeId = getStoreId();
  if (!storeId) {
    ElMessage.warning("未找到门店信息");
    return;
  }
  submitLoading.value = true;
  try {
    const payload = {
      storeId,
      categoryName: form.name.trim(),
      isDisplay: form.isDisplay,
      maxBookingTime: form.maxBookingTime,
      floorPlanImages: form.planeImageUrls.join(",")
    };
    if (editId.value != null) {
      await scheduleEdit({ ...payload, id: editId.value });
    } else {
      await scheduleAdd(payload);
    }
    ElMessage.success(editId.value == null ? "添加成功" : "修改成功");
    dialogVisible.value = false;
    onDialogClose();
    proTable.value?.getTableList();
  } catch (e: any) {
    ElMessage.error(e?.message || "保存失败");
  } finally {
    submitLoading.value = false;
  }
}
</script>
<!-- append-to-body 时弹窗在 body 下，不是 .classify-management 子节点，需用 dialog 自身 class 命中 -->
<style lang="scss">
.classify-edit-dialog .el-dialog__body {
  box-sizing: border-box;
  height: 400px;
}
</style>
<style scoped lang="scss">
.classify-management {
  padding: 0;
}
.table-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 12px 0;
}
.table-header-left {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
}
.sort-tip,
.sort-saving-tip {
  font-size: 12px;
  color: #909399;
}
.sort-saving-tip {
  color: #6c8ff8;
}
.status-show {
  color: #67c23a;
}
.status-hide {
  color: #909399;
}
.plane-upload-wrap {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.upload-tip {
  display: block;
  margin-top: 8px;
  font-size: 12px;
  color: #909399;
}
:deep(.el-upload--picture-card) {
  width: 100px;
  height: 100px;
}
:deep(.el-upload-list--picture-card .el-upload-list__item) {
  width: 100px;
  height: 100px;
}
</style>
