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

import json
from datetime import datetime
from logging import Logger

from cfgtools import Configuration
from typing import (
    Any,
    Dict,
    List,
    Optional
)
from xml.etree import cElementTree as eTree

import sysactions
from bustoken import TK_REMOTE_ORDER_CHECK_RUPTURA_DIFF_ITEMS
from msgbus import MBEasyContext, TK_SYS_ACK, FM_PARAM

from rupture_events import RuptureEvents, RuptureEventTypes
from rupture_repository import RuptureRepository
from model import RuptureItem, RuptureProduct

AUTOMATIC_RUPTURE_AFTER_SALE = "AUTOMATIC_RUPTURE_AFTER_SALE"


class RuptureService(object):
    def __init__(self, mb_context, logger, rupture_repository, configs):
        # type: (MBEasyContext, Logger, RuptureRepository, Configuration) -> None
        super(RuptureService, self).__init__()

        sysactions.mbcontext = mb_context
        self.mb_context = mb_context
        self.logger = logger
        self.rupture_repository = rupture_repository
        self._start = configs.find_value("Rupture.CleanRuptureTimeWindow.StartTime", None)
        self._end = configs.find_value("Rupture.CleanRuptureTimeWindow.EndTime", None)
        self._interval = configs.find_value("Rupture.CleanRuptureTimeWindow.Interval", "00:00:30")
        self._is_clean_enabled = configs.find_value("Rupture.CleanRuptureTimeWindow.Enabled", "false").lower() == "true"
        self._daily_dishes_event_types = ['STORE_ORDER', 'PAID']
        self._daily_dishes_products_cache = None
        self._last_auto_clean_event = None

    def load_daily_dishes_products(self):
        self._daily_dishes_products_cache = self.rupture_repository.get_all_daily_dishes()

    def get_enabled_items(self):
        self.logger.info("[START] Getting enabled items")
        enabled_items = self.rupture_repository.get_enabled_items()
        self.logger.info("[ END ] Getting enabled items")

        return {x.product_code: x.product_name for x in enabled_items}

    def get_enabled_items_list(self):
        self.logger.info("[START] Getting enabled items")
        enabled_items = self.rupture_repository.get_enabled_items()
        self.logger.info("[ END ] Getting enabled items")

        return self.get_rupture_products(enabled_items)

    def get_available_daily_dishes(
        self,
        exclude_daily_dishes    # type: List[str]
    ):
        # type: (...) -> List[RuptureProduct]

        self.logger.info("[START] Getting rupture by quantity list")
        rupture_by_qty_list = self.rupture_repository.get_available_daily_dishes_items(exclude_daily_dishes)
        self.logger.info("[END] Getting rupture by quantity list")

        return self.get_rupture_products(rupture_by_qty_list)

    def get_daily_dishes(
        self
    ):
        # type: () -> Dict[str, Any]

        self.logger.info("[START] Getting rupture daily dishes")

        all_disabled_items = []

        database_daily_dishes_ruptured = self.rupture_repository.get_daily_dishes_items()
        daily_dishes_ruptured = self.get_rupture_products(database_daily_dishes_ruptured)
        all_disabled_items.extend(daily_dishes_ruptured)

        items_ruptured = self.rupture_repository.get_disabled_items()
        all_disabled_items.extend(items_ruptured)

        disabled_product_codes = [str(dish.product_code) for dish in all_disabled_items]
        available_daily_dishes = self.get_available_daily_dishes(disabled_product_codes)

        daily_dishes = {
            "available": available_daily_dishes,
            "quantity_set": daily_dishes_ruptured
        }

        self.logger.info("[END] Getting rupture daily dishes")

        return daily_dishes

    def get_disabled_items(self):
        self.logger.info("[START] Getting disabled items")
        disabled_items = self.rupture_repository.get_disabled_items()
        self.logger.info("[ END ] Getting disabled items")

        return {x.product_code: x.product_name for x in disabled_items}

    def get_disabled_items_list(self):
        self.logger.info("[START] Getting disabled items")
        disabled_items = self.rupture_repository.get_disabled_items()
        self.logger.info("[ END ] Getting disabled items")

        return self.get_rupture_products(disabled_items)

    def get_rupture_products(self, items):
        # type: (List[RuptureItem]) -> List[RuptureProduct]
        item_list = []
        for item in items:
            product_code = item.product_code
            product_name = item.product_name
            products_composition = [item.product_part_code] if item.product_part_code else []
            product_quantity = item.product_quantity
            ruptured_product = RuptureProduct(
                product_code=product_code,
                product_name=product_name,
                products_composition=products_composition,
                product_quantity=product_quantity
            )

            if product_quantity == 0:
                delattr(ruptured_product, "product_quantity")

            if not next((x for x in item_list if x.product_code == product_code), None):
                item_list.append(ruptured_product)
            else:
                for list_item in item_list:
                    if self._must_add_product_composition(list_item, product_code, products_composition):
                        list_item.products_composition.extend(products_composition)
                        break
        return item_list

    @staticmethod
    def _must_add_product_composition(list_item, product_code, products_composition):
        if list_item.product_code != product_code:
            return False

        return products_composition is not None and products_composition not in list_item.products_composition

    def set_last_clean_event(
        self,
        period,  # type: str
    ):
        # type: (...) -> None
        self._last_auto_clean_event = period

    def check_items(self, changes_dict, session_id, all_items):
        message = "[START] Checking items in rupture. SessionId #{} - Items: {}"
        self.logger.info(message.format(session_id, changes_dict))

        items_to_disable = changes_dict.get("disabled") or []

        if not items_to_disable:
            self.logger.info("Has no items in rupture")
            return

        disabled_items = [] if all_items == "True" else list(self.get_disabled_items().iterkeys())
        filtered_items = set(map(lambda y: y["product_code"], items_to_disable)).difference(disabled_items)

        data = str(",".join(filtered_items) + "|" + ",".join(""))

        msg = self.mb_context.MB_EasySendMessage(
            dest_name="RemoteOrder",
            token=TK_REMOTE_ORDER_CHECK_RUPTURA_DIFF_ITEMS,
            format=FM_PARAM,
            data=data,
            timeout=180 * 1000000
        )
        if msg.token == TK_SYS_ACK:
            self.logger.info("[END] Checking items in rupture")
            return "" if msg.data == "" else msg.data.split(",")

        raise Exception("Cannot obtain remote order rupture diff items. Response: {}".format(msg.data))

    def update_items(self, changes_json, session_id, is_json=True):
        self.logger.info("[START] Updating items")

        changes_dict = json.loads(changes_json) if is_json else changes_json
        items_to_enable = changes_dict.get("enabled", [])
        items_to_disable = changes_dict.get("disabled", [])
        disabled_items = self.get_disabled_items()

        self.rupture_repository.insert_rupture_update(session_id, items_to_disable, items_to_enable, disabled_items)

        event_data = str(datetime.now().date())
        self.rupture_repository.insert_event_state(event_data)

        snapshot_id = self.create_full_snapshot()

        self._check_rupture_by_quantity(
            items_to_disable=items_to_disable,
            user_id=session_id
        )

        self.logger.info("[END] Updating items")

        return snapshot_id

    def update_daily_dishes(
        self,
        changes_json,   # type: str
        user_id         # type: str
    ):
        # type: (...) -> None

        self.logger.info("[START] Updating daily dishes")

        dishes_update = json.loads(changes_json)
        dishes_to_set_quantity = dishes_update.get("set", [])
        dishes_to_unset_quantity = dishes_update.get("unset", [])
        self.rupture_repository.insert_daily_dishes_update(
            dishes_to_set_quantity=dishes_to_set_quantity,
            dishes_to_unset_quantity=dishes_to_unset_quantity,
            user_id=user_id
        )

        self._check_rupture(
            items_to_enable=dishes_to_set_quantity,
            user_id=user_id
        )

        update_dishes = dict()
        remove_dishes = []
        for dish in dishes_to_set_quantity:
            if dish["product_quantity"]:
                update_dishes[dish["product_code"]] = dish["product_quantity"]
            else:
                remove_dishes.append(dish["product_code"])

        remove_dishes.extend([d["product_code"] for d in dishes_to_unset_quantity])

        self._daily_dishes_products_cache.update(update_dishes)
        for product_code in remove_dishes:
            self._daily_dishes_products_cache.pop(product_code)

        self.logger.info("[END] Updating daily dishes")

    def get_clean_time(self):
        return {
            "start": self._start,
            "end": self._end,
            "interval": self._interval
        }

    def create_full_snapshot(self):
        self.logger.info("[START] Creating rupture full snapshot")

        disabled_items = list(self.get_disabled_items().iterkeys())
        self.rupture_repository.mark_pending_ruptures_as_processed()
        snapshot_id = self.rupture_repository.insert_rupture_snapshot(disabled_items)
        self.rupture_repository.insert_rupture_current_state(snapshot_id)

        self._notify_rupture_changed(disabled_items, snapshot_id)

        self.logger.info("[ END ] Creating rupture full snapshot")

        return snapshot_id

    def clean_rupture_items(self, evt_type=None):
        if evt_type and evt_type in [RuptureEventTypes.CleanRuptureThread, RuptureEventTypes.CleanRuptureOnOpenDay]:
            event = 'AUTO_CLEAN_OPEN_DAY' if evt_type == RuptureEventTypes.CleanRuptureOnOpenDay else 'AUTO_CLEAN_EVENT'
            create_new_snapshot = self._clean_rupture_by_event(event_type=event)
            if create_new_snapshot:
                self.create_full_snapshot()
            self.rupture_repository.mark_clean_rupture_event_as_processed()
        elif evt_type and evt_type != RuptureEvents.CleanRupture:
            self._clean_rupture_by_button(evt_type)
            self.create_full_snapshot()
            self.rupture_repository.mark_clean_rupture_event_as_processed()

    def clean_rupture_with_catalog_update(self):
        create_new_snapshot = self._clean_rupture_by_event('AUTO_CLEAN_CATALOG_UPDATE')
        if create_new_snapshot:
            self.create_full_snapshot()
        self.rupture_repository.mark_clean_rupture_event_as_processed()

    def _notify_rupture_changed(self, disabled_items, snapshot_id):
        params = {
            "disabled_items": disabled_items,
            "snapshot_id": snapshot_id
        }
        self.mb_context.MB_EasyEvtSend("RupturaDataUpdated", "", json.dumps(params))
        self.logger.info("[RuptureDataUpdated] notified!")

    def clean_rupture_totem(self):
        try:
            pos_list = sysactions.get_poslist()
            for pos in pos_list:
                sysactions.set_custom(pos, "RUPTURA_ITEMS", "")
        except BaseException as e:
            self.logger.info('[ruptureItems] Exception {}'.format(e))

    def _clean_rupture_by_event(self, event_type='AUTO_CLEAN_EVENT'):
        # type: (Optional[str]) -> bool

        self.logger.info("[START] Cleaning all rupture products by event")

        if event_type == 'AUTO_CLEAN_OPEN_DAY':
            should_run = self._is_time_available()
            not_open_day_event = should_run and not self.rupture_repository.check_if_has_open_day_event()
            need_to_clean_ruptures = not_open_day_event and self.rupture_repository.check_if_need_to_clean_ruptures()
        else:
            need_to_clean_ruptures = self.rupture_repository.check_if_need_to_clean_ruptures()

        if not need_to_clean_ruptures:
            self.logger.info("[END] Cleaning all rupture products by event. No Ruptures to clean!")
            return False

        self.rupture_repository.clean_rupture_items(event_type)
        self.clean_rupture_totem()
        self.rupture_repository.mark_clean_rupture_event_as_processed()

        self.logger.info("[END] Cleaning all rupture products by event. Ruptures cleaned!")
        return True

    def _is_time_available(
        self,
    ):
        # type:  (...) -> bool
        now = datetime.now().time().isoformat()
        today = datetime.today().strftime("%Y%m%d")
        available_business_time = True
        if self._last_auto_clean_event:
            available_business_time = today != self._last_auto_clean_event
        return self._start and self._start <= now and available_business_time and self._is_clean_enabled

    def _clean_rupture_by_button(self, evt_type):
        self.logger.info("[START] Cleaning all rupture products by button")
        model = sysactions.get_model(evt_type)

        manager_id = sysactions.get_custom(model, "Last Manager ID")
        enable_session_id = "{} / {} / {}".format(manager_id, sysactions.get_operator_session(model), "CLEAN_BUTTON")

        self.rupture_repository.clean_rupture_items_by_btn(enable_session_id)
        self.logger.info("[ END ] Cleaning all rupture products by button")

    def _send_to_rupture(
        self,
        rupture_items,  # type: str
    ):
        # type: (...) -> None

        daily_dishes = self.get_daily_dishes()
        all_daily_dishes = daily_dishes["available"] + daily_dishes["quantity_set"]
        product_target = []
        for p in all_daily_dishes:
            if p.product_code in rupture_items:
                product_target.append(
                    {
                        'product_code': p.product_code,
                        'product_name': p.product_name,
                        'products_composition': p.products_composition
                    }
                )

        if len(product_target) > 0:
            self.update_items(
                changes_json={'disabled': product_target, 'enabled': []},
                session_id=AUTOMATIC_RUPTURE_AFTER_SALE,
                is_json=False
            )

    def _update_daily_dishes_quantity(
        self,
        product_code,   # type: str
        quantity,       # type: int
    ):
        # type: (...) -> List[str]

        quantity = max(quantity, 0)
        user = None if quantity > 0 else AUTOMATIC_RUPTURE_AFTER_SALE
        self.rupture_repository.change_dishes_quantity(
            product_code=product_code,
            quantity=quantity,
            user=user
        )

        if quantity > 0:
            self._daily_dishes_products_cache[product_code] = quantity
            return []

        self._daily_dishes_products_cache.pop(product_code)
        return [product_code]

    @staticmethod
    def _must_verify_rupture_by_quantity(
        order  # type: eTree
    ):
        # type: (...) -> bool

        if order.find('Order').get('subType') in ('SPLIT_SERVICE', 'SLICED_SERVICE'):
            return False

        order_stored_times = map(
            lambda x: x.get('state'),
            order.findall('Order/StateHistory/State')
        ).count('STORED')

        is_table_order = order.find("Order/CustomOrderProperties/OrderProperty[@key='TABLE_ID']") is not None
        is_delivery_and_stored = order.find('Order').get('saleTypeDescr') == "DELIVERY" and order_stored_times > 0

        order_state = order.find("Order").get("state")
        if is_table_order or is_delivery_and_stored:
            return order_state == "STORED" and order_stored_times == 1

        return order_state == "PAID"

    def decrement_item_in_rupture_by_quantity(
        self,
        data,       # type: str
        event_type  # type: unicode
    ):
        # type: (...) -> None

        if event_type in self._daily_dishes_event_types:
            order_picture = eTree.XML(data)
            if not self._must_verify_rupture_by_quantity(order=order_picture):
                return

            rupture_items = []
            for sale_line in order_picture.findall(".//SaleLine"):
                part_code = sale_line.get("partCode")
                if part_code not in self._daily_dishes_products_cache:
                    continue

                sale_line_quantity = int(float(sale_line.get("multipliedQty")))
                if sale_line_quantity == 0:
                    continue

                new_quantity = self._daily_dishes_products_cache[part_code] - sale_line_quantity
                rupture_items.extend(
                    self._update_daily_dishes_quantity(
                        product_code=part_code,
                        quantity=new_quantity
                    )
                )

            if rupture_items:
                self._send_to_rupture(rupture_items=rupture_items)

    def _check_rupture_by_quantity(
        self,
        items_to_disable,   # type: List[Dict]
        user_id             # type: str
    ):
        # type: (...) -> None

        disabled_items = []
        for products in items_to_disable:
            disabled_items.extend(products["products_composition"])
            disabled_items.append(products["product_code"])

        rupture_by_qty_list = self.rupture_repository.get_daily_dishes_items()
        daily_dishes_id = [products.product_code for products in rupture_by_qty_list]

        remove_ids = list(set(disabled_items).intersection(daily_dishes_id))
        dishes_to_unset_quantity = [{"product_code": value} for value in remove_ids]

        if remove_ids:
            self.rupture_repository.insert_daily_dishes_update(
                dishes_to_set_quantity=[],
                dishes_to_unset_quantity=dishes_to_unset_quantity,
                user_id=user_id
            )

    def _check_rupture(
        self,
        items_to_enable,    # type: List[Dict]
        user_id             # type: str
    ):
        # type: (...) -> None

        enabled_items = []
        disabled_items = []
        for products in items_to_enable:
            if products["product_quantity"] == 0:
                disabled_items.append(products)
            else:
                enabled_items.append(products)

        changes_dict = {
            "disabled": disabled_items,
            "enabled": enabled_items
        }

        if enabled_items or disabled_items:
            self.update_items(
                changes_json=changes_dict,
                session_id=user_id,
                is_json=False
            )
