import base64
import json
import time
from logging import Logger
from xml.etree import cElementTree as eTree

from model.error_type import OperationErrorsType, PosValidationErrorsType
from model.exception import OperationErrors, ValidationError
from interactor.route.payment.insert.dto import OrderInsertPaymentRequest
from model.services import FiscalServiceModel
from model.tokens import BusTokens
from interactor.route.electronic_payment.send_responses.dto import SendResponsesRequest
from mbcontextmessagehandler import MbContextMessageBus
from messagebus import Message, DataType
from msgbus import TK_SYS_ACK, TK_PRN_PRINT, TK_SYS_NAK
from mwhelper import BaseRepository
from persistence import Connection
from posot import OrderTaker
from sysactions import get_used_service
from typing import (
    Dict,
    List,
    Optional
)


class FiscalService(FiscalServiceModel, BaseRepository):

    def __init__(self, message_bus, logger):
        # type: (MbContextMessageBus, Logger) -> None

        super(FiscalService, self).__init__(message_bus.mbcontext)
        self.message_bus = message_bus
        self.logger = logger
        self.electronic_payments_id = None

    def process_order_fiscal(
        self,
        model,               # type: eTree
        pos_ot,              # type: OrderTaker
        print_coupon=True,   # type: bool
    ):
        # type: (...) -> Optional[str]

        max_retry = 2
        while True:
            fiscal_response = self._fiscalize_order(model)

            self.logger.info("FiscalResponse: {}".format(fiscal_response.data))

            if fiscal_response.token == TK_SYS_ACK:
                if print_coupon:
                    self._print_fiscal_coupon(model, fiscal_response)
                return fiscal_response.data

            if fiscal_response.data in ("Certificado da Loja Expirado", "Falha na Leitura da Validade do Certificado NFCE"):
                raise ValidationError(PosValidationErrorsType.FiscalCertificateError)

            if len(fiscal_response.data.split('\0')) < 2:
                is_fiscal_ok = "False"
            else:
                is_fiscal_ok, message = fiscal_response.data.split('\0')

            if is_fiscal_ok == "True":
                self.logger.info("Order fiscalized but without fiscal coupon print")
                break

            if max_retry > 0:
                max_retry -= 1
                self.logger.error("Cannot fiscalize coupon. Remaining attempts: {}".format(max_retry))
                time.sleep(1)
                continue

            raise StopIteration()

    def _print_fiscal_coupon(self, model, fiscal_response):
        # type: (eTree, Message) -> None

        try:
            print_response = self._send_print_data(model, fiscal_response)
            if print_response.token == TK_SYS_NAK:
                self.logger.error("Cannot print fiscal coupon")
        except Exception as _:
            self.logger.exception("Error printing fiscal coupon")

    def _send_print_data(self, model, response):
        # type: (eTree, Message) -> Message

        current_printer = get_used_service(model, "printer")

        message = Message(token=TK_PRN_PRINT, data_type=DataType.param, data=response.data)
        return self.message_bus.send_message(current_printer, message)

    def _fiscalize_order(self, model):
        # type: (eTree) -> Message

        order = self._get_current_order(model)
        pos_id = order.get("posId")
        order_id = order.get("orderId")
        data = "\0".join([pos_id, order_id])

        message = Message(token=BusTokens.TK_FISCALWRAPPER_PROCESS_REQUEST, data_type=DataType.param, data=data)
        return self.message_bus.send_message("FiscalWrapper", message)

    def process_payment(
        self,
        model,      # type: eTree
        request     # type: OrderInsertPaymentRequest
    ):
        # type: (...) -> Optional[str]

        self._save_promotional_tenders_payment_data(model)

        if request.electronic_type_id in self.electronic_payments_id:
            return self._process_electronic_payment(model, request)

        return None

    def _process_electronic_payment(
        self,
        model,      # type: eTree
        request     # type: OrderInsertPaymentRequest
    ):
        # type: (...) -> str

        payment_service = self._get_electronic_payment_service(model)
        try:
            response = self._add_electronic_payment(model, payment_service, request)
            self._validate_electronic_payment_response(response, request)
        except Exception as _:
            self.logger.exception("Error processing electronic payment")
            self._cancel_electronic_payment(payment_service)
            raise

        fiscal_params = self._get_electronic_fiscal_params(model, request, response)
        self._store_payment_data(fiscal_params)

        return self._get_tender_details(eTree.fromstring(response.data))

    def _add_electronic_payment(self, model, payment_service, request):
        # type: (eTree, str, OrderInsertPaymentRequest) -> Message

        data = self._get_electronic_payment_data(model, request)
        message = Message(token=BusTokens.TK_ELECTRONIC_PAYMENT_ADD_PAYMENT, data_type=DataType.param, data=data)
        return self.message_bus.send_message(payment_service, message)

    def cancel_electronic_payment(
        self,
        model,      # type: eTree
        request,    # type: OrderInsertPaymentRequest
    ):
        # type: (...) -> None

        if request.electronic_type_id in self.electronic_payments_id:
            payment_service = self._get_electronic_payment_service(model=model)
            self._cancel_electronic_payment(payment_service=payment_service)

    def _cancel_electronic_payment(self, payment_service):
        # type: (str) -> None

        message = Message(token=BusTokens.TK_ELECTRONIC_PAYMENT_CANCEL_PAYMENT)
        self.message_bus.send_message(payment_service, message)

    def _get_electronic_fiscal_params(self, model, request, response):
        # type: (eTree, OrderInsertPaymentRequest, Message) -> Dict

        order = self._get_current_order(model)
        change_amount = "0"
        tender_seq_id = str(self._get_last_tender_seq_id(order))

        fiscal_params = self._build_fiscal_params(
            order.get("posId"),
            order.get("orderId"),
            tender_seq_id,
            str(request.payment_id),
            str(request.amount),
            change_amount
        )

        return self._build_additional_fiscal_params(fiscal_params, response)

    def _validate_electronic_payment_response(self, response, request):
        # type: (Message, OrderInsertPaymentRequest) -> None

        if response.token != TK_SYS_ACK or not response.data:
            raise OperationErrors(OperationErrorsType.InvalidPaymentResponse)

        response_xml = eTree.fromstring(response.data)
        result = response_xml.get("Result")
        if result != "0":
            raise OperationErrors(OperationErrorsType.UnexpectedPaymentResponse)

        approved_amount = response_xml.get("ApprovedAmount").replace(",", ".")
        if not approved_amount or float(approved_amount) == float(request.amount):
            return

        message = "Approved amount [{}] is different of the order amount [{}]".format(approved_amount, request.amount)
        self.logger.info(message)
        request.amount = approved_amount

    @staticmethod
    def _get_electronic_payment_service(model):
        # type: (eTree) -> str

        payment_service = get_used_service(model, "eft")
        if payment_service:
            return payment_service
        raise OperationErrors(OperationErrorsType.InvalidPaymentService)

    def _get_electronic_payment_data(self, model, request):
        # type: (eTree, OrderInsertPaymentRequest) -> str

        order = self._get_current_order(model)

        pos_id = str(request.pos_id)
        order_id = order.get("orderId")
        session_id = model.find("Operator").get("sessionId")
        operator = model.find("Operator").get("id")
        amount = "{:.2f}".format(request.amount)
        electronic_type = str(request.electronic_type_id)
        tender_seq_id = ''

        totaled_states = order.findall("StateHistory/State[@state='TOTALED']")
        last_totaled = totaled_states[-1].get("timestamp").replace("-", "").replace(":", "")
        last_totaled_date = last_totaled
        fiscal_date = last_totaled_date[:8]
        fiscal_time = last_totaled_date[9:15]

        data = ";".join(
            [
                pos_id,
                order_id,
                session_id,
                operator,
                electronic_type,
                amount,
                fiscal_date,
                fiscal_time,
                tender_seq_id
            ]
        )

        return data

    def _save_promotional_tenders_payment_data(self, model):
        # type: (eTree) -> None

        order = self._get_current_order(model)
        promotional_tenders = self._get_order_promotional_tenders(order)
        for tender in promotional_tenders:
            fiscal_params = self._build_fiscal_params(order.get("posId"),
                                                      order.get("orderId"),
                                                      promotional_tenders[tender].get("tender_seq_id"),
                                                      promotional_tenders[tender].get("type"),
                                                      promotional_tenders[tender].get("amount"),
                                                      promotional_tenders[tender].get("change"))

            self._store_payment_data(fiscal_params)

    @staticmethod
    def _build_fiscal_params(pos_id, order_id, tender_seq_id, tender_type, amount, change):
        # type: (str, str, str, str, str, str) -> Dict

        return {
            "pos_id": pos_id,
            "order_id": order_id,
            "tender_seq_id": tender_seq_id,
            "type": tender_type,
            "amount": amount,
            "change": change
        }

    @staticmethod
    def _build_additional_fiscal_params(fiscal_params, response):
        # type: (Dict, Message) -> Dict

        response_xml = eTree.fromstring(response.data)
        if response_xml is not None:
            fiscal_params["auth_code"] = response_xml.get("AuthCode", "")
            fiscal_params["id_auth"] = response_xml.get("IdAuth", "")
            fiscal_params["cnpj_auth"] = response_xml.get("CNPJAuth", "")
            fiscal_params["bandeira"] = response_xml.get("Bandeira", "")
            fiscal_params["receipt_merchant"] = base64.b64decode(response_xml.get("ReceiptMerchant", ""))
            fiscal_params["receipt_customer"] = base64.b64decode(response_xml.get("ReceiptCustomer", ""))

        return fiscal_params

    @staticmethod
    def _get_order_promotional_tenders(order):
        # type: (eTree) -> Dict

        promotional_tenders = {}
        for tender in order.findall("TenderHistory/Tender"):
            tender_detail = tender.get("tenderDetail")
            if tender_detail:
                is_promotional_tender = json.loads(tender_detail).get("promotionalTender", False)
                if is_promotional_tender:
                    tender_id = tender.get("tenderId")
                    promotional_tenders[tender_id] = {"amount": tender.get("tenderAmount"),
                                                      "tender_seq_id": tender_id,
                                                      "type": tender.get("tenderType"),
                                                      "change": "0"}
        return promotional_tenders

    def _store_payment_data(self, fiscal_params):
        # type: (Dict) -> None

        def inner_func(conn):
            # type: (Connection) -> None

            conn.pquery("fiscal_savePaymentData", **fiscal_params)

        self.execute_with_transaction(inner_func, service_name="FiscalPersistence")

    def get_electronic_payments_id(self):
        # type: () -> None

        def inner_func(conn):
            # type: (Connection) -> List[int]

            query = "SELECT ElectronicType FROM TenderType WHERE ElectronicType <> 0"
            cursor = conn.select(query)

            electronic_type_ids = []
            for row in cursor:
                electronic_type_id = int(row.get_entry("ElectronicType"))
                if electronic_type_id not in electronic_type_ids:
                    electronic_type_ids.append(electronic_type_id)

            return electronic_type_ids

        self.electronic_payments_id = self.execute_with_connection(inner_func, service_name="Persistence")

    def send_electronic_payment_message_response(self, model, request):
        # type: (eTree, SendResponsesRequest) -> None

        payment_service = self._get_electronic_payment_service(model)
        data = "|".join([model.get("posId"), request.response])
        message = Message(token=BusTokens.TK_ELECTRONIC_PAYMENT_SEND_RESPONSE, data_type=DataType.param, data=data)
        response = self.message_bus.send_message(payment_service, message)
        if response.token != TK_SYS_ACK:
            raise OperationErrors(OperationErrorsType.InvalidPaymentResponse)

    @staticmethod
    def _get_last_tender_seq_id(order):
        return len(order.findall("TenderHistory/Tender")) + 1

    @staticmethod
    def _get_current_order(model):
        return model.find("CurrentOrder/Order")

    @staticmethod
    def _get_tender_details(
        payment_data  # type: Dict
    ):
        # type: (...) -> str

        tender_details = json.dumps({
            "Media": payment_data.get("Media", "").strip(),
            "CNPJAuth": payment_data.get("CNPJAuth", ""),
            "TransactionProcessor": payment_data.get("TransactionProcessor", ""),
            "Bandeira": payment_data.get("Bandeira", ""),
            "IdAuth": payment_data.get("IdAuth", ""),
            "AuthCode": payment_data.get("AuthCode", ""),
            "ReceiptCustomer": payment_data.get("ReceiptCustomer", ""),
            "ReceiptMerchant": payment_data.get("ReceiptMerchant", ""),
            "AccreditorSat": payment_data.get("AccreditorSat", ""),
            "NfceBrand": payment_data.get("NfceBrand", ""),
            "NsuHost": payment_data.get("NsuHost", ""),
            "CardNumber": payment_data.get("CardNumber", ""),
            "TransactionIdentifier": payment_data.get("TransactionIdentifier"),
            "OriginalNfceBrand": payment_data.get("OriginalNfceBrand", "")
        })

        return tender_details

    def get_customer_receipts(
        self,
        model,      # type: eTree
    ):
        # type: (...) -> List[str]

        order = self._get_current_order(model)
        customer_receipts = []
        for tender in order.findall("TenderHistory/Tender"):
            if int(tender.get("tenderType", -1)) in self.electronic_payments_id:
                tender_detail = json.loads(tender.get("tenderDetail", "{}"))
                tef_customer_receipt = tender_detail.get("ReceiptCustomer", '') if tender_detail is not None else None
                if tef_customer_receipt not in ['None', None, '']:
                    customer_receipts.append(tef_customer_receipt)

        return customer_receipts
