# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

from typing import Dict, List

from gravitino.api.catalog import Catalog
from gravitino.api.model.model import Model
from gravitino.api.model.model_version import ModelVersion
from gravitino.api.model_change import ModelChange
from gravitino.api.model_version_change import ModelVersionChange
from gravitino.client.base_schema_catalog import BaseSchemaCatalog
from gravitino.client.generic_model import GenericModel
from gravitino.client.generic_model_version import GenericModelVersion
from gravitino.dto.audit_dto import AuditDTO
from gravitino.dto.requests.model_register_request import ModelRegisterRequest
from gravitino.dto.requests.model_update_request import ModelUpdateRequest
from gravitino.dto.requests.model_updates_request import ModelUpdatesRequest
from gravitino.dto.requests.model_version_link_request import ModelVersionLinkRequest
from gravitino.dto.requests.model_version_update_request import (
    ModelVersionUpdateRequest,
)
from gravitino.dto.requests.model_version_updates_request import (
    ModelVersionUpdatesRequest,
)
from gravitino.dto.responses.base_response import BaseResponse
from gravitino.dto.responses.drop_response import DropResponse
from gravitino.dto.responses.entity_list_response import EntityListResponse
from gravitino.dto.responses.model_response import ModelResponse
from gravitino.dto.responses.model_version_list_response import (
    ModelVersionInfoListResponse,
    ModelVersionListResponse,
)
from gravitino.dto.responses.model_version_response import ModelVersionResponse
from gravitino.dto.responses.model_version_uri_response import ModelVersionUriResponse
from gravitino.exceptions.handlers.model_error_handler import MODEL_ERROR_HANDLER
from gravitino.name_identifier import NameIdentifier
from gravitino.namespace import Namespace
from gravitino.rest.rest_utils import encode_string
from gravitino.utils import HTTPClient


class GenericModelCatalog(BaseSchemaCatalog):
    """
    The generic model catalog is a catalog that supports model and model version operations,
    for example, model register, model version link, model and model version list, etc.
    A model catalog is under the metalake.
    """

    def __init__(
        self,
        namespace: Namespace,
        name: str = None,
        catalog_type: Catalog.Type = Catalog.Type.UNSUPPORTED,
        provider: str = None,
        comment: str = None,
        properties: Dict[str, str] = None,
        audit: AuditDTO = None,
        rest_client: HTTPClient = None,
    ):
        super().__init__(
            namespace,
            name,
            catalog_type,
            provider,
            comment,
            properties,
            audit,
            rest_client,
        )

    def as_model_catalog(self):
        return self

    def list_models(self, namespace: Namespace) -> List[NameIdentifier]:
        """List the models in a schema namespace from the catalog.

        Args:
            namespace: The namespace of the schema.

        Raises:
            NoSuchSchemaException: If the schema does not exist.

        Returns:
            A list of NameIdentifier of models under the given namespace.
        """
        self._check_model_namespace(namespace)

        model_full_ns = self._model_full_namespace(namespace)
        resp = self.rest_client.get(
            self._format_model_request_path(model_full_ns),
            error_handler=MODEL_ERROR_HANDLER,
        )
        entity_list_resp = EntityListResponse.from_json(resp.body, infer_missing=True)
        entity_list_resp.validate()

        return [
            NameIdentifier.of(ident.namespace().level(2), ident.name())
            for ident in entity_list_resp.identifiers()
        ]

    def get_model(self, ident: NameIdentifier) -> Model:
        """Get a model by its identifier.

        Args:
            ident: The identifier of the model.

        Raises:
            NoSuchModelException: If the model does not exist.

        Returns:
            The model object.
        """
        self._check_model_ident(ident)

        model_full_ns = self._model_full_namespace(ident.namespace())
        resp = self.rest_client.get(
            f"{self._format_model_request_path(model_full_ns)}/{encode_string(ident.name())}",
            error_handler=MODEL_ERROR_HANDLER,
        )
        model_resp = ModelResponse.from_json(resp.body, infer_missing=True)
        model_resp.validate()

        return GenericModel(model_resp.model())

    def register_model(
        self, ident: NameIdentifier, comment: str, properties: Dict[str, str]
    ) -> Model:
        """Register a model in the catalog if the model is not existed, otherwise the
        ModelAlreadyExistsException will be thrown. The Model object will be created when the
        model is registered, users can call ModelCatalog#link_model_version to link the model
        version to the registered Model.

        Args:
            ident: The identifier of the model.
            comment: The comment of the model.
            properties: The properties of the model.

        Raises:
            ModelAlreadyExistsException: If the model already exists.
            NoSuchSchemaException: If the schema does not exist.

        Returns:
            The registered model object.
        """
        self._check_model_ident(ident)

        model_full_ns = self._model_full_namespace(ident.namespace())
        model_req = ModelRegisterRequest(
            name=ident.name(), comment=comment, properties=properties
        )
        model_req.validate()

        resp = self.rest_client.post(
            self._format_model_request_path(model_full_ns),
            model_req,
            error_handler=MODEL_ERROR_HANDLER,
        )
        model_resp = ModelResponse.from_json(resp.body, infer_missing=True)
        model_resp.validate()

        return GenericModel(model_resp.model())

    def delete_model(self, model_ident: NameIdentifier) -> bool:
        """Delete the model from the catalog. If the model does not exist, return false.
        If the model is successfully deleted, return true. The deletion of the model will also
        delete all the model versions linked to this model.

        Args:
            model_ident: The identifier of the model.

        Returns:
            True if the model is deleted successfully, False is the model does not exist.
        """
        self._check_model_ident(model_ident)

        model_full_ns = self._model_full_namespace(model_ident.namespace())
        resp = self.rest_client.delete(
            f"{self._format_model_request_path(model_full_ns)}/{encode_string(model_ident.name())}",
            error_handler=MODEL_ERROR_HANDLER,
        )
        drop_resp = DropResponse.from_json(resp.body, infer_missing=True)
        drop_resp.validate()

        return drop_resp.dropped()

    def list_model_versions(self, model_ident: NameIdentifier) -> List[int]:
        """List all the versions of the register model by NameIdentifier in the catalog.

        Args:
            model_ident: The identifier of the model.

        Raises:
            NoSuchModelException: If the model does not exist.

        Returns:
            A list of model versions.
        """
        self._check_model_ident(model_ident)

        model_full_ident = self._model_full_identifier(model_ident)
        resp = self.rest_client.get(
            f"{self._format_model_version_request_path(model_full_ident)}/versions",
            error_handler=MODEL_ERROR_HANDLER,
        )
        model_version_list_resp = ModelVersionListResponse.from_json(
            resp.body, infer_missing=True
        )
        model_version_list_resp.validate()

        return model_version_list_resp.versions()

    def list_model_version_infos(
        self, model_ident: NameIdentifier
    ) -> List[ModelVersion]:
        """List all the versions with their information of the register model by NameIdentifier in the catalog.

        Args:
            model_ident: The identifier of the model.

        Raises:
            NoSuchModelException: If the model does not exist.

        Returns:
            A list of model versions with their information.
        """
        self._check_model_ident(model_ident)
        model_full_ident = self._model_full_identifier(model_ident)

        params = {"details": "true"}
        resp = self.rest_client.get(
            f"{self._format_model_version_request_path(model_full_ident)}/versions",
            params=params,
            error_handler=MODEL_ERROR_HANDLER,
        )
        model_version_info_list_resp = ModelVersionInfoListResponse.from_json(
            resp.body, infer_missing=True
        )
        model_version_info_list_resp.validate()

        return [
            GenericModelVersion(version)
            for version in model_version_info_list_resp.versions()
        ]

    def get_model_version(
        self, model_ident: NameIdentifier, version: int
    ) -> ModelVersion:
        """Get a model version by its identifier and version.

        Args:
            model_ident: The identifier of the model.
            version: The version of the model.

        Raises:
            NoSuchModelVersionException: If the model version does not exist.

        Returns:
            The model version object.
        """
        self._check_model_ident(model_ident)

        model_full_ident = self._model_full_identifier(model_ident)
        resp = self.rest_client.get(
            f"{self._format_model_version_request_path(model_full_ident)}/versions/{version}",
            error_handler=MODEL_ERROR_HANDLER,
        )
        model_version_resp = ModelVersionResponse.from_json(
            resp.body, infer_missing=True
        )
        model_version_resp.validate()

        return GenericModelVersion(model_version_resp.model_version())

    def get_model_version_by_alias(
        self, model_ident: NameIdentifier, alias: str
    ) -> ModelVersion:
        """
        Get a model version by its identifier and alias.

        Args:
            model_ident: The identifier of the model.
            alias: The alias of the model version.

        Raises:
            NoSuchModelVersionException: If the model version does not exist.

        Returns:
            The model version object.
        """
        self._check_model_ident(model_ident)

        model_full_ident = self._model_full_identifier(model_ident)
        resp = self.rest_client.get(
            f"{self._format_model_version_request_path(model_full_ident)}/aliases/"
            f"{encode_string(alias)}",
            error_handler=MODEL_ERROR_HANDLER,
        )
        model_version_resp = ModelVersionResponse.from_json(
            resp.body, infer_missing=True
        )
        model_version_resp.validate()

        return GenericModelVersion(model_version_resp.model_version())

    def alter_model(self, model_ident: NameIdentifier, *changes: ModelChange) -> Model:
        """Alter the schema by applying the changes.
        Args:
            model_ident: The identifier of the model.
            changes: The changes to apply to the model.
        Raises:
            NoSuchSchemaException: If the schema does not exist.
            IllegalArgumentException: If the changes are invalid.
        Returns:
            The updated schema object.
        """
        self._check_model_ident(model_ident)
        model_full_ns = self._model_full_namespace(model_ident.namespace())

        update_requests = [
            GenericModelCatalog.to_model_update_request(change) for change in changes
        ]

        req = ModelUpdatesRequest(update_requests)
        req.validate()

        resp = self.rest_client.put(
            f"{self._format_model_request_path(model_full_ns)}/{encode_string(model_ident.name())}",
            req,
            error_handler=MODEL_ERROR_HANDLER,
        )
        model_response = ModelResponse.from_json(resp.body, infer_missing=True)
        model_response.validate()
        return GenericModel(model_response.model())

    def alter_model_version(
        self, model_ident: NameIdentifier, version: int, *changes: ModelVersionChange
    ):
        """
        Alter the model version by applying the changes.
        Args:
            model_ident: The identifier of the model.
            version: The version of the model version.
            changes: The changes to apply to the model version.
        Raises:
            NoSuchModelVersionException: If the model version does not exist.
            IllegalArgumentException: If the changes are invalid.
        Returns:
            The updated model version object.
        """
        self._check_model_ident(model_ident)

        model_full_ident = self._model_full_identifier(model_ident)

        update_requests = [
            GenericModelCatalog.to_model_version_update_request(change)
            for change in changes
        ]

        req = ModelVersionUpdatesRequest(update_requests)
        req.validate()

        resp = self.rest_client.put(
            f"{self._format_model_version_request_path(model_full_ident)}/versions/{version}",
            req,
            error_handler=MODEL_ERROR_HANDLER,
        )

        model_version_response = ModelVersionResponse.from_json(
            resp.body, infer_missing=True
        )
        model_version_response.validate()
        return GenericModelVersion(model_version_response.model_version())

    def alter_model_version_by_alias(
        self, model_ident: NameIdentifier, alias: str, *changes: ModelVersionChange
    ):
        """
        Alter the model version by applying the changes.
        Args:
            model_ident: The identifier of the model.
            alias: The alias of the model version.
            changes: The changes to apply to the model version.
        Raises:
            NoSuchModelVersionException: If the model version does not exist.
            IllegalArgumentException: If the changes are invalid.
        Returns:
            The updated model version object.
        """
        self._check_model_ident(model_ident)

        model_full_ident = self._model_full_identifier(model_ident)

        update_requests = [
            GenericModelCatalog.to_model_version_update_request(change)
            for change in changes
        ]

        req = ModelVersionUpdatesRequest(update_requests)
        req.validate()

        resp = self.rest_client.put(
            f"{self._format_model_version_request_path(model_full_ident)}/aliases/"
            f"{encode_string(alias)}",
            req,
            error_handler=MODEL_ERROR_HANDLER,
        )

        model_version_response = ModelVersionResponse.from_json(
            resp.body, infer_missing=True
        )
        model_version_response.validate()
        return GenericModelVersion(model_version_response.model_version())

    def link_model_version(
        self,
        model_ident: NameIdentifier,
        uri: str,
        aliases: List[str],
        comment: str,
        properties: Dict[str, str],
    ) -> None:
        """Link a new model version to the registered model object. The new model version will be
        added to the model object. If the model object does not exist, it will throw an
        exception. If the version alias already exists in the model, it will throw an exception.

        Args:
            model_ident: The identifier of the model.
            uri: The URI of the model version.
            aliases: The aliases of the model version. The aliases of the model version. The
            aliases should be unique in this model, otherwise the
            ModelVersionAliasesAlreadyExistException will be thrown. The aliases are optional and
            can be empty.
            comment: The comment of the model version.
            properties: The properties of the model version.

        Raises:
            NoSuchModelException: If the model does not exist.
            ModelVersionAliasesAlreadyExistException: If the aliases of the model version already exist.
        """
        uris = {ModelVersion.URI_NAME_UNKNOWN: uri} if uri else {}

        return self.link_model_version_with_multiple_uris(
            model_ident, uris, aliases, comment, properties
        )

    def link_model_version_with_multiple_uris(
        self,
        model_ident: NameIdentifier,
        uris: Dict[str, str],
        aliases: List[str],
        comment: str,
        properties: Dict[str, str],
    ) -> None:
        """Link a new model version to the registered model object. The new model version will be
        added to the model object. If the model object does not exist, it will throw an
        exception. If the version alias already exists in the model, it will throw an exception.

        Args:
            model_ident: The identifier of the model.
            uris: The URIs and their names of the model version.
            aliases: The aliases of the model version. The aliases of the model version. The
            aliases should be unique in this model, otherwise the
            ModelVersionAliasesAlreadyExistException will be thrown. The aliases are optional and
            can be empty.
            comment: The comment of the model version.
            properties: The properties of the model version.

        Raises:
            NoSuchModelException: If the model does not exist.
            ModelVersionAliasesAlreadyExistException: If the aliases of the model version already exist.
        """
        self._check_model_ident(model_ident)

        model_full_ident = self._model_full_identifier(model_ident)

        request = ModelVersionLinkRequest(uris, comment, aliases, properties)
        request.validate()

        resp = self.rest_client.post(
            f"{self._format_model_version_request_path(model_full_ident)}/versions",
            request,
            error_handler=MODEL_ERROR_HANDLER,
        )
        base_resp = BaseResponse.from_json(resp.body, infer_missing=True)
        base_resp.validate()

    def get_model_version_uri(
        self, model_ident: NameIdentifier, version: int, uri_name: str = None
    ):
        """Get the URI of the model artifact with a specified version number.

        Args:
            model_ident: The identifier of the model.
            version: The version of the model.
            uri_name: The name of the URI. If None, the default URI will be used.

        Raises:
            NoSuchModelVersionException: If the model version does not exist.
            NoSuchModelVersionURINameException: If the uri name does not exist.

        Returns:
            The URI of the model version.
        """
        self._check_model_ident(model_ident)

        model_full_ident = self._model_full_identifier(model_ident)
        params = {}
        if uri_name is not None:
            params["uriName"] = uri_name

        resp = self.rest_client.get(
            f"{self._format_model_version_request_path(model_full_ident)}/versions/{version}/uri",
            params=params,
            error_handler=MODEL_ERROR_HANDLER,
        )

        model_version_uri_resp = ModelVersionUriResponse.from_json(
            resp.body, infer_missing=True
        )
        model_version_uri_resp.validate()

        return model_version_uri_resp.uri()

    def get_model_version_uri_by_alias(
        self, model_ident: NameIdentifier, alias: str, uri_name: str = None
    ):
        """Get the URI of the model artifact with a specified version alias.

        Args:
            model_ident: The identifier of the model.
            alias: The alias of the model version.
            uri_name: The name of the URI. If None, the default URI will be used.

        Raises:
            NoSuchModelVersionException: If the model version does not exist.
            NoSuchModelVersionURINameException: If the uri name does not exist.

        Returns:
            The URI of the model version.
        """
        self._check_model_ident(model_ident)

        model_full_ident = self._model_full_identifier(model_ident)
        params = {}
        if uri_name is not None:
            params["uriName"] = uri_name

        resp = self.rest_client.get(
            f"{self._format_model_version_request_path(model_full_ident)}/aliases/{encode_string(alias)}/uri",
            params=params,
            error_handler=MODEL_ERROR_HANDLER,
        )

        model_version_uri_resp = ModelVersionUriResponse.from_json(
            resp.body, infer_missing=True
        )
        model_version_uri_resp.validate()

        return model_version_uri_resp.uri()

    def delete_model_version(self, model_ident: NameIdentifier, version: int) -> bool:
        """Delete the model version from the catalog. If the model version does not exist, return false.
        If the model version is successfully deleted, return true.

        Args:
            model_ident: The identifier of the model.
            version: The version of the model.

        Returns:
            True if the model version is deleted successfully, False is the model version does not exist.
        """
        self._check_model_ident(model_ident)

        model_full_ident = self._model_full_identifier(model_ident)
        resp = self.rest_client.delete(
            f"{self._format_model_version_request_path(model_full_ident)}/versions/{version}",
            error_handler=MODEL_ERROR_HANDLER,
        )
        drop_resp = DropResponse.from_json(resp.body, infer_missing=True)
        drop_resp.validate()

        return drop_resp.dropped()

    def delete_model_version_by_alias(
        self, model_ident: NameIdentifier, alias: str
    ) -> bool:
        """Delete the model version by alias from the catalog. If the model version does not exist,
        return false. If the model version is successfully deleted, return true.

        Args:
            model_ident: The identifier of the model.
            alias: The alias of the model version.

        Returns:
            True if the model version is deleted successfully, False is the model version does not exist.
        """
        self._check_model_ident(model_ident)

        model_full_ident = self._model_full_identifier(model_ident)
        resp = self.rest_client.delete(
            f"{self._format_model_version_request_path(model_full_ident)}/aliases/"
            f"{encode_string(alias)}",
            error_handler=MODEL_ERROR_HANDLER,
        )
        drop_resp = DropResponse.from_json(resp.body, infer_missing=True)
        drop_resp.validate()

        return drop_resp.dropped()

    def register_model_version(
        self,
        ident: NameIdentifier,
        uri: str,
        aliases: List[str],
        comment: str,
        properties: Dict[str, str],
    ) -> Model:
        """Register a model in the catalog if the model is not existed, otherwise the
        ModelAlreadyExistsException will be thrown. The Model object will be created when the
        model is registered, in the meantime, the model version (version 0) will also be created and
        linked to the registered model. Register a model in the catalog and link a new model
        version to the registered model.

        Args:
            ident: The identifier of the model.
            uri: The URI of the model version.
            aliases: The aliases of the model version.
            comment: The comment of the model.
            properties: The properties of the model.

        Raises:
            ModelAlreadyExistsException: If the model already exists.
            ModelVersionAliasesAlreadyExistException: If the aliases of the model version already exist.

        Returns:
            The registered model object.
        """
        uris = {ModelVersion.URI_NAME_UNKNOWN: uri} if uri else {}
        return self.register_model_version_with_multiple_uris(
            ident, uris, aliases, comment, properties
        )

    def register_model_version_with_multiple_uris(
        self,
        ident: NameIdentifier,
        uris: Dict[str, str],
        aliases: List[str],
        comment: str,
        properties: Dict[str, str],
    ) -> Model:
        """Register a model in the catalog if the model is not existed, otherwise the
        ModelAlreadyExistsException will be thrown. The Model object will be created when the
        model is registered, in the meantime, the model version (version 0) will also be created and
        linked to the registered model. Register a model in the catalog and link a new model
        version to the registered model.

        Args:
            ident: The identifier of the model.
            uris: The URIs and their names of the model version.
            aliases: The aliases of the model version.
            comment: The comment of the model.
            properties: The properties of the model.

        Raises:
            ModelAlreadyExistsException: If the model already exists.
            ModelVersionAliasesAlreadyExistException: If the aliases of the model version already exist.

        Returns:
            The registered model object.
        """
        model = self.register_model(ident, comment, properties)
        self.link_model_version_with_multiple_uris(
            ident, uris, aliases, comment, properties
        )
        return model

    @staticmethod
    def to_model_update_request(change: ModelChange):
        if isinstance(change, ModelChange.RenameModel):
            return ModelUpdateRequest.UpdateModelNameRequest(change.new_name())

        if isinstance(change, ModelChange.SetProperty):
            return ModelUpdateRequest.ModelSetPropertyRequest(
                change.property(), change.value()
            )

        if isinstance(change, ModelChange.RemoveProperty):
            return ModelUpdateRequest.ModelRemovePropertyRequest(change.property())

        if isinstance(change, ModelChange.UpdateComment):
            return ModelUpdateRequest.UpdateModelCommentRequest(change.new_comment())

        raise ValueError(f"Unknown change type: {type(change).__name__}")

    # pylint: disable=too-many-return-statements
    @staticmethod
    def to_model_version_update_request(change: ModelVersionChange):
        if isinstance(change, ModelVersionChange.UpdateComment):
            return ModelVersionUpdateRequest.UpdateModelVersionComment(
                change.new_comment()
            )

        if isinstance(change, ModelVersionChange.SetProperty):
            return ModelVersionUpdateRequest.SetModelVersionPropertyRequest(
                change.property(), change.value()
            )

        if isinstance(change, ModelVersionChange.RemoveProperty):
            return ModelVersionUpdateRequest.RemoveModelVersionPropertyRequest(
                change.property()
            )

        if isinstance(change, ModelVersionChange.UpdateUri):
            return ModelVersionUpdateRequest.UpdateModelVersionUriRequest(
                change.new_uri(), change.uri_name()
            )

        if isinstance(change, ModelVersionChange.AddUri):
            return ModelVersionUpdateRequest.AddModelVersionUriRequest(
                change.uri_name(), change.uri()
            )

        if isinstance(change, ModelVersionChange.RemoveUri):
            return ModelVersionUpdateRequest.RemoveModelVersionUriRequest(
                change.uri_name()
            )

        if isinstance(change, ModelVersionChange.UpdateAliases):
            return ModelVersionUpdateRequest.ModelVersionAliasesRequest(
                change.aliases_to_add(), change.aliases_to_remove()
            )

        raise ValueError(f"Unknown change type: {type(change).__name__}")

    def _check_model_namespace(self, namespace: Namespace):
        """Check the validity of the model namespace.

        Args:
            namespace: The namespace of the schema.

        Raises:
            IllegalNamespaceException: If the namespace is illegal.
        """
        Namespace.check(
            namespace is not None and namespace.length() == 1,
            f"Model namespace must be non-null and have 1 level, the input namespace is {namespace}",
        )

    def _check_model_ident(self, ident: NameIdentifier):
        """Check the validity of the model identifier.

        Args:
            ident: The identifier of the model.

        Raises:
            IllegalNameIdentifierException: If the identifier is illegal.
            IllegalNamespaceException: If the namespace is illegal.
        """
        NameIdentifier.check(
            ident is not None and ident.has_namespace(),
            f"Model identifier must be non-null and have a namespace, the input identifier is {ident}",
        )
        NameIdentifier.check(
            ident.name() is not None and len(ident.name()) > 0,
            f"Model name must be non-null and non-empty, the input name is {ident.name()}",
        )
        self._check_model_namespace(ident.namespace())

    def _format_model_request_path(self, model_ns: Namespace) -> str:
        """Format the model request path.

        Args:
            model_ns: The namespace of the model.

        Returns:
            The formatted model request path.
        """
        schema_ns = Namespace.of(model_ns.level(0), model_ns.level(1))
        return (
            f"{BaseSchemaCatalog.format_schema_request_path(schema_ns)}/"
            f"{encode_string(model_ns.level(2))}/models"
        )

    def _format_model_version_request_path(self, model_ident: NameIdentifier) -> str:
        """Format the model version request path.

        Args:
            model_ident: The identifier of the model.

        Returns:
            The formatted model version request path.
        """
        return (
            f"{self._format_model_request_path(model_ident.namespace())}"
            f"/{encode_string(model_ident.name())}"
        )

    def _model_full_namespace(self, model_namespace: Namespace) -> Namespace:
        """Get the full namespace of the model.

        Args:
            model_namespace: The namespace of the model.

        Returns:
            The full namespace of the model.
        """
        return Namespace.of(
            self._catalog_namespace.level(0), self.name(), model_namespace.level(0)
        )

    def _model_full_identifier(self, model_ident: NameIdentifier) -> NameIdentifier:
        """Get the full identifier of the model.

        Args:
            model_ident: The identifier of the model.

        Returns:
            The full identifier of the model.
        """
        return NameIdentifier.builder(
            self._model_full_namespace(model_ident.namespace()), model_ident.name()
        )
