"""
Admin endpoint for synchronising tier changes from Stripe/Clerk.

The web application (Next.js) calls this endpoint after updating Clerk metadata
so that the FastAPI backend maintains the same subscription tier and status.
"""

from __future__ import annotations

import logging
from datetime import datetime, timezone
from typing import Literal, Optional

from fastapi import APIRouter, Depends, Header, HTTPException
from pydantic import BaseModel, root_validator
from sqlalchemy import select
from sqlalchemy.exc import OperationalError, ProgrammingError
from sqlalchemy.ext.asyncio import AsyncSession

from ... import settings as saas_settings
from ...auth_security import _ensure_clerk_account_and_user
from ...db import get_session
from ...middleware.quota_checker import TIER_LIMITS
from ...models import (
    Account,
    AccountEntitlements,
    TrialEmailTemplate,
    UsageQuotaModel,
    User,
)
from ...services import license_manager as license_manager_service
from ...services.usage_gateway import (
    UsageGateway,
    resolve_surface_gremlin_daily_limit,
)
from ...services.admin_surface_signals import normalize_product_surface

log = logging.getLogger("fmatch.api.sync_tier")

router = APIRouter(prefix="/api/v1/admin", tags=["admin-internal"])


class TierSyncPayload(BaseModel):
    """Payload for tier synchronisation from Stripe webhooks or admin tooling."""

    clerk_user_id: str
    clerk_org_id: Optional[str] = None
    product_surface: Optional[str] = "gremlin"
    tier: Literal["free", "pro", "scale", "unleashed"]
    status: str  # active, canceled, past_due, trialing, etc.
    monthly_enrichments: Optional[int] = None
    monthly_match_limit: Optional[int] = None
    period_start: Optional[str] = None
    period_end: Optional[str] = None
    source: Optional[str] = "webhook"
    force_reset: Optional[bool] = (
        False  # Allow admin scripts to bypass downgrade protection
    )
    addons: Optional[list[str]] = None

    @root_validator(pre=True)
    def _normalise_aliases(cls, values: dict[str, object]) -> dict[str, object]:
        """Allow camelCase aliases used by the web team."""
        if "clerkUserId" in values and "clerk_user_id" not in values:
            values["clerk_user_id"] = values["clerkUserId"]
        if "clerkOrgId" in values and "clerk_org_id" not in values:
            values["clerk_org_id"] = values["clerkOrgId"]
        if "productSurface" in values and "product_surface" not in values:
            values["product_surface"] = values["productSurface"]
        if "monthlyEnrichmentCredits" in values and "monthly_enrichments" not in values:
            values["monthly_enrichments"] = values["monthlyEnrichmentCredits"]
        if "monthlyMatchLimit" in values and "monthly_match_limit" not in values:
            values["monthly_match_limit"] = values["monthlyMatchLimit"]
        if "periodStart" in values and "period_start" not in values:
            values["period_start"] = values["periodStart"]
        if "periodEnd" in values and "period_end" not in values:
            values["period_end"] = values["periodEnd"]
        if "forceReset" in values and "force_reset" not in values:
            values["force_reset"] = values["forceReset"]
        if "source" not in values and "event_source" in values:
            values["source"] = values["event_source"]
        return values


class TrialSyncPayload(BaseModel):
    """Payload for provisioning a Gremlin trial into the FastAPI entitlement system."""

    clerk_user_id: str
    clerk_org_id: Optional[str] = None
    email: str
    trial_started_at: str
    trial_ends_at: str
    first_name: Optional[str] = None
    account_name: Optional[str] = None
    source: Optional[str] = "agent_trial_start"
    product_surface: Optional[str] = "gremlin"
    send_welcome_now: Optional[bool] = True

    @root_validator(pre=True)
    def _normalise_aliases(cls, values: dict[str, object]) -> dict[str, object]:
        if "clerkUserId" in values and "clerk_user_id" not in values:
            values["clerk_user_id"] = values["clerkUserId"]
        if "clerkOrgId" in values and "clerk_org_id" not in values:
            values["clerk_org_id"] = values["clerkOrgId"]
        if "trialStartedAt" in values and "trial_started_at" not in values:
            values["trial_started_at"] = values["trialStartedAt"]
        if "trialEndsAt" in values and "trial_ends_at" not in values:
            values["trial_ends_at"] = values["trialEndsAt"]
        if "firstName" in values and "first_name" not in values:
            values["first_name"] = values["firstName"]
        if "accountName" in values and "account_name" not in values:
            values["account_name"] = values["accountName"]
        if "productSurface" in values and "product_surface" not in values:
            values["product_surface"] = values["productSurface"]
        if "sendWelcomeNow" in values and "send_welcome_now" not in values:
            values["send_welcome_now"] = values["sendWelcomeNow"]
        if "source" not in values and "event_source" in values:
            values["source"] = values["event_source"]
        return values


async def require_webhook_secret(
    authorization: str = Header(..., alias="Authorization"),
) -> str:
    """
    Verify webhook secret for internal service-to-service calls.

    Expected format: "Bearer <WEBHOOK_SECRET>"
    """

    if not authorization.startswith("Bearer "):
        raise HTTPException(
            status_code=401, detail="Invalid authorization header format"
        )

    token = authorization.replace("Bearer ", "")

    # Development escape hatch to simplify local testing.
    if saas_settings.ENV.lower() in ("dev", "development") and token == "dev-token":
        log.info("[sync-tier] Allowing dev-token in development")
        return token

    webhook_secret = saas_settings.WEBHOOK_SECRET
    if not webhook_secret:
        log.error("[sync-tier] WEBHOOK_SECRET not configured!")
        raise HTTPException(
            status_code=500, detail="Webhook authentication not configured"
        )

    if token != webhook_secret:
        log.warning("[sync-tier] Invalid webhook secret provided")
        raise HTTPException(status_code=401, detail="Invalid webhook secret")

    return token


def _resolve_entitlements(payload: TierSyncPayload) -> tuple[int, int]:
    """Determine entitlement values using payload overrides or tier defaults."""
    tier_limits = TIER_LIMITS.get(payload.tier, TIER_LIMITS["free"])
    monthly_rows = payload.monthly_match_limit or tier_limits["monthly_rows"]
    monthly_enrichments = (
        payload.monthly_enrichments or tier_limits["enrichment_credits"]
    )
    return monthly_rows, monthly_enrichments


def _parse_iso8601_utc(value: str, field_name: str) -> datetime:
    try:
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError as exc:
        raise HTTPException(
            status_code=400, detail=f"Invalid {field_name}: expected ISO8601 timestamp"
        ) from exc

    if parsed.tzinfo is None:
        parsed = parsed.replace(tzinfo=timezone.utc)

    return parsed.astimezone(timezone.utc)


def _default_account_name(email: str) -> str:
    domain = (email.split("@")[1] if "@" in email else "").split(".")[0]
    cleaned = domain.replace("-", " ").replace("_", " ").strip()
    if not cleaned:
        return "Gremlin Trial"
    return " ".join(part.capitalize() for part in cleaned.split())


def _should_refresh_account_name(current_name: Optional[str]) -> bool:
    if not current_name:
        return True

    normalized = current_name.strip().lower()
    return normalized.startswith("clerk org ") or normalized.startswith("cli user ")


def _normalize_optional_email(value: Optional[str]) -> Optional[str]:
    normalized = (value or "").strip().lower() or None
    if not normalized or normalized.endswith("@unknown"):
        return None
    return normalized


def _default_sync_account_name(
    *,
    clerk_user_id: str,
    clerk_org_id: Optional[str] = None,
    email: Optional[str] = None,
) -> str:
    normalized_email = _normalize_optional_email(email)
    if normalized_email:
        return _default_account_name(normalized_email)
    if clerk_org_id:
        return f"Clerk Org {clerk_org_id}"
    return f"CLI User {clerk_user_id}"


async def _ensure_account_context_rows(
    db: AsyncSession,
    *,
    account_id: str,
    account: Optional[Account],
    user: Optional[User],
    clerk_user_id: str,
    clerk_org_id: Optional[str] = None,
    email: Optional[str] = None,
    account_name: Optional[str] = None,
) -> tuple[Account, Optional[User]]:
    resolved_account_name = (
        (account_name or "").strip()
        or _default_sync_account_name(
            clerk_user_id=clerk_user_id,
            clerk_org_id=clerk_org_id,
            email=email,
        )
    )
    normalized_email = _normalize_optional_email(email)

    if not account:
        account = Account(
            id=account_id,
            name=resolved_account_name,
            clerk_org_id=clerk_org_id,
        )
        db.add(account)
    else:
        dirty_account = False
        if clerk_org_id and account.clerk_org_id != clerk_org_id:
            account.clerk_org_id = clerk_org_id
            dirty_account = True
        if _should_refresh_account_name(account.name):
            account.name = resolved_account_name
            dirty_account = True
        if dirty_account:
            db.add(account)

    if user is None:
        user = User(
            id=clerk_user_id,
            account_id=account_id,
            email=normalized_email or f"{clerk_user_id}@unknown",
            clerk_user_id=clerk_user_id,
        )
        db.add(user)
        return account, user

    if not normalized_email:
        dirty_user = False
        if user.account_id != account_id:
            user.account_id = account_id
            dirty_user = True
        if user.clerk_user_id != clerk_user_id:
            user.clerk_user_id = clerk_user_id
            dirty_user = True
        if dirty_user:
            db.add(user)
        return account, user

    dirty_user = False
    if user.email != normalized_email:
        user.email = normalized_email
        dirty_user = True
    if user.account_id != account_id:
        user.account_id = account_id
        dirty_user = True
    if user.clerk_user_id != clerk_user_id:
        user.clerk_user_id = clerk_user_id
        dirty_user = True
    if dirty_user:
        db.add(user)

    return account, user


async def _load_trial_user(
    db: AsyncSession, clerk_user_id: str, email: str
) -> Optional[User]:
    by_id = await db.get(User, clerk_user_id)
    if by_id:
        return by_id

    by_clerk = await db.execute(select(User).where(User.clerk_user_id == clerk_user_id))
    clerk_user = by_clerk.scalar_one_or_none()
    if clerk_user:
        return clerk_user

    by_email = await db.execute(select(User).where(User.email == email))
    return by_email.scalar_one_or_none()


async def _resolve_account_context(
    db: AsyncSession,
    *,
    clerk_user_id: str,
    clerk_org_id: Optional[str] = None,
    email: Optional[str] = None,
) -> tuple[str, Optional[Account], Optional[User]]:
    normalized_user_id = clerk_user_id.strip()
    normalized_org_id = (clerk_org_id or "").strip() or None
    if normalized_org_id == normalized_user_id:
        normalized_org_id = None
    normalized_email = (email or "").strip().lower() or None

    if normalized_org_id:
        try:
            await _ensure_clerk_account_and_user(
                db,
                org_id=normalized_org_id,
                user_id=normalized_user_id,
                email=normalized_email,
                logger=log,
            )
        except Exception as exc:  # noqa: BLE001 - best effort bootstrap
            log.warning(
                "[sync-tier] Clerk bootstrap failed for org=%s user=%s: %s",
                normalized_org_id,
                normalized_user_id,
                exc,
            )

        account_result = await db.execute(
            select(Account).where(
                (Account.id == normalized_org_id) | (Account.clerk_org_id == normalized_org_id)
            )
        )
        account = account_result.scalar_one_or_none()
        user = await _load_trial_user(db, normalized_user_id, normalized_email or "")
        if account is not None:
            return str(account.id), account, user

    user = await _load_trial_user(db, normalized_user_id, normalized_email or "")
    if user is not None and user.account_id:
        account = await db.get(Account, user.account_id)
        return str(user.account_id), account, user

    fallback_account_id = normalized_org_id or normalized_user_id
    account = await db.get(Account, fallback_account_id)
    return fallback_account_id, account, user


def _canonicalize_surface_local_tier(
    product_surface: str,
    legacy_tier: Optional[str],
) -> str:
    normalized_surface = normalize_product_surface(product_surface)
    normalized = (legacy_tier or "").strip().lower()
    if normalized_surface == "gremlin":
        if normalized in {"workspace_ops", "hubspot_admin", "full_stack", "local_free"}:
            return normalized
        if normalized in {"", "free"}:
            return "local_free"
        return "full_stack"
    if normalized_surface == "sheets":
        if normalized in {"free", "pro", "scale", "unleashed"}:
            return normalized
        return "free"
    return normalized or "free"


async def _sync_canonical_surface_grant(
    db: AsyncSession,
    *,
    account_id: str,
    user: Optional[User],
    clerk_user_id: Optional[str],
    primary_email: Optional[str],
    product_surface: str,
    legacy_tier: Optional[str],
    legacy_status: Optional[str],
    source: str,
    grant_type: str,
    starts_at: Optional[datetime],
    ends_at: Optional[datetime],
    attributes: Optional[dict[str, object]] = None,
) -> None:
    normalized_surface = normalize_product_surface(product_surface)
    canonical_local_tier = _canonicalize_surface_local_tier(
        normalized_surface,
        legacy_tier,
    )
    plan_key = license_manager_service.resolve_plan_key(
        product_surface=normalized_surface,
        local_tier=canonical_local_tier,
        grant_status=legacy_status,
    )
    payload_attributes = dict(attributes or {})
    payload_attributes.setdefault("legacy_tier", (legacy_tier or "").strip().lower() or None)
    payload_attributes.setdefault(
        "legacy_status",
        (legacy_status or "").strip().lower() or None,
    )
    if normalized_surface == "gremlin" and grant_type != "paid":
        payload_attributes.setdefault("tier_after_expiry", "local_free")

    try:
        await license_manager_service.upsert_and_sync_license_grant(
            db,
            account_id=account_id,
            product_surface=normalized_surface,
            user_id=user.id if user else None,
            clerk_user_id=clerk_user_id,
            primary_email=primary_email,
            grant_type=grant_type,
            plan_key=plan_key,
            status=legacy_status or "active",
            source=source,
            starts_at=starts_at,
            ends_at=ends_at,
            attributes=payload_attributes,
        )
    except (OperationalError, ProgrammingError) as exc:
        log.warning(
            "[sync-tier] Canonical Gremlin grant sync skipped before migration for account=%s: %s",
            account_id,
            exc,
        )


def _canonical_primary_email(
    user: Optional[User],
    *,
    fallback_email: Optional[str] = None,
) -> Optional[str]:
    normalized_fallback = _normalize_optional_email(fallback_email)
    if normalized_fallback:
        return normalized_fallback
    if user is None:
        return None
    return _normalize_optional_email(user.email)


@router.post("/sync-tier")
async def sync_tier_from_stripe(
    payload: TierSyncPayload,
    _auth: str = Depends(require_webhook_secret),
    db: AsyncSession = Depends(get_session),
) -> dict:
    """
    Update UsageQuotaModel to reflect the tier coming from Stripe/Clerk.

    Flow:
        1. Stripe fires webhook to Next.js
        2. Next.js updates PostgreSQL & Clerk metadata
        3. Next.js calls this endpoint with the same tier information
    """

    tier_limits = TIER_LIMITS.get(payload.tier)
    if not tier_limits:
        raise HTTPException(
            status_code=400,
            detail=f"Invalid tier: {payload.tier}. Must be one of: {list(TIER_LIMITS.keys())}",
        )

    account_id, account, user = await _resolve_account_context(
        db,
        clerk_user_id=payload.clerk_user_id,
        clerk_org_id=payload.clerk_org_id,
    )
    account, user = await _ensure_account_context_rows(
        db,
        account_id=account_id,
        account=account,
        user=user,
        clerk_user_id=payload.clerk_user_id,
        clerk_org_id=payload.clerk_org_id,
        email=user.email if user else None,
    )
    product_surface = normalize_product_surface(payload.product_surface)
    monthly_rows, monthly_enrichments = _resolve_entitlements(payload)

    enrichments_str = (
        f", enrichments={monthly_enrichments}"
        if monthly_enrichments is not None
        else ""
    )
    log.info(
        "[sync-tier] Received tier sync for user %s: tier=%s, status=%s%s, source=%s",
        payload.clerk_user_id,
        payload.tier,
        payload.status,
        enrichments_str,
        payload.source,
    )

    stmt = select(UsageQuotaModel).where(
        UsageQuotaModel.account_id == account_id
    )
    result = await db.execute(stmt)
    quota = result.scalar_one_or_none()

    now = datetime.utcnow()

    period_start = (
        _parse_iso8601_utc(payload.period_start, "period_start")
        if payload.period_start
        else None
    )
    period_end = (
        _parse_iso8601_utc(payload.period_end, "period_end")
        if payload.period_end
        else None
    )

    if not quota:
        quota = UsageQuotaModel(
            account_id=account_id,
            tier=payload.tier,
            monthly_rows_used=0,
            enrichment_credits_used=0,
            subscription_status=payload.status or "unknown",
            tier_synced_at=now,
            tier_sync_source=payload.source or "webhook",
            monthly_enrichment_entitlement=monthly_enrichments,
            monthly_match_entitlement=monthly_rows,
        )
        db.add(quota)
        await db.commit()
        await db.refresh(quota)

        try:
            gateway = UsageGateway(db)
            await gateway.sync_from_stripe(
                account_id=account_id,
                tier=payload.tier,
                addons=payload.addons or [],
                source=payload.source or "webhook",
                monthly_row_limit=monthly_rows,
            )
        except Exception as exc:  # noqa: BLE001 - best effort
            log.warning("[sync-tier] Failed to sync usage gateway: %s", exc)

        grant_type = "paid" if payload.tier != "free" else (
            "trial" if (payload.status or "").strip().lower() == "trialing" else "manual_override"
        )
        await _sync_canonical_surface_grant(
            db,
            account_id=account_id,
            user=user,
            clerk_user_id=payload.clerk_user_id,
            primary_email=_canonical_primary_email(user),
            product_surface=product_surface,
            legacy_tier=payload.tier,
            legacy_status=payload.status,
            source=payload.source or "webhook",
            grant_type=grant_type,
            starts_at=period_start,
            ends_at=period_end,
            attributes={
                "billing_period_start": payload.period_start,
                "billing_period_end": payload.period_end,
                "billing_addons": list(payload.addons or []),
            },
        )

        return {
            "ok": True,
            "updated": True,
            "message": f"Created new quota record with tier={payload.tier}",
            "account_id": account_id,
            "tier": quota.tier,
            "status": quota.subscription_status,
            "enrichments": quota.monthly_enrichment_entitlement,
            "monthly_match_limit": quota.monthly_match_entitlement,
            "source": quota.tier_sync_source,
        }

    old_tier = quota.tier

    # Protect against erroneous downgrades from paid to free
    # If current tier is paid (pro/scale) and new tier is free, check if this is legitimate
    if (
        old_tier in ("pro", "scale")
        and payload.tier == "free"
        and not payload.force_reset
    ):
        # Check if there's an active subscription in the database
        from sqlalchemy import text

        active_sub = await db.execute(
            text("""
                SELECT s.stripe_subscription_id, s.status
                FROM subscription s
                WHERE s.account_id = :aid
                AND s.status IN ('active', 'trialing')
                ORDER BY s.created_at DESC
                LIMIT 1
            """),
            {"aid": account_id},
        )
        active_subscription = active_sub.first()

        if active_subscription:
            # There's an active subscription - reject the downgrade regardless of status
            log.warning(
                "[sync-tier] Rejecting downgrade from %s to free - active subscription %s exists (status=%s). "
                "If this is legitimate, cancel the subscription first or use force_reset=true. Source: %s",
                old_tier,
                active_subscription[0],
                active_subscription[1],
                payload.source,
            )
            return {
                "ok": True,
                "updated": False,
                "message": f"Rejected downgrade from {old_tier} to free - active subscription exists",
                "account_id": account_id,
                "old_tier": old_tier,
                "new_tier": old_tier,  # Kept old tier
                "status": quota.subscription_status,
                "enrichments": quota.monthly_enrichment_entitlement,
                "monthly_match_limit": quota.monthly_match_entitlement,
                "source": quota.tier_sync_source,
                "active_subscription": active_subscription[0],
            }

    # Log if force_reset was used
    if payload.force_reset and old_tier in ("pro", "scale") and payload.tier == "free":
        log.info(
            "[sync-tier] Force reset enabled - allowing downgrade from %s to free. Source: %s",
            old_tier,
            payload.source,
        )

    quota.tier = payload.tier
    quota.subscription_status = payload.status or quota.subscription_status or "unknown"
    quota.tier_synced_at = now
    quota.tier_sync_source = payload.source or quota.tier_sync_source or "webhook"
    quota.monthly_enrichment_entitlement = monthly_enrichments
    quota.monthly_match_entitlement = monthly_rows

    await db.commit()
    await db.refresh(quota)

    try:
        gateway = UsageGateway(db)
        await gateway.sync_from_stripe(
            account_id=account_id,
            tier=payload.tier,
            addons=payload.addons or [],
            source=payload.source or "webhook",
            monthly_row_limit=monthly_rows,
        )
    except Exception as exc:  # noqa: BLE001 - best effort
        log.warning("[sync-tier] Failed to sync usage gateway: %s", exc)

    grant_type = "paid" if payload.tier != "free" else (
        "trial" if (payload.status or "").strip().lower() == "trialing" else "manual_override"
    )
    await _sync_canonical_surface_grant(
        db,
        account_id=account_id,
        user=user,
        clerk_user_id=payload.clerk_user_id,
        primary_email=_canonical_primary_email(user),
        product_surface=product_surface,
        legacy_tier=payload.tier,
        legacy_status=payload.status,
        source=payload.source or "webhook",
        grant_type=grant_type,
        starts_at=period_start,
        ends_at=period_end,
        attributes={
            "billing_period_start": payload.period_start,
            "billing_period_end": payload.period_end,
            "billing_addons": list(payload.addons or []),
        },
    )

    log.info(
        "[sync-tier] Updated tier for %s: %s -> %s (status=%s, source=%s)",
        payload.clerk_user_id,
        old_tier,
        quota.tier,
        quota.subscription_status,
        quota.tier_sync_source,
    )

    return {
        "ok": True,
        "updated": old_tier != quota.tier
        or quota.subscription_status != payload.status
        or quota.monthly_enrichment_entitlement != monthly_enrichments,
        "message": f"Tier updated from {old_tier} to {quota.tier}",
        "account_id": account_id,
        "old_tier": old_tier,
        "new_tier": quota.tier,
        "status": quota.subscription_status,
        "enrichments": quota.monthly_enrichment_entitlement,
        "monthly_match_limit": quota.monthly_match_entitlement,
        "source": quota.tier_sync_source,
    }


@router.post("/sync-trial")
async def sync_trial_start(
    payload: TrialSyncPayload,
    _auth: str = Depends(require_webhook_secret),
    db: AsyncSession = Depends(get_session),
) -> dict:
    """Provision a Gremlin trial user so the drip scheduler can send onboarding emails."""

    clerk_user_id = payload.clerk_user_id.strip()
    email = payload.email.strip().lower()
    if not clerk_user_id or not email:
        raise HTTPException(status_code=400, detail="clerk_user_id and email are required")

    clerk_org_id = (payload.clerk_org_id or "").strip() or None
    account_id, account, user = await _resolve_account_context(
        db,
        clerk_user_id=clerk_user_id,
        clerk_org_id=clerk_org_id,
        email=email,
    )

    trial_started_at = _parse_iso8601_utc(payload.trial_started_at, "trial_started_at")
    trial_ends_at = _parse_iso8601_utc(payload.trial_ends_at, "trial_ends_at")
    if trial_ends_at <= trial_started_at:
        raise HTTPException(
            status_code=400, detail="trial_ends_at must be after trial_started_at"
        )

    account_name = (payload.account_name or "").strip() or _default_account_name(email)
    first_name = (payload.first_name or "").strip() or None
    product_surface = normalize_product_surface(payload.product_surface)
    default_source = (
        "sheets_trial_start"
        if product_surface == "sheets"
        else "agent_trial_start"
    )
    raw_source = (payload.source or "").strip()
    if not raw_source or (
        product_surface == "sheets" and raw_source == "agent_trial_start"
    ):
        source = default_source
    else:
        source = raw_source
    monthly_rows, monthly_enrichments = _resolve_entitlements(
        TierSyncPayload(
            clerk_user_id=clerk_user_id,
            clerk_org_id=clerk_org_id,
            tier="free",
            status="trialing",
            source=source,
        )
    )

    log.info(
        "[sync-trial] Provisioning trial for clerk_user_id=%s account_id=%s email=%s from %s",
        clerk_user_id,
        account_id,
        email,
        source,
    )

    account, user = await _ensure_account_context_rows(
        db,
        account_id=account_id,
        account=account,
        user=user,
        clerk_user_id=clerk_user_id,
        clerk_org_id=clerk_org_id,
        email=email,
        account_name=account_name,
    )

    if not user:
        user = await _load_trial_user(db, clerk_user_id, email)
    if not user:
        profile_metadata = {"first_name": first_name} if first_name else None
        user = User(
            id=clerk_user_id,
            account_id=account_id,
            email=email,
            clerk_user_id=clerk_user_id,
            profile_metadata=profile_metadata,
        )
        db.add(user)
    else:
        dirty = False
        if user.email != email:
            user.email = email
            dirty = True
        if user.account_id != account_id:
            user.account_id = account_id
            dirty = True
        if user.clerk_user_id != clerk_user_id:
            user.clerk_user_id = clerk_user_id
            dirty = True

        metadata = dict(user.profile_metadata or {})
        if first_name and metadata.get("first_name") != first_name:
            metadata["first_name"] = first_name
            user.profile_metadata = metadata
            dirty = True

        if dirty:
            db.add(user)

    stmt = select(UsageQuotaModel).where(UsageQuotaModel.account_id == account_id)
    quota_result = await db.execute(stmt)
    quota = quota_result.scalar_one_or_none()
    now = datetime.utcnow()
    preserve_paid_tier = bool(quota and quota.tier and quota.tier != "free")

    if not quota:
        quota = UsageQuotaModel(
            account_id=account_id,
            tier="free",
            monthly_rows_used=0,
            enrichment_credits_used=0,
            subscription_status="trialing",
            tier_synced_at=now,
            tier_sync_source=source,
            monthly_enrichment_entitlement=monthly_enrichments,
            monthly_match_entitlement=monthly_rows,
        )
        db.add(quota)
        await db.commit()
    elif not preserve_paid_tier:
        quota.tier = "free"
        quota.subscription_status = "trialing"
        quota.tier_synced_at = now
        quota.tier_sync_source = source
        quota.monthly_enrichment_entitlement = monthly_enrichments
        quota.monthly_match_entitlement = monthly_rows
        db.add(quota)
        await db.commit()
    else:
        log.info(
            "[sync-trial] Preserving existing paid tier for %s: tier=%s status=%s",
            account_id,
            quota.tier,
            quota.subscription_status,
        )

    entitlements: Optional[AccountEntitlements] = None
    if not preserve_paid_tier:
        gateway = UsageGateway(db)
        entitlements = await gateway.sync_from_stripe(
            account_id=account_id,
            tier="free",
            addons=[],
            source=source,
            monthly_row_limit=monthly_rows,
            expires_at=trial_ends_at,
        )
        entitlements.gremlin_daily_limit = resolve_surface_gremlin_daily_limit(
            product_surface=product_surface,
            current_limit=entitlements.gremlin_daily_limit,
        )
        entitlements.trial_started_at = trial_started_at
        entitlements.expires_at = trial_ends_at
        db.add(entitlements)
        await db.commit()
        await db.refresh(entitlements)
    else:
        entitlements_result = await db.execute(
            select(AccountEntitlements).where(AccountEntitlements.account_id == account_id)
        )
        entitlements = entitlements_result.scalar_one_or_none()

    final_tier = quota.tier if preserve_paid_tier and quota else "free"
    final_status = (
        quota.subscription_status
        if preserve_paid_tier and quota
        else "trialing"
    )
    await _sync_canonical_surface_grant(
        db,
        account_id=account_id,
        user=user,
        clerk_user_id=clerk_user_id,
        primary_email=email,
        product_surface=product_surface,
        legacy_tier=final_tier,
        legacy_status=final_status,
        source=source,
        grant_type="paid" if preserve_paid_tier else "trial",
        starts_at=None if preserve_paid_tier else trial_started_at,
        ends_at=None if preserve_paid_tier else trial_ends_at,
        attributes={
            "trial_source": source,
            "trial_first_name": first_name,
            "preserved_paid_tier": preserve_paid_tier,
        },
    )

    welcome_sent = False
    if payload.send_welcome_now:
        from ...services.trial_drip_service import send_drip_email

        template_result = await db.execute(
            select(TrialEmailTemplate).where(
                TrialEmailTemplate.day_offset == 0,
                TrialEmailTemplate.product_surface == product_surface,
                TrialEmailTemplate.enabled == True,  # noqa: E712
            )
        )
        template = template_result.scalar_one_or_none()
        if template:
            resend_id = await send_drip_email(
                db=db,
                template=template,
                account=account,
                user=user,
                trial_end_date=trial_ends_at,
            )
            welcome_sent = bool(resend_id)

    return {
        "ok": True,
        "account_id": account_id,
        "tier": entitlements.tier if entitlements else quota.tier,
        "status": "trialing" if not preserve_paid_tier else quota.subscription_status,
        "trial_started_at": trial_started_at.isoformat(),
        "trial_ends_at": trial_ends_at.isoformat(),
        "welcome_sent": welcome_sent,
        "drip_enabled": not preserve_paid_tier,
        "source": source,
    }
