from __future__ import annotations

import re
import secrets
import subprocess
import threading
from dataclasses import dataclass
from functools import lru_cache
from typing import Any

import yaml
from fastapi import Depends, FastAPI, Header, HTTPException
from fastapi.responses import HTMLResponse
from pydantic import BaseModel, Field


_HELM_VALUE_KEY_ESCAPE = re.compile(r"([\\.,=\[\]])")
_HELM_LOCK = threading.Lock()


@dataclass(frozen=True)
class Settings:
    token: str
    release_name: str
    namespace: str
    chart_path: str
    helm_bin: str


@dataclass(frozen=True)
class ModelState:
    name: str
    replicas: int
    num_gpus: int

    @property
    def used_gpus(self) -> int:
        return self.replicas * self.num_gpus


@dataclass(frozen=True)
class ClusterState:
    total_gpus: int
    models: dict[str, ModelState]

    @property
    def used_gpus(self) -> int:
        return sum(model.used_gpus for model in self.models.values())


class HelmCommandError(RuntimeError):
    def __init__(self, cmd: list[str], stdout: str, stderr: str) -> None:
        super().__init__(f"helm command failed: {' '.join(cmd)}")
        self.cmd = cmd
        self.stdout = stdout
        self.stderr = stderr


class ReplicaUpdateRequest(BaseModel):
    replicas: int = Field(ge=0)


app = FastAPI(title="Shenron Replica Manager", version="0.1.0")

UI_HTML = """<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Shenron Replica Manager</title>
    <style>
      :root {
        --bg-start: #f0e6d8;
        --bg-end: #d5e5ef;
        --panel-bg: rgba(251, 248, 241, 0.84);
        --panel-border: rgba(20, 36, 52, 0.2);
        --ink: #102434;
        --muted: #4b6070;
        --accent: #da7b37;
        --success: #1f7d4a;
        --error: #a12d1b;
        --space-border: #2b4156;
        --gpu-columns: 8;
      }

      * {
        box-sizing: border-box;
      }

      body {
        margin: 0;
        min-height: 100vh;
        color: var(--ink);
        font-family: "Sora", "Avenir Next", "Trebuchet MS", sans-serif;
        background:
          radial-gradient(1300px 620px at 100% -15%, rgba(225, 128, 59, 0.17), transparent 62%),
          radial-gradient(900px 420px at -20% 110%, rgba(36, 136, 173, 0.2), transparent 72%),
          linear-gradient(150deg, var(--bg-start), var(--bg-end));
      }

      main {
        max-width: 960px;
        margin: 0 auto;
        padding: 24px 16px 40px;
        animation: lift-in 420ms ease-out;
      }

      .panel {
        background: var(--panel-bg);
        border: 1px solid var(--panel-border);
        border-radius: 20px;
        padding: 18px;
        box-shadow: 0 16px 36px rgba(18, 31, 46, 0.1);
        backdrop-filter: blur(4px);
      }

      h1 {
        margin: 0 0 8px;
        font-size: clamp(1.4rem, 2.8vw, 2rem);
        letter-spacing: 0.01em;
      }

      .sub {
        margin: 0 0 16px;
        color: var(--muted);
        font-size: 0.95rem;
      }

      .auth-row {
        display: flex;
        gap: 8px;
        flex-wrap: wrap;
        margin-bottom: 14px;
      }

      .auth-row input {
        flex: 1;
        min-width: 210px;
        border: 1px solid rgba(16, 36, 52, 0.25);
        border-radius: 12px;
        padding: 10px 12px;
        font: inherit;
        color: inherit;
        background: rgba(255, 255, 255, 0.82);
      }

      button {
        border: none;
        border-radius: 12px;
        font: inherit;
        cursor: pointer;
      }

      .primary-btn {
        background: linear-gradient(155deg, #e08a3c, #ce6427);
        color: #fff;
        font-weight: 600;
        padding: 10px 14px;
      }

      .secondary-btn {
        background: rgba(20, 36, 52, 0.08);
        color: var(--ink);
        font-weight: 600;
        padding: 10px 14px;
      }

      button:disabled {
        opacity: 0.45;
        cursor: not-allowed;
      }

      .status {
        min-height: 20px;
        margin-bottom: 14px;
        font-size: 0.9rem;
      }

      .status-success {
        color: var(--success);
      }

      .status-error {
        color: var(--error);
      }

      .cluster-bar {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: 12px;
        margin-bottom: 8px;
      }

      .cluster-main {
        font-weight: 700;
      }

      .cluster-free {
        color: var(--muted);
        font-size: 0.9rem;
      }

      progress {
        width: 100%;
        height: 10px;
        margin-bottom: 14px;
      }

      progress::-webkit-progress-bar {
        border-radius: 8px;
        background: rgba(16, 36, 52, 0.1);
      }

      progress::-webkit-progress-value {
        border-radius: 8px;
        background: linear-gradient(90deg, #2b7bb6, #da7b37);
      }

      .model-grid {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
        gap: 10px;
        margin-bottom: 16px;
      }

      .model-btn {
        text-align: left;
        background: rgba(255, 255, 255, 0.84);
        border: 1px solid rgba(20, 36, 52, 0.12);
        box-shadow: inset 0 0 0 1px rgba(20, 36, 52, 0.05);
        padding: 10px 12px;
        transform: translateY(0);
        transition: transform 120ms ease, box-shadow 120ms ease;
      }

      .model-btn:hover:not(:disabled) {
        transform: translateY(-1px);
        box-shadow: 0 9px 14px rgba(12, 27, 41, 0.12), inset 0 0 0 2px var(--model-color);
      }

      .model-name {
        display: block;
        font-weight: 700;
        font-size: 0.95rem;
        margin-bottom: 4px;
      }

      .model-meta {
        display: block;
        color: var(--muted);
        font-size: 0.85rem;
      }

      .space-label {
        margin: 0 0 8px;
        font-weight: 700;
      }

      .deployment-space {
        min-height: 220px;
        border: 2px solid var(--space-border);
        border-radius: 16px;
        padding: 10px;
        display: grid;
        grid-template-columns: repeat(var(--gpu-columns), minmax(0, 1fr));
        grid-auto-rows: minmax(88px, auto);
        gap: 8px;
        background:
          linear-gradient(180deg, rgba(255, 255, 255, 0.76), rgba(243, 248, 251, 0.82)),
          repeating-linear-gradient(
            90deg,
            rgba(43, 65, 86, 0.08) 0,
            rgba(43, 65, 86, 0.08) 1px,
            transparent 1px,
            transparent calc(100% / var(--gpu-columns))
          );
      }

      .empty-space {
        grid-column: 1 / -1;
        align-self: center;
        justify-self: center;
        color: var(--muted);
        font-size: 0.95rem;
      }

      .replica-block {
        width: 100%;
        text-align: left;
        padding: 10px;
        border-radius: 12px;
        color: #0d202f;
        background:
          linear-gradient(135deg, rgba(255, 255, 255, 0.82), rgba(245, 250, 253, 0.88));
        box-shadow:
          inset 0 0 0 2px var(--model-color),
          0 6px 14px rgba(14, 29, 43, 0.1);
        animation: block-in 240ms ease both;
      }

      .replica-block:hover:not(:disabled) {
        box-shadow:
          inset 0 0 0 2px var(--model-color),
          0 10px 18px rgba(14, 29, 43, 0.18);
      }

      .replica-name {
        display: block;
        font-weight: 700;
        font-size: 0.86rem;
        margin-bottom: 3px;
        line-height: 1.15;
      }

      .replica-meta {
        display: block;
        font-size: 0.78rem;
        color: #3f5668;
      }

      @media (max-width: 700px) {
        main {
          padding: 14px 10px 26px;
        }

        .panel {
          padding: 14px;
        }

        .deployment-space {
          min-height: 180px;
          grid-auto-rows: minmax(78px, auto);
        }
      }

      @keyframes lift-in {
        from {
          opacity: 0;
          transform: translateY(8px);
        }
        to {
          opacity: 1;
          transform: translateY(0);
        }
      }

      @keyframes block-in {
        from {
          opacity: 0;
          transform: translateY(5px) scale(0.98);
        }
        to {
          opacity: 1;
          transform: translateY(0) scale(1);
        }
      }
    </style>
  </head>
  <body>
    <main>
      <section class="panel">
        <h1>Replica Manager</h1>
        <p class="sub">Click a model to add one replica. Click a block in deployment space to remove one replica.</p>

        <div class="auth-row">
          <input id="tokenInput" type="password" autocomplete="off" placeholder="Bearer token" />
          <button class="primary-btn" id="connectBtn" type="button">Connect</button>
          <button class="secondary-btn" id="refreshBtn" type="button">Refresh</button>
        </div>

        <div id="status" class="status">Enter bearer token to load cluster state.</div>

        <div class="cluster-bar">
          <div id="clusterMain" class="cluster-main">0 / 0 GPUs used</div>
          <div id="clusterFree" class="cluster-free">0 free</div>
        </div>
        <progress id="clusterProgress" value="0" max="1"></progress>

        <div id="modelGrid" class="model-grid"></div>

        <p id="spaceLabel" class="space-label">Deployment Space</p>
        <div id="deploymentSpace" class="deployment-space"></div>
      </section>
    </main>

    <script>
      const TOKEN_STORAGE_KEY = "replica-manager-token";

      const state = {
        token: "",
        busy: false,
        clusterTotalGpus: 0,
        clusterUsedGpus: 0,
        models: [],
      };

      const els = {
        tokenInput: document.getElementById("tokenInput"),
        connectBtn: document.getElementById("connectBtn"),
        refreshBtn: document.getElementById("refreshBtn"),
        status: document.getElementById("status"),
        clusterMain: document.getElementById("clusterMain"),
        clusterFree: document.getElementById("clusterFree"),
        clusterProgress: document.getElementById("clusterProgress"),
        modelGrid: document.getElementById("modelGrid"),
        spaceLabel: document.getElementById("spaceLabel"),
        deploymentSpace: document.getElementById("deploymentSpace"),
      };

      function setStatus(message, tone) {
        els.status.textContent = message;
        els.status.classList.remove("status-success", "status-error");
        if (tone === "success") {
          els.status.classList.add("status-success");
        } else if (tone === "error") {
          els.status.classList.add("status-error");
        }
      }

      function toShortName(name) {
        const parts = name.split("/");
        return parts[parts.length - 1] || name;
      }

      function modelColor(name) {
        let hash = 0;
        for (let i = 0; i < name.length; i += 1) {
          hash = (hash << 5) - hash + name.charCodeAt(i);
          hash |= 0;
        }
        const hue = Math.abs(hash) % 360;
        return `hsl(${hue}, 70%, 46%)`;
      }

      function modelByName(name) {
        return state.models.find((model) => model.name === name) || null;
      }

      function replicaEndpoint(modelName) {
        const modelPath = modelName
          .split("/")
          .map((part) => encodeURIComponent(part))
          .join("/");
        return `/v1/models/${modelPath}/replicas`;
      }

      function freeGpus() {
        return Math.max(0, state.clusterTotalGpus - state.clusterUsedGpus);
      }

      function renderSummary() {
        els.clusterMain.textContent = `${state.clusterUsedGpus} / ${state.clusterTotalGpus} GPUs used`;
        els.clusterFree.textContent = `${freeGpus()} free`;
        els.clusterProgress.max = Math.max(1, state.clusterTotalGpus);
        els.clusterProgress.value = Math.min(state.clusterUsedGpus, els.clusterProgress.max);
        els.spaceLabel.textContent = `Deployment Space (${state.clusterTotalGpus} GPUs)`;
      }

      function renderModelButtons() {
        const fragment = document.createDocumentFragment();
        const available = freeGpus();

        for (const model of state.models) {
          const button = document.createElement("button");
          button.type = "button";
          button.className = "model-btn";
          button.style.setProperty("--model-color", modelColor(model.name));
          button.disabled = state.busy || model.num_gpus > available;

          const title = document.createElement("span");
          title.className = "model-name";
          title.textContent = model.name;

          const meta = document.createElement("span");
          meta.className = "model-meta";
          const gpuLabel = model.num_gpus === 1 ? "GPU" : "GPUs";
          const replicaLabel = model.replicas === 1 ? "replica" : "replicas";
          meta.textContent = `${model.num_gpus} ${gpuLabel} each | ${model.replicas} ${replicaLabel}`;

          button.append(title, meta);
          button.addEventListener("click", () => {
            addReplica(model.name).catch((err) => {
              setStatus(err.message || "Failed to add replica.", "error");
            });
          });
          fragment.appendChild(button);
        }

        els.modelGrid.replaceChildren(fragment);
      }

      function expandReplicaBlocks() {
        const blocks = [];
        for (const model of state.models) {
          for (let idx = 0; idx < model.replicas; idx += 1) {
            blocks.push({
              modelName: model.name,
              numGpus: model.num_gpus,
              index: idx + 1,
            });
          }
        }
        return blocks;
      }

      function renderDeploymentSpace() {
        els.deploymentSpace.style.setProperty(
          "--gpu-columns",
          String(Math.max(1, state.clusterTotalGpus))
        );
        els.deploymentSpace.replaceChildren();

        const blocks = expandReplicaBlocks();
        if (blocks.length === 0) {
          const empty = document.createElement("div");
          empty.className = "empty-space";
          empty.textContent = "No replicas deployed.";
          els.deploymentSpace.appendChild(empty);
          return;
        }

        let staggerMs = 0;
        for (const block of blocks) {
          const model = modelByName(block.modelName);
          if (!model) {
            continue;
          }

          const node = document.createElement("button");
          node.type = "button";
          node.className = "replica-block";
          node.disabled = state.busy;
          node.style.gridColumn = `span ${Math.max(1, Math.min(model.num_gpus, state.clusterTotalGpus))}`;
          node.style.setProperty("--model-color", modelColor(block.modelName));
          node.style.animationDelay = `${staggerMs}ms`;
          node.title = `Remove one replica of ${block.modelName}`;
          staggerMs += 34;

          const title = document.createElement("span");
          title.className = "replica-name";
          title.textContent = toShortName(block.modelName);

          const meta = document.createElement("span");
          meta.className = "replica-meta";
          const gpuLabel = model.num_gpus === 1 ? "GPU" : "GPUs";
          meta.textContent = `Replica ${block.index} | ${model.num_gpus} ${gpuLabel}`;

          node.append(title, meta);
          node.addEventListener("click", () => {
            removeReplica(block.modelName).catch((err) => {
              setStatus(err.message || "Failed to remove replica.", "error");
            });
          });
          els.deploymentSpace.appendChild(node);
        }
      }

      function render() {
        els.connectBtn.disabled = state.busy;
        els.refreshBtn.disabled = state.busy || !state.token;
        renderSummary();
        renderModelButtons();
        renderDeploymentSpace();
      }

      async function apiFetch(path, options) {
        if (!state.token) {
          throw new Error("Bearer token is required.");
        }
        const response = await fetch(path, {
          ...options,
          headers: {
            Authorization: `Bearer ${state.token}`,
            ...(options && options.body ? { "Content-Type": "application/json" } : {}),
            ...((options && options.headers) || {}),
          },
        });

        let payload = {};
        try {
          payload = await response.json();
        } catch (err) {
          payload = {};
        }

        if (!response.ok) {
          const detail = payload && payload.detail ? String(payload.detail) : `${response.status} ${response.statusText}`;
          throw new Error(detail);
        }
        return payload;
      }

      async function loadState() {
        const payload = await apiFetch("/v1/models");
        state.clusterTotalGpus = Number(payload.cluster_total_gpus) || 0;
        state.clusterUsedGpus = Number(payload.cluster_used_gpus) || 0;
        state.models = Array.isArray(payload.models)
          ? payload.models
              .map((model) => ({
                name: String(model.name),
                replicas: Number(model.replicas) || 0,
                num_gpus: Number(model.num_gpus) || 0,
              }))
              .sort((a, b) => a.name.localeCompare(b.name))
          : [];
      }

      async function withBusy(taskFn) {
        if (state.busy) {
          return;
        }
        state.busy = true;
        render();
        try {
          await taskFn();
        } finally {
          state.busy = false;
          render();
        }
      }

      async function setReplicas(modelName, replicas) {
        await withBusy(async () => {
          await apiFetch(replicaEndpoint(modelName), {
            method: "POST",
            body: JSON.stringify({ replicas }),
          });
          await loadState();
          setStatus(`Updated ${modelName} replicas to ${replicas}.`, "success");
        });
      }

      async function addReplica(modelName) {
        const model = modelByName(modelName);
        if (!model) {
          return;
        }
        await setReplicas(modelName, model.replicas + 1);
      }

      async function removeReplica(modelName) {
        const model = modelByName(modelName);
        if (!model || model.replicas <= 0) {
          return;
        }
        await setReplicas(modelName, model.replicas - 1);
      }

      async function connectAndLoad() {
        const token = els.tokenInput.value.trim();
        if (!token) {
          setStatus("Bearer token is required.", "error");
          return;
        }

        state.token = token;
        window.localStorage.setItem(TOKEN_STORAGE_KEY, token);

        await withBusy(async () => {
          await loadState();
          setStatus("Cluster state loaded.", "success");
        });
      }

      async function refreshState() {
        await withBusy(async () => {
          await loadState();
          setStatus("Cluster state refreshed.", "success");
        });
      }

      els.connectBtn.addEventListener("click", () => {
        connectAndLoad().catch((err) => {
          setStatus(err.message || "Failed to connect.", "error");
        });
      });

      els.refreshBtn.addEventListener("click", () => {
        refreshState().catch((err) => {
          setStatus(err.message || "Failed to refresh.", "error");
        });
      });

      els.tokenInput.addEventListener("keydown", (event) => {
        if (event.key === "Enter") {
          connectAndLoad().catch((err) => {
            setStatus(err.message || "Failed to connect.", "error");
          });
        }
      });

      const savedToken = window.localStorage.getItem(TOKEN_STORAGE_KEY);
      if (savedToken) {
        state.token = savedToken;
        els.tokenInput.value = savedToken;
        connectAndLoad().catch((err) => {
          setStatus(err.message || "Failed to load cluster state.", "error");
        });
      } else {
        render();
      }
    </script>
  </body>
</html>
"""


@lru_cache(maxsize=1)
def get_settings() -> Settings:
    token = _required_env("REPLICA_MANAGER_TOKEN")
    release_name = _required_env("HELM_RELEASE_NAME")
    namespace = _required_env("HELM_NAMESPACE")
    chart_path = _required_env("HELM_CHART_PATH")
    helm_bin = _required_env("HELM_BIN")
    return Settings(
        token=token,
        release_name=release_name,
        namespace=namespace,
        chart_path=chart_path,
        helm_bin=helm_bin,
    )


def _required_env(name: str) -> str:
    import os

    value = os.getenv(name, "").strip()
    if not value:
        raise ValueError(f"environment variable {name} is required")
    return value


def _settings_or_http_500() -> Settings:
    try:
        return get_settings()
    except ValueError as exc:
        raise HTTPException(status_code=500, detail=str(exc)) from exc


def _require_bearer_token(
    authorization: str | None = Header(default=None),
) -> None:
    settings = _settings_or_http_500()
    if not authorization or not authorization.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="missing bearer token")
    token = authorization[7:].strip()
    if not secrets.compare_digest(token, settings.token):
        raise HTTPException(status_code=401, detail="invalid bearer token")


def _parse_non_negative_int(value: Any, field_path: str) -> int:
    if isinstance(value, bool):
        raise ValueError(f"{field_path} must be a non-negative integer")
    if isinstance(value, int):
        parsed = value
    elif isinstance(value, str) and re.fullmatch(r"[0-9]+", value.strip()):
        parsed = int(value.strip())
    else:
        raise ValueError(f"{field_path} must be a non-negative integer")

    if parsed < 0:
        raise ValueError(f"{field_path} must be a non-negative integer")
    return parsed


def _escape_helm_map_key(value: str) -> str:
    return _HELM_VALUE_KEY_ESCAPE.sub(r"\\\1", value)


def _run_helm(settings: Settings, args: list[str]) -> str:
    cmd = [settings.helm_bin, *args]
    result = subprocess.run(cmd, capture_output=True, text=True, check=False)
    if result.returncode != 0:
        raise HelmCommandError(cmd, result.stdout, result.stderr)
    return result.stdout


def _load_cluster_state(settings: Settings) -> ClusterState:
    raw_values = _run_helm(
        settings,
        [
            "get",
            "values",
            settings.release_name,
            "--all",
            "--namespace",
            settings.namespace,
            "-o",
            "yaml",
        ],
    )

    parsed = yaml.safe_load(raw_values) or {}
    if not isinstance(parsed, dict):
        raise ValueError("helm values payload must be a YAML object")

    cluster_block = parsed.get("cluster", {})
    if not isinstance(cluster_block, dict):
        raise ValueError("values.cluster must be an object")

    total_gpus = _parse_non_negative_int(cluster_block.get("total_gpus"), "cluster.total_gpus")

    models_block = parsed.get("models", {})
    if not isinstance(models_block, dict):
        raise ValueError("values.models must be an object")

    models: dict[str, ModelState] = {}
    for model_name, model_data in models_block.items():
        if not isinstance(model_name, str):
            raise ValueError("values.models keys must be strings")
        if not isinstance(model_data, dict):
            raise ValueError(f"values.models.{model_name} must be an object")

        replicas = _parse_non_negative_int(
            model_data.get("replicas"), f"values.models.{model_name}.replicas"
        )
        num_gpus = _parse_non_negative_int(
            model_data.get("num_gpus"), f"values.models.{model_name}.num_gpus"
        )
        models[model_name] = ModelState(
            name=model_name,
            replicas=replicas,
            num_gpus=num_gpus,
        )

    return ClusterState(total_gpus=total_gpus, models=models)


def _set_model_replicas(settings: Settings, model_name: str, replicas: int) -> None:
    escaped_model = _escape_helm_map_key(model_name)
    set_arg = f"models.{escaped_model}.replicas={replicas}"
    _run_helm(
        settings,
        [
            "upgrade",
            settings.release_name,
            settings.chart_path,
            "--namespace",
            settings.namespace,
            "--reuse-values",
            "--set-string",
            set_arg,
        ],
    )


@app.get("/healthz")
def healthz() -> dict[str, str]:
    return {"status": "ok"}


@app.get("/", response_class=HTMLResponse)
def frontend() -> HTMLResponse:
    return HTMLResponse(content=UI_HTML)


@app.get("/v1/models", dependencies=[Depends(_require_bearer_token)])
def list_models() -> dict[str, Any]:
    settings = _settings_or_http_500()
    try:
        with _HELM_LOCK:
            cluster = _load_cluster_state(settings)
    except HelmCommandError as exc:
        raise HTTPException(status_code=500, detail=exc.stderr.strip() or str(exc)) from exc
    except ValueError as exc:
        raise HTTPException(status_code=500, detail=str(exc)) from exc

    models = []
    for name in sorted(cluster.models):
        model = cluster.models[name]
        models.append(
            {
                "name": model.name,
                "replicas": model.replicas,
                "num_gpus": model.num_gpus,
                "used_gpus": model.used_gpus,
            }
        )

    return {
        "cluster_total_gpus": cluster.total_gpus,
        "cluster_used_gpus": cluster.used_gpus,
        "models": models,
    }


@app.post("/v1/models/{model_name:path}/replicas", dependencies=[Depends(_require_bearer_token)])
def update_replicas(model_name: str, request: ReplicaUpdateRequest) -> dict[str, Any]:
    settings = _settings_or_http_500()
    try:
        with _HELM_LOCK:
            cluster = _load_cluster_state(settings)
            model = cluster.models.get(model_name)
            if model is None:
                raise HTTPException(status_code=404, detail=f"model '{model_name}' was not found")

            new_used_gpus = cluster.used_gpus - model.used_gpus + (model.num_gpus * request.replicas)
            if new_used_gpus > cluster.total_gpus:
                raise HTTPException(
                    status_code=409,
                    detail=(
                        f"requested replicas exceed cluster.total_gpus: "
                        f"{new_used_gpus} > {cluster.total_gpus}"
                    ),
                )

            _set_model_replicas(settings, model_name, request.replicas)
            updated_cluster = _load_cluster_state(settings)
            updated_model = updated_cluster.models.get(model_name)
            if updated_model is None:
                raise HTTPException(
                    status_code=500,
                    detail="replica update succeeded but model is missing from values",
                )
    except HelmCommandError as exc:
        raise HTTPException(status_code=500, detail=exc.stderr.strip() or str(exc)) from exc
    except ValueError as exc:
        raise HTTPException(status_code=500, detail=str(exc)) from exc

    return {
        "model": updated_model.name,
        "replicas": updated_model.replicas,
        "num_gpus": updated_model.num_gpus,
        "used_gpus": updated_model.used_gpus,
        "cluster_total_gpus": updated_cluster.total_gpus,
        "cluster_used_gpus": updated_cluster.used_gpus,
    }
