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

from model.error_type import (
    ExternalServicesErrorsType,
    OperationErrorsType,
    PosValidationErrorsType,
)
from model.exception import (
    ExternalServiceErrors,
    OperationErrors,
    ValidationError,
)
from interactor.route.payment.insert.dto import OrderInsertPaymentRequest
from model.repository import PosRepositoryModel
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_SYS_NAK,
)
from mwhelper import BaseRepository
from persistence import Connection
from sysactions import (
    get_used_service,
    translate_message,
)
from typing import (
    Dict,
    List,
    Optional,
)

from services import PrinterService


class FiscalService(FiscalServiceModel, BaseRepository):

    def __init__(
        self,
        message_bus,        # type: MbContextMessageBus
        logger,             # type: Logger
        pos_repository,     # type: PosRepositoryModel
        printer_service,    # type: PrinterService
    ):
        # type: (...) -> None

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

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

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

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

            if fiscal_response.token == TK_SYS_ACK:
                if print_coupon:
                    self.logger.info("Start print fiscal coupon")
                    self._print_fiscal_coupon(
                        model=model,
                        fiscal_response=fiscal_response,
                    )
                    self.logger.info("End print fiscal coupon")
                return fiscal_response.data

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

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

            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

            if is_fiscal_ok == "False" and message.startswith("$"):
                message = message[1:] if message.startswith("$") else message
                params = ""
                if "|" in message:
                    message, params = message.split("|", 1)
                message = translate_message(model, message, *(params.split("|")))
                raise ExternalServiceErrors(ExternalServicesErrorsType.FiscalizeError, message)

            raise StopIteration()

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

        try:
            print_response = self.printer_service.send_print_data(
                model=model,
                response=fiscal_response.data,
            )
            if print_response is None or print_response.token == TK_SYS_NAK:
                self.logger.error("Cannot print fiscal coupon")
        except Exception as _:
            self.logger.exception("Error printing fiscal coupon")

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

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

        log_message = "Fiscalize order: [pos_id: {} order_id: {} device_type: {}]"
        self.logger.info(log_message.format(pos_id, order_id, device_type))

        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]

        log_message = "Process payment: save promotional tenders payment data: [pos_id: {} order_id: {}]"
        self.logger.info(log_message.format(request.pos_id, request.order_id))
        self._save_promotional_tenders_payment_data(model)

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

        return None

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

        log_message = "Process payment: get electronic payment service: [pos_id: {} order_id: {}]"
        self.logger.info(log_message.format(request.pos_id, request.order_id))
        payment_service = self._get_electronic_payment_service(model)
        order_picture = None
        try:
            log_message = "Process payment: get order picture: [pos_id: {} order_id: {}]"
            self.logger.info(log_message.format(request.pos_id, request.order_id))
            order_picture = self.pos_repository.get_order_picture(
                pos_id=request.pos_id,
                order_id=request.order_id,
                model=model,
            )

            log_message = "Process payment: add electronic payment: [pos_id: {} order_id: {}]"
            self.logger.info(log_message.format(request.pos_id, request.order_id))
            response = self._add_electronic_payment(
                model=model,
                order=order_picture,
                payment_service=payment_service,
                pos_id=str(request.pos_id),
                amount=request.amount,
                electronic_type_id=str(request.electronic_type_id),
            )

            log_message = "Process payment: validate electronic payment response: [pos_id: {} order_id: {}]"
            self.logger.info(log_message.format(request.pos_id, request.order_id))
            request.amount = self._validate_electronic_payment_response(
                response=response,
                amount=request.amount,
                pos_id=str(request.pos_id),
                order_id=str(request.order_id),
            )
        except Exception as _:
            message = "Error processing electronic payment: [pos_id: {} order_id: {}]: \norderPicture: {}"
            self.logger.exception(message.format(request.pos_id, request.order_id, eTree.tostring(order_picture)))
            self._cancel_electronic_payment(payment_service)
            raise

        log_message = "Process payment: get electronic fiscal params: [pos_id: {} order_id: {}]"
        self.logger.info(log_message.format(request.pos_id, request.order_id))
        fiscal_params = self._get_electronic_fiscal_params(
            model=model,
            pos_id=request.pos_id,
            order_id=request.order_id,
            payment_id=request.payment_id,
            amount=request.amount,
            response=response,
        )

        log_message = "Process payment: store payment data: [pos_id: {} order_id: {}]"
        self.logger.info(log_message.format(request.pos_id, request.order_id))
        self._store_payment_data(fiscal_params)

        log_message = "Process payment: get tender details and Finalize: [pos_id: {} order_id: {}]"
        self.logger.info(log_message.format(request.pos_id, request.order_id))
        return self._get_tender_details(eTree.fromstring(response.data))

    def _add_electronic_payment(
        self,
        model,                # type: eTree
        order,                # type: eTree
        payment_service,      # type: str
        pos_id,               # type: str
        amount,               # type: float
        electronic_type_id,   # type: str
    ):
        # type: (...) -> Message

        data = self._get_electronic_payment_data(
            order=order,
            model=model,
            pos_id=pos_id,
            amount=amount,
            electronic_type_id=electronic_type_id,
        )
        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,        # type: Message
      pos_id,       # type: str
      order_id,     # type: str
      payment_id,   # type: int
      amount,       # type: float
      response,     # type: Message
    ):
        # type: (...) -> Dict

        order = self.pos_repository.get_order_picture(
            pos_id=pos_id,
            order_id=order_id,
            model=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(payment_id),
            str(amount),
            change_amount
        )

        return self._build_additional_fiscal_params(fiscal_params, response)

    def _validate_electronic_payment_response(
        self,
        response,   # type: Message
        amount,     # type: float
        pos_id,     # type: str
        order_id,   # type: str

    ):
        # type: (...) -> float

        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":
            log_message = "Error validate electronic payment: [pos_id: {} order_id: {} result: {}]"
            self.logger.info(log_message.format(pos_id, order_id, result))
            raise OperationErrors(OperationErrorsType.UnexpectedPaymentResponse)

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

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

        return float(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)

    @staticmethod
    def _get_electronic_payment_data(
        model,                # type: eTree
        order,                # type: eTree
        pos_id,               # type: str
        amount,               # type: float
        electronic_type_id,   # type: str
    ):
        # type: (...) -> str

        order_id = order.get("orderId")
        session_id = model.find("Operator").get("sessionId")
        operator = model.find("Operator").get("id")
        amount = "{:.2f}".format(amount)
        electronic_type = 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["t_band"] = response_xml.get("tBand", "")
            fiscal_params["t_pag"] = response_xml.get("tPag", "")
            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", ""),
            "tBand": payment_data.get("tBand", ""),
            "tPag": payment_data.get("tPag", ""),
            "NsuHost": payment_data.get("NsuHost", ""),
            "CardNumber": payment_data.get("CardNumber", ""),
            "TransactionIdentifier": payment_data.get("TransactionIdentifier"),
            "LastDigits": payment_data.get("LastDigits", ""),
        })

        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
