#!/usr/bin/python3

from datetime import date, timedelta
from xml.etree import ElementTree as ET
import sys
from typing import Dict, Generator, List, Optional, Union

sys.path.insert(0, "/usr/local/mgr5/lib/python")
from billmgr.db import db_query_cursor_dict
from billmgr.logger import Logger, init_logging, get_logger
from billmgr.misc import FileLock
from billmgr.modules.paymethod import (
    Feature, PaymethodModule, Param, RecurringType, recurring_type_param
)
from billmgr.payment import (
    PaymentStatus, PaymentInfo, RecurringInfo, RecurringPaymentInfo,
    RecurringStatus, set_canceled, set_paid
)
from billmgr.paymentreceipt.enumerations import PaymentReceiptType
from billmgr.session import get_input_xml, new_node, make_slist, SlistElem

from paymaster.api.client import PayMasterAPI
from paymaster.api.enumerations import (
    PaymentStatus as PayMasterPaymentStatus,
    PaymentMethod as PayMasterPaymentMethod,
    PaymentSubject as PayMasterPaymentSubject,
    VatType, ItemMeasurementUnit
)
from paymaster.api.parameters import PaymentCreateParameters, RefundCreateParameters
from paymaster.exceptions import PayMasterConnectionError, PayMasterException, PayMasterValueError
from paymaster.utils import format_email, format_phone, json_dict, parse_date


MODULE: str = "pmpaymasterv2"

init_logging(MODULE)
logger: Logger = get_logger(MODULE)


class PayMasterModule(PaymethodModule):

    def __init__(self) -> None:
        super().__init__()

        self.set_description("Paymaster v2 integration")

        self._add_feature(Feature.NOT_PROFILE)
        self._add_feature(Feature.RECEIPT)
        self._add_feature(Feature.REDIRECT)
        self._add_feature(Feature.RP_PENDING)

        self._add_callable_feature(Feature.PMTUNE, self.pm_tune)
        self._add_callable_feature(Feature.PMUSERCREATE, lambda: ...)
        self._add_callable_feature(Feature.PMVALIDATE, self.pm_validate)
        self._add_callable_feature(Feature.CHECKPAY, self.checkpay)

        self._add_feature(Feature.RECURRING)
        self._add_feature(Feature.STORED)
        self._add_callable_feature(Feature.RCPAY, self.rc_pay)
        self._add_callable_feature(Feature.RCDELETE, self.rc_delete)

        self._add_feature(Feature.REFUND)
        self._add_callable_feature(Feature.RFSET, self.rf_set)

        self._add_param(Param.PAYMENT_SCRIPT, "/mancgi/paymasterv2payment")
        self._add_param(Param.RECURRING_SCRIPT, "/mancgi/paymasterv2recurring")
        self._add_param(
            Param.RECURRING_TYPE,
            recurring_type_param([
                RecurringType.rtRedirect,
                RecurringType.rtMaxAmount
            ])
        )

    def pm_tune(self) -> None:
        xml: ET.Element = get_input_xml()
        logger.info(f'xml = {ET.tostring(xml, encoding="unicode")}')

        xml.append(new_node("outerlink", r"https://info.paymaster.ru/check/"))

        payment_methods: List[SlistElem] = [
            SlistElem(key="bankcard", msg="bankcard")
        ]
        make_slist(xml=xml, name="payment_method", slist_elems=payment_methods)

        ET.dump(xml)

    def pm_validate(self) -> None:
        xml: ET.Element = get_input_xml()
        logger.info(f'xml = {ET.tostring(xml, encoding="unicode")}')

        merchant_id: str = xml.findtext("merchant_id", "")
        auth_token: str = xml.findtext("api_token", "")

        try:
            # валидация данных авторизации происходит автоматически при
            # инициализации API-клиента
            api = PayMasterAPI(merchant_id, auth_token, logger)

            if xml.findtext("recurring", "") == "on":
                api.check_recurring_payments_access()
        except PayMasterException as error:
            sys.stdout.write(error.as_xml())

    def rf_set(self):
        xml: ET.Element = get_input_xml()
        logger.info(f"refund xml = {ET.tostring(xml, encoding='utf8')}")

        payment_id: int = int(xml.findtext("source_payment", default=""))
        payment_info = PaymentInfo(payment_id, language=None)
        idempotency_key: Optional[str] = payment_info.payment.get("randomnumber")  # type: ignore

        api = PayMasterAPI(
            merchant_id=payment_info.paymethod["merchant_id"],  # type: ignore
            auth_token=payment_info.paymethod["api_token"],  # type: ignore
            logger=logger
        )
        parameters: RefundCreateParameters = self._get_refund_parameters(payment_info)
        api.refund.create(parameters, idempotency_key)

    def rc_pay(self, payment_id: int) -> None:
        payment_info = PaymentInfo(payment_id, language=None)
        recurring_info = RecurringPaymentInfo(
            payment_info.payment["recurring"], language=None  # type: ignore
        )
        idempotency_key: Optional[str] = payment_info.payment.get("randomnumber")  # type: ignore

        try:
            api = PayMasterAPI(
                merchant_id=recurring_info.paymethod["merchant_id"],  # type: ignore
                auth_token=recurring_info.paymethod["api_token"],  # type: ignore
                logger=logger
            )
            parameters: PaymentCreateParameters = self._get_recurring_pay_parameters(
                payment_info, recurring_info  # type: ignore
            )
            response: json_dict = api.payment.create(parameters, idempotency_key)
            payment_status = PayMasterPaymentStatus(response["status"])
            external_id: int = response["id"]  # type: ignore

            if payment_status is PayMasterPaymentStatus.SETTLED:
                set_paid(payment_id, str(external_id))
            elif payment_status in (PayMasterPaymentStatus.CANCELLED,
                                    PayMasterPaymentStatus.REJECTED):
                set_canceled(payment_id, str(external_id))
        except PayMasterException:
            set_canceled(payment_id, "")
            raise

    def rc_delete(self, recurring_id: int) -> None:
        recurring_info = RecurringPaymentInfo(recurring_id, language=None)
        api = PayMasterAPI(
            merchant_id=recurring_info.paymethod["merchant_id"],  # type: ignore
            auth_token=recurring_info.paymethod["api_token"],  # type: ignore
            logger=logger
        )
        try:
            api.recurring.delete(recurring_info.recurring["token"])  # type: ignore
        except PayMasterException as error:
            sys.stdout.write(error.as_xml())

    def checkpay(self) -> None:
        with FileLock(f"var/run/lock/{MODULE}_{Feature.CHECKPAY}"):
            for paymethod in self._get_active_paymethods():
                api = PayMasterAPI(
                    merchant_id=paymethod["merchant_id"],  # type: ignore
                    auth_token=paymethod["api_token"],  # type: ignore
                    logger=logger
                )
                for payment in self._get_in_pay_payments(paymethod["id"]):  # type: ignore
                    self._check_payment_status_with_validation(payment, api)

    def _check_payment_status_with_validation(self, payment: Dict[str, Union[str, int]], 
                                              api: PayMasterAPI) -> None:
        payment_id: int = int(payment["id"])
        external_id: int
        try:
            external_id = int(payment["externalid"])
        except ValueError:
            # ошибка инициализируется для вывода ошибки в лог, но не
            # выбрасывается, чтобы не останавливать выполнение checkpay
            PayMasterValueError(
                name="external_id", value=payment.get("external_id"),
                err_object="checkpay"
            )
            set_canceled(payment_id, external_id="")
            return

        try:
            self._check_payment_status(payment_id, external_id, api)
        except PayMasterConnectionError:
            logger.warning(f"Failed to get status of payment#{payment_id}")
        except PayMasterException:
            logger.warning(f"Got error when getting status of payment#{payment_id}. Cancelling payment")
            set_canceled(payment_id, str(external_id))


    def _check_payment_status(self, payment_id: int, external_id: int,
                              api: PayMasterAPI) -> None:
        response: json_dict = api.payment.get_info(external_id)
        payment_status = PayMasterPaymentStatus(response["status"])

        is_stored_payment: bool = bool(response.get("paymentToken"))
        saved_info: Optional[RecurringInfo] = None
        if is_stored_payment:
            saved_info = RecurringInfo(
                name=f'PayMaster {response["paymentToken"]["title"]}',  # type: ignore
                status=RecurringStatus.rsStored,
                token=response["paymentToken"]["id"],  # type: ignore
                expiration_date=parse_date(response["paymentToken"]["expires"])  # type: ignore
            )

        if payment_status is PayMasterPaymentStatus.SETTLED:
            set_paid(payment_id, str(external_id), saved_info=saved_info)
        elif payment_status in (PayMasterPaymentStatus.CANCELLED,
                                PayMasterPaymentStatus.REJECTED):
            set_canceled(payment_id, str(external_id))

    @staticmethod
    def _get_active_paymethods() -> Generator[Dict[str, Union[str, int]], None, None]:
        merchant_id: str
        api_token: str
        xmlparams: ET.Element

        paymethods_sql_query: str = (
            "SELECT id, xmlparams"
            " FROM paymethod"
           f" WHERE module = '{MODULE}'"
            " AND active = 'on'"
        )
        for db_paymethod in db_query_cursor_dict(paymethods_sql_query):
            xmlparams = ET.fromstring(db_paymethod["xmlparams"])  # type: ignore
            merchant_id = xmlparams.find("merchant_id").text  # type: ignore
            api_token = xmlparams.find("api_token").text  # type: ignore

            paymethod: Dict[str, Union[str, int]] = {
                "id": db_paymethod["id"],
                "merchant_id": merchant_id,
                "api_token": api_token
            }
            yield paymethod

    @staticmethod
    def _get_in_pay_payments(
            paymethod_id: int
            ) -> Generator[Dict[str, Union[str, int]], None, None]:
        seven_days_ago: str = (date.today() - timedelta(days=7)).isoformat()
        payments_sql_query: str = (
            "SELECT id, externalid"
            " FROM payment"
           f" WHERE createdate >= '{seven_days_ago}'"
               f" AND paymethod = {paymethod_id}"
               f" AND status = {PaymentStatus.psInPay.value}"
                " AND IFNULL(externalid, '') != '' "
        )
        for payment in db_query_cursor_dict(payments_sql_query):
            yield payment

    @staticmethod
    def _get_recurring_pay_parameters(
            payment_info: PaymentInfo, recurring_info: RecurringPaymentInfo
            ) -> PaymentCreateParameters:
        payment_params: Dict[str, str] = payment_info.payment  # type: ignore
        paymethod_params: Dict[str, str] = payment_info.paymethod  # type: ignore
        recurring_params: Dict[str, str] = recurring_info.recurring  # type: ignore
        items_params: List[Dict[str, str]] = payment_info.items  # type: ignore

        is_test_mode: bool = paymethod_params.get("testmode", "on") == "on"

        receipt: Optional[PaymentCreateParameters.Receipt] = None
        are_receipts_enabled_via_paymethod: bool = \
            paymethod_params.get("paymentreceipt_type") == PaymentReceiptType.SELF
        if are_receipts_enabled_via_paymethod:
            api_items: List[PaymentCreateParameters.Receipt.Item] = list()
            for item in items_params:
                api_items.append(
                    PaymentCreateParameters.Receipt.Item(
                        name=item["locale_name"],
                        quantity=1,
                        price=float(item["amount"]),
                        vat_type=VatType.parse(float(item["taxrate"])),
                        payment_subject=PayMasterPaymentSubject.parse(int(item["payment_object"])),
                        payment_method=PayMasterPaymentMethod.parse(int(item["payment_method"])),
                        measure=ItemMeasurementUnit.PIECE
                    )
                )
            receipt = PaymentCreateParameters.Receipt(
                client=PaymentCreateParameters.Receipt.Client(
                    email=format_email(payment_params.get("useremail")),
                    phone=format_phone(payment_params.get("userphone"))
                ),
                items=api_items
            )

        return PaymentCreateParameters(
            invoice=PaymentCreateParameters.Invoice(
                description=payment_params.get("description", ""),
                order=payment_params.get("id")
            ),
            amount=PaymentCreateParameters.Amount(
                value=float(payment_params["paymethodamount"]),
                currency=recurring_info.currency["iso"]  # type: ignore
            ),
            payment_data=PaymentCreateParameters.PaymentData(
                payment_method=paymethod_params["payment_method"],
                account=str(payment_params.get("user_id")),
                phone=format_phone(payment_params.get("userphone")),
                token_id=recurring_params["token"]
            ),
            test_mode=is_test_mode,
            customer=PaymentCreateParameters.Customer(
                email=format_email(payment_params.get("useremail")),
                phone=format_phone(payment_params.get("userphone")),
                account=str(payment_params.get("user_id"))
            ),
            receipt=receipt
        )

    @staticmethod
    def _get_refund_parameters(payment_info: PaymentInfo) -> RefundCreateParameters:
        payment_params: Dict[str, str] = payment_info.payment  # type: ignore
        paymethod_params: Dict[str, str] = payment_info.paymethod  # type: ignore
        items_params: List[Dict[str, str]] = payment_info.items  # type: ignore

        amount: float = abs(
            float(payment_params["paymethodamount"])
        )

        receipt: Optional[RefundCreateParameters.Receipt] = None
        are_receipts_enabled_via_paymethod: bool = \
            paymethod_params.get("paymentreceipt_type") == PaymentReceiptType.SELF
        if are_receipts_enabled_via_paymethod:
            api_items: List[RefundCreateParameters.Receipt.Item] = list()
            for item in items_params:
                api_items.append(
                    RefundCreateParameters.Receipt.Item(
                        name=item["locale_name"],
                        quantity=1,
                        price=float(item["amount"]),
                        vat_type=VatType.parse(float(item["taxrate"])),
                        payment_subject=PayMasterPaymentSubject.parse(int(item["payment_object"])),
                        payment_method=PayMasterPaymentMethod.parse(int(item["payment_method"])),
                        measure=ItemMeasurementUnit.PIECE
                    )
                )
            receipt = RefundCreateParameters.Receipt(
                client=RefundCreateParameters.Receipt.Client(
                    email=format_email(payment_params.get("useremail")),
                    phone=format_phone(payment_params.get("userphone"))
                ),
                items=api_items
            )

        return RefundCreateParameters(
            payment_id=int(payment_params["externalid"]),
            amount=RefundCreateParameters.Amount(
                value=amount,
                currency=payment_info.currency["iso"]  # type: ignore
            ),
            receipt=receipt
        )


if __name__ == "__main__":
    PayMasterModule().run()
