"""Unified payment processing for multi-tenant SaaS: subscriptions, one-time payments, webhooks, and access control."""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/16_utils_stripe.ipynb.

# %% auto #0
__all__ = ['logger', 'TIER_HIERARCHY', 'StripeConfig', 'StripeService', 'get_pricing_plans', 'get_pricing_plan',
           'get_active_subscription', 'has_active_subscription', 'require_active_subscription',
           'get_subscription_status', 'check_feature_access', 'require_feature_access', 'create_webhook_route',
           'create_subscription_checkout_route', 'create_one_time_checkout_route', 'create_portal_route',
           'get_stripe_service', 'reset_stripe_service']

# %% ../nbs/16_utils_stripe.ipynb #467bb49b
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, Callable, List
from datetime import datetime, timedelta
from faststripe.core import StripeApi
from .db_host import HostDatabase, Subscription, PricingPlan, gen_id, timestamp
from starlette.responses import Response, RedirectResponse
import os
import json
import logging
import stripe
from dotenv import load_dotenv

load_dotenv()
logger = logging.getLogger(__name__)

# %% ../nbs/16_utils_stripe.ipynb #6d74bb32
@dataclass
class StripeConfig:
    """Configuration for Stripe integration.
    
    Supports both subscription and one-time payment modes with configurable
    trial periods, grace periods, and callback URLs.
    
    Attributes:
        secret_key: Stripe secret API key
        webhook_secret: Webhook signing secret for signature verification
        monthly_price_id: Pre-created Stripe price ID for monthly subscriptions
        yearly_price_id: Pre-created Stripe price ID for yearly subscriptions
        trial_days: Number of days for free trial (default: 30)
        grace_period_days: Days to allow access after payment failure (default: 3)
        base_url: Application base URL for redirect callbacks
        success_path: Path for successful payment redirect
        cancel_path: Path for canceled payment redirect
        allow_promotions: Enable Stripe promotion codes in checkout
        is_development: Enable development mode (skip signature verification)
    
    Example:
        >>> config = StripeConfig.from_env()
        >>> service = StripeService(config)
    """
    secret_key: str
    webhook_secret: str = None
    monthly_price_id: str = None
    yearly_price_id: str = None
    trial_days: int = 30
    grace_period_days: int = 3
    base_url: str = "http://localhost:5001"
    success_path: str = "/payment-success"
    cancel_path: str = "/settings/payment"
    allow_promotions: bool = True
    is_development: bool = False
    
    # Feature tier configuration
    feature_tiers: Dict[str, str] = field(default_factory=lambda: {
        "advanced_analytics": "yearly",
        "exports": "yearly",
        "priority_support": "yearly",
        "api_access": "monthly",
    })
    
    @classmethod
    def from_env(cls) -> 'StripeConfig':
        """Create StripeConfig from environment variables.
        
        Reads STRIPE_* environment variables with sensible defaults.
        
        Returns:
            Configured StripeConfig instance
            
        Raises:
            ValueError: If STRIPE_SECRET_KEY is not set
            
        Example:
            >>> # Set environment variables first
            >>> os.environ['STRIPE_SECRET_KEY'] = 'sk_test_...' 
            >>> config = StripeConfig.from_env()
        """
        secret_key = os.getenv('STRIPE_SECRET_KEY')
        if not secret_key:
            raise ValueError("STRIPE_SECRET_KEY environment variable is required")
        
        # Check if development mode
        is_dev = os.getenv('ENVIRONMENT', 'production').lower() in ('development', 'dev', 'local')
        
        return cls(
            secret_key=secret_key,
            webhook_secret=os.getenv('STRIPE_WEBHOOK_SECRET'),
            monthly_price_id=os.getenv('STRIPE_MONTHLY_PRICE_ID'),
            yearly_price_id=os.getenv('STRIPE_YEARLY_PRICE_ID'),
            trial_days=int(os.getenv('STRIPE_TRIAL_DAYS', '30')),
            grace_period_days=int(os.getenv('STRIPE_GRACE_DAYS', '3')),
            base_url=os.getenv('STRIPE_BASE_URL', 'http://localhost:5001'),
            success_path=os.getenv('STRIPE_SUCCESS_PATH', '/payment-success'),
            cancel_path=os.getenv('STRIPE_CANCEL_PATH', '/settings/payment'),
            allow_promotions=os.getenv('STRIPE_ALLOW_PROMOTIONS', 'true').lower() == 'true',
            is_development=is_dev,
        )
    
    @property
    def success_url(self) -> str:
        """Full success redirect URL with session ID placeholder."""
        return f"{self.base_url}{self.success_path}?session_id={{CHECKOUT_SESSION_ID}}"
    
    @property
    def cancel_url(self) -> str:
        """Full cancel redirect URL."""
        return f"{self.base_url}{self.cancel_path}"

# %% ../nbs/16_utils_stripe.ipynb #7e848f72
class StripeService:
    """Unified Stripe service for payments, subscriptions, and webhooks.
    
    Consolidates checkout creation, subscription management, and webhook 
    handling into a single service with consistent patterns.
    
    Attributes:
        config: StripeConfig instance
        api: FastStripe API wrapper
        host_db: HostDatabase for subscription persistence
    
    Example:
        >>> config = StripeConfig.from_env()
        >>> service = StripeService(config)
        >>> checkout = service.create_subscription_checkout(
        ...     plan_type='monthly',
        ...     tenant_id='tnt_123',
        ...     user_email='user@example.com'
        ... )
        >>> print(checkout.url)  # Redirect user here
    """
    
    def __init__(self, config: StripeConfig, host_db: HostDatabase = None):
        """Initialize Stripe service.
        
        Args:
            config: StripeConfig with API keys and settings
            host_db: Optional HostDatabase, uses from_env() if not provided
        """
        self.config = config
        self.api = StripeApi(config.secret_key)
        self.host_db = host_db or HostDatabase.from_env()
        
        # Also set stripe module API key for signature verification
        stripe.api_key = config.secret_key
    
    # ─────────────────────────────────────────────────────────────────
    # Checkout Sessions
    # ─────────────────────────────────────────────────────────────────
    
    def create_subscription_checkout(
        self,
        plan_type: str,
        tenant_id: str,
        user_email: str,
        metadata: Dict[str, Any] = None,
    ) -> Any:
        """Create a subscription checkout session with trial.
        
        Args:
            plan_type: 'monthly' or 'yearly'
            tenant_id: Tenant ID to associate subscription with
            user_email: Customer email for Stripe
            metadata: Additional metadata to store with subscription
            
        Returns:
            Stripe Checkout Session with 'url' and 'id' attributes
            
        Raises:
            ValueError: If plan_type is invalid or price ID not configured
            
        Example:
            >>> checkout = service.create_subscription_checkout(
            ...     plan_type='monthly',
            ...     tenant_id='tnt_abc123',
            ...     user_email='user@example.com'
            ... )
            >>> return RedirectResponse(checkout.url)
        """
        # Validate plan type and get price ID
        if plan_type == 'monthly':
            price_id = self.config.monthly_price_id
        elif plan_type == 'yearly':
            price_id = self.config.yearly_price_id
        else:
            raise ValueError(f"Invalid plan_type: {plan_type}. Use 'monthly' or 'yearly'")
        
        if not price_id:
            raise ValueError(f"Price ID not configured for {plan_type} plan. Set CONFIG_STRIPE_{plan_type.upper()}_PRICE_ID")
        
        # Build metadata
        sub_metadata = {
            'tenant_id': tenant_id,
            'user_email': user_email,
            'plan_type': plan_type,
            'service': 'fh_saas',
        }
        if metadata:
            sub_metadata.update(metadata)
        
        # Create checkout session
        checkout = self.api.checkout.sessions_post(
            mode='subscription',
            customer_email=user_email,
            line_items=[{'price': price_id, 'quantity': 1}],
            success_url=self.config.success_url,
            cancel_url=self.config.cancel_url,
            subscription_data={
                'trial_period_days': self.config.trial_days,
                'metadata': sub_metadata,
            },
            allow_promotion_codes=self.config.allow_promotions,
            metadata=sub_metadata,  # Also on session for checkout.session.completed
        )
        
        logger.info(f"Created subscription checkout for tenant {tenant_id}: {checkout.id}")
        return checkout
    
    def create_one_time_checkout(
        self,
        amount_cents: int,
        product_name: str,
        tenant_id: str,
        user_email: str,
        currency: str = 'usd',
        metadata: Dict[str, Any] = None,
    ) -> Any:
        """Create a one-time payment checkout session.
        
        Args:
            amount_cents: Payment amount in cents (e.g., 1999 for $19.99)
            product_name: Name displayed on checkout
            tenant_id: Tenant ID for record keeping
            user_email: Customer email for Stripe
            currency: ISO currency code (default: 'usd')
            metadata: Additional metadata to store
            
        Returns:
            Stripe Checkout Session with 'url' and 'id' attributes
            
        Example:
            >>> checkout = service.create_one_time_checkout(
            ...     amount_cents=4999,
            ...     product_name='Premium Report',
            ...     tenant_id='tnt_abc123',
            ...     user_email='user@example.com'
            ... )
            >>> return RedirectResponse(checkout.url)
        """
        # Build metadata
        payment_metadata = {
            'tenant_id': tenant_id,
            'user_email': user_email,
            'payment_type': 'one_time',
            'product_name': product_name,
            'amount_cents': str(amount_cents),
        }
        if metadata:
            payment_metadata.update(metadata)
        
        # Create product and price dynamically for one-time payments
        product = self.api.products.post(
            name=product_name,
            metadata=payment_metadata,
        )
        
        price = self.api.prices.post(
            product=product.id,
            unit_amount=amount_cents,
            currency=currency,
        )
        
        # Create checkout session
        checkout = self.api.checkout.sessions_post(
            mode='payment',
            customer_email=user_email,
            line_items=[{'price': price.id, 'quantity': 1}],
            success_url=self.config.success_url,
            cancel_url=self.config.cancel_url,
            allow_promotion_codes=self.config.allow_promotions,
            metadata=payment_metadata,
        )
        
        logger.info(f"Created one-time checkout for tenant {tenant_id}: {checkout.id}")
        return checkout
    
    def create_customer_portal_session(
        self,
        customer_id: str,
        return_url: str = None,
    ) -> Any:
        """Create a Stripe Customer Portal session for self-service billing.
        
        Args:
            customer_id: Stripe customer ID (cus_*)
            return_url: URL to redirect after portal session (default: base_url)
            
        Returns:
            Portal session with 'url' attribute
            
        Example:
            >>> portal = service.create_customer_portal_session('cus_123')
            >>> return RedirectResponse(portal.url)
        """
        if not return_url:
            return_url = self.config.base_url
        
        portal = self.api.billing_portal.sessions_post(
            customer=customer_id,
            return_url=return_url,
        )
        
        logger.info(f"Created portal session for customer {customer_id}")
        return portal
    
    # ─────────────────────────────────────────────────────────────────
    # Subscription Management
    # ─────────────────────────────────────────────────────────────────
    
    def get_subscription(self, subscription_id: str) -> Any:
        """Retrieve subscription details from Stripe.
        
        Args:
            subscription_id: Stripe subscription ID (sub_*)
            
        Returns:
            Stripe Subscription object
        """
        return self.api.subscriptions.id_get(subscription_id)
    
    def cancel_subscription(
        self,
        subscription_id: str,
        at_period_end: bool = True,
    ) -> Any:
        """Cancel a subscription.
        
        Args:
            subscription_id: Stripe subscription ID
            at_period_end: If True, cancel at end of billing period (default)
                          If False, cancel immediately
                          
        Returns:
            Updated Stripe Subscription object
        """
        if at_period_end:
            # Update to cancel at period end
            subscription = self.api.subscriptions.id_post(
                subscription_id,
                cancel_at_period_end=True,
            )
        else:
            # Immediate cancellation
            subscription = self.api.subscriptions.id_delete(subscription_id)
        
        logger.info(f"Canceled subscription {subscription_id} (at_period_end={at_period_end})")
        return subscription
    
    def change_plan(
        self,
        subscription_id: str,
        new_price_id: str,
        prorate: bool = True,
    ) -> Any:
        """Change subscription plan.
        
        Args:
            subscription_id: Stripe subscription ID
            new_price_id: New Stripe price ID to switch to
            prorate: Calculate prorated charges (default: True)
            
        Returns:
            Updated Stripe Subscription object
        """
        # Get current subscription to find the subscription item
        current = self.get_subscription(subscription_id)
        item_id = current['items']['data'][0]['id']
        
        subscription = self.api.subscriptions.id_post(
            subscription_id,
            items=[{'id': item_id, 'price': new_price_id}],
            proration_behavior='create_prorations' if prorate else 'none',
        )
        
        logger.info(f"Changed plan for subscription {subscription_id} to {new_price_id}")
        return subscription
    
    # ─────────────────────────────────────────────────────────────────
    # Webhook Handling
    # ─────────────────────────────────────────────────────────────────
    
    def verify_signature(
        self,
        payload: bytes,
        sig_header: str,
    ) -> Optional[Dict[str, Any]]:
        """Verify Stripe webhook signature and parse event.
        
        In development mode, skips verification if webhook secret not configured.
        
        Args:
            payload: Raw request body bytes
            sig_header: Stripe-Signature header value
            
        Returns:
            Parsed event dict, or None if verification fails
        """
        # Development mode bypass
        if self.config.is_development and (not self.config.webhook_secret or not sig_header):
            logger.warning("⚠️  DEVELOPMENT MODE: Skipping webhook signature verification")
            try:
                return json.loads(payload.decode('utf-8'))
            except (json.JSONDecodeError, UnicodeDecodeError) as e:
                logger.error(f"Invalid webhook payload in dev mode: {e}")
                return None
        
        # Production: Verify signature
        try:
            event = stripe.Webhook.construct_event(
                payload, sig_header, self.config.webhook_secret
            )
            return event
        except ValueError as e:
            logger.error(f"Invalid webhook payload: {e}")
            return None
        except stripe.error.SignatureVerificationError as e:
            logger.error(f"Invalid webhook signature: {e}")
            return None
    
    def handle_event(self, event: Dict[str, Any]) -> Dict[str, str]:
        """Route webhook event to appropriate handler.
        
        Handles both subscription and one-time payment events.
        
        Args:
            event: Parsed Stripe event dict
            
        Returns:
            Dict with 'status' ('success', 'warning', 'error', 'ignored')
            and 'message' describing the result
        """
        event_type = event.get('type')
        
        handlers = {
            'checkout.session.completed': self._handle_checkout_completed,
            'customer.subscription.created': self._handle_subscription_created,
            'customer.subscription.updated': self._handle_subscription_updated,
            'customer.subscription.deleted': self._handle_subscription_deleted,
            'invoice.payment_succeeded': self._handle_payment_succeeded,
            'invoice.payment_failed': self._handle_payment_failed,
        }
        
        handler = handlers.get(event_type)
        if handler:
            try:
                return handler(event)
            except Exception as e:
                logger.error(f"Error handling {event_type}: {e}")
                import traceback
                traceback.print_exc()
                return {'status': 'error', 'message': str(e)}
        else:
            logger.info(f"Unhandled event type: {event_type}")
            return {'status': 'ignored', 'message': f"Event type {event_type} not handled"}
    
    def _handle_checkout_completed(self, event: Dict[str, Any]) -> Dict[str, str]:
        """Handle checkout.session.completed for both subscription and one-time."""
        session = event['data']['object']
        mode = session.get('mode')
        metadata = session.get('metadata', {})
        
        tenant_id = metadata.get('tenant_id')
        user_email = metadata.get('user_email')
        
        if not tenant_id or not user_email:
            logger.error(f"Missing required metadata in checkout session: {session['id']}")
            return {'status': 'error', 'message': 'Missing tenant_id or user_email'}
        
        if mode == 'subscription':
            return self._record_subscription_from_checkout(session, metadata)
        elif mode == 'payment':
            return self._record_one_time_payment(session, metadata)
        else:
            return {'status': 'warning', 'message': f"Unknown checkout mode: {mode}"}
    
    def _record_subscription_from_checkout(
        self,
        session: Dict[str, Any],
        metadata: Dict[str, Any],
    ) -> Dict[str, str]:
        """Record subscription from checkout session."""
        stripe_sub_id = session.get('subscription')
        stripe_cust_id = session.get('customer')
        
        if not stripe_sub_id:
            return {'status': 'warning', 'message': 'No subscription in session'}
        
        # Fetch full subscription from Stripe
        subscription = self.get_subscription(stripe_sub_id)
        
        self._upsert_subscription(
            tenant_id=metadata['tenant_id'],
            stripe_sub_id=stripe_sub_id,
            stripe_cust_id=stripe_cust_id,
            plan_tier=metadata.get('plan_type', 'monthly'),
            status=subscription.get('status', 'active'),
            current_period_end=self._ts_to_iso(subscription.get('current_period_end')),
            trial_end=self._ts_to_iso(subscription.get('trial_end')),
            payment_type='subscription',
        )
        
        # Update user's Stripe customer ID
        self._update_user_stripe_customer(metadata['user_email'], stripe_cust_id)
        
        logger.info(f"Subscription recorded for tenant {metadata['tenant_id']}: {stripe_sub_id}")
        return {'status': 'success', 'message': 'Subscription created'}
    
    def _record_one_time_payment(
        self,
        session: Dict[str, Any],
        metadata: Dict[str, Any],
    ) -> Dict[str, str]:
        """Record one-time payment from checkout session."""
        amount_cents = int(metadata.get('amount_cents', session.get('amount_total', 0)))
        product_name = metadata.get('product_name', 'One-time purchase')
        stripe_cust_id = session.get('customer')
        
        self._upsert_subscription(
            tenant_id=metadata['tenant_id'],
            stripe_sub_id=session['id'],  # Use session ID for one-time
            stripe_cust_id=stripe_cust_id,
            plan_tier='one_time',
            status='completed',
            current_period_end=None,
            payment_type='one_time',
            amount_cents=amount_cents,
            product_name=product_name,
        )
        
        # Update user's Stripe customer ID if present
        if stripe_cust_id:
            self._update_user_stripe_customer(metadata['user_email'], stripe_cust_id)
        
        logger.info(f"One-time payment recorded for tenant {metadata['tenant_id']}: {session['id']}")
        return {'status': 'success', 'message': 'One-time payment recorded'}
    
    def _handle_subscription_created(self, event: Dict[str, Any]) -> Dict[str, str]:
        """Handle customer.subscription.created."""
        subscription = event['data']['object']
        metadata = subscription.get('metadata', {})
        
        tenant_id = metadata.get('tenant_id')
        if not tenant_id:
            return {'status': 'warning', 'message': 'Subscription created without tenant_id'}
        
        self._upsert_subscription(
            tenant_id=tenant_id,
            stripe_sub_id=subscription['id'],
            stripe_cust_id=subscription['customer'],
            plan_tier=metadata.get('plan_type', 'monthly'),
            status=subscription.get('status', 'active'),
            current_period_end=self._ts_to_iso(subscription.get('current_period_end')),
            trial_end=self._ts_to_iso(subscription.get('trial_end')),
            payment_type='subscription',
        )
        
        logger.info(f"Subscription initialized for tenant {tenant_id}")
        return {'status': 'success', 'message': 'Subscription initialized'}
    
    def _handle_subscription_updated(self, event: Dict[str, Any]) -> Dict[str, str]:
        """Handle customer.subscription.updated."""
        subscription = event['data']['object']
        stripe_sub_id = subscription['id']
        
        # Find existing subscription
        existing = self._find_subscription_by_stripe_id(stripe_sub_id)
        if not existing:
            return {'status': 'warning', 'message': f'Subscription not found: {stripe_sub_id}'}
        
        self._upsert_subscription(
            tenant_id=existing.tenant_id,
            stripe_sub_id=stripe_sub_id,
            stripe_cust_id=subscription['customer'],
            plan_tier=existing.plan_tier,
            status=subscription.get('status', 'active'),
            current_period_end=self._ts_to_iso(subscription.get('current_period_end')),
            trial_end=self._ts_to_iso(subscription.get('trial_end')),
            cancel_at_period_end=subscription.get('cancel_at_period_end', False),
            payment_type='subscription',
        )
        
        logger.info(f"Subscription updated: {stripe_sub_id}")
        return {'status': 'success', 'message': 'Subscription updated'}
    
    def _handle_subscription_deleted(self, event: Dict[str, Any]) -> Dict[str, str]:
        """Handle customer.subscription.deleted."""
        subscription = event['data']['object']
        stripe_sub_id = subscription['id']
        
        existing = self._find_subscription_by_stripe_id(stripe_sub_id)
        if not existing:
            return {'status': 'warning', 'message': f'Subscription not found: {stripe_sub_id}'}
        
        # Update to canceled status
        existing.status = 'canceled'
        existing.cancel_at_period_end = True
        self.host_db.subscriptions.update(existing)
        self.host_db.commit()
        
        logger.info(f"Subscription canceled: {stripe_sub_id}")
        return {'status': 'success', 'message': 'Subscription canceled'}
    
    def _handle_payment_succeeded(self, event: Dict[str, Any]) -> Dict[str, str]:
        """Handle invoice.payment_succeeded."""
        invoice = event['data']['object']
        stripe_sub_id = invoice.get('subscription')
        
        if not stripe_sub_id:
            return {'status': 'success', 'message': 'Payment succeeded (no subscription)'}
        
        existing = self._find_subscription_by_stripe_id(stripe_sub_id)
        if existing:
            existing.status = 'active'
            self.host_db.subscriptions.update(existing)
            self.host_db.commit()
        
        logger.info(f"Payment succeeded for subscription: {stripe_sub_id}")
        return {'status': 'success', 'message': 'Payment recorded'}
    
    def _handle_payment_failed(self, event: Dict[str, Any]) -> Dict[str, str]:
        """Handle invoice.payment_failed."""
        invoice = event['data']['object']
        stripe_sub_id = invoice.get('subscription')
        
        if not stripe_sub_id:
            return {'status': 'success', 'message': 'Payment failed (no subscription)'}
        
        existing = self._find_subscription_by_stripe_id(stripe_sub_id)
        if existing:
            existing.status = 'past_due'
            self.host_db.subscriptions.update(existing)
            self.host_db.commit()
        
        logger.warning(f"Payment failed for subscription: {stripe_sub_id}")
        return {'status': 'success', 'message': 'Payment failure recorded'}
    
    # ─────────────────────────────────────────────────────────────────
    # Database Helpers
    # ─────────────────────────────────────────────────────────────────
    
    def _upsert_subscription(
        self,
        tenant_id: str,
        stripe_sub_id: str,
        stripe_cust_id: str,
        plan_tier: str,
        status: str,
        current_period_end: str = None,
        trial_end: str = None,
        cancel_at_period_end: bool = False,
        payment_type: str = 'subscription',
        amount_cents: int = None,
        product_name: str = None,
    ):
        """Insert or update subscription in database."""
        existing = self._find_subscription_by_stripe_id(stripe_sub_id)
        
        if existing:
            existing.status = status
            existing.plan_tier = plan_tier
            existing.current_period_end = current_period_end
            existing.trial_end = trial_end
            existing.cancel_at_period_end = cancel_at_period_end
            if amount_cents:
                existing.amount_cents = amount_cents
            if product_name:
                existing.product_name = product_name
            self.host_db.subscriptions.update(existing)
        else:
            sub = Subscription(
                id=gen_id(),
                tenant_id=tenant_id,
                stripe_sub_id=stripe_sub_id,
                stripe_cust_id=stripe_cust_id,
                plan_tier=plan_tier,
                status=status,
                current_period_end=current_period_end,
                cancel_at_period_end=cancel_at_period_end,
                payment_type=payment_type,
                amount_cents=amount_cents,
                product_name=product_name,
                trial_end=trial_end,
                created_at=timestamp(),
            )
            self.host_db.subscriptions.insert(sub)
        
        self.host_db.commit()
    
    def _find_subscription_by_stripe_id(self, stripe_sub_id: str) -> Optional[Subscription]:
        """Find subscription by Stripe subscription ID."""
        try:
            results = self.host_db.subscriptions(where=f"stripe_sub_id = '{stripe_sub_id}'")
            return results[0] if results else None
        except Exception as e:
            logger.error(f"Error finding subscription {stripe_sub_id}: {e}")
            return None
    
    def _update_user_stripe_customer(self, email: str, stripe_cust_id: str):
        """Update user's Stripe customer ID in host database."""
        try:
            users = self.host_db.global_users(where=f"email = '{email}'")
            if users:
                user = users[0]
                user.stripe_cust_id = stripe_cust_id
                self.host_db.global_users.update(user)
                self.host_db.commit()
                logger.info(f"Updated Stripe customer ID for {email}")
        except Exception as e:
            logger.error(f"Error updating user Stripe customer ID: {e}")
    
    @staticmethod
    def _ts_to_iso(ts: Optional[int]) -> Optional[str]:
        """Convert Unix timestamp to ISO format string."""
        if ts:
            return datetime.utcfromtimestamp(ts).isoformat()
        return None

# %% ../nbs/16_utils_stripe.ipynb #f5eac032
def get_pricing_plans(
    host_db: HostDatabase = None,
    active_only: bool = True,
) -> List[PricingPlan]:
    """Get all pricing plans from the database.
    
    Retrieves pricing plan configurations stored in the `core_pricing_plans` table.
    Plans define available subscription tiers with their Stripe price IDs,
    amounts, features, and display settings.
    
    Args:
        host_db: Optional HostDatabase instance. Uses from_env() if not provided.
        active_only: If True (default), only returns plans where is_active=True.
                    Set to False to include inactive/archived plans.
    
    Returns:
        List of PricingPlan objects sorted by sort_order, then by tier_level.
        Empty list if no plans are configured.
        
    Example:
        >>> # Get all active plans for pricing page
        >>> plans = get_pricing_plans()
        >>> for plan in plans:
        ...     print(f"{plan.name}: ${plan.amount_monthly/100}/mo")
        Basic Plan: $7.99/mo
        Pro Plan: $19.99/mo
        Enterprise Plan: $49.99/mo
        
        >>> # Get specific plan for checkout
        >>> plans = get_pricing_plans()
        >>> pro_plan = next((p for p in plans if p.id == 'pro'), None)
        >>> if pro_plan:
        ...     price_id = pro_plan.stripe_price_monthly
        
        >>> # Setting up plans (typically in admin or migration)
        >>> from fh_saas.db_host import HostDatabase, PricingPlan, gen_id, timestamp
        >>> host_db = HostDatabase.from_env()
        >>> 
        >>> plans_data = [
        ...     {
        ...         'id': 'basic',
        ...         'name': 'Basic Plan',
        ...         'description': 'Essential features for individuals',
        ...         'stripe_price_monthly': 'price_basic_monthly_xxx',
        ...         'stripe_price_yearly': 'price_basic_yearly_xxx',
        ...         'amount_monthly': 799,   # $7.99
        ...         'amount_yearly': 7990,   # $79.90 (save ~17%)
        ...         'tier_level': 1,
        ...         'features': '["basic_reports", "email_support"]',
        ...         'sort_order': 1,
        ...     },
        ...     {
        ...         'id': 'pro',
        ...         'name': 'Pro Plan', 
        ...         'description': 'Advanced features for teams',
        ...         'stripe_price_monthly': 'price_pro_monthly_xxx',
        ...         'stripe_price_yearly': 'price_pro_yearly_xxx',
        ...         'amount_monthly': 1999,  # $19.99
        ...         'amount_yearly': 19990,  # $199.90
        ...         'tier_level': 2,
        ...         'features': '["basic_reports", "advanced_analytics", "api_access", "priority_support"]',
        ...         'sort_order': 2,
        ...     },
        ...     {
        ...         'id': 'enterprise',
        ...         'name': 'Enterprise Plan',
        ...         'description': 'Full platform access with custom solutions',
        ...         'stripe_price_monthly': 'price_ent_monthly_xxx',
        ...         'stripe_price_yearly': 'price_ent_yearly_xxx',
        ...         'amount_monthly': 4999,  # $49.99
        ...         'amount_yearly': 49990,  # $499.90
        ...         'tier_level': 3,
        ...         'features': '["all_features", "dedicated_support", "custom_integrations", "sla"]',
        ...         'sort_order': 3,
        ...     },
        ... ]
        >>> 
        >>> for plan_data in plans_data:
        ...     plan = PricingPlan(**plan_data, created_at=timestamp())
        ...     host_db.pricing_plans.insert(plan)
        >>> host_db.commit()
    
    Note:
        - Create your Stripe Products and Prices in the Stripe Dashboard first
        - Copy the price IDs (price_xxx) into your database records
        - The `tier_level` field is used by `check_feature_access()` for feature gating
        - The `features` field should be a JSON array of feature keys
    """
    if host_db is None:
        host_db = HostDatabase.from_env()
    
    try:
        if active_only:
            plans = host_db.pricing_plans(where="is_active = TRUE")
        else:
            plans = host_db.pricing_plans()
        
        # Sort by sort_order, then tier_level
        return sorted(plans, key=lambda p: (p.sort_order or 0, p.tier_level or 0))
        
    except Exception as e:
        logger.error(f"Error fetching pricing plans: {e}")
        return []


def get_pricing_plan(
    plan_id: str,
    host_db: HostDatabase = None,
) -> Optional[PricingPlan]:
    """Get a specific pricing plan by ID.
    
    Args:
        plan_id: The plan identifier (e.g., 'basic', 'pro', 'enterprise')
        host_db: Optional HostDatabase instance
        
    Returns:
        PricingPlan object if found, None otherwise
        
    Example:
        >>> plan = get_pricing_plan('pro')
        >>> if plan:
        ...     checkout = service.create_subscription_checkout(
        ...         price_id=plan.stripe_price_monthly,
        ...         ...
        ...     )
    """
    if host_db is None:
        host_db = HostDatabase.from_env()
    
    try:
        plans = host_db.pricing_plans(where=f"id = '{plan_id}'")
        return plans[0] if plans else None
    except Exception as e:
        logger.error(f"Error fetching pricing plan {plan_id}: {e}")
        return None

# %% ../nbs/16_utils_stripe.ipynb #76098deb
def get_active_subscription(
    tenant_id: str,
    host_db: HostDatabase = None,
    grace_period_days: int = 3,
) -> Optional[Subscription]:
    """Get active subscription for a tenant with grace period support.
    
    Returns subscription if:
    - Status is 'active' or 'trialing'
    - Status is 'past_due' but within grace period from current_period_end
    
    Args:
        tenant_id: Tenant ID to check
        host_db: Optional HostDatabase, uses from_env() if not provided
        grace_period_days: Days to allow access after payment failure (default: 3)
        
    Returns:
        Active Subscription object, or None if no valid subscription
        
    Example:
        >>> sub = get_active_subscription('tnt_123')
        >>> if sub:
        ...     print(f"Active until {sub.current_period_end}")
    """
    if host_db is None:
        host_db = HostDatabase.from_env()
    
    try:
        # Get subscription records for tenant (subscription type only)
        subs = host_db.subscriptions(
            where=f"tenant_id = '{tenant_id}' AND payment_type = 'subscription'"
        )
        
        if not subs:
            return None
        
        # Get most recent subscription
        sub = sorted(subs, key=lambda s: s.created_at or '', reverse=True)[0]
        
        # Check active statuses
        if sub.status in ('active', 'trialing'):
            return sub
        
        # Check grace period for past_due
        if sub.status == 'past_due' and sub.current_period_end:
            try:
                period_end = datetime.fromisoformat(sub.current_period_end)
                grace_end = period_end + timedelta(days=grace_period_days)
                if datetime.utcnow() < grace_end:
                    return sub
            except (ValueError, TypeError):
                pass
        
        return None
        
    except Exception as e:
        logger.error(f"Error getting subscription for tenant {tenant_id}: {e}")
        return None


def has_active_subscription(
    tenant_id: str,
    host_db: HostDatabase = None,
    grace_period_days: int = 3,
) -> bool:
    """Check if tenant has an active subscription.
    
    Args:
        tenant_id: Tenant ID to check
        host_db: Optional HostDatabase
        grace_period_days: Days to allow access after payment failure
        
    Returns:
        True if tenant has valid subscription, False otherwise
        
    Example:
        >>> if has_active_subscription('tnt_123'):
        ...     show_premium_features()
    """
    return get_active_subscription(tenant_id, host_db, grace_period_days) is not None


def require_active_subscription(
    tenant_id: str,
    host_db: HostDatabase = None,
    grace_period_days: int = 3,
    redirect_url: str = None,
) -> Optional[Response]:
    """Require active subscription, returning 402 if not found.
    
    Use in route handlers to gate premium features.
    
    Args:
        tenant_id: Tenant ID to check
        host_db: Optional HostDatabase
        grace_period_days: Days to allow access after payment failure
        redirect_url: Optional URL to redirect instead of 402
        
    Returns:
        None if subscription is valid (allow access)
        Response(402) or RedirectResponse if subscription required
        
    Example:
        >>> @app.get('/premium-feature')
        >>> def premium_feature(request):
        ...     error = require_active_subscription(request.state.tenant_id)
        ...     if error:
        ...         return error
        ...     return render_premium_content()
    """
    if has_active_subscription(tenant_id, host_db, grace_period_days):
        return None
    
    if redirect_url:
        return RedirectResponse(redirect_url, status_code=303)
    
    return Response(
        content='Payment required',
        status_code=402,
        media_type='text/plain',
    )


def get_subscription_status(
    tenant_id: str,
    host_db: HostDatabase = None,
    grace_period_days: int = 3,
) -> Dict[str, Any]:
    """Get detailed subscription status for UI display.
    
    Args:
        tenant_id: Tenant ID to check
        host_db: Optional HostDatabase
        grace_period_days: Days for grace period calculation
        
    Returns:
        Dict with subscription details for UI:
        - has_subscription: bool
        - status: str ('active', 'trialing', 'past_due', 'canceled', 'none')
        - plan_tier: str or None
        - is_trial: bool
        - trial_ends_at: str (ISO) or None
        - current_period_end: str (ISO) or None
        - days_remaining: int or None
        - in_grace_period: bool
        - cancel_at_period_end: bool
        
    Example:
        >>> status = get_subscription_status('tnt_123')
        >>> if status['is_trial']:
        ...     show_trial_banner(status['days_remaining'])
    """
    if host_db is None:
        host_db = HostDatabase.from_env()
    
    sub = get_active_subscription(tenant_id, host_db, grace_period_days)
    
    if not sub:
        # Check if there's any subscription (even canceled)
        try:
            subs = host_db.subscriptions(
                where=f"tenant_id = '{tenant_id}' AND payment_type = 'subscription'"
            )
            if subs:
                latest = sorted(subs, key=lambda s: s.created_at or '', reverse=True)[0]
                return {
                    'has_subscription': False,
                    'status': latest.status,
                    'plan_tier': latest.plan_tier,
                    'is_trial': False,
                    'trial_ends_at': None,
                    'current_period_end': latest.current_period_end,
                    'days_remaining': None,
                    'in_grace_period': False,
                    'cancel_at_period_end': latest.cancel_at_period_end,
                }
        except Exception:
            pass
        
        return {
            'has_subscription': False,
            'status': 'none',
            'plan_tier': None,
            'is_trial': False,
            'trial_ends_at': None,
            'current_period_end': None,
            'days_remaining': None,
            'in_grace_period': False,
            'cancel_at_period_end': False,
        }
    
    # Calculate days remaining
    days_remaining = None
    in_grace_period = False
    
    if sub.status == 'trialing' and sub.trial_end:
        try:
            trial_end = datetime.fromisoformat(sub.trial_end)
            days_remaining = (trial_end - datetime.utcnow()).days
        except (ValueError, TypeError):
            pass
    elif sub.current_period_end:
        try:
            period_end = datetime.fromisoformat(sub.current_period_end)
            days_remaining = (period_end - datetime.utcnow()).days
            if sub.status == 'past_due':
                in_grace_period = True
        except (ValueError, TypeError):
            pass
    
    return {
        'has_subscription': True,
        'status': sub.status,
        'plan_tier': sub.plan_tier,
        'is_trial': sub.status == 'trialing',
        'trial_ends_at': sub.trial_end,
        'current_period_end': sub.current_period_end,
        'days_remaining': days_remaining,
        'in_grace_period': in_grace_period,
        'cancel_at_period_end': sub.cancel_at_period_end,
    }

# %% ../nbs/16_utils_stripe.ipynb #ff25dd7d
# Default tier hierarchy (higher number = more features)
TIER_HIERARCHY = {
    'free': 0,
    'monthly': 1,
    'yearly': 2,
    'enterprise': 3,
}


def check_feature_access(
    tenant_id: str,
    feature: str,
    feature_tiers: Dict[str, str] = None,
    host_db: HostDatabase = None,
    grace_period_days: int = 3,
) -> bool:
    """Check if tenant's plan tier allows access to a feature.
    
    Args:
        tenant_id: Tenant ID to check
        feature: Feature name to check access for
        feature_tiers: Optional mapping of feature -> required tier.
                      Defaults to {'advanced_analytics': 'yearly', ...}
        host_db: Optional HostDatabase
        grace_period_days: Days for grace period
        
    Returns:
        True if tenant's plan allows the feature, False otherwise
        
    Example:
        >>> if check_feature_access('tnt_123', 'advanced_analytics'):
        ...     return render_analytics()
        >>> else:
        ...     return render_upgrade_prompt()
    """
    # Default feature tiers
    if feature_tiers is None:
        feature_tiers = {
            'advanced_analytics': 'yearly',
            'exports': 'yearly',
            'priority_support': 'yearly',
            'api_access': 'monthly',
            'basic_features': 'free',
        }
    
    # Get required tier for feature
    required_tier = feature_tiers.get(feature, 'monthly')
    required_level = TIER_HIERARCHY.get(required_tier, 1)
    
    # Get subscription
    sub = get_active_subscription(tenant_id, host_db, grace_period_days)
    
    if not sub:
        # No subscription - only free features
        return required_level == 0
    
    # Check tier hierarchy
    user_level = TIER_HIERARCHY.get(sub.plan_tier, 1)
    return user_level >= required_level


def require_feature_access(
    tenant_id: str,
    feature: str,
    feature_tiers: Dict[str, str] = None,
    host_db: HostDatabase = None,
    redirect_url: str = None,
) -> Optional[Response]:
    """Require feature access, returning 403 if not allowed.
    
    Args:
        tenant_id: Tenant ID to check
        feature: Feature name to check
        feature_tiers: Optional feature -> tier mapping
        host_db: Optional HostDatabase
        redirect_url: Optional URL to redirect instead of 403
        
    Returns:
        None if access allowed, Response(403) or redirect otherwise
        
    Example:
        >>> @app.get('/analytics')
        >>> def analytics(request):
        ...     error = require_feature_access(
        ...         request.state.tenant_id,
        ...         'advanced_analytics',
        ...         redirect_url='/upgrade'
        ...     )
        ...     if error:
        ...         return error
        ...     return render_analytics()
    """
    if check_feature_access(tenant_id, feature, feature_tiers, host_db):
        return None
    
    if redirect_url:
        return RedirectResponse(redirect_url, status_code=303)
    
    return Response(
        content=f'Feature "{feature}" requires plan upgrade',
        status_code=403,
        media_type='text/plain',
    )

# %% ../nbs/16_utils_stripe.ipynb #ab3b57a5
def create_webhook_route(
    app,
    service: StripeService,
    path: str = '/stripe/webhook',
):
    """Create a POST route handler for Stripe webhooks.
    
    Args:
        app: FastHTML application instance
        service: Configured StripeService instance
        path: URL path for webhook endpoint
        
    Returns:
        The registered route handler
        
    Example:
        >>> from fh_saas.utils_stripe import StripeService, StripeConfig, create_webhook_route
        >>> config = StripeConfig.from_env()
        >>> service = StripeService(config)
        >>> create_webhook_route(app, service)
        >>> # Webhook now available at POST /stripe/webhook
    """
    @app.post(path)
    async def stripe_webhook(request):
        payload = await request.body()
        sig_header = request.headers.get('stripe-signature', '')
        
        event = service.verify_signature(payload, sig_header)
        if not event:
            return Response('Invalid signature', status_code=400)
        
        result = service.handle_event(event)
        
        if result['status'] == 'error':
            return Response(result['message'], status_code=500)
        
        return Response('OK', status_code=200)
    
    return stripe_webhook


def create_subscription_checkout_route(
    app,
    service: StripeService,
    path: str = '/checkout/{plan_type}',
):
    """Create a GET route for subscription checkout redirect.
    
    Expects authenticated user with tenant_id in request.state.
    
    Args:
        app: FastHTML application instance
        service: Configured StripeService instance
        path: URL path pattern with {plan_type} placeholder
        
    Returns:
        The registered route handler
        
    Example:
        >>> create_subscription_checkout_route(app, service)
        >>> # Checkout available at GET /checkout/monthly or /checkout/yearly
    """
    @app.get(path)
    def subscription_checkout(request, plan_type: str):
        user = getattr(request.state, 'user', {})
        tenant_id = user.get('tenant_id') or getattr(request.state, 'tenant_id', None)
        user_email = user.get('email')
        
        if not tenant_id or not user_email:
            return Response('Authentication required', status_code=401)
        
        if plan_type not in ('monthly', 'yearly'):
            return Response('Invalid plan type', status_code=400)
        
        try:
            checkout = service.create_subscription_checkout(
                plan_type=plan_type,
                tenant_id=tenant_id,
                user_email=user_email,
            )
            return RedirectResponse(checkout.url, status_code=303)
        except Exception as e:
            logger.error(f"Checkout error: {e}")
            return Response(str(e), status_code=500)
    
    return subscription_checkout


def create_one_time_checkout_route(
    app,
    service: StripeService,
    products: Dict[str, Dict[str, Any]],
    path: str = '/buy/{product_id}',
):
    """Create a GET route for one-time payment checkout.
    
    Args:
        app: FastHTML application instance
        service: Configured StripeService instance
        products: Dict mapping product_id to {'name': str, 'amount_cents': int}
        path: URL path pattern with {product_id} placeholder
        
    Returns:
        The registered route handler
        
    Example:
        >>> products = {
        ...     'report': {'name': 'Premium Report', 'amount_cents': 4999},
        ...     'credits': {'name': '100 Credits', 'amount_cents': 1999},
        ... }
        >>> create_one_time_checkout_route(app, service, products)
        >>> # Checkout available at GET /buy/report or /buy/credits
    """
    @app.get(path)
    def one_time_checkout(request, product_id: str):
        user = getattr(request.state, 'user', {})
        tenant_id = user.get('tenant_id') or getattr(request.state, 'tenant_id', None)
        user_email = user.get('email')
        
        if not tenant_id or not user_email:
            return Response('Authentication required', status_code=401)
        
        product = products.get(product_id)
        if not product:
            return Response('Product not found', status_code=404)
        
        try:
            checkout = service.create_one_time_checkout(
                amount_cents=product['amount_cents'],
                product_name=product['name'],
                tenant_id=tenant_id,
                user_email=user_email,
            )
            return RedirectResponse(checkout.url, status_code=303)
        except Exception as e:
            logger.error(f"Checkout error: {e}")
            return Response(str(e), status_code=500)
    
    return one_time_checkout


def create_portal_route(
    app,
    service: StripeService,
    path: str = '/billing-portal',
):
    """Create a GET route for Stripe Customer Portal redirect.
    
    Args:
        app: FastHTML application instance
        service: Configured StripeService instance
        path: URL path for portal endpoint
        
    Returns:
        The registered route handler
        
    Example:
        >>> create_portal_route(app, service)
        >>> # Portal available at GET /billing-portal
    """
    @app.get(path)
    def billing_portal(request):
        user = getattr(request.state, 'user', {})
        
        # Need to find stripe_cust_id from user or subscription
        tenant_id = user.get('tenant_id') or getattr(request.state, 'tenant_id', None)
        
        if not tenant_id:
            return Response('Authentication required', status_code=401)
        
        # Get subscription to find customer ID
        sub = get_active_subscription(tenant_id)
        if not sub or not sub.stripe_cust_id:
            return Response('No subscription found', status_code=404)
        
        try:
            portal = service.create_customer_portal_session(sub.stripe_cust_id)
            return RedirectResponse(portal.url, status_code=303)
        except Exception as e:
            logger.error(f"Portal error: {e}")
            return Response(str(e), status_code=500)
    
    return billing_portal

# %% ../nbs/16_utils_stripe.ipynb #4460acf2
_stripe_service: Optional[StripeService] = None

def get_stripe_service(config: StripeConfig = None) -> StripeService:
    """Get or create singleton StripeService instance.
    
    Args:
        config: Optional StripeConfig. Uses from_env() on first call if not provided.
        
    Returns:
        StripeService singleton instance
        
    Example:
        >>> service = get_stripe_service()
        >>> checkout = service.create_subscription_checkout(...)
    """
    global _stripe_service
    
    if _stripe_service is None:
        if config is None:
            config = StripeConfig.from_env()
        _stripe_service = StripeService(config)
    
    return _stripe_service


def reset_stripe_service():
    """Reset singleton instance (for testing only)."""
    global _stripe_service
    _stripe_service = None
