import json
import time

from datetime import datetime
from logging import Logger
from threading import Condition, Lock
from xml.etree import cElementTree as eTree

import sysactions

from cfgtools import Group
from modelcommon.electronicpayment import TransactionProcessors

from mbcontextmessagehandler import MbContextMessageBus
from msgbus import MBException
from modelcommon.model import SitefResponse
from typing import (
    Any,
    Union,
    Optional,
    Dict,
)
from util import (
    get_transaction_identifier,
    Cache,
)

from ._MobileServiceFinder import MobileServiceFinder
from dialogmanager import DialogManager
from model import (
    CancelPaymentOrderData,
    ConfirmFinishPaymentData,
    FinishPaymentData,
    PaymentResponseMessage,
    TestConnectionData,
)
from paymentprocessor import PaymentProcessor


class MobilePaymentProcessor(PaymentProcessor):

    def __init__(
        self,
        message_bus,        # type: MbContextMessageBus
        dialog_manager,     # type: DialogManager
        config,             # type: Group
        logger,             # type: Logger
        cache,              # type: Cache
    ):
        # type: (...) -> None

        super(MobilePaymentProcessor, self).__init__(message_bus, dialog_manager, config, logger, cache)

        sysactions.mbcontext = message_bus.mbcontext
        self.message_bus = message_bus
        self.is_sync = config.find_value("IsMobileSynch", "False") == "True"
        self.sitef_service_finder = MobileServiceFinder()
        self.sync_condition = Condition()
        self.sync_condition_lock = Lock()
        self.has_thread_waiting = False
        self.finalized_status = 0
        self.finalized_data = None
        self.logger = logger
        self.pos_id = config.find_value("PosId")
        self.cache = cache

    def connection_status(
        self,
        test_connection_data    # type: TestConnectionData
    ):
        # type: (...) -> bool

        return True

    def add_payment(self, start_payment_data):
        return PaymentResponseMessage("Mobile Sitef cannot process transactions")

    def cancel_payment_order(
        self,
        cancel_payment_order_data,  # type: CancelPaymentOrderData
        additional_params="",       # type: Optional[str]
    ):
        # type: (...) -> Any[Union[SitefResponse]]

        self.logger.debug("MobileProcessor Cancel payment order - STARTED")

        while self.has_thread_waiting:
            time.sleep(0.2)

        amount = "{:.2f}".format(float(cancel_payment_order_data.tender_amount)).replace(",", "")
        tenders = json.loads(cancel_payment_order_data.tenders)
        tender_is_list = isinstance(tenders, list)
        tenders = [{"detail": tenders[0] if tender_is_list else tenders}]

        data = self._get_json_data(
            cancel_payment_order_data.order_id,
            cancel_payment_order_data.tender_id,
            cancel_payment_order_data.fiscal_date,
            cancel_payment_order_data.fiscal_time,
            tenders,
            is_reversal=True,
            status="0"
        )
        data["tenderType"] = cancel_payment_order_data.tender_type
        data["tenderAmount"] = amount
        data["operator"] = cancel_payment_order_data.operator_id
        data["nsuHost"] = cancel_payment_order_data.nsu_host
        data["cardNumber"] = cancel_payment_order_data.card_number
        data["media"] = cancel_payment_order_data.media
        data = json.dumps(data)
        self.logger.debug("Cancel payment order request: {}", data)

        retry = 0
        while retry < 3:
            try:
                self._publish_event(
                    pos_id=int(cancel_payment_order_data.pos_id),
                    event_type="MOBILE",
                    action_type="MOBILE_REVERSAL_TRANSACTION",
                    event_data=data,
                )
                self._wait_confirmation(
                    seconds_timeout=120
                )
                response = self._get_json_cancel_data(amount=amount)
                if response is not None:
                    self._set_cache_receipt_data(
                        order_id=cancel_payment_order_data.order_id,
                        tender_id=cancel_payment_order_data.tender_id,
                        receipt_customer=response.customer_receipt,
                        receipt_merchant=response.merchant_receipt,
                    )
                    return response
                time.sleep(2)
                break
            except MBException as _:
                self.logger.exception("Error sending mobile cancel transaction")
            finally:
                retry += 1
        self.logger.debug("MobileProcessor Cancel payment order - FINISH")
        return ""

    def cancel_payment(self):
        pass  # Mobile Sitef doesn't handle cancel non-finished transactions

    def finish_payment(
        self,
        finish_payment_data     # type: FinishPaymentData
    ):
        # type: (...) -> bool

        pos_id = finish_payment_data.pos_id
        order_id = finish_payment_data.order_id
        tender_id = finish_payment_data.tender_id
        status = str(finish_payment_data.status)
        fiscal_date = finish_payment_data.fiscal_date
        fiscal_time = finish_payment_data.fiscal_time
        tender_details = finish_payment_data.tender_details

        if self._get_order_state(int(pos_id), order_id, 'VOIDED'):
            return True

        data = self._get_json_data(
            order_id,
            tender_id,
            fiscal_date,
            fiscal_time,
            tender_details,
            is_reversal=False,
            status=status
        )
        data = json.dumps(data)
        retry = 0
        while retry < 3:
            try:
                self._publish_event(
                    int(pos_id),
                    "MOBILE",
                    "MOBILE_CONFIRM_TRANSACTION",
                    data
                )
                break
            except MBException as _:
                self.logger.exception("Error sending mobile confirm transaction")
            finally:
                retry += 1

        if not self.is_sync:
            return True

        return self._wait_confirmation()

    def confirm_finish_payment(
        self,
        confirm_finish_payment_data     # type: ConfirmFinishPaymentData
    ):
        # type: (...) -> None

        try:
            self.sitef_service_finder.lock_sitef()
            try_num = 0
            while not self.has_thread_waiting and try_num < 10:
                time.sleep(0.2)
                try_num += 1

            if not self.has_thread_waiting:
                raise Exception("No thread is waiting to finalize")

            with self.sync_condition_lock:
                self.finalized_status = confirm_finish_payment_data.status
                self.finalized_data = confirm_finish_payment_data.data
                with self.sync_condition:
                    self.sync_condition.notify_all()
        except Exception as ex:
            self.logger.exception("Error cancel payment order: {}".format(ex))
        finally:
            self.sitef_service_finder.release_sitef()

    def _publish_event(
        self,
        pos_id,         # type: int
        event_type,     # type: str
        action_type,    # type: str
        event_data      # type: str
    ):
        # type: (...) -> None

        event_subject = "POS{}".format(pos_id)
        event_data = str(event_data)
        event_xml = """<Event subject="{0}" type="{1}">
                               <{2}>
                                   {3}
                               </{2}>
                          </Event>""".format(event_subject, event_type, action_type, event_data)

        self.message_bus.mbcontext.MB_EasyEvtSend(
            subject=event_subject,
            type=event_type,
            xml=event_xml
        )

    @staticmethod
    def _get_json_data(
        order_id,           # type: str
        tender_id,          # type: str
        fiscal_date,        # type: str
        fiscal_time,        # type: str
        tenders,            # type: str
        is_reversal=False,  # type: bool
        status=None         # type: str
    ):
        # type: (...) -> Dict

        if is_reversal:
            fiscal_date = datetime.strptime(fiscal_date, '%Y%m%d').strftime('%d%m%Y')

        transaction_identifier = get_transaction_identifier(order_id, tender_id)
        transaction_data = {
            "timeStamp": time.time(),
            "transactionIdentifier": transaction_identifier,
            "fiscalDate": fiscal_date,
            "fiscalHour": fiscal_time,
            "fiscalTime": fiscal_time,
            "tenders": tenders,
            "isReversal": is_reversal
        }

        if status:
            transaction_data["confirmTransaction"] = str(status)

        return transaction_data

    @staticmethod
    def _get_order_state(
        pos_id,     # type: int
        order_id,   # type: str
        state,      # type: str
    ):
        # type: (...) -> Optional[str]

        from sysactions import get_model, get_posot

        order = get_posot(get_model(pos_id)).orderPicture(orderid=order_id)
        order = eTree.XML(order).find("Order")
        return order.find("StateHistory/State[@state='{}']".format(state)) is not None

    def _wait_confirmation(
        self,
        seconds_timeout=540  # type: int
    ):
        # type: (...) -> int

        self.sync_condition_lock.acquire()
        acquired = True
        try:
            if not self.sync_condition.acquire(False):
                raise Exception("Another thread has the finalize lock")
            try:
                self.has_thread_waiting = True
                self.sync_condition_lock.release()
                acquired = False
                if self.sync_condition.wait(seconds_timeout):
                    raise Exception("Timeout waiting for payment conclusion")
                self.has_thread_waiting = False
                return 0
            finally:
                self.sync_condition.release()
        finally:
            if acquired:
                self.sync_condition_lock.release()

    def get_name(
        self,
    ):
        # type: (...) -> str

        self.register_eft_processor(
            pos_id=self.pos_id,
            processor_name=TransactionProcessors.MOBILE,
        )

        return TransactionProcessors.MOBILE

    def get_document(
        self,
        pos_id      # type: str
    ):
        # type: (...) -> str

        pass

    def stop(
        self,
    ):
        # type: (...) -> None

        pass

    def get_last_receipt(
        self,
        eft_processor,      # type: str
    ):
        # type: (...) -> Optional[str]

        if str(eft_processor.upper()) not in [TransactionProcessors.SITEF, TransactionProcessors.CAPPTA]:
            self._publish_event(
                pos_id=int(self.pos_id),
                event_type="MOBILE",
                action_type="MOBILE_PRINT_LAST_RECEIPT",
                event_data="",
            )

        return self.cache.get_last_receipt(eft_processor=eft_processor)

    def _get_json_cancel_data(
        self,       # type: MobilePaymentProcessor
        amount,     # type: str
    ):
        # type: (...) -> Optional[SitefResponse]

        if self.finalized_status == 0 or self.finalized_data in [None, '', 'None']:
            return None

        self.logger.debug("Cancel payment order response: {}", self.finalized_data)
        data_cancel = json.loads(self.finalized_data)
        response = SitefResponse()
        response.transaction_identifier = data_cancel.get("TransactionIdentifier", "")
        response.nsu = data_cancel.get("Nsu", "")
        response.nsu_host = data_cancel.get("NsuHost", "")
        response.nsu_origin = data_cancel.get("AuthCode", "")
        response.transaction_value = amount
        response.customer_receipt = data_cancel.get("ReceiptCustomer", "")
        response.merchant_receipt = data_cancel.get("ReceiptMerchant", "")

        return response

    def _set_cache_receipt_data(
        self,               # type: MobilePaymentProcessor
        order_id,           # type: str
        tender_id,          # type: str
        receipt_customer,   # type: str
        receipt_merchant,   # type: str
    ):
        # type: (...) -> None

        receipt_data = {
            "order_id": order_id,
            "tender_id": tender_id,
            "receipt_customer": receipt_customer,
            "receipt_merchant": receipt_merchant,
        }

        receipt_data = json.dumps(receipt_data)
        self.cache.set_last_receipt(
            receipt_data=receipt_data,
            eft_processor=TransactionProcessors.SITEF,
        )
