"""
Fabric Variable Library item.

Provides the VariableLibrary class for managing CI/CD variable libraries,
including definition management, value set activation, and metadata updates.

Variable libraries store named typed variables and alternative value sets
used across deployment pipeline stages for application lifecycle management.
"""

import base64
import json as _json
import re
from dataclasses import asdict, dataclass, is_dataclass
from datetime import datetime
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast, overload

from ..._shared.exceptions import FabiasError, NotFoundError
from ._shared import Item, ItemAccessor

# JSON Schema URIs for definition parts
_VARIABLES_SCHEMA = (
    "https://developer.microsoft.com/json-schemas/fabric/item/"
    "variableLibrary/definition/variables/1.0.0/schema.json"
)
_VALUESET_SCHEMA = (
    "https://developer.microsoft.com/json-schemas/fabric/item/"
    "variableLibrary/definition/valueSet/1.0.0/schema.json"
)
_SETTINGS_SCHEMA = (
    "https://developer.microsoft.com/json-schemas/fabric/item/"
    "variableLibrary/definition/settings/1.0.0/schema.json"
)

if TYPE_CHECKING:
    from .._client import FabricClient


# ---------------------------------------------------------------------------
# Module-level helpers (shared by VariableAccessor and VariableLibrary)
# ---------------------------------------------------------------------------


def _decode_part(payload: str) -> Dict[str, Any]:
    """Base64-decode a definition part payload into a Python dict."""
    return _json.loads(base64.b64decode(payload))


def _encode_part(data: Dict[str, Any]) -> str:
    """JSON-encode and Base64-encode a Python dict for a definition part payload.

    Matches Fabric's internal serialisation exactly:

    * ``indent=2`` — 2-space indented JSON (not compact)
    * ``sort_keys=True`` — keys sorted alphabetically at every nesting level
    * ``\\r\\n`` line endings (CRLF) — Fabric stores Windows-style line endings
    * No trailing newline — Fabric does not append a final newline after ``}``

    Getting all four of these right is critical for git-sync stability: Fabric
    will silently reformat content that doesn't match and the resulting Base64
    change looks like an uncommitted edit that can't be pushed back through
    protected-branch PRs.
    """
    serialized = _json.dumps(data, cls=_FabricEncoder, indent=2, sort_keys=True)
    # Convert LF → CRLF (no trailing newline — Fabric ends with bare `}`)
    serialized = serialized.replace("\n", "\r\n")
    return base64.b64encode(serialized.encode()).decode()


def _coerce_value(
    type_str: str, raw: Any
) -> Union[bool, str, int, float, "ItemReference", datetime, None]:
    """Coerce a raw API value to the appropriate Python type."""
    if raw is None:
        return None
    if type_str == "Boolean":
        return raw if isinstance(raw, bool) else str(raw).lower() in ("true", "1", "yes")
    if type_str == "Integer":
        return int(raw)
    if type_str == "Number":
        return float(raw)
    if type_str == "DateTime":
        return raw if isinstance(raw, datetime) else datetime.fromisoformat(str(raw))
    if type_str == "ItemReference":
        if isinstance(
            raw, ItemReference
        ):  # noqa: F821  (defined below, forward ref fine at runtime)
            return raw
        if isinstance(raw, dict):
            return ItemReference(
                itemId=raw.get("itemId", ""), workspaceId=raw.get("workspaceId", "")
            )
        raise FabiasError(f"Cannot coerce {raw!r} to ItemReference")
    return str(raw)  # String (default)


class _FabricEncoder(_json.JSONEncoder):
    """JSON encoder for Fabric definition objects.

    Handles ``datetime`` (→ ISO-8601 string) and any ``dataclass`` (→ dict via
    ``asdict``). This means ``Variable``, ``ItemReference``, or any future
    dataclass can be serialised without extra glue code.
    """

    def default(self, obj: Any) -> Any:  # noqa: ANN401
        if is_dataclass(obj) and not isinstance(obj, type):
            return asdict(obj)
        if isinstance(obj, datetime):
            s = obj.isoformat()
            # Normalize UTC offset to 'Z' to match the API's expected format.
            if s.endswith("+00:00"):
                s = s[:-6] + "Z"
            return s
        return super().default(obj)


def _to_json_safe(data: Any) -> Any:
    """Recursively convert any non-JSON-serializable values (datetime, dataclasses)
    to their JSON equivalents by round-tripping through _FabricEncoder.

    The result is a plain Python structure (dicts, lists, strings, numbers, bools,
    None) safe for both ``json.dumps`` and API payloads.
    """
    return _json.loads(_json.dumps(data, cls=_FabricEncoder))


@dataclass
class ItemReference:
    """Represents a reference to another item, used for ItemReference variable types."""

    itemId: str
    workspaceId: str


@dataclass(init=False)
class Variable:
    """Represents a single variable in a variable library.

    ``type`` is optional — it is inferred from the Python type of ``value`` when
    omitted::

        Variable("region", "eastus")                  # str   → "String"
        Variable("retries", 3)                          # int   → "Integer"
        Variable("ratio", 0.5)                          # float → "Number"
        Variable("enabled", True)                       # bool  → "Boolean"
        Variable("lh", ItemReference(...))              # → "ItemReference"
        Variable("ts", datetime(2024, 1, 1))            # → "DateTime"

    ``Guid`` values are stored as Python strings and cannot be distinguished from
    ``String`` automatically — pass ``type="Guid"`` explicitly when needed.
    """

    name: str
    type: str
    value: Union[bool, str, int, float, "ItemReference", datetime]
    note: str = ""
    overrides: Optional[Dict[str, Union[bool, str, int, float, "ItemReference", datetime]]] = None

    def __init__(
        self,
        name: str,
        value: Any = None,
        type: Optional[str] = None,
        note: Optional[str] = "",
        overrides: Optional[Dict] = None,
    ):
        self.name = name
        self.note = note or ""
        self.overrides = overrides
        resolved: str
        if type is not None:
            resolved = type
        elif isinstance(value, bool):
            resolved = "Boolean"
        elif isinstance(value, int):
            resolved = "Integer"
        elif isinstance(value, float):
            resolved = "Number"
        elif isinstance(value, datetime):
            resolved = "DateTime"
        elif isinstance(value, ItemReference):
            resolved = "ItemReference"  # noqa: F821
        elif isinstance(value, str):
            resolved = "String"
        else:
            raise FabiasError(
                f"Cannot infer Fabric type from Python type '{value.__class__.__name__}'; "
                "pass type= explicitly (e.g. type='Guid')"
            )
        self.type = resolved
        self.value = cast(
            Union[bool, str, int, float, ItemReference, datetime],
            _coerce_value(resolved, value),
        )

    def override(
        self, value_set: str, value: Union[bool, str, int, float, "ItemReference", datetime]
    ) -> None:
        """Add or update an override value for a specific value set."""
        if self.overrides is None:
            self.overrides = {}
        self.overrides[value_set] = cast(
            Union[bool, str, int, float, ItemReference, datetime],
            _coerce_value(self.type, value),
        )

    def __repr__(self):
        return f"Variable(name={self.name!r}, type={self.type!r}, value={self.value!r}, overrides={self.overrides!r})"


@dataclass
class VariableSchemas:
    variables: Optional[str] = _VARIABLES_SCHEMA
    valueSet: Optional[str] = _VALUESET_SCHEMA
    settings: Optional[str] = _SETTINGS_SCHEMA


class VariableAccessor:
    """
    Lazy-loading, cache-backed accessor for variables in a :class:`VariableLibrary`.

    Fetches the variable collection **once** on first access, then all reads and
    in-memory mutations are zero-cost. Call :meth:`commit` when finished to push
    all accumulated changes in a **single** ``updateDefinition`` call.

    Read access::

        var  = lib.variables["environment"]        # Variable — KeyError if missing
        var  = lib.variables.get("region")         # Variable or None
        exists = "maxRetries" in lib.variables
        all  = lib.variables()                     # List[Variable]
        for var in lib.variables: ...
        for name, var in lib.variables.items(): ...

    In-memory mutation (zero API calls)::

        lib.variables.add("region", "String", "eastus")
        lib.variables["environment"].value = "prod"   # direct field mutation
        lib.variables.remove("legacyFlag")

    Commit everything at once::

        lib.variables.commit()                     # one updateDefinition call

        # Or chain before committing
        lib.variables.add("region", "String", "eastus").add("tier", "String", "standard").commit()
    """

    def __init__(self, library: "VariableLibrary") -> None:
        self._lib = library
        self._cache: Optional[Dict[str, Variable]] = None
        self._valuesets: Dict[str, Dict[str, Any]] = {}
        self._platform: Optional[str] = None
        self._settings: Optional[Dict[str, Union[str, List[str]]]] = None

        self._schemas: VariableSchemas = VariableSchemas()

    # -----------------------------------------------------------------------
    # Internal
    # -----------------------------------------------------------------------

    def _load(self) -> Dict[str, Variable]:
        """Fetch and cache the variable collection (runs at most once)."""
        if self._cache is not None:
            return self._cache

        definition = self._lib._fetchDefinition()
        parts = definition.get("parts", [])

        vars_part = next((p for p in parts if p.get("path") == "variables.json"), None)
        settings_part = next((p for p in parts if p.get("path") == "settings.json"), None)
        platform_part = next((p for p in parts if p.get("path") == ".platform"), None)

        self._cache = {}
        if vars_part:
            vars_decoded = _decode_part(vars_part["payload"])
            self._schemas.variables = vars_decoded.get("$schema", _VARIABLES_SCHEMA)
            for v in vars_decoded.get("variables", []):
                vtype = v.get("type", "String")
                self._cache[v["name"]] = Variable(
                    name=v["name"],
                    type=vtype,
                    value=v.get("value"),
                    note=v.get("note", ""),
                    overrides={},
                )

        # Populate overrides from value set parts
        _skip = {"variables.json", "settings.json", ".platform"}
        for part in (p for p in parts if p.get("path") not in _skip):
            m = re.match(r"valueSets/(.+)\.json$", part.get("path", ""))
            value_set = m.group(1) if m else None
            if value_set is None:
                continue
            pld = _decode_part(part["payload"])
            self._schemas.valueSet = pld.get("$schema", _VALUESET_SCHEMA)
            self._valuesets[value_set] = {}
            for vr in pld.get("variableOverrides", []):
                var_name = vr.get("name")
                if var_name in self._cache and value_set:
                    self._valuesets[value_set][var_name] = vr.get("value")
                    cached_var = self._cache[var_name]
                    if cached_var.overrides is None:
                        cached_var.overrides = {}
                    cached_var.overrides[value_set] = cast(
                        Union[bool, str, int, float, ItemReference, datetime],
                        _coerce_value(cached_var.type, vr.get("value")),
                    )

        decoded_settings = _decode_part(settings_part["payload"]) if settings_part else None
        if decoded_settings:
            self._schemas.settings = decoded_settings.get("$schema", _SETTINGS_SCHEMA)
            self._settings = {"valueSetsOrder": decoded_settings.get("valueSetsOrder", [])}
        self._platform = platform_part if platform_part else None

        return self._cache

    def _invalidate(self) -> None:
        """Clear the cache and accumulated state. Called by :meth:`VariableLibrary.refresh` and after :meth:`commit`."""
        self._cache = None
        self._valuesets = {}
        self._settings = None
        self._platform = None

    # -----------------------------------------------------------------------
    # Read interface
    # -----------------------------------------------------------------------
    def __getitem__(self, name: str) -> Variable:
        result = self._load().get(name)
        if result is None:
            raise KeyError(f"Variable '{name}' not found in library")
        return result

    def __contains__(self, name: object) -> bool:
        return name in self._load()

    def __iter__(self):
        return iter(self._load().values())

    def __len__(self) -> int:
        return len(self._load())

    def __call__(self) -> List[Variable]:
        """Return all variables as a list."""
        return list(self._load().values())

    def get(self, name: str, default: Optional[Variable] = None) -> Optional[Variable]:
        """Return the variable by name, or *default* if not found."""
        return self._load().get(name, default)

    def items(self):
        """Yield ``(name, Variable)`` pairs."""
        return self._load().items()

    def keys(self):
        """Return variable names."""
        return self._load().keys()

    def values(self):
        """Return :class:`Variable` objects."""
        return self._load().values()

    # -----------------------------------------------------------------------
    # In-memory mutation (no API calls)
    # -----------------------------------------------------------------------

    @overload
    def add(self, variable: Variable) -> "VariableAccessor": ...

    @overload
    def add(
        self, name: str, value: Any = None, type: Optional[str] = None, note: Optional[str] = None
    ) -> "VariableAccessor": ...

    def add(
        self,
        name_or_variable: Union[str, Variable],
        value: Any = None,
        type: Optional[str] = None,
        note: Optional[str] = None,
    ) -> "VariableAccessor":
        """
        Add a variable to the in-memory collection.

        Does **not** call the API. Call :meth:`commit` when finished.

        Accepts either a :class:`Variable` object (with overrides pre-populated)
        or individual parameters for quick additions.

        Args:
            name_or_variable: A :class:`Variable` instance, or a variable name string
            type: Fabric type — ``"Boolean"``, ``"DateTime"``, ``"Integer"``,
                ``"Number"``, ``"String"``, ``"ItemReference"``, or ``"Guid"``.
                Inferred from the Python type of ``value`` when omitted.
                ``"Guid"`` must always be passed explicitly (stored as ``str``,
                indistinguishable from ``"String"`` by Python type alone).
                Only used when ``name_or_variable`` is a string.
            value: Value (only used when ``name_or_variable`` is a string)
            note: Optional note (only used when ``name_or_variable`` is a string)

        Returns:
            VariableAccessor: Self for chaining

        Examples:
            Quick add — type inferred from value:

            >>> lib.variables.add("region", "eastus").commit()             # → String
            >>> lib.variables.add("retries", 3).commit()                   # → Integer
            >>> lib.variables.add("enabled", True).commit()                # → Boolean

            Explicit type (required for Guid):

            >>> lib.variables.add("correlationId", "some-guid", type="Guid").commit()

            With a pre-built Variable (including overrides):

            >>> var = Variable("region", "eastus",
            ...                overrides={"Production": "westus2", "Staging": "centralus"})
            >>> lib.variables.add(var).commit()

            Duplicate names are silently replaced (upsert):

            >>> lib.variables.add("region", "westus2").commit()
        """
        if isinstance(name_or_variable, Variable):
            var = name_or_variable
        else:
            var = Variable(
                name=name_or_variable,
                value=value,
                type=type,
                note=note or "",
                overrides={},
            )

        # Ensure overrides is a dict (not None)
        if var.overrides is None:
            var.overrides = {}
        self._load()[var.name] = var
        return self

    def remove(self, name: str) -> "VariableAccessor":
        """
        Remove a variable from the in-memory collection.

        Does **not** call the API. Call :meth:`commit` when finished.

        Args:
            name: Variable name to remove

        Returns:
            VariableAccessor: Self for chaining

        Raises:
            NotFoundError: If the variable does not exist

        Examples:
            >>> lib.variables.remove("legacyFlag").commit()
        """
        cache = self._load()
        if name not in cache:
            raise NotFoundError(
                f"Variable '{name}' not found", resource_type="Variable", resource_id=name
            )
        del cache[name]
        return self

    # -----------------------------------------------------------------------
    # Commit
    # -----------------------------------------------------------------------

    def commit(self, update_meta: bool = False) -> "VariableLibrary":
        """
        Push all in-memory changes to the API in a single ``updateDefinition`` call.

        Serializes the current variable collection — defaults into ``variables.json``
        and overrides back into their respective ``valueSets/*.json`` parts (preserving
        existing name/description metadata). The other parts (settings, platform) were
        cached on the initial fetch, so no extra API call is needed.

        Returns:
            VariableLibrary: The parent library (state refreshed after commit)

        Examples:
            >>> lib.variables["environment"].value = "prod"
            >>> lib.variables["environment"].overrides["Staging"] = "staging"
            >>> lib.variables.add("region", "eastus")
            >>> lib.variables.add(Variable("tier", "free", overrides={"Production": "premium"}))
            >>> lib.variables.remove("legacyFlag")
            >>> lib.variables.commit()
        """
        if self._cache is None:
            return self._lib  # nothing loaded — nothing to commit

        # ── Build variables.json ─────────────────────────────────────────────
        serialized = [
            {
                k: v
                for k, v in _to_json_safe(asdict(var)).items()
                if k != "overrides" and v is not None
            }
            for var in self._cache.values()
        ]
        vars_part = {
            "path": "variables.json",
            "payload": _encode_part({"$schema": self._schemas.variables, "variables": serialized}),
            "payloadType": "InlineBase64",
        }

        # ── Rebuild value sets from scratch using only the current cache ──────
        # This ensures removed variables are excluded and stale entries don't
        # pollute the payload.
        final_valuesets: Dict[str, Dict[str, Any]] = {}
        new_vs_names: set = set()
        existing_order: List[str] = list((self._settings or {}).get("valueSetsOrder", []))

        for var in self._cache.values():
            var_dict = _to_json_safe(asdict(var))
            for vs_name, val in (var_dict.get("overrides") or {}).items():
                final_valuesets.setdefault(vs_name, {})[var.name] = val
                if vs_name not in existing_order:
                    new_vs_names.add(vs_name)

        # Drop empty value sets (can happen when a variable is removed)
        final_valuesets = {k: v for k, v in final_valuesets.items() if v}

        # Prune existing_order: remove any names that are now empty / gone
        updated_order = [n for n in existing_order if n in final_valuesets]
        # Append new value set names in sorted order
        final_order = updated_order + sorted(new_vs_names)

        # ── Helper ────────────────────────────────────────────────────────────
        def _make_vs_part(vs_name: str, overrides: Dict[str, Any]) -> Dict[str, Any]:
            return {
                "path": f"valueSets/{vs_name}.json",
                "payload": _encode_part(
                    {
                        "$schema": self._schemas.valueSet,
                        "name": vs_name,
                        "variableOverrides": [
                            {"name": k, "value": v} for k, v in overrides.items()
                        ],
                    }
                ),
                "payloadType": "InlineBase64",
            }

        platform_parts = [self._platform] if (update_meta and self._platform) else []

        all_vs_parts = [
            _make_vs_part(vs_name, overrides) for vs_name, overrides in final_valuesets.items()
        ]
        all_parts = (
            [vars_part]
            + all_vs_parts
            + [
                {
                    "path": "settings.json",
                    "payload": _encode_part(
                        {"$schema": self._schemas.settings, "valueSetsOrder": final_order}
                    ),
                    "payloadType": "InlineBase64",
                }
            ]
            + platform_parts
        )

        self._invalidate()
        return self._lib.updateDefinition(all_parts, update_meta)

    def __repr__(self) -> str:
        if self._cache is not None:
            return f"VariableAccessor({list(self._cache.keys())!r})"
        return "VariableAccessor(<not loaded>)"


class VariableLibraryAccessor(ItemAccessor):
    """Accessor for variable library list and creation operations."""

    ITEM_TYPE = "VariableLibrary"
    ITEM_CLASS: Optional[type] = None  # Set after VariableLibrary definition

    @overload
    def __call__(self, identifier: str) -> "VariableLibrary": ...

    @overload
    def __call__(self, identifier: None = None) -> List["VariableLibrary"]: ...

    def __call__(
        self, identifier: Optional[str] = None
    ) -> Union["VariableLibrary", List["VariableLibrary"]]:
        """Get variable library by name/ID, or list all variable libraries."""
        result = super().__call__(identifier)
        if isinstance(result, list):
            return cast(List["VariableLibrary"], result)
        return cast("VariableLibrary", result)

    def add(
        self,
        name: str,
        description: Optional[str] = None,
        folder_id: Optional[str] = None,
        definition: Optional[Dict[str, Any]] = None,
    ) -> "VariableLibrary":
        """
        Create a new variable library.

        Uses the VariableLibraries-specific endpoint, which supports an optional
        definition payload to seed the library with variables and value sets at
        creation time.

        Args:
            name: Display name for the variable library
            description: Optional description (max 256 characters)
            folder_id: Optional folder GUID to place the library in
            definition: Optional public definition. Format::

                {
                    "format": "VariableLibraryV1",
                    "parts": [
                        {
                            "path": "variables.json",
                            "payload": "<base64>",
                            "payloadType": "InlineBase64"
                        },
                        ...
                    ]
                }

        Returns:
            VariableLibrary: The created variable library

        Examples:
            Minimal:

            >>> lib = workspace.variableLibraries.add("Deployment Config")

            With description:

            >>> lib = workspace.variableLibraries.add(
            ...     "Deployment Config",
            ...     description="Stage-specific deployment variables"
            ... )
        """
        payload: Dict[str, Any] = {"displayName": name}
        if description is not None:
            payload["description"] = description
        if folder_id is not None:
            payload["folderId"] = folder_id
        if definition is not None:
            payload["definition"] = definition

        endpoint = f"/workspaces/{self._workspace_id}/VariableLibraries"

        try:
            response = self._client.post(endpoint, data=payload)

            if response.status_code == 202:
                operation_id = response.headers.get("x-ms-operation-id")
                raise FabiasError(
                    f"Variable library creation started asynchronously "
                    f"(operation: {operation_id}). Polling not yet implemented."
                )

            item_data = response.json()
            return VariableLibrary(
                client=self._client, workspace_id=self._workspace_id, item_data=item_data
            )

        except Exception as e:
            import time as _time

            from ..._shared.exceptions import ApiError

            msg = str(e).lower()
            is_conflict = (
                isinstance(e, ApiError) and e.status_code == 409
            ) or "already exists" in msg
            is_not_ready = (
                isinstance(e, ApiError) and e.status_code == 400
            ) and "not available yet" in msg

            if is_conflict or is_not_ready:
                # Item already exists or was just created but isn't queryable yet —
                # poll the list until it appears (Fabric eventual consistency).
                for _ in range(12):  # up to ~60s
                    _time.sleep(5)
                    matches = [item for item in self.__call__() if item.name == name]
                    if matches:
                        return cast("VariableLibrary", matches[0])
            raise

    def delete(self, identifier: str) -> None:  # pragma: no cover
        """
        Delete a variable library by name or GUID.

        Args:
            identifier: Variable library name or GUID

        Examples:
            >>> workspace.variableLibraries.delete("Old Config")
        """
        lib = VariableLibrary(self._client, self._workspace_id, identifier)

        if not lib.id:
            raise FabiasError(f"Cannot delete variable library: ID not resolved for '{identifier}'")

        self._client.delete(f"/workspaces/{self._workspace_id}/VariableLibraries/{lib.id}")


class VariableLibrary(Item):
    """
    Represents a Microsoft Fabric Variable Library.

    Variable libraries store typed named variables and alternative value sets for
    use in application lifecycle management (ALM) pipelines. Different value sets
    can be activated per deployment stage (dev/test/prod) allowing the same
    pipeline run with different configurations.

    Variable types: Boolean, DateTime, Number, Integer, String, ItemReference

    Attributes:
        id (str): Variable library GUID
        name (str): Display name
        description (str): Description
        active_value_set (str | None): Name of the currently active value set

    Examples:
        Get a variable library:

        >>> lib = workspace.variableLibrary("Deployment Config")
        >>> print(f"Active value set: {lib.active_value_set}")

        Activate a different value set:

        >>> lib.activateValueSet("Production")

        Retrieve the full definition (variables + value sets):

        >>> definition = lib.getDefinition()
        >>> for part in definition["parts"]:
        ...     print(part["path"])

        Set variables in bulk (SDK handles all encoding):

        >>> lib.setVariables([
        ...     {"name": "environment", "type": "String", "value": "dev"},
        ...     {"name": "maxRetries",  "type": "Integer", "value": 3},
        ... ])

        Manage value sets:

        >>> lib.setValueSet("Production", {"environment": "prod", "maxRetries": "10"})
        >>> lib.valueSets()   # [{"name": "Production", "variableOverrides": [...]}]
    """

    ITEM_TYPE = "VariableLibrary"
    ITEM_TYPE_DISPLAY = "VariableLibrary"

    def __init__(
        self,
        client: "FabricClient",
        workspace_id: str,
        identifier: Optional[str] = None,
        item_data: Optional[Dict[str, Any]] = None,
    ):
        # Initialize variable-library-specific attribute before super().__init__
        # because super().__init__ calls fromJson() which sets it.
        self.active_value_set: Optional[str] = None
        self.variables: VariableAccessor = VariableAccessor(self)
        super().__init__(client, workspace_id, identifier, item_data)

    def fromJson(self, data: Dict[str, Any]) -> None:
        """
        Populate from API response, capturing variable library properties.

        Extends base fromJson to also parse ``properties.activeValueSetName``.
        """
        super().fromJson(data)
        properties = data.get("properties") or {}
        self.active_value_set = properties.get("activeValueSetName")

    def _resolve_via_api(self, identifier: str) -> None:
        """
        Resolve identifier using the generic items endpoint, then fetch full
        details (including ``properties``) from the VariableLibraries endpoint.
        """
        endpoint = f"/workspaces/{self.workspace_id}/items?type=VariableLibrary"
        response = self._client.get(endpoint)
        data = response.json()
        items = data.get("value", [])

        matches = [
            item
            for item in items
            if item.get("displayName") == identifier or item.get("id") == identifier
        ]

        if not matches:
            raise NotFoundError(
                f"VariableLibrary '{identifier}' not found in workspace",
                resource_type="VariableLibrary",
                resource_id=identifier,
            )

        if len(matches) > 1:
            raise FabiasError(f"Multiple variable libraries found with identifier '{identifier}'")

        item = matches[0]
        self.id = item.get("id")
        self.name = item.get("displayName")
        self.description = item.get("description")
        self.folder_id = item.get("folderId") or item.get("folder", {}).get("id")

        # Fetch full details to populate properties (e.g. activeValueSetName)
        self.refresh()

    @property
    def _endpoint(self) -> str:
        """Base API endpoint for this variable library."""
        return f"/workspaces/{self.workspace_id}/VariableLibraries/{self.id}"

    def refresh(self) -> "VariableLibrary":
        """
        Refresh variable library metadata from the API.

        Fetches the latest state including ``active_value_set``.

        Returns:
            VariableLibrary: Self for method chaining

        Examples:
            >>> lib.refresh()
            >>> print(lib.active_value_set)
        """
        response = self._client.get(self._endpoint)
        self.fromJson(response.json())
        self.variables._invalidate()
        return self

    # =========================================================================
    # Helpers
    # =========================================================================

    def _fetchDefinition(self) -> Dict[str, Any]:
        """
        Fetch the raw definition object from the API, handling LRO transparently.

        Returns the ``definition`` dict (with ``format`` and ``parts`` keys).
        """
        response = self._client.post(f"{self._endpoint}/getDefinition")
        if response.longRunning:
            response.wait()
            data = response.result() or {}
        else:
            data = response.json()
        return data.get("definition", data)

    def getDefinition(self) -> Dict[str, Any]:
        """Fetch the raw definition (``format`` + ``parts``) from the API.

        Returns:
            dict: Definition object with ``format`` and ``parts`` keys.

        Examples:
            >>> definition = lib.getDefinition()
            >>> for part in definition["parts"]:
            ...     print(part["path"])
        """
        return self._fetchDefinition()

    def updateDefinition(
        self, parts: List[Dict[str, Any]], update_metadata: bool = False
    ) -> "VariableLibrary":
        """
        Override the variable library definition with raw parts.

        Replaces the entire definition with the provided parts. Parts must include
        at minimum ``variables.json``. Use ``valueSets/<name>.json`` for value sets
        and ``settings.json`` for ordering. Handles long-running operations (LRO)
        transparently — blocks until the operation completes.

        Args:
            parts: List of definition part dicts, each with:

                - ``path``: e.g. ``"variables.json"``,
                  ``"valueSets/Production.json"``, ``"settings.json"``
                - ``payload``: Base64-encoded JSON content
                - ``payloadType``: ``"InlineBase64"``

        Returns:
            VariableLibrary: Self for method chaining (state refreshed from API)

        Note:
            Prefer :meth:`setVariables` and :meth:`setValueSet` for common
            use cases — they handle all encoding automatically.
        """
        payload = {"definition": {"format": "VariableLibraryV1", "parts": parts}}

        response = self._client.post(
            f"{self._endpoint}/updateDefinition{'?updateMetadata=True' if update_metadata else ''}",
            data=payload,
        )
        if response.longRunning:
            response.wait()
        self.refresh()
        return self

    # =========================================================================
    # Variable management (high-level)
    # =========================================================================

    def variable(self, name: str) -> Variable:
        """
        Return a single variable by name.

        Shorthand for ``lib.variables[name]``.

        Args:
            name: Variable name

        Returns:
            Variable: The variable

        Raises:
            KeyError: If the variable does not exist

        Examples:
            >>> env = lib.variable("environment")
            >>> print(env.value, env.overrides)
        """
        return self.variables[name]

    def setVariables(self, variables: List[Dict[str, Any]]) -> "VariableLibrary":
        """
        Replace all variables in bulk.

        Fetches the current definition to preserve existing value sets and
        settings, replaces the ``variables.json`` part, and pushes the full
        updated definition back. Handles LRO transparently.

        Args:
            variables: List of variable dicts. Each must have ``name``,
                ``type``, and ``value``. ``note`` is optional. Example::

                    [
                        {"name": "environment", "type": "String", "value": "dev"},
                        {"name": "maxRetries",  "type": "Integer", "value": 3},
                        {"name": "enableCache", "type": "Boolean", "value": True},
                    ]

        Returns:
            VariableLibrary: Self for method chaining

        Examples:
            >>> lib.setVariables([
            ...     {"name": "environment", "type": "String", "value": "prod"},
            ...     {"name": "maxRetries",  "type": "Integer", "value": 5},
            ... ])
        """
        current = self._fetchDefinition()
        parts = current.get("parts", [])

        new_variables_part = {
            "path": "variables.json",
            "payload": _encode_part({"$schema": _VARIABLES_SCHEMA, "variables": variables}),
            "payloadType": "InlineBase64",
        }
        updated_parts = [p for p in parts if p.get("path") != "variables.json"]
        updated_parts.insert(0, new_variables_part)

        return self.updateDefinition(updated_parts)

    def valueSets(self) -> List[Dict[str, Any]]:
        """
        Return all value sets as a plain list of decoded dicts.

        Each value set has ``name``, optionally ``description``, and
        ``variableOverrides`` — a list of ``{name, value}`` pairs.

        Returns:
            list[dict]: Value sets decoded from ``valueSets/*.json`` parts

        Examples:
            >>> for vs in lib.valueSets():
            ...     print(vs["name"], vs.get("variableOverrides", []))
        """
        definition = self._fetchDefinition()
        result = []
        for part in definition.get("parts", []):
            path = part.get("path", "")
            if path.startswith("valueSets/") and path.endswith(".json"):
                result.append(_decode_part(part["payload"]))
        return result

    def setValueSet(
        self,
        name: str,
        overrides: Dict[str, Any],
        description: Optional[str] = None,
    ) -> "VariableLibrary":
        """
        Create or update a value set.

        Fetches the current definition, adds or replaces the named value set
        part, and pushes the full updated definition back. Handles LRO.

        Args:
            name: Value set name (also the filename — ``valueSets/{name}.json``)
            overrides: Variable overrides as ``{variable_name: override_value}``
            description: Optional description for the value set

        Returns:
            VariableLibrary: Self for method chaining

        Examples:
            >>> lib.setValueSet("Production", {
            ...     "environment": "prod",
            ...     "maxRetries": "10",
            ... })
        """
        current = self._fetchDefinition()
        parts = current.get("parts", [])

        valueset_path = f"valueSets/{name}.json"
        content: Dict[str, Any] = {
            "$schema": _VALUESET_SCHEMA,
            "name": name,
            "variableOverrides": [{"name": k, "value": v} for k, v in overrides.items()],
        }
        if description is not None:
            content["description"] = description

        new_part = {
            "path": valueset_path,
            "payload": _encode_part(content),
            "payloadType": "InlineBase64",
        }
        updated_parts = [p for p in parts if p.get("path") != valueset_path]
        updated_parts.append(new_part)

        return self.updateDefinition(updated_parts)

    def deleteValueSet(self, name: str) -> "VariableLibrary":
        """
        Remove a value set from the library.

        Fetches the current definition, drops the named value set part,
        and pushes the updated definition back. Handles LRO.

        Note:
            You cannot delete the currently active value set. Call
            :meth:`activateValueSet` on a different value set first.

        Args:
            name: Name of the value set to delete

        Returns:
            VariableLibrary: Self for method chaining

        Examples:
            >>> lib.deleteValueSet("OldStaging")
        """
        current = self._fetchDefinition()
        parts = current.get("parts", [])
        valueset_path = f"valueSets/{name}.json"
        updated_parts = [p for p in parts if p.get("path") != valueset_path]
        return self.updateDefinition(updated_parts)

    def activateValueSet(self, name: str) -> "VariableLibrary":
        """
        Set the active value set for this variable library.

        The active value set determines which variable overrides are in effect.
        Only one value set can be active at a time. You cannot delete an active
        value set—activate another first.

        Args:
            name: Name of the value set to activate

        Returns:
            VariableLibrary: Self for method chaining (``active_value_set`` updated)

        Raises:
            FabiasError: If the update request fails

        Examples:
            >>> lib.activateValueSet("Production")
            >>> print(lib.active_value_set)   # "Production"

            Switch back to default:

            >>> lib.activateValueSet("Default value set")
        """
        payload = {"properties": {"activeValueSetName": name}}
        response = self._client.patch(self._endpoint, data=payload)
        self.fromJson(response.json())
        return self

    def update(
        self,
        name: Optional[str] = None,
        description: Optional[str] = None,
        valueSet: Optional[str] = None,
    ) -> "VariableLibrary":
        """
        Update variable library metadata.

        Uses the VariableLibraries-specific PATCH endpoint, which also preserves
        the ``active_value_set`` in the returned response.

        Args:
            name: New display name
            description: New description (max 256 characters)
            valueSet: Name of the value set to activate (alternative to calling :meth:`activateValueSet` separately)
        Returns:
            VariableLibrary: Self for method chaining

        Examples:
            >>> lib.update(description="Updated for production deployment")
            >>> lib.update(name="Prod Config", description="Production variables")
        """
        payload: Dict[str, Any] = {}
        if name is not None:
            payload["displayName"] = name
        if description is not None:
            payload["description"] = description
        if valueSet is not None:
            payload["properties"] = {"activeValueSetName": valueSet}
        if not payload:
            return self

        response = self._client.patch(self._endpoint, data=payload)
        self.fromJson(response.json())
        return self

    def delete(self) -> None:  # pragma: no cover
        """
        Delete this variable library.

        Warning: This operation is permanent and cannot be undone.

        Examples:
            >>> old_lib = workspace.variableLibrary("Old Config")
            >>> old_lib.delete()
        """
        if not self.id:
            raise FabiasError("Cannot delete variable library: ID not resolved")

        self._client.delete(self._endpoint)

        self.id = None
        self.name = None
        self.description = None
        self.folder_id = None
        self.active_value_set = None

    def __repr__(self) -> str:
        return (
            f"VariableLibrary(id={self.id}, name={self.name!r}, "
            f"active_value_set={self.active_value_set!r})"
        )


# Set class reference after definition
VariableLibraryAccessor.ITEM_CLASS = VariableLibrary
