from typing import List, Union
from uuid import UUID

import requests

from odp.client.exc import OdpValidationError
from odp.client.http_client import OdpHttpClient
from odp.dto.catalogv2.catalog import (
    CreateDataCollectionRequest,
    CreateDatasetRequest,
    DataCollection,
    Dataset,
    UpdateDataCollectionRequest,
    UpdateDatasetRequest,
)
from odp.dto.catalogv2.search import (
    AutoCompletionGroupedResult,
    AutoCompletionRequest,
    AutoCompletionResult,
    AutoCompletionResultItem,
    AutoCompletionResultType,
    AutoCompletionScore,
    CollectionSearchResultPayload,
    DatasetSearchResultPayload,
    PayloadType,
    ResultItem,
    SearchRequest,
    SearchResult,
    SearchScore,
)
from odp.util.cheapdantic.cheapdantic import BaseModel


class CatalogClient(BaseModel):

    """Client for interacting with Datasets."""

    http_client: OdpHttpClient
    catalog_base_url: str = "/api/catalog/v2"

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def create_dataset(
        self,
        request: CreateDatasetRequest,
    ) -> Dataset:
        """Create a dataset.

        Args:
            request: The dataset creation request object

        Returns:
            The created dataset

        Raises:
            OdpValidationError: If the request is invalid
            requests.HTTPError: For other unexpected HTTP errors
        """
        res = self.http_client.post(self.catalog_base_url + "/datasets", content=request)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            if res.status_code == 500:
                raise requests.HTTPError(f"Internal server error ({res.status_code}): {res.text}") from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}")

        return Dataset(**res.json())

    def update_dataset(
        self,
        dataset_id: str,
        request: UpdateDatasetRequest,
    ) -> Dataset:
        """Update a dataset.

        Args:
            dataset_id: The ID of the dataset to update.
            request: The dataset update request object.

        Returns:
            The updated dataset.

        Raises:
            OdpValidationError: If the request is invalid.
            requests.HTTPError: For other unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/datasets/{dataset_id}"
        res = self.http_client.put(url, content=request)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            if res.status_code == 500:
                raise requests.HTTPError(f"Internal server error ({res.status_code}): {res.text}") from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}")

        return Dataset(**res.json())

    def get_datasets(self) -> List[Dataset]:
        """Fetch all datasets accessible to the user.

        Returns:
            A list of Dataset objects.

        Raises:
            requests.HTTPError: For unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/datasets"
        res = self.http_client.get(url)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 500:
                raise requests.HTTPError(f"Internal server error ({res.status_code}): {res.text}") from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}")

        return [Dataset(**item) for item in res.json()]

    def get_dataset_by_id(self, dataset_id: Union[str, UUID]) -> Dataset:
        """Fetch a dataset by ID.

        Args:
            dataset_id: The ID of the dataset to fetch.

        Returns:
            A Dataset object.

        Raises:
            OdpValidationError: If the ID is malformed or request is invalid.
            requests.HTTPError: For other unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/datasets/{dataset_id}"
        res = self.http_client.get(url)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}") from e

        return Dataset(**res.json())

    def delete_dataset(self, dataset_id: Union[str, UUID]) -> None:
        """Delete a dataset by ID.

        Args:
            dataset_id: The ID of the dataset to delete.

        Raises:
            OdpValidationError: If the ID is malformed or request is invalid.
            requests.HTTPError: For other unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/datasets/{dataset_id}"
        res = self.http_client.delete(url)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}") from e

    def archive_dataset(self, dataset_id: Union[str, UUID]) -> Dataset:
        """Archive a dataset by ID.

        Args:
            dataset_id: The ID of the dataset to archive.

        Returns:
            The updated Dataset object.

        Raises:
            OdpValidationError: If the request input is invalid.
            requests.HTTPError: For other unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/datasets/{dataset_id}/archive"
        res = self.http_client.patch(url)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}") from e

        return Dataset(**res.json())

    def make_dataset_public(self, dataset_id: Union[str, UUID]) -> Dataset:
        """Make a dataset public by ID.

        Args:
            dataset_id: The ID of the dataset to make public.

        Returns:
            The updated Dataset object.

        Raises:
            OdpValidationError: If the request input is invalid.
            requests.HTTPError: For other unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/datasets/{dataset_id}/visibility/public"
        res = self.http_client.patch(url)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}") from e

        return Dataset(**res.json())

    def make_dataset_private(self, dataset_id: Union[str, UUID]) -> Dataset:
        """Make a dataset private by ID.

        Args:
            dataset_id: The ID of the dataset to make private.

        Returns:
            The updated Dataset object.

        Raises:
            OdpValidationError: If the request input is invalid.
            requests.HTTPError: For other unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/datasets/{dataset_id}/visibility/private"
        res = self.http_client.patch(url)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}") from e

        return Dataset(**res.json())

    def create_data_collection(self, request: CreateDataCollectionRequest) -> DataCollection:
        """Create a data collection.

        Args:
            request: The data collection creation request object.

        Returns:
            The created DataCollection object.

        Raises:
            OdpValidationError: If the request input is invalid.
            requests.HTTPError: For other unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/data-collections"
        res = self.http_client.post(url, content=request)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}") from e

        return DataCollection(**res.json())

    def get_data_collections(self) -> List[DataCollection]:
        """Fetch all data collections accessible to the user.

        Returns:
            A list of DataCollection objects.

        Raises:
            requests.HTTPError: For unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/data-collections"
        res = self.http_client.get(url)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}") from e

        return [DataCollection(**item) for item in res.json()]

    def get_data_collection_by_id(self, data_collection_id: Union[str, UUID]) -> DataCollection:
        """Fetch a data collection by ID.

        Args:
            data_collection_id: The ID of the data collection to fetch.

        Returns:
            A DataCollection object.

        Raises:
            OdpValidationError: If the input is invalid.
            requests.HTTPError: For other unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/data-collections/{data_collection_id}"
        res = self.http_client.get(url)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}") from e

        return DataCollection(**res.json())

    def update_data_collection(
        self,
        data_collection_id: Union[str, UUID],
        request: UpdateDataCollectionRequest,
    ) -> DataCollection:
        """Update a data collection.

        Args:
            data_collection_id: The ID of the data collection to update.
            request: The update request object.

        Returns:
            The updated DataCollection object.

        Raises:
            OdpValidationError: If the request input is invalid.
            requests.HTTPError: For other unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/data-collections/{data_collection_id}"
        res = self.http_client.put(url, content=request)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}") from e

        return DataCollection(**res.json())

    def delete_data_collection(self, data_collection_id: Union[str, UUID]) -> None:
        """Delete a data collection by ID.

        Args:
            data_collection_id: The ID of the data collection to delete.

        Raises:
            OdpValidationError: If the input is invalid.
            requests.HTTPError: For other unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/data-collections/{data_collection_id}"
        res = self.http_client.delete(url)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}") from e

    def make_data_collection_public(self, data_collection_id: Union[str, UUID]) -> DataCollection:
        """Make a data collection public by ID.

        Args:
            data_collection_id: The ID of the data collection to make public.

        Returns:
            The updated DataCollection object.

        Raises:
            OdpValidationError: If the request input is invalid.
            requests.HTTPError: For other unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/data-collections/{data_collection_id}/visibility/public"
        res = self.http_client.patch(url)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}") from e

        return DataCollection(**res.json())

    def make_data_collection_private(self, data_collection_id: Union[str, UUID]) -> DataCollection:
        """Make a data collection private by ID.

        Args:
            data_collection_id: The ID of the data collection to make private.

        Returns:
            The updated DataCollection object.

        Raises:
            OdpValidationError: If the request input is invalid.
            requests.HTTPError: For other unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/data-collections/{data_collection_id}/visibility/private"
        res = self.http_client.patch(url)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}") from e

        return DataCollection(**res.json())

    def add_dataset_to_data_collection(
        self,
        data_collection_id: Union[str, UUID],
        dataset_id: Union[str, UUID],
    ) -> DataCollection:
        """Add a dataset to a data collection.

        Args:
            data_collection_id: The ID of the data collection.
            dataset_id: The ID of the dataset to add.

        Returns:
            The updated DataCollection object.

        Raises:
            OdpValidationError: If the request input is invalid.
            requests.HTTPError: For other unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/data-collections/{data_collection_id}/datasets/{dataset_id}"
        res = self.http_client.post(url)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}") from e

        return DataCollection(**res.json())

    def remove_dataset_from_data_collection(
        self,
        data_collection_id: Union[str, UUID],
        dataset_id: Union[str, UUID],
    ) -> DataCollection:
        """Remove a dataset from a data collection.

        Args:
            data_collection_id: The ID of the data collection.
            dataset_id: The ID of the dataset to remove.

        Returns:
            The updated DataCollection object.

        Raises:
            OdpValidationError: If the request input is invalid.
            requests.HTTPError: For other unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/data-collections/{data_collection_id}/datasets/{dataset_id}"
        res = self.http_client.delete(url)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}") from e

        return DataCollection(**res.json())

    def search(self, request: SearchRequest) -> SearchResult:
        """Perform a search with typed payloads."""
        url = f"{self.catalog_base_url}/search"
        res = self.http_client.post(url, content=request)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}") from e

        raw = res.json()
        search_result = SearchResult(
            total=raw["total"],
            top_score=SearchScore(**raw["top_score"]) if raw.get("top_score") else None,
            match=raw["match"],
            results=[],
        )

        for item in raw["results"]:
            payload_type = item.get("payload_type")

            if payload_type == PayloadType.DATASET:
                payload = DatasetSearchResultPayload(**item["payload"])
            elif payload_type == PayloadType.DATA_COLLECTION:
                payload = CollectionSearchResultPayload(**item["payload"])
            else:
                payload = item["payload"]

            result_item = ResultItem(
                id=item["id"],
                title=item["title"],
                description=item["description"],
                highlights=item.get("highlights", []),
                match_type=item["match_type"],
                payload_type=payload_type,
                score=SearchScore(**item["score"]),
                payload=payload,
            )
            search_result.results.append(result_item)

        return search_result

    def autocomplete(self, request: AutoCompletionRequest) -> AutoCompletionResult:
        """Perform an autocomplete search.

        Args:
            request: The autocomplete request payload.

        Returns:
            An AutoCompletionResult object.

        Raises:
            OdpValidationError: If the request input is invalid.
            requests.HTTPError: For other unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/autocomplete"
        res = self.http_client.post(url, content=request)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}") from e

        raw = res.json()
        auto_completion_result = AutoCompletionResult(
            total=raw["total"],
            top_score=AutoCompletionScore(**raw["top_score"]) if raw.get("top_score") else None,
            results=[],
        )

        for item in raw["results"]:
            result_item = AutoCompletionResultItem(
                id=item["id"],
                type=AutoCompletionResultType(item["type"]),
                title=item["title"],
                description=item["description"],
                score=AutoCompletionScore(**item["score"]),
            )
            auto_completion_result.results.append(result_item)

        return auto_completion_result

    def autocomplete_grouped(self, request: AutoCompletionRequest) -> List[AutoCompletionGroupedResult]:
        """Perform a grouped autocomplete search.

        Args:
            request: The autocomplete request payload.

        Returns:
            A list of AutoCompletionGroupedResult objects.

        Raises:
            OdpValidationError: If the request input is invalid.
            requests.HTTPError: For other unexpected HTTP errors.
        """
        url = f"{self.catalog_base_url}/autocomplete/grouped"
        res = self.http_client.post(url, content=request)
        try:
            res.raise_for_status()
        except requests.HTTPError as e:
            if res.status_code == 400:
                raise OdpValidationError("Invalid input", res.text) from e
            raise requests.HTTPError(f"HTTP Error - {res.status_code}: {res.text}") from e

        raw = res.json()
        grouped_results = []

        for group_item in raw:
            result_items = [
                AutoCompletionResultItem(
                    id=item["id"],
                    type=AutoCompletionResultType(item["type"]),
                    title=item["title"],
                    description=item["description"],
                    score=AutoCompletionScore(**item["score"]),
                )
                for item in (group_item["result"].get("results") or [])
            ]

            result = AutoCompletionResult(
                total=group_item["result"]["total"],
                top_score=AutoCompletionScore(**group_item["result"]["top_score"])
                if group_item["result"].get("top_score")
                else None,
                results=result_items,
            )

            grouped_result = AutoCompletionGroupedResult(
                group=AutoCompletionResultType(group_item["group"]), result=result
            )

            grouped_results.append(grouped_result)

        return grouped_results
