from datetime import (
    datetime,
)
from logging import Logger
from xml.etree import cElementTree as eTree

from model.config import (
    Config,
    PosMap,
    MapValidatePosId,
)
from model.error_type import (
    AuthErrorsType,
    PosValidationErrorsType,
    OperationErrorsType,
    BadRequestErrorsType,
)
from model.exception import (
    AuthValidationErrors,
    ValidationError,
    OperationErrors,
    BadRequestError,
)
from model.repository import PosRepositoryModel
from typing import (
    List,
    Optional,
)

global api_key_reference


def check_if_business_day_is_opened(repository, model):
    # type: (PosRepositoryModel, eTree) -> None

    is_day_opened = repository.is_day_opened(model)
    if not is_day_opened:
        raise ValidationError(PosValidationErrorsType.BusinessDayNotOpened)


def check_if_operator_is_logged(repository, model):
    # type: (PosRepositoryModel, eTree) -> None

    is_operator_logged = repository.is_operator_logged(model)
    if not is_operator_logged:
        raise ValidationError(PosValidationErrorsType.OperatorIsNotLogged)


def check_if_order_is_in_progress(repository, model):
    # type: (PosRepositoryModel, eTree) -> None

    is_order_in_progress = repository.is_order_in_progress(model)
    if not is_order_in_progress:
        raise ValidationError(PosValidationErrorsType.OrderIsNotInProgress)


def check_if_order_is_totaled(repository, model):
    # type: (PosRepositoryModel, eTree) -> None

    is_order_totaled = repository.is_order_totaled(model)
    if not is_order_totaled:
        raise ValidationError(PosValidationErrorsType.OrderIsNotTotaled)


def check_if_order_is_in_progress_or_totaled(
        repository,     # type: PosRepositoryModel
        model           # type: eTree
):
    # type: (...) -> None

    is_order_in_progress = repository.is_order_in_progress(model)
    is_order_totaled = repository.is_order_totaled(model)

    if not (is_order_in_progress or is_order_totaled):
        raise ValidationError(PosValidationErrorsType.OrderIsNotInProgressOrTotaled)


def check_if_pos_id_is_valid(repository, pos_id):
    # type: (PosRepositoryModel, int) -> None

    pos_list = repository.get_pos_list()
    if not int(pos_id) in pos_list:
        raise ValidationError(PosValidationErrorsType.PosIdNotFound)


def check_if_payment_amount_is_valid(
    payment_value,      # type: float
    pos_id,             # type: str
    model,              # type: eTree
    pos_repository,     # type: PosRepositoryModel
    order_id=None,      # type: Optional[str]
):
    # type: (...) -> None

    order = pos_repository.get_order_picture(
        pos_id=pos_id,
        order_id=order_id,
        model=model,
    )

    due_amount = float(order.get("dueAmount"))
    if (payment_value <= 0) or (payment_value > due_amount):
        raise ValidationError(PosValidationErrorsType.PaymentAmountInvalid)


def check_if_payment_type_is_valid(allowed_methods, payment_type):
    # type: (list, int) -> None

    if payment_type not in allowed_methods:
        raise ValidationError(PosValidationErrorsType.PaymentTypeInvalid)


def check_if_due_amount_is_zero(model):
    # type: (eTree) -> None

    due_amount = float(model.find("CurrentOrder/Order").get("dueAmount"))
    if due_amount != 0:
        raise ValidationError(PosValidationErrorsType.DueAmountInvalid)


def check_has_due_amount(
    model,          # type: eTree
    pos_id,         # type: str
    repository,     # type: PosRepositoryModel
):
    # type: (...) -> None

    order = repository.get_order_picture(
        pos_id=pos_id,
        order_id='',
        model=model,
    )

    due_amount = float(order.get("dueAmount"))
    if due_amount == 0:
        raise ValidationError(
            PosValidationErrorsType.DueAmountInvalid,
        )


def check_if_state_and_due_amount_is_valid_to_finalize(
    logger,             # type: Logger
    pos_id,             # type: str
    order_id,           # type: str
    model,              # type: eTree
    pos_repository,     # type: PosRepositoryModel
):
    # type: (...) -> None

    pos_ot = pos_repository.get_pos_ot(model=model)

    order_details = pos_repository.get_order_details(
        pos_id=pos_id,
        order_id=order_id,
        pos_ot=pos_ot,
    )

    is_order_totaled = pos_repository.is_order_totaled(
        model=model,
        order_picture=order_details,
    )

    if not is_order_totaled:
        message = "Error finalizing: order not totaled: [{}-{}]: OrderPicture: \n{}"
        logger.info(message.format(pos_id, order_id, eTree.tostring(order_details)))
        raise ValidationError(PosValidationErrorsType.OrderIsNotTotaled)

    order = order_details.find(".//Order")
    due_amount = float(order.get("dueAmount"))

    if due_amount != 0:
        message = "Error finalizing: dueAmount is not zero: [{}-{}]: dueAmount: {} - orderPicture: \n{}"
        logger.info(message.format(pos_id, order_id, due_amount, eTree.tostring(order_details)))
        raise ValidationError(PosValidationErrorsType.DueAmountInvalid)


def check_if_operator_is_loggedin_and_logout(repository, operator, model, pos_id, pos_info, pos_ot):
    if operator is not None and operator.get("state") == "LOGGEDIN":
        if repository.has_current_order(model):
            repository.void_order(pos_id, pos_ot)

        operator_id = operator.get("id")
        if int(operator_id) != pos_info.user_id:
            logouted = repository.logout_delivery_user(pos_id, operator_id)
            if not logouted:
                raise OperationErrors(OperationErrorsType.OperatorLogoutError)
            operator = None

    return operator


def check_if_request_order_id_is_equal_current_order_id(
    model,                  # type: eTree
    request_order_id,       # type: Optional[str]
):
    # type: (...) -> None

    if request_order_id not in ['', None]:
        current_order_id = str(model.find("CurrentOrder/Order").get("orderId"))
        if str(current_order_id) != str(request_order_id):
            raise ValidationError(PosValidationErrorsType.InvalidOrderIdRequest)


def check_current_date_and_model_state_and_fix(repository, operator, model, pos_id, current_date):
    if model.find("PosState").get("period") == current_date and model.find("PosState").get("state") == "OPENED":
        return operator

    if operator is not None and operator.get("state") in ["LOGGEDIN", "PAUSED"]:
        operator_id = operator.get("id")
        logged_out = repository.logout_delivery_user(
            pos_id=pos_id,
            operator_id=operator_id,
        )
        if not logged_out:
            raise OperationErrors(ex=OperationErrorsType.OperatorLogoutError)
        operator = None

    if model.find("PosState").get("state") == "OPENED" or model.find("PosState").get("state") == "BLOCKED":
        closed = repository.close_business_period(pos_id=pos_id)
        if not closed:
            raise OperationErrors(ex=OperationErrorsType.BusynessDayCloseError)

    opened = repository.open_business_period(pos_id=pos_id)
    if not opened:
        raise OperationErrors(ex=OperationErrorsType.BusynessDayOpenError)

    return operator


def check_if_operator_is_loggouted_and_login(repository, operator, pos_id, pos_info):
    if operator is None or operator.get("state") != "LOGGEDIN":
        authenticate_msg = repository.authenticate_user(str(pos_info.user_id), str(pos_info.user_password))

        if not authenticate_msg:
            raise OperationErrors(OperationErrorsType.OperatorLoginError)

        level, username, long_user_name = authenticate_msg.split('\x00')[:3]

        if operator is None or (operator.get("state") != "OPENED" and operator.get("state") != "PAUSED"):
            opened = repository.open_user(pos_id, str(pos_info.user_id), long_user_name)
            if not opened:
                raise OperationErrors(OperationErrorsType.BusynessDayOpenError)

        logged = repository.login_user(pos_id, str(pos_info.user_id), long_user_name)
        if not logged:
            raise OperationErrors(OperationErrorsType.OperatorLoginError)


def check_business_period(repository, pos_id, config):
    # type: (PosRepositoryModel, int, Config) -> None

    model = repository.get_model(pos_id)
    pos_ot = repository.get_pos_ot(model)

    current_date = datetime.now().date().strftime("%Y%m%d")
    operator = model.find("Operator")
    pos_info = next((x for x in config.api_key_mapping if x.pos_id == int(pos_id)), None)  # type: PosMap

    operator = check_if_operator_is_loggedin_and_logout(repository, operator, model, pos_id, pos_info, pos_ot)
    model = repository.get_model(pos_id)

    operator = check_current_date_and_model_state_and_fix(repository, operator, model, pos_id, current_date)

    check_if_operator_is_loggouted_and_login(repository, operator, pos_id, pos_info)


def validate_api_key(request, secure_api_key_list):
    api_key_accepted = False
    pos_id_accepted = False
    if hasattr(request, "pos_id"):
        received_pos_id = int(request.pos_id)
    else:
        received_pos_id = None
    received_api_key = request.system_api_key
    real_pos_id = ""

    for key in secure_api_key_list:
        if received_pos_id is not None:
            if key.api_key == received_api_key:
                api_key_accepted = True
                if received_pos_id == key.abstract_pos_id:
                    pos_id_accepted = True
                    real_pos_id = int(key.pos_id)
        elif key.api_key == received_api_key:
            api_key_accepted = True

    if not api_key_accepted:
        raise AuthValidationErrors(AuthErrorsType.InvalidApiKey)

    if received_pos_id and not pos_id_accepted:
        raise ValidationError(PosValidationErrorsType.PosIdNotFound)

    return str(real_pos_id)


def get_pos_id_from_pos_mapper(abstracted_pos_id, api_key_mapping):
    # type: (str, List[PosMap]) -> str

    for pos_map in api_key_mapping:
        if pos_map.abstract_pos_id == abstracted_pos_id:
            return pos_map.pos_id


def get_abstracted_pos_id_from_pos_mapper(pos_id, api_key_mapping):
    # type: (str, List[PosMap]) -> str

    for pos_map in api_key_mapping:
        if pos_map.pos_id == pos_id:
            return pos_map.abstract_pos_id


def validate_pos_id(
    request,                # type: MapValidatePosId
):
    # type: (...) -> str

    try:
        return validate_api_key(
            request=request,
            secure_api_key_list=request.secure_api_key_list,
        )
    except AuthValidationErrors as ex:
        raise ex
    except (Exception, ValidationError) as _:
        raise BadRequestError(
            ex=BadRequestErrorsType.InvalidPosId,
        )
