from __future__ import annotations

import logging
import time
import typing as t
from os import path

import attr
import click
import yaml
from deepmerge.merger import Merger
from rich.live import Live
from rich.progress import TaskID
from simple_di import Provide
from simple_di import inject

from bentoml._internal.cloud.base import Spinner

if t.TYPE_CHECKING:
    from _bentoml_impl.client import AsyncHTTPClient
    from _bentoml_impl.client import SyncHTTPClient
    from bentoml._internal.bento.bento import BentoStore
    from bentoml._internal.cloud.bentocloud import BentoCloudClient


from ...exceptions import BentoMLException
from ...exceptions import NotFound
from ..bento.bento import BentoInfo
from ..configuration.containers import BentoMLContainer
from ..tag import Tag
from ..utils import bentoml_cattr
from ..utils import resolve_user_filepath
from .config import get_rest_api_client
from .schemas.modelschemas import DeploymentStatus
from .schemas.modelschemas import DeploymentTargetHPAConf
from .schemas.schemasv2 import CreateDeploymentSchema as CreateDeploymentSchemaV2
from .schemas.schemasv2 import DeploymentSchema
from .schemas.schemasv2 import DeploymentTargetSchema
from .schemas.schemasv2 import UpdateDeploymentSchema as UpdateDeploymentSchemaV2

logger = logging.getLogger(__name__)

config_merger = Merger(
    # merge dicts
    type_strategies=[(dict, "merge")],
    # override all other types
    fallback_strategies=["override"],
    # override conflicting types
    type_conflict_strategies=["override"],
)


@attr.define
class DeploymentConfigParameters:
    name: str | None = None
    path_context: str | None = None
    context: str | None = None
    bento: Tag | str | None = None
    cluster: str | None = None
    access_authorization: bool | None = None
    scaling_min: int | None = None
    scaling_max: int | None = None
    instance_type: str | None = None
    strategy: str | None = None
    envs: t.List[dict[str, t.Any]] | None = None
    extras: dict[str, t.Any] | None = None
    config_dict: dict[str, t.Any] | None = None
    config_file: str | t.TextIO | None = None
    cli: bool = False
    service_name: str | None = None
    cfg_dict: dict[str, t.Any] | None = None
    _param_config: dict[str, t.Any] | None = None

    def verify(
        self,
    ):
        deploy_by_param = (
            self.name
            or self.bento
            or self.cluster
            or self.access_authorization
            or self.scaling_min
            or self.scaling_max
            or self.instance_type
            or self.strategy
            or self.envs
            or self.extras
        )

        if (
            self.config_dict
            and self.config_file
            or self.config_dict
            and deploy_by_param
            or self.config_file
            and deploy_by_param
        ):
            raise BentoMLException(
                "Configure a deployment can only use one of the following: config_dict, config_file, or the other parameters"
            )

        if deploy_by_param:
            self.cfg_dict = {
                k: v
                for k, v in [
                    ("name", self.name),
                    ("bento", self.bento),
                    ("cluster", self.cluster),
                    ("access_authorization", self.access_authorization),
                    ("envs", self.envs),
                ]
                if v is not None
            }
            # add service name
            self._param_config = {
                k: v
                for k, v in [
                    ("scaling_min", self.scaling_min),
                    ("scaling_max", self.scaling_max),
                    ("instance_type", self.instance_type),
                    ("strategy", self.strategy),
                    ("extras", self.extras),
                ]
                if v is not None
            }
        elif self.config_dict:
            self.cfg_dict = self.config_dict
        elif isinstance(self.config_file, str):
            real_path = resolve_user_filepath(self.config_file, self.path_context)
            try:
                with open(real_path, "r") as file:
                    self.cfg_dict = yaml.safe_load(file)
            except FileNotFoundError:
                raise ValueError(f"File not found: {real_path}")
            except yaml.YAMLError as exc:
                logger.error("Error while parsing YAML file: %s", exc)
                raise
            except Exception as e:
                raise ValueError(
                    f"An error occurred while reading the file: {real_path}\n{e}"
                )
        elif self.config_file:
            try:
                self.cfg_dict = yaml.safe_load(self.config_file)
            except yaml.YAMLError as exc:
                logger.error("Error while parsing YAML config-file stream: %s", exc)
                raise
        else:
            raise BentoMLException(
                "Must provide either config_dict, config_file, or the other parameters"
            )

        bento_name = self.cfg_dict.get("bento")
        # determine if bento is a path or a name
        if bento_name:
            if isinstance(bento_name, str) and path.exists(bento_name):
                # target is a path
                if self.cli:
                    click.echo(f"building bento from {bento_name} ...")
                bento_info = get_bento_info(
                    project_path=bento_name,
                    context=self.context,
                )
            else:
                if self.cli:
                    click.echo(f"using bento {bento_name}...")
                bento_info = get_bento_info(
                    bento=str(bento_name),
                    context=self.context,
                )
            self.cfg_dict["bento"] = bento_info.tag
            if self.service_name is None:
                self.service_name = bento_info.entry_service

    def get_name(self):
        if self.cfg_dict is None:
            raise BentoMLException(
                "DeploymentConfigParameters.verify() must be called first"
            )
        return self.cfg_dict.get("name")

    def get_cluster(self, pop: bool = True):
        if self.cfg_dict is None:
            raise BentoMLException(
                "DeploymentConfigParameters.verify() must be called first"
            )
        if pop:
            return self.cfg_dict.pop("cluster", None)
        else:
            return self.cfg_dict.get("cluster")

    def get_config_dict(self, bento: str | None = None):
        if self.cfg_dict is None:
            raise BentoMLException(
                "DeploymentConfigParameters.verify() must be called first"
            )
        if self.service_name is None:
            if bento is None:
                if self.cfg_dict.get("bento") is None:
                    raise BentoMLException(
                        "DeploymentConfigParameters.verify() must be called first"
                    )
                bento = self.cfg_dict.get("bento")

            info = get_bento_info(
                bento=bento,
                context=self.context,
            )
            if info.entry_service == "":
                # for compatibility
                self.service_name = "apiserver"
            else:
                self.service_name = info.entry_service
        if self._param_config is not None:
            scaling_min = self._param_config.pop("scaling_min", None)
            scaling_max = self._param_config.pop("scaling_max", None)
            if scaling_min is not None or scaling_max is not None:
                self._param_config["scaling"] = {
                    "min_replicas": scaling_min,
                    "max_replicas": scaling_max,
                }
                self._param_config["scaling"] = {
                    k: v
                    for k, v in self._param_config["scaling"].items()
                    if v is not None
                }

            strategy = self._param_config.pop("strategy", None)
            if strategy is not None:
                self._param_config["deployment_strategy"] = strategy
            self.cfg_dict["services"] = {self.service_name: self._param_config}

        return self.cfg_dict


def get_args_from_config(
    name: str | None = None,
    bento: Tag | str | None = None,
    cluster: str | None = None,
    config_dict: dict[str, t.Any] | None = None,
    config_file: str | t.TextIO | None = None,
    path_context: str | None = None,
):
    file_dict: dict[str, t.Any] | None = None

    if name is None and config_dict is not None and "name" in config_dict:
        name = config_dict["name"]
    if bento is None and config_dict is not None and "bento" in config_dict:
        bento = config_dict["bento"]
    if cluster is None and config_dict is not None and "cluster" in config_dict:
        cluster = config_dict["cluster"]

    if isinstance(config_file, str):
        real_path = resolve_user_filepath(config_file, path_context)
        try:
            with open(real_path, "r") as file:
                file_dict = yaml.safe_load(file)
        except FileNotFoundError:
            raise ValueError(f"File not found: {real_path}")
        except yaml.YAMLError as exc:
            logger.error("Error while parsing YAML file: %s", exc)
            raise
        except Exception as e:
            raise ValueError(
                f"An error occurred while reading the file: {real_path}\n{e}"
            )
    elif config_file is not None:
        try:
            file_dict = yaml.safe_load(config_file)
        except yaml.YAMLError as exc:
            logger.error("Error while parsing YAML config-file stream: %s", exc)
            raise

    if file_dict is not None:
        if bento is None and "bento" in file_dict:
            bento = file_dict["bento"]
        if name is None and "name" in file_dict:
            name = file_dict["name"]
        if cluster is None and "cluster" in file_dict:
            cluster = file_dict["cluster"]

    return name, bento, cluster


@inject
def get_bento_info(
    project_path: str | None = None,
    bento: str | Tag | None = None,
    context: str | None = None,
    _bento_store: BentoStore = Provide[BentoMLContainer.bento_store],
    _cloud_client: BentoCloudClient = Provide[BentoMLContainer.bentocloud_client],
) -> BentoInfo:
    if project_path:
        from bentoml.bentos import build_bentofile

        bento_obj = build_bentofile(build_ctx=project_path, _bento_store=_bento_store)
        _cloud_client.push_bento(bento=bento_obj, context=context)
        return bento_obj.info
    elif bento:
        bento = Tag.from_taglike(bento)
        try:
            bento_obj = _bento_store.get(bento)
        except NotFound:
            bento_obj = None

        # try to get from bentocloud
        try:
            bento_schema = _cloud_client.get_bento(
                context=context, name=bento.name, version=bento.version
            )
        except NotFound:
            bento_schema = None

        if bento_obj is None and bento_schema is None:
            raise NotFound(f"bento {bento} not found in both local and cloud")
        elif bento_obj is not None and bento_schema is None:
            # push to bentocloud
            _cloud_client.push_bento(bento=bento_obj, context=context)
            return bento_obj.info
        else:
            with Live(_cloud_client.spinner.progress_group):
                _cloud_client.spinner.log_progress.add_task(
                    f"[bold blue]Using bento {bento.name}:{bento.version} from bentocloud to deploy"
                )
            return BentoInfo(
                tag=Tag(name=bento.name, version=bento.version),
                entry_service=bento_schema.manifest.entry_service,
                service=bento_schema.manifest.service,
            )
    else:
        raise BentoMLException(
            "Create a deployment needs a target; project path or bento is necessary"
        )


@attr.define
class DeploymentConfig(CreateDeploymentSchemaV2):
    def to_yaml(self, with_meta: bool = True):
        config_dict = self.to_dict(with_meta=with_meta)
        return yaml.dump(config_dict, sort_keys=False)

    def to_dict(self, with_meta: bool = True):
        config_dict = bentoml_cattr.unstructure(self)
        if with_meta is False:
            # delete name and bento
            config_dict.pop("name", None)
            config_dict.pop("bento", None)

        return config_dict


@attr.define
class DeploymentState:
    status: str
    created_at: str
    updated_at: str
    # no error message for now
    # error_msg: str

    def to_dict(self) -> dict[str, t.Any]:
        return bentoml_cattr.unstructure(self)


@attr.define
class DeploymentInfo:
    name: str
    admin_console: str
    created_at: str
    created_by: str
    cluster: str
    _context: t.Optional[str] = attr.field(alias="_context", repr=False)
    _schema: DeploymentSchema = attr.field(alias="_schema", repr=False)
    _urls: t.Optional[list[str]] = attr.field(alias="_urls", default=None, repr=False)

    def to_dict(self) -> dict[str, t.Any]:
        return {
            "name": self.name,
            "bento": self.get_bento(refetch=False),
            "cluster": self.cluster,
            "admin_console": self.admin_console,
            "created_at": self.created_at,
            "created_by": self.created_by,
            "config": self.get_config(refetch=False).to_dict(with_meta=False),
            "status": self.get_status(refetch=False).to_dict(),
        }

    def to_yaml(self):
        return yaml.dump(self.to_dict(), sort_keys=False)

    def _refetch(self) -> None:
        res = Deployment.get(self.name, self.cluster, self._context)
        self._schema = res._schema
        self._urls = res._urls

    def _refetch_target(self, refetch: bool) -> DeploymentTargetSchema:
        if refetch:
            self._refetch()
        if self._schema.latest_revision is None:
            raise BentoMLException(f"Deployment {self.name} has no latest revision")
        if len(self._schema.latest_revision.targets) == 0:
            raise BentoMLException(
                f"Deployment {self.name} has no latest revision targets"
            )
        target = self._schema.latest_revision.targets[0]
        if target is None:
            raise BentoMLException(f"Deployment {self.name} has no target")
        return target

    def get_config(self, refetch: bool = True) -> DeploymentConfig:
        target = self._refetch_target(refetch)
        if target.config is None:
            raise BentoMLException(f"Deployment {self.name} has no config")

        return DeploymentConfig(
            name=self.name,
            bento=self.get_bento(refetch=False),
            services=target.config.services,
            access_authorization=target.config.access_authorization,
            envs=target.config.envs,
        )

    def get_status(self, refetch: bool = True) -> DeploymentState:
        if refetch:
            self._refetch()
        updated_at = self._schema.created_at.strftime("%Y-%m-%d %H:%M:%S")
        if self._schema.updated_at is not None:
            updated_at = self._schema.updated_at.strftime("%Y-%m-%d %H:%M:%S")
        return DeploymentState(
            status=self._schema.status.value,
            created_at=self._schema.created_at.strftime("%Y-%m-%d %H:%M:%S"),
            updated_at=updated_at,
        )

    def get_bento(self, refetch: bool = True) -> str:
        target = self._refetch_target(refetch)
        if target.bento is None:
            raise BentoMLException(f"Deployment {self.name} has no bento")
        return target.bento.repository.name + ":" + target.bento.version

    def get_client(
        self,
        media_type: str = "application/json",
        token: str | None = None,
    ) -> SyncHTTPClient:
        from _bentoml_impl.client import SyncHTTPClient

        self._refetch()
        if self._schema.status != DeploymentStatus.Running:
            raise BentoMLException(f"Deployment status is {self._schema.status}")
        if self._urls is None or len(self._urls) != 1:
            raise BentoMLException("Deployment url is not ready")

        return SyncHTTPClient(self._urls[0], media_type=media_type, token=token)

    def get_async_client(
        self,
        media_type: str = "application/json",
        token: str | None = None,
    ) -> AsyncHTTPClient:
        from _bentoml_impl.client import AsyncHTTPClient

        self._refetch()
        if self._schema.status != DeploymentStatus.Running:
            raise BentoMLException(f"Deployment status is {self._schema.status}")
        if self._urls is None or len(self._urls) != 1:
            raise BentoMLException("Deployment url is not ready")
        return AsyncHTTPClient(self._urls[0], media_type=media_type, token=token)

    def wait_until_ready(
        self,
        timeout: int = 3600,
        check_interval: int = 10,
        spinner: Spinner | None = None,
        spinner_task_id: TaskID | None = None,
    ) -> None:
        start_time = time.time()
        if spinner is not None and spinner_task_id is not None:
            while time.time() - start_time < timeout:
                status = self.get_status()
                spinner.spinner_progress.update(
                    spinner_task_id,
                    action=f"Waiting for deployment '{self.name}' to be ready. Current status: '{status.status}'.",
                )
                if status.status == DeploymentStatus.Running.value:
                    spinner.spinner_progress.update(
                        spinner_task_id,
                        action=f'[bold green] Deployment "{self.name}" is ready.[/bold green]',
                    )
                    spinner.spinner_progress.stop_task(spinner_task_id)
                    return
                time.sleep(check_interval)
            spinner.spinner_progress.update(
                spinner_task_id,
                action=f'[bold red]Time out waiting for Deployment "{self.name}" ready.[/bold red]',
            )
            spinner.spinner_progress.stop_task(spinner_task_id)
        else:
            while time.time() - start_time < timeout:
                status = self.get_status()
                if status.status == DeploymentStatus.Running.value:
                    logger.info(
                        f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] Deployment '{self.name}' is ready."
                    )
                    return
                logger.info(
                    f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] Waiting for deployment '{self.name}' to be ready. Current status: '{status.status}'."
                )
                time.sleep(check_interval)

        raise TimeoutError(
            f"Timed out waiting for deployment '{self.name}' to be ready."
        )


@attr.define
class Deployment:
    @staticmethod
    def _fix_scaling(
        scaling: DeploymentTargetHPAConf | None,
    ) -> DeploymentTargetHPAConf:
        if scaling is None:
            return DeploymentTargetHPAConf(1, 1)
        if scaling.min_replicas is None:
            scaling.min_replicas = 1
        if scaling.max_replicas is None:
            scaling.max_replicas = max(scaling.min_replicas, 1)
        # one edge case:
        if scaling.min_replicas > scaling.max_replicas:
            raise BentoMLException(
                "min scaling values must be less than or equal to max scaling values"
            )
        if scaling.min_replicas < 0:
            raise BentoMLException(
                "min scaling values must be greater than or equal to 0"
            )
        if scaling.max_replicas <= 0:
            raise BentoMLException("max scaling values must be greater than 0")
        return scaling

    @classmethod
    def _fix_and_validate_schema(
        cls,
        config_struct: UpdateDeploymentSchemaV2,
    ):
        # fix scaling
        for _, svc in config_struct.services.items():
            svc.scaling = cls._fix_scaling(svc.scaling)

    @staticmethod
    def _convert_schema_to_update_schema(_schema: DeploymentSchema) -> dict[str, t.Any]:
        if _schema.latest_revision is None:
            raise BentoMLException(f"Deployment {_schema.name} has no latest revision")
        if len(_schema.latest_revision.targets) == 0:
            raise BentoMLException(
                f"Deployment {_schema.name} has no latest revision targets"
            )
        target_schema = _schema.latest_revision.targets[0]
        if target_schema is None:
            raise BentoMLException(f"Deployment {_schema.name} has no target")
        if target_schema.config is None:
            raise BentoMLException(f"Deployment {_schema.name} has no config")
        if target_schema.bento is None:
            raise BentoMLException(f"Deployment {_schema.name} has no bento")
        update_schema = UpdateDeploymentSchemaV2(
            services=target_schema.config.services,
            access_authorization=target_schema.config.access_authorization,
            envs=target_schema.config.envs,
            bento=target_schema.bento.repository.name + ":" + target_schema.bento.name,
        )
        return bentoml_cattr.unstructure(update_schema)

    @staticmethod
    def _convert_schema_to_bento(_schema: DeploymentSchema) -> Tag:
        if _schema.latest_revision is None:
            raise BentoMLException(f"Deployment {_schema.name} has no latest revision")
        target_schema = _schema.latest_revision.targets[0]
        if target_schema is None:
            raise BentoMLException(f"Deployment {_schema.name} has no target")
        if target_schema.bento is None:
            raise BentoMLException(f"Deployment {_schema.name} has no bento")
        return Tag.from_taglike(
            target_schema.bento.repository.name + ":" + target_schema.bento.name
        )

    @staticmethod
    def _generate_deployment_info_(
        context: str | None, res: DeploymentSchema, urls: list[str] | None = None
    ) -> DeploymentInfo:
        client = get_rest_api_client(context)
        cluster_display_name = res.cluster.host_cluster_display_name
        if cluster_display_name is None:
            cluster_display_name = res.cluster.name
        return DeploymentInfo(
            name=res.name,
            # TODO: update this after the url in the frontend is fixed
            admin_console=f"{client.v1.endpoint}/clusters/{res.cluster.name}/namespaces/{res.kube_namespace}/deployments/{res.name}",
            created_at=res.created_at.strftime("%Y-%m-%d %H:%M:%S"),
            created_by=res.creator.name,
            cluster=cluster_display_name,
            _schema=res,
            _context=context,
            _urls=urls,
        )

    @classmethod
    def list(
        cls,
        context: str | None = None,
        cluster: str | None = None,
        search: str | None = None,
    ) -> list[DeploymentInfo]:
        cloud_rest_client = get_rest_api_client(context)
        if cluster is None:
            res_count = cloud_rest_client.v2.list_deployment(all=True, search=search)
            if res_count.total == 0:
                return []
            res = cloud_rest_client.v2.list_deployment(
                search=search, count=res_count.total, all=True
            )
        else:
            res_count = cloud_rest_client.v2.list_deployment(cluster, search=search)
            if res_count.total == 0:
                return []
            res = cloud_rest_client.v2.list_deployment(
                cluster, search=search, count=res_count.total
            )
        return [cls._generate_deployment_info_(context, schema) for schema in res.items]

    @classmethod
    def create(
        cls,
        deployment_config_params: DeploymentConfigParameters,
        context: str | None = None,
    ) -> DeploymentInfo:
        cluster = deployment_config_params.get_cluster()
        config_params = deployment_config_params.get_config_dict()
        if config_params.get("bento") is None:
            raise ValueError("bento is required")

        config_struct = bentoml_cattr.structure(config_params, CreateDeploymentSchemaV2)
        cls._fix_and_validate_schema(config_struct)

        cloud_rest_client = get_rest_api_client(context)
        res = cloud_rest_client.v2.create_deployment(
            create_schema=config_struct, cluster=cluster
        )

        logger.debug("Deployment Schema: %s", config_struct)

        return cls._generate_deployment_info_(context, res, res.urls)

    @classmethod
    def update(
        cls,
        deployment_config_params: DeploymentConfigParameters,
        context: str | None = None,
    ) -> DeploymentInfo:
        name = deployment_config_params.get_name()
        if name is None:
            raise ValueError("name is required")
        cluster = deployment_config_params.get_cluster()

        cloud_rest_client = get_rest_api_client(context)
        deployment_schema = cloud_rest_client.v2.get_deployment(name, cluster)
        orig_dict = cls._convert_schema_to_update_schema(deployment_schema)

        config_params = deployment_config_params.get_config_dict(
            orig_dict.get("bento"),
        )

        config_merger.merge(orig_dict, config_params)
        config_struct = bentoml_cattr.structure(orig_dict, UpdateDeploymentSchemaV2)

        cls._fix_and_validate_schema(config_struct)

        res = cloud_rest_client.v2.update_deployment(
            cluster=deployment_schema.cluster.host_cluster_display_name,
            name=name,
            update_schema=config_struct,
        )
        logger.debug("Deployment Schema: %s", config_struct)
        return cls._generate_deployment_info_(context, res, res.urls)

    @classmethod
    def apply(
        cls,
        deployment_config_params: DeploymentConfigParameters,
        context: str | None = None,
    ) -> DeploymentInfo:
        cloud_rest_client = get_rest_api_client(context)
        name = deployment_config_params.get_name()
        if name is not None:
            try:
                deployment_schema = cloud_rest_client.v2.get_deployment(
                    name, deployment_config_params.get_cluster(pop=False)
                )
            except NotFound:
                return cls.create(
                    deployment_config_params=deployment_config_params,
                    context=context,
                )
            # directly update the deployment with the schema, do not merge with the existing schema
            config_struct = bentoml_cattr.structure(
                deployment_config_params.get_config_dict(), UpdateDeploymentSchemaV2
            )
            cls._fix_and_validate_schema(config_struct)

            res = cloud_rest_client.v2.update_deployment(
                name=name,
                update_schema=config_struct,
                cluster=deployment_schema.cluster.host_cluster_display_name,
            )
            logger.debug("Deployment Schema: %s", config_struct)
            return cls._generate_deployment_info_(context, res, res.urls)

        return cls.create(
            deployment_config_params=deployment_config_params,
            context=context,
        )

    @classmethod
    def get(
        cls,
        name: str,
        cluster: str | None = None,
        context: str | None = None,
    ) -> DeploymentInfo:
        cloud_rest_client = get_rest_api_client(context)
        res = cloud_rest_client.v2.get_deployment(name, cluster)
        return cls._generate_deployment_info_(context, res, res.urls)

    @classmethod
    def terminate(
        cls,
        name: str,
        cluster: str | None = None,
        context: str | None = None,
    ) -> DeploymentInfo:
        cloud_rest_client = get_rest_api_client(context)
        res = cloud_rest_client.v2.terminate_deployment(name, cluster)
        return cls._generate_deployment_info_(context, res, res.urls)

    @classmethod
    def delete(
        cls,
        name: str,
        cluster: str | None = None,
        context: str | None = None,
    ) -> None:
        cloud_rest_client = get_rest_api_client(context)
        cloud_rest_client.v2.delete_deployment(name, cluster)

    @classmethod
    def list_instance_types(
        cls,
        cluster: str | None = None,
        context: str | None = None,
    ) -> list[InstanceTypeInfo]:
        cloud_rest_client = get_rest_api_client(context)
        res = cloud_rest_client.v2.list_instance_types(cluster)
        return [
            InstanceTypeInfo(
                name=schema.display_name,
                description=schema.description,
                cpu=schema.config.resources.requests.cpu,
                memory=schema.config.resources.requests.memory,
                gpu=schema.config.resources.requests.gpu,
                gpu_type=schema.config.gpu_config.type,
                price=schema.config.price,
            )
            for schema in res
        ]


@attr.define
class InstanceTypeInfo:
    name: t.Optional[str] = None
    price: t.Optional[str] = None
    description: t.Optional[str] = None
    cpu: t.Optional[str] = None
    memory: t.Optional[str] = None
    gpu: t.Optional[str] = None
    gpu_type: t.Optional[str] = None

    def to_dict(self):
        return {k: v for k, v in attr.asdict(self).items() if v is not None and v != ""}
