"""
Stripe webhook handler for the main API.
Handles invoice, payment, and subscription events from Stripe.
"""

import os
import json
import logging
from uuid import UUID, uuid4
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Iterable, Any, Dict, Optional, Tuple, List, Set

from fastapi import APIRouter, Request, HTTPException
from sqlalchemy import text, inspect, create_engine, bindparam
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.exc import SQLAlchemyError
from dateutil import parser as dateparser

from .. import settings as saas_settings
from ..config.tier_limits import (
    TIER_CONFIG as ENRICHMENT_CONFIG,
    get_tier_config,
    normalize_tier_name,
)
from ..models import SubscriptionEvent, SeatTransaction, CreditTransaction
from ..services.usage_gateway import UsageGateway

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/webhooks", tags=["webhooks"])
legacy_router = APIRouter(prefix="/api/v1/webhooks", tags=["webhooks"])

_ADMIN_ENGINE = None
_ADMIN_SESSION_FACTORY = None
_ADMIN_SCHEMA_PREPARED = False
_STRIPE_EVENTS_TABLE_PRESENT: Optional[bool] = None
_WEBHOOK_SCHEMA_PREPARED = False
_ACTIVE_SUBSCRIPTION_STATUSES = {"active", "trialing", "past_due"}
_TIER_RANK = {"free": 0, "pro": 1, "scale": 2, "unleashed": 3}
_STRIPE_FETCH_TIMEOUT_SECONDS = 8.0


def get_stripe_client():
    """Get configured Stripe client."""
    try:
        import stripe

        stripe_key = os.getenv("STRIPE_SECRET_KEY")
        if not stripe_key:
            raise RuntimeError("STRIPE_SECRET_KEY not configured")
        stripe.api_key = stripe_key
        return stripe
    except ImportError:
        raise RuntimeError("Stripe library not installed. Run: pip install stripe")


def _make_admin_db_url() -> str:
    """
    Resolve the admin database URL, normalizing async dialects to sync variants
    so SQLAlchemy's blocking engine can connect without extra drivers.
    """
    candidates = [
        os.getenv("ADMIN_DATABASE_URL"),
        getattr(saas_settings, "ADMIN_DATABASE_URL", None),
        saas_settings.DATABASE_URL,
        "postgresql://postgres:postgres@localhost:5432/foundrymatch",
    ]

    for raw in candidates:
        if not raw:
            continue
        url = raw.strip()
        if url.startswith("postgresql+asyncpg://"):
            return url.replace("postgresql+asyncpg://", "postgresql://", 1)
        if url.startswith("sqlite"):
            raise RuntimeError(
                "SQLite is no longer supported for admin database connections."
            )
        return url

    raise RuntimeError("Unable to resolve admin database URL")


def _ensure_admin_schema(engine) -> None:
    """Run backend_admin SQL migrations if core billing tables are missing."""
    global _ADMIN_SCHEMA_PREPARED
    if _ADMIN_SCHEMA_PREPARED:
        return

    inspector = inspect(engine)
    required_tables = (
        "accounts",
        "product",
        "plan",
        "subscription",
        "invoice",
        "invoice_line",
        "payment",
        "usage_event",
        "credits_ledger",
    )

    missing = [table for table in required_tables if not inspector.has_table(table)]
    if not missing:
        _ADMIN_SCHEMA_PREPARED = True
        return

    migrations_dir = (
        Path(__file__).resolve().parents[4] / "backend_admin" / "migrations"
    )
    if not migrations_dir.exists():
        logger.warning(
            "[STRIPE WEBHOOK] Admin migrations directory missing at %s; tables %s not present",
            migrations_dir,
            ", ".join(missing),
        )
        _ADMIN_SCHEMA_PREPARED = True  # Avoid retry storms
        return

    sql_files: Iterable[Path] = sorted(migrations_dir.glob("*.sql"))
    if not sql_files:
        logger.warning(
            "[STRIPE WEBHOOK] No SQL migrations found in %s; tables %s not present",
            migrations_dir,
            ", ".join(missing),
        )
        _ADMIN_SCHEMA_PREPARED = True
        return

    logger.info(
        "[STRIPE WEBHOOK] Applying admin migrations (%s) because tables %s were missing",
        ", ".join(path.name for path in sql_files),
        ", ".join(missing),
    )

    try:
        with engine.begin() as conn:
            raw = conn.connection
            cursor = raw.cursor()
            try:
                for script in sql_files:
                    sql = script.read_text(encoding="utf-8")
                    cursor.execute(sql)
            finally:
                cursor.close()
    except Exception as exc:  # pragma: no cover - depends on external DB
        logger.error(
            "[STRIPE WEBHOOK] Failed to apply admin migrations: %s", exc, exc_info=True
        )
    else:
        inspector = inspect(engine)
        remaining = [
            table for table in required_tables if not inspector.has_table(table)
        ]
        if remaining:
            logger.warning(
                "[STRIPE WEBHOOK] Admin migrations applied but tables still missing: %s",
                ", ".join(remaining),
            )
        _ADMIN_SCHEMA_PREPARED = True


def _ensure_webhook_schema(db: Session) -> None:
    """Ensure webhook-side tables exist (account_subscriptions, stripe_events)."""
    global _WEBHOOK_SCHEMA_PREPARED, _STRIPE_EVENTS_TABLE_PRESENT
    if _WEBHOOK_SCHEMA_PREPARED:
        return

    dialect = ""
    try:
        dialect = db.bind.dialect.name
    except Exception:
        dialect = ""

    if dialect == "sqlite":
        account_subs_sql = """
            CREATE TABLE IF NOT EXISTS account_subscriptions (
                id TEXT PRIMARY KEY,
                account_id TEXT NOT NULL,
                stripe_subscription_id TEXT UNIQUE NOT NULL,
                stripe_customer_id TEXT NOT NULL,
                subscription_type TEXT NOT NULL,
                tier TEXT,
                addon_key TEXT,
                price_id TEXT,
                status TEXT NOT NULL,
                current_period_start TEXT,
                current_period_end TEXT,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP,
                updated_at TEXT DEFAULT CURRENT_TIMESTAMP
            )
        """
        stripe_events_sql = """
            CREATE TABLE IF NOT EXISTS stripe_events (
                event_id TEXT PRIMARY KEY,
                event_type TEXT NOT NULL,
                processed_at TEXT DEFAULT CURRENT_TIMESTAMP,
                account_id TEXT,
                subscription_id TEXT
            )
        """
    else:
        account_subs_sql = """
            CREATE TABLE IF NOT EXISTS account_subscriptions (
                id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
                account_id TEXT NOT NULL,
                stripe_subscription_id TEXT UNIQUE NOT NULL,
                stripe_customer_id TEXT NOT NULL,
                subscription_type TEXT NOT NULL,
                tier TEXT,
                addon_key TEXT,
                price_id TEXT,
                status TEXT NOT NULL,
                current_period_start TIMESTAMPTZ,
                current_period_end TIMESTAMPTZ,
                created_at TIMESTAMPTZ DEFAULT now(),
                updated_at TIMESTAMPTZ DEFAULT now()
            )
        """
        stripe_events_sql = """
            CREATE TABLE IF NOT EXISTS stripe_events (
                event_id TEXT PRIMARY KEY,
                event_type TEXT NOT NULL,
                processed_at TIMESTAMPTZ DEFAULT now(),
                account_id TEXT,
                subscription_id TEXT
            )
        """

    try:
        db.execute(text(account_subs_sql))
        db.execute(
            text(
                "CREATE INDEX IF NOT EXISTS idx_account_subscriptions_account "
                "ON account_subscriptions(account_id)"
            )
        )
        db.execute(
            text(
                "CREATE INDEX IF NOT EXISTS idx_account_subscriptions_customer "
                "ON account_subscriptions(stripe_customer_id)"
            )
        )
        db.execute(text(stripe_events_sql))
        db.execute(
            text(
                "CREATE INDEX IF NOT EXISTS idx_stripe_events_subscription "
                "ON stripe_events(subscription_id)"
            )
        )
        _STRIPE_EVENTS_TABLE_PRESENT = True
    except Exception as exc:  # pragma: no cover - defensive
        logger.warning("[STRIPE WEBHOOK] Failed to ensure webhook schema: %s", exc)
    finally:
        _WEBHOOK_SCHEMA_PREPARED = True


def get_db_connection():
    """
    Get database connection for webhook processing.
    Uses the consolidated admin backend database.
    """
    global _ADMIN_ENGINE, _ADMIN_SESSION_FACTORY

    if _ADMIN_SESSION_FACTORY is None:
        db_url = _make_admin_db_url()
        _ADMIN_ENGINE = create_engine(db_url, pool_pre_ping=True)
        _ensure_admin_schema(_ADMIN_ENGINE)
        _ADMIN_SESSION_FACTORY = sessionmaker(bind=_ADMIN_ENGINE)

    try:
        return _ADMIN_SESSION_FACTORY()
    except SQLAlchemyError as exc:
        logger.error(
            "[STRIPE WEBHOOK] Failed to obtain DB connection: %s", exc, exc_info=True
        )
        raise


def _normalize_tier(raw_tier: Any) -> str:
    """Normalize tier strings to known keys."""
    value = normalize_tier_name(raw_tier)
    return value or "free"


def _infer_tier_from_text(text_value: Any) -> Optional[str]:
    """Best-effort inference of tier from plan or price names.

    Uses centralized tier inference, then maps to webhook's internal naming
    (scale/unleashed -> enterprise for entitlement lookups).
    """
    from ..services.tier import infer_tier, Tier

    if not text_value:
        return None

    tier = infer_tier(str(text_value))

    # Map to webhook's internal naming convention
    if tier == Tier.UNLEASHED:
        return "unleashed"
    if tier == Tier.SCALE:
        return "scale"
    if tier == Tier.PRO:
        return "pro"
    if tier == Tier.FREE:
        return "free"
    return None


def _get_tier_entitlements(tier: str) -> Dict[str, int]:
    normalized = _normalize_tier(tier)
    try:
        config = get_tier_config(normalized)
    except Exception:
        try:
            config = get_tier_config("free")
        except Exception:
            return {
                "monthly_match_entitlement": 0,
                "monthly_enrichment_entitlement": 0,
            }
    return {
        "monthly_match_entitlement": int(config.monthly_rows or 0),
        "monthly_enrichment_entitlement": int(config.monthly_enrichments or 0),
    }


def _tier_for_price_id(price_id: Optional[str]) -> Optional[str]:
    """Resolve tier from Stripe price ID using tier config."""
    if not price_id or not ENRICHMENT_CONFIG:
        return None
    for tier_name, tier_cfg in ENRICHMENT_CONFIG.tiers.items():
        stripe = getattr(tier_cfg, "stripe", None)
        if not stripe:
            continue
        if price_id in (stripe.price_id_monthly, stripe.price_id_annual):
            return _normalize_tier(tier_name)
    return None


def _extract_addons_from_subscription(subscription: Dict[str, Any]) -> List[str]:
    """Extract add-on keys from Stripe subscription items based on price IDs."""
    if not ENRICHMENT_CONFIG:
        logger.warning("[STRIPE WEBHOOK] ENRICHMENT_CONFIG is None - cannot extract addons")
        return []
    if not getattr(ENRICHMENT_CONFIG, "addons", None):
        logger.warning("[STRIPE WEBHOOK] ENRICHMENT_CONFIG.addons is missing or empty")
        return []

    addons = set()
    items = (subscription.get("items") or {}).get("data") or []
    price_ids_seen = []
    for item in items:
        price = item.get("price") or {}
        price_id = price.get("id")
        if not price_id:
            continue
        price_ids_seen.append(price_id)
        for addon_key, addon in ENRICHMENT_CONFIG.addons.items():
            stripe = addon.stripe if addon else None
            if not stripe:
                continue
            if price_id in (stripe.price_id_monthly, stripe.price_id_annual):
                addons.add(addon_key)
                logger.info(
                    "[STRIPE WEBHOOK] Matched addon %s from price_id %s",
                    addon_key,
                    price_id,
                )

    result = sorted(addons)
    logger.info(
        "[STRIPE WEBHOOK] _extract_addons_from_subscription: price_ids=%s -> addons=%s",
        price_ids_seen,
        result,
    )
    return result


def _stripe_events_supported(db: Session) -> bool:
    """Check if stripe_events table exists (cached)."""
    global _STRIPE_EVENTS_TABLE_PRESENT
    if _STRIPE_EVENTS_TABLE_PRESENT is None:
        try:
            _STRIPE_EVENTS_TABLE_PRESENT = inspect(db.bind).has_table("stripe_events")
        except Exception as exc:  # pragma: no cover - defensive
            logger.debug(
                "[STRIPE WEBHOOK] Unable to inspect stripe_events table: %s", exc
            )
            _STRIPE_EVENTS_TABLE_PRESENT = False
    return bool(_STRIPE_EVENTS_TABLE_PRESENT)


def _has_processed_stripe_event(db: Session, event_id: str) -> bool:
    """Check for duplicate events using stripe_events table if present."""
    if not event_id or not _stripe_events_supported(db):
        return False
    try:
        row = db.execute(
            text("SELECT 1 FROM stripe_events WHERE event_id=:event_id"),
            {"event_id": event_id},
        ).first()
        return bool(row)
    except Exception as exc:  # pragma: no cover - defensive
        logger.debug(
            "[STRIPE WEBHOOK] stripe_events lookup failed (disabling): %s", exc
        )
        # Avoid repeated failures if schema differs
        global _STRIPE_EVENTS_TABLE_PRESENT
        _STRIPE_EVENTS_TABLE_PRESENT = False
        return False


def _mark_stripe_event_processed(
    db: Session,
    event_id: str,
    event_type: str,
    *,
    account_id: Optional[str] = None,
    subscription_id: Optional[str] = None,
    event_created: Optional[datetime] = None,
) -> None:
    """Persist processed events to stripe_events when available."""
    if not event_id or not _stripe_events_supported(db):
        return
    processed_at = event_created or datetime.now(timezone.utc)
    try:
        db.execute(
            text(
                """
                INSERT INTO stripe_events (
                    event_id, event_type, processed_at, account_id, subscription_id
                )
                VALUES (:event_id, :event_type, :processed_at, :account_id, :subscription_id)
                ON CONFLICT (event_id) DO NOTHING
                """
            ),
            {
                "event_id": event_id,
                "event_type": event_type,
                "processed_at": processed_at,
                "account_id": account_id,
                "subscription_id": subscription_id,
            },
        )
    except Exception as exc:  # pragma: no cover - defensive
        logger.debug(
            "[STRIPE WEBHOOK] Failed to persist stripe_events row (disabling): %s", exc
        )
        global _STRIPE_EVENTS_TABLE_PRESENT
        _STRIPE_EVENTS_TABLE_PRESENT = False


def _get_last_subscription_event_ts(
    db: Session, stripe_subscription_id: str
) -> Optional[datetime]:
    """Return the latest processed Stripe event timestamp for a subscription."""
    if not stripe_subscription_id or not _stripe_events_supported(db):
        return None
    try:
        row = db.execute(
            text(
                """
                SELECT MAX(processed_at)
                FROM stripe_events
                WHERE subscription_id=:sid
                """
            ),
            {"sid": stripe_subscription_id},
        ).first()
        if not row:
            return None
        value = row[0]
        if isinstance(value, str):
            try:
                value = dateparser.parse(value)
            except Exception:
                return None
        if value and value.tzinfo is None:
            value = value.replace(tzinfo=timezone.utc)
        return value
    except Exception:  # pragma: no cover - defensive
        return None


def _is_out_of_order_subscription_event(
    db: Session,
    stripe_subscription_id: Optional[str],
    event_created: Optional[datetime],
) -> bool:
    if not stripe_subscription_id or not event_created:
        return False
    last_event_ts = _get_last_subscription_event_ts(db, stripe_subscription_id)
    if not last_event_ts:
        return False
    return event_created < last_event_ts


def _find_account_from_metadata(db: Session, metadata: Dict[str, Any]) -> Optional[str]:
    """Lookup account using clerk/org metadata without hitting Stripe."""
    if not metadata:
        return None
    candidates = []
    for key in (
        "orgId",
        "organizationId",
        "clerk_org_id",
        "clerkOrgId",
        "org_id",
        "accountId",
    ):
        val = metadata.get(key)
        if val:
            candidates.append(val)
    for key in ("userId", "clerkId"):
        val = metadata.get(key)
        if val:
            candidates.append(val)

    for candidate in candidates:
        row = db.execute(
            text("SELECT id FROM accounts WHERE id=:cid OR clerk_org_id=:cid"),
            {"cid": candidate},
        ).first()
        if row:
            return str(row[0])
    return None


def _update_account_refs(
    db: Session,
    account_id: str,
    stripe_customer_id: Optional[str] = None,
    stripe_subscription_id: Optional[str] = None,
    *,
    allow_subscription_override: bool = False,
) -> None:
    """Update stripe identifiers on the account record."""
    sets = []
    params: Dict[str, Any] = {"aid": str(account_id)}
    if stripe_customer_id:
        sets.append("stripe_customer_id=:cid")
        params["cid"] = stripe_customer_id
    if stripe_subscription_id and allow_subscription_override:
        sets.append("stripe_subscription_id=:sid")
        params["sid"] = stripe_subscription_id
    if not sets:
        return
    db.execute(
        text(f"UPDATE accounts SET {', '.join(sets)} WHERE id=:aid"),
        params,
    )


def _set_primary_subscription(
    db: Session, account_id: str, stripe_subscription_id: Optional[str]
) -> None:
    """Set the primary tier subscription reference on the account."""
    db.execute(
        text(
            """
            UPDATE accounts
            SET stripe_subscription_id = :sid
            WHERE id = :aid
            """
        ),
        {"sid": stripe_subscription_id, "aid": str(account_id)},
    )


def _get_existing_quota_tier(db: Session, account_id: str) -> Optional[str]:
    """Fetch current tier from usage_quotas if present."""
    row = db.execute(
        text("SELECT tier FROM usage_quotas WHERE account_id=:aid"),
        {"aid": str(account_id)},
    ).first()
    return _normalize_tier(row[0]) if row and row[0] else None


def _upsert_usage_quota(
    db: Session,
    account_id: str,
    tier: str,
    status: str,
    source: str,
    reset_usage: bool = False,
    addons: Optional[List[str]] = None,
) -> Dict[str, Any]:
    """
    Upsert usage_quotas with the requested tier/status and entitlements.

    Optionally resets usage counters when reset_usage is True.
    """
    normalized_tier = _normalize_tier(tier)
    entitlements = _get_tier_entitlements(normalized_tier)
    reset_clause = ""
    if reset_usage:
        reset_clause = (
            ", monthly_rows_used=0,"
            " enrichment_credits_used=0,"
            " daily_runs_used=0,"
            " daily_rows_used=0,"
            " grace_uses_remaining=1,"
            " last_grace_at=NULL"
        )

    db.execute(
        text(
            f"""
            INSERT INTO usage_quotas (
                account_id, tier, subscription_status,
                monthly_enrichment_entitlement, monthly_match_entitlement,
                monthly_rows_used, enrichment_credits_used,
                tier_synced_at, tier_sync_source,
                daily_runs_used, daily_rows_used,
                active_schedules, grace_uses_remaining,
                list_builder_monthly_exports, list_builder_daily_exports,
                timezone, created_at, updated_at
            )
            VALUES (
                :aid, :tier, :status,
                :enrichment_entitlement, :match_entitlement,
                0, 0, NOW(), :source,
                0, 0, 0, 1,
                0, 0,
                'UTC', NOW(), NOW()
            )
            ON CONFLICT (account_id) DO UPDATE SET
                tier=:tier,
                subscription_status=:status,
                monthly_enrichment_entitlement=:enrichment_entitlement,
                monthly_match_entitlement=:match_entitlement,
                tier_synced_at=NOW(),
                tier_sync_source=:source,
                updated_at=NOW()
                {reset_clause}
            """
        ),
        {
            "aid": str(account_id),
            "tier": normalized_tier,
            "status": status,
            "enrichment_entitlement": entitlements["monthly_enrichment_entitlement"],
            "match_entitlement": entitlements["monthly_match_entitlement"],
            "source": source,
        },
    )
    logger.info(
        "[STRIPE WEBHOOK] usage_quotas upsert account=%s tier=%s status=%s reset=%s",
        account_id,
        normalized_tier,
        status,
        reset_usage,
    )
    try:
        UsageGateway.sync_from_stripe_sync(
            db,
            account_id,
            tier=normalized_tier,
            addons=addons or [],
            source=source,
            monthly_row_limit=entitlements.get("monthly_match_entitlement"),
            commit=False,
        )
    except Exception as exc:  # noqa: BLE001 - best effort only
        logger.warning(
            "[STRIPE WEBHOOK] Failed to sync usage gateway entitlements for %s: %s",
            account_id,
            exc,
        )
    return {"tier": normalized_tier, **entitlements}


def _extract_tier_from_subscription(
    subscription: Dict[str, Any],
) -> Tuple[str, Optional[str], Optional[str]]:
    """
    Extract tier, interval, and base price id from subscription payload.

    Returns (tier, interval, base_price_id).
    """
    metadata = subscription.get("metadata") or {}
    raw_tier = (
        metadata.get("tier")
        or metadata.get("plan")
        or metadata.get("product_tier")
        or None
    )
    interval = metadata.get("interval") or metadata.get("billing_interval")
    price_id = None

    items = (subscription.get("items") or {}).get("data") or []
    for item in items:
        price = item.get("price") or {}
        item_price_id = price.get("id")
        price_id = price_id or item_price_id
        price_meta = price.get("metadata") or {}
        raw_tier = raw_tier or price_meta.get("tier") or price_meta.get("plan")
        raw_tier = raw_tier or _tier_for_price_id(item_price_id)
        interval = interval or (price.get("recurring") or {}).get("interval")
        nickname = price.get("nickname")
        if not raw_tier:
            raw_tier = _infer_tier_from_text(nickname)
        if raw_tier:
            break

    if not raw_tier:
        plan_obj = subscription.get("plan") or {}
        raw_tier = _infer_tier_from_text(plan_obj.get("nickname"))

    normalized = _normalize_tier(raw_tier) if raw_tier else None
    return normalized, interval, price_id


def _extract_tier_from_line_items(
    line_items: Dict[str, Any],
) -> Tuple[Optional[str], Optional[str], Optional[str]]:
    """Derive tier/interval/price_id from checkout line items."""
    tier = None
    interval = None
    price_id = None

    for line in line_items.get("data", []):
        price = line.get("price") or {}
        item_price_id = price.get("id")
        price_id = price_id or item_price_id
        price_meta = price.get("metadata") or {}
        tier = tier or price_meta.get("tier") or price_meta.get("plan")
        tier = tier or _tier_for_price_id(item_price_id)
        interval = interval or (price.get("recurring") or {}).get("interval")
        nickname = price.get("nickname")
        if not tier:
            tier = _infer_tier_from_text(nickname)
        if tier:
            break

    return (_normalize_tier(tier) if tier else None, interval, price_id)


def _get_account_from_stripe_customer(db: Session, stripe_customer_id: str):
    """
    Find account by Stripe customer ID, falling back to Clerk user ID from metadata.

    Returns account_id (str) or None if not found.
    """
    # Try direct lookup by stripe_customer_id first
    acct = db.execute(
        text("SELECT id FROM accounts WHERE stripe_customer_id=:cid"),
        {"cid": stripe_customer_id},
    ).first()

    if acct:
        return str(acct[0])

    # If not found, retrieve customer from Stripe and check metadata
    try:
        stripe = get_stripe_client()
        customer = stripe.Customer.retrieve(stripe_customer_id)

        # Try to get org ID or user ID from metadata
        org_id = customer.metadata.get("orgId") or customer.metadata.get(
            "organizationId"
        )
        user_id = customer.metadata.get("userId") or customer.metadata.get("clerkId")

        if not org_id and not user_id:
            logger.warning(
                f"[STRIPE WEBHOOK] Customer {stripe_customer_id} has no orgId/userId in metadata"
            )
            return None

        # Try organization ID first (preferred for multi-user accounts)
        acct = None
        if org_id:
            acct = db.execute(
                text(
                    "SELECT id FROM accounts WHERE id=:org_id OR clerk_org_id=:org_id"
                ),
                {"org_id": org_id},
            ).first()
            if acct:
                logger.info(f"[STRIPE WEBHOOK] Found account via orgId={org_id}")

        # Fallback to user ID if org ID lookup failed
        if not acct and user_id:
            acct = db.execute(
                text(
                    "SELECT id FROM accounts WHERE id=:user_id OR clerk_org_id=:user_id"
                ),
                {"user_id": user_id},
            ).first()
            if acct:
                logger.info(f"[STRIPE WEBHOOK] Found account via userId={user_id}")

        if acct:
            account_id = str(acct[0])

            # Update the account with stripe_customer_id for faster future lookups
            db.execute(
                text("UPDATE accounts SET stripe_customer_id=:cid WHERE id=:aid"),
                {"cid": stripe_customer_id, "aid": account_id},
            )
            logger.info(
                f"[STRIPE WEBHOOK] Updated account {account_id} with stripe_customer_id"
            )

            return account_id
        else:
            # Auto-create account if it doesn't exist
            # Prefer org_id (for team accounts) over user_id (for individual accounts)
            account_id = org_id or user_id
            if account_id:
                logger.info(
                    f"[STRIPE WEBHOOK] Auto-creating account {account_id} from Stripe customer {stripe_customer_id}"
                )
                db.execute(
                    text("""
                        INSERT INTO accounts (
                            id,
                            name,
                            stripe_customer_id,
                            clerk_org_id,
                            telemetry_opt_in,
                            credits_balance,
                            purchased_credits,
                            data_region,
                            paid_seat_count
                        )
                        VALUES (
                            :id,
                            :name,
                            :stripe_cid,
                            :clerk_org_id,
                            TRUE,
                            0,
                            0,
                            'us-east-1',
                            1
                        )
                        ON CONFLICT (id) DO UPDATE
                        SET stripe_customer_id = :stripe_cid,
                            clerk_org_id = COALESCE(accounts.clerk_org_id, :clerk_org_id)
                    """),
                    {
                        "id": account_id,
                        "name": f"Clerk Org {account_id}" if org_id else f"User {user_id}",
                        "stripe_cid": stripe_customer_id,
                        "clerk_org_id": org_id if org_id else None,
                    },
                )
                logger.info(
                    f"[STRIPE WEBHOOK] Created account {account_id} for customer {stripe_customer_id}"
                )
                return account_id
            else:
                logger.warning(
                    f"[STRIPE WEBHOOK] No account found and no orgId/userId to create account for customer {stripe_customer_id}"
                )
                return None

    except Exception as e:
        logger.error(
            f"[STRIPE WEBHOOK] Error retrieving customer {stripe_customer_id}: {e}",
            exc_info=True,
        )
        return None


def _json_dumps(value: Any) -> str:
    """Serialize webhook payload/header dictionaries safely."""

    def _default(obj: Any):
        if isinstance(obj, (datetime,)):
            return obj.isoformat()
        return str(obj)

    return json.dumps(value, default=_default)


def _is_uuid_like(value: Any) -> bool:
    try:
        UUID(str(value))
        return True
    except Exception:
        return False


def _event_created_at(evt: Optional[dict]) -> Optional[datetime]:
    """Normalize Stripe event created timestamp to datetime."""
    if not evt:
        return None
    created = evt.get("created")
    if created is None:
        return None
    try:
        return datetime.fromtimestamp(int(created), tz=timezone.utc)
    except Exception:
        return None


def _tier_rank(value: Optional[str]) -> int:
    normalized = _normalize_tier(value)
    return _TIER_RANK.get(normalized, -1)


def _is_active_subscription_status(status: Optional[str]) -> bool:
    return (status or "").lower() in _ACTIVE_SUBSCRIPTION_STATUSES


def _get_webhook_audit_row(db: Session, event_id: str):
    return db.execute(
        text(
            "SELECT id, status, attempts FROM webhook_audit "
            "WHERE provider='stripe' AND event_id=:event_id"
        ),
        {"event_id": event_id},
    ).first()


def _record_webhook_attempt(
    db: Session,
    evt: Dict[str, Any],
    headers: Dict[str, str],
    previous_attempts: int = 0,
) -> str:
    """Insert or update the webhook audit log with the incoming attempt."""
    event_id = evt.get("id")
    scope = None
    data_obj = (evt.get("data") or {}).get("object") or {}
    for candidate in ("customer", "subscription", "account"):
        value = data_obj.get(candidate)
        if value:
            scope = value
            break

    payload_json = _json_dumps(evt)
    headers_json = _json_dumps(headers)

    row = db.execute(
        text(
            """
            INSERT INTO webhook_audit (
                id, provider, event_id, event_type, scope, status, attempts,
                payload, headers, received_at
            )
            VALUES (
                gen_random_uuid(), 'stripe', :event_id, :event_type, :scope,
                'received', :attempts, :payload, :headers, NOW()
            )
            ON CONFLICT (provider, event_id) DO UPDATE
            SET status='received',
                attempts=:attempts,
                scope=COALESCE(webhook_audit.scope, :scope),
                payload=COALESCE(webhook_audit.payload, :payload),
                headers=COALESCE(webhook_audit.headers, :headers),
                last_error=NULL
            RETURNING id
            """
        ),
        {
            "event_id": event_id,
            "event_type": evt.get("type"),
            "scope": scope,
            "attempts": max(previous_attempts + 1, 1),
            "payload": payload_json,
            "headers": headers_json,
        },
    ).first()

    return str(row[0]) if row else ""


def _update_webhook_audit_status(
    db: Session,
    audit_id: Optional[str],
    status: str,
    error: Optional[str] = None,
) -> None:
    if not audit_id:
        return

    db.execute(
        text(
            """
            UPDATE webhook_audit
            SET status=:status,
                processed_at=CASE WHEN :status IN ('processed','ignored') THEN NOW() ELSE processed_at END,
                last_error=:error
            WHERE id=:id
            """
        ),
        {"status": status, "error": error, "id": audit_id},
    )


def _extract_subscription_item_snapshot(
    subscription: Dict[str, Any],
) -> Tuple[Optional[int], int, Optional[int], Optional[str], Tuple[str, ...]]:
    """
    Returns (base_quantity, seat_addon_quantity, seat_total, base_price_id, seat_item_ids).
    """
    items = (subscription.get("items") or {}).get("data") or []
    base_quantity: Optional[int] = None
    seat_addon_total = 0
    base_price_id: Optional[str] = None
    seat_item_ids = []

    for item in items:
        price = item.get("price") or {}
        quantity = item.get("quantity") or 0
        metadata = price.get("metadata") or {}
        lookup_key = (price.get("lookup_key") or "").lower()
        nickname = (price.get("nickname") or "").lower()
        is_seat_addon = (
            metadata.get("kind") == "seat"
            or metadata.get("seat_addon") == "true"
            or "seat" in lookup_key
            or "seat" in nickname
        )

        if is_seat_addon:
            seat_addon_total += quantity
            if item.get("id"):
                seat_item_ids.append(item["id"])
            continue

        # Treat the first non-seat line as the base subscription item
        if base_price_id is None:
            base_price_id = price.get("id")
            base_quantity = quantity or base_quantity

    total = None
    if base_quantity is not None or seat_addon_total:
        total = (base_quantity or 0) + seat_addon_total

    return base_quantity, seat_addon_total, total, base_price_id, tuple(seat_item_ids)


def _format_addon_keys(addons: List[str]) -> Optional[str]:
    if not addons:
        return None
    cleaned = {str(addon).strip() for addon in addons if addon}
    return ",".join(sorted(cleaned)) if cleaned else None


def _classify_subscription(
    tier: Optional[str], addons: List[str]
) -> Tuple[str, Optional[str]]:
    """Return (subscription_type, addon_key) for account_subscriptions."""
    addon_key = _format_addon_keys(addons)
    if tier:
        return "tier", addon_key
    if addon_key:
        return "addon", addon_key
    return "tier", None


def _upsert_account_subscription(
    db: Session,
    *,
    account_id: str,
    stripe_customer_id: str,
    stripe_subscription_id: str,
    subscription_type: str,
    tier: Optional[str],
    addon_key: Optional[str],
    price_id: Optional[str],
    status: Optional[str],
    current_period_start: Optional[Any],
    current_period_end: Optional[Any],
) -> None:
    """Upsert account_subscriptions row for a Stripe subscription."""
    def _coerce_ts(value: Optional[Any]) -> Optional[datetime]:
        if value is None:
            return None
        if isinstance(value, datetime):
            return value
        try:
            return datetime.fromtimestamp(int(value), tz=timezone.utc)
        except Exception:
            return None

    cps_dt = _coerce_ts(current_period_start)
    cpe_dt = _coerce_ts(current_period_end)

    db.execute(
        text(
            """
            INSERT INTO account_subscriptions (
                id,
                account_id,
                stripe_subscription_id,
                stripe_customer_id,
                subscription_type,
                tier,
                addon_key,
                price_id,
                status,
                current_period_start,
                current_period_end,
                created_at,
                updated_at
            )
            VALUES (
                :id,
                :account_id,
                :stripe_subscription_id,
                :stripe_customer_id,
                :subscription_type,
                :tier,
                :addon_key,
                :price_id,
                :status,
                :current_period_start,
                :current_period_end,
                CURRENT_TIMESTAMP,
                CURRENT_TIMESTAMP
            )
            ON CONFLICT (stripe_subscription_id) DO UPDATE SET
                account_id = :account_id,
                stripe_customer_id = :stripe_customer_id,
                subscription_type = :subscription_type,
                tier = :tier,
                addon_key = :addon_key,
                price_id = :price_id,
                status = :status,
                current_period_start = :current_period_start,
                current_period_end = :current_period_end,
                updated_at = CURRENT_TIMESTAMP
            """
        ),
        {
            "id": str(uuid4()),
            "account_id": str(account_id),
            "stripe_subscription_id": stripe_subscription_id,
            "stripe_customer_id": stripe_customer_id,
            "subscription_type": subscription_type,
            "tier": tier,
            "addon_key": addon_key,
            "price_id": price_id,
            "status": status or "unknown",
            "current_period_start": cps_dt,
            "current_period_end": cpe_dt,
        },
    )


def _fetch_active_account_subscriptions(
    db: Session, account_id: str
) -> List[Dict[str, Any]]:
    """Load active account_subscriptions rows for an account."""
    statuses = sorted(_ACTIVE_SUBSCRIPTION_STATUSES)
    stmt = (
        text(
            """
            SELECT stripe_subscription_id,
                   subscription_type,
                   tier,
                   addon_key,
                   price_id,
                   status
            FROM account_subscriptions
            WHERE account_id = :aid
              AND status IN :statuses
            """
        )
        .bindparams(bindparam("statuses", expanding=True))
    )
    rows = db.execute(stmt, {"aid": str(account_id), "statuses": statuses}).mappings()
    return list(rows.all())


def _aggregate_entitlements_from_rows(
    rows: List[Dict[str, Any]],
) -> Tuple[str, List[str], Optional[str], Optional[str]]:
    tier = "free"
    best_rank = _tier_rank(tier)
    primary_subscription_id = None
    primary_status = None
    addons: Set[str] = set()

    for row in rows:
        row_tier = _normalize_tier(row.get("tier"))
        if row.get("subscription_type") == "tier" and row_tier:
            rank = _tier_rank(row_tier)
            if rank > best_rank:
                best_rank = rank
                tier = row_tier
                primary_subscription_id = row.get("stripe_subscription_id")
                primary_status = row.get("status")
        addon_value = row.get("addon_key")
        if addon_value:
            for entry in str(addon_value).split(","):
                entry = entry.strip()
                if entry:
                    addons.add(entry)

    return tier, sorted(addons), primary_subscription_id, primary_status


def _derive_entitlements_from_subscriptions(
    subscriptions: Iterable[Dict[str, Any]],
) -> Tuple[str, List[str], Optional[str], Optional[str]]:
    tier = "free"
    best_rank = _tier_rank(tier)
    primary_subscription_id = None
    primary_status = None
    addons: Set[str] = set()

    for subscription in subscriptions:
        sub_tier, _interval, _price_id = _extract_tier_from_subscription(subscription)
        sub_tier = _normalize_tier(sub_tier) if sub_tier else None
        sub_addons = _extract_addons_from_subscription(subscription)
        for addon in sub_addons:
            addons.add(addon)

        if sub_tier:
            rank = _tier_rank(sub_tier)
            if rank > best_rank:
                best_rank = rank
                tier = sub_tier
                primary_subscription_id = subscription.get("id")
                primary_status = subscription.get("status")

    return tier, sorted(addons), primary_subscription_id, primary_status


def _fetch_active_subscriptions_from_stripe(
    stripe_customer_id: str,
) -> Tuple[List[Dict[str, Any]], bool]:
    """Fetch active Stripe subscriptions for a customer."""
    stripe = get_stripe_client()
    active_subs: List[Dict[str, Any]] = []
    fetch_failed = False
    timeout_value = os.getenv("STRIPE_SUBSCRIPTION_FETCH_TIMEOUT", "").strip()
    try:
        timeout = float(timeout_value) if timeout_value else _STRIPE_FETCH_TIMEOUT_SECONDS
    except ValueError:
        timeout = _STRIPE_FETCH_TIMEOUT_SECONDS
    try:
        subs = stripe.Subscription.list(
            customer=stripe_customer_id,
            status="all",
            limit=100,
            expand=["data.items.data.price"],
            timeout=timeout,
        )
        for sub in subs.auto_paging_iter():
            status = sub.get("status") if isinstance(sub, dict) else getattr(sub, "status", None)
            if _is_active_subscription_status(status):
                active_subs.append(sub)
    except Exception as exc:
        fetch_failed = True
        logger.warning(
            "[STRIPE WEBHOOK] Failed to fetch active subscriptions for %s: %s",
            stripe_customer_id,
            exc,
        )
    return active_subs, fetch_failed


def _calculate_credit_balance(db: Session, account_id: str) -> int:
    """Return the sum of purchased + active bucket credits for the account."""
    bucket_remaining = (
        db.execute(
            text(
                """
            SELECT COALESCE(SUM(quantity_remaining), 0)
            FROM enrichment_credit_buckets
            WHERE account_id = :account_id
              AND status = 'active'
            """
            ),
            {"account_id": account_id},
        ).scalar()
        or 0
    )

    purchased = (
        db.execute(
            text(
                "SELECT COALESCE(purchased_credits, 0) FROM accounts WHERE id=:account_id"
            ),
            {"account_id": account_id},
        ).scalar()
        or 0
    )

    return int(bucket_remaining) + int(purchased)


def _detect_credit_pack_purchase(price: Dict[str, Any]) -> Tuple[bool, int]:
    """Determine whether the given Stripe price represents a credit pack purchase."""
    metadata = price.get("metadata") or {}
    for key in ("credits", "enrichment_credits", "pack_credits"):
        value = metadata.get(key)
        if value is None:
            continue
        try:
            credits = int(value)
            if credits > 0:
                return True, credits
        except (TypeError, ValueError):
            continue

    lookup_key = (price.get("lookup_key") or "").lower()
    nickname = (price.get("nickname") or "").lower()
    if "credit" in lookup_key or "credit" in nickname:
        default_credits = metadata.get("default_credits")
        try:
            credits = int(default_credits)
            if credits > 0:
                return True, credits
        except (TypeError, ValueError):
            pass

    # Fallback: parse nickname/lookup for patterns like "100 enrichments" or "100 credits"
    import re

    for text in (nickname, lookup_key):
        if not text:
            continue
        m = re.search(r"(\\d+)\\s*(enrich|credit)", text)
        if m:
            try:
                credits = int(m.group(1))
                if credits > 0:
                    return True, credits
            except (TypeError, ValueError):
                continue

    return False, 0


@router.post("/stripe")
async def stripe_webhook(request: Request):
    """
    Handle Stripe webhooks for billing events.

    Events handled:
    - invoice.created
    - invoice.finalized
    - invoice.paid
    - payment_intent.succeeded
    - customer.subscription.created
    - customer.subscription.updated
    """
    payload = await request.body()
    sig = request.headers.get("stripe-signature")

    stripe = get_stripe_client()
    webhook_secret = os.getenv("STRIPE_WEBHOOK_SECRET")

    if not webhook_secret:
        logger.error(
            "[STRIPE WEBHOOK] STRIPE_WEBHOOK_SECRET not configured; set env var for signature verification."
        )
        raise HTTPException(
            500, "Stripe webhook secret not configured. Set STRIPE_WEBHOOK_SECRET."
        )

    try:
        evt = stripe.Webhook.construct_event(payload, sig, webhook_secret)
    except stripe.error.SignatureVerificationError as e:  # type: ignore[attr-defined]
        logger.error(f"[STRIPE WEBHOOK] Signature verification failed: {e}")
        raise HTTPException(400, f"Webhook error: {e}")
    except Exception as e:
        logger.error(f"[STRIPE WEBHOOK] Webhook construction failed: {e}")
        raise HTTPException(400, f"Webhook error: {e}")

    typ = evt["type"]
    data = evt["data"]["object"]

    logger.info(f"[STRIPE WEBHOOK] Received event: {typ}")

    headers_map = {k: v for k, v in request.headers.items()}

    db = get_db_connection()
    audit_id = None
    account_id_for_event: Optional[str] = None
    subscription_id_for_event: Optional[str] = None
    event_created = _event_created_at(evt)

    try:
        _ensure_webhook_schema(db)

        if _has_processed_stripe_event(db, evt["id"]):
            logger.info(
                "[STRIPE WEBHOOK] Event %s already processed - skipping", evt["id"]
            )
            return {"ok": True, "duplicate": True}

        existing_audit = _get_webhook_audit_row(db, evt["id"])
        previous_attempts = existing_audit[2] if existing_audit else 0
        audit_id = _record_webhook_attempt(db, evt, headers_map, previous_attempts or 0)

        final_status = "processed"

        if typ in ("invoice.created", "invoice.finalized", "invoice.paid"):
            account_id_for_event = await _handle_invoice_event(db, data, typ, evt)
        elif typ == "payment_intent.succeeded":
            account_id_for_event = _handle_payment_event(db, data)
        elif typ in (
            "customer.subscription.created",
            "customer.subscription.updated",
            "customer.subscription.deleted",
        ):
            subscription_id_for_event = data.get("id")
            if _is_out_of_order_subscription_event(
                db, subscription_id_for_event, event_created
            ):
                logger.info(
                    "[STRIPE WEBHOOK] Out-of-order subscription event %s for %s; skipping",
                    evt["id"],
                    subscription_id_for_event,
                )
                final_status = "ignored"
            else:
                account_id_for_event = _handle_subscription_event(db, data, typ, evt)
        elif typ == "checkout.session.completed":
            account_id_for_event = await _handle_checkout_session_completed(db, data, evt)
        else:
            logger.info(f"[STRIPE WEBHOOK] Unhandled event type: {typ}")
            final_status = "ignored"

        _mark_stripe_event_processed(
            db,
            evt["id"],
            typ,
            account_id=account_id_for_event,
            subscription_id=subscription_id_for_event,
            event_created=event_created,
        )
        _update_webhook_audit_status(db, audit_id, final_status, None)
        db.commit()
        response = {"ok": True}
        if final_status == "ignored":
            response["ignored"] = True
        return response

    except Exception as e:
        logger.error(
            f"[STRIPE WEBHOOK] Error processing event {typ}: {e}", exc_info=True
        )
        db.rollback()
        raise HTTPException(500, f"Failed to process webhook: {e}")

    finally:
        db.close()


@legacy_router.post("/stripe")
async def stripe_webhook_v1(request: Request):
    """Backward-compatible Stripe webhook endpoint (/api/v1/webhooks/stripe)."""
    return await stripe_webhook(request)


async def _handle_invoice_event(
    db: Session, data: dict, event_type: str, evt: Optional[dict] = None
) -> Optional[str]:
    """Handle invoice.* events."""
    stripe_invoice_id = data["id"]
    stripe_customer_id = data["customer"]

    # Find account by Stripe customer ID (with metadata fallback)
    account_id = _get_account_from_stripe_customer(db, stripe_customer_id)

    if not account_id:
        logger.warning(f"Account not found for Stripe customer: {stripe_customer_id}")
        return None

    logger.info(
        "[STRIPE WEBHOOK] Invoice event %s for account %s (invoice %s, status=%s)",
        event_type,
        account_id,
        stripe_invoice_id,
        data.get("status"),
    )

    # Parse dates
    issued_at = _parse_timestamp(data.get("created"))
    if not issued_at:
        issued_at = _parse_timestamp(
            data.get("status_transitions", {}).get("finalized_at")
        )

    status = data.get("status")
    amount_due = data.get("amount_due", 0)
    amount_paid = data.get("amount_paid", 0)
    due_date = _parse_timestamp(data.get("due_date"))
    paid_at = _parse_timestamp(data.get("status_transitions", {}).get("paid_at"))

    invoice_pk = None
    if _is_uuid_like(account_id):
        inv = db.execute(
            text("SELECT id FROM invoice WHERE stripe_invoice_id=:sid"),
            {"sid": stripe_invoice_id},
        ).first()

        if inv:
            invoice_pk = inv[0]
            # Update existing invoice
            db.execute(
                text("""
                    UPDATE invoice
                    SET amount_due_cents=:due, amount_paid_cents=:paid, status=:st,
                        due_date=:due_date, paid_at=:paid_at, updated_at=now()
                    WHERE id=:id
                """),
                {
                    "due": amount_due,
                    "paid": amount_paid,
                    "st": status,
                    "due_date": due_date,
                    "paid_at": paid_at,
                    "id": invoice_pk,
                },
            )
            logger.info(f"Updated invoice {stripe_invoice_id}")
        else:
            # Create new invoice
            db.execute(
                text("""
                    INSERT INTO invoice (
                        id, account_id, stripe_invoice_id, amount_due_cents,
                        amount_paid_cents, status, issued_at, due_date, paid_at, currency
                    )
                    VALUES (
                        gen_random_uuid(), :aid, :sid, :due, :paid, :st,
                        to_timestamp(:created), :due_date, :paid_at, :currency
                    )
                """),
                {
                    "aid": str(account_id),
                    "sid": stripe_invoice_id,
                    "due": amount_due,
                    "paid": amount_paid,
                    "st": status,
                    "created": data["created"],
                    "due_date": due_date,
                    "paid_at": paid_at,
                    "currency": data.get("currency", "usd"),
                },
            )
            logger.info(f"Created invoice {stripe_invoice_id}")
    else:
        logger.warning(
            "[STRIPE WEBHOOK] Skipping admin invoice write (account_id not UUID): %s",
            account_id,
        )

    # 🎯 NEW: Mint monthly enrichment credits when invoice paid
    minted_bucket = None
    if event_type == "invoice.paid" and status == "paid":
        minted_bucket = await _mint_enrichment_credits_for_invoice(
            db, account_id, data, stripe_invoice_id
        )
        if minted_bucket:
            db.add(
                CreditTransaction(
                    account_id=str(account_id),
                    source="tier",
                    event_type="issue",
                    quantity=minted_bucket.quantity_total,
                    balance_after=_calculate_credit_balance(db, str(account_id)),
                    stripe_invoice_id=stripe_invoice_id,
                    stripe_customer_id=stripe_customer_id,
                    details={
                        "bucket_id": minted_bucket.id,
                        "tier": minted_bucket.notes,
                        "event": event_type,
                    },
                )
            )
            logger.info(
                "[ENRICHMENT] Minted %s credits for invoice %s (bucket_id=%s)",
                minted_bucket.quantity_total,
                stripe_invoice_id,
                minted_bucket.id,
            )
        else:
            logger.warning(
                "[ENRICHMENT] Skipped minting credits for invoice %s (no bucket returned)",
                stripe_invoice_id,
            )

        entitlements = _upsert_usage_quota(
            db,
            account_id,
            _get_existing_quota_tier(db, account_id) or "free",
            "active",
            source=event_type,
            reset_usage=True,
        )
        logger.info(
            "[STRIPE WEBHOOK] %s customer=%s subscription=%s reset usage counters for account %s -> tier=%s match=%s enrich=%s",
            event_type,
            stripe_customer_id,
            data.get("subscription"),
            account_id,
            entitlements["tier"],
            entitlements["monthly_match_entitlement"],
            entitlements["monthly_enrichment_entitlement"],
        )

    # Update invoice lines only when an invoice row exists
    if invoice_pk:
        db.execute(
            text("DELETE FROM invoice_line WHERE invoice_id=:inv_id"),
            {"inv_id": invoice_pk},
        )

        for line in data.get("lines", {}).get("data", []):
            price = line.get("price") or {}

            # Try to find plan by stripe_price_id
            plan_row = db.execute(
                text("SELECT id, product_id FROM plan WHERE stripe_price_id=:pid"),
                {"pid": price.get("id")},
            ).first()

            plan_id = plan_row[0] if plan_row else None
            product_id = plan_row[1] if plan_row else None

            db.execute(
                text("""
                    INSERT INTO invoice_line (
                        id, invoice_id, product_id, plan_id, stripe_price_id,
                        description, quantity, unit_amount_cents, amount_cents, type,
                        period_start, period_end
                    )
                    VALUES (
                        gen_random_uuid(),
                        :inv_id,
                        :pid2, :plan, :price_id, :desc, :qty, :unit, :amount, :typ,
                        to_timestamp(:ps), to_timestamp(:pe)
                    )
                """),
                {
                    "inv_id": invoice_pk,
                    "pid2": product_id,
                    "plan": plan_id,
                    "price_id": price.get("id"),
                    "desc": line.get("description"),
                    "qty": line.get("quantity") or 1,
                    "unit": price.get("unit_amount") or 0,
                    "amount": line.get("amount") or 0,
                    "typ": line.get("type") or "recurring",
                    "ps": line.get("period", {}).get("start") or data["created"],
                    "pe": line.get("period", {}).get("end") or data["created"],
                },
            )

            is_credit_pack, credits_per_unit = _detect_credit_pack_purchase(price)
            if (
                event_type == "invoice.paid"
                and status == "paid"
                and is_credit_pack
                and credits_per_unit > 0
            ):
                quantity = line.get("quantity") or 0
                total_credits = credits_per_unit * quantity
                if total_credits > 0:
                    db.execute(
                        text(
                            """
                            UPDATE accounts
                            SET purchased_credits = COALESCE(purchased_credits, 0) + :credits
                            WHERE id=:account_id
                            """
                        ),
                        {"credits": total_credits, "account_id": str(account_id)},
                    )
                    db.add(
                        CreditTransaction(
                            account_id=str(account_id),
                            source="pack",
                            event_type="issue",
                            quantity=total_credits,
                            balance_after=_calculate_credit_balance(db, str(account_id)),
                            stripe_invoice_id=stripe_invoice_id,
                            stripe_customer_id=stripe_customer_id,
                            details={
                                "price_id": price.get("id"),
                                "line_item_id": line.get("id"),
                                "invoice_id": stripe_invoice_id,
                            },
                        )
                    )
                    logger.info(
                        "[ENRICHMENT] Issued %s prepaid credits (pack) for account %s (invoice %s)",
                        total_credits,
                        account_id,
                        stripe_invoice_id,
                    )
    else:
        logger.warning(
            "[STRIPE WEBHOOK] Skipping invoice_line write; no invoice row for %s (account_id=%s)",
            stripe_invoice_id,
            account_id,
        )
    return str(account_id)

def _handle_payment_event(db: Session, data: dict) -> Optional[str]:
    """Handle payment_intent.succeeded events."""
    stripe_customer_id = data.get("customer")

    if not stripe_customer_id:
        logger.warning("Payment intent has no customer ID")
        return None

    # Find account by Stripe customer ID (with metadata fallback)
    account_id = _get_account_from_stripe_customer(db, stripe_customer_id)

    if not account_id:
        logger.warning(f"Account not found for Stripe customer: {stripe_customer_id}")
        return None

    # Insert payment record (admin DB expects UUID account_id)
    if _is_uuid_like(account_id):
        db.execute(
            text("""
                INSERT INTO payment (
                    id, account_id, invoice_id, stripe_payment_intent_id,
                    amount_cents, currency, status, created_at, captured_at
                )
                VALUES (
                    gen_random_uuid(), :aid,
                    (SELECT id FROM invoice WHERE stripe_invoice_id=:inv LIMIT 1),
                    :pi, :amount, :cur, 'succeeded',
                    to_timestamp(:created), to_timestamp(:created)
                )
                ON CONFLICT (stripe_payment_intent_id) DO NOTHING
            """),
            {
                "aid": str(account_id),
                "inv": data.get("invoice"),
                "pi": data["id"],
                "amount": data.get("amount_received") or data.get("amount") or 0,
                "cur": data.get("currency", "usd"),
                "created": data.get("created", 0),
            },
        )
        logger.info(f"Recorded payment {data['id']}")
    else:
        logger.warning(
            "[STRIPE WEBHOOK] Skipping admin payment write (account_id not UUID): %s",
            account_id,
        )
    return str(account_id)


def _handle_subscription_event(
    db: Session, data: dict, event_type: str, evt: Optional[dict] = None
) -> Optional[str]:
    """Handle customer.subscription.* events."""
    stripe_sub_id = data["id"]
    stripe_cust = data["customer"]

    # Find account by Stripe customer ID (with metadata fallback)
    account_id = _get_account_from_stripe_customer(db, stripe_cust)

    if not account_id:
        logger.warning(f"Account not found for Stripe customer: {stripe_cust}")
        return None

    status = data.get("status", "incomplete")
    cps = data.get("current_period_start")
    cpe = data.get("current_period_end")
    trial_end = data.get("trial_end")
    occurred_at = _parse_timestamp((evt or {}).get("created")) or datetime.now(
        timezone.utc
    )

    base_qty, addon_qty, seat_total, stripe_price_id, seat_item_ids = (
        _extract_subscription_item_snapshot(data)
    )
    tier, billing_interval, price_id_from_metadata = _extract_tier_from_subscription(
        data
    )
    stripe_price_id = stripe_price_id or price_id_from_metadata
    addons = _extract_addons_from_subscription(data)

    subscription_status = status
    if event_type == "customer.subscription.deleted":
        subscription_status = "canceled"
        status = "canceled"

    subscription_type, addon_key = _classify_subscription(tier, addons)
    _upsert_account_subscription(
        db,
        account_id=str(account_id),
        stripe_customer_id=stripe_cust,
        stripe_subscription_id=stripe_sub_id,
        subscription_type=subscription_type,
        tier=_normalize_tier(tier) if tier else None,
        addon_key=addon_key,
        price_id=stripe_price_id,
        status=subscription_status,
        current_period_start=cps,
        current_period_end=cpe,
    )

    _update_account_refs(
        db,
        account_id,
        stripe_customer_id=stripe_cust,
    )

    primary_subscription_id = None
    primary_status = None
    if event_type == "customer.subscription.deleted":
        active_subs, fetch_failed = _fetch_active_subscriptions_from_stripe(stripe_cust)
        if fetch_failed:
            logger.warning(
                "[STRIPE WEBHOOK] Falling back to account_subscriptions after Stripe fetch failure (customer=%s)",
                stripe_cust,
            )
            rows = _fetch_active_account_subscriptions(db, account_id)
            tier, addons, primary_subscription_id, primary_status = (
                _aggregate_entitlements_from_rows(rows)
            )
        else:
            if active_subs:
                for sub in active_subs:
                    sub_tier, _interval, sub_price_id = _extract_tier_from_subscription(
                        sub
                    )
                    sub_addons = _extract_addons_from_subscription(sub)
                    sub_type, sub_addon_key = _classify_subscription(
                        sub_tier, sub_addons
                    )
                    _upsert_account_subscription(
                        db,
                        account_id=str(account_id),
                        stripe_customer_id=stripe_cust,
                        stripe_subscription_id=sub.get("id"),
                        subscription_type=sub_type,
                        tier=_normalize_tier(sub_tier) if sub_tier else None,
                        addon_key=sub_addon_key,
                        price_id=sub_price_id,
                        status=sub.get("status"),
                        current_period_start=sub.get("current_period_start"),
                        current_period_end=sub.get("current_period_end"),
                    )
            else:
                logger.info(
                    "[STRIPE WEBHOOK] Stripe reports no active subscriptions for customer=%s after deletion",
                    stripe_cust,
                )
            tier, addons, primary_subscription_id, primary_status = (
                _derive_entitlements_from_subscriptions(active_subs)
            )
    else:
        rows = _fetch_active_account_subscriptions(db, account_id)
        tier, addons, primary_subscription_id, primary_status = (
            _aggregate_entitlements_from_rows(rows)
        )

    # Look up plan by stripe_price_id
    plan_id = None
    if stripe_price_id:
        plan_row = db.execute(
            text("SELECT id FROM plan WHERE stripe_price_id=:pid"),
            {"pid": stripe_price_id},
        ).first()
        if plan_row:
            plan_id = str(plan_row[0])
            logger.info(
                f"[STRIPE WEBHOOK] Linked subscription to plan {plan_id} via price {stripe_price_id}"
            )
        else:
            logger.warning(
                f"[STRIPE WEBHOOK] No plan found for stripe_price_id={stripe_price_id}"
            )

    # Check if subscription exists
    existing = db.execute(
        text(
            "SELECT id, seat_count, status FROM subscription WHERE stripe_subscription_id=:sid"
        ),
        {"sid": stripe_sub_id},
    ).first()

    previous_seat_count = existing[1] if existing else 0
    previous_status = existing[2] if existing else None

    # Determine new seat totals (treat deletion as zero seats)
    effective_seat_total = seat_total
    if event_type == "customer.subscription.deleted":
        effective_seat_total = 0

    seat_delta = None
    if effective_seat_total is not None:
        seat_delta = effective_seat_total - (previous_seat_count or 0)

    qty_value = effective_seat_total if effective_seat_total is not None else base_qty

    # Admin subscription table expects UUID account_id; skip if not UUID-like
    if _is_uuid_like(account_id):
        if existing:
            db.execute(
                text("""
                    UPDATE subscription
                    SET status=:st,
                        current_period_start=to_timestamp(:cps),
                        current_period_end=to_timestamp(:cpe),
                        trial_end=to_timestamp(:te),
                        seat_count=COALESCE(:qty, seat_count),
                        plan_id=:plan_id,
                        updated_at=now()
                    WHERE stripe_subscription_id=:sid
                """),
                {
                    "st": status,
                    "cps": cps or 0,
                    "cpe": cpe or 0,
                    "te": trial_end or 0,
                    "qty": qty_value,
                    "plan_id": plan_id,
                    "sid": stripe_sub_id,
                },
            )
            logger.info(f"Updated subscription {stripe_sub_id} with plan {plan_id}")
        else:
            db.execute(
                text("""
                    INSERT INTO subscription (
                        id, account_id, product_id, plan_id, stripe_subscription_id,
                        status, current_period_start, current_period_end,
                        trial_end, seat_count, created_at, updated_at
                    )
                    VALUES (
                        gen_random_uuid(), :aid,
                        (SELECT id FROM product WHERE key='web' LIMIT 1),
                        :plan_id, :sid, :st, to_timestamp(:cps), to_timestamp(:cpe),
                        to_timestamp(:te), COALESCE(:qty, 1), NOW(), NOW()
                    )
                """),
                {
                    "aid": str(account_id),
                    "plan_id": plan_id,
                    "sid": stripe_sub_id,
                    "st": status,
                    "cps": cps or 0,
                    "cpe": cpe or 0,
                    "te": trial_end or 0,
                    "qty": qty_value or 1,
                },
            )
            logger.info(f"Created subscription {stripe_sub_id} linked to plan {plan_id}")
    else:
        logger.warning(
            "[STRIPE WEBHOOK] Skipping admin subscription write (account_id not UUID): %s",
            account_id,
        )

    if effective_seat_total is not None:
        db.execute(
            text(
                """
                UPDATE accounts
                SET paid_seat_count = GREATEST(:seats, 1)
                WHERE id = :aid
                """
            ),
            {"seats": max(effective_seat_total, 0), "aid": str(account_id)},
        )

    plan_details = None
    if plan_id:
        plan_details = db.execute(
            text("""
                SELECT p.name, p.price_cents, p.billing_interval, prod.key
                FROM plan p
                JOIN product prod ON p.product_id = prod.id
                WHERE p.id = :plan_id
            """),
            {"plan_id": plan_id},
        ).first()
        if plan_details:
            if not tier:
                tier = _infer_tier_from_text(plan_details[0]) or tier
            billing_interval = billing_interval or plan_details[2]

    _set_primary_subscription(db, account_id, primary_subscription_id)

    quota_status = primary_status or status or "unknown"
    if tier == "free" and not primary_status:
        if event_type == "customer.subscription.deleted":
            quota_status = "canceled"

    entitlements = _upsert_usage_quota(
        db,
        account_id,
        tier or "free",
        quota_status or "unknown",
        source=event_type,
        reset_usage=False,
        addons=addons,
    )

    # Set enrichment rate limits (paid tiers only)
    # Note: Unleashed has BYO bundled with unlimited enrichment (set high limits)
    final_tier = (tier or "free").lower()
    paid_tiers = ("pro", "scale", "unleashed")
    if final_tier == "unleashed":
        daily_limit = 50000  # High limit for BYO bundled
        monthly_limit = None  # Unlimited
    elif final_tier in ("pro", "scale"):
        daily_limit = 500
        monthly_limit = 2000
    else:
        daily_limit = 0
        monthly_limit = 0

    db.execute(
        text("""
            INSERT INTO enrichment_quota (account_id, daily_limit_credits, monthly_limit_credits, enabled)
            VALUES (:aid, :daily, :monthly, :enabled)
            ON CONFLICT (account_id) DO UPDATE SET
                daily_limit_credits = :daily,
                monthly_limit_credits = :monthly,
                enabled = :enabled
        """),
        {
            "aid": str(account_id),
            "daily": daily_limit,
            "monthly": monthly_limit,
            "enabled": final_tier in paid_tiers,
        },
    )
    logger.info(
        "[STRIPE WEBHOOK] Set enrichment rate limits for account %s: daily=%s monthly=%s enabled=%s",
        account_id,
        daily_limit,
        monthly_limit,
        final_tier in paid_tiers,
    )

    logger.info(
        "[STRIPE WEBHOOK] %s customer=%s subscription=%s tier=%s status=%s interval=%s",
        event_type,
        stripe_cust,
        stripe_sub_id,
        entitlements["tier"],
        quota_status,
        billing_interval or "n/a",
    )

    # Record subscription event for audit trail
    db.add(
        SubscriptionEvent(
            account_id=str(account_id),
            stripe_customer_id=stripe_cust,
            stripe_subscription_id=stripe_sub_id,
            plan_id=plan_id,
            event_type=event_type,
            previous_status=previous_status,
            status=status,
            seat_delta=seat_delta,
            seat_total=effective_seat_total,
            occurred_at=occurred_at,
            event_metadata={
                "base_quantity": base_qty,
                "seat_addon_quantity": addon_qty,
                "seat_item_ids": list(seat_item_ids),
                "raw_status": status,
            },
        )
    )

    if seat_delta and seat_delta != 0:
        db.add(
            SeatTransaction(
                account_id=str(account_id),
                user_id=None,
                quantity_delta=seat_delta,
                seat_total=effective_seat_total,
                reason=(
                    "subscription_deleted"
                    if event_type.endswith("deleted")
                    else "subscription_updated"
                    if event_type.endswith("updated")
                    else "subscription_created"
                ),
                stripe_customer_id=stripe_cust,
                stripe_subscription_id=stripe_sub_id,
                stripe_subscription_item_id=seat_item_ids[0] if seat_item_ids else None,
                stripe_invoice_id=data.get("latest_invoice"),
                recorded_at=datetime.now(timezone.utc),
                details={
                    "base_quantity": base_qty,
                    "addon_quantity": addon_qty,
                    "event_type": event_type,
                },
            )
        )

    return str(account_id)


async def _handle_checkout_session_completed(
    db: Session, data: dict, evt: Optional[dict] = None
) -> Optional[str]:
    """Handle checkout.session.completed events for subscriptions and credit packs."""
    stripe_customer_id = data.get("customer")
    session_metadata = data.get("metadata") or {}
    if not stripe_customer_id:
        logger.warning("[STRIPE WEBHOOK] Checkout session has no customer ID")
        return None

    account_id = _get_account_from_stripe_customer(db, stripe_customer_id)
    if not account_id:
        account_id = _find_account_from_metadata(db, session_metadata)
        if account_id:
            _update_account_refs(
                db, account_id, stripe_customer_id=stripe_customer_id
            )

    if not account_id:
        logger.warning(
            f"[STRIPE WEBHOOK] Account not found for Stripe customer: {stripe_customer_id}"
        )
        return None

    payment_status = data.get("payment_status") or data.get("status")
    if payment_status not in ("paid", "complete"):
        logger.info(
            f"[STRIPE WEBHOOK] Checkout session {data.get('id')} not paid yet (status={payment_status}), skipping"
        )
        return None

    session_id = data.get("id")
    subscription_id = data.get("subscription")
    tier_hint = session_metadata.get("tier") or session_metadata.get("plan")
    interval_hint = session_metadata.get("interval") or session_metadata.get(
        "billing_interval"
    )

    stripe = get_stripe_client()
    try:
        line_items = stripe.checkout.Session.list_line_items(
            session_id,
            limit=100,
            expand=["data.price"],
        )
    except Exception as exc:
        logger.error(
            f"[STRIPE WEBHOOK] Failed to load line items for checkout session {session_id}: {exc}",
            exc_info=True,
        )
        return None

    tier_from_lines, interval_from_lines, _price_id = _extract_tier_from_line_items(
        line_items
    )
    tier_candidate = _normalize_tier(tier_hint or tier_from_lines) if (tier_hint or tier_from_lines) else None
    tier = _normalize_tier(tier_candidate or "free")
    interval = interval_hint or interval_from_lines

    _update_account_refs(db, account_id, stripe_customer_id=stripe_customer_id)

    if subscription_id:
        # Subscription checkout: do NOT update tier here; subscription events handle entitlements.
        if tier_candidate and tier_candidate != "free":
            _set_primary_subscription(db, account_id, subscription_id)
        logger.info(
            "[STRIPE WEBHOOK] checkout.session.completed customer=%s subscription=%s (skipping tier update - handled by subscription events)",
            stripe_customer_id,
            subscription_id,
        )

    total_added = 0
    for line in line_items.get("data", []):
        price = line.get("price") or {}

        from ..config.enrichment import ENRICHMENT_ENABLED
        if not ENRICHMENT_ENABLED:
            logger.info("[ENRICHMENT] Skipping credit pack processing - enrichment disabled")
            continue

        is_pack, credits_per_unit = _detect_credit_pack_purchase(price)
        if not is_pack or credits_per_unit <= 0:
            continue
        quantity = line.get("quantity") or 0
        total_credits = credits_per_unit * quantity
        if total_credits <= 0:
            continue

        # Create enrichment credit bucket (new system)
        from datetime import datetime, timezone
        from sqlalchemy import text
        import json

        # Purchased credits expire in 180 days
        issued_at = datetime.now(timezone.utc)
        valid_from = issued_at.date()
        expires_at_date = (issued_at + timedelta(days=180)).date()

        # Insert bucket using raw SQL (models don't match schema)
        result = db.execute(
            text("""
                INSERT INTO enrichment_credit_buckets
                (account_id, source, source_ref, issued_at, valid_from, valid_to, expires_at,
                 quantity_total, quantity_remaining, status, notes, created_at)
                VALUES
                (:account_id, 'pack', :source_ref, :issued_at, :valid_from, :valid_to, :expires_at,
                 :quantity, :quantity, 'active', :notes, NOW())
                RETURNING id
            """),
            {
                "account_id": str(account_id),
                "source_ref": data.get("invoice"),
                "issued_at": issued_at,
                "valid_from": valid_from,
                "valid_to": expires_at_date,
                "expires_at": expires_at_date,
                "quantity": total_credits,
                "notes": f"Purchased credit pack - {total_credits} enrichments",
            },
        )
        bucket_id = result.fetchone()[0]

        # Create ledger entry
        metadata = {
            "session_id": session_id,
            "price_id": price.get("id"),
            "line_item_id": line.get("id"),
            "quantity": quantity,
            "credits_per_unit": credits_per_unit,
        }

        db.execute(
            text("""
                INSERT INTO enrichment_credit_ledger
                (account_id, bucket_id, kind, quantity, metadata, notes, occurred_at)
                VALUES
                (:account_id, :bucket_id, 'issue', :quantity, :metadata, :notes, NOW())
            """),
            {
                "account_id": str(account_id),
                "bucket_id": bucket_id,
                "quantity": total_credits,
                "metadata": json.dumps(metadata),
                "notes": f"Credit pack purchase via Stripe checkout {session_id}",
            },
        )

        total_added += total_credits

    if total_added:
        logger.info(
            f"[STRIPE WEBHOOK] Account {account_id} purchased {total_added} credits via checkout session {session_id}"
        )
    else:
        logger.info(
            f"[STRIPE WEBHOOK] Checkout session {session_id} contained no recognizable credit packs"
        )
    return str(account_id)


async def _mint_enrichment_credits_for_invoice(
    db: Session, account_id: str, invoice_data: dict, stripe_invoice_id: str
):
    """
    Mint monthly enrichment credits when invoice is paid.

    Determines tier from subscription metadata and mints appropriate bucket.
    """
    from ..config.enrichment import ENRICHMENT_ENABLED
    if not ENRICHMENT_ENABLED:
        logger.info("[ENRICHMENT] Skipping credit minting - enrichment disabled")
        return None

    try:
        # Get subscription from invoice
        stripe = get_stripe_client()

        subscription_id = invoice_data.get("subscription")
        invoice_obj = None

        # If the invoice payload doesn't include subscription, fetch the invoice from Stripe
        if not subscription_id:
            try:
                invoice_obj = stripe.Invoice.retrieve(
                    stripe_invoice_id,
                    expand=["subscription", "lines.data.price"],
                )
                subscription_ref = getattr(invoice_obj, "subscription", None)
                if isinstance(subscription_ref, str):
                    subscription_id = subscription_ref
                elif subscription_ref is not None:
                    subscription_id = getattr(subscription_ref, "id", None)
                else:
                    subscription_id = invoice_obj.get("subscription")
            except Exception as e:
                logger.warning(
                    f"[ENRICHMENT] Invoice {stripe_invoice_id} missing subscription and retrieval failed: {e}"
                )

        if not subscription_id:
            # Fallback: check if account has a subscription ID stored
            try:
                acct = db.execute(
                    text("SELECT stripe_subscription_id FROM accounts WHERE id=:aid"),
                    {"aid": str(account_id)},
                ).first()
                if acct and acct[0]:
                    subscription_id = acct[0]
                    logger.info(
                        f"[ENRICHMENT] Retrieved subscription {subscription_id} from account record"
                    )
            except Exception as e:
                logger.warning(f"[ENRICHMENT] Failed to lookup account subscription: {e}")

            if not subscription_id:
                logger.info(
                    f"[ENRICHMENT] Invoice {stripe_invoice_id} has no subscription reference, skipping mint"
                )
                return

        # Get subscription details from Stripe (prefer expanded invoice.subscription if available)
        try:
            if invoice_obj and getattr(invoice_obj, "subscription", None):
                subscription = invoice_obj.subscription
            else:
                subscription = stripe.Subscription.retrieve(
                    subscription_id,
                    expand=["items.data.price"],
                )
        except Exception as e:
            logger.warning(
                f"[ENRICHMENT] Failed to retrieve subscription {subscription_id}: {e}"
            )
            return

        # Extract tier from subscription metadata
        metadata_map = subscription.metadata or {}
        tier = metadata_map.get("tier", "")
        tier = tier.lower() if isinstance(tier, str) else ""
        logger.info(
            "[ENRICHMENT] Inspecting subscription metadata for %s: tier=%s keys=%s",
            account_id,
            tier,
            list(metadata_map.keys()),
        )

        if tier not in ("pro", "scale", "unleashed", "enterprise"):
            # Fallback 1: usage_quotas table
            try:
                quota_row = db.execute(
                    text("SELECT tier FROM usage_quotas WHERE account_id=:aid"),
                    {"aid": str(account_id)},
                ).first()
            except Exception as quota_err:
                logger.warning(
                    f"[ENRICHMENT] Failed to look up usage_quotas for {account_id}: {quota_err}"
                )
                quota_row = None

            if quota_row:
                quota_tier = (quota_row[0] or "").lower()
                if quota_tier in ("pro", "scale", "unleashed", "enterprise"):
                    tier = quota_tier
                    logger.info(
                        "[ENRICHMENT] Falling back to usage_quotas tier=%s for account %s",
                        tier,
                        account_id,
                    )

        if tier not in ("pro", "scale", "unleashed", "enterprise"):
            # Fallback 2: infer from Stripe price / local plan table
            price_id = None
            try:
                price_id = (
                    subscription.get("items", {})
                    .get("data", [{}])[0]
                    .get("price", {})
                    .get("id")
                )
            except Exception:
                price_id = None

            if price_id:
                plan_row = db.execute(
                    text("SELECT name FROM plan WHERE stripe_price_id = :pid"),
                    {"pid": price_id},
                ).first()
                if plan_row:
                    plan_name = (plan_row[0] or "").lower()
                    if "unleashed" in plan_name:
                        tier = "unleashed"
                        logger.info(
                            "[ENRICHMENT] Inferred tier=unleashed via plan lookup for price %s account %s",
                            price_id,
                            account_id,
                        )
                    elif "scale" in plan_name:
                        tier = "scale"
                        logger.info(
                            "[ENRICHMENT] Inferred tier=scale via plan lookup for price %s account %s",
                            price_id,
                            account_id,
                        )
                    elif "pro" in plan_name:
                        tier = "pro"
                        logger.info(
                            "[ENRICHMENT] Inferred tier=pro via plan lookup for price %s account %s",
                            price_id,
                            account_id,
                        )

        if tier not in ("pro", "scale", "unleashed", "enterprise"):
            logger.info(
                f"[ENRICHMENT] Subscription has no valid tier context, got: {tier}"
            )
            return

        # Map "enterprise" to "scale" for bucket manager (which uses TIER_LIMITS from config)
        mint_tier = "scale" if tier == "enterprise" else tier

        # Import the bucket manager lazily to avoid circulars
        from ..services.enrichment_bucket_manager import mint_monthly_tier_credits
        from ..db import async_session_maker

        # Mint credits using the async session
        logger.info(
            f"[ENRICHMENT] Minting {mint_tier} tier credits for account {account_id}"
        )

        try:
            async with async_session_maker() as async_db:
                bucket = await mint_monthly_tier_credits(
                    account_id=account_id,
                    tier=mint_tier,
                    invoice_id=stripe_invoice_id,
                    db=async_db,
                )
            logger.info(
                f"[ENRICHMENT] Successfully minted {bucket.quantity_total} credits "
                f"for {account_id} ({mint_tier} tier), expires {bucket.expires_at}"
            )
            return bucket

        except Exception as e:
            logger.error(f"[ENRICHMENT] Failed to mint credits: {e}", exc_info=True)
            # Don't fail the webhook - we can manually mint credits later

    except Exception as e:
        logger.error(f"[ENRICHMENT] Error in mint handler: {e}", exc_info=True)
    return None


def _parse_timestamp(ts):
    """Parse Unix timestamp or ISO date string."""
    if not ts:
        return None
    try:
        if isinstance(ts, str):
            return dateparser.parse(ts)
        elif isinstance(ts, int):
            return dateparser.parse(str(ts))
    except Exception:
        pass
    return None
