import io
import json

from xml.etree import cElementTree as eTree
from typing import (
    Any,
    Dict,
    List,
    Optional,
    Tuple,
    Union,
)
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 (
    Goals,
    GoalsCache,
    ItemGoals,
    NotCacheGoals,
    OperatorGoals,
    PartCode,
    StoreGoals,
)
from dailygoals_services.model.repository import (
    DailyGoalsRepository,
    OrderRepository,
    ProductRepository,
)
from externals.util import (
    LoggerUtil,
    contexts_match,
)

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


class DailyGoalsService(object):
    def __init__(
        self,
        daily_goals_repository,     # type: DailyGoalsRepository
        order_repository,           # type: OrderRepository
        product_repository,         # type: ProductRepository
        mb_context,                 # type: MBEasyContext
    ):
        # type: (...) -> None

        self.repository = daily_goals_repository
        self.order_repository = order_repository
        self.product_repository = product_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,               # type: str
        goals_cache_filename,   # type: str
    ):
        # type: (...) -> 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(
                        posid=pos_id,
                        key='DAILY_GOALS',
                        value='{}',
                    )

        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(
                dest_name="PosController",
                token=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=store_goals,
                    item_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
    ):
        # type: (...) -> 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

    def _populate_item_goals(
        self,
        items_goals_json,   # type: List[Dict]
    ):
        # type: (...) -> 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 = self._build_part_codes_objects(part_codes=item.get("partCodes"))
                    part_codes_set = self._build_part_codes_objects(part_codes=item.get("partCodesSet"))
                    quantity = item.get("quantity")
                    quantity_operator_goal = item.get("operatorQuantity")
                    item_goals.append(ItemGoals(
                        name=name,
                        part_codes=part_codes,
                        part_codes_set=part_codes_set,
                        quantity_goal=quantity,
                        quantity_operator_goal=quantity_operator_goal,
                        quantity_sold=0,
                    ))
        return item_goals

    @staticmethod
    def _build_part_codes_objects(
        part_codes,     # type: List
    ):
        # type: (...) -> List

        if not part_codes:
            return []

        part_code_objects = []
        for part_code in part_codes:
            obj = PartCode(
                id=part_code.get("id"),
                context=part_code.get("context"),
            )
            part_code_objects.append(obj)
        return part_code_objects

    @staticmethod
    def _populate_store_goals(
        store_goals_json,   # type: List[Dict]
        item_goals,         # type: List[ItemGoals]
    ):
        # type: (...) -> 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(
                        amount_goal=total_sales,
                        average_sale_goal=average_sale_value,
                        operator_sale_goal=operator_sale_goal,
                        item_goals=item_goals,
                    )
                    break
        return store_goals

    def update_daily_sold(
        self,
        order_picture='',                   # type: Optional[eTree.Element]
        operator_id='',                     # type: Optional[str]
        number_of_seats=1,                  # type: Optional[int]
        get_values_from_database=False,     # type: Optional[bool]
    ):
        # type: (...) -> 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=order_picture,
                    number_of_seats=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=end_date,
                    initial_date=initial_date,
                    store_goals=store_goals,
                    item_goals=items_goals,
                    operators_goals=operators_goals,
                )
                if store_goals is not None:
                    self._populate_store_sold_values(
                        end_date=end_date,
                        initial_date=initial_date,
                        operator_id=operator_id,
                        store_goals=store_goals,
                    )

                self.daily_goals = Goals(
                    store_goals=store_goals,
                    operator_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(
                posid=pos_id,
                key='DAILY_GOALS',
                value=goals_str,
            )

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

        operator_id = order_picture.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=daily_goals,
                operator_id=operator_id,
            )

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

        return daily_goals

    def _process_operator_goal(
        self,
        added_tickets,      # type: bool
        daily_goals,        # type: Goals
        line,               # type: eTree.Element
        number_of_seats,    # type: int
        operator_id,        # type: str
    ):
        # type (...) -> bool

        part_code = line.get("partCode")
        context = line.get("itemId")
        for operator_daily_goal in daily_goals.operator_goals:
            if operator_daily_goal.id != operator_id:
                continue

            for index, daily_goal_item in enumerate(operator_daily_goal.item_goals):
                if not self._check_part_code_context(
                    part_code=part_code,
                    context=context,
                    part_codes=daily_goal_item.part_codes,
                ):
                    continue

                operator_daily_goal = self._update_sold_info(
                    line=line,
                    daily_goal=operator_daily_goal,
                    part_code=part_code,
                    context=context,
                    key="amount_sold",
                    daily_goal_index=index,
                )

            if daily_goals.store_goals:
                for index, store_goal_item in enumerate(daily_goals.store_goals.item_goals):
                    if not self._check_part_code_context(
                        part_code=part_code,
                        context=context,
                        part_codes=store_goal_item.part_codes,
                    ):
                        continue

                    daily_goals.store_goals = self._update_sold_info(
                        line=line,
                        daily_goal=daily_goals.store_goals,
                        part_code=part_code,
                        context=context,
                        key="sold_quantity",
                        daily_goal_index=index,
                    )

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

        self._update_goal_percentage(
            item_goals=daily_goals.store_goals.item_goals,
            is_mem=True,
            store_goal=True,
        )

        return added_tickets

    @staticmethod
    def _check_part_code_context(
        part_code,      # type: str
        context,        # type: str
        part_codes,     # type: List
    ):
        # type: (...) -> bool

        for item in part_codes:
            context_match = contexts_match(
                item_context=str(context),
                part_code_context=str(item.context),
            )
            if part_code == item.id and (item.context in [None, ""] or context_match):
                return True

        return False

    def _update_sold_info(
        self,
        line,               # type: eTree.XML
        daily_goal,         # type: Any[Union[OperatorGoals, StoreGoals]]
        part_code,          # type: str
        context,            # type: str
        key,                # type: str
        daily_goal_index,   # type: int
    ):
        # type: (...) -> Any[Union[OperatorGoals, StoreGoals]]

        if self._check_part_code_context(
            part_code=part_code,
            context=context,
            part_codes=daily_goal.item_goals[daily_goal_index].part_codes,
        ):
            daily_goal.item_goals[daily_goal_index].quantity_sold += float(line.get("multipliedQty"))
            price = line.get("unitPrice") or line.get("addedUnitPrice") or 0.0

            if line.get("multipliedQty") is None:
                return daily_goal

            if key == "amount_sold":
                daily_goal.amount_sold += float(line.get("multipliedQty")) * float(price)
            elif key == "sold_quantity":
                daily_goal.sold_quantity += float(line.get("multipliedQty")) * float(price)
                self._update_sold_part_codes(
                    daily_goal_item=daily_goal.item_goals[daily_goal_index],
                    context=context,
                    part_code=part_code,
                    qty=float(line.get("multipliedQty")),
                )

        return daily_goal

    @staticmethod
    def _update_sold_part_codes(
        daily_goal_item,    # type: ItemGoals
        context,            # type: str
        part_code,          # type: str
        qty,                # type: float
    ):
        # type: (...) -> None

        part_codes_type = [daily_goal_item.part_codes, daily_goal_item.part_codes_set]
        for part_code_item in part_codes_type:
            for item_part_code in part_code_item:
                context_match = contexts_match(
                    item_context=str(context),
                    part_code_context=str(item_part_code.context),
                )
                if context_match and part_code == item_part_code.id:
                    if item_part_code.quantity_sold is None:
                        item_part_code.quantity_sold = 0
                    item_part_code.quantity_sold += qty

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

        users_tickets = self._get_tickets(business_period=initial_date)
        logger.info(users_tickets)

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

            user_tickets = list({int(v) for k, v in users_tickets.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(
                items=operator_goals.item_goals,
                initial_date=initial_date,
                end_date=end_date,
                operator_id=op,
                store_goal=False,
            )
            self._update_goal_percentage(
                item_goals=operator_goals.item_goals,
                operator_id=op,
                store_goal=False,
            )
            operators_goals.append(operator_goals)

    def _get_tickets(
        self,
        business_period,    # type: datetime
    ):
        # type: (...) -> Dict

        date_format = "%Y-%m-%d"
        business_period = business_period.strftime(date_format)
        user_table_orders = self.order_repository.get_paid_ticket(business_period=business_period)
        user_orders = self.order_repository.get_paid_orders_ticket(business_period=business_period.replace("-", ""))
        tickets = {}
        for table_order in user_table_orders:
            user = table_order[0]
            quantity = int(table_order[1])
            tickets[user] = quantity

        for order in user_orders:
            if order[0] is None:
                continue

            user = order[0].split("user=")[1].split(",")[0]
            quantity = int(order[1])
            if user in tickets:
                tickets[user] += quantity
                continue
            tickets[user] = quantity

        return tickets

    def _populate_store_sold_values(
        self,
        end_date,       # type: datetime
        initial_date,   # type: datetime
        operator_id,    # type: str
        store_goals,    # type: StoreGoals
    ):
        # type: (...) -> None

        operator_id = "0" if operator_id in ["", "None"] else operator_id
        store_goals.sold_quantity = self._get_current_sold_qty_items_from_database(
            items=store_goals.item_goals,
            initial_date=initial_date,
            end_date=end_date,
            operator_id=operator_id,
            store_goal=True,
        )
        self._update_goal_percentage(
            item_goals=store_goals.item_goals,
            store_goal=True,
        )

    def _add_new_operator_to_mem(
        self,
        daily_goals,    # type: Goals
        operator_id,    # type: int
    ):
        # type: (...) -> 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,              # type: List
        initial_date,       # type: datetime
        end_date,           # type: datetime
        operator_id,        # type: str
        key="part_codes",   # type: Optional[str]
        store_goal=False,   # type: Optional[bool]
    ):
        # type: (...) -> Union[list, Union[list, float]]

        logger.info('[_get_current_sold_qty_items_from_database] Begin')

        total_sold = 0
        order_items = json.loads(
            self._get_order_items_from_database(
                initial_date=initial_date,
                end_date=end_date,
                operator_id=operator_id,
            )
        )

        is_part_code_set = key == 'part_codes_set'
        for item in items:
            total_sold_item, quantity_sold_item = self._get_sold_info(
                part_codes=item[key],
                order_items=order_items,
                store_goal=store_goal,
            )
            if not is_part_code_set:
                item.quantity_sold += quantity_sold_item
                total_sold += total_sold_item

        logger.info('[_get_current_sold_qty_items_from_database] End')

        if is_part_code_set:
            return items[0]

        return total_sold

    @staticmethod
    def _get_sold_info(
        part_codes,         # type: List
        order_items,        # type: List
        store_goal=False,   # type: Optional[bool]
    ):
        # type: (...) -> Tuple

        total_sold = 0
        quantity_sold = 0
        for order_item in order_items:
            for part_code in part_codes:
                if int(part_code.id) != order_item['pcode']:
                    continue
                context_match = contexts_match(
                    item_context=str(order_item['context']),
                    part_code_context=str(part_code.context),
                )
                if part_code.context is None or context_match:
                    quantity_sold += order_item['quantity']
                    unit_price = float(order_item["unit_price"] if order_item["unit_price"] != "None" else 0.0)
                    total_sold += order_item['quantity'] * unit_price
                    if store_goal:
                        if part_code.quantity_sold is None:
                            part_code.quantity_sold = 0
                        part_code.quantity_sold += order_item['quantity']

        return total_sold, quantity_sold

    def _get_order_items_from_database(
        self,
        initial_date,   # type: str
        end_date,       # type: str
        operator_id,    # type: str
    ):
        # type: (...) -> str

        paid_orders = self.order_repository.get_paid_orders(
            operator_id=operator_id,
            initial_date=initial_date,
            end_date=end_date,
        )
        order_items = self.order_repository.get_orders_items(paid_orders=paid_orders)
        order_items = filter(lambda x: x != "error", order_items)
        order_items = self.product_repository.get_order_items_names(order_items=order_items)
        order_items = sorted(order_items, key=lambda order_item: order_item.context)
        return json.dumps(map(lambda x: x.__dict__, order_items))

    def _update_goal_percentage(
        self,
        item_goals,         # type: ItemGoals
        operator_id='0',    # type: Optional[str]
        is_mem=False,       # type: Optional[bool]
        store_goal=False,   # type: Optional[bool]
    ):
        # type: (...) -> None

        for item in item_goals:
            if not item.part_codes_set:
                continue

            part_code_set_info = item

            if not is_mem:
                initial_date = datetime.now()
                end_date = datetime.now()
                part_code_set_info = self._get_current_sold_qty_items_from_database(
                    items=[item],
                    initial_date=initial_date,
                    end_date=end_date,
                    operator_id=operator_id,
                    key='part_codes_set',
                    store_goal=store_goal,
                )

            item.total_goal_percentage = round(item.quantity_sold * 100 / item.quantity_goal, 2)
            quantity_sold_set = 0
            for part_code_set in part_code_set_info.part_codes_set:
                if part_code_set.quantity_sold is not None:
                    quantity_sold_set = part_code_set.quantity_sold + quantity_sold_set
            if item.quantity_sold != 0:
                item.part_code_set_percentage = round(quantity_sold_set * 100 / item.quantity_sold, 2)
            else:
                item.part_code_set_percentage = 0
