<script setup lang="ts">
/* ========= Imports ========= */
import { MlflowService } from "@/components/service/mlflow/MlflowService";
import { KubeflowRunService } from "@/components/service/management/KubeflowRunService";
import { KubeflowService } from "@/components/service/management/kubeflowService";
import { AdminService } from "@/components/service/management/adminService";
import { ExternalAuthControllerService } from "@/components/service/management/ExternalAuthControllerService";
import {
  computed,
  watch,
  ref,
  nextTick,
  onMounted,
  onBeforeUnmount,
} from "vue";
import Plotly from "plotly.js-dist-min";
import CompareRunsDialog from "@/components/atoms/organisms/CompareRunDialog.vue";
import DeploymentDialog from "@/components/atoms/organisms/DeploymentDialog.vue";
import IconDownloadBtn from "@/components/atoms/button/IconDownloadBtn.vue";
import IconDeployBtn from "@/components/atoms/button/IconDeployBtn.vue";

/* ========= Constants & Types ========= */
const AUTH_KEY = "external-auth";

type MlflowArtifactList = {
  root_uri?: string;
  files?: { path: string; is_dir?: boolean; file_size?: number }[];
  file_infos?: { path: string; is_dir?: boolean; file_size?: number }[];
  data?: { files?: { path: string; is_dir?: boolean; file_size?: number }[]; file_infos?: { path: string; is_dir?: boolean; file_size?: number }[] };
};
/** MLflow 응답에서 파일 목록 추출 (files / file_infos / data.files / data가 JSON문자열인 경우 등) */
function getArtifactFiles(obj: MlflowArtifactList | null | undefined): { path: string; is_dir: boolean; file_size?: number }[] {
  let resolved = obj;
  if (resolved?.data && typeof resolved.data === "string") {
    try {
      resolved = JSON.parse(resolved.data as unknown as string) as MlflowArtifactList;
    } catch {
      resolved = obj;
    }
  }
  const list =
    resolved?.files ?? resolved?.file_infos ?? (resolved as any)?.data?.files ?? (resolved as any)?.data?.file_infos ?? [];
  return (Array.isArray(list) ? list : []).map((f) => ({
    path: f.path ?? "",
    is_dir: !!f.is_dir,
    file_size: f.file_size,
  }));
}
type ArtifactRow = {
  path: string;
  is_dir?: boolean;
  file_size?: number;
  depth?: number;
};
type MetricKV = { key: string; value: number };
type RunDetailType = {
  info: any;
  data: { metrics: MetricKV[]; params?: any[]; tags?: any[] };
};
type ExternalAuth = { id: string; name: string; token: string };
type PackageOption = { label: string; value: string; raw: any };
type ArtifactGroup = { dir: string; files: ArtifactRow[] };
const FILE_ICON = "mdi-file-document-outline";
/* ========= Props/Emits ========= */
const props = defineProps<{ experimentInfo: any }>();
const emit = defineEmits<{ (e: "close"): void }>();

/* ========= Auth State ========= */
const loginDialog = ref(false);
const loginLoading = ref(false);
const loginForm = ref({ id: "", password: "" });
const loginError = ref("");
const isAuthenticated = ref(false);
const externalAuth = ref<ExternalAuth | null>(null);
const externalToken = computed(() => externalAuth.value?.token ?? "");

/* ========= Deployment State ========= */
const isEditVisible = ref(false);
const lastArtifactUri = ref<string>("");
const pendingArtifactPath = ref<string | null>(null);
const packageOptions = ref<PackageOption[]>([]);
const packagesLoading = ref(false);
const packagesError = ref("");
const shouldOpenDeploymentAfterLogin = ref(false);
const dlgHideArtifactPath = ref(false);
const dlgHideUploadFile = ref(false);
/* ========= MLflow State ========= */
const runs = ref<any[]>([]);
const loadingRuns = ref(false);
const selectedRunId = ref<string>("");
const loadingRunDetail = ref(false);
const runDetail = ref<RunDetailType | null>(null);
const currentExperimentId = ref<string>("");
const runDetailCache = ref<Map<string, RunDetailType>>(new Map());
/** 방금 로드한 run id (주기 새로고침 시 같은 run이면 runDetail 미리 비우지 않아 깜빡임 방지) */
const lastFetchedRunId = ref<string>("");
/** Run 상세 로딩 시 디버그용 (화면에 표시, 원인 확인 후 제거 가능) */
const fetchRunsDebug = ref<{
  parentRunId: string;
  byTagCalled: boolean;
  byTagRunCount: number;
  fallbackReason: string;
}>({ parentRunId: "", byTagCalled: false, byTagRunCount: 0, fallbackReason: "" });
const fetchRunsDebugText = computed(() => {
  const d = fetchRunsDebug.value;
  let s = `[디버그] parentRunId: ${d.parentRunId} | by-kubeflow-run-id 호출: ${d.byTagCalled ? "예" : "아니오"}`;
  if (d.byTagCalled) s += ` (응답 run 수: ${d.byTagRunCount})`;
  if (d.fallbackReason) s += ` | ${d.fallbackReason}`;
  return s;
});

/* ========= UI Tabs / Compare ========= */
const mainTab = ref<"details" | "viz" | "artifacts">("details");
const vizTab = ref<"metrics" | "scatter" | "box" | "contour">("metrics");
const deployOpenMode = ref<"artifact" | "upload">("artifact");
const compareDialog = ref(false);
const compareLoading = ref(false);
const compareChartMode = ref<"byMetric" | "byRun">("byMetric");
const compareSelectedRunIds = ref<string[]>([]);
const compareSelectedMetricKeys = ref<string[]>([]);
const normalizeValues = ref(false);
const baselineRunId = ref<string | null>(null);

const elMetrics = ref<HTMLDivElement | null>(null);
const elCompare = ref<HTMLDivElement | null>(null);

/* ========= Artifacts: Two-step(flat) ========= */
const artifactGroups = computed<ArtifactGroup[]>(() => {
  const rootFiles = (lvl1.value || []).filter((x) => !x.is_dir);
  const dirs = (lvl1.value || []).filter((x) => x.is_dir).map((x) => x.path);

  const groups: ArtifactGroup[] = [];
  for (const d of dirs) {
    const children = (lvl2.value || [])
      .filter((f) => f.path.startsWith(`${d}/`))
      .map((f) => {
        const rel = f.path.slice(d.length + 1);
        const depth = Math.max(rel.split("/").length - 1, 0);
        return { ...f, depth };
      })

      .sort((a, b) => {
        if (!!b.is_dir - !!a.is_dir) return !!b.is_dir - !!a.is_dir;
        return a.path.localeCompare(b.path);
      });

    groups.push({ dir: d, files: children });
  }

  if (rootFiles.length > 0) {
    groups.unshift({
      dir: "(root)",
      files: rootFiles.map((f) => ({ ...f, depth: 0 })),
    });
  }
  return groups;
});

const lvl1 = ref<ArtifactRow[]>([]);
const lvl2 = ref<ArtifactRow[]>([]);
const twoStepLoading = ref(false);
const twoStepError = ref("");
/** Artifacts 목록/다운로드에 사용한 MLflow run_id (KFP run id 선택 시 해석해서 저장) */
const artifactRunId = ref<string>("");
const artifactExperimentId = ref<string>("");
/** 방금 로드한 run id (같은 run 재조회 시 목록 미리 비우지 않아 깜빡임 방지) */
const lastFetchedArtifactRunId = ref<string>("");
/** 아티팩트 조회 시 MLflow run 연결 여부 (빈 목록일 때 안내용) */
const artifactMlflowResolved = ref<boolean>(false);

async function fetchArtifactsTwoStep(runId: string) {
  if (!runId) {
    lastFetchedArtifactRunId.value = "";
    lvl1.value = [];
    lvl2.value = [];
    twoStepError.value = "";
    artifactRunId.value = "";
    artifactExperimentId.value = "";
    artifactMlflowResolved.value = false;
    return;
  }
  const isSameRun = runId === lastFetchedArtifactRunId.value;
  if (!isSameRun) {
    lvl1.value = [];
    lvl2.value = [];
    twoStepError.value = "";
    artifactRunId.value = "";
    artifactExperimentId.value = "";
    artifactMlflowResolved.value = false;
    twoStepLoading.value = true;
  }
  try {
    // runId는 KFP run id. 아티팩트는 MLflow에 있으므로 kubeflow_run_id 태그로 MLflow run을 찾아 그 run_id로 조회해야 함.
    let mlflowRunId = runId;
    try {
      // KFP run id → kubeflow_run_id 태그로 MLflow run 1건 조회 → 그 run_id로 아티팩트 조회 (기본 흐름만 유지)
      const rawByTag = await MlflowService.getRunsByKubeflowRunId(runId);
      const byTagRes = parseJsonIfString(unwrapAxiosLike(rawByTag));
      const byTagRuns =
        byTagRes?.runs ?? byTagRes?.run_infos ?? byTagRes?.data?.runs ?? (Array.isArray(byTagRes) ? byTagRes : []);
      const first = Array.isArray(byTagRuns) && byTagRuns.length > 0 ? byTagRuns[0] : null;
      const resolvedRunId =
        first?.info?.run_id ?? first?.run_info?.run_id ?? first?.run_id;
      const resolvedExpId =
        first?.info?.experiment_id ?? first?.info?.experimentId ?? first?.run_info?.experiment_id ?? "";
      if (resolvedRunId) {
        mlflowRunId = resolvedRunId;
        artifactRunId.value = mlflowRunId;
        artifactExperimentId.value = String(resolvedExpId) || "";
        artifactMlflowResolved.value = true;
      } else {
        artifactRunId.value = runId;
        artifactMlflowResolved.value = false;
        lvl1.value = [];
        lvl2.value = [];
        lastFetchedArtifactRunId.value = runId;
        twoStepError.value =
          "이 Run에 연결된 MLflow run을 찾을 수 없습니다. kubeflow_run_id 태그가 있는 Run인지 확인하세요. " +
          "AutoFlow 백엔드( autoflow-server )가 MLflow 서버( mlflow.url )에 접근 가능해야 합니다.";
        return;
      }
    } catch (err: any) {
      artifactRunId.value = runId;
      artifactMlflowResolved.value = false;
      lvl1.value = [];
      lvl2.value = [];
      lastFetchedArtifactRunId.value = runId;
      twoStepError.value =
        "MLflow run 조회 실패: " + (err?.response?.data?.message || err?.message || String(err)) +
        ". AutoFlow 백엔드가 MLflow( mlflow.url )에 접근 가능한지 확인하세요.";
      return;
    }

    // 1) 루트(=MLflow run_id) 기준 1단계 (백엔드가 JSON 문자열로 내려줄 수 있음)
    const res1 = await MlflowService.artifact(mlflowRunId);
    const body1 = parseJsonIfString(unwrapAxiosLike(res1)) as MlflowArtifactList;
    const firstFiles = getArtifactFiles(body1) as ArtifactRow[];
    lvl1.value = firstFiles;

    // 2) 1단계의 "디렉터리"에 대해 하위(=2단계) 조회
    const dirPaths = firstFiles.filter((x) => x.is_dir).map((x) => x.path);
    let merged: ArtifactRow[] = [];
    if (dirPaths.length > 0) {
      const results = await Promise.all(
        dirPaths.map(async (p) => {
          const r = await MlflowService.artifact(mlflowRunId, p);
          return parseJsonIfString(unwrapAxiosLike(r)) as MlflowArtifactList;
        }),
      );
      for (let i = 0; i < results.length; i++) {
        const p = dirPaths[i];
        const r = results[i];
        const files2 = getArtifactFiles(r).map((f) => {
          // MLflow는 path 조회 시 상대 경로를 반환할 수 있음 → artifactGroups 매칭을 위해 부모 경로 보정
          let path = (f.path || "").trim();
          if (path && !path.startsWith(p + "/") && path !== p) path = p + "/" + path;
          return { path, is_dir: !!f.is_dir, file_size: f.file_size };
        }) as ArtifactRow[];
        merged.push(...files2);
      }

      // 3) 2단계 결과 중 "하위 폴더"가 있으면 그 하위(=3단계)도 한 번 더 조회 (lazy 없이 자동 1번만)
      const subDirs = merged.filter((x) => x.is_dir);
      if (subDirs.length > 0) {
        const results3 = await Promise.all(
          subDirs.map(async (sd) => {
            const r3 = await MlflowService.artifact(mlflowRunId, sd.path);
            return parseJsonIfString(unwrapAxiosLike(r3)) as MlflowArtifactList;
          }),
        );
        for (let j = 0; j < results3.length; j++) {
          const parentPath = subDirs[j].path;
          const r3 = results3[j];
          const files3 = getArtifactFiles(r3).map((f) => {
            let path = (f.path || "").trim();
            if (path && !path.startsWith(parentPath + "/") && path !== parentPath)
              path = parentPath + "/" + path;
            return { path, is_dir: !!f.is_dir, file_size: f.file_size };
          }) as ArtifactRow[];
          merged.push(...files3);
        }
      }
    }
    lvl2.value = merged;
    lastFetchedArtifactRunId.value = runId;
  } catch (e: any) {
    const status = e?.response?.status;
    lvl1.value = [];
    lvl2.value = [];
    lastFetchedArtifactRunId.value = runId;
    if (status === 500 || status === 404) {
      twoStepError.value =
        "아티팩트 목록 조회 실패 (" +
        (status === 404 ? "404" : "500") +
        "). MLflow artifact 저장소 및 백엔드 /api/mlflow/artifacts 접근을 확인하세요.";
    } else {
      twoStepError.value =
        e?.response?.data?.message || e?.message || "artifact 조회 실패";
    }
    console.error("[Artifacts][TwoStep] ERROR", e);
  } finally {
    if (!isSameRun) twoStepLoading.value = false;
  }
}

/* ========= Helpers ========= */
const safeParse = <T = any,>(v: any): T | null => {
  try {
    return typeof v === "string" ? (JSON.parse(v) as T) : (v as T);
  } catch {
    return null;
  }
};
const unwrapAxiosLike = (res: any) => res?.data ?? res;

/** 백엔드가 JSON 문자열로 내려줄 때 파싱 (MLflow API 프록시 응답 대응) */
function parseJsonIfString(val: any): any {
  if (typeof val === "string") {
    try {
      return JSON.parse(val) as any;
    } catch {
      return val;
    }
  }
  return val;
}

const normalizeRun = (res: any): RunDetailType | null => {
  let obj = unwrapAxiosLike(res);
  if (!obj?.run && (res?.response || res?.responseText)) {
    const parsed = safeParse(res.response) ?? safeParse(res.responseText);
    if (parsed?.run) obj = parsed;
  }
  const v = obj?.run ?? obj;
  return v?.info && v?.data ? v : null;
};

const pickExpName = (v: any) =>
  v?.expName || v?.experiment || v?.name || v?.raw?.displayName || "";

/** MLflow run에서 태그 값 추출 (info/data, run_info/run_data 등 응답 형식 모두 대응) */
const getTag = (run: any, key: string): string | undefined => {
  const tagSources = [
    run?.data?.tags,
    run?.run_data?.tags,
    run?.info?.tags,
    run?.run_info?.tags,
  ].filter(Boolean);
  for (const tags of tagSources) {
    if (Array.isArray(tags)) {
      const v = tags.find((t: any) => t?.key === key)?.value;
      if (v !== undefined && v !== null) return String(v);
    }
    if (tags && typeof tags === "object" && tags[key] != null) {
      return String(tags[key]);
    }
  }
  return undefined;
};

/** MLflow run이 이 Kubeflow 실행과 연결되었는지 (experiment_id 또는 kubeflow_run_id 태그) */
function runMatchesKubeflowRunId(run: any, parentRunId: string): boolean {
  if (!parentRunId) return false;
  return (
    getTag(run, "experiment_id") === parentRunId ||
    getTag(run, "kubeflow_run_id") === parentRunId
  );
}

const pickParentRunId = (v: any) =>
  String(
    [
      v?.run_id,
      v?.runId,
      v?.raw?.run_id,
      v?.raw?.runId,
      v?.raw?.kfp_run_id,
      v?.raw?.kubeflow_run_id,
      v?.raw?.id,
    ].filter(Boolean)[0] ?? "",
  );

const getProjectId = (): number => {
  const v = Number(localStorage.getItem("projectId"));
  return Number.isFinite(v) ? v : 0;
};

/** 백엔드(KFP) Run 한 건을 runs[] 형식으로 변환 (run_id = KFP run id) */
function backendRunToRunShape(r: any) {
  const createdAt = r?.createdAt ?? r?.startTime ?? r?.startedAt ?? r?.start_time;
  const finishedAt = r?.finishedAt ?? r?.endTime ?? r?.end_time ?? r?.end_time;
  return {
    info: {
      run_name: r?.displayName ?? r?.name ?? r?.runId ?? "-",
      run_id: r?.runId ?? r?.run_id ?? r?.id,
      start_time: createdAt,
      end_time: finishedAt,
      status: r?.state ?? r?.status ?? r?.lifecycle_state,
    },
    data: { tags: r?.data?.tags ?? [] },
    raw: r,
  };
}

const fmtNumber = (v: number | null, digits = 3) => {
  if (v == null || !Number.isFinite(v)) return "";
  const abs = Math.abs(v);
  if (abs >= 1000) return v.toLocaleString();
  if (abs >= 1) return v.toFixed(digits);
  if (abs >= 0.01) return v.toFixed(digits + 1);
  return v.toExponential(2);
};

const labelOfRun = (r: RunDetailType) => r.info.run_name || r.info.run_id;
const valueOf = (run: RunDetailType, key: string): number | null => {
  const m = run.data.metrics.find((x) => x.key === key)?.value;
  return Number.isFinite(m as number) ? Number(m) : null;
};

const fileExt = (name: string) => {
  const n = (name || "").toLowerCase().trim();
  const idx = n.lastIndexOf(".");
  return idx >= 0 ? n.slice(idx + 1) : "";
};

const bytes = (n?: number) =>
  typeof n === "number" && isFinite(n)
    ? n < 1024
      ? `${n} B`
      : n < 1024 ** 2
        ? `${(n / 1024).toFixed(1)} KB`
        : n < 1024 ** 3
          ? `${(n / 1024 ** 2).toFixed(1)} MB`
          : `${(n / 1024 ** 3).toFixed(2)} GB`
    : "—";

/* ========= Derived (computed) ========= */
const currentRunId = computed(() =>
  (pickParentRunId(props.experimentInfo) ?? "").trim(),
);
const runItems = computed(() => {
  const current = currentRunId.value;
  return (runs.value || []).map((r: any) => {
    const id = r?.info?.run_id || r?.info?.run_uuid || "";
    const name = r?.info?.run_name || r?.info?.run_id || "";
    const isCurrent = !!id && id === current;
    return {
      value: id,
      label: name,
      isCurrent,
    };
  });
});

/** runId → Details 탭 Name과 동일(실행 시 입력한 이름, 예: auto26run). */
const runIdToExecutionName = computed(() => {
  const map: Record<string, string> = {};
  (runs.value || []).forEach((r: any) => {
    const id = r?.info?.run_id || r?.info?.run_uuid || "";
    const name =
      r?.info?.run_name ||
      r?.raw?.displayName ||
      r?.raw?.name ||
      r?.raw?.runName ||
      "";
    if (id) map[id] = name || id;
  });
  return map;
});

const selectedMetrics = computed<MetricKV[]>(() =>
  (runDetail.value?.data?.metrics ?? []).map((m) => ({
    key: m.key,
    value: m.value,
  })),
);

/** run 응답에 experiment name이 없을 수 있음. experiment_id로 Experiments 목록 조회해 채움 */
const mlflowExperimentNameByExpId = ref<string>("");

const mlflowExperimentName = computed(() => {
  if (!runDetail.value) return "—";
  const fromRun =
    runDetail.value.info?.experiment_name ??
    runDetail.value.info?.experimentName ??
    getTag(runDetail.value, "mlflow.experiment.name");
  if (fromRun) return fromRun;
  return mlflowExperimentNameByExpId.value || "—";
});

/* ========= URI 조합 & 다운로드/배포 ========= */
function buildArtifactUri(fullPath: string) {
  const expId =
    artifactExperimentId.value ||
    currentExperimentId.value ||
    runDetail.value?.info?.experiment_id ||
    runs.value[0]?.info?.experiment_id ||
    "";
  const runId =
    artifactRunId.value ||
    runDetail.value?.info?.run_id ||
    selectedRunId.value ||
    "";
  return `${expId}/${runId}/artifacts/${fullPath}`;
}

async function onClickArtifact(fullPath: string) {
  const runId =
    artifactRunId.value ||
    runDetail.value?.info?.run_id ||
    selectedRunId.value ||
    "";
  try {
    artifactsLoading.value = true;
    await MlflowService.downloadArtifact(runId, fullPath);
  } finally {
    artifactsLoading.value = false;
  }
}

/* ========= Fetchers ========= */
const findFromList = (runId: string) =>
  runs.value.find(
    (r: any) =>
      r?.info?.run_id === runId ||
      r?.info?.run_uuid === runId ||
      r?.info?.run_name === runId,
  ) ?? null;

/** KFP run id로 MLflow run 조회 후 RunDetailType 반환 (메트릭은 MLflow 기준) */
async function ensureRunDetail(runId: string): Promise<RunDetailType | null> {
  if (!runId) return null;
  const cached = runDetailCache.value.get(runId);
  if (cached) return cached;
  let v: any = null;
  try {
    const rawByTag = await MlflowService.getRunsByKubeflowRunId(runId);
    const byTagRes = parseJsonIfString(unwrapAxiosLike(rawByTag));
    const byTagRuns =
      byTagRes?.runs ?? byTagRes?.data?.runs ?? (Array.isArray(byTagRes) ? byTagRes : []);
    const mlflowRun = Array.isArray(byTagRuns) && byTagRuns.length > 0 ? byTagRuns[0] : null;
    if (mlflowRun?.info?.run_id) {
      const res = await MlflowService.getExperimentRun(mlflowRun.info.run_id);
      v = normalizeRun(res);
    }
  } catch {
    // KFP run id가 아니거나 조회 실패 시 MLflow run id로 직접 조회
  }
  if (!v) {
    const res = await MlflowService.getExperimentRun(runId);
    v = normalizeRun(res) || findFromList(runId);
  }
  if (!v) return null;
  const shaped: RunDetailType = {
    info: v.info ?? {},
    data: {
      metrics: (v.data?.metrics ?? []).map((m: any) => ({
        key: m.key,
        value: Number(m.value),
      })),
      params: v.data?.params,
      tags: v.data?.tags,
    },
  };
  runDetailCache.value.set(runId, shaped);
  return shaped;
}

/** KFP experiment 이름 기준으로 같은 experiment의 run 목록 조회 (메트릭은 run별 MLflow 연결로 표시) */
async function fetchRuns(expName?: string) {
  const parentRunId = (pickParentRunId(props.experimentInfo) ?? "").trim();
  fetchRunsDebug.value = {
    parentRunId: parentRunId || "(비어 있음)",
    byTagCalled: false,
    byTagRunCount: 0,
    fallbackReason: "",
  };
  loadingRuns.value = true;
  try {
    if (!expName?.trim()) {
      fetchRunsDebug.value.fallbackReason = "experiment 이름 없음";
      runs.value = [];
      selectedRunId.value = "";
      return;
    }
    const projectId = getProjectId();
    if (!projectId) {
      fetchRunsDebug.value.fallbackReason = "projectId 없음";
      runs.value = [];
      selectedRunId.value = "";
      return;
    }
    const res = await KubeflowRunService.search({
      projectId: projectId as any,
      page: 0,
      size: 1000,
      sortField: "createdAt",
      sortDirection: "DESC",
    });
    const result = res?.data ?? res;
    let list: any[] = [];
    if (Array.isArray(result)) list = result;
    else if (Array.isArray(result?.content)) list = result.content;
    else if (Array.isArray(result?.data)) list = result.data;
    else if (Array.isArray(result?.runs)) list = result.runs;

    if (list.length === 0) {
      runs.value = [];
      selectedRunId.value = "";
      fetchRunsDebug.value.fallbackReason = "KFP run 목록 없음";
      return;
    }

    const expIds = [
      ...new Set(
        list.map((r: any) => r.experimentId ?? r.experiment_id ?? r.experiment?.id).filter(Boolean),
      ),
    ];
    const expNameById: Record<string, string> = {};
    await Promise.all(
      expIds.map(async (id: string | number) => {
        try {
          const expRes = await KubeflowService.experimentData(String(id));
          const body = expRes?.data ?? expRes ?? {};
          expNameById[String(id)] =
            body.display_name ?? body.name ?? body.experiment_name ?? String(id);
        } catch {
          expNameById[String(id)] = String(id);
        }
      }),
    );

    const expNameLower = String(expName).toLowerCase().trim();
    const filtered = list.filter((r: any) => {
      const runExpId = r.experimentId ?? r.experiment_id ?? r.experiment?.id;
      const runExpName = (
        expNameById[String(runExpId)] ??
        r.experiment?.displayName ??
        r.experiment?.name ??
        ""
      )
        .toLowerCase()
        .trim();
      return runExpName === expNameLower;
    });

    const shaped = filtered.map(backendRunToRunShape);
    const sorted = [...shaped].sort(
      (a, b) => (b?.info?.start_time ?? 0) - (a?.info?.start_time ?? 0),
    );
    runs.value = sorted;

    const currentInList = sorted.find(
      (r: any) => (r?.info?.run_id ?? "") === parentRunId,
    );
    selectedRunId.value = currentInList
      ? currentInList.info.run_id
      : sorted[0]?.info?.run_id ?? "";

    if (sorted.length > 0) {
      fetchRunsDebug.value.fallbackReason = "KFP experiment 기준";
      try {
        const firstMl = await MlflowService.getRunsByKubeflowRunId(
          sorted[0]?.info?.run_id ?? "",
        );
        const firstBody = parseJsonIfString(unwrapAxiosLike(firstMl));
        const firstRuns =
          firstBody?.runs ??
          firstBody?.data?.runs ??
          (Array.isArray(firstBody) ? firstBody : []);
        if (Array.isArray(firstRuns) && firstRuns.length > 0) {
          currentExperimentId.value =
            String(
              firstRuns[0]?.info?.experiment_id ?? firstRuns[0]?.info?.experimentId ?? "",
            ) || currentExperimentId.value;
        }
      } catch {
        // optional: MLflow experiment id for artifacts
      }
    }
  } catch (e) {
    fetchRunsDebug.value.fallbackReason = "KFP run 조회 실패: " + String(e);
    runs.value = [];
    selectedRunId.value = "";
  } finally {
    loadingRuns.value = false;
  }
}

/** 선택한 run의 메트릭 표시 (KFP run id면 MLflow run으로 해석). 정보 없으면 표시하지 않음 */
async function fetchRunDetail(runId: string) {
  if (!runId) {
    runDetail.value = null;
    lastFetchedRunId.value = "";
    mlflowExperimentNameByExpId.value = "";
    await nextTick();
    drawSelectedRunChart();
    return;
  }
  const isSameRun = runId === lastFetchedRunId.value;
  if (!isSameRun) {
    runDetail.value = null;
    mlflowExperimentNameByExpId.value = "";
  }
  if (!isSameRun) loadingRunDetail.value = true;
  try {
    let v: any = null;
    try {
      const rawByTag = await MlflowService.getRunsByKubeflowRunId(runId);
      const byTagRes = parseJsonIfString(unwrapAxiosLike(rawByTag));
      const byTagRuns =
        byTagRes?.runs ?? byTagRes?.data?.runs ?? (Array.isArray(byTagRes) ? byTagRes : []);
      const mlflowRun =
        Array.isArray(byTagRuns) && byTagRuns.length > 0 ? byTagRuns[0] : null;
      if (mlflowRun?.info?.run_id) {
        const res = await MlflowService.getExperimentRun(mlflowRun.info.run_id);
        v = normalizeRun(res);
      }
    } catch {
      // runId가 KFP id가 아니면 MLflow run id로 직접 조회
    }
    if (!v) {
      try {
        const res = await MlflowService.getExperimentRun(runId);
        v = normalizeRun(res);
      } catch {
        // 없음 → runDetail 유지 null, 메트릭 블록 숨김
      }
    }
    runDetail.value = v ?? null;
    lastFetchedRunId.value = runId;
    if (runDetail.value) runDetailCache.value.set(runId, runDetail.value);
    if (runDetail.value?.info?.experiment_id && !runDetail.value.info?.experiment_name && !runDetail.value.info?.experimentName && !getTag(runDetail.value, "mlflow.experiment.name")) {
      try {
        const rawExp = await MlflowService.getExperiments();
        const expRes = parseJsonIfString(unwrapAxiosLike(rawExp));
        const list = expRes?.experiments ?? expRes?.data?.experiments ?? (Array.isArray(expRes) ? expRes : []);
        const expId = String(runDetail.value.info.experiment_id ?? runDetail.value.info.experimentId ?? "");
        const exp = (Array.isArray(list) ? list : []).find(
          (e: any) => String(e?.experiment_id ?? e?.experimentId ?? e?.id ?? "") === expId,
        );
        mlflowExperimentNameByExpId.value = exp?.name ?? "";
      } catch {
        mlflowExperimentNameByExpId.value = "";
      }
    } else {
      mlflowExperimentNameByExpId.value = "";
    }
    await nextTick();
    drawSelectedRunChart();
  } finally {
    if (!isSameRun) loadingRunDetail.value = false;
  }
}

/* ========= Charts ========= */
function drawSelectedRunChart() {
  if (!elMetrics.value) return;
  const mm = selectedMetrics.value.filter(
    (m) => typeof m.value === "number" && isFinite(m.value),
  );
  const x = mm.map((m) => m.key);
  const y = mm.map((m) => m.value);

  if (x.length === 0) {
    Plotly.purge(elMetrics.value);
    elMetrics.value.innerHTML =
      '<div style="padding:16px;color:#999;text-align:center;">No Data</div>';
    return;
  }

  Plotly.react(
    elMetrics.value,
    [
      {
        type: "bar",
        x,
        y,
        text: y.map((v) => fmtNumber(v, 4)),
        textposition: "auto",
        hovertemplate: "%{x}: %{text}<extra></extra>",
      },
    ],
    {
      margin: { t: 40, r: 20, b: 60, l: 50 },
      height: 400,
      showlegend: false,
      paper_bgcolor: "rgba(0,0,0,0)",
      plot_bgcolor: "rgba(0,0,0,0)",
      font: { color: "#E0E0E0" },
      xaxis: {
        automargin: true,
        gridcolor: "rgba(255,255,255,0.08)",
        zerolinecolor: "rgba(255,255,255,0.18)",
        linecolor: "rgba(255,255,255,0.18)",
      },
      yaxis: {
        rangemode: "tozero",
        gridcolor: "rgba(255,255,255,0.08)",
        zerolinecolor: "rgba(255,255,255,0.18)",
        linecolor: "rgba(255,255,255,0.18)",
      },
    },
    { displayModeBar: false, responsive: true },
  );
}

const commonLayout: Partial<any> = {
  autosize: true,
  margin: { t: 28, r: 12, b: 56, l: 56 },
  paper_bgcolor: "rgba(0,0,0,0)",
  plot_bgcolor: "rgba(0,0,0,0)",
  font: { color: "#E0E0E0" },
  xaxis: { automargin: true, gridcolor: "rgba(255,255,255,0.08)" },
  yaxis: {
    rangemode: "tozero",
    gridcolor: "rgba(255,255,255,0.08)",
    automargin: true,
  },
  legend: {
    orientation: "h",
    x: 0,
    y: 1.12,
    yanchor: "bottom",
    xanchor: "left",
  },
};

function createTracesByMetric(metricKeys: string[], runsData: RunDetailType[]) {
  const runLabels = runsData.map(labelOfRun);
  return runsData.map((run, i) => {
    const yRaw = metricKeys.map((k) => valueOf(run, k));
    const y = normalizeValues.value ? normalizeArray(yRaw) : yRaw;
    const text = yRaw.map((v) => fmtNumber(v));
    return {
      type: "bar",
      name: runLabels[i],
      x: metricKeys,
      y,
      text: normalizeValues.value ? undefined : text,
      textposition: "auto",
      customdata: text,
      hovertemplate: `${runLabels[i]}<br>%{x}: %{customdata}<extra></extra>`,
    };
  });
}
function createTracesByRun(metricKeys: string[], runsData: RunDetailType[]) {
  const rowLabels = runsData.map(labelOfRun);
  return metricKeys.map((key) => {
    const yRaw = rowLabels.map((_, i) => valueOf(runsData[i], key));
    const y = normalizeValues.value ? normalizeArray(yRaw) : yRaw;
    const text = yRaw.map((v) => fmtNumber(v));
    return {
      type: "bar",
      name: key,
      x: rowLabels,
      y,
      text: normalizeValues.value ? undefined : text,
      textposition: "auto",
      customdata: text,
      hovertemplate: `${key}: %{customdata}<extra>%{x}</extra>`,
    };
  });
}
const normalizeArray = (vals: (number | null)[]) => {
  const xs = vals.filter((v): v is number => Number.isFinite(v as number));
  if (xs.length === 0) return vals;
  const min = Math.min(...xs),
    max = Math.max(...xs);
  if (max === min) return vals.map((v) => (v == null ? v : 1));
  return vals.map((v) => (v == null ? v : (v - min) / (max - min)));
};
function drawCompareChart() {
  if (!elCompare.value) return;
  const metricKeys = activeMetricKeys.value;
  const runsData = compareRuns.value;
  if (!metricKeys.length || !runsData.length) {
    elCompare.value.innerHTML =
      '<div style="padding:16px;color:#999;text-align:center;">No Data</div>';
    return;
  }
  const traces =
    compareChartMode.value === "byMetric"
      ? createTracesByMetric(metricKeys, runsData)
      : createTracesByRun(metricKeys, runsData);

  Plotly.react(
    elCompare.value,
    traces,
    { ...commonLayout, barmode: "group" },
    { displayModeBar: false, responsive: true },
  );
}

/* ========= Compare Derived/Actions ========= */
const compareRuns = computed<RunDetailType[]>(
  () =>
    compareSelectedRunIds.value
      .map((id) => runDetailCache.value.get(id))
      .filter(Boolean) as RunDetailType[],
);
const metricsKeySetByRun = computed(() =>
  compareRuns.value.map((r) => new Set(r.data.metrics.map((m) => m.key))),
);
const commonMetricKeys = computed<string[]>(() => {
  if (metricsKeySetByRun.value.length === 0) return [];
  const [first, ...rest] = metricsKeySetByRun.value;
  let inter = new Set(first);
  rest.forEach((s) => (inter = new Set([...inter].filter((k) => s.has(k)))));
  return Array.from(inter).sort();
});
const activeMetricKeys = computed(() =>
  compareSelectedMetricKeys.value.length
    ? compareSelectedMetricKeys.value
    : commonMetricKeys.value,
);

function openCompareDialog() {
  compareSelectedRunIds.value = Array.from(
    new Set([selectedRunId.value].filter(Boolean)),
  );
  compareSelectedMetricKeys.value = [];
  compareDialog.value = true;
}
async function loadCompareData() {
  if (!compareDialog.value) return;
  compareLoading.value = true;
  try {
    await Promise.all(compareSelectedRunIds.value.map(ensureRunDetail));
    if (compareSelectedMetricKeys.value.length === 0) {
      compareSelectedMetricKeys.value = commonMetricKeys.value.slice(0, 6);
    }
    await nextTick();
    drawCompareChart();
  } finally {
    compareLoading.value = false;
  }
}

/* ========= Auth ========= */
function restoreAuthFromStorage() {
  const raw = localStorage.getItem(AUTH_KEY);
  if (!raw) return;
  try {
    const parsed = JSON.parse(raw) as ExternalAuth;
    if (parsed?.token) {
      isAuthenticated.value = true;
      externalAuth.value = parsed;
    }
  } catch {
    /* no-op */
  }
}
function openLoginManually() {
  shouldOpenDeploymentAfterLogin.value = false;
  pendingArtifactPath.value = null;
  loginDialog.value = true;
}

const handleLogout = () => {
  localStorage.removeItem(AUTH_KEY);
  isAuthenticated.value = false;
  externalAuth.value = null;
  packageOptions.value = [];
};
const handleLogin = async () => {
  try {
    loginLoading.value = true;
    loginError.value = "";
    const { id, password } = loginForm.value;
    if (!id || !password) {
      loginError.value = "ID와 비밀번호를 입력하세요.";
      return;
    }

    const res = await ExternalAuthControllerService.signIn(id, password);
    const raw = res?.data ?? res;
    if (!raw) {
      loginError.value = "로그인에 실패했습니다. (서버 응답이 비어있습니다)";
      return;
    }
    const payload = Object.prototype.hasOwnProperty.call(raw, "data")
      ? raw.data
      : raw;
    if (!payload?.token) {
      loginError.value = "로그인에 실패했습니다. 아이디/비밀번호를 확인하세요.";
      return;
    }

    const toSave: ExternalAuth = {
      id: payload.id ?? id,
      name: payload.name ?? id,
      token: payload.token,
    };
    localStorage.setItem(AUTH_KEY, JSON.stringify(toSave));
    externalAuth.value = toSave;
    isAuthenticated.value = true;

    loginDialog.value = false;
    loginForm.value = { id: "", password: "" };

    if (shouldOpenDeploymentAfterLogin.value) {
      shouldOpenDeploymentAfterLogin.value = false;
      await nextTick();
      openDeploymentModal();
    }
  } catch (e: any) {
    loginError.value =
      e?.response?.data?.message || e?.message || "로그인에 실패했습니다.";
  } finally {
    loginLoading.value = false;
  }
};

/* ========= Deployment Modal ========= */
const openDeploymentModal = async (
  fullPath?: string,
  mode: "upload" | "artifact" = fullPath ? "artifact" : "upload",
) => {
  if (fullPath) {
    const uri = buildArtifactUri(fullPath);
    lastArtifactUri.value = uri;
    pendingArtifactPath.value = uri;
  } else {
    lastArtifactUri.value = "";
    pendingArtifactPath.value = null;
  }

  if (!isAuthenticated.value) {
    shouldOpenDeploymentAfterLogin.value = true;
    loginDialog.value = true;
    return;
  }

  if (mode === "artifact") {
    dlgHideArtifactPath.value = false;
    dlgHideUploadFile.value = true;
    if (!fullPath && pendingArtifactPath.value)
      lastArtifactUri.value = pendingArtifactPath.value;
  } else {
    dlgHideArtifactPath.value = true;
    dlgHideUploadFile.value = false;
    lastArtifactUri.value = "";
  }

  isEditVisible.value = true;
  packagesError.value = "";
  packageOptions.value = [];
  const auth =
    externalAuth.value ||
    (() => {
      try {
        const raw = localStorage.getItem(AUTH_KEY);
        return raw ? (JSON.parse(raw) as ExternalAuth) : null;
      } catch {
        return null;
      }
    })();
  if (!auth?.id || !auth?.token) {
    packagesError.value = "로그인 정보가 없습니다.";
    return;
  }

  try {
    packagesLoading.value = true;
    const res = await ExternalAuthControllerService.search(auth.id, auth.token);
    const body = res?.data ?? res;
    const list = body?.data?.data ?? body?.data ?? body?.result ?? body ?? [];
    const arr = Array.isArray(list) ? list : [];
    packageOptions.value = arr.map((it: any) => ({
      label: it?.package_name || it?.package_id || "Unknown",
      value: it?.package_id,
      raw: it,
    }));
  } catch (e: any) {
    packagesError.value =
      e?.response?.data?.message || e?.message || "패키지 조회 실패";
  } finally {
    packagesLoading.value = false;
  }
};

const closeCreateModal = () => {
  isEditVisible.value = false;
};
const saveData = (payload: any) => {
  console.log("[DeploymentDialog payload]", payload);
  isEditVisible.value = false;
};

/* ========= Timeline (fallback) ========= */
const rawHistory = computed<any[]>(() => {
  const h =
    props.experimentInfo?.raw?.state_history ??
    props.experimentInfo?.raw?.stateHistory ??
    props.experimentInfo?.state_history ??
    props.experimentInfo?.stateHistory ??
    [];
  if (Array.isArray(h) && h.length > 0) return h;

  const startIso =
    props.experimentInfo?.startTime &&
    !isNaN(new Date(props.experimentInfo.startTime).getTime())
      ? new Date(props.experimentInfo.startTime).toISOString()
      : undefined;

  const latest = [...(runs.value ?? [])].sort(
    (a, b) => (b?.info?.start_time ?? 0) - (a?.info?.start_time ?? 0),
  )[0];
  const endIso =
    latest?.info?.end_time && isFinite(Number(latest.info.end_time))
      ? new Date(Number(latest.info.end_time)).toISOString()
      : undefined;

  const status = String(props.experimentInfo?.status ?? "").toUpperCase();
  const out: Array<{ state: string; update_time?: string }> = [];
  if (startIso) out.push({ state: "PENDING", update_time: startIso });
  if (startIso) out.push({ state: "RUNNING", update_time: startIso });
  if (status === "SUCCEEDED" || status === "FAILED")
    out.push({ state: status, update_time: endIso ?? startIso });
  return out;
});
const history = computed(() =>
  rawHistory.value
    .slice()
    .filter((h) => h && h.state)
    .sort(
      (a: any, b: any) =>
        new Date(a.update_time || 0).getTime() -
        new Date(b.update_time || 0).getTime(),
    ),
);
const fmt = (iso?: string) => (iso ? new Date(iso).toLocaleString() : "—");

/* ========= Lifecycle & Watchers ========= */
async function refreshIfActive() {
  await fetchRuns(pickExpName(props.experimentInfo));
  await fetchRunDetail(selectedRunId.value);
}
watch(
  () => mainTab.value,
  async (t) => {
    if (t === "viz" || t === "artifacts") await refreshIfActive();
    if (t === "artifacts" && selectedRunId.value) {
      await fetchArtifactsTwoStep(selectedRunId.value);
    }
  },
  { immediate: true },
);
watch(
  () => props.experimentInfo,
  async () => {
    if (mainTab.value === "viz" || mainTab.value === "artifacts")
      await refreshIfActive();
  },
);
watch(selectedRunId, async (id) => {
  fetchRunDetail(id);
  if (mainTab.value === "artifacts" && id) {
    await fetchArtifactsTwoStep(id);
  }
});
watch(vizTab, async (t) => {
  if (mainTab.value === "viz" && t === "metrics") {
    await nextTick();
    drawSelectedRunChart();
    if (elMetrics.value) Plotly.Plots.resize(elMetrics.value);
  }
});
watch([compareSelectedRunIds, compareSelectedMetricKeys], async () => {
  if (!compareDialog.value) return;
  await loadCompareData();
});
watch(compareDialog, (open) => {
  if (open) loadCompareData();
});

const onResize = () => {
  if (mainTab.value === "viz" && vizTab.value === "metrics" && elMetrics.value)
    Plotly.Plots.resize(elMetrics.value);
  if (elCompare.value) Plotly.Plots.resize(elCompare.value);
};

const POLL_INTERVAL_MS = 10 * 1000;
let pollTimerId: ReturnType<typeof setInterval> | null = null;
function startRunPolling() {
  if (pollTimerId != null) return;
  pollTimerId = setInterval(() => {
    if (pickExpName(props.experimentInfo) || pickParentRunId(props.experimentInfo))
      refreshIfActive();
  }, POLL_INTERVAL_MS);
}
function stopRunPolling() {
  if (pollTimerId != null) {
    clearInterval(pollTimerId);
    pollTimerId = null;
  }
}

onMounted(() => {
  restoreAuthFromStorage();
  window.addEventListener("resize", onResize);
  startRunPolling();
});
onBeforeUnmount(() => {
  window.removeEventListener("resize", onResize);
  stopRunPolling();
});

const artifactsLoading = ref(false);

/* ========= Run Pod Log (별도 창) ========= */
const logDialogOpen = ref(false);
const logContent = ref("");
const logLoading = ref(false);
/** false: KFP처럼 해당 Run의 한 스텝(실패 스텝 우선). true: 모든 스텝 로그 */
const logAllSteps = ref(false);

const LOG_WSL_HINT = `
---
[로그가 비어 있거나 실패할 때]
• WSL에서 kubectl logs 는 되는데 UI만 안 되면: 관리 서버(Spring)가 Windows에서 돌면 Windows용 kubeconfig를 씁니다. 서버를 WSL에서 실행하거나, Windows의 KUBECONFIG를 WSL과 동일하게 맞추세요. (문서: docs/KFP_EXECUTION_LOGS.md)
`.trim();

function looksLikeLogFailure(text: string): boolean {
  if (text == null || typeof text !== "string") return true;
  const t = text.trim();
  if (t.length < 40) return true;
  if (t.startsWith("로그 조회 실패")) return true;
  if (t.includes("K8s Pod 로그는 비활성화")) return true;
  if (t.includes("이 Run에 해당하는 Pod가 없습니다")) return true;
  if (t.includes("admin.k8s.namespace 미설정")) return true;
  if (t.includes("연결 실패") || t.includes("Forbidden") || t.includes("Unauthorized")) return true;
  return false;
}

/** Run 라벨로 Pod 목록 → system-container-impl 우선으로 직접 로그 (백엔드 경로와 다른 우회) */
async function tryLogsViaPodList(runId: string): Promise<string | null> {
  try {
    const res = await AdminService.getPodsByRunId(runId);
    const d = (res as any)?.data ?? res;
    const ns = String(d?.namespace || "kubeflow").trim() || "kubeflow";
    const pods: { name?: string }[] = Array.isArray(d?.pods) ? d.pods : [];
    let names = pods.map((p) => p?.name).filter((n): n is string => !!n && String(n).length > 0);
    const impls = names.filter((n) => n.includes("system-container-impl"));
    if (impls.length > 0) names = impls;
    names = [...names].sort((a, b) => a.localeCompare(b));
    for (let i = names.length - 1; i >= 0; i--) {
      const pod = names[i];
      const lr = await AdminService.getPodLog({
        namespace: ns,
        pod,
        tailLines: 0,
      });
      const log = (lr as any)?.data ?? lr ?? "";
      if (
        typeof log === "string" &&
        log.length > 100 &&
        !log.trimStart().startsWith("로그 조회 실패")
      ) {
        return `-- kubectl logs ${pod} -n ${ns} (Run Pod 목록에서 직접 조회) --\n\n${log}`;
      }
    }
  } catch {
    /* ignore */
  }
  return null;
}

async function openLogDialog() {
  const runId = (pickParentRunId(props.experimentInfo) ?? "").trim();
  const justOpened = !logDialogOpen.value;
  logDialogOpen.value = true;
  if (justOpened) logAllSteps.value = false;
  logContent.value = "";
  if (!runId) {
    logContent.value = "Run ID가 없습니다.";
    return;
  }
  logLoading.value = true;
  logContent.value = logAllSteps.value
    ? "전체 스텝 로그 불러오는 중…"
    : "해당 스텝 로그 불러오는 중… (실패 스텝 우선, KFP 노드 로그와 동일)";
  try {
    const workflowName = String((props as any)?.experimentInfo?.workflow ?? "").trim();
    const res = await AdminService.getPodLogsByRunId(runId, {
      tailLines: 0,
      allSteps: logAllSteps.value,
      workflowName: workflowName || undefined,
    });
    let text = (res as any)?.data ?? res ?? "";
    text = typeof text === "string" ? text : "로그를 불러올 수 없습니다.";
    if (looksLikeLogFailure(text) && !logAllSteps.value) {
      const viaList = await tryLogsViaPodList(runId);
      if (viaList && !looksLikeLogFailure(viaList)) {
        text = viaList;
      } else {
        text = text + "\n\n" + LOG_WSL_HINT;
      }
    } else if (looksLikeLogFailure(text) && logAllSteps.value) {
      text = text + "\n\n" + LOG_WSL_HINT;
    }
    logContent.value = text;
  } catch {
    const viaList = await tryLogsViaPodList(runId);
    logContent.value =
      (viaList && !looksLikeLogFailure(viaList) ? viaList : "로그 조회 실패") +
      "\n\n" +
      LOG_WSL_HINT;
  } finally {
    logLoading.value = false;
  }
}

async function reloadRunLog() {
  if (!logDialogOpen.value) return;
  await openLogDialog();
}

/** 업데이트: 다이얼로그 열린 채 전체 로그 다시 조회 */
async function refreshRunLog() {
  if (!logDialogOpen.value) return;
  await openLogDialog();
}

function logLineHighlightClass(line: string): string {
  if (!line) return "";
  if (/error\s*=\s*["']?<nil>["']?/i.test(line)) return "";
  const tr = line.trim();

  /** Python / pytest / 노트북 스타일 예외·트레이스 */
  const isPythonOrTestError =
    /traceback \(most recent call last\)/i.test(line) ||
    /Cell In\[\d+\], line \d+/i.test(line) ||
    /^\s*-{2,}>\s*\d+/.test(line) ||
    /^\s*File ["'][^"']+["'], line \d+/i.test(line) ||
    /during handling of the above exception/i.test(line) ||
    /the above exception was the direct cause/i.test(line) ||
    /for a raised exception/i.test(line) ||
    /\braised exception\b/i.test(line) ||
    /\braise\s+/.test(line) ||
    /\w+Error:\s*\S/.test(line) ||
    /\w+Exception:\s*\S/.test(line) ||
    /:\s*\w+Error:\s/.test(line) ||
    /\b(AssertionError|KeyboardInterrupt|SystemExit|BaseException)\b/.test(line) ||
    /^E\s{2,}\S/.test(tr) ||
    /^ERROR\s+(at|collecting)/i.test(tr) ||
    /\bFAILED\b.*\b(Error|Exception|fail)/i.test(line);

  if (isPythonOrTestError) {
    return "run-log-line--error";
  }

  if (
    /\b(traceback|exception:\s|panic:|fatal\b|critical\b)\b/i.test(line) ||
    /\blevel=error\b/i.test(line) ||
    /\sERROR\s|[\[,]ERROR[\],]|^ERROR\s|::error::/i.test(line) ||
    /로그 조회 실패|MinIO.*실패|읽기 실패|ApiException\(code=(4\d\d|5\d\d)/i.test(line) ||
    /\bForbidden\b|\bUnauthorized\b/i.test(line)
  ) {
    return "run-log-line--error";
  }
  if (
    /\blevel=warn/i.test(line) ||
    /\bWARN(ING)?\b/i.test(line) ||
    /warning:/i.test(line) ||
    /deprecationwarning/i.test(line)
  ) {
    return "run-log-line--warn";
  }
  return "";
}

const logLineRows = computed(() => {
  const raw = logContent.value ?? "";
  return raw.split(/\r?\n/).map((text, i) => ({
    key: i,
    text,
    lineClass: logLineHighlightClass(text),
  }));
});
</script>

<template>
  <v-container fluid class="h-100 pa-5 d-flex flex-column align-center">
    <v-card
      flat
      class="bg-shades-transparent d-flex flex-column justify-center w-100"
    >
      <v-card flat class="bg-shades-transparent w-100">
        <v-card-item class="text-h5 font-weight-bold pt-0 pa-5 pl-0">
          <div class="d-flex flex-row justify-space-between align-center w-100">
            <div>Compare Executions</div>
            <v-btn color="primary" variant="elevated" @click="emit('close')">
              Back to List
            </v-btn>
          </div>
        </v-card-item>
      </v-card>

      <!-- Main Tabs -->
      <v-tabs v-model="mainTab" density="comfortable" color="primary">
        <v-tab value="details">Details</v-tab>
        <v-tab value="viz">Metrics</v-tab>
        <v-tab value="artifacts">Artifacts</v-tab>
      </v-tabs>

      <v-window v-model="mainTab">
        <!-- ========= Details ========= -->
        <v-window-item value="details">
          <v-card flat class="bordered-box mb-6 w-100 rounded-lg pa-8">
            <v-card-title class="grey lighten-4 py-2 px-4 d-flex align-center justify-space-between">
              <span class="font-weight-bold">Execution Information</span>
              <v-btn color="primary" variant="tonal" size="small" @click="openLogDialog">
                <v-icon start>mdi-text-box-outline</v-icon>
                Log
              </v-btn>
            </v-card-title>

            <!-- MLflow 매칭 디버그: run이 없을 때만 Details 탭에서도 표시 -->
            <v-card-text class="px-6 pb-6 pt-4">
              <v-row align="center" class="py-2">
                <v-col cols="3" class="text-h6 font-weight-bold">Name</v-col>
                <v-col cols="9">{{ props.experimentInfo.name }}</v-col>
              </v-row>
              <VDivider class="my-2" />

              <v-row align="center" class="py-2">
                <v-col cols="3" class="text-h6 font-weight-bold">Status</v-col>
                <v-col cols="9">
                  <v-icon
                    v-if="props.experimentInfo.status === 'Succeeded'"
                    color="green"
                    >mdi-check-circle</v-icon
                  >
                  <v-icon
                    v-else-if="props.experimentInfo.status === 'Failed'"
                    color="red"
                    >mdi-close-circle</v-icon
                  >
                  {{ props.experimentInfo.status }}
                </v-col>
              </v-row>
              <VDivider class="my-2" />

              <v-row align="center" class="py-2">
                <v-col cols="3" class="text-h6 font-weight-bold"
                  >Duration</v-col
                >
                <v-col cols="9">{{ props.experimentInfo.duration }}</v-col>
              </v-row>
              <VDivider class="my-2" />

              <v-row align="center" class="py-2">
                <v-col cols="3" class="text-h6 font-weight-bold"
                  >Experiment Name</v-col
                >
                <v-col cols="9">{{ props.experimentInfo.expName }}</v-col>
              </v-row>
              <VDivider class="my-2" />

              <v-row align="center" class="py-2">
                <v-col cols="3" class="text-h6 font-weight-bold"
                  >Workflow Name</v-col
                >
                <v-col cols="9">{{ props.experimentInfo.workflow }}</v-col>
              </v-row>
              <VDivider class="my-2" />

              <v-row align="center" class="py-2">
                <v-col cols="3" class="text-h6 font-weight-bold"
                  >Start Time</v-col
                >
                <v-col cols="9">{{ props.experimentInfo.startTime }}</v-col>
              </v-row>
              <VDivider class="my-2" />

              <v-row align="center" class="py-2">
                <v-col cols="3" class="text-h6 font-weight-bold"
                  >Registry Status</v-col
                >
                <v-col cols="9">{{
                  props.experimentInfo.registryStatus
                }}</v-col>
              </v-row>

              <!-- State History -->
              <v-row align="center" class="py-6">
                <v-col cols="3" class="text-h6 font-weight-bold"
                  >State History</v-col
                >
                <v-col cols="9">
                  <v-timeline direction="horizontal">
                    <v-timeline-item
                      v-for="(h, idx) in history"
                      :key="idx"
                      :dot-color="
                        (h.state || '').toUpperCase() === 'FAILED'
                          ? 'error'
                          : (h.state || '').toUpperCase() === 'SUCCEEDED'
                            ? 'success'
                            : 'primary'
                      "
                      :icon="
                        (h.state || '').toUpperCase() === 'FAILED'
                          ? 'mdi-close'
                          : (h.state || '').toUpperCase() === 'SUCCEEDED'
                            ? 'mdi-check'
                            : 'mdi-progress-clock'
                      "
                    >
                      <div class="text-caption text-medium-emphasis">
                        {{ fmt(h.update_time) }}
                      </div>
                      <div class="text-body-2 font-weight-medium">
                        {{ (h.state || "").toUpperCase() }}
                      </div>
                    </v-timeline-item>
                  </v-timeline>
                </v-col>
              </v-row>
            </v-card-text>
          </v-card>
        </v-window-item>

        <!-- ========= Visualizations ========= -->
        <v-window-item value="viz">
          <v-card class="rounded-lg pa-8 w-100">
            <v-window v-model="vizTab">
              <v-window-item value="metrics">
                <v-card-text>
                  <v-row class="mb-4" align="center">
                    <v-col
                      cols="12"
                      class="d-flex align-center justify-between flex-wrap ga-3"
                    >
                      <div class="text-body-2">
                        Runs loaded:
                        <strong>{{ runs.length.toLocaleString() }}</strong>
                      </div>

                      <div
                        class="d-flex align-center ga-2"
                        style="min-width: 280px; max-width: 620px"
                      >
                        <v-select
                          v-model="selectedRunId"
                          :items="runItems"
                          item-title="label"
                          item-value="value"
                          density="comfortable"
                          hide-details
                          :clearable="false"
                          clear-icon=""
                          style="min-width: 280px; max-width: 440px"
                        >
                          <template #item="{ item, props: itemProps }">
                            <v-list-item v-bind="itemProps">
                              <template #title>
                                <span :class="{ 'font-weight-bold': item.raw?.isCurrent }">
                                  {{ item.raw?.isCurrent ? '* ' : '' }}{{ item.raw?.label ?? item.title }}
                                </span>
                              </template>
                            </v-list-item>
                          </template>
                          <template #selection="{ item }">
                            <span :class="{ 'font-weight-bold': item.raw?.isCurrent }">
                              {{ item.raw?.isCurrent ? '* ' : '' }}{{ item.raw?.label ?? item.title }}
                            </span>
                          </template>
                        </v-select>
                        <v-btn
                          color="primary"
                          variant="elevated"
                          density="comfortable"
                          @click="openCompareDialog"
                          :disabled="runs.length < 2"
                          title="여러 Run을 골라 메트릭 비교"
                        >
                          Compare
                        </v-btn>
                      </div>

                      <v-progress-circular
                        v-if="loadingRuns || loadingRunDetail"
                        indeterminate
                        size="16"
                        class="ml-2"
                      />
                    </v-col>
                  </v-row>

                  <!-- Selected Run: 메트릭 등 정보가 있을 때만 표시 -->
                  <template v-if="runDetail">
                  <v-card flat class="mb-6">
                    <v-card-title
                      class="py-2 px-0 text-button text-medium-emphasis"
                    >
                      Selected Run
                    </v-card-title>

                    <v-card-text class="px-0">
                      <v-row>
                        <!-- 기본 정보 -->
                        <v-col cols="12" md="6">
                          <v-table density="comfortable">
                            <thead>
                              <tr>
                                <th style="width: 40%">Field</th>
                                <th>Value</th>
                              </tr>
                            </thead>
                            <tbody>
                              <tr>
                                <td>Run ID</td>
                                <td>{{ runDetail?.info?.run_id || "—" }}</td>
                              </tr>
                              <tr>
                                <td>Run Name</td>
                                <td>{{ runDetail?.info?.run_name || "—" }}</td>
                              </tr>
                              <tr>
                                <td>MLflow Experiment Name</td>
                                <td>{{ mlflowExperimentName }}</td>
                              </tr>
                              <tr>
                                <td>Status</td>
                                <td>{{ runDetail?.info?.status || "—" }}</td>
                              </tr>
                              <tr>
                                <td>Start</td>
                                <td>
                                  {{
                                    runDetail?.info?.start_time
                                      ? new Date(
                                          runDetail.info.start_time,
                                        ).toLocaleString()
                                      : "—"
                                  }}
                                </td>
                              </tr>
                              <tr>
                                <td>End</td>
                                <td>
                                  {{
                                    runDetail?.info?.end_time
                                      ? new Date(
                                          runDetail.info.end_time,
                                        ).toLocaleString()
                                      : "—"
                                  }}
                                </td>
                              </tr>
                            </tbody>
                          </v-table>
                        </v-col>

                        <!-- 파라미터 + 태그 -->
                        <v-col cols="12" md="6">
                          <div
                            class="text-subtitle-2 text-medium-emphasis mb-2"
                          >
                            Parameters
                          </div>
                          <v-table density="compact" class="mb-4">
                            <thead>
                              <tr>
                                <th style="width: 40%">Key</th>
                                <th>Value</th>
                              </tr>
                            </thead>
                            <tbody>
                              <tr v-if="!runDetail?.data?.params?.length">
                                <td colspan="2" class="text-medium-emphasis">
                                  No params
                                </td>
                              </tr>
                              <tr
                                v-for="p in runDetail?.data?.params || []"
                                :key="p.key"
                              >
                                <td>{{ p.key }}</td>
                                <td>{{ p.value }}</td>
                              </tr>
                            </tbody>
                          </v-table>

                          <div
                            class="text-subtitle-2 text-medium-emphasis mb-2"
                          >
                            Tags
                          </div>
                          <v-table density="compact">
                            <thead>
                              <tr>
                                <th style="width: 40%">Key</th>
                                <th>Value</th>
                              </tr>
                            </thead>
                            <tbody>
                              <tr v-if="!runDetail?.data?.tags?.length">
                                <td colspan="2" class="text-medium-emphasis">
                                  No tags
                                </td>
                              </tr>
                              <tr
                                v-for="t in runDetail?.data?.tags || []"
                                :key="t.key"
                              >
                                <td>{{ t.key }}</td>
                                <td
                                  class="text-truncate"
                                  style="max-width: 420px"
                                >
                                  {{ t.value }}
                                </td>
                              </tr>
                            </tbody>
                          </v-table>
                        </v-col>
                      </v-row>
                    </v-card-text>
                  </v-card>

                  <!-- Model Metrics table -->
                  <v-card flat class="mb-6">
                    <v-card-title
                      class="py-2 px-0 text-button text-medium-emphasis"
                      >Model Metrics (selected run)</v-card-title
                    >
                    <v-table density="comfortable">
                      <thead>
                        <tr>
                          <th class="text-left" style="width: 50%">Metric</th>
                          <th class="text-left">Value</th>
                        </tr>
                      </thead>
                      <tbody>
                        <tr v-if="!selectedMetrics.length">
                          <td
                            colspan="2"
                            class="text-center py-6 text-medium-emphasis"
                          >
                            No Data
                          </td>
                        </tr>
                        <tr v-for="m in selectedMetrics" :key="m.key">
                          <td>{{ m.key }}</td>
                          <td>{{ m.value }}</td>
                        </tr>
                      </tbody>
                    </v-table>
                  </v-card>

                  <!-- Bar chart -->
                  <v-card flat class="mb-6">
                    <v-card-title
                      class="py-2 px-0 text-button text-medium-emphasis"
                      >Metrics (bar chart)</v-card-title
                    >
                    <div
                      ref="elMetrics"
                      style="width: 100%; height: 400px"
                    ></div>
                  </v-card>
                  </template>
                </v-card-text>
              </v-window-item>

              <v-window-item value="scatter">
                <v-card-text class="px-6 py-10 text-medium-emphasis"
                  >(준비중) X/Y 축 선택 후 산점도 표시</v-card-text
                >
              </v-window-item>
              <v-window-item value="box">
                <v-card-text class="px-6 py-10 text-medium-emphasis"
                  >(준비중) 메트릭 분포 Box Plot</v-card-text
                >
              </v-window-item>
              <v-window-item value="contour">
                <v-card-text class="px-6 py-10 text-medium-emphasis"
                  >(준비중) 2D/3D Contour Plot</v-card-text
                >
              </v-window-item>
            </v-window>
          </v-card>
        </v-window-item>

        <!-- ========= Artifacts (Two-step, Flat) ========= -->
        <v-window-item value="artifacts">
          <v-card class="rounded-lg pa-8 w-100">
            <v-card-text>
              <v-row class="mb-4" align="center">
                <v-col
                  cols="12"
                  class="d-flex align-center justify-between flex-wrap ga-3"
                >
                  <div class="text-body-2">
                    Runs loaded:
                    <strong>{{ runs.length.toLocaleString() }}</strong>
                  </div>

                  <v-select
                    v-model="selectedRunId"
                    :items="runItems"
                    item-title="label"
                    item-value="value"
                    density="comfortable"
                    hide-details
                    :clearable="false"
                    clear-icon=""
                    style="min-width: 280px; max-width: 440px"
                  >
                    <template #item="{ item, props: itemProps }">
                      <v-list-item v-bind="itemProps">
                        <template #title>
                          <span :class="{ 'font-weight-bold': item.raw?.isCurrent }">
                            {{ item.raw?.isCurrent ? '* ' : '' }}{{ item.raw?.label ?? item.title }}
                          </span>
                        </template>
                      </v-list-item>
                    </template>
                    <template #selection="{ item }">
                      <span :class="{ 'font-weight-bold': item.raw?.isCurrent }">
                        {{ item.raw?.isCurrent ? '* ' : '' }}{{ item.raw?.label ?? item.title }}
                      </span>
                    </template>
                  </v-select>

                  <div class="d-flex align-center ga-2 ml-auto">
                    <v-btn
                      size="small"
                      variant="tonal"
                      :loading="twoStepLoading"
                      @click="fetchArtifactsTwoStep(selectedRunId)"
                    >
                      Refresh
                    </v-btn>

                    <v-chip
                      v-if="isAuthenticated"
                      color="success"
                      variant="tonal"
                      size="small"
                      class="text-body-2"
                    >
                      <v-icon start size="16">mdi-check-decagram</v-icon>
                      {{ externalAuth?.name || externalAuth?.id }}
                    </v-chip>

                    <v-btn
                      size="small"
                      color="primary"
                      variant="text"
                      @click="openDeploymentModal(undefined, 'upload')"
                      title="업로드 파일로 등록(Artifact Path 숨김)"
                    >
                      <v-icon start size="16">mdi-rocket-launch</v-icon> Deploy
                    </v-btn>

                    <v-btn
                      v-if="isAuthenticated"
                      size="small"
                      color="primary"
                      variant="text"
                      @click="handleLogout"
                    >
                      <v-icon start size="16">mdi-logout</v-icon> Logout
                    </v-btn>
                    <v-btn
                      v-else
                      size="small"
                      color="primary"
                      variant="text"
                      @click="openLoginManually"
                    >
                      <v-icon start size="16">mdi-login</v-icon> Login
                    </v-btn>

                    <v-progress-circular
                      v-if="twoStepLoading || loadingRuns || loadingRunDetail"
                      indeterminate
                      size="16"
                      class="ml-2"
                    />
                  </div>
                </v-col>
              </v-row>

              <v-alert
                v-if="twoStepError"
                type="error"
                variant="tonal"
                class="mb-3"
              >
                {{ twoStepError }}
              </v-alert>

              <!-- 바깥 카드도 flat, 타이틀 톤 통일 -->
              <v-card flat class="mb-2">
                <v-card-title
                  class="py-2 px-0 text-button text-medium-emphasis"
                >
                  Artifacts
                </v-card-title>

                <v-card-text class="px-0">
                  <v-table density="comfortable">
                    <thead>
                      <tr>
                        <th style="width: 48px"></th>
                        <th>Path</th>
                        <th style="width: 160px" class="text-right">Size</th>
                        <th style="width: 180px" class="text-right">Actions</th>
                      </tr>
                    </thead>

                    <tbody>
                      <tr v-if="twoStepLoading">
                        <td
                          colspan="4"
                          class="text-center py-6 text-medium-emphasis"
                        >
                          Loading…
                        </td>
                      </tr>

                      <template v-else>
                        <tr v-if="!artifactGroups.length">
                          <td
                            colspan="4"
                            class="text-center py-6 text-medium-emphasis"
                          >
                            <template v-if="lastFetchedArtifactRunId && !artifactMlflowResolved && !twoStepError">
                              MLflow run을 찾을 수 없습니다. Refresh 후에도 같으면 백엔드가 MLflow에 접근 가능한지 확인하세요.
                            </template>
                            <template v-else-if="lastFetchedArtifactRunId && artifactMlflowResolved && !twoStepError">
                              MLflow run에는 연결되었으나 아티팩트 목록이 비어 있습니다. MLflow 서버의 artifact 저장소(S3/MinIO) 설정 및 접근 권한을 확인하세요.
                            </template>
                            <template v-else>
                              No Artifacts
                            </template>
                          </td>
                        </tr>

                        <!-- 디렉터리 헤더 -->
                        <template v-for="grp in artifactGroups" :key="grp.dir">
                          <tr class="group-row">
                            <td class="text-center">
                              <v-icon>mdi-folder</v-icon>
                            </td>
                            <td>
                              <strong>{{ grp.dir }}</strong>
                            </td>
                            <td class="text-right"></td>
                            <td />
                          </tr>

                          <tr
                            v-for="(it, idx) in grp.files"
                            :key="grp.dir + '-' + idx"
                          >
                            <td colspan="2">
                              <div
                                class="path-cell"
                                :style="{
                                  paddingLeft: `${18 * (1 + (it.depth ?? 0))}px`,
                                }"
                              >
                                <v-icon
                                  :icon="it.is_dir ? 'mdi-folder' : FILE_ICON"
                                  size="18"
                                  class="mr-2"
                                />
                                <span>{{ it.path }}</span>
                              </div>
                            </td>

                            <td class="text-right">
                              {{ bytes(it.file_size) }}
                            </td>

                            <td class="text-right">
                              <template v-if="!it.is_dir">
                                <IconDownloadBtn
                                  @onClick="onClickArtifact(it.path)"
                                />
                                <IconDeployBtn
                                  class="ml-2"
                                  :tooltip="
                                    isAuthenticated
                                      ? 'Deploy'
                                      : 'Login required'
                                  "
                                  @onClick="
                                    openDeploymentModal(it.path, 'artifact')
                                  "
                                />
                              </template>
                              <template v-else>
                                <v-btn size="small" variant="text">Open</v-btn>
                              </template>
                            </td>
                          </tr>
                        </template>
                      </template>
                    </tbody>
                  </v-table>
                </v-card-text>
              </v-card>
            </v-card-text>
          </v-card>
        </v-window-item>
      </v-window>
    </v-card>

    <CompareRunsDialog
      v-model="compareDialog"
      :items="runItems"
      v-model:selectedRunIds="compareSelectedRunIds"
      v-model:selectedMetricKeys="compareSelectedMetricKeys"
      :ensureRunDetail="ensureRunDetail"
      :runDetailCache="runDetailCache"
      :runIdToExecutionName="runIdToExecutionName"
      :compareChartMode="compareChartMode"
      :normalizeValues="normalizeValues"
      :baselineRunId="baselineRunId"
    />
  </v-container>

  <!-- 배포 다이얼로그 -->
  <v-dialog v-model="isEditVisible" max-width="800" persistent>
    <DeploymentDialog
      :edit-data="null"
      :mode="'create'"
      :packages="packageOptions"
      :packages-loading="packagesLoading"
      :packages-error="packagesError"
      :artifact-path="lastArtifactUri"
      :token="externalToken"
      :hide-artifact-path="dlgHideArtifactPath"
      :hide-upload-file="dlgHideUploadFile"
      @close-modal="closeCreateModal"
      @handle-data="saveData"
      :user-option="[]"
    />
  </v-dialog>

  <!-- Run 로그: 기본 한 스텝(KFP 노드), 옵션으로 전체 스텝 -->
  <v-dialog v-model="logDialogOpen" max-width="min(96vw, 1200px)" persistent scrollable>
    <v-card>
      <v-card-title class="d-flex align-center flex-wrap gap-2">
        Run 로그 — {{ pickParentRunId(props.experimentInfo) || '—' }}
        <v-spacer />
        <v-checkbox
          v-model="logAllSteps"
          label="전체 스텝 로그"
          density="compact"
          hide-details
          class="mr-2"
          @update:model-value="reloadRunLog"
        />
        <v-btn
          color="primary"
          variant="tonal"
          size="small"
          class="mr-2"
          :loading="logLoading"
          @click="refreshRunLog"
        >
          <v-icon start size="small">mdi-refresh</v-icon>
          업데이트
        </v-btn>
        <v-btn icon variant="text" @click="logDialogOpen = false"><v-icon>mdi-close</v-icon></v-btn>
      </v-card-title>
      <v-card-text>
        <v-progress-linear v-if="logLoading" indeterminate class="mb-2" />
        <div
          v-if="logContent"
          class="run-log-viewer run-log-viewer--full"
          aria-label="실행 로그"
        >
          <div
            v-for="row in logLineRows"
            :key="row.key"
            class="run-log-line"
            :class="row.lineClass"
          >
            {{ row.text || "\u00a0" }}
          </div>
        </div>
      </v-card-text>
    </v-card>
  </v-dialog>

  <!-- 로그인 모달 -->
  <v-dialog v-model="loginDialog" max-width="450" persistent>
    <v-card>
      <v-card-title
        class="text-white font-weight-bold text-h6"
        style="background-color: #1976d2"
      >
        External Login
      </v-card-title>
      <v-card-text class="pa-6">
        <v-form @submit.prevent="handleLogin">
          <div class="mb-5">
            <label class="text-subtitle-2 font-weight-medium mb-1 d-block"
              >ID</label
            >
            <v-text-field
              v-model="loginForm.id"
              variant="outlined"
              :disabled="loginLoading"
              dense
              hide-details="auto"
              autocomplete="username"
              required
              @keyup.enter.prevent="handleLogin"
            />
          </div>
          <div class="mb-5">
            <label class="text-subtitle-2 font-weight-medium mb-1 d-block"
              >Password</label
            >
            <v-text-field
              v-model="loginForm.password"
              variant="outlined"
              :disabled="loginLoading"
              dense
              hide-details="auto"
              type="password"
              autocomplete="current-password"
              required
              @keyup.enter.prevent="handleLogin"
            />
          </div>
          <div v-if="loginError" class="mt-3 text-error">{{ loginError }}</div>
        </v-form>
      </v-card-text>
      <v-card-actions class="justify-end" style="padding: 16px 24px">
        <v-btn color="success" :loading="loginLoading" @click="handleLogin"
          >Login</v-btn
        >
        <v-btn text :disabled="loginLoading" @click="loginDialog = false"
          >Close</v-btn
        >
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>

<style scoped>
:root {
  --dot-size: 28px;
}
.group-row {
  background: rgba(255, 255, 255, 0.04);
}
.child-path {
  padding-left: 18px;
}
.run-log-viewer {
  background: #1e1e1e;
  color: #d4d4d4;
  padding: 8px 12px;
  border-radius: 4px;
  max-height: 400px;
  overflow: auto;
  font-size: 12px;
}
.run-log-viewer--full {
  max-height: min(70vh, 900px);
}
.run-log-line {
  display: block;
  font-family: ui-monospace, "Cascadia Code", "Consolas", monospace;
  white-space: pre-wrap;
  word-break: break-word;
  min-height: 1.35em;
  line-height: 1.35;
  padding: 1px 6px;
  margin: 0 -6px;
  border-radius: 2px;
}
.run-log-line--error {
  background: rgba(220, 60, 60, 0.42);
  color: #ffdede;
}
.run-log-line--warn {
  background: rgba(200, 160, 30, 0.4);
  color: #fff8e0;
}
</style>
