import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Optional

from semantic_models.glossary_types import (
    BindingTargetType,
    GlossaryBinding,
    GlossaryTerm,
    GlossaryTermSource,
)
from semantic_models.services.glossary_utils import normalize_term
from semantic_models.utils.ids import generate_uuid

from semantic_models.exceptions import NotFoundError, ValidationError
from semantic_models.logging_utils import summarize_for_logging, truncate_text

logger = logging.getLogger(__name__)


def _now() -> datetime:
    return datetime.now(timezone.utc)


def _get_private_data(term: GlossaryTerm) -> dict[str, Any]:
    return term.privateEditorData or {}


def _normalize_private_editor_data(data: Optional[dict[str, Any]]) -> dict[str, Any]:
    """Return private editor data as-is for persisted terms."""
    return dict(data or {})


def _get_term_language(term: GlossaryTerm) -> Optional[str]:
    return _get_private_data(term).get("language")


def _set_term_language(term: GlossaryTerm, language: Optional[str]) -> None:
    private = _get_private_data(term)
    if language is None:
        private.pop("language", None)
    else:
        private["language"] = language
    term.privateEditorData = private



@dataclass
class GoldenQuery:
    name: str
    question: str
    generatedSql: str
    description: Optional[str] = None
    created: Optional[dict[str, Any]] = None


@dataclass
class Model:
    id: str
    projectKey: str
    name: str
    activeVersionId: Optional[str]
    versionTag: Optional[dict[str, Any]] = None
    creationTag: Optional[dict[str, Any]] = None
    tags: list[str] = field(default_factory=list)
    customFields: dict[str, Any] = field(default_factory=dict)
    checklists: dict[str, Any] = field(default_factory=dict)
    description: Optional[str] = None
    shortDesc: Optional[str] = None


@dataclass
class Version:
    id: str
    description: Optional[str] = None
    created: Optional[dict[str, Any]] = None
    entities: list[dict[str, Any]] = field(default_factory=list)
    relationships: list[dict[str, Any]] = field(default_factory=list)
    goldenQueries: list[GoldenQuery] = field(default_factory=list)
    vocabularyTermIds: list[str] = field(default_factory=list)
    glossaryBindings: list["GlossaryBinding"] = field(default_factory=list)
    glossaryTerms: list["GlossaryTerm"] = field(default_factory=list)
    indexingSettings: Optional[dict[str, Any]] = None
    privateEditorData: dict[str, Any] = field(default_factory=dict)
    sqlGenerationConfig: dict[str, Any] = field(default_factory=dict)


@dataclass
class ModelWithVersions:
    model: Model
    versions: list[Version]


class SemanticModelsService:

    def __init__(self, client: object):
        self._client = client

    def _model_from_dict(self, data: dict) -> Model:
        return Model(
            id=data["id"],
            projectKey=data["projectKey"],
            name=data.get("name") or "",
            activeVersionId=data.get("activeVersionId"),
            versionTag=data.get("versionTag"),
            creationTag=data.get("creationTag"),
            tags=data.get("tags") or [],
            customFields=data.get("customFields") or {},
            checklists=data.get("checklists") or {},
            description=data.get("description"),
            shortDesc=data.get("shortDesc"),
        )

    def _version_from_dict(self, data: dict) -> Version:
        sql_generation_config = data.get("sqlGenerationConfig") or {}
        # vocabularyTermIds is stored in sqlGenerationConfig for DSS persistence
        vocabulary_term_ids = sql_generation_config.pop("vocabularyTermIds", [])

        return Version(
            id=data["id"],
            description=data.get("description"),
            created=data.get("created"),
            entities=data.get("entities", []),
            relationships=data.get("relationships", []),
            goldenQueries=[
                GoldenQuery(
                    name=q.get("name") or "",
                    question=q.get("question") or "",
                    generatedSql=q.get("generatedSql") or "",
                    description=q.get("description"),
                    created=q.get("created"),
                )
                for q in data.get("goldenQueries", [])
            ],
            vocabularyTermIds=vocabulary_term_ids,
            glossaryBindings=[
                GlossaryBinding(
                    termId=b.get("termId") or "",
                    targetEntityClass=b.get("targetEntityClass") or "",
                    targetType=b.get("targetType"),
                    targetName=b.get("targetName"),
                    created=b.get("created"),
                )
                for b in data.get("glossaryBindings", [])
            ],
            glossaryTerms=[
                GlossaryTerm(
                    id=t.get("id") or "",
                    term=t.get("term") or "",
                    description=t.get("description"),
                    source=t.get("source") or "MANUAL",
                    userModified=t.get("userModified", False),
                    synonyms=t.get("synonyms") or [],
                    created=t.get("created"),
                    modified=t.get("modified"),
                    privateEditorData=_normalize_private_editor_data(t.get("privateEditorData")),
                )
                for t in data.get("glossaryTerms", [])
            ],
            indexingSettings=data.get("indexingSettings"),
            privateEditorData=data.get("privateEditorData") or {},
            sqlGenerationConfig=sql_generation_config,
        )

    def _version_to_dict(self, version: Version) -> dict:
        sql_generation_config = dict(version.sqlGenerationConfig or {})
        # Store vocabularyTermIds in sqlGenerationConfig for DSS persistence
        sql_generation_config["vocabularyTermIds"] = version.vocabularyTermIds

        data = {
            "id": version.id,
            "description": version.description,
            "entities": version.entities,
            "relationships": version.relationships,
            "goldenQueries": [
                {
                    "name": q.name,
                    "question": q.question,
                    "generatedSql": q.generatedSql,
                    "description": q.description,
                    "created": q.created,
                }
                for q in version.goldenQueries
            ],
            "glossaryBindings": [
                {
                    "termId": b.termId,
                    "targetEntityClass": b.targetEntityClass,
                    "targetType": b.targetType,
                    "targetName": b.targetName,
                    "created": b.created,
                }
                for b in version.glossaryBindings
            ],
            "glossaryTerms": [
                {
                    "id": t.id,
                    "term": t.term,
                    "description": t.description,
                    "source": t.source,
                    "userModified": t.userModified,
                    "synonyms": t.synonyms,
                    "created": t.created,
                    "modified": t.modified,
                    "privateEditorData": t.privateEditorData,
                }
                for t in version.glossaryTerms
            ],
            "indexingSettings": version.indexingSettings,
            "privateEditorData": version.privateEditorData,
            "sqlGenerationConfig": sql_generation_config,
        }
        if version.created is not None:
            data["created"] = version.created
        return data

    def _save_version(self, model_id: str, version: Version) -> None:
        logger.debug(
            "Persisting version: model_id=%s version_id=%s entities=%d relationships=%d glossary_terms=%d glossary_bindings=%d vocabulary_terms=%d",
            model_id,
            version.id,
            len(version.entities),
            len(version.relationships),
            len(version.glossaryTerms),
            len(version.glossaryBindings),
            len(version.vocabularyTermIds),
        )
        self._client.put_version(model_id, version.id, self._version_to_dict(version))

    def _model_sort_key(self, model: Model) -> int:
        tag = model.creationTag or {}
        return int(tag.get("lastModifiedOn") or 0)

    def _make_created_tag(self, created_by: Optional[str]) -> dict[str, Any]:
        tag: dict[str, Any] = {"on": _now().isoformat()}
        if created_by:
            tag["by"] = created_by
        return tag

    def _normalize_binding_target_type(self, target_type: Any) -> str:
        if hasattr(target_type, "value"):
            return str(target_type.value)
        return str(target_type)

    def _validate_binding_target(
        self,
        version: Version,
        entity_id: str,
        target_type: Any,
        target_id: Optional[str],
    ) -> tuple[str, Optional[str]]:
        entity = next((e for e in version.entities if e.get("name") == entity_id), None)
        if not entity:
            raise ValidationError(f"Entity not found: {entity_id}")

        target_type_str = self._normalize_binding_target_type(target_type)
        if target_type_str == "ENTITY_CLASS":
            return target_type_str, None

        if not target_id:
            raise ValidationError(f"target_id is required for {target_type_str} bindings")

        if target_type_str == "ATTRIBUTE":
            candidates = entity.get("attributes", [])
        elif target_type_str == "METRIC":
            candidates = entity.get("metrics", [])
        elif target_type_str == "FILTER":
            candidates = entity.get("filters", [])
        else:
            raise ValidationError(f"Unsupported target type: {target_type_str}")

        target = next((item for item in candidates if item.get("name") == target_id), None)
        if not target:
            raise ValidationError(f"{target_type_str.capitalize()} not found: {target_id}")

        return target_type_str, target_id

    def _binding_key(
        self,
        term_id: str,
        entity_id: str,
        target_type: str,
        target_id: Optional[str],
    ) -> tuple[str, str, str, Optional[str]]:
        return term_id, entity_id, target_type, target_id

    def list_models(self) -> list[Model]:
        models = [self._model_from_dict(m) for m in self._client.list_models()]
        return sorted(models, key=self._model_sort_key, reverse=True)

    def get_model(self, model_id: str) -> ModelWithVersions:
        model_dict = self._client.get_model(model_id)
        model = self._model_from_dict(model_dict)
        raw_versions = model_dict.get("versions", [])
        versions = [self._version_from_dict(v) for v in raw_versions]
        return ModelWithVersions(model=model, versions=versions)

    def update_model(
        self, model_id: str, name: Optional[str], description: Optional[str], short_desc: Optional[str]
    ) -> Model:
        logger.info(
            "Updating model metadata: model_id=%s payload=%s",
            model_id,
            summarize_for_logging(
                {"name": name, "description": description, "short_desc": short_desc},
                max_chars=300,
                max_items=20,
            ),
        )
        data = self._client.update_model(model_id, name, description, short_desc)
        logger.info("Updated model metadata: model_id=%s", model_id)
        return self._model_from_dict(data)

    def delete_model(self, model_id: str) -> None:
        logger.info("Deleting model: model_id=%s", model_id)
        self._client.delete_model(model_id)
        logger.info("Deleted model: model_id=%s", model_id)

    def list_versions(self, model_id: str) -> list[Version]:
        raw = self._client.list_versions(model_id)
        return [self._version_from_dict(v) for v in raw]

    def get_version(self, model_id: str, version_id: str) -> Version:
        data = self._client.get_version(model_id, version_id)
        return self._version_from_dict(data)

    def create_version(
        self,
        model_id: str,
        version_data: Optional[dict],
        created_by: Optional[str],
        from_version: Optional[str] = None,
    ) -> Version:
        logger.info(
            "Creating version: model_id=%s from_version=%s payload_keys=%s created_by=%s",
            model_id,
            from_version,
            sorted((version_data or {}).keys()),
            created_by or "-",
        )
        model_before = self._model_from_dict(self._client.get_model(model_id))
        has_active_version = bool(model_before.activeVersionId)

        payload = dict(version_data or {})
        created_tag = payload.get("created")
        if not isinstance(created_tag, dict):
            created_tag = {}
        if "on" not in created_tag:
            created_tag["on"] = _now().isoformat()
        if created_by and "by" not in created_tag:
            created_tag["by"] = created_by
        payload["created"] = created_tag

        if not from_version:
            payload.setdefault("entities", [])
            payload.setdefault("relationships", [])
            payload.setdefault("goldenQueries", [])
            payload.setdefault("glossaryTerms", [])
            payload.setdefault("glossaryBindings", [])
            payload.setdefault("vocabularyTermIds", [])
            payload.setdefault("indexingSettings", {})
            payload.setdefault("privateEditorData", {})

        stored = self._client.create_version(model_id, payload, duplicate_of=from_version)
        version = self._version_from_dict(stored)
        logger.info(
            "Created version: model_id=%s version_id=%s entities=%d relationships=%d glossary_terms=%d",
            model_id,
            version.id,
            len(version.entities),
            len(version.relationships),
            len(version.glossaryTerms),
        )

        if from_version:
            private = dict(version.privateEditorData or {})
            if private.get("duplicatedFromVersionId") != from_version:
                private["duplicatedFromVersionId"] = from_version
                version.privateEditorData = private
                self._save_version(model_id, version)
                logger.info(
                    "Stored version duplication metadata: model_id=%s version_id=%s duplicated_from=%s",
                    model_id,
                    version.id,
                    from_version,
                )

        if not has_active_version:
            self._client.set_active_version(model_id, version.id)
            logger.info("Set active version after create: model_id=%s version_id=%s", model_id, version.id)
        return version

    def update_version(self, model_id: str, version_id: str, version_patch: dict) -> Version:
        logger.info(
            "Updating version: model_id=%s version_id=%s patch=%s",
            model_id,
            version_id,
            summarize_for_logging(version_patch, max_chars=300, max_items=40, max_depth=3),
        )
        version = self.get_version(model_id, version_id)
        version_dict = self._version_to_dict(version)
        version_dict.update(version_patch)
        updated = self._version_from_dict(version_dict)
        self._save_version(model_id, updated)
        logger.info(
            "Updated version: model_id=%s version_id=%s entities=%d relationships=%d glossary_terms=%d",
            model_id,
            version_id,
            len(updated.entities),
            len(updated.relationships),
            len(updated.glossaryTerms),
        )
        return updated

    def delete_version(self, model_id: str, version_id: str) -> None:
        logger.info("Deleting version: model_id=%s version_id=%s", model_id, version_id)
        model_with_versions = self.get_model(model_id)
        model = model_with_versions.model
        versions = model_with_versions.versions

        if len(versions) <= 1:
            raise ValidationError("Cannot delete the last version of a model. Delete the entire model instead.")
        if not model.activeVersionId:
            raise ValidationError("Model has versions but no active version is set.")
        if model.activeVersionId == version_id:
            raise ValidationError("Cannot delete the active version. Set a different active version first.")
        if not any(v.id == version_id for v in versions):
            raise NotFoundError(f"Version {version_id} not found for model {model_id}")

        self._client.delete_version(model_id, version_id)
        logger.info("Deleted version: model_id=%s version_id=%s", model_id, version_id)

    def set_active_version(self, model_id: str, version_id: str) -> Model:
        logger.info("Setting active version: model_id=%s version_id=%s", model_id, version_id)
        self.get_version(model_id, version_id)
        data = self._client.set_active_version(model_id, version_id)
        logger.info("Set active version: model_id=%s version_id=%s", model_id, version_id)
        return self._model_from_dict(data)

    def get_active_version(self, model_id: str) -> Optional[Version]:
        model_with_versions = self.get_model(model_id)
        active_version_id = model_with_versions.model.activeVersionId
        versions = model_with_versions.versions
        if not versions:
            return None
        if not active_version_id:
            raise ValidationError("Model has versions but no active version is set.")

        active_version = next((version for version in versions if version.id == active_version_id), None)
        if active_version is None:
            raise NotFoundError(f"Version {active_version_id} not found for model {model_id}")

        return active_version

    def list_glossary_terms(self, model_id: str, version_id: str) -> list[GlossaryTerm]:
        version = self.get_version(model_id, version_id)
        return version.glossaryTerms

    def get_glossary_term(self, model_id: str, version_id: str, term_id: str) -> GlossaryTerm:
        version = self.get_version(model_id, version_id)
        for term in version.glossaryTerms:
            if term.id == term_id:
                return term
        raise NotFoundError(f"Glossary term not found: {term_id}")

    def create_glossary_term(
        self,
        model_id: str,
        version_id: str,
        term: str,
        description: Optional[str] = None,
        source: GlossaryTermSource = "MANUAL",
        synonyms: Optional[list[str]] = None,
        user_modified: bool = False,
        private_editor_data: Optional[dict[str, Any]] = None,
        created_by: Optional[str] = None,
        term_id: Optional[str] = None,
    ) -> GlossaryTerm:
        logger.info(
            "Creating glossary term: model_id=%s version_id=%s term=%s source=%s synonyms=%d user_modified=%s created_by=%s",
            model_id,
            version_id,
            truncate_text(term, max_chars=200),
            source,
            len(synonyms or []),
            user_modified,
            created_by or "-",
        )
        version = self.get_version(model_id, version_id)

        normalized = normalize_term(term)
        for existing in version.glossaryTerms:
            if normalize_term(existing.term) == normalized:
                raise ValidationError(f"A term with this name already exists: '{existing.term}'")
        if term_id and any(existing.id == term_id for existing in version.glossaryTerms):
            raise ValidationError(f"A term with this id already exists: '{term_id}'")

        new_term = GlossaryTerm(
            id=term_id or generate_uuid(),
            term=term,
            description=description,
            source=source,
            userModified=user_modified,
            synonyms=synonyms or [],
            created=self._make_created_tag(created_by),
            privateEditorData=_normalize_private_editor_data(private_editor_data),
        )
        version.glossaryTerms.append(new_term)
        self._save_version(model_id, version)
        logger.info(
            "Created glossary term: model_id=%s version_id=%s term_id=%s total_terms=%d",
            model_id,
            version_id,
            new_term.id,
            len(version.glossaryTerms),
        )
        return new_term

    def update_glossary_term(
        self,
        model_id: str,
        version_id: str,
        term_id: str,
        term: Optional[str] = None,
        description: Optional[str] = None,
        source: Optional[GlossaryTermSource] = None,
        synonyms: Optional[list[str]] = None,
        user_modified: Optional[bool] = None,
        private_editor_data: Optional[dict[str, Any]] = None,
        modified_by: Optional[str] = None,
    ) -> GlossaryTerm:
        logger.info(
            "Updating glossary term: model_id=%s version_id=%s term_id=%s patch=%s modified_by=%s",
            model_id,
            version_id,
            term_id,
            summarize_for_logging(
                {
                    "term": term,
                    "description": description,
                    "source": source,
                    "synonyms": synonyms,
                    "user_modified": user_modified,
                    "private_editor_data": private_editor_data,
                },
                max_chars=300,
                max_items=20,
                max_depth=3,
            ),
            modified_by or "-",
        )
        version = self.get_version(model_id, version_id)

        for i, t in enumerate(version.glossaryTerms):
            if t.id == term_id:
                if term is not None:
                    normalized = normalize_term(term)
                    for existing in version.glossaryTerms:
                        if existing.id != term_id and normalize_term(existing.term) == normalized:
                            raise ValidationError(f"A term with this name already exists: '{existing.term}'")
                    t.term = term
                if description is not None:
                    t.description = description
                if source is not None:
                    t.source = source
                if synonyms is not None:
                    t.synonyms = synonyms
                if user_modified is not None:
                    t.userModified = user_modified
                if private_editor_data is not None:
                    t.privateEditorData = {
                        **t.privateEditorData,
                        **_normalize_private_editor_data(private_editor_data),
                    }
                t.modified = self._make_created_tag(modified_by)
                version.glossaryTerms[i] = t
                self._save_version(model_id, version)
                logger.info(
                    "Updated glossary term: model_id=%s version_id=%s term_id=%s",
                    model_id,
                    version_id,
                    term_id,
                )
                return t
        raise NotFoundError(f"Glossary term not found: {term_id}")

    def delete_glossary_term(self, model_id: str, version_id: str, term_id: str) -> None:
        logger.info("Deleting glossary term: model_id=%s version_id=%s term_id=%s", model_id, version_id, term_id)
        version = self.get_version(model_id, version_id)

        found = False
        for i, term in enumerate(version.glossaryTerms):
            if term.id == term_id:
                version.glossaryTerms.pop(i)
                found = True
                break
        if not found:
            raise NotFoundError(f"Glossary term not found: {term_id}")

        version.glossaryBindings = [b for b in version.glossaryBindings if b.termId != term_id]
        if term_id in version.vocabularyTermIds:
            version.vocabularyTermIds.remove(term_id)

        self._save_version(model_id, version)
        logger.info(
            "Deleted glossary term: model_id=%s version_id=%s term_id=%s remaining_terms=%d remaining_bindings=%d remaining_vocabulary=%d",
            model_id,
            version_id,
            term_id,
            len(version.glossaryTerms),
            len(version.glossaryBindings),
            len(version.vocabularyTermIds),
        )

    def list_glossary_bindings(self, model_id: str, version_id: str) -> list[GlossaryBinding]:
        version = self.get_version(model_id, version_id)
        return version.glossaryBindings

    def create_glossary_binding(
        self,
        model_id: str,
        version_id: str,
        term_id: str,
        entity_id: str,
        target_type: BindingTargetType,
        target_id: Optional[str] = None,
    ) -> GlossaryBinding:
        logger.info(
            "Creating glossary binding: model_id=%s version_id=%s term_id=%s entity_id=%s target_type=%s target_id=%s",
            model_id,
            version_id,
            term_id,
            entity_id,
            target_type,
            target_id,
        )
        version = self.get_version(model_id, version_id)

        if not any(t.id == term_id for t in version.glossaryTerms):
            raise NotFoundError(f"Glossary term not found: {term_id}")

        if term_id in version.vocabularyTermIds:
            raise ValidationError(f"Term {term_id} is in vocabulary. Remove from vocabulary before creating binding.")

        target_type_str, normalized_target_id = self._validate_binding_target(version, entity_id, target_type, target_id)

        binding = GlossaryBinding(
            termId=term_id,
            targetEntityClass=entity_id,
            targetType=target_type_str,
            targetName=normalized_target_id,
            created=self._make_created_tag(None),
        )
        version.glossaryBindings.append(binding)
        self._save_version(model_id, version)
        logger.info(
            "Created glossary binding: model_id=%s version_id=%s term_id=%s entity_id=%s target_type=%s target_id=%s total_bindings=%d",
            model_id,
            version_id,
            term_id,
            entity_id,
            target_type_str,
            target_id,
            len(version.glossaryBindings),
        )
        return binding

    def create_glossary_bindings_bulk(
        self,
        model_id: str,
        version_id: str,
        bindings: list[dict],
    ) -> list[GlossaryBinding]:
        logger.info(
            "Creating glossary bindings in bulk: model_id=%s version_id=%s bindings_count=%d",
            model_id,
            version_id,
            len(bindings),
        )
        version = self.get_version(model_id, version_id)
        term_ids = {t.id for t in version.glossaryTerms}
        vocabulary_term_ids = set(version.vocabularyTermIds)
        existing_keys = {
            self._binding_key(
                b.termId,
                b.targetEntityClass,
                self._normalize_binding_target_type(b.targetType),
                b.targetName,
            )
            for b in version.glossaryBindings
        }

        created = []
        for b in bindings:
            term_id = b.get("termId")
            entity_id = b.get("targetEntityClass")
            target_type = b.get("targetType")
            target_id = b.get("targetName")

            if term_id not in term_ids:
                raise NotFoundError(f"Glossary term not found: {term_id}")
            if term_id in vocabulary_term_ids:
                raise ValidationError(
                    f"Term {term_id} is in vocabulary. Remove from vocabulary before creating binding."
                )

            target_type_str, normalized_target_id = self._validate_binding_target(version, entity_id, target_type, target_id)
            key = self._binding_key(term_id or "", entity_id or "", target_type_str, normalized_target_id)
            if key in existing_keys:
                raise ValidationError(
                    "Glossary binding already exists: "
                    f"term_id={term_id}, entity_id={entity_id}, target_type={target_type_str}, target_id={normalized_target_id}"
                )
            existing_keys.add(key)

            binding = GlossaryBinding(
                termId=term_id or "",
                targetEntityClass=entity_id or "",
                targetType=target_type_str,
                targetName=normalized_target_id,
                created=self._make_created_tag(None),
            )
            version.glossaryBindings.append(binding)
            created.append(binding)

        self._save_version(model_id, version)
        logger.info(
            "Created glossary bindings in bulk: model_id=%s version_id=%s created=%d total_bindings=%d",
            model_id,
            version_id,
            len(created),
            len(version.glossaryBindings),
        )
        return created

    def delete_glossary_binding(
        self,
        model_id: str,
        version_id: str,
        term_id: str,
        entity_id: str,
        target_type: BindingTargetType,
        target_id: Optional[str],
    ) -> None:
        logger.info(
            "Deleting glossary binding: model_id=%s version_id=%s term_id=%s entity_id=%s target_type=%s target_id=%s",
            model_id,
            version_id,
            term_id,
            entity_id,
            target_type,
            target_id,
        )
        version = self.get_version(model_id, version_id)
        target_type_str = self._normalize_binding_target_type(target_type)
        for i, binding in enumerate(version.glossaryBindings):
            if (
                binding.termId == term_id
                and binding.targetEntityClass == entity_id
                and binding.targetType == target_type_str
                and binding.targetName == target_id
            ):
                version.glossaryBindings.pop(i)
                self._save_version(model_id, version)
                logger.info(
                    "Deleted glossary binding: model_id=%s version_id=%s term_id=%s entity_id=%s target_type=%s target_id=%s remaining_bindings=%d",
                    model_id,
                    version_id,
                    term_id,
                    entity_id,
                    target_type_str,
                    target_id,
                    len(version.glossaryBindings),
                )
                return
        raise NotFoundError("Glossary binding not found")

    def update_glossary_binding(
        self,
        model_id: str,
        version_id: str,
        term_id: str,
        entity_id: str,
        target_type: BindingTargetType,
        target_id: Optional[str],
        new_entity_id: str | None = None,
        new_target_type: BindingTargetType | None = None,
        new_target_id: str | None = None,
    ) -> GlossaryBinding:
        logger.info(
            "Updating glossary binding: model_id=%s version_id=%s term_id=%s entity_id=%s target_type=%s target_id=%s -> new_entity_id=%s new_target_type=%s new_target_id=%s",
            model_id,
            version_id,
            term_id,
            entity_id,
            target_type,
            target_id,
            new_entity_id,
            new_target_type,
            new_target_id,
        )
        version = self.get_version(model_id, version_id)

        target_type_str = self._normalize_binding_target_type(target_type)
        for i, binding in enumerate(version.glossaryBindings):
            if (
                binding.termId == term_id
                and binding.targetEntityClass == entity_id
                and binding.targetType == target_type_str
                and binding.targetName == target_id
            ):
                final_entity_id = new_entity_id if new_entity_id is not None else binding.targetEntityClass
                final_target_type = new_target_type if new_target_type is not None else binding.targetType
                final_target_id = new_target_id if new_target_id is not None else binding.targetName

                final_target_type_str, final_target_id = self._validate_binding_target(
                    version,
                    final_entity_id,
                    final_target_type,
                    final_target_id,
                )

                binding.targetEntityClass = final_entity_id
                binding.targetType = final_target_type_str
                binding.targetName = final_target_id
                version.glossaryBindings[i] = binding
                self._save_version(model_id, version)
                logger.info(
                    "Updated glossary binding: model_id=%s version_id=%s term_id=%s entity_id=%s target_type=%s target_id=%s",
                    model_id,
                    version_id,
                    binding.termId,
                    binding.targetEntityClass,
                    binding.targetType,
                    binding.targetName,
                )
                return binding

        raise NotFoundError("Glossary binding not found")

    def list_vocabulary_term_ids(self, model_id: str, version_id: str) -> list[str]:
        version = self.get_version(model_id, version_id)
        return version.vocabularyTermIds

    def add_vocabulary_term(self, model_id: str, version_id: str, term_id: str) -> None:
        logger.info("Adding term to vocabulary: model_id=%s version_id=%s term_id=%s", model_id, version_id, term_id)
        version = self.get_version(model_id, version_id)

        if not any(t.id == term_id for t in version.glossaryTerms):
            raise NotFoundError(f"Glossary term not found: {term_id}")

        if any(b.termId == term_id for b in version.glossaryBindings):
            raise ValidationError(f"Term {term_id} has bindings. Delete bindings before adding to vocabulary.")

        if term_id not in version.vocabularyTermIds:
            version.vocabularyTermIds.append(term_id)
            self._save_version(model_id, version)
            logger.info(
                "Added term to vocabulary: model_id=%s version_id=%s term_id=%s vocabulary_count=%d",
                model_id,
                version_id,
                term_id,
                len(version.vocabularyTermIds),
            )
        else:
            logger.info(
                "Term already in vocabulary: model_id=%s version_id=%s term_id=%s",
                model_id,
                version_id,
                term_id,
            )

    def remove_vocabulary_term(self, model_id: str, version_id: str, term_id: str) -> None:
        logger.info("Removing term from vocabulary: model_id=%s version_id=%s term_id=%s", model_id, version_id, term_id)
        version = self.get_version(model_id, version_id)
        if term_id in version.vocabularyTermIds:
            version.vocabularyTermIds.remove(term_id)
            self._save_version(model_id, version)
            logger.info(
                "Removed term from vocabulary: model_id=%s version_id=%s term_id=%s vocabulary_count=%d",
                model_id,
                version_id,
                term_id,
                len(version.vocabularyTermIds),
            )
        else:
            logger.info(
                "Term not in vocabulary, no change: model_id=%s version_id=%s term_id=%s",
                model_id,
                version_id,
                term_id,
            )

    def update_vocabulary_term_ids(self, model_id: str, version_id: str, term_ids: list[str]) -> None:
        logger.info(
            "Replacing vocabulary terms: model_id=%s version_id=%s term_ids=%s",
            model_id,
            version_id,
            summarize_for_logging(term_ids, max_chars=100, max_items=100),
        )
        version = self.get_version(model_id, version_id)

        existing_term_ids = {t.id for t in version.glossaryTerms}
        for term_id in term_ids:
            if term_id not in existing_term_ids:
                raise NotFoundError(f"Glossary term not found: {term_id}")

        bound_term_ids = {b.termId for b in version.glossaryBindings}
        for term_id in term_ids:
            if term_id in bound_term_ids:
                raise ValidationError(f"Term {term_id} has bindings. Delete bindings before adding to vocabulary.")

        version.vocabularyTermIds = term_ids
        self._save_version(model_id, version)
        logger.info(
            "Replaced vocabulary terms: model_id=%s version_id=%s vocabulary_count=%d",
            model_id,
            version_id,
            len(version.vocabularyTermIds),
        )
