from typing import Dict, List, Optional, Union

from galtea.application.services.evaluation_service import EvaluationService
from galtea.application.services.metric_type_service import MetricTypeService
from galtea.application.services.session_service import SessionService
from galtea.application.services.test_case_service import TestCaseService
from galtea.domain.models.evaluation_task import EvaluationTask
from galtea.domain.models.inference_result import CostInfoProperties, UsageInfoProperties
from galtea.domain.models.test_case import TestCase
from galtea.infrastructure.clients.http_client import Client
from galtea.utils.custom_score_metric import CustomScoreEvaluationMetric
from galtea.utils.string import build_query_params, is_valid_id


class EvaluationTaskService:
    """
    Service for managing Evaluation Tasks.
    An Evaluation Task is the result of an evaluation against a specific metric and its criteria.
    Evaluations are created implicitly when evaluation tasks are created.
    """

    def __init__(
        self,
        client: Client,
        evaluation_service: EvaluationService,
        metric_type_service: MetricTypeService,
        session_service: SessionService,
        test_case_service: TestCaseService,
    ):
        self._client = client
        self.evaluation_service = evaluation_service
        self.metric_type_service = metric_type_service
        self.session_service = session_service
        self.test_case_service = test_case_service

    def create_single_turn(
        self,
        version_id: str,
        actual_output: str,
        metrics: List[Union[str, CustomScoreEvaluationMetric]],
        test_case_id: Optional[str] = None,
        input: Optional[str] = None,
        is_production: Optional[bool] = None,
        retrieval_context: Optional[str] = None,
        latency: Optional[float] = None,
        usage_info: Optional[Dict[str, float]] = None,
        cost_info: Optional[Dict[str, float]] = None,
        conversation_simulator_version: Optional[str] = None,
    ) -> Optional[List[EvaluationTask]]:
        """
        Creates evaluation tasks for a single-turn evaluation, assessing product performance based on
        specified metrics. For each metric (standard or custom) provided, a new evaluation task is created.

        Args:
            version_id (str): ID of the version to associate with the tasks.
            actual_output (str): The actual output generated by the system under evaluation.
            metrics (List[str | CustomScoreEvaluationMetric]):
                List of metrics as strings, and/or custom metric objects.
                - Standard metrics should be provided as strings.
                - Metrics with a custom score should inherit from CustomScoreEvaluationMetric.
            test_case_id (Optional[str]): ID of the test case used for the evaluation, linking to
                predefined inputs, expected outputs, and context. Mandatory when not tracking production data.
            input (Optional[str]): The input text/prompt for the evaluation task. Only applicable
                when `test_case_id` is not provided, for tracking production data.
            is_production (Optional[bool]): If True, the evaluation task is considered PRODUCTION data and
                no test_case_id parameter is needed. Default is False.
            retrieval_context (Optional[str]): Context retrieved by a RAG system, if applicable.
            latency (Optional[float]): Latency in milliseconds since the model was called until
                the response was received.
            usage_info (Optional[Dict[str, float]]): Information about token usage during the
                model call. Possible keys include:
                - 'input_tokens': Number of input tokens sent to the model.
                - 'output_tokens': Number of output tokens generated by the model.
                - 'cache_read_input_tokens': Number of input tokens read from the cache.
            cost_info (Optional[Dict[str, float]]): Information about the cost per token during
                the model call. Possible keys include:
                - 'cost_per_input_token': Cost per input token sent to the model.
                - 'cost_per_output_token': Cost per output token generated by the model.
                - 'cost_per_cache_read_input_token': Cost per input token read from the cache.
            conversation_simulator_version (Optional[str]): The version of Galtea's conversation simulator
                used to generate the user message (input). This should only be provided if the input
                was generated using the conversation_simulator_service.

        Returns:
            Optional[List[EvaluationTask]]: List of created evaluation tasks, or None if an error occurs.

        Example usage:
            >>> galtea.evaluation_tasks.create_single_turn(
            ...     test_case_id="test_case.id",
            ...     version_id="version.id",
            ...     actual_output="actual output...",
            ...     metrics=["anyStandardMetric", custom_relevance, custom_accuracy],
            ...     conversation_simulator_version="1.2.3",
            ... )
        """
        try:
            if metrics and not isinstance(metrics, list):
                raise TypeError("'metrics' parameter must be a list.")

            metric_names: List[str] = []
            metric_scores: List[Union[float, None]] = []

            test_case: Union[TestCase, None] = None

            for metric_obj in metrics:
                metric_name = metric_obj if isinstance(metric_obj, str) else metric_obj.name
                metric_names.append(metric_name)
                if isinstance(metric_obj, str):
                    metric_scores.append(None)
                elif isinstance(metric_obj, CustomScoreEvaluationMetric):
                    self.metric_type_service.get_by_name(metric_name)

                    if test_case is None and test_case_id is not None:
                        test_case = self.test_case_service.get(test_case_id)

                    try:
                        metric_scores.append(
                            metric_obj(
                                input=input,
                                actual_output=actual_output,
                                expected_output=test_case.expected_output if test_case else None,
                                retrieval_context=retrieval_context,
                                context=test_case.context if test_case else None,
                            )
                        )
                    except Exception as e:
                        raise ValueError(f"Failed to generate score for custom metric '{metric_obj.name}': {e}") from e
                else:
                    raise TypeError(f"Invalid metric type: {type(metric_name)}.")

            if usage_info is not None:
                for key, _ in usage_info.items():
                    if key not in UsageInfoProperties.model_fields:
                        raise KeyError(
                            f"Invalid key: {key}. Must be one of: {', '.join(UsageInfoProperties.model_fields.keys())}"
                        )

            if cost_info is not None:
                for key, _ in cost_info.items():
                    if key not in CostInfoProperties.model_fields:
                        raise KeyError(
                            f"Invalid key: {key}. Must be one of: {', '.join(CostInfoProperties.model_fields.keys())}"
                        )

            request_body = {
                "metricNames": metric_names,
                "versionId": version_id,
                "testCaseId": test_case_id,
                "actualOutput": actual_output,
                "input": input,
                "scores": metric_scores,
                "retrievalContext": retrieval_context,
                "latency": latency,
                "usageInfo": usage_info,
                "costInfo": cost_info,
                "isProduction": is_production,
                "conversationSimulatorVersion": conversation_simulator_version,
            }

            response = self._client.post("evaluations/singleTurn", json=request_body)
            evaluation_tasks = [EvaluationTask(**evaluation_task) for evaluation_task in response.json()]
            return evaluation_tasks
        except Exception:
            return None

    def create(
        self,
        metrics: List[str],
        session_id: str,
    ) -> List[EvaluationTask]:
        """
        Creates evaluation tasks for a given session, assessing product performance based on specified standard metrics.
        For each metric type provided, a new evaluation task is created.

        Args:
            metrics (List[str]):
                List of standard metric types as strings to evaluate.
                Metrics with a custom score are not supported for session-based evaluation.
            session_id (str): ID of the session to associate with the evaluation tasks.

        Returns:
            List[EvaluationTask]: List of created evaluation tasks.
        """
        if not metrics:
            raise ValueError("The 'metrics' parameter must be a non-empty list.")
        if not isinstance(metrics, list):
            raise TypeError("'metrics' parameter must be a list.")
        if not is_valid_id(session_id):
            raise ValueError("Session ID provided is not valid.")

        metric_names: List[str] = []
        for metric_obj in metrics:
            if isinstance(metric_obj, CustomScoreEvaluationMetric):
                raise ValueError(
                    "Custom-score metrics are not supported for session-based evaluation. "
                    "Use 'create_single_turn' for custom-scored metrics."
                )

            metric_name = metric_obj if isinstance(metric_obj, str) else metric_obj.name

            metric_names.append(metric_name)

        request_body = {
            "metricNames": metric_names,
            "sessionId": session_id,
        }

        # Send the request with the complete body
        response = self._client.post("evaluations/fromSession", json=request_body)
        evaluation_tasks = [EvaluationTask(**evaluation_task) for evaluation_task in response.json()]
        return evaluation_tasks

    def get(self, evaluation_task_id: str):
        """
        Retrieve an evaluation task by its ID.

        Args:
            evaluation_task_id (str): ID of the evaluation task to retrieve.

        Returns:
            EvaluationTask: The retrieved evaluation task object.
        """
        if not is_valid_id(evaluation_task_id):
            raise ValueError("Evaluation task ID provided is not valid.")

        response = self._client.get(f"evaluations/{evaluation_task_id}")
        return EvaluationTask(**response.json())

    def list(
        self,
        session_id: Optional[str] = None,
        offset: Optional[int] = None,
        limit: Optional[int] = None,
    ):
        """
        Get a list of evaluation tasks for a given evaluation or session.

        Args:
            evaluation_id (str, optional): ID of the evaluation.
            session_id (str, optional): ID of the session.
            offset (int, optional): Offset for pagination. This refers to the number of items to skip before
                starting to collect the result set. The default value is 0.
            limit (int, optional): Limit for pagination. This refers to the maximum number of items to collect
                in the result set.

        Returns:
            List[EvaluationTask]: List of evaluation tasks.
        """
        # 1. Validate IDs filter parameters
        if session_id is not None and not is_valid_id(session_id):
            raise ValueError("Session ID provided is not valid.")

        query_params = build_query_params(sessionIds=[session_id], offset=offset, limit=limit)
        response = self._client.get(f"evaluations?{query_params}")
        evaluation_tasks = [EvaluationTask(**evaluation_task) for evaluation_task in response.json()]

        if not evaluation_tasks:
            try:
                if session_id:
                    self.session_service.get(session_id)
            except Exception:
                raise ValueError(f"Session with ID {session_id} does not exist.")

        return evaluation_tasks

    def delete(self, evaluation_task_id: str):
        """
        Delete an evaluation task by its ID.

        Args:
            evaluation_task_id (str): ID of the evaluation task to delete.

        Returns:
            None.
        """
        if not is_valid_id(evaluation_task_id):
            raise ValueError("Evaluation task ID provided is not valid.")
        self._client.delete(f"evaluations/{evaluation_task_id}")
