import json
import base64

from datetime import date
from typing import Optional

from messagebus import (
    DataType,
    DefaultToken,
    Message,
    MessageBus,
)
from .shared import error
from .shared.exception import NReportsException
from .shared.reports import Reports
from .shared.token import (
    TK_NREPORTS_REPORT_JSON_PARSER,
    TK_NREPORTS_BUILD_REPORT,
    TK_NREPORTS_BUILD_RAW_REPORT,
    TK_NREPORTS_BUILD_NFE_PDF_REPORT,
)


class NReports(object):

    def __init__(
        self,           # type: NReports
        message_bus,    # type: MessageBus
    ):
        # type: (...) -> None

        self.message_bus = message_bus

    def request_report(
        self,           # type: NReports
        pos_id,         # type: Optional[int]
        report_name,    # type: str
        **kwargs
    ):
        # type: (...) -> str

        request = self._build_request_data(
            pos_id=pos_id,
            report_name=report_name,
            **kwargs
        )

        reply = self.message_bus.send_message(
            component="nReports",
            message=Message(
                token=TK_NREPORTS_BUILD_REPORT,
                data_type=DataType.json,
                data=request,
            )
        )

        if reply.token == DefaultToken.TK_SYS_ACK:
            return reply.data

        raise self._build_exception_from_reply(reply=reply)

    def request_raw_report(
        self,           # type: NReports
        pos_id,         # type: Optional[int]
        report_name,    # type: str
        **kwargs
    ):
        # type: (...) -> str

        request = self._build_request_data(
            pos_id=pos_id,
            report_name=report_name,
            **kwargs
        )

        reply = self.message_bus.send_message(
            component="nReports",
            message=Message(
                token=TK_NREPORTS_BUILD_RAW_REPORT,
                data_type=DataType.json,
                data=request,
            )
        )

        if reply.token == DefaultToken.TK_SYS_ACK:
            return base64.b64decode(reply.data).decode("utf-8")

        raise self._build_exception_from_reply(reply=reply)

    def request_report_json_parser(
        self,           # type: NReports
        pos_id,         # type: Optional[int]
        report_json,    # type: str
    ):
        # type: (...) -> str

        request = {
            "posId": int(pos_id) if pos_id is not None else None,
            "reportJson": report_json,
        }
        request = json.dumps(request).encode("utf-8")

        reply = self.message_bus.send_message(
            component="nReports",
            message=Message(
                token=TK_NREPORTS_REPORT_JSON_PARSER,
                data_type=DataType.json,
                data=request,
            )
        )

        if reply.token == DefaultToken.TK_SYS_ACK:
            return reply.data

        raise self._build_exception_from_reply(reply=reply)

    def canceled_fiscal_coupon(
        self,           # type: NReports
        pos_id,         # type: int
        order_id,       # type: int
        is_mobile=False,   # type: Optional[bool]
    ):
        # type: (...) -> str

        return self.request_report(
            pos_id=pos_id,
            report_name=Reports.canceled_fiscal_coupon,
            order_id=order_id,
            is_mobile=is_mobile,
        )

    def order_receipt(
        self,           # type: NReports
        pos_id,         # type: int
        order_id,       # type: int
    ):
        # type: (...) -> str

        return self.request_report(
            pos_id=pos_id,
            report_name=Reports.order_receipt,
            order_id=order_id,
        )

    def package_order_payment(
        self,             # type: NReports
        pos_id,           # type: int
        order_id,         # type: int
        payment_amount,   # type: float
        tender_descr,     # type: str
    ):
        # type: (...) -> str

        return self.request_report(
            pos_id=pos_id,
            report_name=Reports.package_order_payment,
            order_id=order_id,
            payment_amount=payment_amount,
            tender_descr=tender_descr,
        )

    def canceled_receipt(
        self,               # type: NReports
        pos_id,             # type: int
        order_id,           # type: int
        is_mobile=False,    # type: Optional[bool]
    ):
        # type: (...) -> str

        return self.request_report(
            pos_id=pos_id,
            report_name=Reports.canceled_receipt,
            order_id=order_id,
            is_mobile=is_mobile,
        )

    def end_of_sale(
        self,                           # type: NReports
        pos_id,                         # type: int
        order_id,                       # type: int
        mobile=False,                   # type: bool
        fiscal_coupon=True,             # type: bool
        print_donation_coupon=True,     # type: bool
        paper_width=80,                 # type: int
        b64_end_of_sales_notes=None,    # type: Optional[str]
    ):
        # type: (...) -> str

        return self.request_report(
            pos_id=pos_id,
            report_name=Reports.end_of_sale,
            order_id=order_id,
            is_mobile=mobile,
            fiscal_coupon=fiscal_coupon,
            print_donation_coupon=print_donation_coupon,
            paper_width=paper_width,
            b64_end_of_sales_notes=b64_end_of_sales_notes,
        )

    def fiscal_coupon(
        self,       # type: NReports
        pos_id,     # type: int
        order_id,   # type: int
    ):
        # type: (...) -> str

        return self.request_report(
            pos_id=pos_id,
            report_name=Reports.fiscal_coupon,
            order_id=order_id,
        )

    def pre_account(
        self,                                   # type: NReports
        pos_id,                                 # type: int
        order_id=None,                          # type: Optional[int]
        table_id=None,                          # type: Optional[int]
        is_mobile=False,                        # type: Optional[bool]
    ):
        # type: (...) -> str

        return self.request_report(
            pos_id=pos_id,
            report_name=Reports.pre_account,
            order_id=order_id,
            table_id=table_id,
            is_mobile=is_mobile,
        )

    def pre_account_quick_service(
        self,               # type: NReports
        pos_id,             # type: int
        order_id,           # type: int
        is_mobile=False,    # type: Optional[bool]
    ):
        # type: (...) -> str

        return self.request_report(
            pos_id=pos_id,
            report_name=Reports.pre_account_quick_service,
            order_id=order_id,
            is_mobile=is_mobile,
        )

    def pre_account_by_seats(
        self,                 # type: NReports
        pos_id,               # type: int
        table_id,             # type: str
        filter_table_seat,    # type: str
        print_by,             # type: str
        is_mobile,            # type: bool
    ):
        # type: (...) -> str

        return self.request_report(
            pos_id=pos_id,
            report_name=Reports.pre_account_by_seats,
            table_id=table_id,
            print_by=print_by,
            filter_table_seat=filter_table_seat,
            is_mobile=is_mobile,
        )

    def conference_items_by_seats(
        self,                 # type: NReports
        pos_id,               # type: int
        table_id,             # type: str
        filter_table_seat,    # type: str
        print_by,             # type: str
        is_mobile,            # type: bool
    ):
        # type: (...) -> str

        return self.request_report(
            pos_id=pos_id,
            report_name=Reports.conference_items_by_seats,
            table_id=table_id,
            print_by=print_by,
            filter_table_seat=filter_table_seat,
            is_mobile=is_mobile,
        )

    def conference_items(
        self,                                   # type: NReports
        pos_id,                                 # type: int
        order_id=None,                          # type: Optional[int]
        table_id=None,                          # type: Optional[int]
        is_mobile=False,                        # type: Optional[bool]
    ):
        # type: (...) -> str

        return self.request_report(
            pos_id=pos_id,
            report_name=Reports.conference_items,
            order_id=order_id,
            table_id=table_id,
            is_mobile=is_mobile,
        )

    def kitchen_label(
        self,                # type: NReports
        production_order,    # type: str
    ):
        # type: (...) -> str

        return self.request_report(
            pos_id=None,
            report_name=Reports.kitchen_label,
            production_order=production_order,
        )

    def delivery_fee(
        self,          # type: NReports
        pos_id,        # type: int
        since_date,    # type: str
        until_date,    # type: str
    ):
        # type: (...) -> str

        return self.request_report(
            pos_id=pos_id,
            report_name=Reports.delivery_fee,
            since_date=since_date,
            until_date=until_date,
        )

    def delivery_coupon(
        self,               # type: NReports
        pos_id,             # type: int
        order_id,           # type: int
        paper_width=80,     # type: int
    ):
        # type: (...) -> str

        return self.request_report(
            pos_id=pos_id,
            report_name=Reports.delivery_coupon,
            order_id=order_id,
            paper_width=paper_width,
        )

    def customer_call_coupon(
        self,       # type: NReports
        pos_id,     # type: int
        order_id,   # type: int
    ):
        # type: (...) -> str

        return self.request_report(
            pos_id=pos_id,
            report_name=Reports.customer_call_coupon,
            order_id=order_id,
        )

    def charge_account_payment(
        self,                 # type: NReports
        pos_id,               # type: int
        order_id,             # type: int
        customer_phone,       # type: str
        payment_amount,       # type: float
        operator_id,          # type: str
        table_id=None,        # type: Optional[str]
    ):
        # type: (...) -> str

        return self.request_report(
            report_name=Reports.charge_account_payment,
            pos_id=pos_id,
            order_id=order_id,
            customer_phone=customer_phone,
            payment_amount=payment_amount,
            operator_id=operator_id,
            table_id=table_id,
        )

    def charge_account_balance(
        self,                # type: NReports
        pos_id,              # type: int
        customer_phone,      # type: str
        start_date,          # type: date
        end_date,            # type: date
        transaction_type,    # type: str
    ):
        # type: (...) -> str

        return self.request_report(
            report_name=Reports.charge_account_balance,
            pos_id=pos_id,
            customer_phone=customer_phone,
            start_date=start_date.strftime("%Y-%m-%d"),
            end_date=end_date.strftime("%Y-%m-%d"),
            transaction_type=transaction_type,
        )

    def charge_account_add_balance(
        self,              # type: NReports
        pos_id,            # type: int
        customer_phone,    # type: str
        operator_id,       # type: str
        tender_descr,      # type: str
        payment_amount,    # type: float
    ):
        # type: (...) -> str

        return self.request_report(
            report_name=Reports.charge_account_add_balance,
            pos_id=pos_id,
            customer_phone=customer_phone,
            operator_id=operator_id,
            tender_descr=tender_descr,
            payment_amount=payment_amount,
        )

    def electronic_payment_receipt(
        self,                       # type: NReports
        pos_id,                     # type: int
        payment_response_json,      # type: str
        receipt_title="",           # type: str
    ):
        # type: (...) -> str

        return self.request_report(
            pos_id=pos_id,
            report_name=Reports.electronic_payment_receipt,
            payment_response_json=payment_response_json,
            receipt_title=receipt_title,
        )

    def nfe_pdf(
        self,       # type: NReports
        pos_id,     # type: int
        order_id,   # type: int
    ):
        # type: (...) -> str

        request = self._build_request_data(
            pos_id=pos_id,
            report_name=Reports.nfe_pdf,
            order_id=order_id,
        )

        reply = self.message_bus.send_message(
            component="nReports",
            message=Message(
                token=TK_NREPORTS_BUILD_NFE_PDF_REPORT,
                data_type=DataType.json,
                data=request,
            )
        )

        if reply.token == DefaultToken.TK_SYS_ACK:
            return json.loads(reply.data.decode("utf-8"))

        raise self._build_exception_from_reply(reply=reply)

    def _build_request_data(
        self,           # type: NReports
        pos_id,         # type: Optional[int]
        report_name,    # type: str
        **kwargs
    ):
        # type: (...) -> bytes

        request = {
            "posId": int(pos_id) if pos_id is not None else None,
            "reportName": report_name,
        }
        for key, value in kwargs.items():
            request[self._to_camel_case(key)] = value

        return json.dumps(request).encode("utf-8")

    @staticmethod
    def _build_exception_from_reply(
        reply,  # type: Message
    ):
        # type: (...) -> NReportsException

        try:
            data_text = reply.data.decode("utf-8")
        except UnicodeDecodeError:
            data_text = reply.data.hex()

        try:
            response_dict = json.loads(data_text)
            return NReportsException(
                message=response_dict["errorMessage"],
                error_code=response_dict["errorCode"],
            )
        except (Exception,):
            return NReportsException(
                message=data_text,
                error_code=error.UNEXPECTED_ERROR,
            )

    @staticmethod
    def _to_camel_case(
        snake_str,  # type: str
    ):
        # type: (...) -> str

        components = snake_str.split("_")
        return components[0].lower() + "".join(x.lower().title() for x in components[1:])
