from datetime import datetime, timezone
from enum import Enum, EnumMeta
import re
from typing import Any, List, Optional, Union
from urllib.parse import parse_qsl, urlsplit

import keble_exceptions
from pydantic import (
    AliasChoices,
    AnyHttpUrl,
    BaseModel,
    Field,
    GetCoreSchemaHandler,
    field_validator,
    model_validator,
)
from pydantic_core import CoreSchema, core_schema
from typing_extensions import Annotated

from keble_helpers.typings.id import ObjectId

from . import PydanticModelConfig
from .standard_number import StdMoneyAmount, StdPercentage

DEFAULT_MODEL_CONFIG = PydanticModelConfig.default()

__all__ = [
    "AmazonMarketplace",
    "AmazonMarketplaceEU",
    "Currency",
    "ExchangeRateInUsd",
    "Language",
    "Marketplace",
    "Money",
    "MoneyRange",
    "MongoObjectBase",
    "NaturalLanguage",
    "OneOrMoreVectors",
    "OwnerType",
    "PercentageRange",
    "PromptRole",
    "QdrantSchemaBase",
    "SchemaBase",
    "SharingScope",
    "Status",
    "StrapiSchemaBase",
    "Timestamp",
    "Vector",
    "is_vectors_list",
]

Vector = List[float]

OneOrMoreVectors = Vector | List[Vector]


def is_vectors_list(vectors: OneOrMoreVectors) -> bool:
    if vectors is None or len(vectors) == 0:
        raise keble_exceptions.ServerSideInvalidParams(
            invalid_params="vectors",
            admin_note=vectors,
            alert_admin=True,
            expected="not(vectors is None or len(vectors) == 0)",
            but_got=f"vectors={vectors}",
        )
        # raise ValueError("[Typing] Empty vectors")

    if isinstance(vectors, list) and len(vectors) > 0 and isinstance(vectors[0], list):
        return True
    return False


class _PromptRoleAliasEnumMeta(EnumMeta):
    # name alias in Enum class
    # credit: https://stackoverflow.com/questions/24105268/is-it-possible-to-override-new-in-an-enum-to-parse-strings-to-an-instance
    def __call__(cls, value, *args, **kw):
        if isinstance(value, str):
            # map strings to enum values
            if value == "ai":
                value = "assistant"
            if value == "system":
                value = "system"
            if value == "human":
                value = "user"
        return super().__call__(value, *args, **kw)


class PromptRole(str, Enum, metaclass=_PromptRoleAliasEnumMeta):
    """Prompt role enum in openai chatgpt fashion"""

    system = "system"
    user = "user"
    assistant = "assistant"


class Language(str, Enum):
    ENGLISH = "ENGLISH"
    SIMPLIFIED_CHINESE = "SIMPLIFIED_CHINESE"

    # extends by AI base on marketplace of amazon
    GERMAN = "GERMAN"
    FRENCH = "FRENCH"
    ITALIAN = "ITALIAN"
    SPANISH = "SPANISH"
    DUTCH = "DUTCH"
    PORTUGUESE = "PORTUGUESE"
    JAPANESE = "JAPANESE"
    TURKISH = "TURKISH"
    POLISH = "POLISH"
    SWEDISH = "SWEDISH"
    ARABIC = "ARABIC"
    HINDI = "HINDI"
    THAI = "THAI"
    KOREAN = "KOREAN"
    VIETNAMESE = "VIETNAMESE"
    INDONESIAN = "INDONESIAN"
    HEBREW = "HEBREW"
    NORWEGIAN = "NORWEGIAN"
    DANISH = "DANISH"
    FINNISH = "FINNISH"


class NaturalLanguage(BaseModel):
    """represent natural language"""

    model_config = DEFAULT_MODEL_CONFIG

    simplified_chinese: str
    english: str

    def get_text(self, language: Language) -> str:
        if language == Language.SIMPLIFIED_CHINESE:
            return self.simplified_chinese
        # by default, return english
        return self.english


class UnspecifiedMarketplace(str, Enum):
    GLOBAL = "GLOBAL"
    UNKNOWN = "UNKNOWN"


class Marketplace(str, Enum):
    CN = "CN"
    US = "US"
    CA = "CA"
    JP = "JP"
    FR = "FR"
    IT = "IT"
    DE = "DE"
    ES = "ES"
    UK = "UK"
    BR = "BR"
    BE = "BE"
    NL = "NL"
    SE = "SE"
    PL = "PL"
    EG = "EG"
    TR = "TR"
    SA = "SA"
    AE = "AE"
    IN = "IN"
    SG = "SG"
    AU = "AU"
    MX = "MX"

    @property
    def currency(self) -> "Currency":
        if self.value == Marketplace.US:
            return Currency.USD
        if self.value == Marketplace.CA:
            return Currency.CAD
        if self.value == Marketplace.JP:
            return Currency.JPY
        if self.value == Marketplace.UK:
            return Currency.GBP
        if self.value == Marketplace.SE:
            return Currency.SEK
        if self.value == Marketplace.PL:
            return Currency.PLN
        if self.value in [
            Marketplace.DE,
            Marketplace.ES,
            Marketplace.FR,
            Marketplace.IT,
            Marketplace.BE,
            Marketplace.NL,
        ]:
            return Currency.EUR
        if self.value == Marketplace.MX:
            return Currency.MXN
        if self.value == Marketplace.BR:
            return Currency.BRL
        if self.value == Marketplace.CN:
            return Currency.CNY
        if self.value == Marketplace.EG:
            return Currency.EGP
        if self.value == Marketplace.TR:
            return Currency.TRY
        if self.value == Marketplace.SA:
            return Currency.SAR
        if self.value == Marketplace.AE:
            return Currency.AED
        if self.value == Marketplace.IN:
            return Currency.INR
        if self.value == Marketplace.SG:
            return Currency.SGD
        if self.value == Marketplace.AU:
            return Currency.AUD
        raise keble_exceptions.UnhandledScenarioOrCase(
            admin_note={"self.value": self.value},
            alert_admin=True,
            unhandled_case=f"marketplace={self}",
        )

    def to_amazon_marketplace(self) -> "AmazonMarketplace":
        if self.value in list(AmazonMarketplace):
            return AmazonMarketplace[self.value]
        raise keble_exceptions.ServerSideInvalidParams(
            invalid_params=f"marketplace: {self.value}",
            expected="marketplace in list(AmazonMarketplace)",
            but_got=f"marketplace={self.value}",
            admin_note=self.value,
            alert_admin=True,
        )
        # raise ValueError(
        #     f"[Typing] Failed to convert {self.value} marketplace to amazon marketplace"
        # )

    def to_amazon_marketplace_eu(self) -> "AmazonMarketplaceEU":
        return AmazonMarketplaceEU.from_marketplace(self)

    @classmethod
    def from_amazon_marketplace(
        cls, marketplace: Union[None, "AmazonMarketplace", "AmazonMarketplaceEU"]
    ) -> "Marketplace":
        if marketplace is None or marketplace.value not in list(Marketplace):
            raise keble_exceptions.ClientSideInvalidParams(
                invalid_params="marketplace",
                admin_note=marketplace,
                alert_admin=True,
                expected="marketplace is None or marketplace.value not in list(Marketplace)",
                but_got=f"marketplace={marketplace}",
            )
            # raise ValueError(
            #     f"[Typing] Failed to convert amazon marketplace {marketplace} to marketplace enum"
            # )
        return Marketplace[marketplace.value]


AnyMarketplace = (
    Marketplace | UnspecifiedMarketplace
)  # include marketplace, unspecified marketplace (amazon and eu marketplace are subsets of marketplace)

class AmazonProductUrlParsedResult(BaseModel):
    model_config = PydanticModelConfig.default()
    asin: str
    marketplace: "AmazonMarketplace"

class AmazonMarketplace(str, Enum):
    # NA
    US = "US"
    CA = "CA"
    MX = "MX"

    # ASIA
    JP = "JP"

    # EU + UK
    UK = "UK"
    DE = "DE"
    ES = "ES"
    FR = "FR"
    IT = "IT"
    NL = "NL"
    SE = "SE"
    PL = "PL"
    BE = "BE"

    # Unsupported marketplace:

    # IN = "IN"
    # CN = "CN"
    # SG = "SG"

    # AE = "AE"
    # BR = "BR"
    # AU = "AU"
    # TR = "TR"
    # SA = "SA"

    @property
    def currency(self) -> "Currency":
        if self.value == Marketplace.US:
            return Currency.USD
        if self.value == Marketplace.JP:
            return Currency.JPY
        if self.value == Marketplace.UK:
            return Currency.GBP
        if self.value == Marketplace.SE:
            return Currency.SEK
        if self.value == Marketplace.PL:
            return Currency.PLN
        if self.value in [
            Marketplace.DE,
            Marketplace.ES,
            Marketplace.FR,
            Marketplace.IT,
            Marketplace.BE,
            Marketplace.NL,
        ]:
            return Currency.EUR
        if self.value == Marketplace.CA:
            return Currency.CAD
        if self.value == Marketplace.MX:
            return Currency.MXN
        raise keble_exceptions.UnhandledScenarioOrCase(
            admin_note={"self.value": self.value},
            alert_admin=True,
            unhandled_case=f"marketplace={self}",
        )

    def to_marketplace(self) -> Marketplace:
        return Marketplace[self.value]

    def to_amazon_marketplace_eu(self) -> "AmazonMarketplaceEU":
        return AmazonMarketplaceEU.from_marketplace(self)

    # @classmethod
    # def from_marketplace(
    #     cls, marketplace: Union[None, Marketplace, "AmazonMarketplaceEU"]
    # ) -> "AmazonMarketplace":
    #     if marketplace is None or marketplace.value not in list(AmazonMarketplace):
    #         raise keble_exceptions.ServerSideInvalidParams(
    #             invalid_params="marketplace",
    #             admin_note=marketplace,
    #             alert_admin=True,
    #             but_got=f"marketplace={marketplace}",
    #             expected="not(marketplace is None or marketplace.value not in list(AmazonMarketplace))",
    #         )
    #         # raise ValueError(
    #         #     f"[Typing] Failed to convert marketplace {marketplace} to amazon marketplave enum"
    #         # )
    #     return AmazonMarketplace[marketplace.value]

    @classmethod
    def from_marketplace(
        cls,
        marketplace: Union[
            str, Marketplace, "AmazonMarketplaceEU", "AmazonMarketplace", None
        ],
    ) -> "AmazonMarketplace":
        if isinstance(marketplace, AmazonMarketplace):
            return marketplace

        if isinstance(marketplace, (Marketplace, AmazonMarketplaceEU)):
            if marketplace.value not in list(AmazonMarketplace):
                raise keble_exceptions.ServerSideInvalidParams(
                    invalid_params="marketplace",
                    admin_note={"marketplace": str(marketplace)},
                    alert_admin=True,
                    but_got=f"{marketplace}",
                    expected="Valid Marketplace or AmazonMarketplaceEU value in AmazonMarketplace",
                )
            return cls[marketplace.value]

        if isinstance(marketplace, str):
            if marketplace.upper() not in [am.value for am in list(AmazonMarketplace)]:
                raise keble_exceptions.ServerSideInvalidParams(
                    invalid_params="marketplace",
                    admin_note={"marketplace": marketplace},
                    alert_admin=True,
                    but_got=marketplace,
                    expected="Valid AmazonMarketplace string",
                )
            return cls(marketplace.upper())

        raise keble_exceptions.ServerSideInvalidParams(
            invalid_params="marketplace",
            admin_note={
                "marketplace": str(marketplace),
                "type": str(type(marketplace)),
            },
            alert_admin=True,
            but_got=str(marketplace),
            expected="AmazonMarketplace | Marketplace | AmazonMarketplaceEU | str",
        )

    @property
    def is_eu(self):
        eu_values = [e.value for e in list(AmazonMarketplaceEU)]
        return self.value in eu_values

    @property
    def domain(self) -> str:
        domain_mapping = {
            AmazonMarketplace.US.value: "amazon.com",
            AmazonMarketplace.CA.value: "amazon.ca",
            AmazonMarketplace.MX.value: "amazon.com.mx",
            AmazonMarketplace.UK.value: "amazon.co.uk",
            AmazonMarketplace.DE.value: "amazon.de",
            AmazonMarketplace.FR.value: "amazon.fr",
            AmazonMarketplace.IT.value: "amazon.it",
            AmazonMarketplace.ES.value: "amazon.es",
            AmazonMarketplace.NL.value: "amazon.nl",
            AmazonMarketplace.SE.value: "amazon.se",
            AmazonMarketplace.PL.value: "amazon.pl",
            AmazonMarketplace.BE.value: "amazon.com.be",
            AmazonMarketplace.JP.value: "amazon.co.jp",
            "IN": "amazon.in",
            "AU": "amazon.com.au",
            "AE": "amazon.ae",
            "SG": "amazon.sg",
            "TR": "amazon.com.tr",
            "SA": "amazon.sa",
            "BR": "amazon.com.br",
        }
        return domain_mapping[self.value]
    
    @classmethod
    def parse_amazon_product_url(cls, url: str | AnyHttpUrl) -> AmazonProductUrlParsedResult | None:
        raw_url = str(url).strip()
        if raw_url == "":
            return None

        parsed_url = urlsplit(raw_url)
        host = parsed_url.netloc.lower()
        if host == "":
            return None

        host = host.split("@")[-1]
        host = host.split(":")[0]
        while host.startswith("www."):
            host = host[4:]

        domain_to_marketplace: dict[str, AmazonMarketplace] = {
            marketplace.domain: marketplace for marketplace in cls
        }

        marketplace: AmazonMarketplace | None = None
        for domain, candidate_marketplace in sorted(
            domain_to_marketplace.items(),
            key=lambda pair: len(pair[0]),
            reverse=True,
        ):
            if host == domain or host.endswith(f".{domain}"):
                marketplace = candidate_marketplace
                break

        if marketplace is None:
            return None

        path = parsed_url.path
        if path == "":
            return None

        asin: str | None = None
        product_path_patterns = (
            r"/dp/([A-Z0-9]{10})(?:[/?]|$)",
            r"/gp/product/([A-Z0-9]{10})(?:[/?]|$)",
            r"/gp/aw/d/([A-Z0-9]{10})(?:[/?]|$)",
            r"/gp/aw/dp/([A-Z0-9]{10})(?:[/?]|$)",
            r"/gp/dp/([A-Z0-9]{10})(?:[/?]|$)",
            r"/exec/obidos/asin/([A-Z0-9]{10})(?:[/?]|$)",
            r"/o/asin/([A-Z0-9]{10})(?:[/?]|$)",
            r"/product/([A-Z0-9]{10})(?:[/?]|$)",
            r"/product-reviews/([A-Z0-9]{10})(?:[/?]|$)",
            r"/gp/offer-listing/([A-Z0-9]{10})(?:[/?]|$)",
            r"/gp/customer-reviews/([A-Z0-9]{10})(?:[/?]|$)",
        )
        for pattern in product_path_patterns:
            match = re.search(pattern, path, flags=re.IGNORECASE)
            if match is not None:
                asin = match.group(1).upper()
                break

        if asin is None:
            for query_key, query_value in parse_qsl(parsed_url.query):
                if query_key.lower() != "asin":
                    continue
                if re.fullmatch(r"[A-Z0-9]{10}", query_value, flags=re.IGNORECASE):
                    asin = query_value.upper()
                    break

        if asin is None:
            return None

        return AmazonProductUrlParsedResult(asin=asin, marketplace=marketplace)


class AmazonMarketplaceEU(str, Enum):
    DE = AmazonMarketplace.DE.value
    IT = AmazonMarketplace.IT.value
    FR = AmazonMarketplace.FR.value
    ES = AmazonMarketplace.ES.value
    UK = AmazonMarketplace.UK.value
    NL = AmazonMarketplace.NL.value
    SE = AmazonMarketplace.SE.value
    PL = AmazonMarketplace.PL.value
    BE = AmazonMarketplace.BE.value

    @property
    def currency(self) -> Optional["Currency"]:
        if self.value == AmazonMarketplaceEU.UK:
            return Currency.GBP
        if self.value == AmazonMarketplaceEU.SE:
            return Currency.SEK
        if self.value == AmazonMarketplaceEU.PL:
            return Currency.PLN
        if self.value in [
            AmazonMarketplaceEU.DE,
            AmazonMarketplaceEU.ES,
            AmazonMarketplaceEU.FR,
            AmazonMarketplaceEU.IT,
            AmazonMarketplaceEU.BE,
        ]:
            return Currency.EUR
        return None

    def to_marketplace(self) -> Marketplace:
        return Marketplace[self.value]

    def to_amazon_marketplace(self) -> AmazonMarketplace:
        return AmazonMarketplace[self.value]

    @classmethod
    def from_marketplace(
        cls, marketplace: AmazonMarketplace | Marketplace | None
    ) -> "AmazonMarketplaceEU":
        if marketplace is None or marketplace.value not in list(AmazonMarketplaceEU):
            raise keble_exceptions.ServerSideInvalidParams(
                invalid_params="marketplace",
                expected="not(marketplace is None or marketplace.value not in list(AmazonMarketplaceEU))",
                but_got=f"marketplace={marketplace}",
                alert_admin=True,
            )
            # raise ValueError(
            #     f"[Typing] Failed to convert marketplace {marketplace} to amazon marketplave EU enum"
            # )
        return AmazonMarketplaceEU[marketplace.value]

    @classmethod
    def is_eu_marketplace(
        cls,
        marketplace: Union[
            str, AmazonMarketplace, Marketplace, "AmazonMarketplaceEU", None
        ],
    ) -> bool:
        return marketplace in list(AmazonMarketplaceEU)

    @property
    def domain(self) -> str:
        return self.to_amazon_marketplace().domain


class Currency(str, Enum):
    """
    Currencies
    full list: https://en.wikipedia.org/wiki/ISO_4217#Active_codes
    """

    AED = "AED"
    AFN = "AFN"
    ALL = "ALL"
    AMD = "AMD"
    ANG = "ANG"
    AOA = "AOA"
    ARS = "ARS"
    AUD = "AUD"
    AWG = "AWG"
    AZN = "AZN"
    BAM = "BAM"
    BBD = "BBD"
    BDT = "BDT"
    BGN = "BGN"
    BHD = "BHD"
    BIF = "BIF"
    BMD = "BMD"
    BND = "BND"
    BOB = "BOB"
    BRL = "BRL"
    BSD = "BSD"
    BTN = "BTN"
    BWP = "BWP"
    BYN = "BYN"
    BYR = "BYR"
    BZD = "BZD"
    CAD = "CAD"
    CDF = "CDF"
    CHF = "CHF"
    CLP = "CLP"
    CNY = "CNY"
    COP = "COP"
    CRC = "CRC"
    CVE = "CVE"
    CZK = "CZK"
    DJF = "DJF"
    DKK = "DKK"
    DOP = "DOP"
    DZD = "DZD"
    EGP = "EGP"
    ERN = "ERN"
    ETB = "ETB"
    EUR = "EUR"
    FJD = "FJD"
    FKP = "FKP"
    GBP = "GBP"
    GEL = "GEL"
    GHS = "GHS"
    GIP = "GIP"
    GMD = "GMD"
    GNF = "GNF"
    GTQ = "GTQ"
    GYD = "GYD"
    HKD = "HKD"
    HNL = "HNL"
    HRK = "HRK"
    HTG = "HTG"
    HUF = "HUF"
    IDR = "IDR"
    ILS = "ILS"
    INR = "INR"
    IQD = "IQD"
    IRR = "IRR"
    ISK = "ISK"
    JEP = "JEP"
    JMD = "JMD"
    JOD = "JOD"
    JPY = "JPY"
    KES = "KES"
    KGS = "KGS"
    KHR = "KHR"
    KID = "KID"
    KMF = "KMF"
    KRW = "KRW"
    KWD = "KWD"
    KYD = "KYD"
    KZT = "KZT"
    LAK = "LAK"
    LBP = "LBP"
    LKR = "LKR"
    LRD = "LRD"
    LSL = "LSL"
    LTL = "LTL"
    LVL = "LVL"
    LYD = "LYD"
    MAD = "MAD"
    MDL = "MDL"
    MGA = "MGA"
    MKD = "MKD"
    MMK = "MMK"
    MNT = "MNT"
    MOP = "MOP"
    MRU = "MRU"
    MUR = "MUR"
    MVR = "MVR"
    MWK = "MWK"
    MXN = "MXN"
    MYR = "MYR"
    MZN = "MZN"
    NAD = "NAD"
    NGN = "NGN"
    NIO = "NIO"
    NOK = "NOK"
    NPR = "NPR"
    NZD = "NZD"
    OMR = "OMR"
    PAB = "PAB"
    PEN = "PEN"
    PGK = "PGK"
    PHP = "PHP"
    PKR = "PKR"
    PLN = "PLN"
    PYG = "PYG"
    QAR = "QAR"
    RON = "RON"
    RSD = "RSD"
    RUB = "RUB"
    RWF = "RWF"
    SAR = "SAR"
    SBD = "SBD"
    SCR = "SCR"
    SDG = "SDG"
    SEK = "SEK"
    SGD = "SGD"
    SHP = "SHP"
    SLL = "SLL"
    SOS = "SOS"
    SRD = "SRD"
    SSP = "SSP"
    STD = "STD"
    SYP = "SYP"
    SZL = "SZL"
    THB = "THB"
    TJS = "TJS"
    TMT = "TMT"
    TND = "TND"
    TOP = "TOP"
    TRY = "TRY"
    TTD = "TTD"
    TWD = "TWD"
    TZS = "TZS"
    UAH = "UAH"
    UGX = "UGX"
    USD = "USD"
    UYU = "UYU"
    UZS = "UZS"
    VEF = "VEF"
    VES = "VES"
    VND = "VND"
    VUV = "VUV"
    WST = "WST"
    XAF = "XAF"
    XCD = "XCD"
    XOF = "XOF"
    XPF = "XPF"
    XXX = "XXX"  # 未知货币类型
    YER = "YER"
    ZAR = "ZAR"
    ZMW = "ZMW"

    @property
    def symbol(self):
        _map = {
            "USD": "$",  # US Dollar
            "CAD": "C$",  # Canadian Dollar
            "JPY": "¥",  # Japanese Yen
            "GBP": "£",  # British Pound
            "EUR": "€",  # Euro
            "SEK": "kr",  # Swedish Krona
            "PLN": "zł",  # Polish Zloty
            "MXN": "$",  # mexico peso
        }
        if self.value in _map:
            return _map[self.value]
        return self.value  # by default keeping the same

    @property
    def has_decimal(self) -> bool:
        """Returns True if the currency supports decimal places, otherwise False.
        Function generated by CHATGPT:
        prompts:
            which currencies like JPY, has no floating in decimal

            give me a function
            def has_decial(self) -> bool: ...
        """
        NON_DECIMAL_CURRENCIES = {
            "JPY",
            "KRW",
            "VND",
            "XAF",
            "XOF",
            "CLP",
            "PYG",
            "RWF",
            "BIF",
            "DJF",
            "GNF",
            "KMF",
            "UGX",
            "MGA",
            "VUV",
        }
        return self.value not in NON_DECIMAL_CURRENCIES


class ExchangeRateInUsd(BaseModel):
    # an object representing any currency that can exchange to USD
    # with a list of this object, we can convert different
    # currencies into 1 currency

    model_config = DEFAULT_MODEL_CONFIG
    currency: Currency
    exchange_rate: StdMoneyAmount  # 1 currency can have how much USD. For example, if currency is CNY, this value can be approx 700 (for 7 cny)

    # def __init__(self, *, exchange_rate: StdMoneyAmount, currency: Currency):
    #     super().__init__(exchange_rate=exchange_rate, currency=currency)


class Money(BaseModel):
    amount: StdMoneyAmount
    currency: Currency

    def __init__(
        self,
        *,
        amount: Optional[StdMoneyAmount] = None,
        currency: Currency,
        float_money: Optional[float] = None,
    ):
        if amount is not None:
            super().__init__(amount=amount, currency=currency)
        elif float_money is not None:
            super().__init__(
                amount=StdMoneyAmount.from_true_value(float_money=float_money),
                currency=currency,
            )
        else:
            raise keble_exceptions.ServerSideMissingParams(
                alert_admin=True,
                missing_params="amount or float_money",
                admin_note=f"amount={amount}, float_money={float_money}",
            )
            # raise ValueError(
            #     "[Typing] You need to provide either amount or float_money to create an instance of Money"
            # )

    def exchange_to(
        self, exchange_rates: List[ExchangeRateInUsd], currency: Currency
    ) -> "Money":
        """Ensure money are in the target currency"""
        if self.currency == currency:
            return Money(**self.model_dump())  # make a clone
        usd_amount = None
        for er in exchange_rates:
            if er.currency == self.currency:
                usd_amount = self.amount / er.exchange_rate
        # assert usd_amount is not None, (
        #     f"[Typing] Failed to exchange money {self} to currency {currency}, due to {self.currency} to USD exchange rate not found."
        # )
        keble_exceptions.raise_if_not(
            usd_amount is not None,
            keble_exceptions.ServerSideMissingParams(
                missing_params=f"USD exchange rate for {self.currency}",
                admin_note={"self.currency": self.currency, "currency": currency},
                alert_admin=True,
            ),
        )

        for er in exchange_rates:
            if er.currency == currency:
                return Money(amount=er.exchange_rate * usd_amount, currency=currency)

        raise keble_exceptions.ServerSideMissingParams(
            missing_params=f"USD exchange rate for {currency}",
            admin_note={"self.currency": self.currency, "currency": currency},
            alert_admin=True,
        )
        # raise AssertionError(
        #     f"[Typing] Failed to exchange money {self} to currency {currency}, due to {currency} to USD exchange rate not found."
        # )

    def _check_currency_match(self, other):
        """Helper method to check if the currencies match"""
        if self.currency != other.currency:
            raise keble_exceptions.ServerSideInvalidParams(
                invalid_params="two currency values",
                admin_note=f"self.currency={self.currency}, other.currency={other.currency}",
                alert_admin=True,
                but_got=f"self.currency={self.currency}, other.currency={other.currency}",
                expected="self.currency == other.currency",
            )
            # raise ValueError(
            #     f"[Typing] Failed to do operation due to currency unmatched. Left currency {self.currency} and right currency {other.currency}"
            # )

    def __add__(self, other):
        if isinstance(other, Money):
            self._check_currency_match(other)
            return Money(amount=self.amount + other.amount, currency=self.currency)
        raise NotImplementedError

    def __sub__(self, other):
        if isinstance(other, Money):
            self._check_currency_match(other)
            return Money(amount=self.amount - other.amount, currency=self.currency)
        raise NotImplementedError

    def __mul__(self, other):
        if isinstance(other, int):
            return Money(amount=self.amount * other, currency=self.currency)
        raise NotImplementedError

    def __truediv__(self, other):
        if isinstance(other, int):
            return Money(amount=self.amount / other, currency=self.currency)
        raise NotImplementedError

    def __mod__(self, other):
        if isinstance(other, (int, float)):
            return self.true_value % other
        raise NotImplementedError

    def __floordiv__(self, other):
        if isinstance(other, int):
            return Money(amount=self.amount // other, currency=self.currency)
        raise NotImplementedError

    def __pow__(self, other):
        if isinstance(other, int):
            return Money(amount=self.amount**other, currency=self.currency)
        raise NotImplementedError

    def __neg__(self):
        return Money(amount=-self.amount, currency=self.currency)

    def __abs__(self):
        return Money(amount=abs(self.amount), currency=self.currency)

    def __lt__(self, other):
        if isinstance(other, Money):
            self._check_currency_match(other)
            return self.amount < other.amount
        elif isinstance(other, int):
            return self.amount < other
        return NotImplemented

    def __le__(self, other):
        if isinstance(other, Money):
            self._check_currency_match(other)
            return self.amount <= other.amount
        elif isinstance(other, int):
            return self.amount <= other
        return NotImplemented

    def __gt__(self, other):
        if isinstance(other, Money):
            self._check_currency_match(other)
            return self.amount > other.amount
        elif isinstance(other, int):
            return self.amount > other
        return NotImplemented

    def __ge__(self, other):
        if isinstance(other, Money):
            self._check_currency_match(other)
            return self.amount >= other.amount
        elif isinstance(other, int):
            return self.amount >= other
        return NotImplemented

    def __eq__(self, other):
        if isinstance(other, Money):
            self._check_currency_match(other)
            return self.amount == other.amount
        elif isinstance(other, int):
            return self.amount == other
        return NotImplemented

    def __ne__(self, other):
        if isinstance(other, Money):
            self._check_currency_match(other)
            return self.amount != other.amount
        elif isinstance(other, int):
            return self.amount != other
        return NotImplemented

    def __str__(self):
        return f"{self.true_value} {self.currency.value}"

    def __repr__(self):
        return f"Money(amount={self.amount}, currency={self.currency})"

    @property
    def true_value(self) -> float | int:
        # 100% == 10000 -> 1, 10% == 1000 -> 0.1
        if self.currency.has_decimal:
            return round(self.amount.true_value, 2)
        return int(round(self.amount.true_value, 0))

    @classmethod
    def from_true_value(cls, *, float_money: float, currency: Currency):
        return cls(
            amount=StdMoneyAmount.from_true_value(float_money=float_money),
            currency=currency,
        )

    @property
    def string(self) -> str:
        return f"{self.currency.symbol if self.currency.symbol is not None else self.currency.value}{self.true_value}"


class MoneyRange(BaseModel):
    model_config = DEFAULT_MODEL_CONFIG

    lower: Optional[Money]  # gte
    upper: Optional[Money]  # lte
    currency: Currency

    @model_validator(mode="after")
    def validate_self(self):
        if self.lower is not None and self.lower.currency != self.currency:
            raise keble_exceptions.ServerSideInvalidParams(
                invalid_params="lower.currency",
                admin_note={
                    "lower.currency": self.lower.currency,
                    "currency": self.currency,
                },
                alert_admin=True,
                but_got=f"lower.currency={self.lower.currency}",
                expected=f"lower.currency == {self.currency}",
            )

        if self.upper is not None and self.upper.currency != self.currency:
            raise keble_exceptions.ServerSideInvalidParams(
                invalid_params="upper.currency",
                admin_note={
                    "upper.currency": self.upper.currency,
                    "currency": self.currency,
                },
                alert_admin=True,
                but_got=f"upper.currency={self.upper.currency}",
                expected=f"upper.currency == {self.currency}",
            )

        if self.lower is not None and self.upper is not None:
            if self.lower.currency != self.upper.currency:
                raise keble_exceptions.ServerSideInvalidParams(
                    invalid_params="lower.currency and upper.currency",
                    admin_note={
                        "lower.currency": self.lower.currency,
                        "upper.currency": self.upper.currency,
                    },
                    alert_admin=True,
                    but_got=f"lower.currency={self.lower.currency} upper.currency={self.upper.currency}",
                    expected="lower.currency == upper.currency",
                )

            if self.lower > self.upper:
                raise keble_exceptions.ServerSideInvalidParams(
                    invalid_params="lower and upper",
                    admin_note={"lower": str(self.lower), "upper": str(self.upper)},
                    alert_admin=True,
                    but_got="lower > upper",
                    expected="lower <= upper",
                )
        return self

    @classmethod
    def build(
        cls,
        *,
        lower: Optional[Money],
        upper: Optional[Money],
        currency: Currency,
    ):
        return MoneyRange(
            lower=lower,
            upper=upper,
            currency=currency,
        )

    @property
    def string(self):
        prefix = (
            self.currency.symbol
            if self.currency.symbol is not None
            else self.currency.value
        )
        if (
            self.lower is not None
            and self.upper is not None
            and self.lower != self.upper
        ):
            return f"{self.lower.string} ~ {self.upper.string}"
        elif self.lower is not None or self.upper is not None:
            if self.lower is not None:
                return self.lower.string
            assert self.upper is not None
            return self.upper.string
        return f"{prefix}-"

    def __str__(self):
        if self.lower is None and self.upper is None:
            return "-"
        elif self.lower is not None and self.upper is not None:
            return f"{str(self.lower)} ~ {str(self.upper)}"
        elif self.lower is not None:
            return f">= {str(self.lower)}"
        assert self.upper is not None
        return f"<= {str(self.upper)}"


class PercentageRange(BaseModel):
    model_config = DEFAULT_MODEL_CONFIG

    lower: Optional[StdPercentage]  # gte
    upper: Optional[StdPercentage]  # lte

    @model_validator(mode="after")
    def validate_self(self):
        if (
            self.lower is not None
            and self.upper is not None
            and self.lower > self.upper
        ):
            raise keble_exceptions.ServerSideInvalidParams(
                invalid_params="lower and upper",
                admin_note={"lower": str(self.lower), "upper": str(self.upper)},
                alert_admin=True,
                but_got="lower > upper",
                expected="lower <= upper",
            )
        return self

    @classmethod
    def build(cls, *, lower: Optional[StdPercentage], upper: Optional[StdPercentage]):
        return PercentageRange(
            lower=lower,
            upper=upper,
        )

    @property
    def string(self):
        if (
            self.lower is not None
            and self.upper is not None
            and self.lower != self.upper
        ):
            return f"{self.lower.string} ~ {self.upper.string}"
        elif self.lower is not None or self.upper is not None:
            if self.lower is not None:
                return self.lower.string
            assert self.upper is not None
            return self.upper.string
        return "-%"

    def __str__(self):
        if self.lower is None and self.upper is None:
            return "-"
        elif self.lower is not None and self.upper is not None:
            return f"{str(self.lower)} ~ {str(self.upper)}"
        elif self.lower is not None:
            return f">= {str(self.lower)}"
        assert self.upper is not None
        return f"<= {str(self.upper)}"

    def __repr__(self):
        return f"PercentageRange(lower={self.lower}, upper={self.upper})"


class TimestampValidator:
    @classmethod
    def validate(cls, v: Any) -> Any:
        if isinstance(v, (int, float)):
            return datetime.fromtimestamp(v, tz=timezone.utc)
        return v

    @classmethod
    def serialize(cls, v: Any, info: Any) -> float:
        if isinstance(v, datetime):
            return v.timestamp()
        else:
            assert isinstance(v, float)
            return v

    @classmethod
    def __get_pydantic_core_schema__(
        cls, source_type: Any, handler: GetCoreSchemaHandler
    ) -> CoreSchema:
        return core_schema.no_info_before_validator_function(
            cls.validate,
            core_schema.datetime_schema(),
            serialization=core_schema.plain_serializer_function_ser_schema(
                cls.serialize, info_arg=True
            ),
        )


Timestamp = Annotated[datetime, TimestampValidator()]


class QdrantSchemaBase(BaseModel):
    model_config = DEFAULT_MODEL_CONFIG
    created_ts: Timestamp = Field(default_factory=lambda: datetime.now(timezone.utc))
    updated_ts: Timestamp = Field(default_factory=lambda: datetime.now(timezone.utc))


class StrapiSchemaBase(BaseModel):
    model_config = DEFAULT_MODEL_CONFIG
    id: int  # auto increment
    # strapi fields
    document_id: str
    created_at: Optional[datetime]
    updated_at: Optional[datetime]
    published_at: Optional[datetime]
    locale: str
    created_by_id: Optional[int]
    updated_by_id: Optional[int]


class SchemaBase(BaseModel):
    model_config = DEFAULT_MODEL_CONFIG
    created: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
    updated: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

    @field_validator("created", "updated", mode="before")
    @classmethod
    def _ensure_utc(cls, v):
        # If no value provided, let the default_factory run
        if v is None:
            return v

        # parse string if needed (pydantic will already do that by default)
        # at this point v should be a datetime
        if isinstance(v, datetime):
            # if tzinfo is missing, assign UTC
            if v.tzinfo is None:
                return v.replace(tzinfo=timezone.utc)
            # otherwise leave it alone
            return v

        return v  # let pydantic handle invalid types later


class MongoObjectBase(SchemaBase):
    model_config = DEFAULT_MODEL_CONFIG
    id: ObjectId = Field(
        ...,
        validation_alias=AliasChoices("_id", "id"),
        pattern=r"^[0-9a-fA-F]{24}$",
    )


class Status(str, Enum):
    ACTIVE = "ACTIVE"
    INACTIVE = "INACTIVE"


class SharingScope(str, Enum):
    PRIVATE = "PRIVATE"
    PUBLIC = "PUBLIC"


class OwnerType(str, Enum):
    USER = "USER"
    ORG = "ORG"
    ROOT = "ROOT"
