"""
Stripe Checkout and Customer Portal helpers for django_stripe_billing.
"""
from __future__ import annotations

import logging

import stripe
from django.contrib.auth.models import User

from .conf import require_stripe_secret_key, stripe_billing_settings

logger = logging.getLogger(__name__)


def _get_stripe():
    """Configure and return the stripe module with the project's secret key."""
    stripe.api_key = require_stripe_secret_key()
    return stripe


def _non_empty(value: str | None, name: str) -> str:
    """Return value if non-empty; otherwise raise ValueError (Poka-Yoke)."""
    if not value or not str(value).strip():
        raise ValueError(f'{name} must be non-empty.')
    return str(value).strip()


def create_checkout_session(
    price_id: str,
    success_url: str,
    cancel_url: str,
    *,
    customer_id: str | None = None,
    customer_email: str | None = None,
    user: User | None = None,
    metadata: dict | None = None,
    subscription_metadata: dict | None = None,
    trial_period_days: int | None = None,
    payment_method_types: list[str] | None = None,
    **extra_params,
) -> stripe.checkout.Session:
    """
    Create a Stripe Checkout Session for a subscription.

    Args:
        price_id: Stripe Price ID (e.g. ``price_1Nxyz...``).
        success_url: URL to redirect to after successful payment.
        cancel_url: URL to redirect to if the user cancels.
        customer_id: Existing Stripe Customer ID.  If provided,
            ``customer_email`` and the user's email are ignored.
        customer_email: Pre-fill the checkout form with this email.
            Only used when ``customer_id`` is not provided.
        user: Django User instance.  Used to pre-fill email if neither
            ``customer_id`` nor ``customer_email`` is given, and to add a
            ``user_id`` key to ``metadata``.
        metadata: Extra key-value pairs to add to the Checkout Session
            ``metadata`` dict.
        subscription_metadata: Extra key-value pairs to add to the
            Subscription ``metadata`` dict.
        trial_period_days: Number of trial days.  Passed directly to the
            Stripe API as ``subscription_data.trial_period_days``.
        payment_method_types: Defaults to ``['card']``.
        **extra_params: Any additional keyword args are forwarded verbatim
            to ``stripe.checkout.Session.create``.

    Returns:
        The created ``stripe.checkout.Session`` object.

    Raises:
        ValueError: if price_id, success_url, or cancel_url is empty.
        stripe.error.StripeError: on any Stripe API error.
    """
    _non_empty(price_id, 'price_id')
    _non_empty(success_url, 'success_url')
    _non_empty(cancel_url, 'cancel_url')

    s = _get_stripe()

    base_meta: dict = {}
    if user is not None:
        base_meta['user_id'] = str(user.pk)
    base_meta.update(metadata or {})

    sub_meta: dict = {}
    if user is not None:
        sub_meta['user_id'] = str(user.pk)
    sub_meta.update(subscription_metadata or {})

    subscription_data: dict = {'metadata': sub_meta}
    if trial_period_days is not None:
        subscription_data['trial_period_days'] = trial_period_days

    params: dict = {
        'payment_method_types': payment_method_types or ['card'],
        'line_items': [{'price': price_id, 'quantity': 1}],
        'mode': 'subscription',
        'success_url': success_url,
        'cancel_url': cancel_url,
        'metadata': base_meta,
        'subscription_data': subscription_data,
        **extra_params,
    }

    if customer_id:
        params['customer'] = customer_id
    elif customer_email:
        params['customer_email'] = customer_email
    elif user is not None:
        params['customer_email'] = user.email

    session = s.checkout.Session.create(**params)
    logger.info(
        '[CHECKOUT] Session created: %s (price=%s, customer=%s)',
        session.id, price_id, customer_id or (user.email if user else customer_email),
    )
    return session


def create_billing_portal_session(
    customer_id: str,
    return_url: str,
    **extra_params,
) -> stripe.billing_portal.Session:
    """
    Create a Stripe Customer Portal Session and return it.

    The portal lets users update their payment method, cancel, or change plan
    without you building any custom UI.

    Args:
        customer_id: Stripe Customer ID (``cus_...``).
        return_url: URL to send the user back to after they leave the portal.
        **extra_params: Forwarded verbatim to
            ``stripe.billing_portal.Session.create``.

    Returns:
        The created ``stripe.billing_portal.Session`` object.

    Raises:
        ValueError: if customer_id or return_url is empty.
        stripe.error.StripeError: on any Stripe API error.
    """
    _non_empty(customer_id, 'customer_id')
    _non_empty(return_url, 'return_url')

    s = _get_stripe()
    session = s.billing_portal.Session.create(
        customer=customer_id,
        return_url=return_url,
        **extra_params,
    )
    logger.info('[PORTAL] Session created for customer %s', customer_id)
    return session


def cancel_subscription(
    customer_id: str,
    *,
    at_period_end: bool = True,
) -> stripe.Subscription | None:
    """
    Cancel a customer's active subscription.

    Args:
        customer_id: Stripe Customer ID.
        at_period_end: If ``True`` (default), cancel at the end of the current
            billing period.  If ``False``, cancel immediately.

    Returns:
        The updated ``stripe.Subscription`` object, or ``None`` if no active
        subscription was found.

    Raises:
        stripe.error.StripeError: on any Stripe API error.
    """
    s = _get_stripe()
    subscriptions = s.Subscription.list(customer=customer_id, status='active', limit=1)
    if not subscriptions.data:
        logger.warning('[BILLING] cancel_subscription: no active subscription for %s', customer_id)
        return None

    sub = subscriptions.data[0]
    if at_period_end:
        updated = s.Subscription.modify(sub.id, cancel_at_period_end=True)
        logger.info('[BILLING] Subscription %s set to cancel at period end', sub.id)
        return updated
    else:
        cancelled = s.Subscription.cancel(sub.id)
        logger.info('[BILLING] Subscription %s cancelled immediately', sub.id)
        return cancelled


def change_subscription_price(
    customer_id: str,
    new_price_id: str,
    *,
    proration_behavior: str = 'always_invoice',
) -> stripe.Subscription | None:
    """
    Upgrade or downgrade a customer's subscription to a new price.

    Args:
        customer_id: Stripe Customer ID.
        new_price_id: The Stripe Price ID to switch to.
        proration_behavior: One of ``'always_invoice'``, ``'create_prorations'``,
            or ``'none'``.  Defaults to ``'always_invoice'`` so upgrades are
            charged immediately.

    Returns:
        The modified ``stripe.Subscription`` object, or ``None`` if no active
        subscription was found.

    Raises:
        stripe.error.StripeError: on any Stripe API error.
    """
    s = _get_stripe()
    subscriptions = s.Subscription.list(customer=customer_id, status='active', limit=1)
    if not subscriptions.data:
        logger.warning(
            '[BILLING] change_subscription_price: no active subscription for %s', customer_id
        )
        return None

    sub = subscriptions.data[0]
    item_id = sub['items']['data'][0].id

    updated = s.Subscription.modify(
        sub.id,
        items=[{'id': item_id, 'price': new_price_id}],
        proration_behavior=proration_behavior,
    )
    logger.info(
        '[BILLING] Subscription %s price changed to %s (customer=%s)',
        sub.id, new_price_id, customer_id,
    )
    return updated
