from typing import Dict, List, Optional
from warnings import warn


from ...application.services.evaluation_service import EvaluationService
from ...infrastructure.clients.http_client import Client
from ...domain.models.inference_result import CostInfoProperties, InferenceResultBase, UsageInfoProperties
from ...domain.models.evaluation_task import EvaluationTask, EvaluationTaskBase
from ...utils.string import build_query_params, is_valid_id

class EvaluationTaskService:
    def __init__(self, client: Client, evaluation_service: EvaluationService):
        self._client = client
        self.evaluation_service = evaluation_service

    def create(
        self,
        metrics: List[str],
        evaluation_id: str,
        actual_output: str,
        scores: Optional[List[float]] = None,
        retrieval_context: Optional[str] = None,
        test_case_id: Optional[str] = None,
        input: Optional[str] = None,
        expected_output: Optional[str] = None,
        context: Optional[str] = None,
        latency: Optional[float] = None,
        usage_info: Optional[Dict[str, float]] = {},
        cost_info: Optional[Dict[str, float]] = {},
    ):
        """
        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.
            evaluation_id (str): ID of the evaluation to associate with the tasks.
            actual_output (str): The actual output generated by the system under evaluation.
            retrieval_context (str, optional): Context retrieved by a RAG system, if applicable.
            test_case_id (str, optional): ID of the test case used for the evaluation, linking to predefined inputs, expected outputs, and context.
            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]`).
            input (str, optional): Input for the evaluation task (Deprecated: use `test_case_id` instead).
            expected_output (str, optional): Expected output for the evaluation task (Deprecated: use `test_case_id` instead).
            context (str, optional): Context for the evaluation task (Deprecated: use `test_case_id` instead).
            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:
            list[EvaluationTask]: List of created evaluation tasks.
        """
        if input is not None or context is not None or expected_output is not None:
            warn(
                "\nDirectly providing 'input', 'context', or 'expected_output' parameters will be deprecated.\n"
                "Instead, use the 'test_case_id' parameter by:\n"
                "   1. Creating a test case with these parameters\n"
                "   2. Passing the test case ID to this method\n\n"
                "Benefits: Better tracking of test cases and results across versions and evaluations.\n"
                "Documentation: https://docs.galtea.ai/sdk/api/test-case-service\n",
                DeprecationWarning,
                stacklevel=2 
            )

        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.__fields__:
                        raise KeyError(f"Invalid key: {key}. Must be one of: {', '.join(UsageInfoProperties.__fields__.keys())}")
                    
            if cost_info is not None:
                for key, _ in cost_info.items():
                    if key not in CostInfoProperties.__fields__:
                        raise KeyError(f"Invalid key: {key}. Must be one of: {', '.join(CostInfoProperties.__fields__.keys())}")
            
            # Create the base objects with the fields it supports
            evaluation_task = EvaluationTaskBase(
                evaluation_id=evaluation_id,
                test_case_id=test_case_id,
                input=input,
                expected_output=expected_output,
                context=context,
            )
            
            # Create the performance metrics object with the fields it supports
            inference_result = InferenceResultBase(
                evaluation_id=evaluation_id,
                actual_output=actual_output,
                retrieval_context=retrieval_context,
                latency=latency,
                **usage_info,
                **cost_info,
            )
            
            # Validate the base objects
            evaluation_task.model_validate(evaluation_task.model_dump())
            inference_result.model_validate(inference_result.model_dump())
            
            # Create a dictionary with all fields including metric_type_names
            request_body = evaluation_task.model_dump(by_alias=True)
            request_body["metricTypeNames"] = metrics
            request_body["scores"] = scores
            
            # Add performance metrics properties to request body
            inference_result_dict = inference_result.model_dump(by_alias=True)
            request_body.update(inference_result_dict)
            
            # Send the request with the complete body
            response = self._client.post(f"evaluationTasks", 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 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: str = [], offset: Optional[int] = None, limit: Optional[int] = None):
        """
        Get a list of evaluation tasks for a given evaluation.
        
        Args:
            evaluation_id (str): ID of the evaluation.
            offset (int, optional): Offset for pagination.
            limit (int, optional): Limit for pagination.
            
        Returns:
            list: List of evaluation tasks.
        """
        if not is_valid_id(evaluation_id):
            raise ValueError("Evaluation ID provided is not valid.")
        
        query_params = build_query_params(evaluationIds=[evaluation_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:
                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:
            EvaluationTask: Deleted evaluation task object.
        """
        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}")
