import os
import sqlite3
from sqlite3 import Connection
from datetime import datetime

from services import CommonService
from typing import (
    Any,
    Callable,
    Dict,
    List,
    Optional,
    Tuple,
    Union
)

from dbapitransactioncallback import DbApiTransactionCallback
from dbapitemplate import DbApiTemplate
from repository import OrderRepository as BaseOrderRepository


class SqliteOrderRepository(BaseOrderRepository):

    def __init__(
        self,
        common_service,         # type: CommonService
        database_dir,           # type: str
        backup_database_dir,    # type: str
        schema_sql_path,        # type: str
        callback                # type: DbApiTransactionCallback
    ):
        # type: (...) -> None

        self.common_service = common_service
        self.callback = callback
        self.database_dir = database_dir
        self.backup_database_dir = backup_database_dir
        self.schema_sql_path = schema_sql_path

        self.orders_conn = None
        self.multi_template = None

        self.sale_types = None

    def get_backup_database_dir(
        self
    ):
        # type: () -> str

        return self.backup_database_dir

    def get_database_dir(
        self
    ):
        # type: () -> str

        return self.database_dir

    def _get_orders_conn(self):
        if self.orders_conn is None:
            self.orders_conn = self._fill_orders_conn(self.common_service.get_pos_list())

        return self.orders_conn

    def _get_multi_template(self):
        if self.multi_template is None:
            self.multi_template = self._fill_multi_template()

        return self.multi_template

    def _fill_orders_conn(
        self,
        pos_list    # type: List[int]
    ):
        # type: (...) -> Dict[int, Connection]

        orders_conn = {}
        for pos_id in pos_list:
            order_database_path = os.path.join(self.database_dir, "order.db{}".format(pos_id))
            orders_conn[pos_id] = sqlite3.connect(
                database=order_database_path,
                check_same_thread=False
            )
        return orders_conn

    def _fill_multi_template(
        self
    ):
        # type: () -> Dict[int, DbApiTemplate]

        multi_template = {}
        orders_conn = self._get_orders_conn()
        for pos_id in orders_conn:
            multi_template[pos_id] = DbApiTemplate(orders_conn[pos_id])

        return multi_template

    def _update_callback_conn(
        self,
        conn    # type: Connection
    ):
        # type: (...) -> None

        self.callback.update_conn(conn)

    @staticmethod
    def _execute_script(
        connection,     # type: Connection
        query,          # type: str
    ):
        # type: (...) -> None

        connection.executescript(query)

    def _execute_multi_template(
        self,
        pos_id,             # type: str
        query,              # type: str
        params=None,        # type: Tuple
        mapper=None,        # type: Optional[Callable[[tuple], Any]]
        is_script=False     # type: Optional[bool]
    ):
        # type: (...) -> Union[int, list]

        orders_conn = self._get_orders_conn()
        conn = orders_conn[pos_id]
        self._update_callback_conn(conn)

        multi_template = self._get_multi_template()
        if pos_id in multi_template:
            if is_script:
                return self._execute_script(
                    connection=multi_template[pos_id].conn,
                    query=query
                )

            return multi_template[pos_id].execute(
                sql=query,
                params=params if params else (),
                mapper=mapper
            )

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

        query = self._get_orders_by_date_query.format("'" + "','".join(order_states) + "'")

        params = (end_date,) if end_date else None

        def mapper(row):
            return row["OrderId"], str(row["BusinessPeriod"])

        return self._execute_multi_template(
            pos_id=pos_id,
            query=query,
            params=params,
            mapper=mapper
        )

    def get_business_period_from_oldest_order(self):
        # type: (...) -> Optional[datetime]
        def mapper(row):
            return datetime.strptime(str(row["BusinessPeriod"]), "%Y%m%d")

        dates_list = []
        query = self._get_created_at_from_oldest_order_query
        for pos_id in self.common_service.get_pos_list():
            dates_list.extend(self._execute_multi_template(
                pos_id=pos_id,
                query=query,
                mapper=mapper
            ))
        dates_list.sort()
        return dates_list[0] if dates_list else None

    def _get_create_tables_query(self):
        # type: () -> str

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

    def _create_tables_orders(self, db_api):
        # type: (DbApiTemplate) -> None

        query = self._get_table_exists_query
        _, data = db_api.execute(query, ())

        if not data:
            create_table_query = self._get_create_tables_query()
            self._execute_script(
                connection=db_api.conn,
                query=create_table_query
            )

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

        conn = sqlite3.connect(os.path.join(self.backup_database_dir, "orders_{}.db".format(suffix_date)))
        self._update_callback_conn(conn)
        db_api = DbApiTemplate(conn)

        self._create_tables_orders(db_api)

        order_database_path = os.path.join(self.database_dir, "order.db{}".format(pos_id))
        query = self._backup_order_database_query.format(order_database_path, ','.join([str(o) for o in orders]))

        self._execute_script(db_api.conn, query)

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

        query = self._get_delete_orders_query.format(','.join([str(o) for o in orders]))
        self._execute_multi_template(
            pos_id=pos_id,
            query=query,
            is_script=True
        )

    _get_table_exists_query = """
        SELECT count(name) FROM sqlite_master WHERE type='table' AND name='Orders'
    """

    _get_orders_by_date_query = """
        SELECT OrderId, BusinessPeriod FROM Orders
        WHERE BusinessPeriod < ?
        AND StateId IN (
            SELECT StateId 
            FROM OrderState 
            WHERE StateDescr IN ({})
        )
        ORDER BY OrderId LIMIT 1000
    """

    _backup_order_database_query = """
        ATTACH DATABASE '{0}' AS o; 
        INSERT OR REPLACE INTO Orders SELECT * FROM o.Orders WHERE OrderId IN ({1});
        INSERT OR REPLACE INTO OrderCustomProperties SELECT * FROM o.OrderCustomProperties WHERE OrderId IN ({1});
        INSERT OR REPLACE INTO OrderItemCustomProperties SELECT * FROM o.OrderItemCustomProperties WHERE OrderId IN ({1});
        INSERT OR REPLACE INTO OrderStateHistory SELECT * FROM o.OrderStateHistory WHERE OrderId IN ({1});
        INSERT OR REPLACE INTO OrderItem SELECT * FROM o.OrderItem WHERE OrderId IN ({1});
        INSERT OR REPLACE INTO OrderTax SELECT * FROM o.OrderTax WHERE OrderId IN ({1});
        INSERT OR REPLACE INTO OrderDiscount SELECT * FROM o.OrderDiscount WHERE OrderId IN ({1});
        INSERT OR REPLACE INTO OrderVoidHistory SELECT * FROM o.OrderVoidHistory WHERE OrderId IN ({1});
        INSERT OR REPLACE INTO OrderTender SELECT * FROM o.OrderTender WHERE OrderId IN ({1});
        INSERT OR REPLACE INTO OrderComment SELECT * FROM o.OrderComment WHERE OrderId IN ({1});
        INSERT OR REPLACE INTO OrderProduct SELECT * FROM o.OrderProduct WHERE OrderId IN ({1});
        INSERT OR REPLACE INTO OrderPrice SELECT * FROM o.OrderPrice WHERE OrderId IN ({1});
        DETACH o;
    """

    _get_delete_orders_query = """
        DELETE FROM OrderCustomProperties WHERE OrderId IN ({0});
        DELETE FROM OrderItemCustomProperties WHERE OrderId IN ({0});
        DELETE FROM OrderStateHistory WHERE OrderId IN ({0});
        DELETE FROM OrderItem WHERE OrderId IN ({0});
        DELETE FROM OrderTax WHERE OrderId IN ({0});
        DELETE FROM OrderDiscount WHERE OrderId IN ({0});
        DELETE FROM OrderVoidHistory WHERE OrderId IN ({0});
        DELETE FROM OrderTender WHERE OrderId IN ({0});
        DELETE FROM OrderComment WHERE OrderId IN ({0});
        DELETE FROM OrderProduct WHERE OrderId IN ({0});
        DELETE FROM Orders WHERE OrderId IN ({0});
    """

    _get_created_at_from_oldest_order_query = """
            SELECT BusinessPeriod FROM Orders ORDER BY CreatedAt LIMIT 1; 
        """
