import json
import logging

from datetime import datetime

from msgbus import (
    FM_PARAM,
    TK_POS_BUSINESSEND,
    TK_SYS_ACK,
)
from helper import translate_pos_label
from mw_helper import show_message_dialog, show_message_options_dialog
from sysactions import (
    action,
    get_business_period,
    get_model,
    get_poslist,
    print_report,
    send_message,
    set_custom,
    translate_message,
)
from systools import sys_log_info, sys_log_error, sys_log_exception
from bustoken import (
    TK_POS_HAS_BINARY_UPDATE_TO_APPLY,
    TK_POS_UPDATER_PERFORM_BINARIES_UPDATE,
)
from actions.util import list_opened_users, get_operator_name
from actions.pos.check_table_status import has_tab_or_tables_opened, show_open_table_dialog
from actions.util.get_menu_user_logged import get_menu_manager_authenticated_user
from actions.models.mw_sys_errors import MwSysErrors
from application.interactor import MaintenanceInteractor

from .. import mb_context

logger = logging.getLogger("AcceptUpdateBinary")
pos_list = ()


@action
def close_day(
    pos_id,                         # type: str
    pos_id_to_show_message=None,    # type: str
):
    # type: (...) -> bool

    if pos_id_to_show_message is None:
        pos_id_to_show_message = pos_id
    from msgbusboundary import MsgBusTableService as tableService

    error_dict = {}
    model = get_model(pos_id)
    period = get_business_period(model)
    is_table_service = model.find("WorkingMode").get("usrCtrlType") == "TS"

    close_day_pos_list = [pos_id]
    if is_table_service:
        close_day_pos_list = tableService().get_exclusive_pos_list(user_control_type="TS")

    if is_table_service and has_tab_or_tables_opened(period, pos_id):
        show_open_table_dialog(pos_id=pos_id_to_show_message)
        return False

    opened_users = list_opened_users(pos_id)
    _fill_pos_list_with_can_close_day_pos(error_dict, period, close_day_pos_list, opened_users)
    error_messages = _parse_error_messages(model, error_dict)

    if len(error_messages) > 0:
        show_message_dialog(
            pos_id=pos_id_to_show_message,
            message="$CANNOT_CLOSE_DAYS|{}".format("\\".join(error_messages)),
        )
        return False

    if len(close_day_pos_list) == 0:
        show_message_dialog(
            pos_id=pos_id_to_show_message,
            message="$HAS_NO_POS_TO_CLOSE",
        )
        return False

    close_date = datetime.strptime(period, '%Y%m%d').strftime('%d/%m/%Y')
    close_day_pos_list = [int(x) for x in close_day_pos_list]
    pos = translate_pos_label(model=model)
    confirmation_message = "$CONFIRM_CLOSE_DAY|{0}|{1}".format(close_date, "{} {}".format(pos, close_day_pos_list))
    resp = show_message_options_dialog(
        pos_id=pos_id_to_show_message,
        buttons="$OK|$PREVIOUS",
        title="$INFORMATION",
        message=confirmation_message,
    )

    if resp is None or resp == 1:
        return False

    try:
        sys_log_info("Closing business day for POS's: {}".format(close_day_pos_list))
        error_dict = {}
        succeed_pos_id = []

        for pos_no in close_day_pos_list:
            _perform_close_day(pos_no, succeed_pos_id, error_dict)
            set_custom(pos_no, 'POS_MUST_BE_BLOCKED', False)

        if len(error_dict) > 0:
            error_messages = _parse_error_messages(model, error_dict)
            show_message_dialog(
                pos_id=pos_id_to_show_message,
                title="$INFORMATION",
                message="$ERROR_CLOSING_DAYS|{}".format("\\".join(error_messages)),
            )

        if len(succeed_pos_id) > 0:
            report_pos_id = "|".join(map(str, succeed_pos_id))
            menu_manager_user = get_menu_manager_authenticated_user(int(pos_id))
            print_report(
                pos_id_to_show_message,
                model,
                True,
                "cash",
                pos_id,
                period,
                "0",
                "true",
                report_pos_id,
                "end_of_day",
                "",
                menu_manager_user,
            )

            if _is_last_pos(period):
                maintenance_interactor = MaintenanceInteractor()
                maintenance_interactor.verify_clean_databases(period, pos_id)

                _verify_store_close_event(period)

            _verify_updates(menu_manager_user, period, pos_id)

    except Exception as ex:
        show_message_dialog(
            pos_id=pos_id_to_show_message,
            title="$ERROR",
            message="$ERROR_CLOSING_DAY|{}".format(ex.message),
        )


def _is_last_pos(period):
    global pos_list
    if pos_list is None or len(pos_list) == 0:
        pos_list = get_poslist()

    for pos_no in pos_list:
        model = get_model(pos_no)
        if model.find("PosState").get("period") == period and model.find("PosState").get("state") != "CLOSED":
            return False

    return True


def _verify_store_close_event(period):
    global pos_list
    if pos_list is None or len(pos_list) == 0:
        pos_list = get_poslist()

    for pos_no in pos_list:
        model = get_model(pos_no)
        if model.find("PosState").get("period") == period and model.find("PosState").get("state") != "CLOSED":
            break
    else:
        event_xml = '''<STORE_CLOSE businessPeriod="{}"/>'''
        data = event_xml.format(period)

        mb_context.MB_EasyEvtSend(
            subject="STORE_STATUS",
            type="CLOSE_STORE_DAY",
            xml=data,
            synchronous=False,
            timeout=-1
        )


def _verify_updates(menu_manager_user, period, pos_id):
    pending_updates = _has_pending_pos_update(pos_id)
    update_response = json.loads(pending_updates)
    update_type = str(update_response.get("update"))

    if update_type not in ["HAS_PENDING_UPDATE", "UPDATE_NOW"]:
        return

    if check_has_opened_day_others_pos(int(pos_id), period):
        show_message_dialog(pos_id, message="$HAS_OTHERS_POS_OPENED")
        return

    if update_type == "HAS_PENDING_UPDATE":
        confirmation_message = "$HAS_PENDING_UPDATES"
        resp = show_message_options_dialog(
            pos_id=pos_id,
            buttons="$OK|$PREVIOUS",
            title="$INFORMATION",
            message=confirmation_message,
        )
        if resp is not None and resp == 0:
            manager_info = "{} - {}".format(str(menu_manager_user), get_operator_name(menu_manager_user))
            last_version = update_response.get("lastVersion")
            current_version = update_response.get("currentVersion")
            _audit_logger_notify(pos_id, manager_info, last_version, current_version)
            _apply_binaries_update(pos_id)

    elif update_type == "UPDATE_NOW":
        confirmation_message = "$HAS_PENDING_UPDATES_TO_APPLY_NOW"
        show_message_dialog(pos_id, "$INFORMATION", confirmation_message)
        _apply_binaries_update(pos_id)


def _has_pending_pos_update(pos_id):
    msg = send_message("PosUpdater", TK_POS_HAS_BINARY_UPDATE_TO_APPLY, timeout=60 * 1000000)
    if msg.token != TK_SYS_ACK:
        sys_log_error("Error getting update binaries status on pos id: {}. Error: {}".format(pos_id, msg.data))

    return msg.data


def _apply_binaries_update(pos_id):
    msg = send_message(
        "PosUpdater",
        TK_POS_UPDATER_PERFORM_BINARIES_UPDATE,
        FM_PARAM,
        "updateNow",
        timeout=60 * 1000000
    )
    if msg.token == TK_SYS_ACK:
        return msg.data
    else:
        sys_log_error("Error applying update binaries status on pos id: {}. Error: {}".format(pos_id, msg.data))


def _fill_pos_list_with_can_close_day_pos(error_dict, period, pos_list, opened_users):
    for pos_no in sorted(pos_list):
        model = get_model(pos_no)
        if not _verify_if_can_close_day(pos_no, model, period, error_dict, opened_users):
            pos_list.remove(pos_no)


def _parse_error_messages(model, error_dict):
    error_messages = []
    if len(error_dict) > 0:
        for key in error_dict:
            if key in MwSysErrors.list_values():
                msg = translate_message(model, MwSysErrors.get_name(key))
            else:
                msg = str(key)

            pos = translate_pos_label(model=model)
            error_messages.append("{} {}".format(pos, error_dict[key]) + (" - {}".format(msg)))
    return error_messages


def _perform_close_day(pos_id, succeed_pos, error_dict):
    try:
        sys_log_info("Closing business day on POS id: [{}]".format(pos_id))

        pos_model = get_model(pos_id)
        if pos_model.find("PosState").get("state") == "CLOSED":
            return

        msg = send_message("POS%d" % int(pos_id), TK_POS_BUSINESSEND, FM_PARAM, str(pos_id), timeout=60 * 1000000)
        if msg.token == TK_SYS_ACK:
            succeed_pos.append(pos_id)
            return
        else:
            sys_log_error("Error closing business day on pos id: {}. Error: {}".format(pos_id, msg.data))
            _fill_error_dict(error_dict, pos_id, msg.data)

    except Exception as ex:
        sys_log_exception("Error closing business day on pos id: {}".format(pos_id))
        _fill_error_dict(error_dict, pos_id, ex.message)


def _verify_if_can_close_day(pos_id, model, period, error_dict, opened_users):
    try:
        if model.find("PosState").get("state") == "CLOSED":
            error_msg = translate_message(model, "DAY_IS_ALREADY_CLOSED")
            _fill_error_dict(error_dict, pos_id, error_msg)
            return False

        operator = model.find("Operator")
        if operator is not None and operator.get('id') in [x.get('id') for x in opened_users]:
            error_msg = translate_message(model, "THERE_ARE_OPEN_OPERATORS")
            _fill_error_dict(error_dict, pos_id, error_msg)
            return False

        if period == '0':
            error_msg = translate_message(model, "NEED_TO_OPEN_DAY_FIRST")
            _fill_error_dict(error_dict, pos_id, error_msg)
            return False
    except Exception as ex:
        _fill_error_dict(error_dict, pos_id, ex.message)

    return True


def _fill_error_dict(error_dict, pos_id, error_message):
    error_message = error_message.split("\0")[0]

    if error_message not in error_dict:
        error_dict[error_message] = [pos_id]
    else:
        error_dict[error_message].append(pos_id)

    return error_dict


def _audit_logger_notify(
    pos_id,                 # type: int
    user_id,                # type: str
    old_version=None,       # type: str
    current_version=None    # type: str
):
    # type: (...)-> None

    event_xml = '''<UPDATE posId="{}" userId="{}" oldVersion="{}" newVersion="{}"/>'''
    data = event_xml.format(pos_id, user_id, old_version, current_version)
    logger.info("User Accept update binary: {}".format(data))

    mb_context.MB_EasyEvtSend(
        subject="POS_UPDATER_LOGGER",
        type="UPDATE_ACCEPTED",
        xml=data,
        synchronous=False,
        timeout=-1
    )


def check_has_opened_day_others_pos(
    current_pos_id,         # type: int
    close_business_period   # type: str
):
    # type: (...) -> bool

    for pos_id in get_poslist():
        if pos_id in [0, current_pos_id]:
            continue

        model = get_model(pos_id)
        working_mode = model.find("WorkingMode")
        pod_type = working_mode.get("podType") if working_mode is not None else None
        if pod_type == "TT":
            continue

        model_pos_state = model.find("PosState")
        pos_state = model_pos_state.get("state") if model_pos_state is not None else None
        if pos_state != "OPENED":
            continue

        pos_business_period = get_business_period(model)
        if pos_business_period >= close_business_period and pos_state:
            return True

    return False
