import glob
import os
import sqlite3
from base64 import b64encode
from datetime import datetime
from logging import Logger
from uuid import uuid4
from zlib import crc32, compressobj, MAX_WBITS, DEFLATED, Z_DEFAULT_COMPRESSION

from model.config import Config
from model.event_dispatch import EventDispatch
from model.log import LogEntry
from model.repository import LogRepositoryModel
from model.resync import ResyncRequest
from typing import List

DATABASE_NAME_TEMPLATE = """mwapp_audit_{}.log"""

TABLE_EXISTS_QUERY = "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?;"

CREATE_UUID_TABLE = "CREATE TABLE IF NOT EXISTS UUID AS SELECT ? AS value;"

CREATE_AUDITLOG_TABLE = """
    CREATE TABLE IF NOT EXISTS AuditLog (
        POSId INTEGER NOT NULL,
        BusinessPeriod VARCHAR NOT NULL,
        EvtType VARCHAR NOT NULL,
        EvtTime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        OrderId INTEGER,
        CreatedAt VARCHAR,
        SessionId VARCHAR,
        Encoded INTEGER NOT NULL DEFAULT 0,
        EvtData VARCHAR
    );
"""

FETCH_ENTRY_QUERY = """
    SELECT
        _ROWID_,
        BusinessPeriod,
        EvtType,
        Encoded,
        EvtData,
        OrderId,
        CreatedAt,
        strftime('%Y-%m-%dT%H:%M:%f', EvtTime) AS EvtTime
    FROM AuditLog
    WHERE 
        (_ROWID_ > :row_id AND BusinessPeriod = :period
        OR BusinessPeriod != :period)
        AND EvtType IN ({})
    ORDER BY _ROWID_;
"""


def _get_unsigned_crc32(data, start=0xFFFFFFFF):
    return crc32(data, start)


def _table_exists(conn, table_name):
    for _ in conn.execute(TABLE_EXISTS_QUERY, (table_name,)):
        return True
    return False


class LogRepository(LogRepositoryModel):
    def __init__(self, output_path, encode, logger, config):
        # type: (str, bool, Logger, Config) -> None

        self.output_path = output_path
        self.encode = encode
        self.logger = logger
        self.conn_timeout = config.connection_timeout
        self.audit_dir = self.create_dir()

    def create_dir(self):
        # type: () -> str
        audit_dir = os.path.normpath(self.output_path.format(**os.environ))
        if not os.path.exists(audit_dir):
            os.makedirs(audit_dir)
        return audit_dir

    def insert_entry(self, log_entry):
        # type: (LogEntry) -> EventDispatch
        database_full_path = self._get_database_name_from_business_period(log_entry.business_period)

        new_database = not os.path.exists(database_full_path)

        with self._connect_database(database_full_path) as database_conn:

            if new_database or self._database_needs_initialization(database_conn):
                uuid = uuid4().hex
                checksum = _get_unsigned_crc32(uuid)
                self.logger.debug("\nDatabase '{}' must be initialized".format(database_full_path))
                self._initialize_database(checksum, database_conn, uuid)
            else:
                self.logger.debug("\nDatabase '{}' was already initialized".format(database_full_path))

            self._compress_and_encode_data_from_log_entry(log_entry)
            last_row_id = self._commit_and_update_checksum(database_conn, log_entry)
            self.logger.debug("\nRegistry {} inserted on database {}".format(last_row_id, database_full_path))

            event_dispatch_time = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.000")
            return EventDispatch(event_dispatch_time, last_row_id, log_entry, self.encode)

    def _commit_and_update_checksum(self, database_conn, log_entry):
        new_checksum = self._get_new_checksum(database_conn, log_entry.event_data)
        last_row_id = self._commit_new_entry(database_conn, log_entry)
        self._save_checksum(database_conn, new_checksum)
        return last_row_id

    def _get_new_checksum(self, database_conn, data):
        old_checksum = self._get_database_checksum(database_conn)
        return _get_unsigned_crc32(data, old_checksum)

    def _get_databases_list(self, period_to_search):
        files_list = glob.glob(os.path.join(self.audit_dir, DATABASE_NAME_TEMPLATE.format("[0-9]" * 8)))
        filtered_files_list = filter(lambda x: x[-12:-4] >= period_to_search, files_list)
        return sorted(filtered_files_list)

    def fetch_resync_events(self, resync_request):
        # type: (ResyncRequest) -> List[EventDispatch]
        log_message = "Receive Resync request: Date {} / RowId {}"
        self.logger.info(log_message.format(resync_request.period, resync_request.last_row_id))
        all_events = []
        for file_name in self._get_databases_list(resync_request.period):
            database_full_path = file_name

            with self._connect_database(database_full_path) as database_conn:
                event_types_str = ','.join("'{}'".format(x) for x in resync_request.event_types.split(','))
                formatted_query = FETCH_ENTRY_QUERY.format(event_types_str)

                params = {"period": resync_request.period, "row_id": resync_request.last_row_id}
                for row in database_conn.execute(formatted_query, params):
                    log_entry = self._create_log_entry_obj_with_query_result(row)
                    all_events.append(EventDispatch(row[7], row[0], log_entry, bool(row[3])))

        self.logger.info("Found {} event(s) to resync".format(len(all_events)))

        return all_events

    def _connect_database(self, database_full_path):
        try:
            return sqlite3.connect(database_full_path, self.conn_timeout)
        except Exception as _:
            self.logger.exception("Error opening Database {}".format(database_full_path))
            raise

    @staticmethod
    def _create_log_entry_obj_with_query_result(row):
        return LogEntry(
            business_period=row[1],
            event_type=row[2],
            created_at_gmt=row[6],
            event_data=row[4],
            order_id=row[5]
        )

    def _get_database_name_from_business_period(self, business_period):
        formatted_name = DATABASE_NAME_TEMPLATE.format(business_period)
        return self._get_database_full_path(formatted_name)

    def _get_database_full_path(self, file_name):
        return os.path.normpath(os.path.join(self.output_path, file_name).format(**os.environ))

    @staticmethod
    def _database_needs_initialization(conn):
        if not _table_exists(conn, "UUID"):
            return True

        if not _table_exists(conn, "AuditLog"):
            return True

        return False

    @staticmethod
    def _initialize_database(checksum, database_conn, uuid_data):
        database_conn.execute(CREATE_UUID_TABLE, (uuid_data,))

        database_conn.execute(CREATE_AUDITLOG_TABLE)
        LogRepository._save_checksum(database_conn, checksum)
        database_conn.commit()

    @staticmethod
    def _save_checksum(database_conn, checksum):
        database_conn.execute("PRAGMA user_version={};".format(checksum))
        database_conn.commit()

    @staticmethod
    def _get_database_checksum(database_conn):
        for row in database_conn.execute("PRAGMA user_version;"):
            return int(row[0])

    def _commit_new_entry(self, database_conn, log_entry):
        params = log_entry.__dict__.copy()
        params["encode"] = self.encode
        cursor = database_conn.cursor()
        cursor.execute(
            """
            INSERT INTO AuditLog(POSId,BusinessPeriod,EvtType,OrderId,CreatedAt,SessionId,Encoded,EvtData)
            VALUES(:pos_id,:business_period,:event_type,:order_id,:created_at_gmt,:session_id,:encode,:event_data);
            """, params
        )
        last_row_id = cursor.lastrowid
        database_conn.commit()
        return last_row_id

    def _compress_and_encode_data_from_log_entry(self, log_entry):
        compressed_data = self._compress_data(log_entry.event_data)
        if self.encode:
            log_entry.event_data = b64encode(compressed_data)
        else:
            log_entry.event_data = compressed_data

    @staticmethod
    def _compress_data(data):
        compress_obj = compressobj(Z_DEFAULT_COMPRESSION, DEFLATED, MAX_WBITS + 16)
        return compress_obj.compress(data) + compress_obj.flush()
