"""

    AUTHOR:

        Khushal P Soonderji

    DATE:

        Saturday, 7th Dec., 2024.

    OBJECTIVE:

        To define how payment transactions will be stored in the database.

    REFERENCES:

        N/A

    DOWNLOADS:

        N/A

"""


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                   IMPORT                                                      ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# To make sibling directories accessible for imports:
import sys
sys.path.append(".")
sys.path.append("..")

# For making data behaviour_models:
from pydantic import BaseModel, Field, field_validator, PastDatetime, AwareDatetime, model_validator
from typing import Optional, Literal, Union, List, Any

# My utils:
from utils_v2.string import json
from utils_v2.string import regex
from utils_v2.date_time import date_time

# Other core models:
from models.core.user import CoreUserInfoModel

# To work with MongoDB:
from bson.objectid import ObjectId

# To work with date and time:
import datetime

# To work with currencies:
import pycountry


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                           MACROS / ONE-TIME INIT                                              ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# --- Nothing Yet


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                VARIABLES                                                      ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


# --- Nothing Yet


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                                  FUNCTIONS                                                    ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


class CustomerDetails(BaseModel):

    name: str | None = Field(
        description = "the name of the registered customer who must make the payment",
        frozen = True,
        default = None
    )

    contactNo: str = Field(
        description = "the contact no. of the registered customer who must make the payment",
        frozen = True
    )

    payerNo: str | None = Field(
        description = (
            "the phone no. to which the payment request will go;"
            "if not specified, the value of 'contactNo' will be used"
        ),
        frozen = False,
        default = None
    )

    email: str | None = Field(
        description = "the e-mail id of the registered customer",
        pattern = regex.REGEX_EMAIL_ID,
        frozen = True,
        default = None
    )

    # ┏┓    ┏•
    # ┃ ┏┓┏┓╋┓┏┓
    # ┗┛┗┛┛┗┛┗┗┫
    #          ┛

    class Config:
        extra = "allow"
        arbitrary_types_allowed = True

    # ┓┏  ┓• ┓   •
    # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
    # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗

    @model_validator(mode = "after")
    def validate_model(cls, values):
        if values.payerNo is None: values.payerNo = values.contactNo
        return values


# ---------------------------------------------------------------------------------------------------------------------


class PaymentMetadata(BaseModel):

    """
    These are things that the UI will send you, and you must use them as-is for adding receipts. Their internal workings
    and use-cases are not known and are not of this code's direct concern except that you must ensure that these details
    get delivered through the mechanism of adding receipts.
    """

    idUser: int | None = Field(frozen = True, default = None, validate_default = True)
    idClient: int | None = Field(frozen = True, default = None, validate_default = True)
    inAccount: int | None = Field(frozen = True, default = None, validate_default = True)
    reference: str | None = Field(frozen = True, default = None, validate_default = True)
    againstReference: str | None = Field(frozen = True, default = None, validate_default = True)
    documentUrl: str | None = Field(frozen = False, default = None, validate_default = True)

    # ┏┓    ┏•
    # ┃ ┏┓┏┓╋┓┏┓
    # ┗┛┗┛┛┗┛┗┗┫
    #          ┛

    class Config:
        extra = "allow"

    # ┓┏  ┓• ┓   •
    # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
    # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗

    @field_validator("reference")
    def validate_reference(cls, value):
        if not value: value = "On Account"
        return value

    @field_validator("againstReference")
    def validate_against_reference(cls, value):
        if not value: value = None
        return value

    @field_validator("documentUrl")
    def validate_document_url(cls, value):
        if not value: value = None
        return value


# ---------------------------------------------------------------------------------------------------------------------


class PaymentEvent(BaseModel):

    eventTs: AwareDatetime = Field(
        description = "to know the date and time (utc) of this update",
        frozen = True,
        default_factory = lambda: date_time.get_current_utc_date_time(as_string = False)
    )

    paymentStatus: Literal[
        "queued",  # ....... When the UI sends a payment request, but the payment gateway (PG) hasn't received it yet.
        "initFailed",  # ... When we tried to initiate the request, but the PG rejected it.
        "initiated",  # .... When we made a successful payment request, or the customer initiated one from the PG.
        "failed",  # ....... When the customer tried paying, but it failed (e.g.: because of an incorrect pin).
        "rejected",  # ..... When the customer explicitly rejected the payment.
        "authorized",  # ... When the customer made the payment (but it hasn't been settled in your account yet).
        "settled",  # ...... When the PG sends the money to your account.
        "refunded",  # ..... When the money was refunded to the client.
        "unknown"  # ....... When integrating a new gateway and some specific status is not known.
    ] = Field(
        description = "the status of the payment request to see what stage of the process we are in",
        frozen = False
    )

    message: str | None = Field(
        description = "a hint about what happened at this stage",
        frozen = True,
        default = None
    )

    initByPG: bool = Field(
        description = "to figure out whether the payment gateway initiated this event or we did",
        frozen = True
    )

    ipAddr: str | None = Field(
        description = "if initiated by the payment gateway, what ip addr did this request come from",
        frozen = True,
        default = None
    )

    httpCode: int | None = Field(
        description = "the http code generated by the event",
        frozen = True,
        examples = [200, 400, 401]
    )

    headers: dict = Field(
        description = "the headers that came in with the event; useful when trying to decode who generated the event"
    )

    payload: dict = Field(
        description = "the json payload or set of query params received from an event from the payment gateway",
        frozen = True
    )

    # ┏┓    ┏•
    # ┃ ┏┓┏┓╋┓┏┓
    # ┗┛┗┛┛┗┛┗┗┫
    #          ┛

    class Config:
        extra = "allow"
        arbitrary_types_allowed = True

    # ┓┏  ┓• ┓   •
    # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
    # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗

    @field_validator("eventTs", mode = "before")
    def parse_date_time(cls, value):
        return date_time.parse_date_time(input_value = value, timezone = date_time.TIMEZONE_UTC)

    # ┏┓           ┏┓
    # ┃ ┓┏┏╋┏┓┏┳┓  ┣ ┓┏┏┓┏┏
    # ┗┛┗┻┛┗┗┛┛┗┗  ┻ ┗┻┛┗┗┛

    def to_markdown(self):
        message = f"Time: `{self.eventTs}`\n"
        message += f"Brief: `{self.message}`\n"
        message += f"initialized by PG: `{self.initByPG}`\n"
        message += "```\n"
        message += json.to_string(self.payload, default = str)
        message += "\n```"
        return message


# ---------------------------------------------------------------------------------------------------------------------


class CorePaymentModel(BaseModel):

    paymentId: ObjectId = Field(
        description = "the id of the document in mongodb that holds this information",
        frozen = True,
        default = None,
        alias = "_id",
        exclude = True
    )

    user: CoreUserInfoModel = Field(
        description = "how you identify your user",
        frozen = True
    )

    customer: CustomerDetails = Field(
        description = "how you identify your user's customer",
        frozen = True
    )

    ts: AwareDatetime = Field(
        description = "the time (utc) at which the payment was first initiated",
        frozen = True,
        default_factory = lambda: date_time.get_current_utc_date_time(as_string = False)
    )

    lastEventTs: AwareDatetime | None = Field(
        description = "the time (utc) at which the last event occurred",
        frozen = True,
        default = None
    )

    lastEventMessage: str | None = Field(
        description = "a short, human-readable message",
        frozen = False,
        default = None
    )

    lastPaymentStatus: str = Field(
        description = "the status of the payment request to see what stage of the process we are in",
        frozen = False
    )

    tokenId: ObjectId = Field(
        description = "the id of the auth token that is associated with this payment",
        frozen = True
    )

    amount: float | int = Field(
        description = "the amount of money being requested",
        frozen = True
    )

    currencyCode: str = Field(
        description = "the three-letter iso 4217 code to identify the currency",
        frozen = True,
        examples = ["INR", "USD", "KES"]
    )

    metadata: PaymentMetadata = Field(
        description = "any arbitrary amount of data to identify the user and payment details",
        frozen = True
    )

    tags: List[Any] = Field(
        description = "a list of keywords to apply to this file/dir to filter it later",
        frozen = False,
        examples = ["renewal", "subscription"]
    )

    serviceType: Literal["paymentGateway"] = Field(
        description = "to identify the kind of service",
        frozen = True,
        default = None
    )

    client: Literal["razorpay", "safaricomMPesaExpress"] = Field(
        description = "the third-part client that was used",
        frozen = True
    )

    clientPaymentReferenceId: int | str | None = Field(
        description = "the reference id given by the third-party client",
        frozen = False,
        default = None
    )

    events: List[PaymentEvent] = Field(
        description = "an array of all the events that happened in the process of this payment",
        frozen = False
    )

    # ┏┓    ┏•
    # ┃ ┏┓┏┓╋┓┏┓
    # ┗┛┗┛┛┗┛┗┗┫
    #          ┛

    class Config:
        extra = "allow"
        arbitrary_types_allowed = True

    def model_dump(self, *args, **kwargs):
        return super().model_dump(*args, by_alias = True, **kwargs)

    # ┏┓           •
    # ┃┃┏┓┏┓┏┓┏┓┏┓╋┓┏┓┏
    # ┣┛┛ ┗┛┣┛┗ ┛ ┗┗┗ ┛
    #       ┛

    @property
    def full(self):
        payment_json = {
            "paymentId": str(self.paymentId),
            "user": self.user.model_dump(),
            "customer": self.customer.model_dump(),
            "ts": self.ts.isoformat(),
            "lastEventTs": self.lastEventTs,
            "lastEventMessage": self.lastEventMessage,
            "lastPaymentStatus": self.lastPaymentStatus,
            "amount": self.amount,
            "currencyCode": self.currencyCode,
            "client": self.client,
            "clientPaymentReferenceId": self.clientPaymentReferenceId,
            "tags": self.tags,
            "metadata": self.metadata.model_dump(),
            "events": []
        }
        for e in self.events:
            payment_json["events"].append({
                "eventTs": e.eventTs,
                "paymentStatus": e.paymentStatus,
                "initByPG": e.initByPG,
                "message": e.message
            })
        return payment_json

    @property
    def preview(self):
        return {
            "paymentId": str(self.paymentId),
            "user": self.user.model_dump(),
            "customer": self.customer.model_dump(),
            "ts": self.ts.isoformat(),
            "lastEventTs": self.lastEventTs,
            "lastPaymentStatus": self.lastPaymentStatus,
            "amount": self.amount,
            "currencyCode": self.currencyCode,
            "client": self.client,
            "clientPaymentReferenceId": self.clientPaymentReferenceId,
            "tags": self.tags,
            "metadata": self.metadata
        }

    # ┓┏  ┓• ┓   •
    # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
    # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗

    @field_validator("ts", "lastEventTs", mode = "before")
    def parse_date_time(cls, value):
        return date_time.parse_date_time(input_value = value, timezone = date_time.TIMEZONE_UTC)

    @field_validator("tokenId", mode = "before")
    def parse_oid(cls, value):
        try:
            if isinstance(value, str):
                value = ObjectId(value)
        except: pass
        return value

    @field_validator("currencyCode", mode = "before")
    def validate_currency(cls, value):
        currency = pycountry.currencies.get(alpha_3 = value)
        if currency is None: raise ValueError("invalid currency code, please use iso 4217 standard")
        return value

    @field_validator("tags", "events", mode = "before")
    def validate_null_lists(cls, value):
        if value is None: value = []
        return value

    # ┏┓           ┏┓
    # ┃ ┓┏┏╋┏┓┏┳┓  ┣ ┓┏┏┓┏┏
    # ┗┛┗┻┛┗┗┛┛┗┗  ┻ ┗┻┛┗┗┛

    def to_markdown(self, include_events: bool = False):

        # Build the base message:
        message = f"*PAYMENT RECORD*:\n\n"
        message += f"Requested `{self.currencyCode} {self.amount}` from the user identified by/as:\n"
        message += "```\n"
        message += json.to_string(self.customer.model_dump(), default = str)
        message += "\n```\n\n"
        message += f"Last Status: `{self.lastPaymentStatus}`\n"
        message += f"Last Message: `{self.lastEventMessage}`\n\n"

        # Build the events (if asked):
        if include_events:
            message += "Here are all the events for your reference:\n\n"
            for index, event in enumerate(self.events):
                message += f"*EVENT {index:0>2}*.\n"
                message += event.to_markdown()
                message += "\n"

        # Done here:
        return message


#   *****************************************************************************************************************
#  *****                                                                                                          ****
# ***                                               MAIN PROGRAM                                                    ***
#  *****                                                                                                          ****
#   *****************************************************************************************************************


if __name__ == "__main__":

    from utils_v2.string import json

    now = date_time.get_current_utc_date_time(as_string = False)

    payment = CorePaymentModel(
        tokenId = "67519cf3a7804fcbc6f12452",
        user = CoreUserInfoModel(
            userId = 1
        ),
        customer = CustomerDetails(
            name = "Bhopli Narangi",
            contactNo = "9876543210",
            email = "bhopli@orange.com"
        ),
        amount = 1.00,
        currencyCode = "INR",
        metadata = {
            "key": "value"
        },
        tags = [
            "some",
            "tags"
        ],
        client = "razorpay",
        clientPaymentReferenceId = "txn_123_abc",
        lastPaymentStatus = "settled",
        lastEventTs = now,
        events = [
            PaymentEvent(
                eventTs = now - datetime.timedelta(minutes = 1, seconds = 12),
                paymentStatus = "initiated",
                initByPG = False,
                httpCode = None,
                headers = {
                    "some": "header"
                },
                payload = {
                    "status": "captured",
                    "from": "Barfi",
                }
            ),
            PaymentEvent(
                eventTs = now,
                paymentStatus = "settled",
                initByPG = True,
                httpCode = None,
                headers = {
                    "some": "header"
                },
                payload = {
                    "status": "authorized",
                    "from": "Barfi",
                    "amount": -100.00,
                    "description": "meow"
                }
            )
        ]
    )

    print("PAYMENT TXN. MODEL:", json.to_string(payment.model_dump(), default = str))
