from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Sequence
import logging

from sqlalchemy import and_, bindparam, case, func, select, text
from sqlalchemy.ext.asyncio import AsyncSession

from ..middleware.quota_checker import TIER_LIMITS
from ..models import (
    Account,
    AccountEntitlements,
    AccountLifecycle,
    Integration,
    IntegrationProvider,
    IntegrationStatus,
    SaaSEvent,
    ScheduledJob,
    User,
    UsageQuotaModel,
    WorkspaceSecret,
)

log = logging.getLogger(__name__)

ACTIVE_SUBSCRIPTION_STATUSES = {"active", "trialing", "past_due", "incomplete"}
INACTIVE_SUBSCRIPTION_STATUSES = {
    "canceled",
    "cancelled",
    "unpaid",
    "incomplete_expired",
}


@dataclass
class AccountSignalSnapshot:
    account_id: str
    name: str
    primary_email: Optional[str]
    primary_contact_name: Optional[str]
    tier: str
    usage_used: int
    usage_limit: int
    usage_percent: float
    lifecycle_override: Optional[str]
    health_override: Optional[int]
    tags: Optional[List[str]]
    notes: Optional[str]
    updated_by: Optional[str]
    updated_at: Optional[datetime]
    events_30d: int
    last_event_at: Optional[datetime]
    first_event_at: Optional[datetime]
    first_report_pulled_at: Optional[datetime]
    first_match_job_at: Optional[datetime]
    sfdc_connected: bool
    first_sfdc_connected_at: Optional[datetime]
    active_automations: int
    first_automation_at: Optional[datetime]
    byo_validated: bool
    first_byo_validated_at: Optional[datetime]
    first_byo_created_at: Optional[datetime]
    subscription_active: bool
    subscription_status: Optional[str]
    next_renewal_at: Optional[datetime]
    first_subscription_at: Optional[datetime]

    @property
    def milestone_count(self) -> int:
        return sum(
            1
            for value in (
                self.first_sfdc_connected_at,
                self.first_report_pulled_at,
                self.first_automation_at,
                self.first_byo_validated_at,
                self.first_match_job_at,
            )
            if value is not None
        )

    @property
    def account_start_at(self) -> Optional[datetime]:
        return _min_datetime(
            self.first_event_at,
            self.first_sfdc_connected_at,
            self.first_automation_at,
            self.first_byo_created_at,
            self.first_subscription_at,
        )


class PLGSegmentsService:
    def __init__(self, db: AsyncSession):
        self.db = db

    async def compute_health_score(self, account_id: str) -> int:
        snapshot = await self._build_snapshot(account_id)
        if snapshot is None:
            return 0
        return self._calculate_health_score(snapshot)

    async def compute_lifecycle_stage(self, account_id: str) -> str:
        snapshot = await self._build_snapshot(account_id)
        if snapshot is None:
            return "unknown"
        health_score = self._calculate_health_score(snapshot)
        return self._calculate_lifecycle_stage(snapshot, health_score)

    async def get_activation_milestones(self, account_id: str) -> Dict[str, Optional[str]]:
        snapshot = await self._build_snapshot(account_id)
        if snapshot is None:
            return {
                "sfdc_connected": None,
                "first_report_pulled": None,
                "automation_enabled": None,
                "byo_provider_usage": None,
                "first_match_job": None,
            }
        return {
            "sfdc_connected": _to_iso(snapshot.first_sfdc_connected_at),
            "first_report_pulled": _to_iso(snapshot.first_report_pulled_at),
            "automation_enabled": _to_iso(snapshot.first_automation_at),
            "byo_provider_usage": _to_iso(snapshot.first_byo_validated_at),
            "first_match_job": _to_iso(snapshot.first_match_job_at),
        }

    async def list_segments(
        self,
        *,
        page: int = 1,
        limit: int = 50,
        search: Optional[str] = None,
        tier: Optional[str] = None,
        lifecycle_stage: Optional[str] = None,
        min_health: Optional[int] = None,
        max_health: Optional[int] = None,
        has_sfdc: Optional[bool] = None,
        has_automation: Optional[bool] = None,
        has_byo: Optional[bool] = None,
    ) -> Dict[str, Any]:
        base_rows = await self._fetch_accounts(search=search, tier=tier)
        snapshots = await self._build_snapshots(base_rows)

        items: List[Dict[str, Any]] = []
        for snapshot in snapshots:
            health_score = self._calculate_health_score(snapshot)
            stage = self._calculate_lifecycle_stage(snapshot, health_score)
            item = _snapshot_to_segment_item(snapshot, health_score, stage)
            items.append(item)

        items = _apply_segment_filters(
            items,
            lifecycle_stage=lifecycle_stage,
            min_health=min_health,
            max_health=max_health,
            has_sfdc=has_sfdc,
            has_automation=has_automation,
            has_byo=has_byo,
        )

        total = len(items)
        offset = max((page - 1) * limit, 0)
        paginated = items[offset : offset + limit]

        return {
            "items": paginated,
            "total": total,
            "page": page,
            "page_size": limit,
        }

    async def get_segment_detail(self, account_id: str) -> Optional[Dict[str, Any]]:
        snapshot = await self._build_snapshot(account_id)
        if snapshot is None:
            return None
        health_score = self._calculate_health_score(snapshot)
        stage = self._calculate_lifecycle_stage(snapshot, health_score)
        detail = _snapshot_to_segment_item(snapshot, health_score, stage)
        detail.update(
            {
                "primary_email": snapshot.primary_email,
                "primary_contact_name": snapshot.primary_contact_name,
                "notes": snapshot.notes,
                "tags": snapshot.tags or [],
                "updated_by": snapshot.updated_by,
                "updated_at": _to_iso(snapshot.updated_at),
                "subscription_status": snapshot.subscription_status,
                "subscription_active": snapshot.subscription_active,
            }
        )
        return detail

    async def get_decision_queue(
        self,
        *,
        days: int = 30,
        limit: int = 50,
    ) -> Dict[str, Any]:
        base_rows = await self._fetch_accounts(search=None, tier=None)
        snapshots = await self._build_snapshots(base_rows)

        now = _utcnow()
        items: List[Dict[str, Any]] = []

        for snapshot in snapshots:
            health_score = self._calculate_health_score(snapshot)
            stage = self._calculate_lifecycle_stage(snapshot, health_score)
            account_age_days = _days_since(snapshot.account_start_at, now)
            milestones = snapshot.milestone_count
            renewal_days = _days_until(snapshot.next_renewal_at, now)

            decision = _decision_for_snapshot(
                snapshot=snapshot,
                health_score=health_score,
                stage=stage,
                account_age_days=account_age_days,
                milestones=milestones,
                renewal_days=renewal_days,
                renewal_window=days,
            )
            if decision:
                items.append(decision)

        items.sort(key=lambda row: (row["priority"], row["health_score"]))
        if limit:
            items = items[:limit]

        return {
            "items": items,
            "days": days,
            "limit": limit,
            "total": len(items),
        }

    async def get_renewal_forecast(
        self,
        *,
        days_ahead: int = 90,
        limit: int = 100,
    ) -> Dict[str, Any]:
        cutoff = _utcnow() + timedelta(days=days_ahead)
        stmt = (
            text(
                """
                SELECT s.account_id,
                       a.name AS account_name,
                       s.status,
                       s.tier,
                       s.subscription_type,
                       s.current_period_end
                  FROM account_subscriptions s
                  JOIN accounts a ON a.id = s.account_id
                 WHERE s.current_period_end IS NOT NULL
                   AND s.current_period_end <= :cutoff
                   AND s.status NOT IN :inactive_statuses
                 ORDER BY s.current_period_end ASC
                 LIMIT :limit
                """
            )
            .bindparams(bindparam("inactive_statuses", expanding=True))
        )

        result = await self.db.execute(
            stmt,
            {
                "cutoff": cutoff,
                "inactive_statuses": list(INACTIVE_SUBSCRIPTION_STATUSES),
                "limit": limit,
            },
        )
        rows = result.mappings().all()

        items = [
            {
                "account_id": row["account_id"],
                "name": row["account_name"],
                "status": row["status"],
                "tier": row["tier"],
                "subscription_type": row["subscription_type"],
                "renewal_date": _to_iso(row["current_period_end"]),
            }
            for row in rows
        ]

        return {
            "items": items,
            "days_ahead": days_ahead,
            "total": len(items),
        }

    async def _fetch_accounts(
        self,
        *,
        search: Optional[str],
        tier: Optional[str],
        account_ids: Optional[Sequence[str]] = None,
    ) -> List[Dict[str, Any]]:
        stmt = (
            select(
                Account.id.label("account_id"),
                Account.name.label("account_name"),
                AccountEntitlements.tier.label("entitlement_tier"),
                AccountEntitlements.monthly_row_limit.label("entitlement_limit"),
                UsageQuotaModel.tier.label("quota_tier"),
                UsageQuotaModel.monthly_rows_used.label("usage_used"),
                UsageQuotaModel.monthly_match_entitlement.label("usage_limit"),
                AccountLifecycle.lifecycle_stage_override,
                AccountLifecycle.health_score_override,
                AccountLifecycle.tags,
                AccountLifecycle.notes,
                AccountLifecycle.updated_by,
                AccountLifecycle.updated_at,
            )
            .select_from(Account)
            .outerjoin(AccountEntitlements, Account.id == AccountEntitlements.account_id)
            .outerjoin(UsageQuotaModel, Account.id == UsageQuotaModel.account_id)
            .outerjoin(AccountLifecycle, Account.id == AccountLifecycle.account_id)
        )

        if account_ids:
            stmt = stmt.where(Account.id.in_(account_ids))

        if search:
            stmt = stmt.where(Account.name.ilike(f"%{search}%"))

        if tier:
            stmt = stmt.where(
                (AccountEntitlements.tier == tier) | (UsageQuotaModel.tier == tier)
            )

        stmt = stmt.order_by(Account.name.asc())
        result = await self.db.execute(stmt)
        return [dict(row._mapping) for row in result.all()]

    async def _build_snapshot(self, account_id: str) -> Optional[AccountSignalSnapshot]:
        rows = await self._fetch_accounts(
            search=None, tier=None, account_ids=[account_id]
        )
        target = rows[0] if rows else None
        if not target:
            return None
        snapshots = await self._build_snapshots([target])
        return snapshots[0] if snapshots else None

    async def _build_snapshots(
        self, base_rows: Sequence[Dict[str, Any]]
    ) -> List[AccountSignalSnapshot]:
        account_ids = [row["account_id"] for row in base_rows]
        if not account_ids:
            return []

        events_map = await self._fetch_event_metrics(account_ids)
        integrations_map = await self._fetch_integration_metrics(account_ids)
        jobs_map = await self._fetch_job_metrics(account_ids)
        secrets_map = await self._fetch_secret_metrics(account_ids)
        subscriptions_map = await self._fetch_subscription_metrics(account_ids)
        user_map = await self._fetch_user_profiles(account_ids)

        snapshots: List[AccountSignalSnapshot] = []
        for row in base_rows:
            account_id = row["account_id"]
            events = events_map.get(account_id, {})
            integrations = integrations_map.get(account_id, {})
            jobs = jobs_map.get(account_id, {})
            secrets = secrets_map.get(account_id, {})
            subs = subscriptions_map.get(account_id, {})
            user_profile = user_map.get(account_id, {})

            tier_value = row.get("entitlement_tier") or row.get("quota_tier") or "free"
            usage_used = int(row.get("usage_used") or 0)
            usage_limit = _resolve_usage_limit(
                tier_value,
                row.get("entitlement_limit"),
                row.get("usage_limit"),
            )
            usage_percent = _usage_percent(usage_used, usage_limit)

            snapshots.append(
                AccountSignalSnapshot(
                    account_id=account_id,
                    name=row.get("account_name") or "",
                    primary_email=user_profile.get("email"),
                    primary_contact_name=user_profile.get("contact_name"),
                    tier=tier_value,
                    usage_used=usage_used,
                    usage_limit=usage_limit,
                    usage_percent=usage_percent,
                    lifecycle_override=row.get("lifecycle_stage_override"),
                    health_override=row.get("health_score_override"),
                    tags=_normalize_tags(row.get("tags")),
                    notes=row.get("notes"),
                    updated_by=row.get("updated_by"),
                    updated_at=row.get("updated_at"),
                    events_30d=int(events.get("events_30d") or 0),
                    last_event_at=events.get("last_event_at"),
                    first_event_at=events.get("first_event_at"),
                    first_report_pulled_at=events.get("first_report_pulled_at"),
                    first_match_job_at=events.get("first_match_job_at"),
                    sfdc_connected=bool(integrations.get("sfdc_connected")),
                    first_sfdc_connected_at=integrations.get("first_sfdc_connected_at"),
                    active_automations=int(jobs.get("active_automations") or 0),
                    first_automation_at=jobs.get("first_automation_at"),
                    byo_validated=bool(secrets.get("byo_validated")),
                    first_byo_validated_at=secrets.get("first_byo_validated_at"),
                    first_byo_created_at=secrets.get("first_byo_created_at"),
                    subscription_active=bool(subs.get("subscription_active")),
                    subscription_status=subs.get("subscription_status"),
                    next_renewal_at=subs.get("next_renewal_at"),
                    first_subscription_at=subs.get("first_subscription_at"),
                )
            )
        return snapshots

    async def _fetch_event_metrics(self, account_ids: Sequence[str]) -> Dict[str, Dict[str, Any]]:
        cutoff_30 = _utcnow() - timedelta(days=30)
        stmt = (
            select(
                SaaSEvent.org_id.label("account_id"),
                func.sum(
                    case((SaaSEvent.ts >= cutoff_30, 1), else_=0)
                ).label("events_30d"),
                func.max(SaaSEvent.ts).label("last_event_at"),
                func.min(SaaSEvent.ts).label("first_event_at"),
                func.min(
                    case(
                        (
                            and_(
                                SaaSEvent.action == "export",
                                SaaSEvent.status == "success",
                            ),
                            SaaSEvent.ts,
                        ),
                        else_=None,
                    )
                ).label("first_report_pulled_at"),
                func.min(
                    case(
                        (
                            and_(
                                SaaSEvent.action.in_(["export", "field_update"]),
                                SaaSEvent.status == "success",
                            ),
                            SaaSEvent.ts,
                        ),
                        else_=None,
                    )
                ).label("first_match_job_at"),
            )
            .where(SaaSEvent.org_id.in_(account_ids))
            .group_by(SaaSEvent.org_id)
        )
        result = await self.db.execute(stmt)
        return {row.account_id: dict(row._mapping) for row in result.all()}

    async def _fetch_integration_metrics(
        self, account_ids: Sequence[str]
    ) -> Dict[str, Dict[str, Any]]:
        stmt = (
            select(
                Integration.account_id.label("account_id"),
                func.sum(
                    case(
                        (
                            and_(
                                Integration.provider
                                == IntegrationProvider.SALESFORCE,
                                Integration.status == IntegrationStatus.CONNECTED,
                            ),
                            1,
                        ),
                        else_=0,
                    )
                ).label("sfdc_connected"),
                func.min(
                    case(
                        (
                            and_(
                                Integration.provider
                                == IntegrationProvider.SALESFORCE,
                                Integration.status == IntegrationStatus.CONNECTED,
                            ),
                            Integration.created_at,
                        ),
                        else_=None,
                    )
                ).label("first_sfdc_connected_at"),
            )
            .where(Integration.account_id.in_(account_ids))
            .group_by(Integration.account_id)
        )
        result = await self.db.execute(stmt)
        return {row.account_id: dict(row._mapping) for row in result.all()}

    async def _fetch_job_metrics(self, account_ids: Sequence[str]) -> Dict[str, Dict[str, Any]]:
        stmt = (
            select(
                ScheduledJob.account_id.label("account_id"),
                func.sum(
                    case((ScheduledJob.is_active.is_(True), 1), else_=0)
                ).label("active_automations"),
                func.min(
                    case(
                        (ScheduledJob.is_active.is_(True), ScheduledJob.created_at),
                        else_=None,
                    )
                ).label("first_automation_at"),
            )
            .where(ScheduledJob.account_id.in_(account_ids))
            .group_by(ScheduledJob.account_id)
        )
        result = await self.db.execute(stmt)
        return {row.account_id: dict(row._mapping) for row in result.all()}

    async def _fetch_secret_metrics(
        self, account_ids: Sequence[str]
    ) -> Dict[str, Dict[str, Any]]:
        stmt = (
            select(
                WorkspaceSecret.workspace_id.label("account_id"),
                func.sum(
                    case((WorkspaceSecret.validated_at.isnot(None), 1), else_=0)
                ).label("byo_validated"),
                func.min(
                    case(
                        (
                            WorkspaceSecret.validated_at.isnot(None),
                            WorkspaceSecret.validated_at,
                        ),
                        else_=None,
                    )
                ).label("first_byo_validated_at"),
                func.min(WorkspaceSecret.created_at).label("first_byo_created_at"),
            )
            .where(WorkspaceSecret.workspace_id.in_(account_ids))
            .group_by(WorkspaceSecret.workspace_id)
        )
        result = await self.db.execute(stmt)
        return {row.account_id: dict(row._mapping) for row in result.all()}

    async def _fetch_user_profiles(
        self, account_ids: Sequence[str]
    ) -> Dict[str, Dict[str, Any]]:
        stmt = (
            select(
                User.account_id.label("account_id"),
                User.email.label("email"),
                User.profile_metadata.label("profile_metadata"),
                User.created_at.label("created_at"),
            )
            .where(User.account_id.in_(account_ids))
            .order_by(User.created_at.asc())
        )
        result = await self.db.execute(stmt)
        rows = result.all()

        profiles: Dict[str, Dict[str, Any]] = {}
        for row in rows:
            account_id = row.account_id
            if account_id in profiles:
                continue
            contact_name = _extract_contact_name(row.profile_metadata)
            profiles[account_id] = {"email": row.email, "contact_name": contact_name}
        return profiles

    async def _fetch_subscription_metrics(
        self, account_ids: Sequence[str]
    ) -> Dict[str, Dict[str, Any]]:
        stmt = (
            text(
                """
                SELECT account_id,
                       MIN(CASE WHEN status IN :active_statuses THEN current_period_end END) AS next_renewal_at,
                       MIN(created_at) AS first_subscription_at,
                       MAX(CASE WHEN status IN :active_statuses THEN 1 ELSE 0 END) AS subscription_active,
                       MAX(status) AS subscription_status
                  FROM account_subscriptions
                 WHERE account_id IN :account_ids
                 GROUP BY account_id
                """
            )
            .bindparams(
                bindparam("account_ids", expanding=True),
                bindparam("active_statuses", expanding=True),
            )
        )

        try:
            result = await self.db.execute(
                stmt,
                {
                    "account_ids": list(account_ids),
                    "active_statuses": list(ACTIVE_SUBSCRIPTION_STATUSES),
                },
            )
            return {row["account_id"]: dict(row) for row in result.mappings().all()}
        except Exception as exc:  # pragma: no cover - defensive
            log.warning(
                "Failed to load subscription metrics from account_subscriptions: %s",
                exc,
            )
            return {}

    def _calculate_health_score(self, snapshot: AccountSignalSnapshot) -> int:
        if snapshot.health_override is not None:
            return int(snapshot.health_override)

        recent_activity_score = _score_recent_activity(snapshot.events_30d)
        usage_score = _score_usage(snapshot.usage_percent)
        integration_score = _score_integrations(snapshot.sfdc_connected, snapshot.byo_validated)
        automation_score = _score_automations(snapshot.active_automations)
        recency_score = _score_recency(snapshot.last_event_at)

        total = (
            recent_activity_score
            + usage_score
            + integration_score
            + automation_score
            + recency_score
        )
        return max(0, min(100, total))

    def _calculate_lifecycle_stage(
        self, snapshot: AccountSignalSnapshot, health_score: int
    ) -> str:
        if snapshot.lifecycle_override:
            return snapshot.lifecycle_override

        now = _utcnow()
        last_event = snapshot.last_event_at
        days_since_event = _days_since(last_event, now)
        account_age = _days_since(snapshot.account_start_at, now)
        milestones = snapshot.milestone_count

        inactive_60d = days_since_event is None or days_since_event > 60
        inactive_30d = days_since_event is None or days_since_event > 30

        if inactive_60d and not snapshot.subscription_active:
            return "churned"

        if inactive_30d or health_score < 20:
            return "at_risk"

        if account_age is not None and account_age < 14 and milestones < 2:
            return "new"

        if account_age is not None and account_age < 60 and 2 <= milestones <= 3:
            return "activating"

        if health_score >= 75 and milestones >= 4 and snapshot.active_automations > 0:
            return "power_user"

        return "active"


def _apply_segment_filters(
    items: List[Dict[str, Any]],
    *,
    lifecycle_stage: Optional[str],
    min_health: Optional[int],
    max_health: Optional[int],
    has_sfdc: Optional[bool],
    has_automation: Optional[bool],
    has_byo: Optional[bool],
) -> List[Dict[str, Any]]:
    def _keep(item: Dict[str, Any]) -> bool:
        if lifecycle_stage and item.get("lifecycle_stage") != lifecycle_stage:
            return False
        if min_health is not None and item.get("health_score", 0) < min_health:
            return False
        if max_health is not None and item.get("health_score", 0) > max_health:
            return False
        if has_sfdc is not None and item.get("flags", {}).get("has_sfdc") != has_sfdc:
            return False
        if has_automation is not None and item.get("flags", {}).get("has_automation") != has_automation:
            return False
        if has_byo is not None and item.get("flags", {}).get("has_byo") != has_byo:
            return False
        return True

    return [item for item in items if _keep(item)]


def _snapshot_to_segment_item(
    snapshot: AccountSignalSnapshot, health_score: int, stage: str
) -> Dict[str, Any]:
    return {
        "account_id": snapshot.account_id,
        "name": snapshot.name,
        "primary_email": snapshot.primary_email,
        "primary_contact_name": snapshot.primary_contact_name,
        "tier": snapshot.tier,
        "health_score": health_score,
        "lifecycle_stage": stage,
        "usage_percent": snapshot.usage_percent,
        "usage_used": snapshot.usage_used,
        "usage_limit": snapshot.usage_limit,
        "renewal_date": _to_iso(snapshot.next_renewal_at),
        "milestones": {
            "sfdc_connected": _to_iso(snapshot.first_sfdc_connected_at),
            "first_report_pulled": _to_iso(snapshot.first_report_pulled_at),
            "automation_enabled": _to_iso(snapshot.first_automation_at),
            "byo_provider_usage": _to_iso(snapshot.first_byo_validated_at),
            "first_match_job": _to_iso(snapshot.first_match_job_at),
        },
        "flags": {
            "has_sfdc": snapshot.sfdc_connected,
            "has_automation": snapshot.active_automations > 0,
            "has_byo": snapshot.byo_validated,
        },
        "overrides": {
            "lifecycle_stage_override": snapshot.lifecycle_override,
            "health_score_override": snapshot.health_override,
        },
        "account_start_at": _to_iso(snapshot.account_start_at),
    }


def _decision_for_snapshot(
    *,
    snapshot: AccountSignalSnapshot,
    health_score: int,
    stage: str,
    account_age_days: Optional[int],
    milestones: int,
    renewal_days: Optional[int],
    renewal_window: int,
) -> Optional[Dict[str, Any]]:
    name = snapshot.name or snapshot.account_id
    usage_percent = snapshot.usage_percent

    if (
        renewal_days is not None
        and 0 <= renewal_days <= renewal_window
        and health_score < 50
    ):
        return {
            "account_id": snapshot.account_id,
            "name": name,
            "primary_email": snapshot.primary_email,
            "primary_contact_name": snapshot.primary_contact_name,
            "category": "renewal",
            "reason": "At-risk renewal",
            "suggested_action": "Reach out before renewal and troubleshoot adoption blockers.",
            "health_score": health_score,
            "lifecycle_stage": stage,
            "usage_percent": usage_percent,
            "renewal_date": _to_iso(snapshot.next_renewal_at),
            "priority": 1,
        }

    if stage == "at_risk":
        return {
            "account_id": snapshot.account_id,
            "name": name,
            "primary_email": snapshot.primary_email,
            "primary_contact_name": snapshot.primary_contact_name,
            "category": "retention",
            "reason": "Re-engage",
            "suggested_action": "Send a check-in and offer help getting value quickly.",
            "health_score": health_score,
            "lifecycle_stage": stage,
            "usage_percent": usage_percent,
            "renewal_date": _to_iso(snapshot.next_renewal_at),
            "priority": 2,
        }

    if snapshot.tier == "free" and usage_percent >= 0.8:
        return {
            "account_id": snapshot.account_id,
            "name": name,
            "primary_email": snapshot.primary_email,
            "primary_contact_name": snapshot.primary_contact_name,
            "category": "expansion",
            "reason": "Upgrade candidate",
            "suggested_action": "Highlight value unlocked by upgrading before they hit limits.",
            "health_score": health_score,
            "lifecycle_stage": stage,
            "usage_percent": usage_percent,
            "renewal_date": _to_iso(snapshot.next_renewal_at),
            "priority": 3,
        }

    if stage == "activating" and account_age_days is not None and account_age_days > 30:
        return {
            "account_id": snapshot.account_id,
            "name": name,
            "primary_email": snapshot.primary_email,
            "primary_contact_name": snapshot.primary_contact_name,
            "category": "activation",
            "reason": "Stalled onboarding",
            "suggested_action": "Offer concierge setup or a quick win report to unblock activation.",
            "health_score": health_score,
            "lifecycle_stage": stage,
            "usage_percent": usage_percent,
            "renewal_date": _to_iso(snapshot.next_renewal_at),
            "priority": 4,
        }

    if stage == "new" and account_age_days is not None and account_age_days > 7 and milestones == 0:
        return {
            "account_id": snapshot.account_id,
            "name": name,
            "primary_email": snapshot.primary_email,
            "primary_contact_name": snapshot.primary_contact_name,
            "category": "activation",
            "reason": "Needs onboarding help",
            "suggested_action": "Send a quick-start guide or offer to set up their first report.",
            "health_score": health_score,
            "lifecycle_stage": stage,
            "usage_percent": usage_percent,
            "renewal_date": _to_iso(snapshot.next_renewal_at),
            "priority": 5,
        }

    return None


def _resolve_usage_limit(
    tier: str, ent_limit: Optional[int], quota_limit: Optional[int]
) -> int:
    if ent_limit is not None and ent_limit > 0:
        return int(ent_limit)
    if quota_limit is not None and quota_limit > 0:
        return int(quota_limit)
    limits = TIER_LIMITS.get(tier, TIER_LIMITS.get("free", {}))
    return int(limits.get("monthly_rows", 0) or 0)


def _usage_percent(used: int, limit: int) -> float:
    if limit <= 0:
        return 0.0
    return min(max(used / float(limit), 0.0), 1.5)


def _score_recent_activity(events_30d: int) -> int:
    if events_30d <= 0:
        return 0
    if events_30d <= 5:
        return 10
    if events_30d <= 20:
        return 18
    return 25


def _score_usage(usage_percent: float) -> int:
    return int(round(min(max(usage_percent, 0.0), 1.0) * 25))


def _score_integrations(has_sfdc: bool, has_byo: bool) -> int:
    if has_sfdc and has_byo:
        return 20
    if has_sfdc:
        return 10
    return 0


def _score_automations(active_automations: int) -> int:
    if active_automations >= 3:
        return 15
    if active_automations >= 1:
        return 10
    return 0


def _score_recency(last_event_at: Optional[datetime]) -> int:
    if not last_event_at:
        return 0
    days = _days_since(last_event_at, _utcnow())
    if days is None:
        return 0
    if days <= 3:
        return 15
    if days <= 7:
        return 12
    if days <= 14:
        return 8
    if days <= 30:
        return 4
    return 0


def _normalize_tags(value: Any) -> Optional[List[str]]:
    if value is None:
        return None
    if isinstance(value, list):
        return [str(item) for item in value]
    if isinstance(value, str):
        return [value]
    return [str(value)]


def _extract_contact_name(metadata: Any) -> Optional[str]:
    if not isinstance(metadata, dict):
        return None
    first = metadata.get("first_name") or metadata.get("given_name")
    last = metadata.get("last_name") or metadata.get("family_name")
    if first or last:
        return " ".join([part for part in [str(first or "").strip(), str(last or "").strip()] if part]).strip() or None
    for key in ("full_name", "name", "display_name"):
        value = metadata.get(key)
        if isinstance(value, str) and value.strip():
            return value.strip()
    return None


def _ensure_aware(dt: datetime) -> datetime:
    """Coerce a naive datetime to UTC-aware so comparisons never fail."""
    if dt.tzinfo is None:
        return dt.replace(tzinfo=timezone.utc)
    return dt


def _min_datetime(*values: Optional[datetime]) -> Optional[datetime]:
    filtered = [_ensure_aware(value) for value in values if value is not None]
    if not filtered:
        return None
    return min(filtered)


def _to_iso(value: Optional[datetime]) -> Optional[str]:
    if not value:
        return None
    return _ensure_aware(value).isoformat()


def _utcnow() -> datetime:
    return datetime.now(timezone.utc)


def _days_since(value: Optional[datetime], now: datetime) -> Optional[int]:
    if value is None:
        return None
    value = _ensure_aware(value)
    now = _ensure_aware(now)
    return max((now - value).days, 0)


def _days_until(value: Optional[datetime], now: datetime) -> Optional[int]:
    if value is None:
        return None
    value = _ensure_aware(value)
    now = _ensure_aware(now)
    return (value - now).days
