"""
REST API client for Eddytor server endpoints.

Provides wrappers for table, storage, and column domain operations.
"""

from __future__ import annotations

import os
import urllib.parse
from pathlib import Path
from typing import TYPE_CHECKING, Any, BinaryIO

import requests

from .exceptions import EddytorError, EddytorConnectionError, EddytorQueryError
from .models import (
    AIAction,
    AIProvider,
    AllowedValues,
    ColumnDomain,
    ExplainRowsResult,
    InferredSchema,
    ListObjectsResult,
    MagicDustResult,
    MoveObjectsResult,
    MoveTableResult,
    RegisteredTable,
    StorageConfig,
    StorageObject,
    TableHistory,
    TableMetadata,
)

if TYPE_CHECKING:
    pass


class RestClient:
    """
    REST API client for Eddytor server.

    Handles authentication and provides methods for table, storage,
    and column domain operations.
    """

    def __init__(
        self,
        api_key: str,
        host: str = "api.eddytor.com",
        port: int = 443,
        use_tls: bool = True,
        skip_verify: bool = False,
    ):
        """
        Initialize the REST client.

        Args:
            api_key: Eddytor API key
            host: Server host
            port: Server port
            use_tls: Use HTTPS
            skip_verify: Skip TLS certificate verification
        """
        self.api_key = api_key

        scheme = "https" if use_tls else "http"
        self.base_url = f"{scheme}://{host}:{port}/api"

        self._session = requests.Session()
        self._session.headers.update({
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        })
        if skip_verify:
            self._session.verify = False

    def close(self) -> None:
        """Close the REST client session."""
        self._session.close()

    @staticmethod
    def _normalize_params(params: dict[str, Any] | None) -> dict[str, Any] | None:
        """Convert Python bools to lowercase strings so the server receives true/false."""
        if params is None:
            return None
        return {k: str(v).lower() if isinstance(v, bool) else v for k, v in params.items()}

    def _request(
        self,
        method: str,
        path: str,
        params: dict[str, Any] | None = None,
        json: dict[str, Any] | None = None,
        data: Any = None,
        files: dict[str, Any] | None = None,
    ) -> Any:
        """Make an HTTP request."""
        url = f"{self.base_url}{path}"
        params = self._normalize_params(params)

        # Remove Content-Type for multipart
        headers = None
        if files:
            headers = {"Authorization": f"Bearer {self.api_key}"}

        try:
            response = self._session.request(
                method=method,
                url=url,
                params=params,
                json=json,
                data=data,
                files=files,
                headers=headers,
            )
        except requests.RequestException as e:
            raise EddytorConnectionError(f"Request failed: {e}") from e

        if response.status_code == 401:
            raise EddytorError("Authentication failed: invalid API key")
        if response.status_code == 403:
            raise EddytorError("Forbidden: insufficient permissions")
        if response.status_code == 404:
            raise EddytorError("Not found")
        if not response.ok:
            try:
                error_data = response.json()
                message = error_data.get("message", error_data.get("error", response.text))
            except Exception:
                message = response.text
            raise EddytorQueryError(f"Request failed ({response.status_code}): {message}")

        if response.status_code == 204:
            return {}

        try:
            return response.json()
        except Exception:
            return {}

    def _request_binary(
        self,
        path: str,
        params: dict[str, Any] | None = None,
    ) -> tuple[bytes, str]:
        """Make an HTTP request expecting binary response."""
        url = f"{self.base_url}{path}"
        params = self._normalize_params(params)

        try:
            response = self._session.get(url, params=params)
        except requests.RequestException as e:
            raise EddytorConnectionError(f"Request failed: {e}") from e

        if not response.ok:
            raise EddytorQueryError(f"Request failed ({response.status_code}): {response.text}")

        # Extract filename from Content-Disposition (safe parsing)
        content_disposition = response.headers.get("Content-Disposition", "")
        filename = "download"
        if "filename=" in content_disposition:
            raw = content_disposition.split("filename=")[1].split(";")[0].strip().strip('"').strip("'")
            # Prevent path traversal — only use the basename
            basename = os.path.basename(raw)
            if basename:
                filename = basename

        return response.content, filename

    # =========================================================================
    # Table API
    # =========================================================================

    def get_table_metadata(self, catalog: str, schema: str, table: str) -> TableMetadata:
        """
        Get metadata for a table.

        Args:
            catalog: Catalog name (storage config name)
            schema: Schema name
            table: Table name

        Returns:
            TableMetadata with columns, partitions, etc.
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}"
        data = self._request("GET", path)
        return TableMetadata.from_dict(data)

    def get_table_history(self, catalog: str, schema: str, table: str) -> TableHistory:
        """
        Get the version history of a table.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name

        Returns:
            TableHistory with list of versions and operations
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/history"
        data = self._request("GET", path)
        return TableHistory.from_dict(data)

    def rollback_table(self, catalog: str, schema: str, table: str, version: int) -> None:
        """
        Rollback a table to a specific version.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            version: Version number to rollback to
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/rollback/{version}"
        self._request("PUT", path)

    def add_constraints(
        self,
        catalog: str,
        schema: str,
        table: str,
        constraints: list[dict[str, Any]],
    ) -> None:
        """
        Add constraints to a table.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            constraints: List of constraint definitions
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/constraints"
        self._request("POST", path, json={"constraints": constraints})

    def drop_constraint(
        self,
        catalog: str,
        schema: str,
        table: str,
        constraint_name: str,
    ) -> None:
        """
        Drop a constraint from a table.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            constraint_name: Name of the constraint to drop
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/constraints/{_encode(constraint_name)}"
        self._request("DELETE", path)

    def update_field_metadata(
        self,
        catalog: str,
        schema: str,
        table: str,
        field_name: str,
        metadata: dict[str, Any],
    ) -> None:
        """
        Update metadata for a table field.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            field_name: Name of the field
            metadata: Metadata key-value pairs
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/fields/metadata"
        self._request("PUT", path, json={"field_name": field_name, "metadata": metadata})

    def check_enum_value_usage(
        self,
        catalog: str,
        schema: str,
        table: str,
        column: str,
        value: str,
    ) -> bool:
        """
        Check if an enum value is in use (for safe deletion).

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            column: Column name
            value: Enum value to check

        Returns:
            True if value is in use, False otherwise
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/enums/check"
        try:
            self._request("POST", path, json={"column": column, "value": value})
            return False
        except EddytorQueryError:
            return True

    def delete_table(self, catalog: str, schema: str, table: str) -> None:
        """Permanently delete a table and all its data from the object store.

        This action is irreversible.
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}"
        self._request("DELETE", path)

    def move_table(
        self, catalog: str, schema: str, table: str,
        destination_config_id: str, destination_path: str,
    ) -> MoveTableResult:
        """Move a Delta table to a different storage configuration.

        The table is deregistered from the source and re-discovered at the destination.
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/move"
        result = self._request("POST", path, json={
            "destinationConfigId": destination_config_id,
            "destinationPath": destination_path,
        })
        return MoveTableResult.from_dict(result)

    def infer_schema(
        self,
        file_path: str | Path,
        has_header: bool = True,
        delimiter: str = ",",
    ) -> InferredSchema:
        """
        Infer schema from a CSV file.

        Args:
            file_path: Path to the CSV file
            has_header: Whether the file has a header row
            delimiter: Column delimiter

        Returns:
            InferredSchema with detected columns
        """
        path = Path(file_path)
        with open(path, "rb") as f:
            files = {"file": (path.name, f, "text/csv")}
            data = {
                "hasHeader": str(has_header).lower(),
                "delimiter": delimiter,
            }
            result = self._request("POST", "/v1/tables/infer", files=files, data=data)
        return InferredSchema.from_dict(result)

    def magic_dust(
        self,
        catalog: str,
        schema: str,
        table: str,
        provider: AIProvider | str,
        action: AIAction | str,
        model: str,
        max_tokens: int = 4096,
        sample_size: int = 1000,
    ) -> MagicDustResult:
        """
        Execute AI-powered analysis on table data.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            provider: AI provider (Claude, OpenAI, Gemini, Mistral)
            action: Analysis action (Summary, DetectAnomalies, FindDuplicates, FixNulls, ExplainRows)
            model: Model name (e.g., "claude-sonnet-4-6", "gpt-4o")
            max_tokens: Maximum response tokens (default: 4096)
            sample_size: Maximum rows to analyze (default: 1000)

        Returns:
            MagicDustResult with analysis content and token usage
        """
        if isinstance(provider, AIProvider):
            provider = provider.value
        if isinstance(action, AIAction):
            action = action.value

        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/magic-dust"
        result = self._request(
            "POST",
            path,
            json={
                "provider": provider,
                "action": action,
                "model": model,
                "max_tokens": max_tokens,
                "sample_size": sample_size,
            },
        )
        return MagicDustResult.from_dict(result)

    def explain_rows(
        self,
        catalog: str,
        schema: str,
        table: str,
        provider: AIProvider | str,
        model: str,
        row_pks: list[Any] | None = None,
        pk_column: str | None = None,
        sample_size: int = 20,
        max_tokens: int = 4096,
    ) -> ExplainRowsResult:
        """
        Generate a narrative explanation of selected rows with tagged cell references.

        The LLM returns a flowing story with ``{ref:N}`` markers linking to specific
        cell values, which the frontend can render as interactive badges.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            provider: AI provider (Claude, OpenAI, Gemini, Mistral)
            model: Model name (e.g., "claude-sonnet-4-6", "gpt-4o")
            row_pks: Primary key values to select specific rows (optional)
            pk_column: Name of the primary key column (required when row_pks is set)
            sample_size: Number of rows to sample when row_pks is not set (default: 20)
            max_tokens: Maximum response tokens (default: 4096)

        Returns:
            ExplainRowsResult with explanation text, cell references, and confidence
        """
        if isinstance(provider, AIProvider):
            provider = provider.value

        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/explain-rows"
        payload: dict[str, Any] = {
            "provider": provider,
            "model": model,
            "max_tokens": max_tokens,
            "sample_size": sample_size,
        }
        if row_pks is not None:
            payload["row_pks"] = [str(pk) for pk in row_pks]
            if pk_column:
                payload["pk_column"] = pk_column

        result = self._request("POST", path, json=payload)
        return ExplainRowsResult.from_dict(result)

    # =========================================================================
    # Storage API
    # =========================================================================

    def has_sandbox(self) -> bool:
        """Check if user has a sandbox storage assigned."""
        try:
            self._request("HEAD", "/v1/storages/sandbox")
            return True
        except EddytorError:
            return False

    def register_s3_storage(
        self,
        bucket: str,
        access_key_id: str,
        secret_access_key: str,
        region: str = "us-east-1",
        endpoint: str | None = None,
    ) -> str:
        """
        Register an S3-compatible storage.

        Args:
            bucket: Bucket name
            access_key_id: AWS access key ID
            secret_access_key: AWS secret access key
            region: AWS region (default: us-east-1)
            endpoint: Custom endpoint for S3-compatible storage

        Returns:
            Storage configuration ID
        """
        payload: dict[str, Any] = {
            "bucket": bucket,
            "access_key_id": access_key_id,
            "secret_access_key": secret_access_key,
            "region": region,
        }
        if endpoint:
            payload["endpoint"] = endpoint

        result = self._request("POST", "/v1/storages/s3", json=payload)
        return result.get("id", result.get("config_id", ""))

    def register_azure_storage(
        self,
        account_name: str,
        container: str,
        access_key: str | None = None,
        sas_token: str | None = None,
    ) -> str:
        """
        Register an Azure Blob/Data Lake storage.

        Args:
            account_name: Azure storage account name
            container: Container name
            access_key: Storage account access key
            sas_token: SAS token (alternative to access_key)

        Returns:
            Storage configuration ID
        """
        payload: dict[str, Any] = {
            "account_name": account_name,
            "container": container,
        }
        if access_key:
            payload["access_key"] = access_key
        if sas_token:
            payload["sas_token"] = sas_token

        result = self._request("POST", "/v1/storages/az", json=payload)
        return result.get("id", result.get("config_id", ""))

    def get_storage_configs(self) -> list[StorageConfig]:
        """
        Get all storage configurations for the user.

        Returns:
            List of StorageConfig objects
        """
        result = self._request("GET", "/v1/storages/configs")
        configs = result.get("configs", [])
        return [StorageConfig.from_dict(c) for c in configs]

    def delete_storage_config(self, config_id: str) -> None:
        """
        Delete a storage configuration.

        Args:
            config_id: Storage configuration UUID
        """
        self._request("DELETE", f"/v1/storages/configs/{config_id}")

    def get_registered_tables(self, with_discovery: bool = False) -> list[RegisteredTable]:
        """
        Get all registered tables.

        The server returns a file explorer tree; this method walks the tree
        and returns a flat list of discovered tables.

        Args:
            with_discovery: Include tables from discovery (default: False)

        Returns:
            List of RegisteredTable objects
        """
        result = self._request("GET", "/v1/storages/tables", params={"with_discovery": with_discovery})
        if not result:
            return []
        return RegisteredTable.collect_from_explorer(result)

    def list_objects(
        self,
        config_id: str,
        path: str = "",
        extensions: str | None = None,
        limit: int = 100,
        continuation_token: str | None = None,
        delimiter: bool = True,
    ) -> ListObjectsResult:
        """
        List objects in a storage configuration.

        Args:
            config_id: Storage configuration UUID
            path: Path prefix to filter (default: "")
            extensions: Comma-separated file extensions (e.g., "csv,parquet")
            limit: Maximum results (default: 100, max: 1000)
            continuation_token: Pagination token
            delimiter: Enable directory-style listing (default: True)

        Returns:
            ListObjectsResult with objects and directories
        """
        params: dict[str, Any] = {
            "limit": min(limit, 1000),
            "delimiter": delimiter,
        }
        if path:
            params["path"] = path
        if extensions:
            params["extensions"] = extensions
        if continuation_token:
            params["continuation_token"] = continuation_token

        result = self._request("GET", f"/v1/storages/configs/{config_id}/objects", params=params)
        return ListObjectsResult.from_dict(result)

    def download_object(
        self,
        config_id: str,
        path: str,
        inline: bool = False,
    ) -> tuple[bytes, str]:
        """
        Download an object from storage.

        Args:
            config_id: Storage configuration UUID
            path: Path to the object
            inline: Preview instead of download (default: False)

        Returns:
            Tuple of (bytes, filename)
        """
        params = {"path": path, "inline": inline}
        return self._request_binary(f"/v1/storages/configs/{config_id}/objects/download", params=params)

    def delete_object(self, config_id: str, path: str) -> None:
        """
        Delete an object from storage.

        Args:
            config_id: Storage configuration UUID
            path: Path to the object
        """
        self._request("DELETE", f"/v1/storages/configs/{config_id}/objects", params={"path": path})

    def create_folder(self, config_id: str, path: str) -> str:
        """Create a folder (empty .folder marker) in a storage configuration.

        Returns the full path of the created folder.
        """
        result = self._request(
            "POST", f"/v1/storages/configs/{config_id}/folders",
            json={"path": path},
        )
        return result.get("path", path)

    def move_objects(
        self, source_config_id: str, source_path: str,
        destination_config_id: str, destination_path: str,
    ) -> MoveObjectsResult:
        """Move objects between storage configurations.

        Copies all objects under source_path to destination_path, then deletes
        from source. Delta tables at the source are deregistered and
        re-discovery is triggered at the destination.
        """
        result = self._request("POST", "/v1/storages/move", json={
            "sourceConfigId": source_config_id,
            "sourcePath": source_path,
            "destinationConfigId": destination_config_id,
            "destinationPath": destination_path,
        })
        return MoveObjectsResult.from_dict(result)

    def upload_files(
        self,
        files: list[str | Path | tuple[str, BinaryIO]],
        path: str = "",
    ) -> None:
        """
        Upload files to storage.

        Args:
            files: List of file paths or (filename, file_object) tuples
            path: Destination path prefix
        """
        file_parts = []
        opened_files = []

        try:
            for f in files:
                if isinstance(f, (str, Path)):
                    fp = open(f, "rb")
                    opened_files.append(fp)
                    file_parts.append(("files", (Path(f).name, fp)))
                else:
                    file_parts.append(("files", f))

            self._request(
                "POST",
                "/v1/storages/uploads",
                files=file_parts,
                data={"path": path},
            )
        finally:
            for fp in opened_files:
                fp.close()

    # =========================================================================
    # Column Domain API
    # =========================================================================

    def get_column_domain(
        self,
        catalog: str,
        schema: str,
        table: str,
        column: str,
    ) -> ColumnDomain:
        """
        Get the domain configuration for a column.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            column: Column name

        Returns:
            ColumnDomain configuration
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/columns/{_encode(column)}/domain"
        result = self._request("GET", path)
        return ColumnDomain.from_dict(result)

    def set_fixed_domain(
        self,
        catalog: str,
        schema: str,
        table: str,
        column: str,
        values: list[str],
    ) -> ColumnDomain:
        """
        Set a fixed domain (enum) on a column.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            column: Column name
            values: List of allowed values

        Returns:
            Updated ColumnDomain
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/columns/{_encode(column)}/domain/fixed"
        result = self._request("PUT", path, json={"values": values})
        return ColumnDomain.from_dict(result)

    def set_hierarchical_inline_domain(
        self,
        catalog: str,
        schema: str,
        table: str,
        column: str,
        hierarchy: dict[str, list[str]],
    ) -> ColumnDomain:
        """
        Set a hierarchical inline domain on a column.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            column: Column name
            hierarchy: Dict mapping parent values to lists of children

        Returns:
            Updated ColumnDomain
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/columns/{_encode(column)}/domain/hierarchical/inline"
        nodes = [{"parent": k, "children": v} for k, v in hierarchy.items()]
        result = self._request("PUT", path, json={"hierarchy": nodes})
        return ColumnDomain.from_dict(result)

    def set_hierarchical_derived_domain(
        self,
        catalog: str,
        schema: str,
        table: str,
        column: str,
        parent_column: str,
    ) -> ColumnDomain:
        """
        Set a hierarchical derived domain (linked to another column).

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            column: Column name
            parent_column: The column this domain derives from

        Returns:
            Updated ColumnDomain
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/columns/{_encode(column)}/domain/hierarchical/derived"
        result = self._request("PUT", path, json={"parent_column": parent_column})
        return ColumnDomain.from_dict(result)

    def set_hierarchical_table_domain(
        self,
        catalog: str,
        schema: str,
        table: str,
        column: str,
        source_table: str,
        source_column: str,
        parent_column: str | None = None,
    ) -> ColumnDomain:
        """
        Set a hierarchical domain from another table.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            column: Column name
            source_table: Fully qualified source table name
            source_column: Column in source table for values
            parent_column: Column in source table for parent relationship

        Returns:
            Updated ColumnDomain
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/columns/{_encode(column)}/domain/hierarchical/table"
        payload: dict[str, Any] = {
            "source_table": source_table,
            "source_column": source_column,
        }
        if parent_column:
            payload["parent_column"] = parent_column
        result = self._request("PUT", path, json=payload)
        return ColumnDomain.from_dict(result)

    def delete_column_domain(
        self,
        catalog: str,
        schema: str,
        table: str,
        column: str,
    ) -> None:
        """
        Remove the domain from a column.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            column: Column name
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/columns/{_encode(column)}/domain"
        self._request("DELETE", path)

    def add_fixed_value(
        self,
        catalog: str,
        schema: str,
        table: str,
        column: str,
        value: str,
    ) -> ColumnDomain:
        """
        Add a value to a fixed domain.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            column: Column name
            value: Value to add

        Returns:
            Updated ColumnDomain
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/columns/{_encode(column)}/domain/values"
        result = self._request("POST", path, json={"value": value})
        return ColumnDomain.from_dict(result)

    def set_fixed_values(
        self,
        catalog: str,
        schema: str,
        table: str,
        column: str,
        values: list[str],
    ) -> ColumnDomain:
        """
        Replace all values in a fixed domain.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            column: Column name
            values: New list of values

        Returns:
            Updated ColumnDomain
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/columns/{_encode(column)}/domain/values"
        result = self._request("PUT", path, json={"values": values})
        return ColumnDomain.from_dict(result)

    def remove_fixed_value(
        self,
        catalog: str,
        schema: str,
        table: str,
        column: str,
        value: str,
    ) -> ColumnDomain:
        """
        Remove a value from a fixed domain.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            column: Column name
            value: Value to remove

        Returns:
            Updated ColumnDomain
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/columns/{_encode(column)}/domain/values/{_encode(value)}"
        result = self._request("DELETE", path)
        return ColumnDomain.from_dict(result)

    def add_hierarchy_parent(
        self,
        catalog: str,
        schema: str,
        table: str,
        column: str,
        parent: str,
    ) -> ColumnDomain:
        """
        Add a parent to a hierarchical inline domain.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            column: Column name
            parent: Parent value to add

        Returns:
            Updated ColumnDomain
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/columns/{_encode(column)}/domain/parents"
        result = self._request("POST", path, json={"value": parent})
        return ColumnDomain.from_dict(result)

    def remove_hierarchy_parent(
        self,
        catalog: str,
        schema: str,
        table: str,
        column: str,
        parent: str,
    ) -> ColumnDomain:
        """
        Remove a parent from a hierarchical inline domain.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            column: Column name
            parent: Parent value to remove

        Returns:
            Updated ColumnDomain
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/columns/{_encode(column)}/domain/parents/{_encode(parent)}"
        result = self._request("DELETE", path)
        return ColumnDomain.from_dict(result)

    def add_hierarchy_child(
        self,
        catalog: str,
        schema: str,
        table: str,
        column: str,
        parent: str,
        child: str,
    ) -> ColumnDomain:
        """
        Add a child to a parent in a hierarchical inline domain.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            column: Column name
            parent: Parent value
            child: Child value to add

        Returns:
            Updated ColumnDomain
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/columns/{_encode(column)}/domain/parents/{_encode(parent)}/children"
        result = self._request("POST", path, json={"child": child})
        return ColumnDomain.from_dict(result)

    def set_hierarchy_children(
        self,
        catalog: str,
        schema: str,
        table: str,
        column: str,
        parent: str,
        children: list[str],
    ) -> ColumnDomain:
        """
        Replace all children for a parent in a hierarchical inline domain.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            column: Column name
            parent: Parent value
            children: New list of children

        Returns:
            Updated ColumnDomain
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/columns/{_encode(column)}/domain/parents/{_encode(parent)}/children"
        result = self._request("PUT", path, json={"children": children})
        return ColumnDomain.from_dict(result)

    def remove_hierarchy_child(
        self,
        catalog: str,
        schema: str,
        table: str,
        column: str,
        parent: str,
        child: str,
    ) -> ColumnDomain:
        """
        Remove a child from a parent in a hierarchical inline domain.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            column: Column name
            parent: Parent value
            child: Child value to remove

        Returns:
            Updated ColumnDomain
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/columns/{_encode(column)}/domain/parents/{_encode(parent)}/children/{_encode(child)}"
        result = self._request("DELETE", path)
        return ColumnDomain.from_dict(result)

    def get_allowed_values(
        self,
        catalog: str,
        schema: str,
        table: str,
        column: str,
        parent_value: str | None = None,
    ) -> AllowedValues:
        """
        Get allowed values for a column, optionally filtered by parent.

        Args:
            catalog: Catalog name
            schema: Schema name
            table: Table name
            column: Column name
            parent_value: Filter by parent value (for hierarchical domains)

        Returns:
            AllowedValues with list of values
        """
        path = f"/v1/tables/{_encode(catalog)}/{_encode(schema)}/{_encode(table)}/columns/{_encode(column)}/allowed-values"
        params = {}
        if parent_value:
            params["parent_value"] = parent_value
        result = self._request("GET", path, params=params if params else None)
        return AllowedValues.from_dict(result)


def _encode(value: str) -> str:
    """URL-encode a path component."""
    return urllib.parse.quote(value, safe="")
