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

import json
import time
from typing import (
    Dict,
    List,
    Optional,
)

from xml.etree import cElementTree as eTree
import xml.etree.ElementTree as ET

import persistence
import sysactions
from sysactions import (
    get_poslist,
    show_messagebox,
    format_amount,
    get_model,
)
from actions.models import GLAccount, TransferType, CloseOperator
from actions.util import (
    get_drawer_amount,
    list_ids_opened_users,
    get_all_tender_types,
)
from dto import ConferenceTypes
from actions.config import (
    get_close_operator_conference_type,
    considers_tef_on_bordereau,
)
from application.repository import AccountRepository
from msgbus import TK_ACCOUNT_TRANSFER, FM_PARAM, TK_SYS_ACK
from mw_helper import show_bordereau_dialog
from systools import sys_log_exception

from .. import mb_context
from .. import pos_config
from ..drawer.do_open_drawer import doOpenDrawer
from ..util import (
    get_drawer_initial_amount,
    get_operator_info,
    get_payments_amount_by_type,
)


skim_limit_min = 5
skim_limit_max = 30
sangria_levels = None


@sysactions.action
def do_transfer(
    pos_id,                     # type: str
    transfer_type,              # type: int
    operator_id,                # type: str
    authorizer=None,            # type: Optional[str]
    pos_to_close_user=None,     # type: Optional[str]
    transfer_values=None,       # type: Optional[Dict[str, float]]
):
    # type: (...) -> dict

    if pos_to_close_user is None:
        pos_to_close_user = pos_id

    global skim_limit_min, skim_limit_max, sangria_levels
    skim_limit_min = pos_config.skim_digit_limit['min']
    skim_limit_max = pos_config.skim_digit_limit['max']
    sangria_levels = pos_config.sangria_levels
    transfer_type = int(transfer_type)

    model = sysactions.get_model(
        posid=pos_to_close_user,
    )
    _check_if_can_do_transfer(
        pos_id=pos_id,
        model=model,
        operator_id=operator_id,
        transfer_values=transfer_values,
    )

    _, session = get_operator_info(model, operator_id)
    period = sysactions.get_business_period(model)
    manager_id = sysactions.get_custom(model, 'Last Manager ID') or ""
    if not manager_id and authorizer is not None:
        manager_id = authorizer

    if transfer_type == TransferType.TRANSFER_CASH_IN or transfer_type == TransferType.TRANSFER_CASH_OUT:
        drawer_amount = get_drawer_amount(
            pos_id=pos_to_close_user,
            period=period,
            session=session,
        )
        return _process_cash_in_and_out(
            pos_id=pos_id,
            model=model,
            session=session,
            operator_id=operator_id,
            period=period,
            manager_id=manager_id,
            drawer_amount=drawer_amount,
            transfer_type=transfer_type,
            pos_to_close_user=pos_to_close_user,
        )
    else:
        sold_amount_by_tender_type = get_sold_amount_by_tender_type(session)
        return _process_close_operator(
            pos_id=pos_id,
            session=session,
            period=period,
            manager_id=manager_id,
            amount_by_tender_type=sold_amount_by_tender_type,
            pos_to_close_user=pos_to_close_user,
            transfer_values=transfer_values,
        )


def get_transfer_values(
    pos_id,     # type: str
    period,     # type: str
    session     # type: str
):
    # type: (...) -> List[Dict[str, float]]

    conference_type = get_close_operator_conference_type()
    has_valid_tender = _has_valid_bordereau_tender()
    if conference_type == ConferenceTypes.BORDEREAU.value and has_valid_tender:
        return _get_tender_types_inserted_values(pos_id=pos_id)

    if conference_type == ConferenceTypes.CASH_DRAWER.value and has_valid_tender:
        message = "$TYPE_THE_DRAWER_VALUE"
        amount = sysactions.show_keyboard(
            posid=pos_id,
            message=message,
            title='',
            mask="CURRENCY",
            numpad=True,
            timeout=720000,
        )
        if amount is None:
            raise sysactions.StopAction()

        return [{"tenderId": 0, "amount": amount}]

    drawer_amount = get_drawer_amount(
        pos_id=pos_id,
        period=period,
        session=session,
    )
    return [{"tenderId": 0, "amount": drawer_amount}]


def _show_drawer_funds_message(
    pos_id,                 # type: str
    initial_value_drawer,   # type: float
    envelope_amount,        # type: float
    transfer_amount,        # type: float
):
    # type: (...) -> None

    model = get_model(posid=pos_id)
    message = "$DRAWER_FUNDS_MESSAGE|{0}|{1}".format(
        format_amount(
            model=model,
            amount=initial_value_drawer,
        ),
        format_amount(
            model=model,
            amount=envelope_amount,
        )
    )

    if transfer_amount <= initial_value_drawer:
        message = "$DRAWER_RESERVE_FUNDS_MESSAGE|{0}".format(
            format_amount(
                model=model,
                amount=initial_value_drawer,
            )
        )

    sysactions.show_messagebox(
        posid=pos_id,
        message=message,
        timeout=720000,
        buttons="$OK",
    )


def _has_valid_bordereau_tender():
    # type: (...) -> bool

    for tender in get_all_tender_types():
        has_child = tender.get("hasChild")
        if has_child:
            continue

        is_electronic_payment = tender.get("electronicTypeId") != 0
        if is_electronic_payment and not considers_tef_on_bordereau():
            continue

        custom_attr = tender.get("customAttr")
        if not custom_attr:
            return True

        custom_attr = json.loads(custom_attr)
        show_in_bordereau = custom_attr.get("showInBordereau", True)
        if show_in_bordereau:
            return True

    return False


def _process_close_operator(
    pos_id,                     # type: str
    session,                    # type: str
    period,                     # type: str
    manager_id,                 # type: str
    amount_by_tender_type,      # type: Dict[int, List[Dict]]
    pos_to_close_user=None,     # type: str
    transfer_values=None,       # type: Optional[List[Dict[str, float]]]
):
    # type: (...) -> dict

    if transfer_values is None:
        transfer_values = get_transfer_values(
            pos_id=pos_id,
            period=period,
            session=session,
        )

    initial_value_drawer = _get_initial_drawer_amount(
        pos_id=pos_to_close_user,
        session=session,
    )
    transfer_amount = _get_cash_amount_in_transfer_values(transfer_values=transfer_values)

    envelope_amount = transfer_amount - initial_value_drawer
    envelope = _get_envelope(
        pos_id=pos_id,
        envelope_amount=envelope_amount,
    )

    _fill_tender_attributes(inserted_values=transfer_values)

    if pos_to_close_user is None:
        pos_to_close_user = pos_id

    try:
        operator = CloseOperator(
            pos_id=pos_to_close_user,
            values=transfer_values,
            envelope=envelope,
            session=session,
            payments=amount_by_tender_type,
            manager_id=manager_id,
            period=period,
        )
        has_some_difference = save_inserted_values(operator)
        gl_account = GLAccount(manager=manager_id)
        extra_info = json.dumps(gl_account, default=lambda o: o.__dict__, sort_keys=True)
        status = TransferType.FINAL_AMOUNT
        if initial_value_drawer:
            _show_drawer_funds_message(
                pos_id=pos_id,
                initial_value_drawer=initial_value_drawer,
                envelope_amount=envelope_amount,
                transfer_amount=transfer_amount,
            )

        data = "{}\0{}\0{}\0{}\0{}\0{}".format(
            session,
            status,
            "FINAL_AMOUNT",
            initial_value_drawer,
            0,
            extra_info
        )
        sysactions.send_message(
            dest_name="account%s" % pos_id,
            token=TK_ACCOUNT_TRANSFER,
            format=FM_PARAM,
            data=data,
        )

        if transfer_amount > 0:
            _send_audit_cash_event(
                xml_name="DeclaredAmount",
                event_name="DECLARED_AMOUNT",
                store_id=pos_config.store_id,
                period=period,
                pos_id=pos_id,
                session=session,
                transfer_amount=str(transfer_amount),
                timestamp=str(time.time()),
                extra_info=extra_info,
            )

        if has_some_difference:
            return {'need_ask_justification': True}

    except (Exception,):
        sysactions.show_messagebox(pos_id, message="$ERROR_SAVING_CLOSE_OPERATOR_REPORT")
        raise sysactions.StopAction()

    return {'need_ask_justification': False}


def _send_audit_cash_event(
    xml_name,           # type: str
    event_name,         # type: str
    store_id,           # type: str
    period,             # type: str
    pos_id,             # type: str
    session,            # type: str
    transfer_amount,    # type: str
    timestamp,          # type: str
    extra_info,         # type: str
):
    # type: (...) -> None

    declared_amount_xml = ET.Element(
        xml_name,
        storeId=store_id,
        period=period,
        posId=pos_id,
        sessionId=session,
        amount=transfer_amount,
        tenderId="0",
        timestamp=timestamp,
        extraInfo=extra_info,
    )

    mb_context.MB_EasyEvtSend(
        subject=event_name,
        type=event_name,
        xml=eTree.tostring(declared_amount_xml),
    )


def _get_cash_amount_in_transfer_values(
    transfer_values,     # type: Optional[List[Dict[str, float]]]
):
    # type: (...) -> float

    cash_transfer_amount = 0.0
    for value in transfer_values:
        if value.get("tenderId") != 0:
            continue
        cash_transfer_amount += float(value.get("amount"))

    return cash_transfer_amount


def _get_envelope(
    pos_id,             # type: str
    envelope_amount,    # type: float
):
    # type: (...) -> Optional[str]

    envelope = ""
    if envelope_amount > 0:
        envelope = _get_skim_number(pos_id)

    if envelope is None:
        raise sysactions.StopAction()

    return envelope


def save_inserted_values(
    operator,  # type: CloseOperator
):
    # type: (...) -> bool

    account_repository = AccountRepository(mb_context)
    total_bordereau_difference = account_repository.insert_close_operator_info(operator)
    total_bordereau_difference = abs(total_bordereau_difference)
    if total_bordereau_difference == 0:
        return False

    return total_bordereau_difference >= pos_config.min_value_to_ask_bordereau_justify_config


def get_justification(
    account_repository,    # type: AccountRepository
    pos_id,                # type: str
    session,               # type: str
):
    # type: (...) -> None

    justification = _get_justification(pos_id=pos_id)
    account_repository.update_justification(
        pos_id=pos_id,
        session=session,
        justification=justification,
    )


def _get_justification(
    pos_id,                                  # type: str
    dialog_message="$CASH_BREAK_MESSAGE",    # type: Optional[str]
    buttons="$OK",                           # type: Optional[str]
):
    # type: (...) -> str

    justification = None
    while justification is None or len(justification) < 5:
        justification = sysactions.show_keyboard(
            posid=pos_id,
            message=dialog_message,
            title="",
            timeout=1800000,
            buttons=buttons,
        )

        if justification is not None:
            justification = justification.strip()
            if len(justification) < 5:
                show_messagebox(
                    posid=pos_id,
                    message="$MINIMUM_5_CHARACTERS_REQUIRED",
                )
            else:
                break
        else:
            break
    return justification


def _process_cash_in_and_out(
        pos_id,                     # type: str
        model,                      # type: eTree.XML
        session,                    # type: str
        operator_id,                # type: str
        period,                     # type: str
        manager_id,                 # type: str
        drawer_amount,              # type: float
        transfer_type,              # type: int
        pos_to_close_user=None,     # type: str
):
    # type: (...) -> dict

    if pos_to_close_user is None:
        pos_to_close_user = pos_id

    tile = "$CASH_IN" if transfer_type == TransferType.TRANSFER_CASH_IN else "$CASH_OUT"
    transfer_text = "TRANSFER_CASH_IN" if transfer_type == TransferType.TRANSFER_CASH_IN else "TRANSFER_CASH_OUT"
    translated_transfer_text = sysactions.translate_message(model, transfer_text)
    message = "$ENTER_AMOUNT_TO_TRANSFER_TYPE|%s" % translated_transfer_text

    amount = _get_inserted_amount(
        pos_id=pos_to_close_user,
        message=message,
        title=tile,
    )
    if amount is None or amount == 0:
        raise sysactions.StopAction()

    if not is_valid_amount_by_transfer_type(
        pos_id=pos_to_close_user,
        transfer_type=transfer_type,
        amount=amount,
        drawer_amount=drawer_amount,
    ):
        raise sysactions.StopAction()

    envelope_number = ""
    justification = ""
    if transfer_type == TransferType.TRANSFER_CASH_OUT:
        if _get_ask_cash_out_justification():
            justification = _get_justification(
                pos_id=pos_id,
                dialog_message='$CASH_OUT_JUSTIFICATION',
                buttons="$OK|$CANCEL",
            )
        envelope_number = _get_skim_number(
            pos_id=pos_to_close_user,
        )
        if envelope_number is None or justification is None:
            raise sysactions.StopAction()

    return _open_drawer(
        pos_id=pos_id,
        model=model,
        operator_id=operator_id,
        manager_id=manager_id,
        period=period,
        envelope_number=envelope_number,
        justification=justification,
        session=session,
        transfer_type=transfer_type,
        transfer_text=transfer_text,
        amount=amount,
        pos_to_close_user=pos_to_close_user,
    )


def _open_drawer(
    pos_id,                 # type: str
    model,                  # type: eTree.XML
    operator_id,            # type: str
    manager_id,             # type: str
    period,                 # type: str
    envelope_number,        # type: str
    justification,          # type: str
    session,                # type: str
    transfer_type,          # type: int
    transfer_text,          # type: str
    amount,                 # type: float
    pos_to_close_user,      # type: str
):
    # type: (...) -> Optional[Dict[str, bool]]

    from table_actions import print_operator_copy

    gl_account = GLAccount(
        manager=manager_id,
        envelope=envelope_number,
        justification=justification,
    )
    extra_info = json.dumps(gl_account, default=lambda o: o.__dict__, sort_keys=True)
    data = "%s\0%s\0%s\0%s\0%s\0%s" % (session, transfer_type, transfer_text, amount, 0, extra_info)
    msg = sysactions.send_message(
        dest_name="account%s" % pos_to_close_user,
        token=TK_ACCOUNT_TRANSFER,
        format=FM_PARAM,
        data=data,
    )
    if msg.token == TK_SYS_ACK:
        doOpenDrawer(
            pos_id=pos_to_close_user,
            need_logged=operator_id != manager_id,
        )
        if sysactions.print_report(pos_id, model, True, "transfer_report", pos_id, operator_id, transfer_type, amount,
                                   period, envelope_number, False, justification):

            if print_operator_copy:
                sysactions.print_report(pos_id, model, False, "transfer_report", pos_id, operator_id, transfer_type,
                                        amount, period, envelope_number, True, justification)
            sysactions.show_messagebox(pos_id, "$OPERATION_SUCCEEDED")

        _send_audit_cash_event(
            xml_name="TransferInfo",
            event_name="TRANSFER_CASH_OUT",
            store_id=pos_config.store_id,
            period=period,
            pos_id=pos_id,
            session=session,
            transfer_amount=str(amount),
            timestamp=str(time.time()),
            extra_info=extra_info,
        )

        return {'need_ask_justification': False}

    sysactions.show_messagebox(pos_id, "$OPERATION_FAILED", "$ERROR")
    raise sysactions.StopAction()


def get_sold_amount_by_tender_type(
    session_id,     # type: str
):
    # type: (...) -> Dict[int, List[Dict]]

    pos_list = get_poslist()
    payments_amount = {}
    for pos_id in pos_list:
        get_payments_amount_by_type(
            pos_id,
            session_id,
            payments_amount,
        )

    return payments_amount


def valid_amount_by_skim_limit(amount):
    if pos_config.max_transfer_value == 0:
        return True
    if round(float(amount), 2) > round(pos_config.max_transfer_value, 2):
        return False
    return True


def _check_if_can_do_transfer(
    pos_id,             # type: str
    model,              # type: eTree.XML
    operator_id,        # type: str
    transfer_values,    # type: Optional[Dict[int, List[Dict]]]
):
    # type: (...) -> None

    if not sysactions.is_day_opened(model):
        sysactions.show_messagebox(pos_id, message="$DAY_IS_CLOSED")
        raise sysactions.StopAction()

    if operator_id == '':
        sysactions.show_messagebox(pos_id, message="$OPERATOR_IS_NOT_OPENED")
        raise sysactions.StopAction()

    if transfer_values is not None:
        return

    has_open_operator = _has_operator_opened(
        model=model,
        operator_id=operator_id,
    )
    if has_open_operator:
        return

    sysactions.show_messagebox(pos_id, message="$OPERATOR_IS_NOT_OPENED")
    raise sysactions.StopAction()


def _has_operator_opened(
    model,          # type: eTree.XML
    operator_id,    # type: str
):
    # type: (...) -> bool

    if int(operator_id) in list_ids_opened_users():
        return True

    login_type = model.find("WorkingMode").get("usrCtrlType")
    operators = model.findall(".//Operators/Operator")
    is_any_available = any([op.get("state") == "LOGGEDIN" for op in operators])
    if is_any_available and login_type == "TS":
        return True

    is_any_paused = any([op.get("state") == "PAUSED" for op in operators])
    if login_type == "QSR" and is_any_paused:
        return True

    return False


def _get_operator(model):
    return model.findall('.//Operator')


def is_valid_amount_by_transfer_type(pos_id, transfer_type, amount, drawer_amount):
    if not _inserted_amount_is_valid(amount):
        message = "$CASH_OUT_MAX_VALUE|%s" % "{0:.2f}".format(pos_config.max_transfer_value)
        sysactions.show_messagebox(pos_id, message, "$INFORMATION")
        return False

    is_cash_out = transfer_type == TransferType.TRANSFER_CASH_OUT
    if is_cash_out and drawer_amount < amount and pos_config.max_transfer_value != 0:
        sysactions.show_messagebox(pos_id, "$SKIM_VALUE_MUCH_BIGGER", "$INFORMATION")
        return False

    return True


def _inserted_amount_is_valid(amount):
    invalid_amount = amount in ("", ".") or round(float(amount), 2) < 0
    return not invalid_amount and valid_amount_by_skim_limit(amount)


def _get_skim_number(pos_id):
    if pos_config.auto_generate_skim_number:
        return _auto_generate_skim_number(pos_id)

    return _request_skim_number(pos_id)


def _get_ask_cash_out_justification():
    # type: (...) -> bool

    return pos_config.ask_cash_out_justification


def _request_skim_number(pos_id):
    valid_envelope_number = False
    skim_number = None
    while not valid_envelope_number:
        skim_number = sysactions.show_keyboard(pos_id, "$TYPE_SKIM_NUMBER", numpad=pos_config.keyboard_skim_numeric)
        if skim_number is not None and (len(skim_number) < skim_limit_min or len(skim_number) > skim_limit_max):
            msg = "$INVALID_SKIM_NUMBER|{0}|{1}".format(skim_limit_min, skim_limit_max)
            sysactions.show_messagebox(pos_id, msg)
            continue
        elif skim_number is None:
            return None
        valid_envelope_number = True
    return skim_number


def _auto_generate_skim_number(pos_id):
    model = sysactions.get_model(pos_id)
    period = sysactions.get_business_period(model)
    day = int(period[6:8] + period[4:6] + period[2:4])
    count_cash_out = _get_number_of_cash_out(pos_id, period)
    skim_number = "{:05d}{:06d}{}".format(abs(hash(pos_config.store_id)), day, count_cash_out)

    return skim_number


def _get_initial_drawer_amount(pos_id, session):
    return get_drawer_initial_amount(pos_id, session)


def _get_inserted_amount(
    pos_id,
    message,
    title="",
):
    amount = sysactions.show_keyboard(
        posid=pos_id,
        message=message,
        title=title,
        mask="CURRENCY",
        numpad=True,
        timeout=720000,
        buttons='$OK|$PREVIOUS',
    )
    return None if amount is None else round(float(amount), 2)


def _get_tender_types_inserted_values(pos_id):
    values = show_bordereau_dialog(pos_id)
    if values is None:
        raise sysactions.StopAction()

    return json.loads(values)


def _get_number_of_cash_out(pos_id, period):
    conn = None
    skim_count = 0
    try:
        conn = persistence.Driver().open(mb_context, pos_id)
        cursor = conn.select(
            """
                SELECT
                    COUNT(*)
                FROM account.Transfer
                WHERE
                    Description in ('FINAL_AMOUNT', 'TRANSFER_CASH_OUT')
                    AND Period = '{}'
                GROUP BY Period
            """.format(period)
        )
        for row in cursor:
            skim_count = int(row.get_entry(0) or 0)
        return skim_count

    except (Exception,):
        sys_log_exception("get amount of cash out Error")
    finally:
        if conn:
            conn.close()


def _fill_tender_attributes(
    inserted_values,  # type: List[Dict]
):
    # type: (...) -> None

    conn = None

    _normalize_amount_type(inserted_values)
    tender_ids = [str(value["tenderId"]) for value in inserted_values]

    try:
        conn = persistence.Driver().open(mb_context)
        cursor = conn.select(
            """
            SELECT TenderId, CustomAttr
            FROM TenderType
            WHERE TenderId IN ({})
            """.format(",".join(tender_ids))
        )
        for row in cursor:
            tender_id = int(row.get_entry("TenderId"))
            for value in inserted_values:
                if value["tenderId"] == tender_id:
                    custom_attr = json.loads(row.get_entry("CustomAttr") or "{}")
                    value["linkWithTef"] = str(custom_attr.get("linkWithTef", "false")).lower() == "true"
                    value["customAttr"] = custom_attr
                    break

    except:  # noqa
        sys_log_exception("Error filling tender attributes")
    finally:
        if conn:
            conn.close()


def _normalize_amount_type(
    inserted_values,  # type: List[Dict]
):
    # type: (...) -> None

    for value in inserted_values:
        if not isinstance(value["amount"], float):
            value["amount"] = float(value["amount"])
