"""
Cron jobs for telemetry insight detection.

Usage:
    python -m src.fmatch.saas.cron.insight_detector_jobs run-all
    python -m src.fmatch.saas.cron.insight_detector_jobs error-first-seen
    python -m src.fmatch.saas.cron.insight_detector_jobs error-rate-regression
"""

from __future__ import annotations

import asyncio
import hashlib
import logging
import sys
from datetime import datetime, timedelta, timezone
from typing import Dict, Iterable, List, Optional, Sequence, Tuple
from uuid import UUID

from sqlalchemy import func, select, text
from sqlalchemy.ext.asyncio import AsyncSession

from ..db import AsyncSessionLocal
from ..models import (
    InsightTicketSeverity,
    InsightTicketStatus,
    InsightTicketType,
    TelemetryInsightAction,
    TelemetryInsightTicket,
    UsageQuotaModel,
)
from ..repos.telemetry_insight_repo import TelemetryInsightRepo
from ..services.insight_context_sanitizer import sanitize_context
from ..services.proactive_outreach import evaluate_proactive_outreach

logger = logging.getLogger(__name__)


def _parse_ts(value: object, default: datetime) -> datetime:
    """Parse timestamp from SQL result - handles both datetime (Postgres) and string (SQLite)."""
    if value is None:
        return default
    if isinstance(value, datetime):
        return value
    if isinstance(value, str):
        # SQLite returns ISO format strings
        try:
            # Try parsing with timezone
            return datetime.fromisoformat(value.replace("Z", "+00:00"))
        except ValueError:
            # Try parsing without timezone and assume UTC
            try:
                return datetime.fromisoformat(value).replace(tzinfo=timezone.utc)
            except ValueError:
                return default
    return default


# Guardrail constants
MIN_BASELINE_TOTAL = 200
MIN_BASELINE_FAILED = 5
MIN_RECENT_FAILED = 5
MIN_ABSOLUTE_INCREASE = 0.03
MIN_RATE_RATIO = 2.0
MIN_REOPEN_EVENTS = 2
MIN_REOPEN_ORGS = 2


def compute_fingerprint(ticket_type: str, **dims: Optional[str]) -> str:
    key_parts = [ticket_type]
    for k in sorted(dims.keys()):
        if dims[k] is not None:
            key_parts.append(f"{k}={dims[k]}")
    raw = "|".join(key_parts)
    return hashlib.sha256(raw.encode()).hexdigest()[:64]


def extract_version_bucket(source_version: Optional[str]) -> str:
    if not source_version:
        return "unknown"
    parts = source_version.split(".")
    if len(parts) >= 2:
        return f"{parts[0]}.{parts[1].split('-')[0]}"
    return parts[0] if parts else "unknown"


def _dialect_name(db: AsyncSession) -> str:
    dialect = getattr(db.bind, "dialect", None)
    return dialect.name if dialect else ""


def _json_expr(dialect_name: str, key: str) -> str:
    if dialect_name == "sqlite":
        return f"json_extract(attrs, '$.{key}')"
    return f"attrs->>'{key}'"


async def count_paying_orgs(db: AsyncSession, org_ids: Sequence[str]) -> int:
    if not org_ids:
        return 0
    stmt = (
        select(func.count())
        .select_from(UsageQuotaModel)
        .where(UsageQuotaModel.account_id.in_(org_ids))
        .where(UsageQuotaModel.tier.in_(["pro", "scale", "unleashed"]))
    )
    result = await db.execute(stmt)
    return int(result.scalar() or 0)


def classify_first_seen_severity(
    *, affected_orgs: int, event_count: int, affected_paying: int
) -> InsightTicketSeverity:
    if affected_paying >= 1:
        if affected_orgs >= 3 or event_count >= 20:
            return InsightTicketSeverity.CRITICAL
        return InsightTicketSeverity.HIGH

    if affected_orgs >= 5 or event_count >= 50:
        return InsightTicketSeverity.CRITICAL
    if affected_orgs >= 3 or event_count >= 20:
        return InsightTicketSeverity.HIGH
    if affected_orgs >= 2 or event_count >= 5:
        return InsightTicketSeverity.MEDIUM
    return InsightTicketSeverity.LOW


def classify_regression_severity(
    *,
    rate_ratio: float,
    absolute_increase: float,
    affected_orgs: int,
    affected_paying: int,
) -> InsightTicketSeverity:
    if affected_paying >= 1:
        if rate_ratio >= 3.0 or absolute_increase >= 0.10:
            return InsightTicketSeverity.CRITICAL
        return InsightTicketSeverity.HIGH

    if rate_ratio >= 5.0 or absolute_increase >= 0.15:
        return InsightTicketSeverity.CRITICAL
    if rate_ratio >= 3.0 or absolute_increase >= 0.10 or affected_orgs >= 5:
        return InsightTicketSeverity.HIGH
    if rate_ratio >= 2.0 or absolute_increase >= 0.05:
        return InsightTicketSeverity.MEDIUM
    return InsightTicketSeverity.LOW


async def _fetch_org_ids(
    db: AsyncSession,
    *,
    feature_expr: str,
    error_expr: str,
    feature: Optional[str],
    action: Optional[str],
    error_code: Optional[str],
    since: Optional[datetime],
    status: str = "failed",
) -> List[str]:
    clauses = ["status = :status"]
    params: Dict[str, object] = {"status": status}
    if since is not None:
        clauses.append("ts >= :since")
        params["since"] = since
    if feature is not None:
        clauses.append(f"{feature_expr} = :feature")
        params["feature"] = feature
    if action is not None:
        clauses.append("action = :action")
        params["action"] = action
    if error_code is not None:
        clauses.append(f"{error_expr} = :error_code")
        params["error_code"] = error_code

    stmt = text(f"SELECT DISTINCT org_id FROM saas_events WHERE {' AND '.join(clauses)}")
    rows = await db.execute(stmt, params)
    return [row[0] for row in rows.all() if row and row[0]]


async def _fetch_sample_events(
    db: AsyncSession,
    *,
    feature_expr: str,
    error_expr: str,
    feature: Optional[str],
    action: Optional[str],
    error_code: Optional[str],
    since: Optional[datetime],
    status: str = "failed",
    limit: int = 5,
) -> List[Dict[str, object]]:
    clauses = ["status = :status"]
    params: Dict[str, object] = {"limit": limit}
    params["status"] = status
    if since is not None:
        clauses.append("ts >= :since")
        params["since"] = since
    if feature is not None:
        clauses.append(f"{feature_expr} = :feature")
        params["feature"] = feature
    if action is not None:
        clauses.append("action = :action")
        params["action"] = action
    if error_code is not None:
        clauses.append(f"{error_expr} = :error_code")
        params["error_code"] = error_code

    stmt = text(
        f"""
        SELECT id, source_version, duration_ms
        FROM saas_events
        WHERE {' AND '.join(clauses)}
        ORDER BY ts DESC
        LIMIT :limit
        """
    )
    rows = await db.execute(stmt, params)
    return [
        {"id": row[0], "source_version": row[1], "duration_ms": row[2]}
        for row in rows.all()
    ]


async def _fetch_sample_event_ids(
    db: AsyncSession,
    *,
    feature_expr: str,
    error_expr: str,
    feature: Optional[str],
    action: Optional[str],
    error_code: Optional[str],
    since: Optional[datetime],
    status: str,
    limit: int,
) -> List[object]:
    clauses = ["status = :status"]
    params: Dict[str, object] = {"limit": limit, "status": status}
    if since is not None:
        clauses.append("ts >= :since")
        params["since"] = since
    if feature is not None:
        clauses.append(f"{feature_expr} = :feature")
        params["feature"] = feature
    if action is not None:
        clauses.append("action = :action")
        params["action"] = action
    if error_code is not None:
        clauses.append(f"{error_expr} = :error_code")
        params["error_code"] = error_code

    stmt = text(
        f"""
        SELECT id
        FROM saas_events
        WHERE {' AND '.join(clauses)}
        ORDER BY ts DESC
        LIMIT :limit
        """
    )
    rows = await db.execute(stmt, params)
    result = []
    for row in rows.all():
        if row and row[0]:
            val = row[0]
            # Convert string to UUID if needed (SQLite returns strings)
            if isinstance(val, str):
                try:
                    val = UUID(val.replace("-", ""))
                except ValueError:
                    continue
            result.append(val)
    return result


async def _upsert_insight_ticket(
    db: AsyncSession,
    *,
    fingerprint: str,
    ticket_type: InsightTicketType,
    severity: InsightTicketSeverity,
    title: str,
    source_version: Optional[str],
    primary_org_id: Optional[str],
    affected_org_ids: Sequence[str],
    affected_paying_orgs: int,
    context: Dict[str, object],
    event_count: int,
    affected_orgs: int,
    first_seen: datetime,
    last_seen: datetime,
    feature: Optional[str],
    action: Optional[str],
    error_code: Optional[str],
    sample_event_ids: Optional[Sequence[object]] = None,
) -> None:
    repo = TelemetryInsightRepo(db)
    existing = await repo.get_by_fingerprint(fingerprint)
    merged_context = dict(existing.context_json or {}) if existing else {}
    merged_context.update(context)
    sanitized = sanitize_context(merged_context)
    ticket = await repo.upsert_ticket(
        fingerprint=fingerprint,
        ticket_type=ticket_type,
        severity=severity,
        title=title,
        context_json=sanitized,
        primary_org_id=primary_org_id,
        affected_org_ids=affected_org_ids,
        feature=feature,
        action=action,
        error_code=error_code,
        source_version=source_version,
        event_count=event_count,
        affected_orgs=affected_orgs,
        affected_paying_orgs=affected_paying_orgs,
        first_seen_at=first_seen,
        last_seen_at=last_seen,
    )
    if existing is None:
        await repo.add_action(
            ticket_id=ticket.id,
            action_type="created",
            actor_email="system",
            details={"source": "detector"},
        )
    if sample_event_ids:
        await repo.add_samples(ticket_id=ticket.id, saas_event_ids=sample_event_ids)


async def detect_error_first_seen(db: AsyncSession) -> None:
    """
    Find error signatures seen in last 1 hour that weren't seen in prior 7 days.
    """
    now = datetime.now(timezone.utc)
    recent_window = now - timedelta(hours=1)
    baseline_start = now - timedelta(days=7)
    baseline_end = recent_window

    dialect_name = _dialect_name(db)
    feature_expr = _json_expr(dialect_name, "feature")
    error_expr = _json_expr(dialect_name, "error_code")

    recent_stmt = text(
        f"""
        SELECT
            {feature_expr} AS feature,
            action,
            {error_expr} AS error_code,
            source_version,
            COUNT(*) AS event_count,
            COUNT(DISTINCT org_id) AS affected_orgs,
            MIN(ts) AS first_seen,
            MAX(ts) AS last_seen
        FROM saas_events
        WHERE status = 'failed'
          AND ts >= :recent_window
          AND {error_expr} IS NOT NULL
        GROUP BY {feature_expr}, action, {error_expr}, source_version
        """
    )
    recent_rows = await db.execute(recent_stmt, {"recent_window": recent_window})

    for row in recent_rows.mappings():
        feature = row["feature"]
        action = row["action"]
        error_code = row["error_code"]

        if not feature or not action or not error_code:
            continue

        baseline_stmt = text(
            f"""
            SELECT 1 FROM saas_events
            WHERE status = 'failed'
              AND {feature_expr} = :feature
              AND action = :action
              AND {error_expr} = :error_code
              AND ts >= :baseline_start
              AND ts < :baseline_end
            LIMIT 1
            """
        )
        exists = await db.execute(
            baseline_stmt,
            {
                "feature": feature,
                "action": action,
                "error_code": error_code,
                "baseline_start": baseline_start,
                "baseline_end": baseline_end,
            },
        )

        if exists.scalar() is not None:
            continue

        org_ids = await _fetch_org_ids(
            db,
            feature_expr=feature_expr,
            error_expr=error_expr,
            feature=feature,
            action=action,
            error_code=error_code,
            since=recent_window,
        )
        affected_paying = await count_paying_orgs(db, org_ids)
        severity = classify_first_seen_severity(
            affected_orgs=int(row["affected_orgs"] or 0),
            event_count=int(row["event_count"] or 0),
            affected_paying=affected_paying,
        )
        severity_reasons = [
            "first_seen_last_1h",
            f"affected_orgs={int(row['affected_orgs'] or 0)}",
            f"event_count={int(row['event_count'] or 0)}",
        ]
        if affected_paying:
            severity_reasons.append(f"paying_orgs={affected_paying}")

        fingerprint = compute_fingerprint(
            "error_first_seen",
            feature=feature,
            action=action,
            error_code=error_code,
        )

        failed_sample_ids = await _fetch_sample_event_ids(
            db,
            feature_expr=feature_expr,
            error_expr=error_expr,
            feature=feature,
            action=action,
            error_code=error_code,
            since=recent_window,
            status="failed",
            limit=10,
        )
        started_sample_ids = await _fetch_sample_event_ids(
            db,
            feature_expr=feature_expr,
            error_expr=error_expr,
            feature=feature,
            action=action,
            error_code=None,
            since=recent_window,
            status="started",
            limit=5,
        )
        sample_events = await _fetch_sample_events(
            db,
            feature_expr=feature_expr,
            error_expr=error_expr,
            feature=feature,
            action=action,
            error_code=error_code,
            since=recent_window,
            status="failed",
            limit=5,
        )
        sample_attrs = [
            {
                "source_version": row.get("source_version"),
                "duration_ms": row.get("duration_ms"),
            }
            for row in sample_events
        ]

        context = {
            "type": "error_first_seen",
            "dimensions": {
                "feature": feature,
                "action": action,
                "error_code": error_code,
            },
            "recent": {
                "event_count": int(row["event_count"] or 0),
                "affected_orgs": int(row["affected_orgs"] or 0),
                "first_hour": recent_window.isoformat(),
            },
            "severity_reasons": severity_reasons,
            "sample_attrs": sample_attrs,
        }

        await _upsert_insight_ticket(
            db,
            fingerprint=fingerprint,
            ticket_type=InsightTicketType.ERROR_FIRST_SEEN,
            severity=severity,
            title=f"New error: {error_code} in {feature}/{action}",
            source_version=row.get("source_version"),
            primary_org_id=org_ids[0] if org_ids else None,
            affected_org_ids=org_ids,
            affected_paying_orgs=affected_paying,
            context=context,
            event_count=int(row["event_count"] or 0),
            affected_orgs=int(row["affected_orgs"] or 0),
            first_seen=_parse_ts(row["first_seen"], recent_window),
            last_seen=_parse_ts(row["last_seen"], now),
            feature=feature,
            action=action,
            error_code=error_code,
            sample_event_ids=failed_sample_ids + started_sample_ids,
        )


async def detect_error_rate_regression(db: AsyncSession) -> None:
    """
    Compare last 1-hour error rate to 7-day baseline.
    Alert if rate increases by >=2x AND absolute increase >=3%.
    """
    now = datetime.now(timezone.utc)
    recent_window = now - timedelta(hours=1)
    baseline_start = now - timedelta(days=7)

    dialect_name = _dialect_name(db)
    feature_expr = _json_expr(dialect_name, "feature")
    error_expr = _json_expr(dialect_name, "error_code")

    baseline_stmt = text(
        f"""
        SELECT
            {feature_expr} AS feature,
            action,
            COUNT(*) AS total,
            SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed
        FROM saas_events
        WHERE ts >= :baseline_start
          AND ts < :recent_window
          AND {feature_expr} IS NOT NULL
        GROUP BY {feature_expr}, action
        HAVING COUNT(*) >= :min_total
          AND SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) >= :min_failed
        """
    )
    baseline_rows = await db.execute(
        baseline_stmt,
        {
            "baseline_start": baseline_start,
            "recent_window": recent_window,
            "min_total": MIN_BASELINE_TOTAL,
            "min_failed": MIN_BASELINE_FAILED,
        },
    )

    baselines: Dict[tuple, Dict[str, float]] = {}
    for row in baseline_rows.mappings():
        key = (row["feature"], row["action"])
        total = float(row["total"] or 0)
        failed = float(row["failed"] or 0)
        baselines[key] = {
            "total": total,
            "failed": failed,
            "rate": failed / total if total else 0.0,
        }

    if not baselines:
        return

    recent_stmt = text(
        f"""
        SELECT
            {feature_expr} AS feature,
            action,
            source_version,
            COUNT(*) AS total,
            SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed,
            COUNT(DISTINCT org_id) AS affected_orgs
        FROM saas_events
        WHERE ts >= :recent_window
          AND {feature_expr} IS NOT NULL
        GROUP BY {feature_expr}, action, source_version
        HAVING SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) >= :min_failed
        """
    )
    recent_rows = await db.execute(
        recent_stmt,
        {"recent_window": recent_window, "min_failed": MIN_RECENT_FAILED},
    )

    for row in recent_rows.mappings():
        key = (row["feature"], row["action"])
        if key not in baselines:
            continue

        baseline = baselines[key]
        recent_rate = (
            float(row["failed"]) / float(row["total"]) if row["total"] else 0.0
        )
        baseline_rate = baseline["rate"]
        rate_ratio = recent_rate / baseline_rate if baseline_rate > 0 else float("inf")
        absolute_increase = recent_rate - baseline_rate

        if rate_ratio < MIN_RATE_RATIO or absolute_increase < MIN_ABSOLUTE_INCREASE:
            continue

        version_bucket = extract_version_bucket(row.get("source_version"))
        fingerprint = compute_fingerprint(
            "error_rate_regression",
            feature=row["feature"],
            action=row["action"],
            source_version=version_bucket,
        )

        org_ids = await _fetch_org_ids(
            db,
            feature_expr=feature_expr,
            error_expr=error_expr,
            feature=row["feature"],
            action=row["action"],
            error_code=None,
            since=recent_window,
        )
        affected_paying = await count_paying_orgs(db, org_ids)
        severity = classify_regression_severity(
            rate_ratio=rate_ratio,
            absolute_increase=absolute_increase,
            affected_orgs=int(row["affected_orgs"] or 0),
            affected_paying=affected_paying,
        )
        severity_reasons = [
            f"rate_ratio={rate_ratio:.1f}x",
            f"absolute_increase={absolute_increase:.1%}",
            f"affected_orgs={int(row['affected_orgs'] or 0)}",
        ]
        if affected_paying:
            severity_reasons.append(f"paying_orgs={affected_paying}")

        failed_sample_ids = await _fetch_sample_event_ids(
            db,
            feature_expr=feature_expr,
            error_expr=error_expr,
            feature=row["feature"],
            action=row["action"],
            error_code=None,
            since=recent_window,
            status="failed",
            limit=10,
        )
        started_sample_ids = await _fetch_sample_event_ids(
            db,
            feature_expr=feature_expr,
            error_expr=error_expr,
            feature=row["feature"],
            action=row["action"],
            error_code=None,
            since=recent_window,
            status="started",
            limit=5,
        )
        sample_events = await _fetch_sample_events(
            db,
            feature_expr=feature_expr,
            error_expr=error_expr,
            feature=row["feature"],
            action=row["action"],
            error_code=None,
            since=recent_window,
            status="failed",
            limit=5,
        )
        sample_attrs = [
            {
                "source_version": row.get("source_version"),
                "duration_ms": row.get("duration_ms"),
            }
            for row in sample_events
        ]

        context = {
            "type": "error_rate_regression",
            "dimensions": {
                "feature": row["feature"],
                "action": row["action"],
            },
            "baseline": {
                "total": baseline["total"],
                "failed": baseline["failed"],
                "rate": baseline_rate,
            },
            "recent": {
                "total": float(row["total"] or 0),
                "failed": float(row["failed"] or 0),
                "rate": recent_rate,
                "time_bucket": now.strftime("%Y-%m-%d-%H"),
            },
            "rate_ratio": rate_ratio,
            "absolute_increase": absolute_increase,
            "version_bucket": version_bucket,
            "severity_reasons": severity_reasons,
            "sample_attrs": sample_attrs,
        }

        await _upsert_insight_ticket(
            db,
            fingerprint=fingerprint,
            ticket_type=InsightTicketType.ERROR_RATE_REGRESSION,
            severity=severity,
            title=(
                f"Error rate spike: {row['feature']}/{row['action']} "
                f"({baseline_rate:.1%} -> {recent_rate:.1%})"
            ),
            source_version=row.get("source_version"),
            primary_org_id=org_ids[0] if org_ids else None,
            affected_org_ids=org_ids,
            affected_paying_orgs=affected_paying,
            context=context,
            event_count=int(row["failed"] or 0),
            affected_orgs=int(row["affected_orgs"] or 0),
            first_seen=now,
            last_seen=now,
            feature=row["feature"],
            action=row["action"],
            error_code=None,
            sample_event_ids=failed_sample_ids + started_sample_ids,
        )


async def run_all_detectors() -> None:
    async with AsyncSessionLocal() as db:
        await detect_error_first_seen(db)
        await detect_error_rate_regression(db)
        await auto_resolve_tickets(db)
        await auto_reopen_tickets(db)
        await evaluate_proactive_outreach(db)


def main() -> None:
    if len(sys.argv) < 2:
        print(
            "Usage: python -m src.fmatch.saas.cron.insight_detector_jobs <job-name>"
        )
        print("Jobs: run-all, error-first-seen, error-rate-regression, proactive-outreach")
        sys.exit(1)

    job_name = sys.argv[1]

    if job_name == "run-all":
        asyncio.run(run_all_detectors())
    elif job_name == "error-first-seen":
        asyncio.run(_run_single(detector=detect_error_first_seen))
    elif job_name == "error-rate-regression":
        asyncio.run(_run_single(detector=detect_error_rate_regression))
    elif job_name == "proactive-outreach":
        asyncio.run(_run_single(detector=evaluate_proactive_outreach))
    else:
        print(f"Unknown job: {job_name}")
        print("Available jobs: run-all, error-first-seen, error-rate-regression, proactive-outreach")
        sys.exit(1)


async def _run_single(detector) -> None:
    async with AsyncSessionLocal() as db:
        await detector(db)


def _status_for_ticket(ticket: TelemetryInsightTicket) -> Optional[str]:
    if ticket.type in (
        InsightTicketType.ERROR_FIRST_SEEN,
        InsightTicketType.ERROR_RATE_REGRESSION,
    ):
        return "failed"
    return None


def _bump_severity(severity: InsightTicketSeverity) -> InsightTicketSeverity:
    order = [
        InsightTicketSeverity.LOW,
        InsightTicketSeverity.MEDIUM,
        InsightTicketSeverity.HIGH,
        InsightTicketSeverity.CRITICAL,
    ]
    try:
        idx = order.index(severity)
    except ValueError:
        return InsightTicketSeverity.MEDIUM
    return order[min(idx + 1, len(order) - 1)]


async def _has_recent_events(
    db: AsyncSession,
    *,
    ticket: TelemetryInsightTicket,
    since: datetime,
) -> bool:
    status_filter = _status_for_ticket(ticket)
    if status_filter is None:
        return False

    dialect_name = _dialect_name(db)
    feature_expr = _json_expr(dialect_name, "feature")
    error_expr = _json_expr(dialect_name, "error_code")

    clauses = ["ts >= :since", "status = :status"]
    params: Dict[str, object] = {"since": since, "status": status_filter}

    if ticket.feature:
        clauses.append(f"{feature_expr} = :feature")
        params["feature"] = ticket.feature
    if ticket.action:
        clauses.append("action = :action")
        params["action"] = ticket.action
    if ticket.error_code:
        clauses.append(f"{error_expr} = :error_code")
        params["error_code"] = ticket.error_code

    stmt = text(f"SELECT 1 FROM saas_events WHERE {' AND '.join(clauses)} LIMIT 1")
    result = await db.execute(stmt, params)
    return result.scalar() is not None


async def _recent_event_counts(
    db: AsyncSession,
    *,
    ticket: TelemetryInsightTicket,
    since: datetime,
) -> Tuple[int, int]:
    status_filter = _status_for_ticket(ticket)
    if status_filter is None:
        return 0, 0

    dialect_name = _dialect_name(db)
    feature_expr = _json_expr(dialect_name, "feature")
    error_expr = _json_expr(dialect_name, "error_code")

    clauses = ["ts >= :since", "status = :status"]
    params: Dict[str, object] = {"since": since, "status": status_filter}

    if ticket.feature:
        clauses.append(f"{feature_expr} = :feature")
        params["feature"] = ticket.feature
    if ticket.action:
        clauses.append("action = :action")
        params["action"] = ticket.action
    if ticket.error_code:
        clauses.append(f"{error_expr} = :error_code")
        params["error_code"] = ticket.error_code

    stmt = text(
        f"""
        SELECT COUNT(*) AS total, COUNT(DISTINCT org_id) AS orgs
        FROM saas_events
        WHERE {' AND '.join(clauses)}
        """
    )
    row = (await db.execute(stmt, params)).first()
    if not row:
        return 0, 0
    return int(row[0] or 0), int(row[1] or 0)


async def auto_resolve_tickets(db: AsyncSession) -> None:
    now = datetime.now(timezone.utc)
    cutoff = now - timedelta(hours=24)

    stmt = select(TelemetryInsightTicket).where(
        TelemetryInsightTicket.status == InsightTicketStatus.OPEN
    )
    tickets = (await db.execute(stmt)).scalars().all()
    changed = False
    for ticket in tickets:
        has_events = await _has_recent_events(db, ticket=ticket, since=cutoff)
        context = ticket.context_json or {}
        if has_events:
            if context.pop("resolved_candidate_at", None):
                ticket.context_json = context
                ticket.updated_at = now
                db.add(
                    TelemetryInsightAction(
                        ticket_id=ticket.id,
                        action_type="auto_resolve_cleared",
                        actor_email="system",
                        details={"reason": "events_seen"},
                        created_at=now,
                    )
                )
                db.add(ticket)
                changed = True
            continue

        if context.get("resolved_candidate_at"):
            continue

        context["resolved_candidate_at"] = now.isoformat()
        ticket.context_json = context
        ticket.updated_at = now
        db.add(ticket)
        db.add(
            TelemetryInsightAction(
                ticket_id=ticket.id,
                action_type="auto_resolve_candidate",
                actor_email="system",
                details={"window_hours": 24},
                created_at=now,
            )
        )
        changed = True

    if changed:
        await db.commit()


async def auto_reopen_tickets(db: AsyncSession) -> None:
    now = datetime.now(timezone.utc)
    cutoff = now - timedelta(hours=72)

    stmt = select(TelemetryInsightTicket).where(
        TelemetryInsightTicket.status == InsightTicketStatus.CLOSED,
        TelemetryInsightTicket.closed_at.is_not(None),
        TelemetryInsightTicket.closed_at >= cutoff,
    )
    tickets = (await db.execute(stmt)).scalars().all()
    changed = False
    for ticket in tickets:
        since = ticket.closed_at or cutoff
        event_count, org_count = await _recent_event_counts(db, ticket=ticket, since=since)
        if event_count < MIN_REOPEN_EVENTS and org_count < MIN_REOPEN_ORGS:
            continue

        ticket.status = InsightTicketStatus.OPEN
        ticket.closed_at = None
        ticket.closed_by = None
        ticket.updated_at = now
        ticket.severity = _bump_severity(ticket.severity)
        db.add(ticket)
        db.add(
            TelemetryInsightAction(
                ticket_id=ticket.id,
                action_type="regressed",
                actor_email="system",
                details={"previous_status": "closed", "window_hours": 72},
                created_at=now,
            )
        )
        changed = True

    if changed:
        await db.commit()


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    main()
