import logging
from datetime import datetime
from typing import Optional
from uuid import UUID

import stripe
from pybreaker import CircuitBreaker, CircuitBreakerError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import select

from .. import settings
from ..models import UsageEvent
from ..models import Account

log = logging.getLogger(__name__)

# Configure Stripe API key from settings
if settings.STRIPE_SECRET_KEY:
    stripe.api_key = settings.STRIPE_SECRET_KEY

stripe_breaker = CircuitBreaker(
    fail_max=5,
    reset_timeout=60,
    exclude=[stripe.error.InvalidRequestError],
)


class BillingProcessor:
    """
    Processes usage events and reports them to Stripe for metered billing.
    Includes a circuit breaker to handle Stripe API outages gracefully.
    """

    def __init__(self, db_session: AsyncSession):
        self.db = db_session
        # Stripe circuit breaker to prevent cascading failures if Stripe is down.
        # It will trip after 5 consecutive failures and stay open for 60 seconds.
        # We exclude InvalidRequestError because that indicates a problem with our
        # data (e.g., bad ID), not a Stripe outage.
        self.stripe_breaker = CircuitBreaker(
            fail_max=5,
            reset_timeout=60,  # seconds
            exclude=[stripe.error.InvalidRequestError],
        )

    async def get_subscription_item_for_account(
        self, account_id: UUID
    ) -> Optional[str]:
        """
        Retrieves the Stripe Subscription Item ID for a given account.

        In a real application, this ID would be stored on an Account or Subscription
        model when the customer subscribes. This is a placeholder for that logic.
        """
        account = await self.db.get(Account, account_id)
        if not account or not account.stripe_customer_id:
            return None

        try:
            subscriptions = stripe.Subscription.list(
                customer=account.stripe_customer_id, limit=1, status="active"
            )
            if subscriptions.data:
                # Assuming the first item on the first active subscription is the metered one.
                # This logic might need to be more specific in a multi-product setup.
                return subscriptions.data[0].items.data[0].id
            return None
        except Exception as e:
            log.error(
                f"Could not fetch Stripe subscription for customer {account.stripe_customer_id}: {e}"
            )
            return None

    @stripe_breaker
    async def report_to_stripe(self, event: UsageEvent):
        """
        Reports a single usage event to Stripe's metered billing API.
        This method is protected by the circuit breaker.
        """
        if not settings.STRIPE_SECRET_KEY:
            log.warning("Stripe is not configured. Skipping usage reporting.")
            return

        subscription_item_id = await self.get_subscription_item_for_account(
            event.account_id
        )

        if not subscription_item_id:
            log.error(
                f"Could not find subscription_item_id for account {event.account_id}. Cannot report usage for event {event.id}."
            )
            # This is a permanent failure for this event; you might want to mark it as failed.
            return

        log.info(
            f"Reporting usage for event {event.id} to Stripe subscription item {subscription_item_id}"
        )

        try:
            stripe.SubscriptionItem.create_usage_record(
                subscription_item_id,
                quantity=event.quantity,
                timestamp=int(
                    event.timestamp.timestamp()
                ),  # Stripe requires a Unix timestamp
                action="increment",
                idempotency_key=event.idempotency_key or str(event.id),
            )
            log.info(f"Successfully reported usage for event {event.id}")
            event.processed_at = datetime.utcnow()
            self.db.add(event)
            await self.db.commit()
        except stripe.error.StripeError as e:
            log.error(f"Stripe API error reporting usage for event {event.id}: {e}")
            raise  # Re-raise to trip the circuit breaker

    async def process_unprocessed_events(self, limit: int = 100):
        """Fetches a batch of unprocessed usage events and reports them to Stripe."""
        stmt = (
            select(UsageEvent)
            .where(UsageEvent.processed_at.is_(None))
            .order_by(UsageEvent.created_at)
            .limit(limit)
        )
        events_to_process = (await self.db.execute(stmt)).scalars().all()

        if not events_to_process:
            return

        log.info(
            f"Found {len(events_to_process)} unprocessed usage events. Reporting to Stripe..."
        )
        for event in events_to_process:
            try:
                await self.report_to_stripe(event)
            except CircuitBreakerError:
                log.warning(
                    "Stripe circuit breaker is open. Halting usage reporting for this cycle."
                )
                break  # Stop processing this batch; will retry on the next run.
            except Exception:
                # Error is logged inside report_to_stripe. Continue to the next event for non-breaker errors.
                pass
