"""
Billing API endpoints — Stripe Checkout, Billing Portal, and webhook handler.

Endpoints:
  POST /api/portal/billing/checkout  → create Stripe Checkout Session
  POST /api/portal/billing/portal    → create Stripe Billing Portal session
  POST /api/webhooks/stripe          → Stripe webhook event handler

Webhook idempotency:
  All webhook events are checked against the stripe_events table before
  processing. If the event_id already exists, the handler returns "already_processed"
  immediately. This prevents duplicate processing on Stripe's at-least-once delivery.

StripeClient pattern:
  Uses `stripe.StripeClient(api_key=...)` (new v14+ API) rather than the legacy
  `stripe.api_key = ...` module-level approach, which is not thread-safe.

Webhook verification:
  Uses `client.webhooks.construct_event(payload, sig_header, webhook_secret)` to
  verify the Stripe-Signature header before processing any event data.
"""

from __future__ import annotations

import logging
import uuid
from typing import Any

import stripe
from fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel
from sqlalchemy import select, text
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from shared.api.rbac import PortalCaller, require_tenant_admin
from shared.config import settings
from shared.db import get_session
from shared.models.billing import StripeEvent
from shared.models.tenant import Agent, Tenant

logger = logging.getLogger(__name__)

billing_router = APIRouter(prefix="/api/portal/billing", tags=["billing"])
webhook_router = APIRouter(prefix="/api/webhooks", tags=["webhooks"])


# ---------------------------------------------------------------------------
# Pydantic schemas
# ---------------------------------------------------------------------------

class CheckoutRequest(BaseModel):
    tenant_id: uuid.UUID
    agent_count: int = 1


class CheckoutResponse(BaseModel):
    url: str
    session_id: str


class PortalRequest(BaseModel):
    tenant_id: uuid.UUID


class PortalResponse(BaseModel):
    url: str


# ---------------------------------------------------------------------------
# Internal helpers (module-level for testability — patchable)
# ---------------------------------------------------------------------------

async def _get_tenant_by_stripe_customer(
    customer_id: str, session: AsyncSession
) -> Tenant | None:
    """Look up a tenant by Stripe customer ID."""
    result = await session.execute(
        select(Tenant).where(Tenant.stripe_customer_id == customer_id)
    )
    return result.scalar_one_or_none()


async def _deactivate_all_agents(session: AsyncSession, tenant_id: uuid.UUID) -> None:
    """Set is_active=False for all agents belonging to the given tenant."""
    await session.execute(
        text("UPDATE agents SET is_active = FALSE WHERE tenant_id = :tenant_id"),
        {"tenant_id": str(tenant_id)},
    )


async def _reactivate_agents(session: AsyncSession, tenant_id: uuid.UUID) -> None:
    """Set is_active=True for all agents belonging to the given tenant."""
    await session.execute(
        text("UPDATE agents SET is_active = TRUE WHERE tenant_id = :tenant_id"),
        {"tenant_id": str(tenant_id)},
    )


async def _get_tenant_or_404(tenant_id: uuid.UUID, session: AsyncSession) -> Tenant:
    result = await session.execute(select(Tenant).where(Tenant.id == tenant_id))
    tenant = result.scalar_one_or_none()
    if tenant is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found")
    return tenant


# ---------------------------------------------------------------------------
# Core webhook event processor (extracted for testability)
# ---------------------------------------------------------------------------

async def process_stripe_event(event_data: dict[str, Any], session: AsyncSession) -> str:
    """
    Process a Stripe webhook event dict.

    Returns:
      "already_processed" — if the event was already processed (idempotency guard)
      "ok"                — if the event was processed successfully
      "skipped"           — if the event type is not handled

    This function is separated from the HTTP handler so it can be unit-tested
    without a real HTTP request.
    """
    event_id: str = event_data["id"]
    event_type: str = event_data["type"]

    # Idempotency check — look for existing StripeEvent record
    existing = await session.execute(
        select(StripeEvent).where(StripeEvent.event_id == event_id)
    )
    if existing.scalar_one_or_none() is not None:
        return "already_processed"

    # Mark as processing — INSERT with conflict guard
    stripe_event_row = StripeEvent(event_id=event_id)
    session.add(stripe_event_row)
    try:
        await session.flush()
    except IntegrityError:
        # Another process inserted this event_id concurrently
        await session.rollback()
        return "already_processed"

    obj = event_data["data"]["object"]
    customer_id: str = obj.get("customer", "")

    if event_type == "checkout.session.completed":
        subscription_id = obj.get("subscription", "")
        if subscription_id and customer_id:
            tenant = await _get_tenant_by_stripe_customer(customer_id, session)
            if tenant:
                # Fetch subscription details to get item ID and quota
                tenant.stripe_subscription_id = subscription_id
                tenant.subscription_status = "trialing"

    elif event_type == "customer.subscription.updated":
        tenant = await _get_tenant_by_stripe_customer(customer_id, session)
        if tenant:
            new_status: str = obj.get("status", "active")
            items = obj.get("items", {}).get("data", [])
            quantity = items[0]["quantity"] if items else 0
            sub_item_id = items[0]["id"] if items else None

            tenant.subscription_status = new_status
            tenant.agent_quota = quantity
            tenant.stripe_subscription_id = obj.get("id", tenant.stripe_subscription_id)
            if sub_item_id:
                tenant.stripe_subscription_item_id = sub_item_id

            trial_end = obj.get("trial_end")
            if trial_end:
                from datetime import datetime, timezone
                tenant.trial_ends_at = datetime.fromtimestamp(trial_end, tz=timezone.utc)

    elif event_type == "customer.subscription.deleted":
        tenant = await _get_tenant_by_stripe_customer(customer_id, session)
        if tenant:
            tenant.subscription_status = "canceled"
            tenant.agent_quota = 0
            await _deactivate_all_agents(session, tenant.id)

    elif event_type == "invoice.paid":
        tenant = await _get_tenant_by_stripe_customer(customer_id, session)
        if tenant:
            tenant.subscription_status = "active"
            await _reactivate_agents(session, tenant.id)

    elif event_type == "invoice.payment_failed":
        tenant = await _get_tenant_by_stripe_customer(customer_id, session)
        if tenant:
            tenant.subscription_status = "past_due"

    else:
        logger.debug("Unhandled Stripe event type: %s", event_type)
        await session.commit()
        return "skipped"

    await session.commit()
    return "ok"


# ---------------------------------------------------------------------------
# Billing endpoints
# ---------------------------------------------------------------------------

@billing_router.post("/checkout", response_model=CheckoutResponse)
async def create_checkout_session(
    body: CheckoutRequest,
    caller: PortalCaller = Depends(require_tenant_admin),
    session: AsyncSession = Depends(get_session),
) -> CheckoutResponse:
    """
    Create a Stripe Checkout Session for the per-agent subscription plan.

    Creates a Stripe Customer lazily if the tenant does not yet have one.
    Uses 14-day trial period on first checkout.
    """
    if not settings.stripe_secret_key:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="STRIPE_SECRET_KEY not configured",
        )

    tenant = await _get_tenant_or_404(body.tenant_id, session)
    client = stripe.StripeClient(api_key=settings.stripe_secret_key)

    # Lazy Stripe Customer creation
    if not tenant.stripe_customer_id:
        customer = client.customers.create(params={"name": tenant.name})
        tenant.stripe_customer_id = customer.id
        await session.commit()

    # Create Checkout Session
    checkout_session = client.checkout.sessions.create(
        params={
            "customer": tenant.stripe_customer_id,
            "mode": "subscription",
            "line_items": [
                {
                    "price": settings.stripe_per_agent_price_id,
                    "quantity": body.agent_count,
                }
            ],
            "subscription_data": {"trial_period_days": 14},
            "success_url": f"{settings.portal_url}/settings/billing?success=1",
            "cancel_url": f"{settings.portal_url}/settings/billing?canceled=1",
        }
    )

    return CheckoutResponse(
        url=checkout_session.url or "",
        session_id=checkout_session.id,
    )


@billing_router.post("/portal", response_model=PortalResponse)
async def create_billing_portal_session(
    body: PortalRequest,
    caller: PortalCaller = Depends(require_tenant_admin),
    session: AsyncSession = Depends(get_session),
) -> PortalResponse:
    """
    Create a Stripe Billing Portal session for the tenant.

    Returns the Stripe-hosted portal URL where the customer can manage their
    subscription, update payment methods, and view invoices.
    """
    if not settings.stripe_secret_key:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="STRIPE_SECRET_KEY not configured",
        )

    tenant = await _get_tenant_or_404(body.tenant_id, session)

    if not tenant.stripe_customer_id:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Tenant has no Stripe customer — start a subscription first",
        )

    client = stripe.StripeClient(api_key=settings.stripe_secret_key)
    portal_session = client.billing_portal.sessions.create(
        params={
            "customer": tenant.stripe_customer_id,
            "return_url": f"{settings.portal_url}/settings/billing",
        }
    )

    return PortalResponse(url=portal_session.url)


# ---------------------------------------------------------------------------
# Stripe webhook endpoint (no portal auth — separate router prefix)
# ---------------------------------------------------------------------------

@webhook_router.post("/stripe")
async def stripe_webhook(
    request: Request,
    session: AsyncSession = Depends(get_session),
) -> dict[str, str]:
    """
    Handle incoming Stripe webhook events.

    1. Read raw request body (required for HMAC signature verification)
    2. Verify Stripe-Signature header with stripe.Webhook.construct_event()
    3. Check idempotency via stripe_events table
    4. Dispatch to handler by event type
    """
    if not settings.stripe_webhook_secret:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="STRIPE_WEBHOOK_SECRET not configured",
        )

    payload = await request.body()
    sig_header = request.headers.get("stripe-signature", "")

    client = stripe.StripeClient(api_key=settings.stripe_secret_key)
    try:
        event = client.webhooks.construct_event(
            payload=payload,
            sig=sig_header,
            secret=settings.stripe_webhook_secret,
        )
    except stripe.SignatureVerificationError as exc:
        logger.warning("Stripe webhook signature verification failed: %s", exc)
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid Stripe webhook signature",
        ) from exc

    # Convert stripe Event to plain dict for the processor
    event_data = event.to_dict()
    result = await process_stripe_event(event_data, session)
    return {"status": result}
