from typing import List, Optional, Union

from galtea.domain.models.metric import Metric
from galtea.domain.models.specification import Specification, SpecificationBase, SpecificationType
from galtea.infrastructure.clients.http_client import Client
from galtea.utils.sort_order import SortOrder, normalize_sort_order
from galtea.utils.string import build_query_params, is_valid_id
from galtea.utils.test_type import TestType, normalize_test_type
from galtea.utils.timestamp import normalize_timestamp


class SpecificationService:
    """
    Service for managing Specifications.
    A Specification represents a single, testable behavioral expectation for a product.
    """

    def __init__(self, client: Client):
        self.__client = client

    def create(
        self,
        product_id: str,
        description: str,
        type: Union[str, SpecificationType],
        test_type: Optional[Union[str, TestType]] = None,
        test_variant: Optional[str] = None,
        metric_ids: Optional[List[str]] = None,
    ) -> Optional[Specification]:
        """
        Create a new specification.

        Args:
            product_id (str): ID of the product this specification belongs to.
            description (str): Description of the testable behavioral expectation.
            type (str | SpecificationType): Type of specification.
                Valid values: CAPABILITY, INABILITY, POLICY.
            test_type (str | TestType, optional): The type of test for this specification.
                Required for POLICY specifications. Not applicable for CAPABILITY or INABILITY.
                Valid values: QUALITY, RED_TEAMING, SCENARIOS.
            test_variant (str, optional): Variant of the test type. Only applicable for POLICY specs.
            metric_ids (list[str], optional): List of metric IDs to link to this specification.

        Returns:
            Optional[Specification]: The created specification object, or None if an error occurs.
        """
        if not is_valid_id(product_id):
            raise ValueError("Product ID provided is not valid.")

        normalized_test_type: Optional[str] = normalize_test_type(test_type) if test_type is not None else None

        if isinstance(type, str):
            type = SpecificationType(type.upper())

        try:
            spec = SpecificationBase(
                product_id=product_id,
                description=description,
                type=type,
                test_type=normalized_test_type,
                test_variant=test_variant,
                metric_ids=metric_ids,
            )

            spec.model_validate(spec.model_dump())
            response = self.__client.post("specifications", json=spec.model_dump(by_alias=True))
            return Specification(**response.json())
        except Exception as e:
            print(f"Error creating Specification: {e}")
            return None

    def get(self, specification_id: str) -> Specification:
        """
        Retrieve a specification by its ID.

        Args:
            specification_id (str): ID of the specification to retrieve.

        Returns:
            Specification: The retrieved specification object.
        """
        if not is_valid_id(specification_id):
            raise ValueError("Specification ID provided is not valid.")

        response = self.__client.get(f"specifications/{specification_id}")
        return Specification(**response.json())

    def list(
        self,
        product_id: Optional[str] = None,
        type: Optional[Union[str, SpecificationType]] = None,
        test_type: Optional[Union[str, TestType]] = None,
        from_created_at: Optional[Union[str, int]] = None,
        to_created_at: Optional[Union[str, int]] = None,
        sort_by_created_at: Optional[Union[str, SortOrder]] = None,
        offset: Optional[int] = None,
        limit: Optional[int] = None,
    ) -> List[Specification]:
        """
        Get a list of specifications.

        Args:
            product_id (str, optional): Filter by product ID.
            type (str | SpecificationType, optional): Filter by specification type.
            test_type (str | TestType, optional): Filter by test type.
            from_created_at (str | int, optional): Filter specifications created at or after this timestamp.
            to_created_at (str | int, optional): Filter specifications created at or before this timestamp.
            sort_by_created_at (str | SortOrder, optional): Sort by created at. Valid values are 'asc' and 'desc'.
            offset (int, optional): Offset for pagination.
            limit (int, optional): Limit for pagination.

        Returns:
            List[Specification]: List of specifications.
        """
        sort_by_created_at = normalize_sort_order(sort_by_created_at) if sort_by_created_at is not None else None

        from_created_at_normalized = normalize_timestamp(from_created_at)
        to_created_at_normalized = normalize_timestamp(to_created_at)

        # Normalize type to API value
        type_value = None
        if type is not None:
            if isinstance(type, str):
                type_value = type.upper()
            else:
                type_value = type.value

        # Normalize test type to API value
        test_type_value = None
        if test_type is not None:
            test_type_value = normalize_test_type(test_type)

        query_params = build_query_params(
            productIds=product_id,
            types=type_value,
            testTypes=test_type_value,
            fromCreatedAt=from_created_at_normalized,
            toCreatedAt=to_created_at_normalized,
            offset=offset,
            limit=limit,
            sort=["createdAt", sort_by_created_at] if sort_by_created_at else None,
        )
        response = self.__client.get(f"specifications?{query_params}")
        return [Specification(**spec) for spec in response.json()]

    def delete(self, specification_id: str) -> None:
        """
        Delete a specification by its ID.

        Args:
            specification_id (str): ID of the specification to delete.
        """
        if not is_valid_id(specification_id):
            raise ValueError("Specification ID provided is not valid.")

        self.__client.delete(f"specifications/{specification_id}")

    def link_metrics(self, specification_id: str, metric_ids: List[str]) -> None:
        """
        Link metrics to a specification.

        Args:
            specification_id (str): ID of the specification.
            metric_ids (list[str]): List of metric IDs to link.
        """
        if not is_valid_id(specification_id):
            raise ValueError("Specification ID provided is not valid.")

        for metric_id in metric_ids:
            if not is_valid_id(metric_id):
                raise ValueError(f"Metric ID '{metric_id}' is not valid.")

        self.__client.post(f"specifications/{specification_id}/metrics", json={"metricIds": metric_ids})

    def unlink_metrics(self, specification_id: str, metric_ids: List[str]) -> None:
        """
        Unlink metrics from a specification.

        Args:
            specification_id (str): ID of the specification.
            metric_ids (list[str]): List of metric IDs to unlink.
        """
        if not is_valid_id(specification_id):
            raise ValueError("Specification ID provided is not valid.")

        for metric_id in metric_ids:
            if not is_valid_id(metric_id):
                raise ValueError(f"Metric ID '{metric_id}' is not valid.")

        self.__client.delete(f"specifications/{specification_id}/metrics", json={"metricIds": metric_ids})

    def get_metrics(self, specification_id: str) -> List[Metric]:
        """
        Get metrics linked to a specification.

        Args:
            specification_id (str): ID of the specification.

        Returns:
            List[Metric]: List of linked metrics.
        """
        if not is_valid_id(specification_id):
            raise ValueError("Specification ID provided is not valid.")

        response = self.__client.get(f"specifications/{specification_id}/metrics")
        return [Metric(**metric) for metric in response.json()]
