import os
import sys
import threading
import json
import sqlite3
import datetime
import Queue
from threading import Lock, Condition
from xml.etree import cElementTree as eTree

import msgbus
import systools
import syserrors
from application.configurator import Configurator

from mwapp.components import BaseComponent
from typing import Callable


class AuditEventConsumer(BaseComponent):
    def __init__(self, configs, process):
        # type: (Configurator, Callable) -> None

        service_name = configs.service_name
        service_type = configs.service_type
        required_services = configs.required_services

        super(AuditEventConsumer, self).__init__(service_name, service_type, required_services)
        self.logger = configs.logger

        self.service_name = service_name
        self.service_type = service_type
        self.eventFilter = []
        self.eventFilterData = {}
        self.last_row_id = None
        self.initial_period = None
        self.last_period = None
        self.queue_full = False
        self.lock = Lock()
        self.condition = Condition()
        self.terminating = False
        self.skip_event = False
        self.mode = 'DEFAULT'

        self.mbcontext.MB_EasyEvtSubscribe('AUDITLOGGER')
        self.mbcontext.MB_EasyEvtSubscribe(self.name)
        self._read_loader(configs)

        self.q = Queue.Queue()
        self.buffer_q = Queue.Queue(maxsize=self.queue_max_size)

        self.ctrl_conn = None
        self.process = process
        with self.lock:
            self.init_control_file()

    def _read_loader(self, configs):
        group = self.service_name + ".{}"
        cfg_reader = configs.cfg_reader
        self.initial_period = int(cfg_reader.find_value(group.format("initialPeriod"), "0"))
        if self.initial_period == 0:
            self.logger.error("Configuration error: initialPeriod is undefined")
            sys.exit(msgbus.SE_NOTFOUND)

        control = cfg_reader.find_value(group.format("controlFilePath"))
        self.controlFilePath = systools.sys_parsefilepath(control)
        control_path = os.path.dirname(self.controlFilePath)
        if not os.path.exists(control_path):
            os.mkdir(control_path, 0777)
        self.queue_max_size = int(cfg_reader.find_value(group.format("queueMaxSize"), "100"))
        self.resync_required = cfg_reader.find_value(group.format("resyncRequired"), "true").lower() == "true"

        for evt_filter in cfg_reader.find_values(group.format("eventFilter")) or []:
            event = evt_filter.split('/')
            self.eventFilter.append(event[0])
            if len(event) > 1:
                self.eventFilterData[event[0]] = event[1:]
        self.evt_batch_size = int(cfg_reader.find_value(group.format("eventBatchSize"), "0"))
        self.evt_batch_time = cfg_reader.find_value(group.format("eventBatchTime"), "false").lower() == "true"

        if self.evt_batch_size:
            self.mode = 'BATCH_SIZE'
        if self.evt_batch_time:
            self.mode = 'BATCH_TIME'

    def _create_db_conn(self):
        ctrl_conn = None
        try:
            ctrl_conn = sqlite3.connect(self.controlFilePath)
            ctrl_conn.isolation_level = None
        except Exception as _:  # noqa
            self.logger.exception("Failed to get the thread's control file handle.")

        return ctrl_conn

    def init_control_file(self):
        self.ctrl_conn = self._create_db_conn()
        try:
            self._create_events_sent_table(self.ctrl_conn)

            c = self.ctrl_conn.cursor()
            r = c.execute("""
                SELECT Period,LastRowId 
                    FROM EventsSent 
                    WHERE period >= (SELECT max(Period) FROM EventsSent) order by LastRowId desc limit 1;
            """).fetchone()
            if r:
                self.last_period = r[0]
                self.last_row_id = r[1]
            else:
                self.last_period = self.initial_period
                self.last_row_id = "-1"
            c.close()
        except Exception as _:  # noqa
            self.logger.exception("Failure during control file initialization.")
            sys.exit(msgbus.SE_BADPARAM)

    def _create_events_sent_table(self, ctrl_conn):
        if not ctrl_conn:
            ctrl_conn = self.ctrl_conn

        ctrl_conn.execute("""
                CREATE TABLE IF NOT EXISTS EventsSent (
                    Period      INTEGER NOT NULL,
                    LastRowId   INTEGER NOT NULL,
                    PRIMARY KEY (LastRowId)
                );
            """)
        ctrl_conn.execute("CREATE INDEX IF NOT EXISTS `index_LastRowId` ON `EventsSent` (`LastRowId` DESC);")

    def resync(self, last_period, last_row_id):
        self.logger.info("Running resync.")
        try:
            evt_types = (",".join(self.eventFilter))
            params = [last_period, evt_types, last_row_id]
            d = "\0".join(str(p) for p in params)
            self.mbcontext.MB_EasySendMessage("AuditLogger", msgbus.TK_AUDITLOG_RESYNC, format=msgbus.FM_PARAM, data=d)
        except Exception as _:  # noqa
            if self.resync_required:
                self.logger.exception("Exception occurred during resynchronization.")
                sys.exit(syserrors.SE_MB_ERR)

    def force_new_period(self, new_period, ctrl_conn=None):
        if not ctrl_conn:
            ctrl_conn = self.ctrl_conn

        new_period_date = datetime.datetime.strptime(str(new_period), '%Y%m%d')
        last_period_date = datetime.datetime.strptime(str(self.last_period), '%Y%m%d')

        log_message = "Reprocess Sales Command Date {} - Last Period Sent {}"
        self.logger.info(log_message.format(new_period_date, last_period_date))

        if new_period_date <= last_period_date:
            self.lock.acquire()
            ctrl_conn.execute("DELETE FROM EventsSent WHERE Period >= %s" % new_period)
            self._harakiri()

    def start(self):
        t = threading.Thread(target=self.worker)
        t.daemon = True
        t.start()

        while not self.terminating:
            msg = self.mbcontext.MB_EasyGetMessage()
            if not msg or msg.token == msgbus.TK_CMP_TERM_NOW:
                self._send_nack(msg)
                self.terminating = True
                with self.condition:
                    self.condition.notify()
                break

            elif msg.token == msgbus.TK_EVT_EVENT:
                params = msg.data.split('\0') if msg.data else []

                if not params:
                    self._send_nack(msg)
                    continue

                xml, subject, event_type = params[0], params[1], params[2]
                if subject == "SKIPEVENT":
                    self.skip_event = True
                    self._send_ack(msg)
                    continue

                if subject == "AUDITLOGGER":
                    if event_type not in self.eventFilter or self.queue_full:
                        self._send_ack(msg)
                        continue

                    item = [xml, event_type]
                    self._insert_event_into_queue(item)

                elif subject == "BohPump":
                    if event_type == 'SendEvents':
                        item_list = []
                        for _ in range(self.buffer_q.qsize()):
                            item = self.buffer_q.get()
                            item_list.append(item)
                        self.q.put(item_list, False)

                else:
                    self._send_nack(msg)

            self._send_nack(msg)

        if self.ctrl_conn:
            self.ctrl_conn.close()

    def _insert_event_into_queue(self, item):
        try:
            if not self.evt_batch_size and not self.evt_batch_time:
                item = [item]
                self.q.put(item, False)
            else:
                self.buffer_q.put(item, False)

            if self.evt_batch_size and self.buffer_q.qsize() >= self.evt_batch_size:
                item_list = []
                for _ in range(self.evt_batch_size):
                    item = self.buffer_q.get()
                    item_list.append(item)
                self.q.put(item_list, False)
        except Queue.Full:
            self.logger.error("Queue Full - Locking: Size {}".format(self.q.qsize()))
            self.queue_full = True

    @staticmethod
    def fib(n_sequence):
        a, b = 1, 1
        for _ in range(n_sequence - 1):
            a, b = b, a + b
        return a

    def worker(self):
        self.logger.info("Starting worker process...")
        self.resync(self.last_period, self.last_row_id)

        ctrl_conn = self._create_db_conn()
        while not self.terminating:
            last_error = None
            retry_count = 1
            item = self._get_new_item(ctrl_conn)

            while last_error != msgbus.SE_SUCCESS and not self.terminating:
                try:
                    last_error = self.process(item)
                    if last_error != syserrors.SE_SUCCESS and not self.skip_event:
                        if retry_count < 14:
                            retry_count += 1

                        self.logger.info("Error sending event, retry in {} seconds ...".format(self.fib(retry_count)))
                        with self.condition:
                            self.condition.wait(self.fib(retry_count))
                        continue

                    if self.skip_event:
                        self.logger.info("Event id {} skipped".format(self.last_row_id))
                        last_error = msgbus.SE_SUCCESS
                        self.skip_event = False

                    evt_period = self._mark_events_as_sent(item, ctrl_conn)
                    self._clean_control_file(evt_period, ctrl_conn)

                    if self.queue_full and self.buffer_q.qsize() == 0 and self.q.qsize() == 0:
                        self._harakiri()

                except Exception as _:  # noqa
                    self.logger.exception("Could not start worker thread")

        self.logger.info("Finishing worker process")

    def _clean_control_file(self, period, ctrl_conn):
        if period is None:
            return

        evt_period = datetime.datetime.strptime(str(period), '%Y%m%d')
        last_period = datetime.datetime.strptime(str(self.last_period), '%Y%m%d')

        if evt_period > last_period:
            date_subtract = last_period - datetime.timedelta(days=15)
            ctrl_conn.execute("DELETE FROM EventsSent WHERE Period <= %s" % int(date_subtract.strftime('%Y%m%d')))
            self.last_period = period

    def _mark_events_as_sent(self, item, ctrl_conn):
        evt_period = None
        for idx, q_item in enumerate(item):
            event = eTree.XML(q_item[0])
            audit_log = event.find("AuditLog")
            evt_row_id = audit_log.get("rowId")
            evt_period = audit_log.get("period")
            if not self.lock.locked():
                with self.lock:
                    query = "INSERT OR IGNORE INTO EventsSent(Period, LastRowId) VALUES(:period, :rowid)"
                    ctrl_conn.execute(query, {"period": evt_period, "rowid": evt_row_id})

            self.last_row_id = evt_row_id
        return evt_period

    def _harakiri(self):
        self.logger.info("Exiting to force resync events processed ...")
        hv_service = self.mbcontext.hv_service
        self.mbcontext.MB_SendOneWayMessage(hv_service, msgbus.TK_HV_COMPSTOP, msgbus.FM_STRING, "BohPump")

    def _get_new_item(self, ctrl_conn):
        item = self.q.get()
        for idx, q_item in enumerate(item):
            event = eTree.XML(q_item[0])
            audit_log = event.find("AuditLog")
            evt_row_id = audit_log.get("rowId")
            evt_period = audit_log.get("period")

            self.logger.info("Processing event {} for period {} ...".format(evt_row_id, evt_period))
            if int(evt_row_id) <= int(self.last_row_id):
                query = """SELECT 1 FROM EventsSent WHERE LastRowId = (:last_row_id) and Period = (:period);"""
                evt_check = ctrl_conn.execute(query, {"last_row_id": evt_row_id, "period": evt_period}).fetchone()
                if evt_check:
                    item.pop(idx)
        return item

    def send_response(self, msg, code=200):
        msg.token = msgbus.TK_SYS_ACK if 200 < code < 300 else msgbus.TK_SYS_NAK
        self.mbcontext.MB_ReplyMessage(msg, format=msgbus.FM_PARAM, data=json.dumps({'code': code}))

    def renew_service(self):
        self._harakiri()
