import dataiku
from semantic_models.exceptions import NotFoundError

VERSION_LIST_FIELDS = (
    "entities",
    "relationships",
    "goldenQueries",
    "glossaryTerms",
    "glossaryBindings",
)
# Note: vocabularyTermIds is stored in sqlGenerationConfig for DSS persistence
VERSION_DICT_FIELDS = ("indexingSettings", "privateEditorData", "sqlGenerationConfig")


class LocalSemanticModelClient:
    def __init__(self, project_key: str | None = None):
        self._client = dataiku.api_client()
        self._project = (
            self._client.get_default_project() if project_key is None else self._client.get_project(project_key)
        )
        self._project_key = self._project.project_key

    def _get_semantic_model(self, model_id: str):
        return self._project.get_semantic_model(model_id)

    def _get_definition(self, model_id: str) -> dict:
        return self._get_semantic_model(model_id)._get_definition()

    def _normalize_model(self, dss_model: dict) -> dict:
        """Normalize model for service consumption."""
        model = dict(dss_model)
        model["versions"] = [self._normalize_version(v) for v in dss_model.get("versions", [])]
        return model

    def _normalize_version(self, dss_version: dict) -> dict:
        """Normalize DSS version for service consumption."""
        version = self._normalize_version_fields(dss_version, include_missing=True)
        entities = list(version.get("entities") or [])
        relationships = list(version.get("relationships") or [])
        self._normalize_relationship_entities(relationships, entities)
        version["entities"] = entities
        version["relationships"] = relationships
        return version

    def _normalize_relationship_entities(self, relationships: list[dict], entities: list[dict]) -> None:
        """Normalize relationship entity references to match entity names (case-insensitive)."""
        entity_ids = {e.get("name") for e in entities if isinstance(e.get("name"), str)}
        if not entity_ids:
            return

        # Build case-insensitive map; skip ambiguous entries
        lower_map: dict[str, str | None] = {}
        for entity_id in entity_ids:
            key = entity_id.lower()
            if key in lower_map and lower_map[key] != entity_id:
                lower_map[key] = None
            else:
                lower_map[key] = entity_id

        for rel in relationships:
            for side in ("firstEntity", "secondEntity"):
                val = rel.get(side)
                if not isinstance(val, str):
                    continue
                if val in entity_ids:
                    continue
                mapped = lower_map.get(val.lower())
                if mapped:
                    rel[side] = mapped

    def _normalize_version_payload(self, plugin_version: dict) -> dict:
        """Normalize version payload before saving to DSS."""
        return self._normalize_version_fields(plugin_version, include_missing=False, include_created=True)

    def _normalize_version_fields(
        self,
        version_data: dict,
        *,
        include_missing: bool,
        include_created: bool = False,
    ) -> dict:
        normalized = dict(version_data)

        for key in VERSION_LIST_FIELDS:
            if include_missing or key in version_data:
                normalized[key] = version_data.get(key) or []

        for key in VERSION_DICT_FIELDS:
            if include_missing or key in version_data:
                normalized[key] = version_data.get(key) or {}

        if include_created and "created" in version_data:
            normalized["created"] = version_data.get("created") or {}

        return normalized

    def list_models(self) -> list[dict]:
        # as_type="objects" returns DSSSemanticModel objects directly
        semantic_models = self._project.list_semantic_models(as_type="objects")
        result = []
        for sm in semantic_models:
            definition = sm._get_definition()
            result.append(self._normalize_model(definition))
        return result

    def get_model(self, model_id: str) -> dict:
        definition = self._get_definition(model_id)
        return self._normalize_model(definition)

    def update_model(
        self, model_id: str, name: str | None = None, description: str | None = None, short_desc: str | None = None
    ) -> dict:
        sm = self._get_semantic_model(model_id)
        definition = sm._get_definition()
        if name is not None:
            definition["name"] = name
        if description is not None:
            definition["description"] = description
        if short_desc is not None:
            definition["shortDesc"] = short_desc
        sm._set_definition(definition)
        return self._normalize_model(sm._get_definition())

    def delete_model(self, model_id: str) -> None:
        sm = self._get_semantic_model(model_id)
        sm.delete()

    def set_active_version(self, model_id: str, version_id: str) -> dict:
        sm = self._get_semantic_model(model_id)
        sm.set_active_version_id(version_id)
        return self.get_model(model_id)

    def list_versions(self, model_id: str) -> list[dict]:
        definition = self._get_definition(model_id)
        return [self._normalize_version(v) for v in definition.get("versions", [])]

    def get_version(self, model_id: str, version_id: str) -> dict:
        definition = self._get_definition(model_id)
        for v in definition.get("versions", []):
            if v.get("id") == version_id:
                return self._normalize_version(v)
        raise NotFoundError(f"Version {version_id} not found for model {model_id}")

    def put_version(self, model_id: str, version_id: str, version: dict) -> dict:
        """Update an existing version.

        This method is intentionally update-only: creation is handled by
        create_version().
        """
        sm = self._get_semantic_model(model_id)
        dss_version = self._normalize_version_payload(version)
        dss_version["id"] = version_id

        settings = sm.get_version(version_id).get_settings()

        raw = settings.get_raw()
        for key, value in dss_version.items():
            raw[key] = value

        settings.save()
        return self.get_version(model_id, version_id)

    def create_version(self, model_id: str, version: dict, duplicate_of: str | None = None) -> dict:
        sm = self._get_semantic_model(model_id)
        existing_ids = sm.list_versions_ids()
        max_num = 0
        for version_id in existing_ids:
            if isinstance(version_id, str) and version_id.startswith("v") and version_id[1:].isdigit():
                max_num = max(max_num, int(version_id[1:]))
        new_version_id = f"v{max_num + 1}"

        settings = (
            sm.new_version(new_version_id, duplicate_of=duplicate_of)
            if duplicate_of
            else sm.new_version(new_version_id)
        )

        dss_version = self._normalize_version_payload(version)
        dss_version["id"] = new_version_id

        # Merge into defaults (keeps indexingSettings/privateEditorData when not provided)
        raw = settings.get_raw()
        for key, value in dss_version.items():
            raw[key] = value

        settings.save()
        return self.get_version(model_id, new_version_id)

    def delete_version(self, model_id: str, version_id: str) -> None:
        sm = self._get_semantic_model(model_id)
        definition = sm._get_definition()

        versions = definition.get("versions", [])
        new_versions = [v for v in versions if v.get("id") != version_id]

        if len(new_versions) == len(versions):
            raise NotFoundError(f"Version {version_id} not found for model {model_id}")

        definition["versions"] = new_versions

        if definition.get("activeVersionId") == version_id:
            definition["activeVersionId"] = None

        sm._set_definition(definition)
