<script setup lang="ts">
import { onMounted, onUnmounted, ref, computed, watch, nextTick } from "vue";
import Plotly from "plotly.js-dist-min";
import { useAutoflowStore } from "@/stores/autoflowStore";
import { WorkflowService } from "@/components/service/management/WorkflowService";
import { ExecutionsService } from "@/components/service/management/ExecutionsService";
import { AttachmentsService } from "@/components/service/management/AttachmentsService";
import { KubeflowRunService } from "@/components/service/management/KubeflowRunService";
import { DataGroupService } from "@/components/service/management/DataGroupService";
import { useRouter } from "vue-router";

type UiStatus = "success" | "failed" | "running" | "pending";
type KfFilter = "SUCCEEDED" | "FAILED" | "RUNNING" | "PENDING" | null;

interface RecentRunRow {
  name: string;
  status: UiStatus;
  time: string;
}
interface DatasetGroup {
  groupId: number;
  groupName: string;
  items: DatasetRow[];
}
interface DatasetRow {
  groupId: number;
  groupName: string;
  name: string;
  version: number;
  rows: number;
  last?: string;
  pct: number;
}
interface WorkflowRow {
  id: string | number;
  name: string;
  modDt: string;
}
interface KubeflowRunRow {
  runId: string;
  name: string;
  state: string;
  uiStatus: UiStatus;
  createdAt?: string;
  scheduledAt?: string;
  finishedAt?: string;
  experimentId?: string;
  pipelineName?: string;
  serviceAccount?: string;
}

const COLOR = {
  SUCCESS: "#2ecc71",
  ERROR: "#e74c3c",
  INFO: "#3498db",
  WARNING: "#f1c40f",
  SECONDARY: "#95a5a6",
  MUTED: "#7f8c8d",
  NODATA: "#4b4f55",
} as const;

const STATUS_LABEL: Record<UiStatus, string> = {
  success: "Succeeded",
  failed: "Failed",
  running: "Running",
  pending: "Pending",
};

function colorChipByUiStatus(status: UiStatus) {
  if (status === "success") return "success";
  if (status === "failed") return "error";
  if (status === "running") return "info";
  return "grey";
}
function avatarColorByUiStatus(status: UiStatus) {
  if (status === "success") return "green-lighten-1";
  if (status === "failed") return "red-lighten-1";
  if (status === "running") return "blue-lighten-1";
  return "grey-darken-1";
}
function avatarIconByUiStatus(status: UiStatus) {
  if (status === "success") return "mdi-check";
  if (status === "failed") return "mdi-close";
  if (status === "running") return "mdi-progress-clock";
  return "mdi-clock-outline";
}

const groupSummaries = ref<Array<{ id: number; name: string }>>([]);
const datasetCountByGroup = ref<Record<number, number>>({});
const datasetsByGroup = ref<Record<number, DatasetRow[]>>({});
const groupLoading = ref<Record<number, boolean>>({});
const groupLoaded = ref<Record<number, boolean>>({});
const router = useRouter();

const goExecutions = () => router.push("/run/executions");
const goWorkflows = () => router.push("/workflows");
const goDatasets = () => router.push("/DataGroup");

async function loadDatasetsForGroup(groupId: number, groupName: string) {
  if (groupLoaded.value[groupId] || groupLoading.value[groupId]) return;
  groupLoading.value[groupId] = true;
  try {
    const res = await AttachmentsService.search({
      projectId: currentProjectId.value,
      page: 0,
      size: 200,
      refType: "DATASET",
      refId: groupId,
      sortField: "id",
      sortDirection: "DESC",
    } as any);
    const content: any[] = res?.data?.content ?? res?.data ?? [];

    // Map → Record 로 변경 (ES5 호환)
    const byName: Record<string, DatasetRow> = {};
    for (let i = 0; i < content.length; i++) {
      const f = content[i];
      const name = String(f?.title ?? f?.originalName ?? "(no name)");
      const version = Number(f?.version) || 1;
      const last = (f?.modDt ?? f?.regDt ?? f?.createdAt) as string | undefined;

      const key = name;
      const cur = byName[key] ?? {
        groupId,
        groupName,
        name,
        version: 0,
        rows: 0,
        last: undefined,
        pct: 0,
      };
      cur.rows += 1;
      cur.version = Math.max(cur.version, version);
      if (
        !cur.last ||
        (last && new Date(last).getTime() > new Date(cur.last).getTime())
      ) {
        cur.last = last;
      }
      byName[key] = cur;
    }

    const rows = Object.values(byName);
    const maxVer = Math.max(1, ...rows.map((r) => r.version));
    for (let i = 0; i < rows.length; i++)
      rows[i].pct = Math.round((rows[i].version / maxVer) * 100);

    datasetsByGroup.value[groupId] = rows.sort(
      (a, b) =>
        new Date(b.last ?? 0).getTime() - new Date(a.last ?? 0).getTime() ||
        b.version - a.version,
    );
    groupLoaded.value[groupId] = true;
  } catch (e) {
    console.error("[Dashboard] loadDatasetsForGroup error:", e);
    datasetsByGroup.value[groupId] = [];
  } finally {
    groupLoading.value[groupId] = false;
  }
}

function mapRawStateToUiStatus(raw?: string): UiStatus {
  switch ((raw || "").toUpperCase()) {
    case "SUCCEEDED":
      return "success";
    case "FAILED":
      return "failed";
    case "RUNNING":
      return "running";
    case "PENDING":
    case "QUEUED":
    case "SCHEDULED":
      return "pending";
    default:
      return "pending";
  }
}

const toEpoch = (iso?: string) => new Date(iso ?? 0).getTime();
const fmtLocalDateTime = (iso?: string) =>
  iso ? new Date(iso).toLocaleString() : "-";
function fmtYmdHm(iso?: string) {
  if (!iso) return "-";
  const d = new Date(iso);
  const p = (n: number) => String(n).padStart(2, "0");
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
}

const store = useAutoflowStore();
const currentProjectId = computed(() => store.projectId);
const currentProjectName = computed(
  () => store.projectName ?? localStorage.getItem("projectName") ?? "",
);

const workflows = ref<WorkflowRow[]>([]);
const recentRuns = ref<RecentRunRow[]>([]);
const datasetRows = ref<DatasetRow[]>([]);
const kubeflowRuns = ref<KubeflowRunRow[]>([]);

const isRefreshing = ref(false);
const runsLoading = ref(false);
const dsLoading = ref(false);
const kfRunsLoading = ref(false);

const kubeflowDetailsExplicitOpen = ref(false);
const kubeflowStatusFilter = ref<KfFilter>(null);
const showKubeflowDetails = computed(
  () =>
    kubeflowStatusFilter.value !== null || kubeflowDetailsExplicitOpen.value,
);

const recentLimit = 10;

const workflowStatusPieRef = ref<HTMLElement | null>(null);
const kubeflowPieRef = ref<HTMLElement | null>(null);
const kubeflowPieWrapRef = ref<HTMLElement | null>(null);
let kubeflowResizeObserver: ResizeObserver | null = null;

/* Map → Record 로 변경 */
const datasetGroups = computed<DatasetGroup[]>(() => {
  const dict: Record<number, DatasetGroup> = {};
  const rows = datasetRows.value || [];
  for (let i = 0; i < rows.length; i++) {
    const r = rows[i];
    if (!dict[r.groupId])
      dict[r.groupId] = {
        groupId: r.groupId,
        groupName: r.groupName,
        items: [],
      };
    dict[r.groupId].items.push(r);
  }
  const groups = Object.values(dict);
  for (let i = 0; i < groups.length; i++) {
    groups[i].items.sort(
      (a, b) => toEpoch(b.last) - toEpoch(a.last) || b.version - a.version,
    );
  }
  return groups.sort((a, b) => a.groupName.localeCompare(b.groupName));
});

const kubeflowCounts = computed(() => {
  const agg = { SUCCEEDED: 0, FAILED: 0, RUNNING: 0, PENDING: 0 };
  for (let i = 0; i < kubeflowRuns.value.length; i++) {
    const raw = (kubeflowRuns.value[i].state || "").toUpperCase();
    if (raw.includes("SUCCEED")) agg.SUCCEEDED++;
    else if (raw.includes("FAIL")) agg.FAILED++;
    else if (raw.includes("RUN")) agg.RUNNING++;
    else agg.PENDING++;
  }
  return agg;
});

const kubeflowStats = computed(() => ({
  total: kubeflowCounts.value.SUCCEEDED + kubeflowCounts.value.FAILED,
  succeeded: kubeflowCounts.value.SUCCEEDED,
  failed: kubeflowCounts.value.FAILED,
}));

const kubeflowRunsForList = computed(() => {
  const list = kubeflowRuns.value.filter((kfRunRow) => {
    const raw = (kfRunRow.state || "").toUpperCase();
    if (kubeflowStatusFilter.value === "SUCCEEDED")
      return raw.includes("SUCCEED");
    if (kubeflowStatusFilter.value === "FAILED") return raw.includes("FAIL");
    return raw.includes("SUCCEED") || raw.includes("FAIL");
  });
  return list.sort((a, b) => toEpoch(b.createdAt) - toEpoch(a.createdAt));
});

async function loadRecentRuns() {
  runsLoading.value = true;
  try {
    // Kubeflow에서 전체 Run을 가져와서 최근순 상위 N개를 Recent Run으로 사용
    const res = await KubeflowRunService.getAll();
    const list: any[] = Array.isArray(res?.data) ? res.data : [];

    // 필요한 필드만 정규화
    const normalized = list.map((row: any) => {
      const state = String(row?.state ?? row?.status ?? "PENDING");
      return {
        runId: row?.runId ?? row?.run_id ?? row?.id ?? "",
        name:
          row?.displayName ??
          row?.display_name ??
          row?.name ??
          row?.run_id ??
          "(no name)",
        state,
        createdAt: row?.createdAt ?? row?.created_at,
      };
    });

    // createdAt desc 정렬 후 recentLimit 만큼 자르기
    const topN = normalized
      .sort(
        (a, b) =>
          new Date(b.createdAt ?? 0).getTime() -
          new Date(a.createdAt ?? 0).getTime(),
      )
      .slice(0, recentLimit);

    // RecentRunRow로 매핑
    recentRuns.value = topN.map<RecentRunRow>((run) => ({
      name: run.name,
      status: mapRawStateToUiStatus(run.state),
      time: fmtYmdHm(run.createdAt),
    }));
  } catch (err) {
    console.error("[Dashboard] loadRecentRuns error:", err);
    recentRuns.value = [];
  } finally {
    runsLoading.value = false;
  }
}

async function loadWorkflows() {
  try {
    const payload = {
      page: 0,
      size: 1000,
      projectId: currentProjectId.value,
      sortField: "id",
      sortDirection: "DESC",
    };
    const res = await WorkflowService.search(payload);
    const list: any[] = Array.isArray(res?.data?.content)
      ? res.data.content
      : Array.isArray(res?.data)
        ? res.data
        : [];

    const filtered = list.filter(
      (wf: any) =>
        String(
          wf?.projectId ?? wf?.prjId ?? wf?.project_id ?? wf?.project?.id ?? "",
        ) === String(currentProjectId.value),
    );

    workflows.value = filtered.map((wf: any) => ({
      id: wf.id,
      name: wf.name,
      modDt: wf.modDt,
    }));
    renderWorkflowStatusPie();
  } catch (err) {
    console.error("[Dashboard] loadWorkflows error:", err);
    workflows.value = [];
    renderWorkflowStatusPie();
  }
}

async function loadDatasetActivity() {
  dsLoading.value = true;
  try {
    const dgRes = await DataGroupService.search({
      projectId: currentProjectId.value,
      page: 0,
      size: 200,
      sortField: "id",
      sortDirection: "DESC",
      refType: "DATASET",
    } as any);

    groupSummaries.value = (dgRes?.data?.content ?? dgRes?.data ?? [])
      .map((g: any) => ({
        id: Number(g?.id),
        name: String(g?.dsNm ?? g?.name ?? `(Group ${g?.id})`),
      }))
      .filter((g) => Number.isFinite(g.id));

    datasetsByGroup.value = {};
    groupLoaded.value = {};
    groupLoading.value = {};
    datasetCountByGroup.value = {};

    // (선택) 개수만 먼저 계산
    const tasks = groupSummaries.value.map((g) =>
      AttachmentsService.search({
        projectId: currentProjectId.value,
        page: 0,
        size: 1,
        refType: "DATASET",
        refId: g.id,
        sortField: "id",
        sortDirection: "DESC",
      } as any)
        .then((res: any) => {
          const total = Number(
            res?.data?.totalElements ??
              res?.data?.total ??
              res?.data?.page?.totalElements ??
              0,
          );
          datasetCountByGroup.value = {
            ...datasetCountByGroup.value,
            [g.id]: total,
          };
        })
        .catch(() => {
          datasetCountByGroup.value = {
            ...datasetCountByGroup.value,
            [g.id]: 0,
          };
        }),
    );
    await Promise.allSettled(tasks);

    // ✅ 아코디언 제거 → 모두 펼쳐서 보여주므로 미리 로드
    await Promise.all(
      groupSummaries.value.map((g) => loadDatasetsForGroup(g.id, g.name)),
    );
  } catch (err) {
    console.error("[Dashboard] loadDatasetActivity error:", err);
    groupSummaries.value = [];
  } finally {
    dsLoading.value = false;
  }
}

async function loadKubeflowRuns() {
  kfRunsLoading.value = true;
  try {
    const res = await KubeflowRunService.getAll();
    const list: any[] = Array.isArray(res?.data) ? res.data : [];

    kubeflowRuns.value = list
      .map<KubeflowRunRow>((row: any) => {
        const state = String(row?.state ?? row?.status ?? "PENDING");
        return {
          runId: row?.runId ?? row?.run_id ?? row?.id ?? "",
          name:
            row?.displayName ??
            row?.display_name ??
            row?.name ??
            row?.run_id ??
            "(no name)",
          state,
          uiStatus: mapRawStateToUiStatus(state),
          createdAt: row?.createdAt ?? row?.created_at,
          scheduledAt: row?.scheduledAt ?? row?.scheduled_at,
          finishedAt: row?.finishedAt ?? row?.finished_at,
          experimentId:
            row?.experimentId ?? row?.experiment_id ?? row?.experimentName,
          pipelineName:
            row?.pipelineName ?? row?.pipeline_name ?? row?.pipeline_id,
          serviceAccount: row?.serviceAccount ?? row?.service_account,
        };
      })
      .sort((a, b) => toEpoch(b.createdAt) - toEpoch(a.createdAt))
      .slice(0, 1000);
  } catch (err) {
    console.error("[Dashboard] loadKubeflowRuns error:", err);
    kubeflowRuns.value = [];
  } finally {
    kfRunsLoading.value = false;
    renderKubeflowPie();
  }
}

/* Map → Record 로 변경 (반복자 제거) */
function renderWorkflowStatusPie() {
  const container = workflowStatusPieRef.value;
  if (!container) return;

  const counts: Record<string, number> = {};
  for (let i = 0; i < workflows.value.length; i++) {
    const wfRow = workflows.value[i] as any;
    const raw = wfRow?.kubeflowStatus ?? wfRow?.kubeflow_status ?? "UNKNOWN";
    const key = String(raw).toUpperCase().trim();
    counts[key] = (counts[key] || 0) + 1;
  }

  const keys = Object.keys(counts);
  if (keys.length === 0) {
    const trace: Partial<Plotly.PlotData> = {
      values: [1],
      labels: ["No Data"],
      type: "pie",
      hole: 0.55,
      textinfo: "none",
      marker: { colors: [COLOR.NODATA] },
      hoverinfo: "skip",
    };
    const layout: Partial<Plotly.Layout> = {
      paper_bgcolor: "#1e1e1e",
      plot_bgcolor: "#1e1e1e",
      showlegend: false,
      margin: { t: 20, b: 40, l: 0, r: 0 },
      annotations: [
        {
          text: "<b>No Data</b>",
          showarrow: false,
          font: { color: "#fff", size: 16 },
        },
      ],
    };
    Plotly.react(container, [trace], layout, { displayModeBar: false });
    return;
  }

  const labels = keys;
  const values = labels.map((k) => counts[k]);
  const colors = labels.map((s) => {
    if (s.includes("SUCCEED")) return COLOR.SUCCESS;
    if (s.includes("FAIL")) return COLOR.ERROR;
    if (s.includes("RUN")) return COLOR.INFO;
    if (s.includes("CREATE")) return COLOR.INFO;
    if (s.includes("PEND")) return COLOR.WARNING;
    if (s.includes("SKIP")) return COLOR.SECONDARY;
    return COLOR.MUTED;
  });

  const trace: Partial<Plotly.PlotData> = {
    values,
    labels,
    type: "pie",
    hole: 0.4,
    textinfo: "label+percent",
    textfont: { color: "#fff", size: 13 },
    marker: { colors },
    hovertemplate: "%{label}: %{percent}<extra></extra>",
  };
  const layout: Partial<Plotly.Layout> = {
    paper_bgcolor: "#1e1e1e",
    plot_bgcolor: "#1e1e1e",
    showlegend: true,
    legend: {
      font: { color: "#ffffff", size: 12 },
      orientation: "h",
      x: 0.5,
      xanchor: "center",
      y: -0.2,
    },
    margin: { t: 20, b: 40, l: 0, r: 0 },
  };
  Plotly.react(container, [trace], layout, { displayModeBar: false });
}

function renderKubeflowPie() {
  const plotEl = kubeflowPieRef.value;
  const wrapEl = kubeflowPieWrapRef.value || plotEl;
  if (!plotEl || !wrapEl) return;

  const succeeded = kubeflowCounts.value.SUCCEEDED || 0;
  const failed = kubeflowCounts.value.FAILED || 0;

  // 0인 조각 제거
  const parts: Array<{ label: string; value: number; color: string }> = [];
  if (succeeded > 0)
    parts.push({ label: "SUCCEEDED", value: succeeded, color: COLOR.SUCCESS });
  if (failed > 0)
    parts.push({ label: "FAILED", value: failed, color: COLOR.ERROR });

  const hasData = parts.length > 0;

  const trace: Partial<Plotly.PlotData> = hasData
    ? {
        type: "pie",
        labels: parts.map((p) => p.label),
        values: parts.map((p) => p.value),
        textinfo: "label+value",
        textfont: { color: "#fff", size: 13 },
        marker: { colors: parts.map((p) => p.color) },
        hovertemplate: "%{label}: %{value} (%{percent})<extra></extra>",
      }
    : {
        type: "pie",
        labels: ["No Data"],
        values: [1],
        textinfo: "none",
        marker: { colors: [COLOR.NODATA] },
        hoverinfo: "skip",
      };

  const width = wrapEl.clientWidth || 300;
  const height = wrapEl.clientHeight || 220;
  const layout: Partial<Plotly.Layout> = {
    width,
    height,
    autosize: false,
    paper_bgcolor: "rgba(0,0,0,0)",
    plot_bgcolor: "rgba(0,0,0,0)",
    showlegend: false,
    margin: { t: 8, b: 8, l: 8, r: 8 },
  };

  Plotly.react(plotEl, [trace], layout, {
    displayModeBar: false,
    responsive: true,
  });

  (plotEl as any).on?.("plotly_click", (ev: any) => {
    kubeflowDetailsExplicitOpen.value = true;
    const label = (ev?.points?.[0]?.label ?? "").toUpperCase();
    if (label === "SUCCEEDED" || label === "FAILED") {
      kubeflowStatusFilter.value =
        kubeflowStatusFilter.value === label ? null : (label as KfFilter);
    }
  });

  (plotEl as any).on?.("plotly_doubleclick", () => {
    kubeflowStatusFilter.value = null;
    kubeflowDetailsExplicitOpen.value = false;
    return false;
  });

  if (!kubeflowResizeObserver) {
    kubeflowResizeObserver = new ResizeObserver(() => {
      if (kubeflowPieRef.value) Plotly.Plots.resize(kubeflowPieRef.value);
    });
    kubeflowResizeObserver.observe(wrapEl);
    window.addEventListener("orientationchange", () => {
      if (kubeflowPieRef.value) Plotly.Plots.resize(kubeflowPieRef.value);
    });
  }
}

async function handleRefresh() {
  if (isRefreshing.value) return;
  try {
    isRefreshing.value = true;
    await Promise.all([
      loadRecentRuns(),
      (async () => {
        await loadWorkflows();
        renderWorkflowStatusPie();
      })(),
      loadDatasetActivity(),
      loadKubeflowRuns(),
    ]);
  } catch (err) {
    console.error("[Dashboard] refresh failed:", err);
  } finally {
    isRefreshing.value = false;
  }
}

onMounted(async () => {
  renderWorkflowStatusPie();
  await loadRecentRuns();
  await loadWorkflows();
  await loadDatasetActivity();
  await loadKubeflowRuns();
});

onUnmounted(() => {
  if (kubeflowResizeObserver && kubeflowPieWrapRef.value) {
    kubeflowResizeObserver.unobserve(kubeflowPieWrapRef.value);
  }
  kubeflowResizeObserver = null;
});

watch(currentProjectId, () => loadWorkflows());
watch(kubeflowRuns, () => renderKubeflowPie());
watch(showKubeflowDetails, async () => {
  await nextTick();
  renderKubeflowPie();
});
</script>

<template>
  <v-container fluid>
    <!-- Header -->
    <div class="d-flex justify-space-between align-center mb-6">
      <h2 class="text-h6 font-weight-bold">{{ currentProjectName }}</h2>
      <v-btn
        color="primary"
        prepend-icon="mdi-refresh"
        :loading="isRefreshing"
        :disabled="isRefreshing"
        @click="handleRefresh"
      >
        Refresh
      </v-btn>
    </div>

    <!-- Top: Recent Runs / Workflows -->
    <v-row>
      <!-- Recent Run -->
      <v-col cols="12" md="6">
        <v-card
          class="pa-0"
          :elevation="0"
          style="min-height: 450px; max-height: 450px"
        >
          <div
            class="d-flex align-center justify-space-between w-100"
            style="padding: 16px"
          >
            <h3 class="text-subtitle-1 font-weight-bold mb-0">Recent Run</h3>

            <v-btn
              variant="text"
              class="text-caption font-weight-bold"
              append-icon="mdi-arrow-right"
              style="text-transform: none"
              @click="goExecutions"
            >
              Go to Executions
            </v-btn>
          </div>
          <v-divider />
          <div style="overflow-y: auto; max-height: 380px; padding: 8px 16px">
            <v-skeleton-loader
              v-if="runsLoading"
              type="list-item-two-line"
              class="mb-2"
              v-for="i in 4"
              :key="i"
            />
            <v-list v-else density="comfortable" class="bg-transparent">
              <v-list-item
                v-for="(runRow, idx) in recentRuns"
                :key="idx"
                class="py-2"
              >
                <div class="d-flex align-center justify-space-between w-100">
                  <!-- left: icon + status -->
                  <div class="d-flex align-center ga-2">
                    <v-avatar
                      size="28"
                      :color="avatarColorByUiStatus(runRow.status)"
                    >
                      <v-icon size="20" color="white">
                        {{ avatarIconByUiStatus(runRow.status) }}
                      </v-icon>
                    </v-avatar>
                    <v-chip
                      size="small"
                      :color="colorChipByUiStatus(runRow.status)"
                      variant="tonal"
                      class="text-uppercase"
                    >
                      {{ STATUS_LABEL[runRow.status] }}
                    </v-chip>
                  </div>

                  <!-- right: name & time -->
                  <div
                    class="d-flex flex-column text-right"
                    style="min-width: 220px"
                  >
                    <span class="font-weight-medium text-body-2 truncate">
                      {{ runRow.name }}
                    </span>
                    <span class="text-caption text-grey-darken-1">
                      {{ runRow.time }}
                    </span>
                  </div>
                </div>
              </v-list-item>

              <v-list-item v-if="!runsLoading && recentRuns.length === 0">
                <template #title>
                  <div class="text-caption text-grey">
                    최근 실행 데이터가 없습니다.
                  </div>
                </template>
              </v-list-item>
            </v-list>
          </div>
        </v-card>
      </v-col>

      <!-- Workflows -->
      <v-col cols="12" md="6">
        <v-card
          class="pa-0"
          :elevation="0"
          style="min-height: 450px; max-height: 450px"
        >
          <div
            class="d-flex align-center justify-space-between w-100"
            style="padding: 16px"
          >
            <h3 class="text-subtitle-1 font-weight-bold mb-0">
              Recently Registered Workflow
            </h3>
            <v-btn
              variant="text"
              class="text-caption font-weight-bold"
              append-icon="mdi-arrow-right"
              style="text-transform: none"
              @click="goWorkflows"
            >
              Go to Workflows
            </v-btn>
          </div>
          <v-divider />
          <div style="overflow-y: auto; max-height: 380px; padding: 8px 16px">
            <v-list density="comfortable" class="bg-transparent" nav>
              <v-list-item
                v-for="wfRow in workflows
                  .slice(0, 10)
                  .sort((a, b) => toEpoch(b.modDt) - toEpoch(a.modDt))"
                :key="wfRow.id"
              >
                <template #title>
                  <div class="d-flex justify-space-between align-center w-100">
                    <span class="text-body-2 font-weight-medium">{{
                      wfRow.name
                    }}</span>
                    <span class="text-caption text-body-2">{{
                      fmtYmdHm(wfRow.modDt)
                    }}</span>
                  </div>
                </template>
              </v-list-item>

              <v-list-item v-if="workflows.length === 0">
                <template #title>
                  <div class="text-caption text-grey">
                    최근 등록/수정된 워크플로우가 없습니다.
                  </div>
                </template>
              </v-list-item>
            </v-list>
          </div>
        </v-card>
      </v-col>
    </v-row>

    <!-- Middle: Kubeflow / Dataset -->
    <v-row class="mt-4">
      <!-- Kubeflow -->

      <v-col cols="12" md="6">
        <v-card
          class="pa-0"
          :elevation="0"
          style="min-height: 450px; max-height: 450px"
        >
          <div style="padding: 16px">
            <div class="d-flex align-center justify-space-between w-100">
              <h3 class="text-subtitle-1 font-weight-bold mb-0">
                Run Status Overview
              </h3>
              <div class="d-flex align-center ga-2">
                <v-chip size="small" variant="tonal" color="grey"
                  >Total {{ kubeflowStats.total }}</v-chip
                >
                <v-chip size="small" variant="tonal" color="success"
                  >Succeeded {{ kubeflowStats.succeeded }}</v-chip
                >
                <v-chip size="small" variant="tonal" color="error"
                  >Failed {{ kubeflowStats.failed }}</v-chip
                >

                <v-chip
                  v-if="kubeflowStatusFilter"
                  size="small"
                  color="secondary"
                  variant="tonal"
                  closable
                  @click:close="
                    kubeflowStatusFilter = null;
                    kubeflowDetailsExplicitOpen = false;
                  "
                >
                  Filter: {{ kubeflowStatusFilter }}
                </v-chip>
              </div>
              <v-btn
                variant="text"
                class="text-caption font-weight-bold"
                append-icon="mdi-arrow-right"
                style="text-transform: none"
                @click="goExecutions"
              >
                Go to Executions
              </v-btn>
            </div>
          </div>
          <v-divider />
          <div style="height: 380px">
            <!-- Only Chart -->
            <div
              v-if="!showKubeflowDetails"
              ref="kubeflowPieWrapRef"
              style="
                width: 100%;
                height: 100%;
                display: flex;
                align-items: center;
                justify-content: center;
              "
            >
              <div ref="kubeflowPieRef" style="width: 100%; height: 100%"></div>
            </div>

            <!-- Split 50/50 (details open) -->
            <v-row v-else no-gutters class="h-100">
              <v-col cols="6" class="h-100">
                <div ref="kubeflowPieWrapRef" style="width: 100%; height: 100%">
                  <div
                    ref="kubeflowPieRef"
                    style="width: 100%; height: 100%"
                  ></div>
                </div>
              </v-col>

              <v-col cols="6" class="h-100">
                <div
                  class="d-flex align-center ga-2"
                  style="margin-bottom: 8px"
                >
                  <v-btn
                    v-if="kubeflowStatusFilter"
                    size="small"
                    variant="text"
                    @click="kubeflowStatusFilter = null"
                  >
                    Clear
                  </v-btn>
                </div>

                <div style="height: calc(100% - 36px); overflow: auto">
                  <v-list density="comfortable">
                    <v-list-item
                      v-for="kfRunRow in kubeflowRunsForList"
                      :key="kfRunRow.runId"
                      class="py-2"
                    >
                      <div
                        class="d-flex align-center justify-space-between w-100"
                      >
                        <div class="d-flex align-center ga-2">
                          <v-chip
                            size="small"
                            :color="
                              (kfRunRow.state || '')
                                .toUpperCase()
                                .includes('SUCCEED')
                                ? 'success'
                                : 'error'
                            "
                            variant="tonal"
                          >
                            {{
                              (kfRunRow.state || "")
                                .toUpperCase()
                                .includes("SUCCEED")
                                ? "Succeeded"
                                : "Failed"
                            }}
                          </v-chip>
                          <span
                            class="text-body-2 font-weight-medium truncate"
                            style="max-width: 220px"
                          >
                            {{ kfRunRow.name }}
                          </span>
                        </div>
                        <span class="text-caption text-grey-darken-1">
                          {{ fmtYmdHm(kfRunRow.createdAt) }}
                        </span>
                      </div>
                    </v-list-item>

                    <v-list-item v-if="kubeflowRunsForList.length === 0">
                      <template #title>
                        <div class="text-caption text-grey">
                          표시할 Run이 없습니다.
                        </div>
                      </template>
                    </v-list-item>
                  </v-list>
                </div>
              </v-col>
            </v-row>
          </div>
        </v-card>
      </v-col>

      <!-- Dataset (fixed height, inner scroll, compact) -->
      <v-col cols="12" md="6">
        <v-card
          class="pa-0"
          :elevation="0"
          style="min-height: 450px; max-height: 450px"
        >
          <div style="padding: 16px">
            <div class="d-flex align-center justify-space-between w-100">
              <h3 class="text-subtitle-1 font-weight-bold mb-0">
                DataGroup Update Activity
              </h3>
              <v-btn
                variant="text"
                class="text-caption font-weight-bold"
                append-icon="mdi-arrow-right"
                style="text-transform: none"
                @click="goDatasets"
              >
                Go to DataGroup
              </v-btn>
            </div>
          </div>
          <v-divider />
          <v-sheet class="pa-3 flex-grow-1 overflow-auto bg-transparent">
            <v-skeleton-loader
              v-if="dsLoading"
              type="list-item-two-line"
              class="mb-2"
              v-for="i in 4"
              :key="i"
            />

            <v-expansion-panels v-else multiple variant="accordion">
              <v-expansion-panel
                v-for="g in groupSummaries"
                :key="g.id"
                :elevation="3"
                @group:selected="
                  async (e: any) => {
                    if (e.value) await loadDatasetsForGroup(g.id, g.name);
                  }
                "
              >
                <v-expansion-panel-title density="compact">
                  <div class="d-flex align-center ga-2">
                    <span class="font-weight-bold">{{ g.name }}</span>
                    <v-chip size="x-small" variant="tonal" color="secondary">
                      {{
                        datasetCountByGroup[g.id] ??
                        (datasetsByGroup[g.id] || []).length
                      }}
                      datasets
                    </v-chip>
                  </div>
                </v-expansion-panel-title>

                <v-expansion-panel-text density="compact" class="py-1">
                  <div v-if="groupLoading[g.id]" class="pa-2">
                    <v-skeleton-loader
                      type="list-item-three-line"
                      v-for="i in 2"
                      :key="i"
                      class="mb-2"
                    />
                  </div>

                  <v-list
                    v-else-if="(datasetsByGroup[g.id] || []).length > 0"
                    density="compact"
                  >
                    <v-list-item
                      v-for="row in datasetsByGroup[g.id]"
                      :key="g.id + '-' + row.name"
                      density="compact"
                      class="py-0"
                    >
                      <div
                        class="d-flex justify-space-between align-center w-100"
                      >
                        <span
                          class="text-body-2 font-weight-medium d-flex align-center ga-2"
                        >
                          <span
                            class="text-truncate"
                            style="max-width: 260px"
                            >{{ row.name }}</span
                          >
                          <v-chip size="x-small" color="primary" variant="tonal"
                            >v{{ row.version }}</v-chip
                          >
                        </span>
                        <span class="text-caption text-medium-emphasis"
                          >Last: {{ fmtLocalDateTime(row.last) }}</span
                        >
                      </div>

                      <v-progress-linear
                        :model-value="row.pct"
                        height="4"
                        color="primary"
                        class="mt-1"
                      />
                      <div class="text-caption text-grey mt-1">
                        {{ row.version }} Update{{ row.version > 1 ? "s" : "" }}
                        <span v-if="row.rows && row.rows !== row.version">
                          • {{ row.rows }} Rows</span
                        >
                      </div>
                    </v-list-item>
                  </v-list>

                  <div v-else class="text-caption text-grey px-2 py-1">
                    이 그룹에 표시할 데이터셋이 없습니다.
                  </div>
                </v-expansion-panel-text>
              </v-expansion-panel>

              <div
                v-if="!dsLoading && groupSummaries.length === 0"
                class="text-caption text-grey px-2 py-1"
              >
                표시할 데이터그룹이 없습니다.
              </div>
            </v-expansion-panels>
          </v-sheet>
        </v-card>
      </v-col>
    </v-row>

    <!-- Bottom: Model Deployment (demo) -->
    <!-- <v-card class="rounded-lg pa-4 mt-4">
      <div class="d-flex justify-space-between align-center mt-8 mb-2 px-2">
        <div class="d-flex align-center">
          <span class="text-subtitle-1 font-weight-bold">Model Deployment</span>
        </div>
        <v-btn
          variant="text"
          class="text-caption font-weight-bold"
          append-icon="mdi-arrow-right"
          style="text-transform: none"
        >
          Go to Model Deploy
        </v-btn>
      </div>

      <v-col cols="12">
        <v-sheet>
          <v-table density="comfortable" fixed-header height="625">
            <colgroup>
              <col style="width: 5%" />
              <col style="width: 10%" />
              <col style="width: 10%" />
              <col style="width: 10%" />
              <col style="width: 10%" />
              <col style="width: 10%" />
            </colgroup>
            <thead>
              <tr>
                <th></th>
                <th class="font-weight-bold" style="word-break: keep-all">
                  Model Name
                </th>
                <th class="font-weight-bold" style="word-break: keep-all">
                  Version
                </th>
                <th class="font-weight-bold" style="word-break: keep-all">
                  Deployed At
                </th>
                <th class="font-weight-bold" style="word-break: keep-all">
                  Status
                </th>
                <th class="font-weight-bold" style="word-break: keep-all">
                  Download
                </th>
              </tr>
            </thead>
            <tbody class="text-body-2">
              <tr>
                <td></td>
                <td>LaneDetectionModel</td>
                <td>v1.2.0</td>
                <td>2025-05-13 14:32</td>
                <td>Active</td>
                <td>Finished</td>
              </tr>
              <tr>
                <td></td>
                <td>TrafficSignClassifier</td>
                <td>v0.9.3</td>
                <td>2025-05-13 09:00</td>
                <td>Pending</td>
                <td>-</td>
              </tr>
              <tr>
                <td></td>
                <td>PathPlannerModel</td>
                <td>v2.0.1</td>
                <td>2025-05-12 17:44</td>
                <td>Failed</td>
                <td>Failed</td>
              </tr>
            </tbody>
          </v-table>
        </v-sheet>
      </v-col>
    </v-card> -->
  </v-container>
</template>

<style scoped>
ul {
  list-style: none;
  padding-left: 0;
  margin-top: 8px;
}
li {
  margin-bottom: 8px;
}
</style>
