import base64
import cPickle as Pickle
import os

import production.repository

from Queue import Queue, Empty
from datetime import (
    datetime,
    timedelta,
)
from logging import Logger
from threading import (
    Thread,
)

from dbrepository import SynchronizedDbApiTemplate
from production.model import (
    Item,
    ProductionOrder,
)
from typing import (
    List,
    Dict,
    Optional,
    Union,
)


class ProductionRepository(production.repository.ProductionRepository):

    def __init__(
        self,           # type: ProductionRepository
        loggers,        # type: Dict[unicode, Logger]
        connection,     # type: SynchronizedDbApiTemplate
    ):
        # type: (...) -> None

        self.loggers = loggers
        self.connection = connection

        self.order_queue = Queue()
        self.running = True

        self.processor_thread = Thread(target=self.consume_queue)
        self.processor_thread.daemon = True
        self.processor_thread.start()

    def consume_queue(
        self,   # type: ProductionRepository
    ):
        # type: (...) -> None

        while self.running:
            order_id = None
            try:
                order_to_save = self.order_queue.get(True, 5)
                order_id = order_to_save.order_id
                self._save_order_async(
                    order=order_to_save,
                )
            except Empty:
                continue
            except (Exception,):
                self.loggers["ERROR"].exception("Error saving order: {}".format(order_id))

    def stop(
        self,   # type: ProductionRepository
    ):
        # type: (...) -> None

        self.running = False

    def save_order(
        self,       # type: ProductionRepository
        order,      # type: ProductionOrder
    ):
        # type: (...) -> None

        try:
            self._remove_forbidden_tags(
                items=order.items,
                order_id=str(order.order_id),
            )
        except (Exception,):
            self.loggers["ERROR"].error("Error handling remove_forbidden_tags for order id {}".format(order.order_id))

        self.order_queue.put(order)

    def get_orders_by_orders_ids(
        self,           # type: ProductionRepository
        orders_ids,     # type: List,
    ):
        # type: (...) -> List[ProductionOrder]

        with self.connection as conn:
            orders = []

            chunks = [orders_ids[i:i + 100] for i in range(0, len(orders_ids), 100)]
            for chunk in chunks:
                def mapper(row):
                    order_data = base64.b64decode(row[0])
                    order = Pickle.loads(order_data)
                    orders.append(order)

                orders_list = [int(order_id) for order_id in chunk]
                placeholders = ",".join([
                    "?" for _ in chunk
                ])
                conn.execute(
                    sql=select_query.format(placeholders=placeholders),
                    params=tuple(orders_list),
                    mapper=mapper,
                )
            return orders

    def get_all_orders(
        self,   # type: ProductionRepository
    ):
        # type: (...) -> List[ProductionOrder]

        with self.connection as conn:
            orders_ids = []

            conn.execute(
                sql=select_orders_id_query,
                mapper=lambda row: orders_ids.append(row["OrderId"]),
            )

        return self.get_orders_by_orders_ids(orders_ids)

    def get_orders_to_purge(
        self,           # type: ProductionRepository
        limit_date,     # type: datetime
    ):
        # type: (...) -> List[ProductionOrder]

        with self.connection as conn:
            orders = []

            def mapper(row):
                order_id = int(row["OrderId"])
                orders.append(order_id)

            formatted_limit_date = limit_date.strftime("%Y-%m-%dT%H:%M:%S")
            conn.execute(
                sql=get_orders_to_purge_query,
                params=(formatted_limit_date,),
                mapper=mapper,
            )
            return orders

    def purge_orders(
        self,       # type: ProductionRepository
        orders,     # type: List
    ):
        # type: (...) -> None

        with self.connection as conn:
            orders_list = [int(order) for order in orders]
            placeholders = ",".join([
                "?" for _ in orders_list
            ])
            query = purge_orders_query
            query = query.format(placeholders=placeholders)
            conn.execute(
                sql=query,
                params=tuple(orders_list)
            )
            self._commit(
                connection=conn,
            )

    def save_pick_list_order(
        self,               # type: ProductionRepository
        order_id,           # type: int
        order_id_count,     # type: str
        jit,                # type: str
        course,             # type: str
        receipt,            # type: str
        status,             # type: bool
    ):
        # type: (...) -> None

        with self.connection as conn:
            date = datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f")

            params = (
                order_id,
                order_id_count,
                jit,
                course,
                receipt,
                date,
                status,
            )
            conn.execute(
                sql=insert_pick_list_query,
                params=params,
            )
            self._commit(
                connection=conn,
            )

    def get_picklist_id(
        self,               # type: ProductionRepository
        order_id,           # type: int
        order_id_count,     # type: str
        jit,                # type: str
        course,             # type: str
    ):
        # type: (...) -> Optional[int]

        with self.connection as conn:
            params = (
                order_id,
                order_id_count,
                jit,
                course,
            )
            return conn.first(
                sql=select_get_pick_list_id_query,
                params=params,
            )

    def get_all_pick_list_orders(
        self,           # type: ProductionRepository
        orders_list,    # type: List[str]
    ):
        # type: (...) -> Dict

        with self.connection as conn:
            pick_list_orders = {}

            def mapper(row):
                jit = row["JIT"]
                course_name = row["Course"]

                if course_name != "":
                    course_name = " - {}".format(
                        course_name,
                    )

                key = jit + course_name
                pick_list_orders[key] = row["Id"]

            orders_list = [int(order) for order in orders_list]
            placeholders = ",".join([
                "?" for _ in orders_list
            ])
            query = select_pick_list_orders_query
            query = query.format(placeholders=placeholders)
            conn.execute(
                sql=query,
                params=tuple(orders_list),
                mapper=mapper,
            )
            return pick_list_orders

    def get_data_pick_list_order(
        self,                   # type: ProductionRepository
        pick_list_order_id,     # type: int
    ):
        # type: (...) -> str

        with self.connection as conn:
            return conn.first(
                sql=select_get_data_pick_list_order,
                params=(pick_list_order_id,),
            )

    def delete_outdated_pick_list_orders(
        self,           # type: ProductionRepository
        range_days,     # type: int
    ):
        # type: (...) -> None

        with self.connection as conn:
            self.loggers["INFO"].info("Start delete_outdated_pick_list_orders")

            result_datetime = datetime.utcnow() - timedelta(days=range_days)
            formatted_date = result_datetime.strftime("%Y-%m-%dT%H:%M:%S.%f")
            conn.execute(
                sql=purge_picklist_orders_query,
                params=(formatted_date,),
            )
            self._commit(
                connection=conn,
            )

            self.loggers["INFO"].info("End delete_outdated_pick_list_orders")

    def vacuum_database(
        self,   # type: ProductionRepository
    ):
        # type: (...) -> Dict[str, Union[Dict, int]]

        with self.connection as conn:
            db_info = {
                "db_info": self._get_db_file(
                    conn=conn,
                    db_name_to_find="production.db",
                )
            }
            db_file = db_info.get("db_info").get("db_file")
            db_info["old_file_size"] = self._get_file_size(file_name=db_file)

            db_name = db_info.get("db_info").get("db_name")
            query = "VACUUM {};".format(db_name)
            conn.execute(
                sql=query,
            )

            db_info["new_file_size"] = self._get_file_size(file_name=db_file)
            self.loggers["INFO"].info("Finished [vacuum_production_database]")

            return db_info

    def _remove_forbidden_tags(
        self,       # type: ProductionRepository
        items,      # type: List[Item]
        order_id,   # type: str
    ):
        # type: (...) -> None

        forbidden_tags = [
            "wait-prep-time"
        ]

        for item in items:
            for forbidden_tag in forbidden_tags:
                if item.has_tag(forbidden_tag):
                    item.remove_tag(forbidden_tag)
                    self.loggers["ERROR"].error("Denied TAG for Order ID {}: {}".format(order_id, forbidden_tag))

            if len(item.items) > 0:
                self._remove_forbidden_tags(
                    items=item.items,
                    order_id=order_id,
                )

    def _save_order_async(
        self,   # type: ProductionRepository
        order,  # type: ProductionOrder
    ):
        # type: (...) -> None

        with self.connection as conn:
            self.loggers["INFO"].info("Saving order: {} to Database".format(order.order_id))

            purge_order = 0
            order_data = base64.b64encode(Pickle.dumps(order, Pickle.HIGHEST_PROTOCOL))
            params = (
                order.order_id,
                order_data,
                order.created_at,
                purge_order,
            )
            conn.execute(
                sql=insert_query,
                params=params,
            )
            self._commit(
                connection=conn,
            )

    @staticmethod
    def _commit(
        connection,     # type: SynchronizedDbApiTemplate
    ):
        # type: (...) -> None

        try:
            connection.conn.commit()
        except (Exception,):
            connection.conn.rollback()
            raise

    @staticmethod
    def _get_db_file(
        conn,                # type: SynchronizedDbApiTemplate
        db_name_to_find,     # type: str
    ):
        # type: (...) -> Dict[str, str]

        def mapper(row):
            db_file = row[2]
            if db_file.endswith(db_name_to_find):
                return {
                    "db_name": row[1],
                    "db_file": db_file,
                }

        response = conn.first(
            sql="PRAGMA database_list;",
            mapper=mapper,
        )
        return response

    @staticmethod
    def _get_file_size(
        file_name,  # type: str
    ):
        # type: (...) -> int

        return os.path.getsize(file_name)


insert_query = """
INSERT OR REPLACE INTO ProductionOrders(
    OrderId,
    Object,
    CreatedAt,
    Purged
)
VALUES (?, ?, ?, ?)
"""

select_orders_id_query = """
SELECT OrderId FROM ProductionOrders WHERE PURGED = 0 ORDER BY CreatedAt DESC
"""

select_query = """
SELECT Object FROM ProductionOrders WHERE OrderId IN ({placeholders})
"""

get_orders_to_purge_query = """
SELECT OrderId FROM ProductionOrders WHERE CreatedAt < ? ORDER BY CreatedAt ASC LIMIT 750
"""

purge_orders_query = """
DELETE FROM ProductionOrders WHERE OrderId IN ({placeholders})
"""

insert_pick_list_query = """
INSERT OR REPLACE INTO PicklistOrders
(OrderId, OrderIdCount, JIT, Course, Data, CreatedAt, PrintOK)
VALUES
(?, ?, ?, ?, ?, ?, ?)
"""

select_get_pick_list_id_query = """
SELECT Id FROM PicklistOrders WHERE OrderId = ? AND OrderIdCount = ? AND JIT = ? AND Course = ?
"""

purge_picklist_orders_query = """
DELETE FROM PicklistOrders WHERE CreatedAt <= ?
"""

select_pick_list_orders_query = """
SELECT Id, JIT, Course FROM PicklistOrders WHERE OrderId in ({placeholders}) ORDER BY CreatedAt
"""

select_get_data_pick_list_order = """
SELECT Data FROM PicklistOrders WHERE Id == ?
"""
