from abc import abstractmethod
from typing import Any, Awaitable, Callable, Dict, List

from . import app_config, util
from .base import Base
from .request import Request
from .response import Response


class FollowTask:
    """Optional task config for daisy-chaining tasks.

    If present in TaskConfig, responses from tasks won't directly be returned
    to the caller, but rather will be wrapped in a new task.

    Brokers proessing requests can create or modify the list of follow tasks.

    Attributes:
        task (str): name of the next task
        destination (str): optional other destination
    """
    task: str
    destination: str = ''


class BrokerOptions:
    """Base class for broker options.

    Attributes:
        follow (List[FollowTask]): optional following tasks
    """
    follow: List[FollowTask] = []


class Broker(Base):
    """Main class used by the broker subclasses.

    Inherits Base

    Attributes:
        _send_req_cb (callable): callback that a broker can use to trigger
            sending another request. Set in the constructor.
    """

    _send_req_cb: Callable[[Request], Awaitable[None]]

    def __init__(self, send_req_cb: Callable[[Request],
                                             Awaitable[None]]) -> None:
        """Constructor for Broker class.

        Args:
            send_req_cb (callable): callback that a broker can use to trigger
                sending another request. Set by the App class.

        Returns:
            None
        """
        self._send_req_cb = send_req_cb
        super().__init__()

    @abstractmethod
    async def start(self) -> None:
        """Abstract method to start the backend.

        Must be implemented in the subclass

        """
        pass

    @abstractmethod
    async def process_request(self,
                              request: Request,
                              options: Dict[str, Any],
                              callback: Callable[[Request, Response],
                                                 Awaitable[None]]) -> None:
        """Abstract method to process the request.

        Must be implemented in the subclass

        """
        pass

    async def follow_tasks(self,
                           request: Request,
                           response: Response,
                           options: BrokerOptions | Dict[str, Any],
                           variables: Dict[str, str] = {}) -> None:
        """Helper method to trigger "follow" tasks.

        Tasks that support follow-up tasks can invoke this method at the end of
        their processing pipeline to trigger any tasks that are configured as
        follow-up. If any follow-up tasks are configured, then the response
        data is removed from the actual response and replace with "consumed",
        and instead is given as input data to the follow task(s).

        Args:
            request (Request): original request object
            response (Response): response to the original request
            options (BrokerOptions): broker options configured for the original
                task
            variables (Dict): optional variable values that are to be
                substituted into the follow-up task config using ${} notation

        Returns:
            None
        """
        if response.status != 'done':
            return

        consumed: bool = False

        if isinstance(options, dict):
            broker_options = BrokerOptions()
            util.apply_dict(options, broker_options)
            options = broker_options

        for follow in options.follow:
            follow_req = Request()

            follow_req.task = follow.task
            if follow_req.task[:2] == '${' and follow_req.task[-1] == '}':
                var = follow_req.task[2:-1]
                follow_req.task = variables[var]

            follow_req.src = app_config.ngcp.node_name
            if follow.destination:
                follow_req.dst = follow.destination
            else:
                follow_req.dst = request.dst

            if request.originator:
                follow_req.originator = request.originator
            else:
                follow_req.originator = request.__dict__

            follow_req.data = response.data
            consumed = True

            self.log.debug(util.compact_str(
                f'follow request: \
                {str(vars(follow_req))}'
                )
            )
            await self._send_req_cb(follow_req)

        if consumed:
            response.data = 'consumed'
