import asyncio
import base64
import json
from typing import Any, Awaitable, Callable, Dict

from .. import app_config, util
from ..broker import Broker, BrokerOptions
from ..logger import Logger
from ..request import Request
from ..response import Response

NAME = 'Command'
"""Name of the main class of the module.

Used by the App class
to obtain the correct class via getattr

"""


class CommandOptions(BrokerOptions):
    """Command broker options.

    Attributes:
        run (str): command (script) to run
        args (str): arguments of the command
        as_user (str): run the command as user
        ignore_stderr (bool): use the exit code instead of
            stderr to indicate success or failure
        pipe (bool): execute the command as pipe, passing
            the data as stdin and returning stdout

    """
    run: str
    args: str
    as_user: str
    ignore_stderr: bool = False
    pipe: bool = False


class Command(Broker):
    """Command broker class, inherits Broker."""

    def __init__(self, send_req_cb: Callable[[Request],
                                             Awaitable[None]]) -> None:
        """Constructor for Command 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._logger = Logger(f'broker.{util.short_class_name(self)}')
        super().__init__(send_req_cb)

    async def start(self) -> None:
        """Starts the broker.

        Currently does nothing as there is no
        specific logic related to the broker startup. Reserved for
        future use and consistency with the parent class

        Args:
            None

        Returns
            None

        """
        pass

    async def process_request(self,
                              request: Request,
                              options: Dict[str, Any],
                              callback: Callable[[Request, Response],
                                                 Awaitable[None]]) -> None:
        """Processes the provided request and sends back responses.

        Uses the provided callback function

        Args:
            request (Request): Request message object
            options (Dict): broker options
            callback (Callable[[Request, Response], Awaitable[None]]):
                callback function to dispatch the responses to

        Returns
            None

        """
        command_options = CommandOptions()
        util.apply_dict(options, command_options)

        args = command_options.args
        extra_args: Dict[str, Any] = {}
        stdin: bytes | None = None
        if request.data and not command_options.pipe:
            try:
                extra_args = json.loads(request.data)
            except json.JSONDecodeError:
                response = Response()
                response.status = 'rejected'
                response.reason = 'Cannot parse JSON in data'
                await asyncio.ensure_future(callback(request, response))
                return
        elif command_options.pipe:
            stdin = base64.b64decode(request.data)
        args = util.apply_str_macros(args, extra_args)

        response = Response()
        response.status = 'accepted'
        await asyncio.ensure_future(callback(request, response))

        self.log.debug(util.compact_str(
            f'run command: run={command_options.run} \
                           args="{args}" \
                           as_user={command_options.as_user}'
        ))

        response = Response()
        response.status = 'done'

        try:
            stdout, stderr, code = await util.exec_command(
                    command_options.run,
                    args,
                    command_options.as_user,
                    stdin
            )
            if stderr and not command_options.ignore_stderr:
                response.reason = util.compact_str(bytes.decode(stderr))
                response.status = 'error'
            elif code:
                response.reason = f'exited with code {code}: ' + \
                    util.compact_str(bytes.decode(stderr))
                response.status = 'error'
            else:
                if command_options.pipe:
                    stdout = base64.b64encode(stdout)
                response.data = bytes.decode(stdout)
        except PermissionError as err:
            self.log.error(repr(err))
            response.status = 'error'
            response.reason = util.compact_str(' \
                permission denied to execute the command \
            ')
        finally:
            await super().follow_tasks(request, response, command_options)

            await asyncio.ensure_future(callback(request, response))
