import json
import logging
from typing import List

import boto3
from botocore.config import Config

from blazetest.core.config import MAX_LAMBDA_WORKERS
from blazetest.core.utils.exceptions import LambdaNotCreated

logger = logging.getLogger(__name__)


class Invocation:
    """
    Invocation class, which is used to invoke AWS Lambda

    Attributes:
        region: AWS region.
        resource_prefix: Used CloudFormation stack name

    Methods:
        invoke(): invokes Lambda function with payload
        get_created_lambda_function_details(): returns the exact function name and
            S3 Bucket that it has access to
    """

    LAMBDA_INVOCATION_TYPE = "RequestResponse"
    LAMBDA_LOG_TYPE = "Tail"

    def __init__(
        self,
        region: str = None,
        resource_prefix: str = None,
        aws_access_key_id: str = None,
        aws_secret_access_key: str = None,
    ):
        self.region = region
        self.resource_prefix = resource_prefix
        self.client = boto3.client(
            "lambda",
            region_name=region,
            aws_access_key_id=aws_access_key_id,
            aws_secret_access_key=aws_secret_access_key,
            config=Config(max_pool_connections=MAX_LAMBDA_WORKERS),
        )

    def invoke(
        self,
        node_id: str,
        session_uuid: str,
        function_name: str,
        pytest_args: List[str],
        report_path: str,
        timestamp: str,
        retry: bool = False,
    ) -> dict:
        """Invoke a Lambda function and retrieve the test result.

        Args:
            session_uuid: UUID of the test session.
            node_id: ID of the node to be passed to the Lambda function.
            function_name: Name of the Lambda function to be invoked.
            pytest_args: List of pytest arguments to be passed to the function.
            report_path: Path to the JUnit XML report to be generated by the function.
            timestamp: Timestamp to be passed to the function.
            retry: Boolean value to indicate if the test is a rerun.

        Returns:
            dict: A dictionary containing the test result and the path to the report.
        """
        payload = {
            "pytest_args": pytest_args,
            "node_id": node_id,
            "session_uuid": session_uuid,
            "report_path": report_path,
            "region": self.region,
            "start_timestamp": timestamp,
            "retry": retry,
        }
        return self.process_response(
            response=self.client.invoke(
                FunctionName=function_name,
                InvocationType=self.LAMBDA_INVOCATION_TYPE,
                LogType=self.LAMBDA_LOG_TYPE,
                Payload=json.dumps(payload),
            )
        )

    @staticmethod
    def process_response(response) -> dict:
        """
        Converts response JSON to Python-dict and returns a bool if test passed.
        Raises an error if there is any error message.

        :param response:
        :return:
        """
        result_payload = json.loads(response["Payload"].read())
        try:
            result_body = json.loads(result_payload["body"])
            logger.debug(result_body)
        except KeyError:
            error_message = result_payload["errorMessage"]
            logger.error(
                f"There was an error during test execution: {error_message}",
            )
            return {"test_result": False, "report_path": ""}
        return result_body

    # TODO: is it possible to use other method for retrieving details of created lambda function?
    def get_created_lambda_function_details(self) -> str:
        """
        Retrieves the details of a specific Lambda function that was created.

        The function queries the list of functions from the AWS Lambda service using the `client` object.
        It looks for a function whose name starts with a resource_prefix.
        The function then saves the name of the function in a dictionary.

        Raises:
        If it does not find a function with the specific name, it raises LambdaNotCreated.

        It returns the dictionary with the function details if it is able to find the function
            with the specific namee.

        Returns:
            str : A dictionary containing the function name value.
        """
        functions = []
        response = self.client.list_functions()
        while response:
            functions.extend(response["Functions"])
            response = (
                self.client.list_functions(Marker=response["NextMarker"])
                if "NextMarker" in response
                else None
            )

        function = None
        for f in functions:
            if f["FunctionName"].startswith(f"{self.resource_prefix}"):
                function = f
                break

        if function is None:
            raise LambdaNotCreated("Lambda function seems to be not created")

        function_name = function["FunctionName"]
        return function_name
