# -*- coding: utf-8 -*-
import os
import sys
import base64
import time

from comp_exceptions import (
    OrderWaitingProcessingException,
    ErrorProcessing,
)
from fiscalpersistence import Tender
from typing import Optional, Dict, List

import msgbus
import six
import logging
import sysactions

from sysactions import set_custom
from fiscalinterface import FiscalProcessor
from xml.etree import cElementTree as eTree
from threading import Thread, Condition
from msgbus import MBException, FM_STRING, FM_PARAM, TK_SYS_ACK, MBEasyContext
from syserrors import SE_NOTFOUND, SE_MB_CONNECT
from systools import sys_log_exception
from bustoken import (
    TK_SAT_PROCESS_REQUEST,
    TK_SAT_STATUS_REQUEST,
    TK_SAT_CANCEL_SALE,
)
from old_helper import convert_from_localtime_to_utc
from pos_model import OrderParser
from satbuilder import CfeBuilder
from datetime import datetime
from common import FiscalParameterController
from pos_util import SaleLineUtil

logger = logging.getLogger("FiscalWrapper")


class SatRequestBuilder:

    def __init__(
        self,                           # type: SatRequestBuilder
        mbcontext,                      # type: MBEasyContext
        crt,                            # type: int
        cnpj_sw,                        # type: str
        sign_ac,                        # type: str
        cnpj_contribuinte,              # type: str
        inscr_estadual,                 # type: str
        order_parser,                   # type: OrderParser
        fiscal_parameter_controller,    # type: FiscalParameterController
        sale_line_util,                 # type: SaleLineUtil
        cfe_builder,                    # type: CfeBuilder
        is_mfe,                         # type: bool
    ):
        # type: (...) -> None

        self.crt = crt
        self.mbcontext = mbcontext
        self.cnpj_sw = cnpj_sw
        self.sign_ac = sign_ac
        self.cnpj_contribuinte = cnpj_contribuinte
        self.inscr_estadual = inscr_estadual
        self.order_parser = order_parser
        self.fiscal_parameter_controller = fiscal_parameter_controller
        self.sale_line_util = sale_line_util
        self.cfe_builder = cfe_builder
        self.is_mfe = is_mfe

    def get_sat_info(self):
        return self.cnpj_sw, self.sign_ac

    def build_request(self, order_xml):
        # type: (eTree) -> unicode
        order = self.order_parser.parse_order(order_xml)

        context = {}
        xml = self.cfe_builder.build_xml(order, context)
        return xml


class SatService:
    def __init__(self, service_name):
        self.service_name = service_name
        self.available = True


class SatServiceFinder:
    def __init__(self, mbcontext, max_sat_number, pos_sat_list, sleep_time):
        # type: (MBEasyContext, int, str, int) -> None
        self.mbcontext = mbcontext

        self.max_sat_number = max_sat_number
        self.pos_sat_list = pos_sat_list.split(';') if pos_sat_list else []
        self.sleep_time = sleep_time

        self.search_thread = None  # type: Optional[Thread]
        self.search_thread_condition = Condition()
        self.search_thread_running = False
        self.available_sats = {}  # type: Dict[str, SatService]
        self.available_sats_keys = []
        self.next_sat_index = 0
        self.available_sats_lock = Condition()
        self.processing_orders_id = []  # type: List[str]

        self.service_name_initial = "SAT"

        self.search_sat_modules()
        self.start_search_thread()

    def find_and_lock_sat_service(
        self,      # type: SatServiceFinder
        order_id,  # type: str
    ):
        # type: (...) -> str

        logger.debug("Find and_lock_sat_service for OrderId: {}".format(order_id))

        with self.available_sats_lock:
            if not self.available_sats:
                with self.search_thread_condition:
                    self.search_thread_condition.notify()
                raise Exception("Nenhum SAT encontrado")

            initial_sat_index = self.next_sat_index

            try:
                sat_service_name = self.available_sats_keys[self.next_sat_index]
            except IndexError:
                log_message = "Erro ao acessar SAT com indice {}. Sats disponiveis: {}"
                logger.exception(log_message.format(self.next_sat_index, len(self.available_sats_keys)))
                self.next_sat_index = 0
                initial_sat_index = len(self.available_sats_keys)
                sat_service_name = self.available_sats_keys[self.next_sat_index]
            else:
                self.next_sat_index += 1
                if self.next_sat_index >= len(self.available_sats):
                    self.next_sat_index = 0

            time_limit = (time.time() + 95)
            while not self.available_sats[sat_service_name].available:
                sat_service_name = self.available_sats_keys[self.next_sat_index]
                self.next_sat_index += 1
                if self.next_sat_index >= len(self.available_sats):
                    self.next_sat_index = 0

                if self.next_sat_index == initial_sat_index:
                    self.available_sats_lock.wait(timeout=5)
                    if time.time() > time_limit:
                        logger.warning("Reached time limit while finding an available SAT")
                        raise Exception("Nenhum SAT encontrado")

                    initial_sat_index = self.next_sat_index
                    sat_service_name = self.available_sats_keys[self.next_sat_index]

            self.available_sats[sat_service_name].available = False
            return sat_service_name

    def unlock_sat_service(self, sat_service_name):
        # type: (str) -> None
        with self.available_sats_lock:
            if sat_service_name in self.available_sats:
                self.available_sats[sat_service_name].available = True

                # Como liberamos um SAT, vamos notificar para caso alguem esteja esperando
                self.available_sats_lock.notify()

    def sat_unavailable(self, sat_service_name):
        # type: (str) -> None
        if len(self.available_sats) == 1:
            return
        with self.available_sats_lock:
            if sat_service_name in self.available_sats:
                del self.available_sats[sat_service_name]
                self.available_sats_keys.remove(sat_service_name)
                posid = int(sat_service_name[3:])
                set_custom(posid, 'SAT_STATUS', '2')

                # Removemos um SAT - Vamos arrumar os indices
                self._fix_sat_index()

    def set_order_processing(
        self,       # type: SatServiceFinder
        order_id,   # type: str
    ):
        # type: (...) -> None

        with self.available_sats_lock:
            self.processing_orders_id.append(order_id)

    def is_order_waiting_processing(
        self,       # type: SatServiceFinder
        order_id,   # type: str
    ):
        # type: (...) -> bool

        with self.available_sats_lock:
            return order_id in self.processing_orders_id

    def remove_order_processing(
        self,       # type: SatServiceFinder
        order_id,   # type: str
    ):
        # type: (...) -> None

        with self.available_sats_lock:
            if order_id in self.processing_orders_id:
                logger.debug("Remove order processing - OrderId: {}".format(order_id))
                self.processing_orders_id.remove(order_id)

    def search_sat_modules(self):
        logger.debug("Start Search SAT Function")
        sat_list_to_search = self.pos_sat_list if self.pos_sat_list else range(0, self.max_sat_number + 1)

        now_available = {}
        for i in sat_list_to_search:
            sat_service_name = "SAT%s" % str(i).zfill(2)

            try:
                ret = self.mbcontext.MB_EasySendMessage(
                    dest_name=sat_service_name,
                    token=TK_SAT_STATUS_REQUEST,
                    format=FM_PARAM,
                    timeout=100 * 100000,
                )
                if ret.data == "OK":
                    now_available[sat_service_name] = SatService(sat_service_name)
                    try:
                        set_custom(i, 'SAT_STATUS', '1') if i != 0 else None
                    except Exception:
                        pass
                else:
                    if ret.data != "No SAT attached":
                        logger.warning("{0} not available: {1}".format(sat_service_name, ret.data))
                    try:
                        set_custom(i, 'SAT_STATUS', '2') if i != 0 else None
                    except Exception:
                        pass
            except MBException as e:
                try:
                    set_custom(i, 'SAT_STATUS', '3') if i != 0 else None
                except Exception:
                    pass
                if e.errorcode not in [SE_NOTFOUND, SE_MB_CONNECT]:
                    logger.exception(e.message)

        with self.available_sats_lock:
            # Primeiro removemos os que estavam disponiveis e agora nao estao mais
            sats_to_remove = []
            for sat_service_name in self.available_sats:  # type: str
                if sat_service_name not in now_available:
                    sats_to_remove.append(sat_service_name)

            for sat_service_name in sats_to_remove:
                del self.available_sats[sat_service_name]
                self.available_sats_keys.remove(sat_service_name)

            # Agora adicionamos os que passaram a estar disponiveis
            sats_novos = False
            for sat_service_name in now_available:  # type: str
                if sat_service_name not in self.available_sats:
                    sat = now_available[sat_service_name]  # type: SatService

                    self.available_sats[sat.service_name] = sat
                    self.available_sats_keys.append(sat.service_name)

                    sats_novos = True

            # Removemos um SAT - Vamos arrumar os indices
            self._fix_sat_index()

            if sats_novos:
                # Como temos SATs novos disponiveis, vamos avisar caso algume thread esteja esperando
                self.available_sats_lock.notify()
        logger.debug("End Search SAT Function")

    def start_search_thread(self):
        self.search_thread_running = True
        self.search_thread = Thread(target=self._search_thread_loop)
        self.search_thread.daemon = True
        self.search_thread.start()

    def stop_search_thread(self):
        if self.search_thread_running:
            with self.search_thread_condition:
                self.search_thread_running = False
                self.search_thread_condition.notifyAll()

    def _search_thread_loop(self):
        while self.search_thread_running:
            with self.search_thread_condition:
                if not self.available_sats:
                    self.search_thread_condition.wait(15)
                else:
                    self.search_thread_condition.wait(self.sleep_time)
            self.search_sat_modules()

    def _fix_sat_index(self):
        # Atualizamos o indice, pois houve alguma remoção de SATs
        if self.next_sat_index >= len(self.available_sats):
            self.next_sat_index = len(self.available_sats) - 1

            # Não permirte que o índice fique negativo
            if self.next_sat_index < 0:
                self.next_sat_index = 0


class MfeServiceFinder(SatServiceFinder):
    def find_and_lock_sat_service(self, order_id):
        # type: (str) -> str
        with self.available_sats_lock:
            if not self.available_sats:
                raise Exception("Nenhum SAT encontrado")

            sat_service_name = self.available_sats_keys[int(order_id) % len(self.available_sats_keys)]

            while not self.available_sats[sat_service_name].available:
                self.available_sats_lock.wait()

            self.available_sats[sat_service_name].available = False
            return sat_service_name


class SatProcessor(FiscalProcessor):
    def __init__(self, mbcontext, sat_service_finder, sat_request_builder, fiscal_sent_dir):
        # type: (msgbus.MBEasyContext, SatServiceFinder, SatRequestBuilder, str) -> SatProcessor
        super(SatProcessor, self).__init__()

        self.mbcontext = mbcontext
        self.sat_service_finder = sat_service_finder
        self.sat_request_builder = sat_request_builder
        self.last_sat_used = {}
        self.fiscal_sent_dir = fiscal_sent_dir
        sysactions.mbcontext = self.mbcontext
        self.cancel_data = None

    def get_sat_info(self):
        return self.sat_request_builder.get_sat_info()

    def request_fiscal(
            self,               # type: SatProcessor
            pos_id,             # type: str
            order,              # type: eTree
            tenders,            # type: List[Tender]
            paf=False,          # type: Optional[bool]
            fiscal_id=None      # type: str
    ):
        # type: (...) -> eTree.Element

        order_id = order.get("orderId")
        order_waiting_processing = self.sat_service_finder.is_order_waiting_processing(
            order_id=order_id,
        )
        if order_waiting_processing:
            log_message = "Order waiting processing PosId: {} - OrderId: {}"
            logger.debug(log_message.format(pos_id, order_id))
            raise OrderWaitingProcessingException()

        sat_service_name = None
        try:
            log_message = "Set order processing PosId: {} - OrderId: {}"
            logger.debug(log_message.format(pos_id, order_id))

            self.sat_service_finder.set_order_processing(
                order_id=order_id,
            )

            log_message = "Generating SAT xml request PosId: {} - OrderId: {}"
            logger.debug(log_message.format(pos_id, order_id))

            request = self.sat_request_builder.build_request(
                order_xml=order,
            )

            sat_service_name = self.sat_service_finder.find_and_lock_sat_service(
                order_id=order_id,
            )

            self.last_sat_used[pos_id] = sat_service_name

            dir_sent = self._get_sent_directory()

            log_message = "Sent SAT xml processing PosId: {} - OrderId: {}"
            logger.debug(log_message.format(pos_id, order.get("orderId")))

            request_data = "\0".join(
                [
                    pos_id,
                    order.attrib["orderId"],
                    request.encode("utf-8"),
                    fiscal_id,
                ]
            )
            msg = self.mbcontext.MB_EasySendMessage(
                dest_name=sat_service_name,
                token=TK_SAT_PROCESS_REQUEST,
                format=FM_STRING,
                data=request_data,
            )

            if msg.token == TK_SYS_ACK:
                ret_xml = eTree.XML(msg.data)
                self._process_return_fiscalization(
                    pos_id=pos_id,
                    order_id=order_id,
                    dir_sent=dir_sent,
                    ret_xml=ret_xml,
                    sat_service_name=sat_service_name,
                )
                return ret_xml

            logger.debug("Fail processing SAT xml PosId: {} - OrderId: {}".format(pos_id, order_id))
            self._save_invoice_error_processing(
                pos_id=pos_id,
                order_id=order_id,
                request=request,
                sat_service_name=sat_service_name,
            )
            raise ErrorProcessing(msg.data)

        except Exception as e:
            sys_log_exception(e.message)
            if sat_service_name:
                self.sat_service_finder.sat_unavailable(
                    sat_service_name=sat_service_name,
                )
            raise six.reraise(Exception, e, sys.exc_info()[2])
        finally:
            self.sat_service_finder.remove_order_processing(
                order_id=order_id,
            )

    def check_nf_situation(
        self,           # type: SatProcessor
        initial_date,   # type: str
        final_date,     # type: str
    ):
        # type: (...) -> (bool, str)

        return False, "Funcionalidade exclusiva para o modo NFCe"

    def do_validation(self, get_days_to_expiration=None):
        return True, "OK"

    def get_fiscal_sent_dir(self):
        return self.fiscal_sent_dir

    def terminate(self):
        self.sat_service_finder.stop_search_thread()

    def cancel_sale(
        self,
        order_data,     # type: str
        order_id,       # type: str
    ):
        # type: (...) -> Dict

        self.cancel_data = None
        sat_service = "SAT" + order_data[0]
        cancel_data = self.build_cancel_xml(order_data[1])
        msg = self.mbcontext.MB_EasySendMessage(
            dest_name=sat_service,
            token=TK_SAT_CANCEL_SALE,
            format=FM_PARAM,
            timeout=100 * 100000,
            data=str(cancel_data),
        )
        if msg.token == TK_SYS_ACK:
            self.cancel_data = msg.data
            ret_xml = eTree.XML(msg.data)
            element = ret_xml.find("XmlResponse").text
            message = ret_xml.find("Message").text
            if element:
                padded = element + "=" * ((4 - len(element) % 4) % 4)
                base64_xml = base64.b64decode(padded)
                cfe = cancel_data[1]
                num_nota_cfe = cfe[34:40].zfill(9)
                idx_start = base64_xml.find("<dEmi>") + 6
                idx_final = base64_xml.find("</dEmi>")
                data_emissao = base64_xml[idx_start:idx_final]
                dir_arquivo = os.path.join(data_emissao[0:4], data_emissao[4:6], data_emissao[6:8])
                pos_id = sat_service[-2:]
                file_name = "S{}_{}_{}_cancelamento_{}.xml".format(pos_id, num_nota_cfe, order_id.zfill(9), cfe)
                file_path = os.path.join(self.fiscal_sent_dir, "Enviados", dir_arquivo, file_name)
                with open(file_path, "w+") as cfe_proc_file:
                    cfe_proc_file.write(base64_xml)

                return {'status': True, 'message': message, 'xml_response': padded}

            return {'status': True, 'message': message, 'xml_response': None}
        else:
            return {'status': False, 'message': msg.data, 'xml_response': None}

    def build_cancel_xml(self, order_request):
        sale_request_xml = eTree.XML(base64.b64decode(order_request + "==="))

        chcanc = sale_request_xml.find("infCFe").attrib.get("Id")
        cnpj = self.sat_request_builder.cnpj_sw
        signac = self.sat_request_builder.sign_ac
        posid = sale_request_xml.find("infCFe").find("ide").find("numeroCaixa").text

        cancel_xml = "<CFeCanc>" \
                     "<infCFe chCanc=\"{chcanc}\">" \
                     "<ide>" \
                     "<CNPJ>{cnpj}</CNPJ>" \
                     "<signAC>{signac}</signAC>" \
                     "<numeroCaixa>{posid}</numeroCaixa>" \
                     "</ide>" \
                     "<emit/>" \
                     "<dest/>" \
                     "<total/>" \
                     "</infCFe>" \
                     "</CFeCanc>" \
            .format(chcanc=chcanc, cnpj=cnpj, signac=signac, posid=posid)
        return cancel_xml, chcanc

    def _process_return_fiscalization(
        self,               # type: SatProcessor
        pos_id,             # type: str
        order_id,           # type: str
        dir_sent,           # type: str
        ret_xml,            # type: eTree.Element
        sat_service_name,   # type: str
    ):
        # type: (...) -> None

        element = ret_xml.find("XmlRequest").text
        padded = element + "=" * ((4 - len(element) % 4) % 4)

        logger.debug("PosId: {} - OrderId: {} - request size: {}".format(pos_id, order_id, len(padded)))
        base64_xml = base64.b64decode(padded)
        logger.debug("PosId: {} - OrderId: {} - base64_xml: {}".format(pos_id, order_id, base64_xml))

        req_xml = eTree.XML(base64_xml)
        date_emission = req_xml.find("infCFe/ide/dEmi").text
        time_emission = req_xml.find("infCFe/ide/hEmi").text

        dir_nota = self._get_invoice_directory(
            date_emission=date_emission,
            dir_sent=dir_sent,
        )

        num_nota_cfe_el = req_xml.find("infCFe/ide/nCFe")
        num_nota_cfe = num_nota_cfe_el.text.zfill(9) if num_nota_cfe_el is not None else "000000000"

        log_message = "Success processing SAT Xml - PosId: {} - OrderId: {} - FiscalId: {}"
        logger.debug(log_message.format(pos_id, order_id, num_nota_cfe))

        self._set_order_fiscalization_custom_properties(
            pos_id=pos_id,
            order_id=order_id,
            date_string=date_emission + time_emission,
            num_nota_cfe=num_nota_cfe,
        )

        self.sat_service_finder.unlock_sat_service(
            sat_service_name=sat_service_name,
        )

        cfe_code = ret_xml.find("FiscalData").find("satkey").text
        file_name = "S{0}_{1}_{2}_response_pos{3}_CFe{4}.xml".format(
            sat_service_name[-2:],
            num_nota_cfe,
            order_id.zfill(9),
            str(pos_id).zfill(2),
            cfe_code
        )

        response_file_dir = os.path.join(
            dir_nota,
            file_name,
        )

        log_message = "Salve SAT xml PosId: {} - OrderId: {} - response_file_dir: {}"
        logger.debug(log_message.format(pos_id, order_id, response_file_dir))
        with open(response_file_dir, "w+") as response_file:
            response_file.write(base64_xml)

    def _set_order_fiscalization_custom_properties(
        self,           # type: SatProcessor
        pos_id,         # type: str
        order_id,       # type: str
        date_string,    # type: str
        num_nota_cfe,   # type: str
    ):
        # type: (...) -> None

        local_time = datetime.strptime(
            date_string,
            "%Y%m%d%H%M%S",
        )
        emission_date = convert_from_localtime_to_utc(
            localtime=local_time,
        )
        custom_properties = {
            "FISCALIZATION_DATE": emission_date.strftime("%Y-%m-%dT%H:%M:%S"),
            "FISCAL_ID": str(num_nota_cfe),
        }
        self.order_service.set_custom_properties(
            pos_id=pos_id,
            custom_properties=custom_properties,
            order_id=order_id,
            blk_notify=True,
        )

    def _save_invoice_error_processing(
        self,               # type: SatProcessor
        pos_id,             # type: str
        order_id,           # type: str
        request,            # type: eTree.Element
        sat_service_name,   # type: str
    ):
        # type: (...) -> None

        dir_error = self._get_error_directory()

        file_name = "{0}_request_{1}_pos{2}_order{3}_erro.xml".format(
            order_id.zfill(9),
            sat_service_name,
            str(pos_id).zfill(2),
            order_id,
        )
        file_path = os.path.join(
            dir_error,
            file_name,
        )
        request_error = open(
            file_path,
            "w+",
        )
        request_error.write(str(request))
        request_error.close()

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

        dir_error = os.path.join(
            self.fiscal_sent_dir,
            "Erros",
        )
        if not os.path.exists(dir_error):
            os.makedirs(dir_error)

        return dir_error

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

        dir_sent = os.path.join(
            self.fiscal_sent_dir,
            "Enviados",
        )
        if not os.path.exists(dir_sent):
            os.makedirs(dir_sent)

        return dir_sent

    @staticmethod
    def _get_invoice_directory(
        date_emission,  # type: str
        dir_sent,       # type: str
    ):
        # type: (...) -> str

        dir_nota = os.path.join(
            dir_sent,
            date_emission[0:4],
            date_emission[4:6],
            date_emission[6:8],
        )
        if not os.path.exists(dir_nota):
            os.makedirs(dir_nota)

        return dir_nota
