# -*- coding: utf-8 -*-

import glob
import logging
import os
import sqlite3
import subprocess

from datetime import datetime
from typing import (
    Dict,
    List,
    Optional,
    Tuple
)

from msgbus import MBEasyContext
from persistence import Driver
from repository import (
    FiscalRepository,
    OrderRepository
)
from services import CommonService

logger = logging.getLogger("Maintenance")


class CleanDatabasesProcessor(object):
    def __init__(
        self,
        mb_context,                 # type: MBEasyContext
        comp_no,                    # type: int
        database_backups_to_keep,   # type: int
        order_repository,           # type: OrderRepository
        fiscal_repository,          # type: FiscalRepository
        common_service              # type: CommonService
    ):
        # type: (...) -> None

        self.mb_context = mb_context
        self.database_backups_to_keep = database_backups_to_keep
        self.order_repository = order_repository
        self.fiscal_repository = fiscal_repository
        self.common_service = common_service
        self.database_path = order_repository.get_database_dir()
        self.backup_database_path = order_repository.get_backup_database_dir()

        self.component_is_server = comp_no == 0
        self.maintenance_processor = None
        self.pos_list = None

    def set_server_parameters(self, maintenance_processor):
        self.maintenance_processor = maintenance_processor

    def clean_databases(self):
        # type: () -> None

        try:
            if not self.component_is_server:
                return

            self.pos_list = self.common_service.get_pos_list()
            self._validate_backup_path()
            self._clean_orders_database()
            self._clean_fiscal_database()
        except Exception as _:
            logger.exception("Error cleaning databases")

    def vacuum_databases(self):
        # type: () -> None

        try:
            if not self.component_is_server:
                return

            self._vacuum_production_database()
            self._vacuum_fiscal_database()
            self._vacuum_orders_database()
        except Exception as _:
            logger.exception("Error doing vacuum on databases")

    def clean_database_backups(self):
        # type: () -> None

        logger.info("Starting [_clean_database_backups]")
        try:
            if not self.component_is_server:
                return

            files_to_delete = self._get_backup_files_to_delete()
            self.common_service.delete_files(files_to_delete)
        except Exception as _:
            logger.exception("Error cleaning databases backups")

        logger.info("Finished [_clean_database_backups]")

    def _validate_backup_path(self):
        # type: () -> None
        self.common_service.make_sure_path_exists(self.backup_database_path)

    def _clean_orders_database(self):
        # type: () -> None

        logger.info("Starting [_clean_orders_database]")
        try:
            for pos_id in sorted(self.pos_list, key=lambda _: int(_)):
                logger.info('Starting backup process for POS: %s' % pos_id)
                try:
                    end_date = self.common_service.get_end_date()
                    orders_stored = self._backup_orders(pos_id, end_date)

                    recalled_orders = self.order_repository.get_orders(
                        pos_id=pos_id,
                        end_date=end_date,
                        order_states=['RECALLED']
                    )
                    recalled_orders = [o[0] for o in recalled_orders]

                    orders_to_delete = orders_stored
                    orders_to_delete.extend(x for x in recalled_orders if x not in orders_to_delete)
                    if orders_to_delete:
                        self._delete_orders(
                            pos_id=pos_id,
                            orders=orders_to_delete
                        )
                    else:
                        logger.info("Have no orders to clean in order db: {}".format(pos_id))
                except Exception as _:
                    logger.exception("Error doing backup - POS: %s" % pos_id)
                    break

            logger.info('Finished [_clean_orders_database]')
        except Exception as _:
            logger.exception("Error cleaning order dbs")

    def _delete_orders(
        self,
        pos_id,     # type: int
        orders      # type: List[int]
    ):
        # type: (...) -> None

        orders_batches = [orders[x:x + 100] for x in xrange(0, len(orders), 100)]
        logger.info("Starting [_delete_orders] por POS: {}".format(pos_id))
        for orders_batch in orders_batches:
            self.order_repository.delete_orders(
                pos_id=pos_id,
                orders=orders_batch
            )
        logger.info("Finished [_delete_orders] por POS: {}".format(pos_id))

    def _vacuum_orders_database(self):
        # For each POS - Backup Orders DB
        logger.info("Starting [_vacuum_orders_database]")
        try:
            order_db_files = []
            conn = None

            for pos_id in sorted(self.pos_list, key=lambda _: int(_)):
                try:
                    conn = Driver().open(
                        mbcontext=self.mb_context,
                        service_name='Persistence',
                        dbname=str(pos_id)
                    )

                    order_db_files.append(self.get_db_file(conn, "orderdb"))
                finally:
                    if conn:
                        conn.close()

            for order_db in order_db_files:
                conn = None
                old_file_size = self.common_service.get_file_size(order_db)
                logger.info('Doing VACUUM for order db: {}'.format(order_db.split('db')[1]))

                try:
                    conn = sqlite3.connect(order_db)
                    conn.execute("VACUUM;")  # This will decrease the size of the databases
                except Exception as ex:
                    logger.exception("Error doing vacuum - %s - Message %s" % (order_db, str(ex)))
                    if conn is not None:
                        conn.close()
                else:
                    conn.close()

                new_file_size = self.common_service.get_file_size(order_db)

                logger.info('Order db file sizes. OLD: {}mb; NEW: {}mb'.format(old_file_size, new_file_size))

            logger.info('Finished [_vacuum_orders_database]')
        except Exception as _:
            logger.exception("Error doing vacuum on order dbs")

    def _get_backup_files_to_delete(self):
        # type: () -> List[str]

        files_to_delete = []

        files = glob.glob("{}/*.bkp".format(self.database_path))
        files_by_database_name = {}

        for f in files:
            created_time = os.path.getctime(f)
            file_name = os.path.basename(f)
            database_name = file_name.split('.')[0] if '.' in file_name else ''
            if database_name in files_by_database_name:
                files_by_database_name[database_name].append((f, created_time))
            else:
                files_by_database_name[database_name] = [(f, created_time)]

        for key in files_by_database_name:
            files_by_database_name[key].sort(key=lambda i: i[1], reverse=True)
            files_to_delete.extend([f[0] for f in files_by_database_name[key][self.database_backups_to_keep:]])

        return files_to_delete

    @staticmethod
    def _split_by_months(
        items,              # type: List[Tuple[int, str]]
        timestamp=False     # type: Optional[bool]
    ):
        # type: (...) -> Dict[str, List[int]]

        items_by_months = {}
        for item, day in items:
            if timestamp:
                date = datetime.fromtimestamp(int(day))
                month = date.strftime("%Y%m")
            else:
                month = day[:-2]

            if month in items_by_months:
                items_by_months[month].append(item)
            else:
                items_by_months[month] = [item]

        return items_by_months

    def _backup_orders(
        self,
        pos_id,     # type: int
        end_date,   # str
    ):
        # type: (...) -> List[int]

        logger.info("Starting [_backup_orders]")

        orders = self.order_repository.get_orders(
            pos_id=pos_id,
            end_date=end_date,
            order_states=['PAID', 'VOIDED', 'ABANDONED', 'SYSTEM_VOIDED']
        )
        if len(orders) == 0:
            return []

        orders_stored = []
        orders_by_month = self._split_by_months(orders)

        try:
            for month in orders_by_month:
                self.order_repository.fill_order_backup_database(
                    pos_id=pos_id,
                    suffix_date=month,
                    orders=orders_by_month[month]
                )

            orders_stored = [o[0] for o in orders]
            logger.info("Finished [_backup_orders]")

        except Exception as _:
            logger.exception("Error doing backup")

        return orders_stored

    def _clean_fiscal_database(self):
        try:
            logger.info("Starting [_clean_fiscal_database]")

            orders_backup = self._backup_fiscal()
            if orders_backup:
                self._delete_fiscal(orders_backup)
            else:
                logger.info("Have no order to clean in fiscal db")
        except Exception as ex:
            logger.exception('Error purging orders on fiscal database')
            raise ex
        else:
            logger.info('Finalized purge of orders on fiscal database')
            logger.info("Finished [_clean_fiscal_database]")

    def _vacuum_fiscal_database(self):
        try:
            logger.info("Starting [_vacuum_fiscal_database]")

            conn = None
            try:
                conn = Driver().open(self.mb_context, service_name="FiscalPersistence")
                db_file = self.get_db_file(conn, "fiscal")
            except Exception as ex:
                if "malformed" in ex.message:
                    logger.exception("Malformed database. Executing hard clean")
                    db_file = self.get_db_file(conn, "fiscal")
                    self._hard_clean_fiscal_database(db_file)
                logger.exception('Error doing vacuum on fiscal database')
                raise ex
            finally:
                if conn:
                    conn.close()

            old_file_size = self.common_service.get_file_size(db_file)
            conn = None
            try:
                logger.info('Doing VACUUM for fiscal database')
                if db_file:
                    conn = sqlite3.connect(db_file)
                    conn.execute("VACUUM;")
            except Exception as _:
                logger.exception('Error doing vacuum on fiscal db')
            finally:
                if conn:
                    conn.close()

            new_file_size = self.common_service.get_file_size(db_file)

            logger.info('Fiscal db file sizes. OLD: {}mb; NEW: {}mb'.format(old_file_size, new_file_size))

            logger.info("Finished [_vacuum_fiscal_database]")
        except Exception as _:
            logger.exception("Error doing vacuum on fiscal db")

    def _vacuum_production_database(self):
        try:
            logger.info("Starting [_vacuum_production_database]")

            conn = None
            try:
                conn = Driver().open(self.mb_context)
                db_file = self.get_db_file(conn, "production")
            finally:
                if conn:
                    conn.close()

            old_file_size = self.common_service.get_file_size(db_file)
            conn = None
            try:
                logger.info('Doing VACUUM on production database')
                if db_file:
                    conn = sqlite3.connect(db_file)
                    conn.execute("VACUUM;")
            except Exception as _:
                logger.exception('Error doing vacuum on production database')
            finally:
                if conn:
                    conn.close()

            new_file_size = self.common_service.get_file_size(db_file)

            logger.info('Production database file sizes. OLD: {}mb; NEW: {}mb'.format(old_file_size, new_file_size))

            logger.info("Finished [_vacuum_production_database]")
        except Exception as _:
            logger.exception("Error doing vacuum on production database")

    @staticmethod
    def get_db_file(conn, db_name_to_find):
        db_file = None
        pragma_cursor = conn.select("PRAGMA database_list;")
        for pragma_row in pragma_cursor:
            db_name, db_file = map(pragma_row.get_entry, (1, 2,))
            if db_name == db_name_to_find:
                break
        return db_file

    def _delete_fiscal(
        self,
        orders      # type: List[int]
    ):
        # type: (...) -> None

        orders_batches = [orders[x:x + 100] for x in xrange(0, len(orders), 100)]
        logger.info("Starting [_delete_orders]")
        for orders_batch in orders_batches:
            self.fiscal_repository.delete_fiscals(orders_batch)
        logger.info("Finished [_delete_orders]")

    def _backup_fiscal(self):
        # type: () -> List[int]

        logger.info("Starting [_backup_fiscal]")
        orders_stored = []

        is_timestamp = True
        end_date = self.common_service.get_end_date(is_timestamp)
        orders = self.fiscal_repository.get_orders(end_date)
        if len(orders) < 1:
            return []

        orders_by_month = self._split_by_months(
            items=orders,
            timestamp=is_timestamp
        )

        try:
            for month in orders_by_month:
                self.fiscal_repository.fill_fiscal_backup_database(
                    suffix_date=month,
                    orders=orders_by_month[month]
                )

            orders_stored = [o[0] for o in orders]
            logger.info("Finished [_backup_orders]")

        except Exception as _:
            logger.exception("Error doing backup")

        return orders_stored

    @staticmethod
    def _hard_clean_fiscal_database(db_file):
        logger.info("Starting [_hard_clean_fiscal_database]")
        subprocess.call(
            [
                "sqlite3",
                db_file,
                ".mode insert",
                ".output dump_all.sql",
                ".dump",
                ".read dump_all.sql"
            ],
            shell=True
        )
        logger.info("Finished [_hard_clean_fiscal_database]")


def execute_db_transaction(conn, query, close_conn=True):
    try:
        conn.transaction_start()
        conn.query('''BEGIN TRANSACTION;''')
        conn.query(query)
    except Exception as ex:
        conn.query('''ROLLBACK TRANSACTION;''')
        raise ex
    else:
        conn.query('''COMMIT TRANSACTION;''')
    finally:
        if close_conn:
            conn.close()
