#!/usr/bin/env python3

import argparse
import datetime as dt
import sys
import xml.etree.ElementTree as ET

sys.path.insert(0, "/usr/local/mgr5/lib/python")

import billmgr.db as db
import billmgr.exception as exc
import billmgr.logger as logging
import billmgr.misc as misc
import billmgr.payment as payment
import billmgr.session as session
import paypalexpanded.api as paypal_api
from billmgr.modules.paymethod import (
    Feature,
    Param,
    PaymentBehavior,
    PaymentTypeInfo,
    PaymethodModule,
    RecurringType,
    recurring_type_param,
)

logging.init_logging("pmpaypalexpanded")
logger = logging.get_logger("pmpaypalexpanded")


MODULE_NAME = "pmpaypalexpanded"

class PayPalExpanded(PaymethodModule):
    def __init__(self) -> None:
        super().__init__()
        self.set_description("PayPal Expanded Integration")

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

        self._add_callable_feature(Feature.PMUSERCREATE, self.pm_user_create)
        self._add_callable_feature(Feature.PMTUNE, self.pm_tune)
        self._add_callable_feature(Feature.PMVALIDATE, self.pm_validate)

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

        self._add_feature(Feature.RP_SUCCESS)
        self._add_feature(Feature.RP_FAIL)

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

        self._add_callable_feature(Feature.CHECKPAY, self.checkpay)

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

        self._set_paymethod_payment_type(
            "payment_method",
            "",
            [
                PaymentTypeInfo(PaymentBehavior.pbRedirect, "apple_pay", False, {}),
                PaymentTypeInfo(PaymentBehavior.pbRedirect, "google_pay", False, {}),
                PaymentTypeInfo(PaymentBehavior.pbRedirect, "card", True, {}),
                PaymentTypeInfo(PaymentBehavior.pbRedirect, "paypal", True, {}),
                PaymentTypeInfo(PaymentBehavior.pbRedirect, "bancontact", False, {}),
                PaymentTypeInfo(PaymentBehavior.pbRedirect, "blik", False, {}),
                PaymentTypeInfo(PaymentBehavior.pbRedirect, "eps", False, {}),
                # PaymentTypeInfo(PaymentBehavior.pbRedirect, "giropay", False, {}),
                # deprecated by paypal
                PaymentTypeInfo(PaymentBehavior.pbRedirect, "ideal", False, {}),
                PaymentTypeInfo(PaymentBehavior.pbRedirect, "mybank", False, {}),
                PaymentTypeInfo(PaymentBehavior.pbRedirect, "p24", False, {}),
                # PaymentTypeInfo(PaymentBehavior.pbRedirect, "sofort", False, {}),
                # deprecated by paypal
                PaymentTypeInfo(PaymentBehavior.pbRedirect, "venmo", True, {}),
                PaymentTypeInfo(PaymentBehavior.pbRedirect, "pay_upon_invoice", False, {}),
                PaymentTypeInfo(PaymentBehavior.pbRedirect, "multibanco", False, {}),
                PaymentTypeInfo(PaymentBehavior.pbRedirect, "trustly", False, {}),
            ],
        )

    def _on_raise_exception(self, args: argparse.Namespace, err: exc.XmlException):
        print(err.as_xml())

    def cr_set(self):
        pass

    def pm_user_create(self):
        pass

    def pm_tune(self):
        xml = session.get_input_xml()
        xml.append(
            session.new_node(
                "outerlink", "https://www.paypal.com/ru/cgi-bin/webscr?cmd=_biz-signup-flow"
            )
        )
        ET.dump(xml)

    def pm_validate(self):
        xml = session.get_input_xml()

        client_id = xml.findtext("client_id", default="")
        secret = xml.findtext("secret", default="")
        is_test = xml.findtext("test", default="on") == "on"
        # webhook_id = xml.findtext("webhook_id", default="")

        paypal_api.authenticate(paypal_api.AuthData(client_id, secret, is_test))

        paymethod_id = xml.findtext("paymethod/id", default="")
        paymethod_currency = xml.findtext("paymethod/currency", default="")
        paymethods = xml.findtext("payment_method", default="").split(",")

        if paymethod_id:
            currency_iso = misc.get_currency_data(int(paymethod_currency)).iso
            for pm, supported_currencies in paypal_api.SUPPORTED_CURRENCIES.items():
                if pm in paymethods and currency_iso not in supported_currencies:
                    exception = exc.XmlException("pp_unsupported_currency")
                    exception.add_param("currencies", ", ".join(supported_currencies))
                    exception.add_param("pm", pm)
                    raise exception

    def rf_set(self):
        xml = session.get_input_xml()

        logger.debug(f"xml = {ET.tostring(xml, encoding='utf8')}")

        payment_id = int(xml.findtext("source_payment", default=""))
        refund_payment_id = int(xml.findtext("payment", default=""))

        payment_params = self.get_payment_params(payment_id)
        method_params = self.get_method_params(int(payment_params["paymethod"]))

        paymethod_refund_amount = paypal_api.amount_from_str(
            xml.findtext("payment_paymethodamount", default="")
        )
        paymethod_refund_amount = -paymethod_refund_amount

        auth = paypal_api.authenticate(
            paypal_api.AuthData(
                method_params["client_id"], method_params["secret"], method_params["test"] == "on"
            )
        )

        capture_id = payment_params["externalid"].split("/")[1]

        is_full_refund = payment_id == refund_payment_id
        if is_full_refund:
            response = paypal_api.request_refund_full_captured_order(
                auth, capture_id, invoice=str(refund_payment_id)
            )
        else:
            paymethod_currency = xml.findtext("paymethod_currency", default="")
            response = paypal_api.request_refund_captured_order(
                auth,
                capture_id,
                amount=paymethod_refund_amount,
                currency_iso=misc.get_currency_data(int(paymethod_currency)).iso,
                invoice=str(refund_payment_id),
            )

        json = response.json()
        status = json["status"]

        if status == paypal_api.RefundStatus.COMPLETED.name:
            logger.debug("refund completed")

        elif status == paypal_api.RefundStatus.PENDING.name:
            reason = json.get("status_details", {}).get("reason", "")
            logger.warning(f"refund pending: {reason}")
            raise paypal_api.RefundStatusFailedError("status_pending")

        elif status == paypal_api.RefundStatus.CANCELLED.name:
            raise paypal_api.RefundStatusCancelledError()

        elif status == paypal_api.RefundStatus.FAILED.name:
            reason = json.get("status_details", {}).get("reason", "")
            raise paypal_api.RefundStatusFailedError(reason)

    def rc_pay(self, payment_id: int):
        payment_params = self.get_payment_params(payment_id)
        logger.debug(f"payment_params = {payment_params}")

        method_params = self.get_method_params(int(payment_params["paymethod"]))
        logger.debug(f"method_param = {method_params}")

        recurring_params = self.get_recurring_params(int(payment_params["recurring"]))
        logger.debug(f"recurring_params = {recurring_params}")
        currency_iso: str = payment.PaymentInfo(payment_id, language=None).currency["iso"]  # type: ignore

        try:
            auth = paypal_api.authenticate(
                paypal_api.AuthData(
                    method_params["client_id"],
                    method_params["secret"],
                    method_params["test"] == "on",
                )
            )

            response = paypal_api.request_create_order(
                auth,
                order=paypal_api.Order(
                    amount=paypal_api.amount_from_str(payment_params["amount"]),
                    currency_iso=currency_iso,
                    reference_id=str(payment_id),
                    need_save=False,
                    urls=paypal_api.Urls(
                        return_url="",
                        cancel_url="",
                    ),
                ),
                paymethod=recurring_params["data2"],
                saved_token=recurring_params["token"],
            )

            if response.json()["status"] != "COMPLETED":
                raise exc.XmlException("pp_autopayment_failure")

            captured_order = paypal_api.parse_capture_order(response)
            external_id = f"{captured_order.order_id}/{captured_order.capture_id}"
            payment.set_paid(payment_id, external_id)

        except Exception as e:
            payment.set_canceled(payment_id, "")
            raise e

    def checkpay(self):
        with misc.FileLock(f"var/run/lock/{MODULE_NAME}_{Feature.CHECKPAY}") as _:
            self.__checkpay_no_lock()

    def __checkpay_no_lock(self):
        from_date = dt.datetime.today() - dt.timedelta(days=7)
        records = db.db_query(
            " SELECT p.id, p.externalid, p.xmlparams AS payment_xmlparams, pm.xmlparams"
            " AS paymethod_xmlparams"
            " FROM payment p"
            " JOIN paymethod pm ON pm.id = p.paymethod"
            f' WHERE module = "{MODULE_NAME}"'
            f" AND p.status = {payment.PaymentStatus.psInPay.value}"
            f" AND p.createdate >= '{from_date.strftime('%Y-%m-%d')}'"
        )

        for rec in records:
            external_id = rec.as_str("externalid")
            logger.info(f"elid: {external_id}")

            paymethod_xmlparams = ET.fromstring(rec.as_str("paymethod_xmlparams"))
            auth = paypal_api.authenticate(
                paypal_api.AuthData(
                    paymethod_xmlparams.findtext("client_id", default=""),
                    paymethod_xmlparams.findtext("secret", default=""),
                    paymethod_xmlparams.findtext("test", default="") == "on",
                )
            )

            try:
                if "/" in external_id:
                    self._process_order_with_capture(rec, auth)
                else:
                    self._process_order_only(rec, auth)
            except Exception as e:
                logger.warning(f"error processing payment {external_id}: {e}")

    def _process_order_only(self, rec, auth):
        """For a payment whose externalId is a bare order_id – meaning the client did not 
        reach ppexpandedredirect – we check the status against the order, 
        without using a capture_id."""
        payment_id = rec.as_int("id")
        order_id = rec.as_str("externalid")
        payment_xmlparams = ET.fromstring(rec.as_str("payment_xmlparams"))

        resp = paypal_api.request_show_order_details(auth, order_id)
        data = resp.json()
        status = data["status"]
        logger.info(f"order {order_id} status: {status}")

        if status == paypal_api.CaptureStatus.COMPLETED.name:
            try:
                capture_id = self._get_capture_id(auth, order_id, data)
            except Exception as e:
                logger.error(f"capture failed for completed order {order_id}: {e}")
                return
            self._mark_paid(payment_id, f"{order_id}/{capture_id}", payment_xmlparams, lambda: resp)

        elif status == paypal_api.CaptureStatus.APPROVED.name:
            try:
                capture_resp = paypal_api.request_capture_order(auth, order_id)
                captured = paypal_api.parse_capture_order(capture_resp)
                self._mark_paid(
                    payment_id,
                    f"{captured.order_id}/{captured.capture_id}",
                    payment_xmlparams,
                    lambda: resp,
                )
                logger.info(f"Successfully captured order {order_id} via checkpay")
            except Exception as e:
                logger.warning(f"Capture attempt failed for order {order_id}: {e}")
                if self._is_stale(data.get("create_time")):
                    payment.set_canceled(payment_id, order_id)
                    logger.info(f"Cancelled expired order {order_id}")

        elif status == paypal_api.CaptureStatus.CREATED.name:
            if self._is_stale(data.get("create_time")):
                payment.set_canceled(payment_id, order_id)
                logger.info(f"Cancelled stale order {order_id} (CREATED)")

        elif status in (
            paypal_api.CaptureStatus.DECLINED.name,
            paypal_api.CaptureStatus.FAILED.name,
        ):
            payment.set_canceled(payment_id, order_id)

    def _process_order_with_capture(self, rec, auth):
        """For a payment whose externalId is already in the format order_id/capture_id 
        (previous behavior, no changes)."""
        payment_id = rec.as_int("id")
        external_id = rec.as_str("externalid")
        order_id, capture_id = external_id.split("/", 1)

        response = paypal_api.request_show_captured_payment_details(auth, capture_id)
        status = response.json()["status"]

        if status == paypal_api.CaptureStatus.COMPLETED.name:
            payment_xmlparams = ET.fromstring(rec.as_str("payment_xmlparams"))
            self._mark_paid(
                payment_id,
                external_id,
                payment_xmlparams,
                lambda: paypal_api.request_show_order_details(auth, order_id),
            )

        elif status in (
            paypal_api.CaptureStatus.DECLINED.name,
            paypal_api.CaptureStatus.FAILED.name,
        ):
            payment.set_canceled(payment_id, external_id)

    def _mark_paid(self, payment_id, external_id, payment_xmlparams, get_order_details):
        if payment_xmlparams.findtext("store_payment", default="") != "on":
            payment.set_paid(payment_id, external_id)
            return

        stored_token = paypal_api.get_stored_token(get_order_details())
        if stored_token is None:
            raise paypal_api.TokenNotFoundError()
        payment.set_paid(
            payment_id,
            external_id,
            payment.RecurringInfo(
                status=payment.RecurringStatus.rsStored,
                token=stored_token,
                data1="",
                data2=payment_xmlparams.findtext("payment_method", default=""),
                name="",
            ),
        )

    @staticmethod
    def _get_capture_id(auth, order_id, order_details):
        """It takes the capture_id from any existing captures;
          if there are none, it means the order is confirmed but not yet captured — so 
          it captures it right now."""
        captures = (
            order_details.get("purchase_units", [{}])[0].get("payments", {}).get("captures", [])
        )
        if captures:
            return captures[0]["id"]
        response = paypal_api.request_capture_order(auth, order_id)
        return paypal_api.parse_capture_order(response).capture_id

    @staticmethod
    def _is_stale(create_time) -> bool:
        STALE_ORDER_AGE = dt.timedelta(hours=2)
        """True if the order was created earlier than STALE_ORDER_AGE ago."""
        if not create_time:
            return False
        try:
            created_dt = dt.datetime.fromisoformat(create_time.replace("Z", "+00:00"))
        except Exception as e:
            logger.warning(f'could not parse create_time "{create_time}": {e}')
            return False
        return dt.datetime.now(dt.UTC) - created_dt > STALE_ORDER_AGE


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