import json
import threading
import copy
import os
import time
import random
import sys
from contextlib import contextmanager

import requests
from urllib.parse import urlparse

# Import BlackAnt HTTP client for proper request handling
try:
    from blackant.http.client import HTTPClient
    from blackant.utils.logging import get_logger
except ImportError:
    # Fallback if BlackAnt SDK not available in this context
    HTTPClient = None
    get_logger = None

from calculation.base import CalculationBase

from task.parsers.request import TaskConfiguration
from task.parsers.callback import CallbackParameter
from task.states.idle import TaskIdleState
from task.states.base import TaskStateBase
from task.states.error import TaskErrorState
from task.errors import TaskCallbackFailedError


# Logger writer to redirect stdout/stderr to logger
class LoggerWriter:
    """A class to wrap the logger and redirect writes to it."""

    def __init__(self, logger, level="info"):
        self.logger = logger
        self.level = level

    def write(self, message):
        """Write a message to the logger."""
        if message.strip():  # Avoid logging empty lines
            if self.level == "error":
                self.logger.error(message.strip())
            else:
                self.logger.info(message.strip())

    def flush(self):
        """Flush the stream (not required, but needed for compatibility)."""


# Context manager to redirect stdout and stderr to a logger
@contextmanager
def redirect_stdout_stderr_to_logger(logger):
    """Context manager to redirect stdout and stderr to a logger."""
    original_stdout = sys.stdout
    original_stderr = sys.stderr

    sys.stdout = LoggerWriter(logger)
    sys.stderr = LoggerWriter(logger, level="error")

    try:
        yield
    finally:
        sys.stdout = original_stdout
        sys.stderr = original_stderr


class Task:  # pylint: disable=too-many-instance-attributes
    """Task entity in science module

    Whenever the server receives a request,
    a task is created to handle the calculation.
    """

    __scheduler_callback: CallbackParameter = CallbackParameter(
        url=os.environ["SCHEDULER_URL"],
        method="POST",
        headers={"Content-type": "application/json"},
        request_body={},
    )

    def __init__(
        self,
        calculation: CalculationBase,
        configuration: TaskConfiguration,
        log_file_handler,
        logger,
    ):
        """Task constructor

        Args:
            calculation (CalculationBase): Calculation to perform
            configuration (TaskConfiguration): Configuration composed by the incoming GET request
            logger (Logger): Logger to use
        """
        self.calculation = calculation
        self.logger = logger
        self.task_id = None
        self.log_file_handler = log_file_handler
        self.__configuration = configuration
        self.__start_execution_time = None

        if not self.calculation:
            self.logger.error("The calculation object in not defined.")
            self.__state = TaskErrorState(self)
        else:
            self.__state = TaskIdleState(self)

        self.__finished = False
        self.__executor = threading.Thread(target=self.__execute_state_machine)
        self.__thread_lock = threading.Lock()

    def change_state(self, state: TaskStateBase):
        """Handles state changes (State design pattern)

        Args:
            state (TaskStateBase): New state
        """
        self.__state = state

    def set_finished(self, finished: bool):
        """Setter for finished state

        Args:
            finished (bool): Sets whether the task is in finished state
        """
        self.__finished = finished

    @property
    def state(self) -> str:
        """Returns with the current state's name

        Returns:
            str: The current state's name
        """
        return self.__state.name

    @property
    def configuration(self) -> TaskConfiguration:
        """Returns with the copy of the task's configuration

        Returns:
            TaskConfiguration: The configuration
        """
        return copy.deepcopy(self.__configuration)

    @property
    def response(self) -> dict:
        """Gives the response for the server about the task state

        Returns:
            dict: The task's state
        """

        try:
            with open("/host_hostname", "r") as file:
                host_hostname = file.read().strip()
        except (OSError, IOError):
            self.logger.exception(
                "Cannot get Host name (where the task ran). 'UNKNOWN_HOST_FROM_TASK' is set."
            )
            host_hostname = "UNKNOWN_HOST_FROM_TASK"

        self.logger.info(f"The host's hostname is: {host_hostname}")

        return {
            "id": self.task_id,
            "state": self.state,
            "config": self.configuration.json,
            "node": host_hostname,
        }

    def send_callbacks(self, calculation_result):
        """Sends a callback to both the scheduler and the callback url specified by the customer

        Args:
            calculation_result (any): The result of the calculation.
                                      It can be the real result or an error too
        """

        self.__scheduler_callback.request_body = self.response
        self.__scheduler_callback.request_body["result"] = calculation_result
        self.__scheduler_callback.request_body["calculation_log"] = calculation_result["log"]
        if self.__start_execution_time is not None:
            self.__scheduler_callback.request_body["calculation_execution_time"] = (
                time.time() - self.__start_execution_time
            )
        else:
            self.__scheduler_callback.request_body["calculation_execution_time"] = "NOT_DEFINED"

        if "error" in calculation_result:
            self.__scheduler_callback.request_body["calculation_result"] = calculation_result[
                "error"
            ]
        elif "result" in calculation_result:
            self.__scheduler_callback.request_body["calculation_result"] = calculation_result[
                "result"
            ]

        with self.__thread_lock:
            self.__send_callback(self.__scheduler_callback)

            if self.__configuration.callback.url:
                self.__send_callback(self.__configuration.callback)
            self.logger.info(
                "Callback(s) has been sent-out successfully: {}".format(
                    self.__scheduler_callback.request_body["id"]
                )
            )

    def __send_callback(self, callback_param):
        """Sends a callback to the scheduler

        Args:
            callback_param (CallbackParameter): Response to be sent

        Raises:
            TaskCallbackFailedError: Raised when the callback cannot be successfully
                                     sent after five retries
        """

        last_exception = None
        response = None

        for _ in range(5):
            last_exception = None
            response = None
            time.sleep(random.uniform(0.2, 1))
            self.logger.info("Start to send prepared request: {}".format(callback_param))
            try:
                # with requests.Session() as session:
                #    request = requests.Request(
                #        callback_param.method,
                #        callback_param.url,
                #        json=callback_param.request_body,
                #        headers=callback_param.headers,
                #    )
                #
                #    prepared = session.prepare_request(request)
                #    response = session.send(prepared)

                # Use BlackAnt HTTPClient for better request handling with connection pooling
                # and authentication support, fallback to direct requests if not available
                if HTTPClient:
                    try:
                        # Parse URL to extract base_url and endpoint
                        parsed_url = urlparse(callback_param.url)
                        base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
                        endpoint = parsed_url.path or "/"

                        # Create HTTP client instance
                        http_client = HTTPClient(base_url=base_url)

                        # Send request through BlackAnt HTTPClient
                        response = http_client.send_request(
                            endpoint=endpoint,
                            method=callback_param.method,
                            json=callback_param.request_body,
                            anonymous=True  # Callback doesn't need authentication
                        )
                    except Exception as client_error:
                        self.logger.warning(f"HTTPClient failed, falling back to requests: {client_error}")
                        # Fallback to original requests implementation
                        response = requests.request(
                            callback_param.method,
                            callback_param.url,
                            json=callback_param.request_body,
                            headers=callback_param.headers,
                        )
                else:
                    # Fallback to original requests implementation
                    response = requests.request(
                        callback_param.method,
                        callback_param.url,
                        json=callback_param.request_body,
                        headers=callback_param.headers,
                    )

                if response.status_code != 200:
                    self.logger.warning(
                        "Could not send callback. Status code: {}. Response: {}".format(
                            response.status_code, response.text
                        )
                    )
                    time.sleep(random.uniform(0, 1))
                    continue

                self.logger.info(
                    "Response status code: {} ; Response text: {}".format(
                        response.status_code, response.text
                    )
                )

                task_id_in_response = json.loads(response.request.body)["id"]

                if task_id_in_response != callback_param.request_body["id"]:
                    self.logger.warning(
                        "The Task ID in the request "
                        "and response are not the same (<response> - <request>): "
                        "{} - {}".format(task_id_in_response, callback_param.request_body["id"])
                    )

                return response

            # The too-broad exception warning is suppressed because we want to handle exceptions
            except Exception as ex:  # pylint: disable=broad-except
                self.logger.warning("Could not send callback")
                last_exception = ex
                time.sleep(random.uniform(0, 1))

        if last_exception:
            raise TaskCallbackFailedError(last_exception)
        if not response:
            raise TaskCallbackFailedError(
                "Cannot send out the callback. Please see the previous part of log further details"
            )

        if response.status_code != 200:
            raise TaskCallbackFailedError(
                "Status code: {} , Response: {}".format(response.status_code, response.text)
            )

    def run(self):
        """Starts the executor thread"""
        self.__executor.start()

    def stop(self):
        """Sends a stop signal to the calculation"""
        self.calculation.stop()

    def __execute_state_machine(self):
        """Runs the internal state machine until it does not reach a finished state"""
        # The below 2-3 sec sleep is necessary because sometimes the return statement of POST method
        # is faster than the result request.
        # It means that the Task ID is not available in the DB when the callback handler
        # gets the request about the result of the calculation.
        # The Task running is executed on a separated thread, so it's independent of the POST
        # method. This is the reason why the POST response is faster than Result request.
        time.sleep(random.uniform(2, 3))

        with redirect_stdout_stderr_to_logger(self.logger):
            while not self.__finished:
                try:
                    self.__start_execution_time = time.time()
                    self.__state.run()
                # The too-broad exception warning is suppressed because we want to handle exceptions
                except Exception as exc:  # pylint: disable=broad-except
                    if self.__state.name != "error":
                        self.__state = TaskErrorState(self)
                        self.logger.error("Unhandled exception caught, going to error state")
                        self.logger.exception(exc)
                    else:
                        self.logger.critical(
                            "Unhandled exception caught during error state. Terminating..."
                        )
                        self.logger.exception(exc)
                        return
