import base64
import os
import re
import time
from ctypes import c_long, create_string_buffer, addressof
from threading import Lock
from xml.etree import cElementTree as eTree

from messagebus import DataType, DefaultToken, Message
from model import PaymentResponseData, StartPaymentData, PaymentResponseMessage, PaymentResponse, FinishPaymentData

from ._SitefActions import SitefActions
from ._SitefParams import SitefParams
from ._SitefServiceFinder import SitefServiceFinder

TK_STORECFG_GET = 0x01300001
TK_SLCTRL_OMGR_ORDERPICT = 0x00A00002

API_INIT_OK = 0
API_LOOP_OK = 10000
API_LOOP_FINISHED = 0
API_NO_SITEF_COMMUNICATION = -5


def _get_terminal_id(pos_id):
    return "SW%06d" % int(pos_id)


def _api_has_data(command):
    return command.value == 0


def _format_api_error_response(api_ret):
    error_responses = {
        -2: PaymentResponseMessage("Cancelado"),
        -5: PaymentResponseMessage("Falha no Servidor SiTEF"),
        -6: PaymentResponseMessage("Operacao cancelada pelo usuario"),
        -40: PaymentResponseMessage("Nao Autorizado - SiTEF"),
        -43: PaymentResponseMessage("Erro PinPad"),
        -120: PaymentResponseMessage("Cancelado")
    }
    if api_ret > 0:
        return PaymentResponseMessage("Nao Autorizado")
    return error_responses.get(api_ret, PaymentResponseMessage("Falha na Operacao: Erro {}".format(api_ret)))


def _should_ignore_command(command):
    return command.value in (2, 4, 21, 14, 52)


def _get_dll_path(config):
    dll_path = config.find_value("DLLPath", "")
    for var in re.findall("{[a-zA-Z0-9]*}", dll_path):
        dll_path = dll_path.replace(var, os.environ.get(var.replace('{', '').replace('}', ''), ""))
    return os.path.normpath(os.path.join(dll_path, "clisitef32i.dll"))


class SitefPaymentProcessor(object):
    def __init__(self, message_bus, dialog_manager, config, logger):
        from ctypes import WinDLL
        self.logger = logger
        self.dialog_manager = dialog_manager
        self.message_bus = message_bus

        self.id_loja = config.find_value("IdLoja")
        self.timeout = config.find_value("Timeout")
        self.show_qr_code = config.find_value("ShowQRCode")
        dll_path = _get_dll_path(config)
        self.sitef_dll = WinDLL(dll_path)
        self.configura_sitef_interativo = self.sitef_dll["ConfiguraIntSiTefInterativo"]
        self.inicia_sitef_interativo = self.sitef_dll["IniciaFuncaoSiTefInterativo"]
        self.continua_sitef_interativo = self.sitef_dll["ContinuaFuncaoSiTefInterativo"]
        self.finaliza_sitef_interativo = self.sitef_dll["FinalizaFuncaoSiTefInterativo"]

        self.service_finder = SitefServiceFinder(self.timeout, config.find_values("IP") or [], self.id_loja,
                                                 config.find_value("PosId"), self.logger, dll_path)

        self.current_ip = None
        self.current_order = None

        self.cancel = False
        self.cancel_lock = Lock()

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

    def connection_status(self, test_connection_data):
        return self.service_finder.process(test_connection_data.pos_id, test_connection_data.fiscal_date, test_connection_data.fiscal_time)

    def add_payment(self, start_payment_data, additional_params=""):
        self.service_finder.lock_sitef()
        try:
            result = self._process_loop(start_payment_data, additional_params)
        finally:
            self.service_finder.release_sitef()
        self.dialog_manager.close_dialogs(start_payment_data.pos_id)
        return result

    def cancel_payment(self):
        with self.cancel_lock:
            self.cancel = True
        return ""

    def finish_payment(self, finish_payment_data):
        self.service_finder.lock_sitef()
        self.logger.debug("SiTEF finish payment started")
        try:
            if not self.current_order:
                self.logger.debug("No order to finish, ignoring...")
                return
            order_id = finish_payment_data.order_id
            pos_id = finish_payment_data.pos_id

            if self.current_order != order_id:
                self.logger.error("order to finish = {}, current order = {}".format(order_id, self.current_order))
                return

            self.logger.debug("will finish with data: {}".format(finish_payment_data))

            self.configura_sitef_interativo(str(self.current_ip), str(self.id_loja), _get_terminal_id(pos_id), 0)

            status = int(finish_payment_data.status)
            fiscal_date = str(finish_payment_data.fiscal_date)
            fiscal_time = str(finish_payment_data.fiscal_time)
            order_identifier = "{}{}".format(pos_id, order_id)
            self.finaliza_sitef_interativo(status, order_identifier, fiscal_date, fiscal_time, None)

            self.current_ip = self.current_order = None
            self.logger.debug("SiTEF finish payment ended")
            return

        finally:
            self.service_finder.release_sitef()

    def _check_finalized_last_order(self, pos_id):
        for _ in xrange(0, 3, 1):
            if self.current_ip and self.current_order:
                order_id = self.current_order
                order = self._get_order_picture(pos_id, order_id)
                if not order:
                    continue
                order = eTree.XML(order).find("Order")
                created_at = order.get("createdAt").replace("-", "").replace(":", "")
                data_fiscal = created_at[:8]
                hora_fiscal = created_at[9:15]
                if order.find("StateHistory/State[@state='PAID']") is not None:
                    status = "1"
                elif order.find("StateHistory/State[@state='VOIDED']") is not None:
                    status = "0"
                else:
                    self.logger.error("Order %s sem status PAID ou VOIDED" % order_id)
                    return
                self.finish_payment(FinishPaymentData(pos_id, order_id, data_fiscal, hora_fiscal, status))
            else:
                return
        else:
            self.logger.warning("Impossivel finalizar order %s" % self.current_order)
        self.current_order = self.current_ip = None

    def _get_order_picture(self, pos_id, order_id):
        try:
            pos_id_to_search = pos_id
            if order_id not in ('', None, 0):
                pos_id_to_search = ''
            params = (pos_id_to_search, order_id, 'false')
            reqbuf = '\x00'.join((str(p) for p in params))
            msg = self.message_bus.send_message("ORDERMGR%s" % pos_id, Message(TK_SLCTRL_OMGR_ORDERPICT, DataType.param, reqbuf))
            params = msg.data.split('\x00')
            if msg.token == DefaultToken.TK_SYS_NAK:
                return None
            return params[2]
        except Exception: # noqa
            self.logger.exception("Exception trying to get last order picture")
            return None

    def get_name(self):
        return "Sitef"

    def _check_pin_pad(self, pos_id):
        self.dialog_manager.show_message(pos_id, "Conectando PinPad")
        time_limit = (time.time() + 5.0)
        while (time.time() < time_limit) and (not self.sitef_dll["VerificaPresencaPinPad"]()):
            time.sleep(0.2)
        self.dialog_manager.close_dialogs(pos_id)

    def _process_loop(self, sitef_loop_data, additional_params=""):
        # type: (StartPaymentData, str) -> PaymentResponse
        self.logger.debug("SiTEF loop started")
        self.cancel = False

        pos_id = sitef_loop_data.pos_id
        terminal_id = _get_terminal_id(pos_id)
        order_id = sitef_loop_data.order_id

        self._check_pin_pad(pos_id)

        ip_sitef = None
        if self.current_order == order_id:
            ip_sitef = self.current_ip
        else:
            ip_sitef = self.service_finder.find_sitef_service()
            self._check_finalized_last_order(pos_id)

        if not ip_sitef:
            self.service_finder.wakeup_search()
            self.logger.warning("No IP available")
            return PaymentResponseMessage("Sem conexao SiTEF")

        api_ret = self.configura_sitef_interativo(str(ip_sitef), str(self.id_loja), terminal_id, 0)
        if api_ret != API_INIT_OK:
            self.service_finder.sitef_unavailable(ip_sitef)
            self._log_api_config_ret_error(api_ret)
            self.logger.warning("IP {} not available".format(ip_sitef))
            return PaymentResponseMessage("Servidor SiTEF Inoperante")

        amount = sitef_loop_data.amount

        tender_type = sitef_loop_data.tender_type
        order_identifier = "{}{}".format(pos_id, order_id)
        fiscal_date = str(sitef_loop_data.fiscal_date)
        fiscal_time = str(sitef_loop_data.fiscal_time)
        operator = str(sitef_loop_data.operator)
        disabled_payments = "[10;17;18;19;27;28;29;34;35;36;42;]"
        api_ret = self.inicia_sitef_interativo(tender_type, amount, order_identifier, fiscal_date, fiscal_time,
                                               operator, "{};{}".format(disabled_payments, additional_params))

        if api_ret != API_LOOP_OK:
            self._log_api_start_ret_error(api_ret)
            return PaymentResponseMessage("Pagamento interrompido")

        self.current_order = order_id
        self.current_ip = ip_sitef

        command = c_long(0)
        field_type = c_long(0)
        min_size = c_long(0)
        max_size = c_long(0)
        buffer_size = 32768
        output_buffer = create_string_buffer('\000' * buffer_size)

        voided = False

        institution_name = None
        card_bin = None
        transaction_date = None
        auth_code = None
        authorizer_name = None
        authorizer_cnpj = None
        card_type = None
        expiration_date = None
        nsu = None
        nsu_host = None
        transaction_processor = None
        receipt_merchant = None
        receipt_customer = None
        id_mercado_pago = None
        menu_title = ""

        while api_ret == API_LOOP_OK:
            keep_going = 0
            with self.cancel_lock:
                if self.cancel:
                    keep_going = -1 if tender_type == 122 else 2

            api_ret = self.continua_sitef_interativo(addressof(command), addressof(field_type), addressof(min_size),
                                                     addressof(max_size), output_buffer, buffer_size, keep_going)
            if api_ret != API_LOOP_OK:
                break

            if not _api_has_data(command):
                if command.value in (SitefActions.SHOW_OPERATOR_MESSAGE, SitefActions.SHOW_OPERATOR_AND_CUSTOMER_MESSAGE):
                    self.dialog_manager.show_message(pos_id, output_buffer.value)

                elif command.value in (SitefActions.CLEAN_OPERATOR_MESSAGE, SitefActions.CLEAN_CUSTOMER_MESSAGE, SitefActions.CLEAN_OPERATOR_AND_CUSTOMER_MESSAGE):
                    self.dialog_manager.close_dialogs(pos_id)

                elif command.value == SitefActions.SHOW_MESSAGE_WITH_CONFIRMATION:
                    self.dialog_manager.show_question(pos_id, output_buffer.value, buttons="$OK")

                elif command.value == SitefActions.SHOW_MESSAGE_WITH_CANCEL_BUTTON:
                    self.dialog_manager.show_message(pos_id, output_buffer.value)

                elif command.value == SitefActions.ASK_PIN_CODE:
                    pin = self.dialog_manager.show_input(pos_id, output_buffer.value, input_type="number")
                    if pin == "CANCELA":
                        self.cancel_payment()
                        output_buffer.value = "0" * max_size.value

                    else:
                        output_buffer.value = str(pin)

                elif command.value == SitefActions.CANCEL_OPERATION:
                    output_buffer.value = "0"
                    time.sleep(1.5)
                    if field_type.value != 111:
                        voided = True

                elif command.value == SitefActions.MENU_TITLE:
                    menu_title = output_buffer.value

                elif command.value == SitefActions.MENU_CONTENT:
                    choices = {option.split(':')[0]: option.split(':')[1] for option in output_buffer.value.split(';') if option}
                    selected_choice = self.dialog_manager.show_choices(pos_id, choices, message=menu_title)
                    output_buffer.value = str(selected_choice)

                elif _should_ignore_command(command):
                    time.sleep(0.5)

                else:
                    self.dialog_manager.show_message(pos_id, "Mensagem {} {}".format(command.value, output_buffer.value))

            if field_type.value == SitefParams.CARD_BIN:
                card_bin = output_buffer.value

            elif field_type.value == SitefParams.TRANSACTION_PROCESSOR:
                transaction_processor = output_buffer.value

            elif field_type.value == SitefParams.CARD_TYPE:
                card_type = output_buffer.value

            elif field_type.value in SitefParams.AUTH_CODE:
                auth_code = output_buffer.value

            elif field_type.value == SitefParams.QR_CODE:
                self.dialog_manager.show_barcode(pos_id, output_buffer.value)

            elif field_type.value == SitefParams.RECEIPT_CUSTOMER:
                receipt_customer = output_buffer.value

            elif field_type.value == SitefParams.RECEIPT_MERCHANT:
                receipt_merchant = output_buffer.value

            elif field_type.value == SitefParams.NSU:
                nsu = output_buffer.value

            elif field_type.value == SitefParams.NSU_HOST:
                nsu_host = output_buffer.value

            elif field_type.value == SitefParams.TRANSACTION_DATE:
                transaction_date = output_buffer.value

            elif field_type.value == SitefParams.AUTHORIZER_NAME:
                authorizer_name = output_buffer.value

            elif field_type.value == SitefParams.INSTITUTION_NAME:
                institution_name = output_buffer.value

            elif field_type.value == SitefParams.EXPIRATION_DATE:
                expiration_date = output_buffer.value

            elif field_type.value == SitefParams.AUTHORIZER_CNPJ:
                authorizer_cnpj = output_buffer.value

            elif field_type.value == SitefParams.ID_MERCADO_PAGO:
                id_mercado_pago = output_buffer.value

            elif field_type.value == SitefParams.DIGITAL_WALLET_ID:
                card_type = output_buffer.value

        if api_ret == API_LOOP_FINISHED:
            self.logger.debug("SiTEF loop finished")
            return PaymentResponseData(
                media=institution_name,
                result=api_ret,
                card_number=card_bin,
                auth_code="{}{}".format(transaction_date, id_mercado_pago) if tender_type == 122 else auth_code,
                approved_amount=amount,
                id_auth=authorizer_name,
                cnpj_auth=authorizer_cnpj,
                bandeira=card_type,
                receipt_merchant=base64.b64encode(receipt_merchant or receipt_customer),
                receipt_customer=base64.b64encode(receipt_customer),
                expiration_date=expiration_date,
                nsu=nsu,
                transaction_processor=transaction_processor,
                nsu_host=nsu_host)

        else:
            if voided:
                return PaymentResponseMessage("Cancelado")

            if api_ret == API_NO_SITEF_COMMUNICATION:
                self.service_finder.sitef_unavailable(ip_sitef)

            return _format_api_error_response(api_ret)

    def _log_api_config_ret_error(self, api_ret):
        error_codes = {
            0: "No Error",
            1: "IP Error",
            2: "Store Code Error",
            3: "Terminal Code Error",
            6: "TCP/IP Error",
            7: "Memory Error",
            8: "CliSiTef Error",
            10: "CliSiTef Access Error"
        }
        self.logger.error("API Config Error: {}".format(error_codes.get(api_ret, "Unknown")))

    def _log_api_start_ret_error(self, api_ret):
        self.logger.warning("API Start Error: {}".format(api_ret))
