import io
import json

from xml.etree import cElementTree as eTree
from typing import Optional, List, Dict
from datetime import datetime, timedelta
from threading import Lock
from time import sleep

import sysactions
from sysactions import set_custom, get_user_information
from msgbus import TK_SYS_NAK, TK_POS_GETPOSLIST, MBEasyContext

from dailygoals_services.model import NotCacheGoals, Goals, GoalsCache, ItemGoals, StoreGoals, OperatorGoals
from dailygoals_services.model.repository import DailyGoalsRepository
from externals.util import LoggerUtil


logger = LoggerUtil.get_logger_name(__name__)
DATE_FORMAT = "%Y-%m-%d"


class DailyGoalsService(object):
    def __init__(self, daily_goals_repository, mb_context):
        # type: (DailyGoalsRepository, MBEasyContext) -> None

        self.repository = daily_goals_repository
        self.lock = Lock()
        self.mb_context = mb_context
        sysactions.mbcontext = mb_context

        self.pos_list = None
        self.timestamp = None
        self.daily_goals = None
        self.today_goals = None
        self.store_id = None
        self.goals_cache_filename = None
        self.to_unicode = self._set_unicode()

    @staticmethod
    def _set_unicode():
        try:
            to_unicode = unicode
        except NameError:
            to_unicode = str
        return to_unicode

    def configure(self, store_id, goals_cache_filename):
        # type: (str, str) -> None

        logger.info('[configure] Begin')
        self.store_id = store_id
        self.goals_cache_filename = goals_cache_filename

        try:
            self._get_pos_list()
            if self.update_daily_goals():
                self.update_daily_sold(get_values_from_database=True)
            else:
                for pos_id in self.pos_list:
                    set_custom(pos_id, 'DAILY_GOALS', '{}')

        except Exception as _:
            logger.exception('[configure] Exception:')

        logger.info('[configure] End')

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

        retry_cnt = 10
        while retry_cnt:
            msg = self.mb_context.MB_EasySendMessage("PosController", TK_POS_GETPOSLIST)
            if msg.token == TK_SYS_NAK:
                retry_cnt -= 1
                sleep(1)
            else:
                self.pos_list = sorted(map(int, msg.data.split("\0")))
                retry_cnt = 0

    def update_daily_goals(self):
        # type: () -> Optional[bool]

        logger.info("[update_daily_goals] Begin")
        now = datetime.now()
        cur_date = now.strftime("%d/%m/%Y")

        self.timestamp = cur_date

        try:
            items_goals, store_goals = self._load_goals_from_cache()
            if not store_goals and not items_goals:
                raise NotCacheGoals()

            item_goals = self._populate_item_goals(items_goals)
            store_goals = self._populate_store_goals(store_goals, item_goals)

            with self.lock:
                self.today_goals = GoalsCache(store_goals, item_goals)
            logger.info('[update_daily_goals] End')
            return True

        except Exception as _:
            logger.exception('[update_daily_goals] Exception. Sort issue')
            raise NotImplementedError()

    def _save_to_file(self, goals):
        # type: (GoalsCache) -> None

        with io.open(self.goals_cache_filename, 'w', encoding='utf8') as outfile:
            json_goals = json.dumps(goals, default=lambda o: o.__dict__, sort_keys=True)
            outfile.write(self.to_unicode(json_goals))

    def _load_goals_from_cache(self):
        # type: () -> (List[Dict], List[Dict])

        with open(self.goals_cache_filename) as data_file:
            logger.info("[update_daily_goals] Taking goals from cache file")

            response_json = json.load(data_file)
            store_goals_json = response_json.get("storeGoals", [])
            items_goals_json = response_json.get("itemGoals", {})
        return items_goals_json, store_goals_json

    @staticmethod
    def _populate_item_goals(items_goals_json):
        # type: (List[Dict]) -> List[ItemGoals]

        item_goals = []
        if items_goals_json:
            for item in items_goals_json:
                start_date = datetime.strptime(item["startDate"], DATE_FORMAT)
                end_date = datetime.strptime(item["endDate"], DATE_FORMAT) + timedelta(days=1)
                if start_date < datetime.now() < end_date:
                    name = item.get("name")
                    part_codes = item.get("partCodes")
                    quantity = item.get("quantity")
                    quantity_operator_goal = item.get("operatorQuantity")
                    item_goals.append(ItemGoals(name, part_codes, quantity, quantity_operator_goal, 0))
        return item_goals

    @staticmethod
    def _populate_store_goals(store_goals_json, item_goals):
        # type: (List[Dict], List[ItemGoals]) -> StoreGoals

        store_goals = None
        if store_goals_json:
            for item in store_goals_json:
                start_date = datetime.strptime(item["startDate"], DATE_FORMAT)
                end_date = datetime.strptime(item["endDate"], DATE_FORMAT) + timedelta(days=1)
                if start_date < datetime.now() < end_date:
                    total_sales = item.get("totalSales")
                    average_sale_value = item.get("averageSaleValue")
                    operator_sale_goal = item.get("operatorSaleValue")
                    store_goals = StoreGoals(total_sales, average_sale_value, operator_sale_goal, item_goals)
                    break
        return store_goals

    def update_daily_sold(self, order_picture='', operator_id="", number_of_seats=1, get_values_from_database=False):
        # type: (eTree.Element, str, int, bool) -> None

        if not self.today_goals:
            return

        now = datetime.now()
        cur_date = now.strftime("%d/%m/%Y")
        if self.timestamp != cur_date:
            self.update_daily_goals()
            get_values_from_database = True

        if self.daily_goals is None:
            get_values_from_database = True

        with self.lock:
            if order_picture:
                self.daily_goals = self._update_total_sold_from_mem(order_picture, number_of_seats)
            elif get_values_from_database:
                store_goals = self.today_goals.store_goals
                items_goals = self.today_goals.item_goals
                operators_goals = []

                initial_date = datetime.now()
                end_date = datetime.now()
                self._populate_operators_sold_values(
                    end_date,
                    initial_date,
                    store_goals,
                    items_goals,
                    operators_goals
                )
                if store_goals is not None:
                    self._populate_store_sold_values(end_date, initial_date, operator_id, store_goals)

                self.daily_goals = Goals(store_goals, operators_goals)

            goals_str = json.dumps(self.daily_goals, default=lambda o: o.__dict__, sort_keys=True)
            logger.info(goals_str)
        for pos_id in self.pos_list:
            set_custom(pos_id, 'DAILY_GOALS', goals_str)

    def _update_total_sold_from_mem(self, xml_order, number_of_seats):
        # type: (eTree.Element, int) -> Optional[Goals]

        operator_id = xml_order.get("sessionId").split("user=")[1].split(",")[0]
        daily_goals = self.daily_goals
        if daily_goals is None:
            return

        if not any(x.id == operator_id for x in daily_goals.operator_goals):
            self._add_new_operator_to_mem(daily_goals, operator_id)

        added_tickets = False
        sale_lines = xml_order.findall("SaleLine")
        for line in sale_lines:
            self._process_operator_goal(added_tickets, daily_goals, line, number_of_seats, operator_id)

        return daily_goals

    def _process_operator_goal(self, added_tickets, daily_goals, line, number_of_seats, operator_id):
        part_code = line.get("partCode")
        for operator_daily_goal in daily_goals.operator_goals:
            if operator_daily_goal.id != operator_id:
                continue

            for daily_goal_item in operator_daily_goal.item_goals:
                if part_code not in daily_goal_item.part_codes:
                    continue

                self._update_quantity_sold(line, operator_daily_goal, part_code)
                self._update_quantity_sold(line, daily_goals.store_goals, part_code)

            if line.get("unitPrice") is not None:
                operator_daily_goal.amount_sold += float(line.get("qty")) * float(line.get("unitPrice"))
                daily_goals.store_goals.sold_quantity += float(line.get("qty")) * float(line.get("unitPrice"))

            if not added_tickets:
                operator_daily_goal.tickets += number_of_seats
                added_tickets = True

    @staticmethod
    def _update_quantity_sold(line, operator_daily_goal, part_code):
        for item_goal in operator_daily_goal.item_goals:
            if part_code in item_goal.part_codes:
                item_goal.quantity_sold += float(line.get("qty"))

    def _populate_operators_sold_values(self, end_date, initial_date, store_goals, item_goals, operators_goals):
        # type: (datetime, datetime, StoreGoals, List[ItemGoals], List[OperatorGoals]) -> None

        users_tickets = sysactions.generate_report("generate_tickets_report", initial_date)
        logger.info(users_tickets)
        users_tickets_json = json.loads(users_tickets)

        for op in self._get_all_opened_users():
            operator_item_goals = []
            for item_goal in item_goals:
                operator_item_goals.append(ItemGoals(
                    item_goal.name,
                    item_goal.part_codes,
                    item_goal.quantity_goal,
                    item_goal.quantity_operator_goal,
                    0)
                )

            user_tickets = list({int(v) for k, v in users_tickets_json.items() if k.startswith(op)})
            user_tickets = 0 if len(user_tickets) == 0 else user_tickets[0]

            operator_goals = OperatorGoals(
                op,
                store_goals.operator_sale_goal if store_goals is not None else 0,
                0,
                store_goals.average_sale_goal if store_goals is not None else 0,
                operator_item_goals,
                user_tickets
            )

            operator_goals.amount_sold = self._get_current_sold_qty_items_from_database(
                operator_goals.item_goals,
                initial_date,
                end_date,
                op
            )
            operators_goals.append(operator_goals)

    def _populate_store_sold_values(self, end_date, initial_date, operator_id, store_goals):
        # type: (datetime, datetime, int, List[StoreGoals]) -> None

        store_goals.sold_quantity = self._get_current_sold_qty_items_from_database(
            store_goals.item_goals,
            initial_date,
            end_date,
            operator_id
        )

    def _add_new_operator_to_mem(self, daily_goals, operator_id):
        # type: (Goals, int) -> None

        store_goals = self.today_goals.store_goals
        items_goals = self.today_goals.item_goals
        operator_item_goals = []
        for item_goal in items_goals:
            operator_item_goals.append(
                ItemGoals(
                    item_goal.name,
                    item_goal.part_codes,
                    item_goal.quantity_goal,
                    item_goal.quantity_operator_goal,
                    0
                )
            )
        operator_goals = OperatorGoals(
            operator_id,
            store_goals.operator_sale_goal,
            0,
            store_goals.average_sale_goal,
            operator_item_goals,
            0
        )
        daily_goals.operator_goals.append(operator_goals)

    @staticmethod
    def _get_all_opened_users():
        # type: () -> List
        users_list = []

        for user in eTree.XML(get_user_information()).findall(".//user"):
            users_list.append(user.get("UserId"))

        return users_list

    def _get_current_sold_qty_items_from_database(self, items, initial_date, end_date, operator_id):
        # type: (List, datetime, datetime, int) -> float

        logger.info('[_get_current_sold_qty_items_from_database] Begin')

        total_sold = 0
        pmix_order_items = json.loads(
            sysactions.generate_report(
                "pmix_order_items",
                initial_date,
                end_date,
                self.store_id,
                operator_id
            )
        )

        for pmix_item in pmix_order_items:
            for item in items:
                for part_code in item.part_codes:
                    if int(part_code) == pmix_item['pcode']:
                        item.quantity_sold += pmix_item['quantity']
            total_sold += pmix_item['quantity'] * float(pmix_item["unit_price"])

        logger.info('[_get_current_sold_qty_items_from_database] End')

        return total_sold
