from typing import Dict, List, Optional, Union

from ...application.services.evaluation_service import EvaluationService
from ...domain.models.evaluation_task import EvaluationTask
from ...domain.models.inference_result import (
    CostInfoProperties,
    UsageInfoProperties,
)
from ...infrastructure.clients.http_client import Client
from ...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):
        self._client = client
        self.evaluation_service = evaluation_service

    def create_single_turn(
        self,
        metrics: List[str],
        version_id: str,
        actual_output: str,
        test_case_id: Optional[str] = None,
        input: Optional[str] = None,
        is_production: Optional[bool] = None,
        scores: Optional[List[Union[float, None]]] = None,
        retrieval_context: Optional[str] = None,
        latency: Optional[float] = None,
        usage_info: Optional[Dict[str, float]] = None,
        cost_info: Optional[Dict[str, float]] = None,
    ):
        """
        Creates evaluation tasks for a given evaluation, assessing product performance based on
        specified metrics. For each metric type provided, a new evaluation task is created.

        Args:
            metrics (list[str]): List of metric type names to evaluate.
            version_id (str): ID of the version to associate with the tasks.
            actual_output (str): The actual output generated by the system under evaluation.\n
                You can also provide the output as an extra final turn with the `conversation_turns` parameter.
            test_case_id (str, conditional): ID of the test case used for the evaluation, linking to
                predefined inputs, expected outputs, and context. Mandatory when not tracking production data.
            input (str, conditional): The input text/prompt for the evaluation task. This is only applicable
                when `test_case_id` is not provided, for tracking production data.
            is_production (bool, optional): If True, the evaluation task is considered PRODUCTION data and
                no test_case_id parameter is needed. Default is False.
            retrieval_context (str, optional): Context retrieved by a RAG system, if applicable.
            scores (list[float | None], optional): Precomputed scores for the evaluation tasks,
                corresponding to the provided metrics. Must be a list of the same size as the
                `metrics` parameter, containing numbers between 0 and 1 or `None` values.
                - Providing scores bypasses platform-based evaluation, storing the provided
                  scores for later analysis. Useful for **custom metrics**.
                - Use `None` as a placeholder for metrics that should be evaluated by the
                  platform (e.g., `[0.5, None, 0.9]`).
            latency (float, optional): Latency in milliseconds since the model was called until
                the response was received.
            usage_info (dict[str, float], optional): 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 (dict[str, float], optional): 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.

        Returns:
            Optional[list[EvaluationTask]]: List of created evaluation tasks, or None if an
                error occurs.
        """
        try:
            if scores and len(scores) != len(metrics):
                raise ValueError("The length of scores must match the length of metrics.")

            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())}"
                        )

            # Create a dictionary with all fields including metric_type_names
            request_body = {
                "metricTypeNames": metrics,
                "versionId": version_id,
                "testCaseId": test_case_id,
                "actualOutput": actual_output,
                "input": input,
                "scores": scores,
                "retrievalContext": retrieval_context,
                "latency": latency,
                "usageInfo": usage_info,
                "costInfo": cost_info,
                "isProduction": is_production,
            }

            # Send the request with the complete body
            response = self._client.post("evaluationTasks/singleTurn", json=request_body)
            evaluation_tasks = [EvaluationTask(**evaluation_task) for evaluation_task in response.json()]
            return evaluation_tasks
        except Exception as e:
            print(f"Error creating evaluation task: {e}")
            return None

    def create(
        self,
        metrics: List[str],
        session_id: str,
        scores: Optional[List[Union[float, None]]] = None,
    ):
        """
        Creates evaluation tasks for a given session, assessing product performance based on specified metrics.
        For each metric type provided, a new evaluation task is created.

        Args:
            metrics (list[str]): List of metric type names to evaluate.
            session_id (str): ID of the session to associate with the evaluation tasks.
            scores (list[float | None], optional): Precomputed scores for the evaluation tasks,
                corresponding to the provided metrics. Must be a list of the same size as the
                `metrics` parameter, containing numbers between 0 and 1 or `None` values.
                - Providing scores bypasses platform-based evaluation, storing the provided
                  scores for later analysis. Useful for **custom metrics**.
                - Use `None` as a placeholder for metrics that should be evaluated by the
                  platform (e.g., `[0.5, None, 0.9]`).

        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.")

        request_body = {
            "metricTypeNames": metrics,
            "sessionId": session_id,
            "scores": scores,
        }

        # Send the request with the complete body
        response = self._client.post("evaluationTasks/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"evaluationTasks/{evaluation_task_id}")
        return EvaluationTask(**response.json())

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

        Args:
            evaluation_id (str, optional): ID of the evaluation.
            session_id (str, optional): ID of the session.
            offset (int, optional): Offset for pagination.
            limit (int, optional): Limit for pagination.

        Returns:
            List[EvaluationTask]: List of evaluation tasks.
        """
        if not evaluation_id and not session_id:
            raise ValueError("At least one of 'evaluation_id' or 'session_id' must be provided.")
        if evaluation_id is not None and not is_valid_id(evaluation_id):
            raise ValueError("Evaluation ID provided is not valid.")
        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(
            evaluationIds=[evaluation_id], sessionIds=[session_id], offset=offset, limit=limit
        )
        response = self._client.get(f"evaluationTasks?{query_params}")
        evaluation_tasks = [EvaluationTask(**evaluation_task) for evaluation_task in response.json()]

        if not evaluation_tasks:
            try:
                self.evaluation_service.get(evaluation_id)
            except Exception:
                raise ValueError(f"Evaluation with ID {evaluation_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: None.
        """
        if not is_valid_id(evaluation_task_id):
            raise ValueError("Evaluation task ID provided is not valid.")

        self._client.delete(f"evaluationTasks/{evaluation_task_id}")
