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

import aiomysql

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

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

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

"""


class MySQLSelector:
    """Field names and values to select rows for."""
    field: str
    value: str


class MySQLOperations:
    """Fields and values to describe operations.

    Describes a simple query to either retrieve data from a table,
    or update a field with a new value.

    Queries take the form of:

    $op == select:
        select $field from $table where $selector = $data
    $op == update:
        update $table set $field = $data where $selector = $originator.data

    Attributes:
        op (str): either "select" or "update"
        table (str): table to operate on
        field (str): field to select or update
        selectors (list): fields to select row in WHERE clause
        value (str): new value for update operation
    """
    op: str
    table: str
    field: str
    selectors: List[MySQLSelector]
    value: str


class MySQLOptions(BrokerOptions):
    """MySQL broker options.

    Performs one or more operations on a database. If multipe operations are
    given, they are executed within one transaction.

    Only "localhost" connection are supported at this time.

    Attributes:
        user (str): user name for credentials
        password (str): password for credentials
        database (str): name of the database
        operations (list): one or more things to do
    """
    user: str
    password: str
    database: str
    operations: List[MySQLOperations]


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

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

        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

    def _make_where(self,
                    data: Dict[str, Any],
                    selectors: List[MySQLSelector]) \
            -> Tuple[str, Tuple[str, ...]]:
        where_clause: List[str] = []
        where_values: List[str] = []
        for selector in selectors:
            where_clause.append(selector.field + ' = %s')
            where_values.append(util.apply_str_macros(selector.value, data))
        return (' and '.join(where_clause), tuple(where_values))

    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

        Request options:
            parse_dst (str): dst string to parse
                and return nodes list based on the input

        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
        """
        mysql_options = MySQLOptions()
        util.apply_dict(options, mysql_options)

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

        response = Response()

        try:
            conn = await aiomysql.connect(user=mysql_options.user,
                                          password=mysql_options.password,
                                          db=mysql_options.database)

            await conn.begin()

            for op in mysql_options.operations:
                cursor = await conn.cursor()

                row_selector_json: str

                if 'data' in request.originator:
                    row_selector_json = request.originator['data']
                else:
                    row_selector_json = request.data

                row_selector_values: Dict[str, Any] = \
                    json.loads(row_selector_json)

                (where_clause, where_values) = \
                    self._make_where(row_selector_values, op.selectors)

                if op.op == 'select':
                    await cursor.execute(f"""select {op.field}
                                          from {op.table}
                                          where """ + where_clause,
                                         where_values)
                    row = await cursor.fetchone()

                    if row:
                        response.data = row[0]
                        if isinstance(response.data, bytes):
                            response.data = bytes.decode(
                                    base64.b64encode(response.data))
                    else:
                        response.status = 'error'
                        response.reason = 'nothing returned from database'
                        break
                elif op.op == 'update':
                    value = op.value
                    if value == '${data}':
                        value = request.data

                    await cursor.execute(f"""update {op.table}
                                         set {op.field} = %s
                                         where """ + where_clause,
                                         (value, *where_values))
                else:
                    response.status = 'error'
                    response.reason = 'unsupported "op"'

                await cursor.close()

        except aiomysql.Error as err:
            self.log.error(f'error while performing MySQL query: {err}')
            response.status = 'error'
            response.reason = str(err)

        if response.status != 'error':
            await conn.commit()
            response.status = 'done'
        else:
            await conn.rollback()

        conn.close()

        await super().follow_tasks(request, response, mysql_options)

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