#!/usr/bin/python3

import json
from os import environ, EX_OK, EX_DATAERR, EX_UNAVAILABLE

import sys
from typing import Dict, NoReturn, Optional
from urllib.parse import parse_qs

sys.path.insert(0, "/usr/local/mgr5/lib/python")
from billmgr.exception import log_backtrace
from billmgr.logger import Logger, init_logging, get_logger
from billmgr.modules.paymentcgi import process_test_cgi_run
from billmgr.payment import (
    PaymentInfo, RecurringInfo, RecurringPaymentInfo, RecurringStatus,
    save_recurring_info, set_paid, set_canceled
)

from paymaster.api.enumerations import (
    PaymentStatus as PayMasterPaymentStatus, RecurringStatus as PayMasterRecurringStatus
)
from paymaster.api.client import PayMasterAPI
from paymaster.exceptions import AuthTokenError, PayMasterException, PayMasterValueError
from paymaster.utils import generate_notification_token, parse_date, json_dict


MODULE: str = "paymasterv2notification"

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


# PaymentCgi не приспособлен для наследования класса уведомлений; часть логики
# взята оттуда. В будущем стоит создать абстрактный CGI класс уведомлений
class PayMasterNotificationCGI:
    """CGI-класс для обработки уведомлений от платёжной системы.
    """

    def __init__(self) -> None:
        logger.info("environ list")
        for key, val in environ.items():
            logger.info(f"{key} = {val}")

        self.query_string: Dict[str, str] = dict()
        for key, value in parse_qs(environ["QUERY_STRING"]).items():
            self.query_string[key] = value[0]

        content_length: int = int(environ.get("CONTENT_LENGTH") or 0)
        if not content_length:
            raise PayMasterException(
                err_type="validation_error",
                err_object=type(self).__name__,
                err_value="Request body is empty"
            )
        raw_input: str = sys.stdin.read(content_length)
        self.input: json_dict = json.loads(raw_input)
        logger.info(f"input = {self.input.items()}")

    def process(self) -> None:

        is_payment: bool = self.input["status"] in [
            s.value for s in PayMasterPaymentStatus
        ]
        is_binding: bool = self.input["status"] in [
            s.value for s in PayMasterRecurringStatus
        ]
        is_payment_method_stored: bool = self.query_string.get("storepayment") == "yes"

        if is_payment:
            if is_payment_method_stored:
                self._process_payment_with_binding()
            else:
                self._process_regular_payment()
        elif is_binding:
            self._process_recurring_payment_method()

    def _process_payment_with_binding(self) -> None:
        payment_id: int = int(self.input["invoice"]["orderNo"])  # type: ignore
        external_id: int = int(self.input["id"])  # type: ignore
        status = PayMasterPaymentStatus(self.input["status"])

        self._check_auth(payment_id=payment_id)

        if status is PayMasterPaymentStatus.SETTLED:
            # дополнительное обращение по API в PayMaster для получения информации
            # о токене ССО. Основное уведомление не присылает информацию о токенизации
            payment_info = PaymentInfo(payment_id, language=None)
            api = PayMasterAPI(
                payment_info.paymethod["merchant_id"],  # type: ignore
                payment_info.paymethod["api_token"],  # type: ignore
                logger
            )
            response: json_dict = api.payment.get_info(external_id)

            set_paid(
                payment_id=payment_id,
                external_id=str(external_id),
                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
                )
            )
        elif status in (PayMasterPaymentStatus.CANCELLED,
                                     PayMasterPaymentStatus.REJECTED):
            set_canceled(payment_id, str(external_id))

    def _process_regular_payment(self) -> None:
        payment_id: int = int(self.input["invoice"]["orderNo"])  # type: ignore
        external_id: int = int(self.input["id"])  # type: ignore
        status = PayMasterPaymentStatus(self.input["status"])

        self._check_auth(payment_id=payment_id)

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

    def _process_recurring_payment_method(self) -> None:
        recurring_id: int = int(self.query_string.get("recurringid") or 0)
        if not recurring_id:
            return

        self._check_auth(recurring_id=recurring_id)

        response_status = PayMasterRecurringStatus(self.input["status"])
        billmgr_status: RecurringStatus
        if response_status is PayMasterRecurringStatus.ACTIVE:
            if self.query_string.get("autopayment") == "yes":
                billmgr_status = RecurringStatus.rsActive
            else:
                billmgr_status = RecurringStatus.rsStored
        elif response_status is PayMasterRecurringStatus.REVOKED:
            if self.query_string.get("autopayment") == "yes":
                billmgr_status = RecurringStatus.rsClosed
            else:
                billmgr_status = RecurringStatus.rsDisabled

        save_recurring_info(
            recurring_id=recurring_id,
            recurring_info=RecurringInfo(
                name=f'PayMaster {self.input["title"]} #{recurring_id}',  # type: ignore
                status=billmgr_status,  # type: ignore
                token=self.input["id"],  # type: ignore
                expiration_date=parse_date(self.input["expires"])  # type: ignore
            )
        )

    def _check_auth(
            self, *,
            payment_id: Optional[int] = None,
            recurring_id: Optional[int] = None
            ) -> Optional[NoReturn]:
        if payment_id:
            info = PaymentInfo(payment_id, None)
        elif recurring_id:
            info = RecurringPaymentInfo(recurring_id, None)
        else:
            raise PayMasterValueError(name="payment_id/recurring_id", value=None,
                                      err_object=type(self).__name__)
        real_token: str = generate_notification_token(
            merchant_id=info.paymethod["merchant_id"],  # type: ignore
            api_token=info.paymethod["api_token"],  # type: ignore
            id_=payment_id or recurring_id  # type: ignore
        )
        provided_token: Optional[str] = self.query_string.get("notificationtoken")

        if real_token != provided_token:
            raise AuthTokenError(type(self).__name__)


if __name__ == "__main__":
    process_test_cgi_run()

    exit_code: int = EX_OK
    http_response_code: str = "200 OK"

    try:
        cgi = PayMasterNotificationCGI()
        cgi.process()
    except PayMasterException as error:
        sys.stderr.write(error.as_xml())
        exit_code = EX_DATAERR
        http_response_code = "400 Bad Request"
    except Exception:
        log_backtrace()
        exit_code = EX_UNAVAILABLE
        http_response_code = "500 Internal Server Error"

    sys.stdout.write(
       f"HTTP/1.1 {http_response_code}\n"
        "Content-Type: text/html; charset=utf-8\n"
        "\n"
       f"<html><body><div>{http_response_code}</div></body></html>"
    )
    sys.exit(exit_code)
