import base64
import logging
import os
import sqlite3
import shutil
import time
from datetime import datetime
from sqlite3 import Connection
from threading import Thread
from xml.etree import cElementTree as eTree

import msgbus
import persistence
from persistence import (
    AprDbdException,
    Driver,
)
from typing import List, Tuple, Optional, Union
from old_helper import OrderTaker

from services import OrderService
from models import ErrorType

logger = logging.getLogger("FiscalWrapper")


class FiscalDataRepository:
    def __init__(
        self,
        mbcontext,              # type: msgbus.MBEasyContext
        database_dir="",        # type: Optional[str]
        schema_sql_path="",     # type: Optional[str]
    ):
        # type: (...) -> FiscalDataRepository

        self.mbcontext = mbcontext
        self.conn = None  # type: Optional[persistence.Connection]
        self.order_service = OrderService()
        self.database_dir = database_dir
        self.schema_sql_path = schema_sql_path

    def __enter__(self):
        self.conn = Driver().open(self.mbcontext, service_name="FiscalPersistence")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.conn:
            self.conn.close()

    def start_save_fiscal_data_thread(self, pos_id, order, xml_request, numero_sat, sent_to_nfce, xml_response):
        self.set_fiscal_xml_custom_property(pos_id, xml_request, order.get("orderId", ""))
        save_fiscal_data_thread = Thread(target=self.save_fiscal_data(pos_id, order, xml_request, numero_sat, sent_to_nfce, xml_response))
        save_fiscal_data_thread.daemon = True
        save_fiscal_data_thread.start()

    def set_fiscal_xml_custom_property(
        self,               # type: FiscalDataRepository
        pos_id,             # type: str
        xml_request,        # type: str
        order_id='',        # type: str
        key='FISCAL_XML',   # type: str
        blk_notify=True,    # type: bool
    ):
        # type: (...) -> None

        self.order_service.set_custom_property(
            pos_id=pos_id,
            key=key,
            value=xml_request,
            order_id=order_id,
            blk_notify=blk_notify,
        )

    def notify_pump(
            self,
            pos_id='',                      # type: str
            order_id='',                    # type: str
            xml_format='CancelledOrder',    # type: str
            order_picture=None              # type: Union[None, str]
    ):
        # type: (...) -> None

        try:
            event_type = "ORDER_REPROCESS"
            if not order_picture:
                order_picture = OrderTaker().get_order_picture(pos_id, order_id)

            xml = "<{}>{}</{}>".format(xml_format, order_picture, xml_format)
            self.mbcontext.MB_EasyEvtSend(event_type, event_type, xml)
        except Exception as _:
            logger.exception("Error notifying bohbump on POS ID: {} for ORDER ID: {}".format(pos_id, order_id))

    def save_fiscal_data(self, posid, order, xml_request, numero_sat, sent_to_nfce, xml_response):
        retries = 0
        while retries < 3:
            try:
                xml = base64.b64decode(xml_request)
                parsed_xml = eTree.XML(xml)

                from nfce import NfceRequestBuilder
                numero_nota_element = parsed_xml.find("infCFe/ide/nCFe")
                if numero_nota_element is None:
                    # TODO: Obter o numero da nota via parametro e remover esta logica daqui
                    numero_nota_element = parsed_xml.find(
                        "{{{0}}}NFe/{{{0}}}infNFe/{{{0}}}ide/{{{0}}}nNF".format(
                            NfceRequestBuilder.NAMESPACE_NFE))
                    if numero_nota_element is None:
                        numero_nota_element = parsed_xml.find(
                            "{{{0}}}infNFe/{{{0}}}ide/{{{0}}}nNF".format(
                                NfceRequestBuilder.NAMESPACE_NFE))

                numero_nota = numero_nota_element.text

                data_nota_element = parsed_xml.find("infCFe/ide/dEmi")
                hora_nota_element = parsed_xml.find("infCFe/ide/hEmi")
                if data_nota_element is None:
                    dh_elment = parsed_xml.find(
                        "{{{0}}}NFe/{{{0}}}infNFe/{{{0}}}ide/{{{0}}}dhEmi".format(
                            NfceRequestBuilder.NAMESPACE_NFE))
                    if dh_elment is None:
                        dh_elment = parsed_xml.find(
                            "{{{0}}}infNFe/{{{0}}}ide/{{{0}}}dhEmi".format(
                                NfceRequestBuilder.NAMESPACE_NFE))

                    data_nota_str = dh_elment.text

                    data_emissao_date_utc = data_nota_str[:19]
                    data_nota_date = datetime.strptime(data_emissao_date_utc, "%Y-%m-%dT%H:%M:%S")
                else:
                    data_nota_str = data_nota_element.text
                    hora_nota_str = hora_nota_element.text
                    data_nota_date = datetime.strptime(data_nota_str + hora_nota_str, "%Y%m%d%H%M%S")

                data_nota_seconds = int(time.mktime(data_nota_date.timetuple()))

                params_fiscal = {
                    "pos_id": posid,
                    "order_id": order.get("orderId"),
                    "data_nota": data_nota_seconds,
                    "numero_nota": numero_nota,
                    "numero_sat": numero_sat,
                    "xml_request": xml_request,
                    "sent_to_nfce": sent_to_nfce,
                    "xml_response": xml_response
                }

                self.conn.pquery("fiscal_saveFiscalData", **params_fiscal)
                break
            except Exception as _:
                logger.exception("Error saving fiscal data on database. RetryNum: {}".format(retries))
                time.sleep(0.2)
                retries += 1
        else:
            logger.error(">>> Save fiscal data failed after maximum retries!")

    def set_order_picture(self, order_id, order_picture):
        # type: (str, str) -> None
        params = {
            "order_id": order_id,
            "order_picture": order_picture
        }
        self.conn.pquery("fiscal_setOrderPicture", **params)

    def set_order_canceled(self, order_id):
        # type: (str) -> None
        params = {
            "order_id": order_id,
        }
        self.conn.pquery("fiscal_setOrderCanceled", **params)

    def _create_tables_fiscals(
        self,
        connection,     # type: Connection
    ):
        # type: (...) -> bool

        try:
            with open(self.schema_sql_path, "r") as f:
                query = f.read()

            if query:
                connection.executescript(query)

            return True
        except Exception as ex:
            logger.error("Error filling new_fiscal_persistcomp.db: {}".format(ex.message))
            return False

    def _backup_current_fiscal(
        self,
    ):
        # type: (...) -> bool

        try:
            logger.info("Creating backup database for fiscal_persistcomp.db")

            new_name = os.path.join(self.database_dir, "new_fiscal_persistcomp.db")
            connection = sqlite3.connect(database=new_name)

            filled_tables = self._create_tables_fiscals(connection=connection)
            if not filled_tables:
                os.remove(path=new_name)
                return False

            old_name = os.path.join(self.database_dir, "fiscal_persistcomp.db")
            time_format = datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
            backup_name = os.path.join(self.database_dir, "fiscal_persistcomp.db_automatic_bkp_{}".format(time_format))
            success = False
            try:
                shutil.move(
                    src=old_name,
                    dst=backup_name,
                )
                success = True
            except WindowsError as ex:
                logger.error(ex.message)
                if os.path.exists(path=backup_name):
                    success = True

            if not success:
                return False

            try:
                shutil.move(
                    src=new_name,
                    dst=old_name,
                )
            except WindowsError as ex:
                logger.error(ex.message)
                pass

            if os.path.exists(path=new_name):
                connection.close()
                os.remove(path=new_name)

            return True

        except Exception as ex:
            logger.error("Error at backup of fiscal_persistcomp.db: {}".format(ex.message))
            return False

    def get_nfce_orders_to_send(
        self,
        limit,              # type: int
        first_try=True,     # type: Optional[bool]
    ):
        # type: (...) -> List

        try:
            params_fiscal = {
                "limit": limit
            }
            orders = [
                Order(
                    order_id=x.get_entry(0),
                    pos_id=x.get_entry(1),
                    xml=x.get_entry(2),
                    num_nota=x.get_entry(3),
                    order_picture=x.get_entry(4),
                )
                for x in self.conn.pselect("fiscal_getNfceToSend", **params_fiscal)
            ]
            return orders
        except AprDbdException as ex:
            if ex.errorcode == ErrorType.MALFORMED_DATABASE_DISK_IMAGE and first_try:
                logger.error("Error processing procedure fiscal_getNfceToSend: {}.".format(ex.message))

                backup_created = self._backup_current_fiscal()
                if not backup_created:
                    raise ex

                return self.get_nfce_orders_to_send(
                    limit=limit,
                    first_try=False,
                )
            raise ex

    def get_count_nfce_orders_to_send(self):
        # type: () -> int
        return [int(x.get_entry(0)) for x in self.conn.pselect("fiscal_getNfceToSendCount")][0]

    def set_nfce_sent(self, order_id, status):
        # type: (str, int) -> None
        params_fiscal = {
            "status": status,
            "order_id": order_id
        }
        self.conn.pquery("fiscal_setNfceSent", **params_fiscal)

    def set_xml_response(self, order_id, xml_base64):
        # type: (str, str) -> None
        params_fiscal = {
            "order_id": order_id,
            "xml_response": xml_base64
        }
        self.conn.pquery("fiscal_setXmlResponse", **params_fiscal)

    def set_nfce_sent_with_xml(self, order_id, xml_base64, status):
        # type: (str, str, int) -> None
        params_fiscal = {
            "status": status,
            "xml_request": xml_base64,
            "order_id": order_id
        }
        self.conn.pquery("fiscal_setNfceSentXml", **params_fiscal)

    def set_nfce_sent_with_xml_bkoffice(self, order_id, xml_base64, status, bkoffice_status):
        # type: (str, str, int, int) -> None
        params_fiscal = {
            "status": status,
            "xml_request": xml_base64,
            "order_id": order_id,
            "sent_to_bkoffice": bkoffice_status
        }
        self.conn.pquery("fiscal_setNfceSentXmlBKOffice", **params_fiscal)

    def retry_fiscal_orders_with_exception(self, range_of_days):
        # type: (str) -> None
        params_fiscal = {
            "range_of_days": "-{} days".format(range_of_days),
        }
        self.conn.pquery("fiscal_retryFiscalOrdersWithException", **params_fiscal)

    def get_xml_request(self, order_id):
        # type: (str) -> str
        params_fiscal = {
            "order_id": order_id
        }
        cursor = self.conn.pselect("fiscal_getRequestXMLFiscalData", **params_fiscal)
        if cursor.rows() > 0:
            ret = [x.get_entry(0) for x in cursor][0]
            return ret

    def get_xml_response(self, order_id):
        # type: (str) -> str
        params_fiscal = {
            "order_id": order_id
        }
        cursor = self.conn.pselect("fiscal_getResponseXMLFiscalData", **params_fiscal)
        if cursor.rows() > 0:
            ret = [x.get_entry(0) for x in cursor][0]
            return ret

    def get_numero_sat_and_xml_request(self, order_id):
        # type: (str) -> Tuple
        params_fiscal = {
            "order_id": order_id
        }
        cursor = self.conn.pselect("fiscal_getSatNumberAndRequestXMLFiscalData", **params_fiscal)
        if cursor.rows() > 0:
            ret = [(x.get_entry(0), x.get_entry(1)) for x in cursor][0]
            return ret


class Order:
    def __init__(self, order_id, pos_id, xml, num_nota, order_picture):
        self.order_id = order_id
        self.pos_id = pos_id
        self.xml = xml
        self.num_nota = num_nota
        self.order_picture = order_picture


class Tender(object):
    def __init__(self):
        # type: () -> Tender
        self.tender_seq_id = ""
        self.type = ""
        self.amount = ""
        self.change = ""
        self.bandeira = ""
        self.auth_code = ""
        self.cnpj_auth = ""
        self.receipt_customer = ""
        self.receipt_merchant = ""
