# src/fmatch/saas/api/v2/reporting.py
from __future__ import annotations
from typing import Optional, List, Dict, Any
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, desc, and_, insert
from sqlalchemy.exc import OperationalError, ArgumentError
from datetime import datetime, timedelta
import asyncio
import csv
import io
import uuid

from ...db import get_session
from ...auth import (
    get_current_account as require_account,
    get_current_user as require_user,
)
from ...model_defs.reporting_models import (
    ChangeSet,
    ChangeEvent,
    QASample,
    ReportingWatermark,
    MetricSnapshot,
    ChangeSubscription,
)
from ...services.reporting_emitter import (
    EventEmitter,
    decompress,
    _compress,
    _partition_key,
)
from ...services.salesforce_gateway import SalesforceGateway, get_salesforce_gateway

router = APIRouter(prefix="/api/v2", tags=["reporting"])
emitter = EventEmitter()


# ---- ChangeSets ----
@router.post("/changesets")
async def start_changeset(
    name: Optional[str] = None,
    type: str = Query(..., description="l2a_apply|dedupe_merge|routing_assign|..."),
    source: str = "ui",
    scope: Dict[str, Any] = {},
    policy_id: Optional[str] = None,
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
    user_id: str = Depends(require_user),
):
    cs = ChangeSet(
        id=str(uuid.uuid4()),
        account_id=str(account_id),
        name=name,
        type=type,
        source=source,
        scope=scope or {},
        policy_id=policy_id,
        actor_type="user" if user_id else "system",
        actor_id=user_id,
        status="open",
    )
    db.add(cs)
    await db.commit()
    return {"id": cs.id, "status": cs.status}


@router.post("/changesets/{changeset_id}/finalize")
async def finalize_changeset(
    changeset_id: str,
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
):
    cs = await db.get(ChangeSet, changeset_id)
    if not cs or cs.account_id != str(account_id):
        raise HTTPException(404, "Changeset not found")
    # roll-ups
    total = await db.scalar(
        select(func.count())
        .select_from(ChangeEvent)
        .where(ChangeEvent.changeset_id == changeset_id)
    )
    applied = await db.scalar(
        select(func.count())
        .select_from(ChangeEvent)
        .where(
            and_(
                ChangeEvent.changeset_id == changeset_id,
                ChangeEvent.status == "applied",
            )
        )
    )
    failed = await db.scalar(
        select(func.count())
        .select_from(ChangeEvent)
        .where(
            and_(
                ChangeEvent.changeset_id == changeset_id, ChangeEvent.status == "failed"
            )
        )
    )
    cs.attempted_count, cs.applied_count, cs.failed_count = (
        int(total or 0),
        int(applied or 0),
        int(failed or 0),
    )
    cs.status, cs.finalized_at = "finalized", datetime.utcnow()
    await db.commit()
    return {
        "id": cs.id,
        "attempted": cs.attempted_count,
        "applied": cs.applied_count,
        "failed": cs.failed_count,
        "status": cs.status,
    }


@router.get("/changesets")
async def list_changesets(
    from_dt: Optional[str] = None,
    to_dt: Optional[str] = None,
    source: Optional[str] = None,
    actor_id: Optional[str] = None,
    export: Optional[str] = Query(None, description="csv to export"),
    page: int = 1,
    page_size: int = 25,
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
):
    try:
        q = select(ChangeSet).where(ChangeSet.account_id == str(account_id))
        if from_dt:
            q = q.where(ChangeSet.created_at >= datetime.fromisoformat(from_dt))
        if to_dt:
            q = q.where(ChangeSet.created_at <= datetime.fromisoformat(to_dt))
        if source:
            q = q.where(ChangeSet.source == source)
        if actor_id:
            q = q.where(ChangeSet.actor_id == actor_id)
        q = q.order_by(desc(ChangeSet.created_at))
        total = await db.scalar(select(func.count()).select_from(q.subquery()))
        rows = (
            (await db.execute(q.offset((page - 1) * page_size).limit(page_size)))
            .scalars()
            .all()
        )
    except ArgumentError as e:
        # Mapping problem — keep UI alive rather than 500
        return {
            "items": [],
            "page": page,
            "page_size": page_size,
            "total": 0,
            "error": "ChangeSet mapping error",
            "detail": str(e),
        }
    except OperationalError as e:
        if "no such table" in str(e).lower():
            # Table doesn't exist yet
            return {
                "items": [],
                "page": page,
                "page_size": page_size,
                "total": 0,
                "error": "ChangeSets table not initialized",
            }
        raise

    if export == "csv":
        buf = io.StringIO()
        w = csv.writer(buf)
        w.writerow(
            [
                "id",
                "created_at",
                "type",
                "source",
                "actor_type",
                "actor_id",
                "status",
                "attempted",
                "applied",
                "failed",
            ]
        )
        for r in rows:
            w.writerow(
                [
                    r.id,
                    r.created_at,
                    r.type,
                    r.source,
                    r.actor_type,
                    r.actor_id,
                    r.status,
                    r.attempted_count,
                    r.applied_count,
                    r.failed_count,
                ]
            )
        return StreamingResponse(
            iter([buf.getvalue()]),
            media_type="text/csv",
            headers={"Content-Disposition": "attachment; filename=changesets.csv"},
        )
    return {
        "items": [
            {
                "id": r.id,
                "created_at": r.created_at,
                "type": r.type,
                "source": r.source,
                "actor_type": r.actor_type,
                "actor_id": r.actor_id,
                "status": r.status,
                "attempted": r.attempted_count,
                "applied": r.applied_count,
                "failed": r.failed_count,
            }
            for r in rows
        ],
        "total": int(total or 0),
        "page": page,
        "page_size": page_size,
    }


@router.get("/changesets/{changeset_id}")
async def get_changeset(
    changeset_id: str,
    sample: int = 50,
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
):
    try:
        cs = await db.get(ChangeSet, changeset_id)
        if not cs or cs.account_id != str(account_id):
            raise HTTPException(404, "Not found")
        events = (
            (
                await db.execute(
                    select(ChangeEvent)
                    .where(ChangeEvent.changeset_id == changeset_id)
                    .order_by(ChangeEvent.sequence_num)
                    .limit(sample)
                )
            )
            .scalars()
            .all()
        )
    except ArgumentError as e:
        return {
            "changeset": None,
            "events": [],
            "error": "Model mapping error",
            "detail": str(e),
        }
    except OperationalError as e:
        if "no such table" in str(e).lower():
            return {"changeset": None, "events": [], "error": "Tables not initialized"}
        raise

    return {
        "changeset": {
            "id": cs.id,
            "created_at": cs.created_at,
            "finalized_at": cs.finalized_at,
            "type": cs.type,
            "source": cs.source,
            "scope": cs.scope,
            "status": cs.status,
            "attempted": cs.attempted_count,
            "applied": cs.applied_count,
            "failed": cs.failed_count,
        },
        "events": [e.__dict__ for e in events],
    }


# ---- QA Sampling ----
@router.post("/qa/sample")
async def create_qa_samples(
    event_ids: List[str],
    rate: float = Query(0.05, ge=0.01, le=0.10, description="Sampling rate (1-10%)"),
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
):
    """Create QA samples for specified events"""
    import random

    # Sample events based on rate
    sample_size = max(1, int(len(event_ids) * rate))
    sampled_ids = random.sample(event_ids, min(sample_size, len(event_ids)))

    samples_created = 0
    for event_id in sampled_ids:
        # Check if sample already exists
        existing = await db.execute(
            select(QASample).where(
                QASample.event_id == event_id, QASample.account_id == str(account_id)
            )
        )
        if existing.scalar_one_or_none():
            continue

        sample = QASample(
            id=str(uuid.uuid4()),
            account_id=str(account_id),
            event_id=event_id,
            label=None,  # To be filled by reviewer
            reviewer_id=None,
            reviewed_at=None,
            created_at=datetime.utcnow(),
        )
        db.add(sample)
        samples_created += 1

    await db.commit()
    return {
        "samples_created": samples_created,
        "sample_rate": rate,
        "total_events": len(event_ids),
    }


@router.get("/qa/precision")
async def calculate_precision(
    from_date: Optional[datetime] = Query(None),
    to_date: Optional[datetime] = Query(None),
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
):
    """Calculate precision metrics from QA samples"""
    query = select(QASample).where(
        QASample.account_id == str(account_id),
        QASample.label.in_(["tp", "fp"]),  # Only labeled samples
    )

    if from_date:
        query = query.where(QASample.created_at >= from_date)
    if to_date:
        query = query.where(QASample.created_at <= to_date)

    result = await db.execute(query)
    samples = result.scalars().all()

    tp_count = sum(1 for s in samples if s.label == "tp")
    fp_count = sum(1 for s in samples if s.label == "fp")
    total = tp_count + fp_count

    precision = tp_count / total if total > 0 else 0.0

    return {
        "precision": precision,
        "true_positives": tp_count,
        "false_positives": fp_count,
        "total_samples": total,
        "from_date": from_date,
        "to_date": to_date,
    }


# ---- Timeline / audit ----
@router.get("/audit/logs")
async def audit_logs(
    object_type: str = Query(...),
    object_id: str = Query(...),
    export: Optional[str] = None,
    page: int = 1,
    page_size: int = 50,
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
    user=Depends(require_user),  # Get full user object for role check
):
    try:
        q = (
            select(ChangeEvent)
            .where(
                ChangeEvent.account_id == str(account_id),
                ChangeEvent.object_type == object_type,
                ChangeEvent.object_id == object_id,
            )
            .order_by(ChangeEvent.sequence_num.desc())
        )
        total = await db.scalar(select(func.count()).select_from(q.subquery()))
        rows = (
            (await db.execute(q.offset((page - 1) * page_size).limit(page_size)))
            .scalars()
            .all()
        )
    except ArgumentError as e:
        return {
            "items": [],
            "page": page,
            "page_size": page_size,
            "total": 0,
            "error": "ChangeEvent mapping error",
            "detail": str(e),
        }
    except OperationalError as e:
        if "no such table" in str(e).lower():
            return {
                "items": [],
                "page": page,
                "page_size": page_size,
                "total": 0,
                "error": "Audit logs table not initialized",
            }
        raise

    if export == "csv":
        # Check user role for CSV export
        is_admin = hasattr(user, "role") and user.role == "admin"

        buf = io.StringIO()
        w = csv.writer(buf)
        w.writerow(
            [
                "seq",
                "when",
                "op",
                "field",
                "before",
                "after",
                "actor",
                "confidence",
                "tier",
                "status",
            ]
        )
        for e in rows:
            # Apply same redaction logic for CSV
            before_val = decompress(e.before_value)
            after_val = decompress(e.after_value)

            # Only redact if not admin
            if not is_admin and (
                e.contains_pii
                or (
                    e.field
                    and any(
                        pii in e.field.lower()
                        for pii in ["ssn", "social", "tax", "ein", "passport"]
                    )
                )
            ):
                before_val = "***REDACTED***" if before_val else ""
                after_val = "***REDACTED***" if after_val else ""

            w.writerow(
                [
                    e.sequence_num,
                    e.applied_at,
                    e.op,
                    e.field,
                    before_val,
                    after_val,
                    f"{e.actor_type}:{e.actor_id}",
                    e.confidence,
                    e.tier,
                    e.status,
                ]
            )
        return StreamingResponse(
            iter([buf.getvalue()]),
            media_type="text/csv",
            headers={"Content-Disposition": "attachment; filename=audit.csv"},
        )
    # Enhanced role-based PII redaction
    import re

    def _detect_pii(field_name: str, value: str) -> bool:
        """Detect if a field/value contains PII"""
        if not value:
            return False

        # Field name indicators
        pii_fields = [
            "ssn",
            "social",
            "tax",
            "ein",
            "passport",
            "license",
            "credit",
            "card",
            "bank",
            "account",
            "routing",
        ]
        if any(pii in field_name.lower() for pii in pii_fields):
            return True

        # Pattern-based detection
        patterns = [
            r"\b\d{3}-\d{2}-\d{4}\b",  # SSN
            r"\b\d{2}-\d{7}\b",  # EIN
            r"\b[A-Z]\d{7,8}\b",  # Passport
            r"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b",  # Credit card
        ]
        for pattern in patterns:
            if re.search(pattern, value):
                return True

        return False

    def _redact(value: str, field: str, is_admin: bool, marked_pii: bool) -> str:
        """Apply role-based PII redaction"""
        if not value:
            return value

        # Admins see everything
        if is_admin:
            return value

        # Check if marked as PII or detected as PII
        if marked_pii or _detect_pii(field, value):
            # Partial redaction for emails (show domain)
            if "@" in value and "." in value:
                parts = value.split("@")
                if len(parts) == 2:
                    username = parts[0]
                    redacted = (
                        username[0] + "•" * (len(username) - 1) if username else "•••"
                    )
                    return f"{redacted}@{parts[1]}"

            # Partial redaction for phone numbers (show area code)
            phone_match = re.match(
                r"(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?(\d{3})[-.\s]?(\d{4})", value
            )
            if phone_match:
                groups = phone_match.groups()
                return f"{groups[0] or ''}(•••) •••-••••"

            # Full redaction for sensitive data
            if len(value) > 20:
                return value[:3] + "•" * (len(value) - 6) + value[-3:]
            else:
                return "•" * len(value)

        return value

    # Check user role
    is_admin = hasattr(user, "role") and user.role == "admin"

    # Process events with role-based redaction
    items = []
    for e in rows:
        before_val = decompress(e.before_value)
        after_val = decompress(e.after_value)

        items.append(
            {
                "sequence_num": e.sequence_num,
                "applied_at": e.applied_at,
                "op": e.op,
                "field": e.field,
                "before_value": _redact(
                    before_val, e.field or "", is_admin, e.contains_pii
                ),
                "after_value": _redact(
                    after_val, e.field or "", is_admin, e.contains_pii
                ),
                "actor_type": e.actor_type,
                "actor_id": e.actor_id,
                "status": e.status,
                "confidence": e.confidence,
                "tier": e.tier,
                "reason_text": e.reason_text,
                "changeset_id": e.changeset_id,
            }
        )

    return {
        "items": items,
        "total": int(total or 0),
        "page": page,
        "page_size": page_size,
    }


# ---- Revert semantics (field updates only, conflict-aware) ----
@router.post("/changesets/{changeset_id}/revert")
async def revert_changeset(
    changeset_id: str,
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
    user_id: str = Depends(require_user),
    sf: SalesforceGateway = Depends(get_salesforce_gateway),
):
    """Revert a changeset with conflict detection"""
    # For safety, only attempt revert of update_field ops
    events = (
        (
            await db.execute(
                select(ChangeEvent)
                .where(
                    ChangeEvent.changeset_id == changeset_id,
                    ChangeEvent.account_id == str(account_id),
                    ChangeEvent.op == "update_field",
                    ChangeEvent.status == "applied",
                )
                .order_by(ChangeEvent.sequence_num.desc())
            )
        )
        .scalars()
        .all()
    )

    if not events:
        return {
            "reverted": 0,
            "conflicts": [],
            "message": "No events eligible for revert",
        }

    # Using gateway; tokens are persisted via gateway internals

    conflicts = []
    reverted = 0

    # Create revert changeset
    revert_cs = ChangeSet(
        id=str(uuid.uuid4()),
        account_id=str(account_id),
        name=f"Revert of {changeset_id}",
        type="revert",
        source="ui",
        actor_type="user",
        actor_id=user_id,
        status="open",
    )
    db.add(revert_cs)
    await db.flush()

    for ev in events:
        try:
            # Fetch current value from Salesforce
            current_record = await sf.get_record(
                ev.object_type, ev.object_id, fields=[ev.field]
            )
            current_value = current_record.get(ev.field)

            # Check for conflict: current value must match what we expect (after_value)
            if str(current_value) != str(ev.after_value):
                conflicts.append(
                    {
                        "event_id": ev.id,
                        "object_type": ev.object_type,
                        "object_id": ev.object_id,
                        "field": ev.field,
                        "expected": ev.after_value,
                        "current": current_value,
                        "reason": "Value changed since original update",
                    }
                )
                continue

            # Safe to revert - write back the before_value
            await sf.update(
                ev.object_type,
                ev.object_id,
                {ev.field: ev.before_value},
            )

            # Mark event as reverted
            ev.status = "reverted"
            ev.reverted_at = datetime.utcnow()
            ev.revert_changeset_id = revert_cs.id
            reverted += 1

        except Exception as e:
            conflicts.append(
                {
                    "event_id": ev.id,
                    "object_type": ev.object_type,
                    "object_id": ev.object_id,
                    "field": ev.field,
                    "error": str(e),
                }
            )

    # Finalize revert changeset
    revert_cs.status = "finalized"
    revert_cs.finalized_at = datetime.utcnow()
    revert_cs.attempted_count = len(events)
    revert_cs.applied_count = reverted
    revert_cs.failed_count = len(conflicts)

    await db.commit()

    return {
        "reverted": reverted,
        "conflicts": conflicts,
        "revert_changeset_id": revert_cs.id,
        "message": f"Reverted {reverted} changes, {len(conflicts)} conflicts detected",
    }


# ---- Change Subscriptions ----
@router.post("/reporting/subscriptions")
async def create_subscription(
    object_type: str,
    object_id: str,
    notify_on: List[str] = Query(
        default=["update_field", "merge"], description="Event types to watch"
    ),
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
    user_id: str = Depends(require_user),
):
    """Subscribe to changes for a specific record"""
    # Check if subscription already exists
    existing = await db.execute(
        select(ChangeSubscription).where(
            ChangeSubscription.account_id == str(account_id),
            ChangeSubscription.user_id == user_id,
            ChangeSubscription.object_type == object_type,
            ChangeSubscription.object_id == object_id,
        )
    )
    if existing.scalar_one_or_none():
        return {"message": "Subscription already exists"}

    subscription = ChangeSubscription(
        id=str(uuid.uuid4()),
        account_id=str(account_id),
        user_id=user_id,
        object_type=object_type,
        object_id=object_id,
        notify_on=notify_on,
    )
    db.add(subscription)
    await db.commit()

    return {
        "subscription_id": subscription.id,
        "object_type": object_type,
        "object_id": object_id,
        "notify_on": notify_on,
    }


@router.get("/reporting/subscriptions")
async def list_subscriptions(
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
    user_id: str = Depends(require_user),
):
    """List all subscriptions for current user"""
    result = await db.execute(
        select(ChangeSubscription).where(
            ChangeSubscription.account_id == str(account_id),
            ChangeSubscription.user_id == user_id,
        )
    )
    subscriptions = result.scalars().all()

    return {
        "subscriptions": [
            {
                "id": s.id,
                "object_type": s.object_type,
                "object_id": s.object_id,
                "notify_on": s.notify_on,
            }
            for s in subscriptions
        ]
    }


@router.delete("/reporting/subscriptions/{subscription_id}")
async def delete_subscription(
    subscription_id: str,
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
    user_id: str = Depends(require_user),
):
    """Delete a subscription"""
    result = await db.execute(
        select(ChangeSubscription).where(
            ChangeSubscription.id == subscription_id,
            ChangeSubscription.account_id == str(account_id),
            ChangeSubscription.user_id == user_id,
        )
    )
    subscription = result.scalar_one_or_none()

    if not subscription:
        raise HTTPException(404, "Subscription not found")

    await db.delete(subscription)
    await db.commit()

    return {"message": "Subscription deleted"}


# ---- Background Rollup Task ----
async def incremental_rollup_task(db: AsyncSession):
    """Background task to incrementally process events and update metrics"""
    while True:
        try:
            # Get watermark
            watermark = await db.get(ReportingWatermark, "global_rollup")
            if not watermark:
                watermark = ReportingWatermark(
                    metric_name="global_rollup",
                    last_processed_at=datetime.utcfromtimestamp(0),
                    last_sequence=0,
                )
                db.add(watermark)
                await db.commit()

            # Process new events since watermark
            new_events = await db.execute(
                select(ChangeEvent)
                .where(ChangeEvent.sequence_num > watermark.last_sequence)
                .order_by(ChangeEvent.sequence_num)
                .limit(1000)  # Process in batches
            )
            events = new_events.scalars().all()

            if events:
                # Aggregate metrics by account
                account_metrics = {}
                for event in events:
                    if event.account_id not in account_metrics:
                        account_metrics[event.account_id] = {
                            "total_events": 0,
                            "by_type": {},
                            "by_status": {},
                        }

                    metrics = account_metrics[event.account_id]
                    metrics["total_events"] += 1
                    metrics["by_type"][event.op] = (
                        metrics["by_type"].get(event.op, 0) + 1
                    )
                    metrics["by_status"][event.status] = (
                        metrics["by_status"].get(event.status, 0) + 1
                    )

                # Update metric snapshots
                for account_id, metrics in account_metrics.items():
                    key = f"rollup.events.5min.{account_id}"
                    snapshot = await db.get(MetricSnapshot, key)
                    if not snapshot:
                        snapshot = MetricSnapshot(
                            metric_key=key,
                            account_id=account_id,
                            value={},
                            ttl_seconds=300,
                        )
                        db.add(snapshot)

                    # Merge metrics
                    current = snapshot.value or {}
                    current["total_events"] = (
                        current.get("total_events", 0) + metrics["total_events"]
                    )

                    for op, count in metrics["by_type"].items():
                        if "by_type" not in current:
                            current["by_type"] = {}
                        current["by_type"][op] = current["by_type"].get(op, 0) + count

                    for status, count in metrics["by_status"].items():
                        if "by_status" not in current:
                            current["by_status"] = {}
                        current["by_status"][status] = (
                            current["by_status"].get(status, 0) + count
                        )

                    snapshot.value = current
                    snapshot.computed_at = datetime.utcnow()

                # Update watermark
                watermark.last_sequence = events[-1].sequence_num
                watermark.last_processed_at = datetime.utcnow()
                watermark.last_event_id = events[-1].id

                await db.commit()

        except Exception as e:
            print(f"Rollup task error: {e}")
            await db.rollback()

        # Run every 5 minutes
        await asyncio.sleep(300)


# Background task for automatic archival
async def auto_archive_task(db: AsyncSession):
    """Automatically archive old events based on retention policy"""
    while True:
        try:
            # Run daily at 2 AM UTC
            now = datetime.utcnow()
            next_run = now.replace(hour=2, minute=0, second=0, microsecond=0)
            if next_run <= now:
                next_run += timedelta(days=1)

            wait_seconds = (next_run - now).total_seconds()
            await asyncio.sleep(wait_seconds)

            # Archive events older than 90 days for all accounts
            accounts_result = await db.execute(select(ChangeSet.account_id).distinct())
            account_ids = [row[0] for row in accounts_result]

            for account_id in account_ids:
                cutoff_date = datetime.utcnow() - timedelta(days=90)

                # Find events to archive (batch of 5000)
                events_to_archive = await db.execute(
                    select(ChangeEvent)
                    .where(
                        and_(
                            ChangeEvent.account_id == account_id,
                            ChangeEvent.applied_at < cutoff_date,
                            ChangeEvent.status.in_(["applied", "failed", "skipped"]),
                        )
                    )
                    .limit(5000)
                )
                events = events_to_archive.scalars().all()

                if events:
                    # Create archive
                    import json
                    import zlib
                    import base64

                    archive_id = str(uuid.uuid4())
                    archive_data = {
                        "archive_id": archive_id,
                        "account_id": account_id,
                        "created_at": datetime.utcnow().isoformat(),
                        "cutoff_date": cutoff_date.isoformat(),
                        "event_count": len(events),
                        "events": [
                            {
                                "id": e.id,
                                "sequence_num": e.sequence_num,
                                "applied_at": e.applied_at.isoformat()
                                if e.applied_at
                                else None,
                                "op": e.op,
                                "field": e.field,
                                "before_value": decompress(e.before_value),
                                "after_value": decompress(e.after_value),
                                "object_type": e.object_type,
                                "object_id": e.object_id,
                                "changeset_id": e.changeset_id,
                                "contains_pii": e.contains_pii,
                            }
                            for e in events
                        ],
                    }

                    # Compress and store
                    archive_json = json.dumps(archive_data)
                    compressed = zlib.compress(archive_json.encode("utf-8"))
                    archive_b64 = base64.b64encode(compressed).decode("ascii")

                    # Store archive
                    archive_record = MetricSnapshot(
                        metric_key=archive_id,
                        account_id=account_id,
                        computed_at=datetime.utcnow(),
                        value={
                            "archive_b64": archive_b64,
                            "event_count": len(events),
                            "cutoff_date": cutoff_date.isoformat(),
                            "compressed_size": len(compressed),
                            "auto_archived": True,
                        },
                        ttl_seconds=86400 * 30,  # 30 days
                    )
                    db.add(archive_record)

                    # Delete archived events
                    for event in events:
                        await db.delete(event)

                    await db.commit()
                    print(
                        f"Auto-archived {len(events)} events for account {account_id}"
                    )

        except Exception as e:
            print(f"Auto-archive error: {e}")
            await asyncio.sleep(3600)  # Wait an hour on error


# ---- Retention Policy & Archival ----
@router.post("/reporting/archive")
async def archive_old_events(
    days_to_keep: int = Query(
        90, ge=30, le=365, description="Days of data to keep active"
    ),
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
    user=Depends(require_user),
):
    """Archive events older than specified days"""
    import json
    import zlib
    import base64
    from datetime import timedelta

    # Only admins can trigger archival
    if not (hasattr(user, "role") and user.role == "admin"):
        raise HTTPException(403, "Admin access required")

    cutoff_date = datetime.utcnow() - timedelta(days=days_to_keep)

    # Find events to archive
    events_to_archive = await db.execute(
        select(ChangeEvent)
        .where(
            and_(
                ChangeEvent.account_id == str(account_id),
                ChangeEvent.applied_at < cutoff_date,
                ChangeEvent.status != "archived",  # Don't re-archive
            )
        )
        .limit(10000)  # Process in batches
    )
    events = events_to_archive.scalars().all()

    if not events:
        return {"message": "No events to archive", "archived_count": 0}

    # Create archive entry
    archive_id = str(uuid.uuid4())
    archive_data = {
        "archive_id": archive_id,
        "account_id": str(account_id),
        "created_at": datetime.utcnow().isoformat(),
        "cutoff_date": cutoff_date.isoformat(),
        "event_count": len(events),
        "events": [],
    }

    # Compress and store event data
    for event in events:
        archive_data["events"].append(
            {
                "id": event.id,
                "sequence_num": event.sequence_num,
                "applied_at": event.applied_at.isoformat()
                if event.applied_at
                else None,
                "op": event.op,
                "field": event.field,
                "before_value": decompress(event.before_value),
                "after_value": decompress(event.after_value),
                "object_type": event.object_type,
                "object_id": event.object_id,
                "changeset_id": event.changeset_id,
                "contains_pii": event.contains_pii,
            }
        )

        # Mark as archived
        event.status = "archived"

    # Store archive in a compressed format
    archive_json = json.dumps(archive_data)
    compressed_archive = zlib.compress(archive_json.encode("utf-8"))
    archive_b64 = base64.b64encode(compressed_archive).decode("ascii")

    # Store in MetricSnapshot table (repurposed for archives)
    archive_record = MetricSnapshot(
        metric_key=archive_id,
        account_id=str(account_id),
        computed_at=datetime.utcnow(),
        value={
            "archive_b64": archive_b64,
            "event_count": len(events),
            "cutoff_date": cutoff_date.isoformat(),
            "compressed_size": len(compressed_archive),
        },
        ttl_seconds=86400 * 30,  # 30 days
    )
    db.add(archive_record)

    await db.commit()

    return {
        "message": f"Successfully archived {len(events)} events",
        "archive_id": archive_id,
        "archived_count": len(events),
        "cutoff_date": cutoff_date.isoformat(),
        "compressed_size_kb": round(len(compressed_archive) / 1024, 2),
    }


@router.get("/reporting/archives")
async def list_archives(
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
):
    """List available archives"""
    stmt = (
        select(MetricSnapshot)
        .where(
            and_(
                MetricSnapshot.account_id == str(account_id),
                MetricSnapshot.metric_key.like("archive.%"),
            )
        )
        .order_by(MetricSnapshot.computed_at.desc())
    )
    result = await db.execute(stmt)
    archives = result.scalars().all()

    return {
        "archives": [
            {
                "id": a.metric_key,
                "created_at": a.computed_at,
                "event_count": a.value.get("event_count") if a.value else 0,
                "cutoff_date": a.value.get("cutoff_date") if a.value else None,
                "compressed_size_kb": round(a.value.get("compressed_size", 0) / 1024, 2)
                if a.value
                else 0,
            }
            for a in archives
        ]
    }


@router.post("/reporting/archives/{archive_id}/restore")
async def restore_archive(
    archive_id: str,
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
    user=Depends(require_user),
):
    """Restore events from an archive"""
    import json
    import zlib
    import base64

    # Only admins can restore
    if not (hasattr(user, "role") and user.role == "admin"):
        raise HTTPException(403, "Admin access required")

    # Get archive
    archive = await db.get(MetricSnapshot, archive_id)
    if not archive or archive.account_id != str(account_id):
        raise HTTPException(404, "Archive not found")

    if archive.metric_type != "archive":
        raise HTTPException(400, "Not an archive")

    # Decompress archive
    archive_b64 = archive.data.get("archive_b64")
    if not archive_b64:
        raise HTTPException(500, "Archive data corrupted")

    compressed = base64.b64decode(archive_b64)
    archive_json = zlib.decompress(compressed).decode("utf-8")
    archive_data = json.loads(archive_json)

    # Restore events
    restored_count = 0
    for event_data in archive_data["events"]:
        # Check if event already exists
        existing = await db.execute(
            select(ChangeEvent).where(ChangeEvent.id == event_data["id"])
        )
        if existing.scalar_one_or_none():
            continue

        # Recreate event
        event = ChangeEvent(
            id=event_data["id"],
            account_id=str(account_id),
            sequence_num=event_data["sequence_num"],
            applied_at=datetime.fromisoformat(event_data["applied_at"])
            if event_data["applied_at"]
            else None,
            op=event_data["op"],
            field=event_data["field"],
            before_value=_compress(event_data["before_value"]),
            after_value=_compress(event_data["after_value"]),
            object_type=event_data["object_type"],
            object_id=event_data["object_id"],
            changeset_id=event_data["changeset_id"],
            contains_pii=event_data.get("contains_pii", False),
            status="restored",
            partition_key=_partition_key(datetime.utcnow()),
        )
        db.add(event)
        restored_count += 1

    await db.commit()

    return {
        "message": f"Successfully restored {restored_count} events",
        "archive_id": archive_id,
        "restored_count": restored_count,
        "total_events": len(archive_data["events"]),
    }


# ---- Metrics (with enhanced TTL-based caching) ----
@router.get("/reporting/metrics")
async def reporting_metrics(
    force: bool = Query(False, description="Force refresh, bypass cache"),
    period: str = Query("24h", description="Time period: 1h, 24h, 7d, 30d"),
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
):
    """Get reporting metrics with intelligent caching based on period"""

    # Determine TTL based on period
    ttl_map = {
        "1h": 60,  # 1 minute cache for hourly
        "24h": 300,  # 5 minutes cache for daily
        "7d": 1800,  # 30 minutes cache for weekly
        "30d": 3600,  # 1 hour cache for monthly
    }
    ttl_seconds = ttl_map.get(period, 300)

    # Cache key includes period for different granularities
    cache_key = f"reporting.metrics.{period}.{account_id}"

    # Try to get from cache
    if not force:
        try:
            stmt = select(MetricSnapshot).where(
                and_(
                    MetricSnapshot.metric_key == cache_key,
                    MetricSnapshot.account_id == str(account_id),
                )
            )
            result = await db.execute(stmt)
            snap = result.scalar_one_or_none()

            if snap and snap.computed_at:
                age_seconds = (datetime.utcnow() - snap.computed_at).total_seconds()
                if age_seconds < ttl_seconds:
                    return {
                        "metrics": snap.value,
                        "computed_at": snap.computed_at,
                        "cache_hit": True,
                        "cache_age_seconds": int(age_seconds),
                        "ttl_seconds": ttl_seconds,
                    }
        except ArgumentError as e:
            # Mapping error - model not properly registered
            return {
                "metrics": {},
                "computed_at": datetime.utcnow(),
                "cache_hit": False,
                "error": "MetricSnapshot mapping error",
                "detail": str(e),
            }
        except OperationalError as e:
            if "no such table" in str(e).lower():
                # Table doesn't exist yet, return empty metrics
                return {
                    "metrics": {},
                    "computed_at": datetime.utcnow(),
                    "cache_hit": False,
                    "error": "Metrics table not initialized",
                }
            raise

    # Compute metrics (cache miss or forced refresh)
    now = datetime.utcnow()

    # Calculate time window based on period
    if period == "1h":
        start_time = now - timedelta(hours=1)
    elif period == "24h":
        start_time = now - timedelta(days=1)
    elif period == "7d":
        start_time = now - timedelta(days=7)
    elif period == "30d":
        start_time = now - timedelta(days=30)
    else:
        start_time = now - timedelta(days=1)

    # Compute comprehensive metrics
    metrics = {}

    # 1. Total events in period
    total_events = await db.scalar(
        select(func.count())
        .select_from(ChangeEvent)
        .where(
            and_(
                ChangeEvent.account_id == str(account_id),
                ChangeEvent.applied_at >= start_time,
            )
        )
    )
    metrics["total_events"] = int(total_events or 0)

    # 2. Events by operation type
    events_by_op = await db.execute(
        select(ChangeEvent.op, func.count())
        .where(
            and_(
                ChangeEvent.account_id == str(account_id),
                ChangeEvent.applied_at >= start_time,
            )
        )
        .group_by(ChangeEvent.op)
    )
    metrics["by_operation"] = {op: int(count) for op, count in events_by_op}

    # 3. Events by object type
    events_by_obj = await db.execute(
        select(ChangeEvent.object_type, func.count())
        .where(
            and_(
                ChangeEvent.account_id == str(account_id),
                ChangeEvent.applied_at >= start_time,
            )
        )
        .group_by(ChangeEvent.object_type)
    )
    metrics["by_object_type"] = {obj: int(count) for obj, count in events_by_obj}

    # 4. Events by status
    events_by_status = await db.execute(
        select(ChangeEvent.status, func.count())
        .where(
            and_(
                ChangeEvent.account_id == str(account_id),
                ChangeEvent.applied_at >= start_time,
            )
        )
        .group_by(ChangeEvent.status)
    )
    metrics["by_status"] = {status: int(count) for status, count in events_by_status}

    # 5. Active changesets
    active_changesets = await db.scalar(
        select(func.count())
        .select_from(ChangeSet)
        .where(
            and_(
                ChangeSet.account_id == str(account_id),
                ChangeSet.created_at >= start_time,
                ChangeSet.status == "open",
            )
        )
    )
    metrics["active_changesets"] = int(active_changesets or 0)

    # 6. QA samples (if any)
    qa_pending = await db.scalar(
        select(func.count())
        .select_from(QASample)
        .where(
            and_(
                QASample.account_id == str(account_id),
                QASample.created_at >= start_time,
                QASample.reviewed_at.is_(None),
            )
        )
    )
    metrics["qa_pending_review"] = int(qa_pending or 0)

    # 7. Routing metrics (new events we added)
    routing_events = await db.execute(
        select(ChangeEvent.op, func.count())
        .where(
            and_(
                ChangeEvent.account_id == str(account_id),
                ChangeEvent.applied_at >= start_time,
                ChangeEvent.op.in_(["assign_owner", "assign_pool", "routing_failed"]),
            )
        )
        .group_by(ChangeEvent.op)
    )
    routing_metrics = {op: int(count) for op, count in routing_events}
    metrics["routing"] = {
        "total_routed": routing_metrics.get("assign_owner", 0),
        "total_pooled": routing_metrics.get("assign_pool", 0),
        "total_failed": routing_metrics.get("routing_failed", 0),
    }

    # 8. Calculate rates
    if period == "1h":
        time_divisor = 1
        rate_label = "per_hour"
    elif period == "24h":
        time_divisor = 24
        rate_label = "per_hour"
    elif period == "7d":
        time_divisor = 7
        rate_label = "per_day"
    elif period == "30d":
        time_divisor = 30
        rate_label = "per_day"
    else:
        time_divisor = 1
        rate_label = "per_period"

    metrics["rates"] = {
        rate_label: round(metrics["total_events"] / time_divisor, 2)
        if time_divisor > 0
        else 0
    }

    # Store in cache (if table exists)
    try:
        cache_entry = MetricSnapshot(
            metric_key=cache_key,
            account_id=str(account_id),
            computed_at=now,
            value=metrics,
            ttl_seconds=ttl_seconds,
        )

        # Upsert (update if exists, insert if not)
        await db.merge(cache_entry)
        await db.commit()
    except OperationalError as e:
        if "no such table" not in str(e).lower():
            raise
        # Table doesn't exist, skip caching but still return metrics

    return {
        "metrics": metrics,
        "computed_at": now,
        "cache_hit": False,
        "ttl_seconds": ttl_seconds,
        "period": period,
    }


# ---- Cache Management ----
@router.post("/reporting/cache/invalidate")
async def invalidate_cache(
    metric_type: Optional[str] = Query(
        None, description="Specific metric type to invalidate"
    ),
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
    user=Depends(require_user),
):
    """Invalidate cached metrics for the account"""
    # Only admins or system can invalidate cache
    if not (hasattr(user, "role") and user.role in ["admin", "system"]):
        # Regular users can only invalidate their own recent cache
        cutoff = datetime.utcnow() - timedelta(minutes=1)
        q = select(MetricSnapshot).where(
            and_(
                MetricSnapshot.account_id == str(account_id),
                MetricSnapshot.computed_at > cutoff,
            )
        )
    else:
        # Admins can invalidate all cache for the account
        q = select(MetricSnapshot).where(MetricSnapshot.account_id == str(account_id))

    if metric_type:
        q = q.where(MetricSnapshot.metric_key.like(f"{metric_type}.%"))

    # Delete cached entries
    result = await db.execute(q)
    snapshots = result.scalars().all()
    deleted_count = len(snapshots)

    for snap in snapshots:
        await db.delete(snap)

    await db.commit()

    return {
        "message": f"Invalidated {deleted_count} cached metric entries",
        "deleted_count": deleted_count,
        "account_id": str(account_id),
    }


@router.get("/reporting/cache/status")
async def cache_status(
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
):
    """Get cache status and statistics"""
    now = datetime.utcnow()

    # Get all cache entries for account
    stmt = (
        select(MetricSnapshot)
        .where(
            and_(
                MetricSnapshot.account_id == str(account_id),
                MetricSnapshot.metric_key.like("reporting.metrics.%"),
            )
        )
        .order_by(MetricSnapshot.computed_at.desc())
    )
    result = await db.execute(stmt)
    snapshots = result.scalars().all()

    cache_info = []
    for snap in snapshots:
        age_seconds = (
            (now - snap.computed_at).total_seconds() if snap.computed_at else 0
        )
        cache_info.append(
            {
                "key": snap.metric_key,
                "type": snap.metric_key.split(".")[0]
                if "." in snap.metric_key
                else "unknown",
                "computed_at": snap.computed_at,
                "age_seconds": int(age_seconds),
                "size_bytes": len(str(snap.value)) if snap.value else 0,
            }
        )

    # Calculate cache statistics
    total_size = sum(c["size_bytes"] for c in cache_info)
    avg_age = (
        sum(c["age_seconds"] for c in cache_info) / len(cache_info) if cache_info else 0
    )

    return {
        "cache_entries": cache_info,
        "total_entries": len(cache_info),
        "total_size_bytes": total_size,
        "average_age_seconds": round(avg_age, 2),
        "account_id": str(account_id),
    }


# ---- QA sampling ----
@router.post("/reporting/qa/sample")
async def create_qa_sample(
    rate: float = Query(0.02, ge=0.0, le=1.0),
    db: AsyncSession = Depends(get_session),
    account_id: str = Depends(require_account),
):
    # naive reservoir: sample last 10k events
    last = (
        (
            await db.execute(
                select(ChangeEvent.id)
                .where(ChangeEvent.account_id == str(account_id))
                .order_by(ChangeEvent.sequence_num.desc())
                .limit(10000)
            )
        )
        .scalars()
        .all()
    )
    import random
    import uuid

    picks = [eid for eid in last if random.random() < rate]
    rows = [
        {"id": str(uuid.uuid4()), "account_id": str(account_id), "event_id": eid}
        for eid in picks
    ]
    if rows:
        await db.execute(insert(QASample), rows)
        await db.commit()
    return {"queued": len(rows)}


# ---- Debug endpoints (dev only) ----
@router.get("/_debug/mapping")
async def mapping_debug():
    """Debug endpoint to check if MetricSnapshot is properly mapped"""
    from sqlalchemy.orm import class_mapper

    results = {}

    # Check MetricSnapshot mapping
    try:
        class_mapper(MetricSnapshot)
        results["MetricSnapshot_mapped"] = True
        results["MetricSnapshot_error"] = None
    except Exception as e:
        results["MetricSnapshot_mapped"] = False
        results["MetricSnapshot_error"] = str(e)

    # Check other reporting models
    models_to_check = [
        ("ChangeSet", ChangeSet),
        ("ChangeEvent", ChangeEvent),
        ("QASample", QASample),
        ("ReportingWatermark", ReportingWatermark),
        ("ChangeSubscription", ChangeSubscription),
    ]

    for name, model_class in models_to_check:
        try:
            class_mapper(model_class)
            results[f"{name}_mapped"] = True
        except Exception as e:
            results[f"{name}_mapped"] = False
            results[f"{name}_error"] = str(e)

    return results


@router.get("/_debug/mapping/detail")
async def mapping_detail():
    """Detailed mapping information for troubleshooting"""
    from sqlalchemy.orm import class_mapper

    out = {}

    def snap(cls):
        try:
            m = class_mapper(cls)
            return {
                "mapped": True,
                "module": cls.__module__,
                "table": getattr(cls, "__tablename__", None),
                "registry_id": id(m.registry),
                "table_key": str(getattr(cls, "__table__", None)),
            }
        except Exception as e:
            return {
                "mapped": False,
                "error": str(e),
                "module": getattr(cls, "__module__", "unknown"),
            }

    # Check all reporting models (use same import names as l2a.py for consistency)
    from ...model_defs.reporting_models import ChangeSet as ORMChangeSet

    out["ChangeSet"] = snap(ORMChangeSet)  # use the same import as l2a.py
    out["ChangeEvent"] = snap(ChangeEvent)
    out["MetricSnapshot"] = snap(MetricSnapshot)
    out["QASample"] = snap(QASample)
    out["ReportingWatermark"] = snap(ReportingWatermark)
    out["ChangeSubscription"] = snap(ChangeSubscription)

    return out
