import contextlib
import typing
import uuid
from collections.abc import AsyncGenerator, AsyncIterator, Sequence

import stripe as stripe_lib
import structlog
from pydantic import UUID4
from pydantic import ValidationError as PydanticValidationError
from sqlalchemy import func, select
from sqlalchemy.orm import contains_eager, joinedload, selectinload

from polar.auth.models import Anonymous, AuthSubject
from polar.checkout.guard import has_product_checkout
from polar.checkout.schemas import (
    CheckoutConfirm,
    CheckoutCreate,
    CheckoutCreatePublic,
    CheckoutPriceCreate,
    CheckoutProductCreate,
    CheckoutUpdate,
    CheckoutUpdatePublic,
)
from polar.config import settings
from polar.custom_field.data import validate_custom_field_data
from polar.customer.repository import CustomerRepository
from polar.customer_session.service import customer_session as customer_session_service
from polar.discount.service import DiscountNotRedeemableError
from polar.discount.service import discount as discount_service
from polar.enums import PaymentProcessor
from polar.event.service import event as event_service
from polar.event.system import (
    CheckoutCreatedMetadata,
    SystemEvent,
    build_checkout_event,
)
from polar.exceptions import (
    BadRequest,
    NotPermitted,
    PaymentNotReady,
    PolarError,
    PolarRequestValidationError,
    ResourceNotFound,
    ValidationError,
)
from polar.integrations.stripe.schemas import ProductType
from polar.integrations.stripe.service import stripe as stripe_service
from polar.integrations.stripe.utils import get_fingerprint
from polar.kit.address import AddressInput
from polar.kit.crypto import generate_token
from polar.kit.operator import attrgetter
from polar.kit.pagination import PaginationParams
from polar.kit.sorting import Sorting
from polar.kit.tax import (
    InvalidTaxID,
    TaxCalculationError,
    TaxCode,
    TaxID,
    calculate_tax,
    to_stripe_tax_id,
    validate_tax_id,
)
from polar.kit.utils import utc_now
from polar.locker import Locker
from polar.logging import Logger
from polar.member import member_service
from polar.models import (
    Account,
    Checkout,
    CheckoutLink,
    Customer,
    Discount,
    LegacyRecurringProductPriceCustom,
    LegacyRecurringProductPriceFixed,
    Organization,
    Payment,
    PaymentMethod,
    Product,
    ProductPrice,
    Subscription,
    User,
)
from polar.models.checkout import CheckoutStatus
from polar.models.checkout_product import CheckoutProduct
from polar.models.discount import DiscountDuration
from polar.models.order import OrderBillingReasonInternal
from polar.models.product_price import ProductPriceAmountType, ProductPriceSource
from polar.models.webhook_endpoint import WebhookEventType
from polar.order.service import order as order_service
from polar.organization.service import organization as organization_service
from polar.postgres import AsyncReadSession, AsyncSession
from polar.product.guard import (
    is_currency_price,
    is_custom_price,
    is_discount_applicable,
    is_fixed_price,
    is_seat_price,
)
from polar.product.repository import ProductPriceRepository, ProductRepository
from polar.product.schemas import ProductPriceCreateList
from polar.product.service import product as product_service
from polar.subscription.repository import SubscriptionRepository
from polar.subscription.service import subscription as subscription_service
from polar.trial_redemption.service import trial_redemption as trial_redemption_service
from polar.webhook.service import webhook as webhook_service
from polar.worker import enqueue_job

from . import ip_geolocation
from .eventstream import CheckoutEvent, publish_checkout_event
from .price import get_default_price
from .repository import CheckoutRepository
from .sorting import CheckoutSortProperty

log: Logger = structlog.get_logger()


class CheckoutError(PolarError): ...


class ExpiredCheckoutError(CheckoutError):
    def __init__(self) -> None:
        message = "This checkout session has expired."
        super().__init__(message, 410)


class AlreadyActiveSubscriptionError(CheckoutError):
    def __init__(self) -> None:
        message = "You already have an active subscription."
        super().__init__(message, 403)


class PaymentError(CheckoutError):
    def __init__(
        self, checkout: Checkout, error_type: str | None, error: str | None
    ) -> None:
        self.checkout = checkout
        self.error_type = error_type
        self.error = error
        message = (
            f"The payment failed{f': {error}' if error else '.'} "
            "Please try again with a different payment method."
        )
        super().__init__(message, 400)


class CheckoutDoesNotExist(CheckoutError):
    def __init__(self, checkout_id: uuid.UUID) -> None:
        self.checkout_id = checkout_id
        message = f"Checkout {checkout_id} does not exist."
        super().__init__(message)


class NotOpenCheckout(CheckoutError):
    def __init__(self, checkout: Checkout) -> None:
        self.checkout = checkout
        self.status = checkout.status
        message = f"Checkout {checkout.id} is not open: {checkout.status}"
        super().__init__(message, 403)


class NotConfirmedCheckout(CheckoutError):
    def __init__(self, checkout: Checkout) -> None:
        self.checkout = checkout
        self.status = checkout.status
        message = f"Checkout {checkout.id} is not confirmed: {checkout.status}"
        super().__init__(message)


class ArchivedPriceCheckout(CheckoutError):
    def __init__(self, checkout: Checkout) -> None:
        self.checkout = checkout
        self.price = checkout.product_price
        message = (
            f"Checkout {checkout.id} has an archived price: {checkout.product_price_id}"
        )
        super().__init__(message)


class NoPaymentMethodOnIntent(CheckoutError):
    def __init__(self, checkout: Checkout, intent_id: str) -> None:
        self.checkout = checkout
        self.intent_id = intent_id
        message = (
            f"Intent {intent_id} for {checkout.id} has no payment method associated."
        )
        super().__init__(message)


class TrialAlreadyRedeemed(CheckoutError):
    def __init__(self, checkout: Checkout) -> None:
        self.checkout = checkout
        message = (
            "You have already used a trial for this product. "
            "Trials can only be used once per customer."
        )
        super().__init__(message, 403)


CHECKOUT_CLIENT_SECRET_PREFIX = "polar_c_"


class CheckoutService:
    async def list(
        self,
        session: AsyncReadSession,
        auth_subject: AuthSubject[User | Organization],
        *,
        organization_id: Sequence[uuid.UUID] | None = None,
        product_id: Sequence[uuid.UUID] | None = None,
        customer_id: Sequence[uuid.UUID] | None = None,
        status: Sequence[CheckoutStatus] | None = None,
        query: str | None = None,
        pagination: PaginationParams,
        sorting: list[Sorting[CheckoutSortProperty]] = [
            (CheckoutSortProperty.created_at, True)
        ],
    ) -> tuple[Sequence[Checkout], int]:
        repository = CheckoutRepository.from_session(session)
        statement = repository.get_readable_statement(auth_subject).options(
            *repository.get_eager_options()
        )

        if organization_id is not None:
            statement = statement.where(Checkout.organization_id.in_(organization_id))

        if product_id is not None:
            statement = statement.where(Checkout.product_id.in_(product_id))

        if customer_id is not None:
            statement = statement.where(Checkout.customer_id.in_(customer_id))

        if status is not None:
            statement = statement.where(Checkout.status.in_(status))

        if query is not None:
            statement = statement.where(Checkout.customer_email.ilike(f"%{query}%"))

        statement = repository.apply_sorting(statement, sorting)

        return await repository.paginate(
            statement, limit=pagination.limit, page=pagination.page
        )

    async def get_by_id(
        self,
        session: AsyncReadSession,
        auth_subject: AuthSubject[User | Organization],
        id: uuid.UUID,
    ) -> Checkout | None:
        repository = CheckoutRepository.from_session(session)
        statement = (
            repository.get_readable_statement(auth_subject)
            .where(Checkout.id == id)
            .options(*repository.get_eager_options())
        )
        checkout = await repository.get_one_or_none(statement)

        if checkout is None:
            return None

        if checkout.organization.is_blocked():
            raise NotPermitted()

        return checkout

    async def create(
        self,
        session: AsyncSession,
        checkout_create: CheckoutCreate,
        auth_subject: AuthSubject[User | Organization],
        ip_geolocation_client: ip_geolocation.IPGeolocationClient | None = None,
    ) -> Checkout:
        ad_hoc_prices: dict[Product, Sequence[ProductPrice]] = {}
        if isinstance(checkout_create, CheckoutPriceCreate):
            products, product, price = await self._get_validated_price(
                session, auth_subject, checkout_create.product_price_id
            )
        elif isinstance(checkout_create, CheckoutProductCreate):
            products, product, price = await self._get_validated_product(
                session, auth_subject, checkout_create.product_id
            )
        else:
            products = await self._get_validated_products(
                session, auth_subject, checkout_create.products
            )
            if checkout_create.prices:
                ad_hoc_prices = await self._get_validated_prices(
                    session, auth_subject, products, checkout_create.prices
                )

            product = products[0]
            try:
                price = get_default_price(ad_hoc_prices[product])
            except KeyError:
                price = get_default_price(product.prices)

        if product.organization.is_blocked():
            raise NotPermitted()

        if checkout_create.amount is not None and is_custom_price(price):
            self._validate_custom_price_amount(price, checkout_create.amount)

        discount: Discount | None = None
        if checkout_create.discount_id is not None:
            discount = await self._get_validated_discount(
                session,
                product.organization,
                product,
                price,
                discount_id=checkout_create.discount_id,
            )

        customer_tax_id: TaxID | None = None
        if checkout_create.customer_tax_id is not None:
            if checkout_create.customer_billing_address is None:
                raise PolarRequestValidationError(
                    [
                        {
                            "type": "missing",
                            "loc": ("body", "customer_billing_address"),
                            "msg": "Country is required to validate tax ID.",
                            "input": None,
                        }
                    ]
                )
            try:
                customer_tax_id = validate_tax_id(
                    checkout_create.customer_tax_id,
                    checkout_create.customer_billing_address.country,
                )
            except InvalidTaxID as e:
                raise PolarRequestValidationError(
                    [
                        {
                            "type": "value_error",
                            "loc": ("body", "customer_tax_id"),
                            "msg": "Invalid tax ID.",
                            "input": checkout_create.customer_tax_id,
                        }
                    ]
                ) from e

        # Validate seats for seat-based pricing
        if is_seat_price(price):
            if checkout_create.seats is None or checkout_create.seats < 1:
                raise PolarRequestValidationError(
                    [
                        {
                            "type": "missing",
                            "loc": ("body", "seats"),
                            "msg": "Seats is required for seat-based pricing.",
                            "input": checkout_create.seats,
                        }
                    ]
                )
        elif checkout_create.seats is not None:
            raise PolarRequestValidationError(
                [
                    {
                        "type": "value_error",
                        "loc": ("body", "seats"),
                        "msg": "Seats can only be set for seat-based pricing.",
                        "input": checkout_create.seats,
                    }
                ]
            )

        product = await self._eager_load_product(session, product)

        subscription: Subscription | None = None
        customer: Customer | None = None
        customer_repository = CustomerRepository.from_session(session)
        if checkout_create.subscription_id is not None:
            subscription, customer = await self._get_validated_subscription(
                session, checkout_create.subscription_id, product.organization_id
            )
        elif checkout_create.customer_id is not None:
            customer = await customer_repository.get_by_id_and_organization(
                checkout_create.customer_id, product.organization_id
            )
            if customer is None:
                raise PolarRequestValidationError(
                    [
                        {
                            "type": "value_error",
                            "loc": ("body", "customer_id"),
                            "msg": "Customer does not exist.",
                            "input": checkout_create.customer_id,
                        }
                    ]
                )
        elif checkout_create.external_customer_id is not None:
            # Link customer by external ID, if it exists.
            # It not, that's fine': we'll create a new customer on confirm.
            customer = await customer_repository.get_by_external_id_and_organization(
                checkout_create.external_customer_id, product.organization_id
            )

        amount = checkout_create.amount
        currency = None
        if is_fixed_price(price):
            amount = price.price_amount
            currency = price.price_currency
        elif is_custom_price(price):
            currency = price.price_currency
            if amount is None:
                amount = (
                    price.preset_amount
                    or price.minimum_amount
                    or settings.CUSTOM_PRICE_PRESET_FALLBACK
                )
        elif is_seat_price(price):
            # Calculate amount based on seat count
            seats = checkout_create.seats or 1
            amount = price.calculate_amount(seats)
            currency = price.price_currency
        else:
            amount = 0
            currency = price.price_currency if is_currency_price(price) else "usd"

        custom_field_data = validate_custom_field_data(
            product.attached_custom_fields,
            checkout_create.custom_field_data,
            validate_required=False,
        )

        checkout_products = [
            CheckoutProduct(product=product, order=i, ad_hoc_prices=[])
            for i, product in enumerate(products)
        ]

        require_billing_address = checkout_create.require_billing_address
        customer_billing_address = checkout_create.customer_billing_address
        if customer_billing_address is not None and any(
            (
                customer_billing_address.has_address(),
                customer_billing_address.has_state()
                and customer_billing_address.country not in {"US", "CA"},
            )
        ):
            require_billing_address = True

        checkout = Checkout(
            payment_processor=PaymentProcessor.stripe,
            client_secret=generate_token(prefix=CHECKOUT_CLIENT_SECRET_PREFIX),
            amount=amount,
            currency=currency,
            organization=product.organization,
            checkout_products=checkout_products,
            product=product,
            product_price=price,
            discount=discount,
            customer_billing_address=customer_billing_address,
            require_billing_address=require_billing_address,
            customer_tax_id=customer_tax_id,
            subscription=subscription,
            customer=customer,
            custom_field_data=custom_field_data,
            **checkout_create.model_dump(
                exclude={
                    "product_price_id",
                    "product_id",
                    "products",
                    "prices",
                    "amount",
                    "require_billing_address",
                    "customer_billing_address",
                    "customer_tax_id",
                    "subscription_id",
                    "custom_field_data",
                },
                by_alias=True,
            ),
        )

        if checkout.customer is not None:
            prefill_attributes = (
                "email",
                "name",
                "billing_name",
                "billing_address",
                "tax_id",
            )
            for attribute in prefill_attributes:
                checkout_attribute = f"customer_{attribute}"
                if getattr(checkout, checkout_attribute) is None:
                    setattr(
                        checkout,
                        checkout_attribute,
                        getattr(checkout.customer, attribute),
                    )

            # Auto-select business customer if they have both a billing name (without the fallback to customer.name)
            # and a billing address since that means they've previously checked the is_business_customer checkbox
            # Only auto-select if is_business_customer wasn't explicitly set in the request
            if (
                "is_business_customer" not in checkout_create.model_fields_set
                and checkout.customer.actual_billing_name is not None
                and checkout.customer.billing_address is not None
                and checkout.customer.billing_address.has_address()
            ):
                checkout.is_business_customer = True

        if checkout.payment_processor == PaymentProcessor.stripe:
            checkout.payment_processor_metadata = {
                **(checkout.payment_processor_metadata or {}),
                "publishable_key": settings.STRIPE_PUBLISHABLE_KEY,
            }
            if checkout.customer and checkout.customer.stripe_customer_id is not None:
                stripe_customer_session = await stripe_service.create_customer_session(
                    checkout.customer.stripe_customer_id
                )
                checkout.payment_processor_metadata = {
                    **(checkout.payment_processor_metadata or {}),
                    "customer_session_client_secret": stripe_customer_session.client_secret,
                }

        session.add(checkout)

        checkout = await self._update_checkout_ip_geolocation(
            session, checkout, ip_geolocation_client
        )
        checkout = await self._update_trial_end(checkout)

        try:
            checkout = await self._update_checkout_tax(session, checkout)
        # Swallow incomplete tax calculation error: require it only on confirm
        except TaxCalculationError:
            pass

        await session.flush()

        if ad_hoc_prices:
            for checkout_product in checkout.checkout_products:
                checkout_product.ad_hoc_prices = ad_hoc_prices.get(
                    checkout_product.product, []
                )
                session.add(checkout_product)
            await session.flush()

        await self._after_checkout_created(session, checkout)

        return checkout

    async def client_create(
        self,
        session: AsyncSession,
        checkout_create: CheckoutCreatePublic,
        auth_subject: AuthSubject[User | Anonymous],
        ip_geolocation_client: ip_geolocation.IPGeolocationClient | None = None,
        ip_address: str | None = None,
    ) -> Checkout:
        product_repository = ProductRepository.from_session(session)
        product = await product_repository.get_by_id(
            checkout_create.product_id, options=product_repository.get_eager_options()
        )

        if product is None:
            raise PolarRequestValidationError(
                [
                    {
                        "type": "value_error",
                        "loc": ("body", "product_id"),
                        "msg": "Product does not exist.",
                        "input": checkout_create.product_id,
                    }
                ]
            )

        if product.is_archived:
            raise PolarRequestValidationError(
                [
                    {
                        "type": "value_error",
                        "loc": ("body", "product_id"),
                        "msg": "Product is archived.",
                        "input": checkout_create.product_id,
                    }
                ]
            )

        if product.organization.blocked_at is not None:
            raise PolarRequestValidationError(
                [
                    {
                        "type": "value_error",
                        "loc": ("body", "product_id"),
                        "msg": "Organization is blocked.",
                        "input": checkout_create.product_id,
                    }
                ]
            )

        # Select the static price in priority, as it determines the amount and specific behavior
        price = product.get_static_price() or product.prices[0]

        # Validate seats for seat-based pricing
        if is_seat_price(price):
            if checkout_create.seats is None or checkout_create.seats < 1:
                raise PolarRequestValidationError(
                    [
                        {
                            "type": "missing",
                            "loc": ("body", "seats"),
                            "msg": "Seats is required for seat-based pricing.",
                            "input": checkout_create.seats,
                        }
                    ]
                )
        elif checkout_create.seats is not None:
            raise PolarRequestValidationError(
                [
                    {
                        "type": "value_error",
                        "loc": ("body", "seats"),
                        "msg": "Seats can only be set for seat-based pricing.",
                        "input": checkout_create.seats,
                    }
                ]
            )

        amount = 0
        currency = "usd"
        if is_fixed_price(price):
            amount = price.price_amount
            currency = price.price_currency
        elif is_custom_price(price):
            currency = price.price_currency
            amount = (
                price.preset_amount
                or price.minimum_amount
                or settings.CUSTOM_PRICE_PRESET_FALLBACK
            )
        elif is_seat_price(price):
            # Calculate amount based on seat count
            seats = checkout_create.seats or 1
            amount = price.calculate_amount(seats)
            currency = price.price_currency
        elif is_currency_price(price):
            currency = price.price_currency

        checkout = Checkout(
            payment_processor=PaymentProcessor.stripe,
            client_secret=generate_token(prefix=CHECKOUT_CLIENT_SECRET_PREFIX),
            amount=amount,
            currency=currency,
            seats=checkout_create.seats,
            allow_trial=True,
            organization=product.organization,
            checkout_products=[
                CheckoutProduct(product=product, order=0, ad_hoc_prices=[])
            ],
            product=product,
            product_price=price,
            discount=None,
            customer=None,
            subscription=None,
            customer_email=checkout_create.customer_email,
        )

        if checkout.payment_processor == PaymentProcessor.stripe:
            checkout.payment_processor_metadata = {
                **(checkout.payment_processor_metadata or {}),
                "publishable_key": settings.STRIPE_PUBLISHABLE_KEY,
            }
            if checkout.customer and checkout.customer.stripe_customer_id is not None:
                stripe_customer_session = await stripe_service.create_customer_session(
                    checkout.customer.stripe_customer_id
                )
                checkout.payment_processor_metadata = {
                    **(checkout.payment_processor_metadata or {}),
                    "customer_session_client_secret": stripe_customer_session.client_secret,
                }

        checkout.customer_ip_address = ip_address
        checkout = await self._update_checkout_ip_geolocation(
            session, checkout, ip_geolocation_client
        )
        checkout = await self._update_trial_end(checkout)

        try:
            checkout = await self._update_checkout_tax(session, checkout)
        # Swallow incomplete tax calculation error: require it only on confirm
        except TaxCalculationError:
            pass

        session.add(checkout)

        await session.flush()
        await self._after_checkout_created(session, checkout)

        return checkout

    async def checkout_link_create(
        self,
        session: AsyncSession,
        checkout_link: CheckoutLink,
        embed_origin: str | None = None,
        ip_geolocation_client: ip_geolocation.IPGeolocationClient | None = None,
        ip_address: str | None = None,
        query_prefill: dict[str, str | UUID4 | dict[str, str] | None] | None = None,
        **query_metadata: str | None,
    ) -> Checkout:
        products: list[Product] = []
        for product in checkout_link.products:
            if not product.is_archived:
                products.append(product)

        if len(products) == 0:
            raise PolarRequestValidationError(
                [
                    {
                        "type": "value_error",
                        "loc": ("body", "products"),
                        "msg": "No valid products.",
                        "input": checkout_link.products,
                    }
                ]
            )

        # Pre-select product if product_id is provided and matches a configured product
        product = products[0]
        query_product_id = query_prefill.get("product_id") if query_prefill else None
        product_id = (
            query_product_id if isinstance(query_product_id, uuid.UUID) else None
        )

        if product_id is not None:
            for p in products:
                if p.id == product_id:
                    product = p
                    break
        # Select the static price in priority, as it determines the amount and specific behavior
        price = product.get_static_price() or product.prices[0]

        amount = 0
        currency = "usd"
        seats = None
        if is_fixed_price(price):
            amount = price.price_amount
            currency = price.price_currency
        elif is_custom_price(price):
            currency = price.price_currency
            query_amount_str = query_prefill.get("amount") if query_prefill else None

            # Try to parse and validate query amount
            valid_query_amount = None
            if query_amount_str is not None and isinstance(query_amount_str, str):
                try:
                    query_amount_int = int(float(query_amount_str))
                    self._validate_custom_price_amount(price, query_amount_int)
                    valid_query_amount = query_amount_int
                except (ValueError, TypeError, PolarRequestValidationError):
                    pass

            amount = (
                valid_query_amount
                or price.preset_amount
                or price.minimum_amount
                or settings.CUSTOM_PRICE_PRESET_FALLBACK
            )
        elif is_seat_price(price):
            # Default to 1 seat for checkout links with seat-based pricing
            seats = 1
            amount = price.calculate_amount(seats)
            currency = price.price_currency
        elif is_currency_price(price):
            currency = price.price_currency

        discount: Discount | None = None
        if checkout_link.discount_id is not None:
            try:
                discount = await self._get_validated_discount(
                    session,
                    product.organization,
                    product,
                    price,
                    discount_id=checkout_link.discount_id,
                )
            # If the discount is not valid, just ignore it
            except PolarRequestValidationError:
                pass

        checkout = Checkout(
            client_secret=generate_token(prefix=CHECKOUT_CLIENT_SECRET_PREFIX),
            amount=amount,
            currency=currency,
            seats=seats,
            trial_interval=checkout_link.trial_interval,
            trial_interval_count=checkout_link.trial_interval_count,
            allow_discount_codes=checkout_link.allow_discount_codes,
            allow_trial=True,
            require_billing_address=checkout_link.require_billing_address,
            organization=checkout_link.organization,
            checkout_products=[
                CheckoutProduct(product=p, order=i, ad_hoc_prices=[])
                for i, p in enumerate(products)
            ],
            product=product,
            product_price=price,
            discount=discount,
            embed_origin=embed_origin,
            customer_ip_address=ip_address,
            payment_processor=checkout_link.payment_processor,
            success_url=checkout_link.success_url,
            user_metadata=checkout_link.user_metadata,
        )

        # Handle query parameter prefill
        if query_prefill:
            customer_email = query_prefill.get("customer_email")
            if customer_email is not None and isinstance(customer_email, str):
                checkout.customer_email = customer_email

            customer_name = query_prefill.get("customer_name")
            if customer_name is not None and isinstance(customer_name, str):
                checkout.customer_name = customer_name

            discount_code = query_prefill.get("discount_code")
            if discount_code is not None and isinstance(discount_code, str):
                try:
                    discount = await self._get_validated_discount(
                        session,
                        product.organization,
                        product,
                        price,
                        discount_code=discount_code,
                    )
                    checkout.discount = discount
                except PolarRequestValidationError:
                    pass

            custom_field_data_value = query_prefill.get("custom_field_data")
            if custom_field_data_value is not None and isinstance(
                custom_field_data_value, dict
            ):
                valid_slugs = {
                    cf.custom_field.slug for cf in product.attached_custom_fields
                }

                filtered_data = {
                    slug: value
                    for slug, value in custom_field_data_value.items()
                    if slug in valid_slugs
                }

                if filtered_data:
                    try:
                        validated_data = validate_custom_field_data(
                            product.attached_custom_fields,
                            filtered_data,
                            validate_required=False,
                        )
                        checkout.custom_field_data = {
                            **(checkout.custom_field_data or {}),
                            **validated_data,
                        }
                    except PolarRequestValidationError:
                        # If validation fails, just ignore the custom field data
                        pass

        for key, value in query_metadata.items():
            if value is not None and key not in checkout.user_metadata:
                checkout.user_metadata = {
