    if campaign_id:
        q = q.where(EmailEvent.campaign_id == campaign_id)
    if event_type:
        q = q.where(EmailEvent.event_type == event_type)
    total = (await db.execute(
        select(func.count()).select_from(q.subquery()))).scalar() or 0
    rows = (await db.execute(
        q.order_by(EmailEvent.id.desc()).offset((page - 1) * page_size).limit(page_size)
    )).all()
    out = []
    for e, cr in rows:
        out.append({
            "id": e.id,
            "campaign_id": e.campaign_id,
            "email": cr.email,
            "event_type": e.event_type,
            "detail": (e.detail or "")[:300],
            "message_id": e.message_id,
            "created_at": e.created_at.isoformat() if e.created_at else None,
        })
    return {"events": out, "total": total, "page": page, "page_size": page_size}


@router.get("/audit")
async def list_audit(action: str = Query(""),
                     page: int = Query(1, ge=1),
                     page_size: int = Query(50, ge=1, le=200),
                     db: AsyncSession = Depends(get_db),
                     user: User = Depends(get_current_user)):
    """Audit log (own actions only). Secrets are redacted at write time."""
    q = select(AuditLog).where(AuditLog.user_id == user.id)
    if action:
        q = q.where(AuditLog.action == action)
    total = (await db.execute(
        select(func.count()).select_from(q.subquery()))).scalar() or 0
    rows = (await db.execute(
        q.order_by(AuditLog.id.desc()).offset((page - 1) * page_size).limit(page_size)
    )).scalars().all()
    out = []
    for r in rows:
        out.append({
            "id": r.id,
            "action": r.action,
            "entity": r.entity,
            "entity_id": r.entity_id,
            "detail": r.detail if isinstance(r.detail, dict) else {},
            "ip_address": r.ip_address,
            "created_at": r.created_at.isoformat() if r.created_at else None,
        })
    return {"audit": out, "total": total, "page": page, "page_size": page_size}


@router.get("/dlq")
async def list_dlq(page: int = Query(1, ge=1), page_size: int = Query(50, ge=1, le=200),
                   user: User = Depends(get_current_user)):
    """Dead-letter queue (Redis list dlq:send): jobs that exhausted retries."""
    import redis as redis_lib
    from app.core.config import settings
    r = redis_lib.Redis.from_url(settings.REDIS_URL, decode_responses=True)
    try:
        items = r.lrange("dlq:send", 0, -1)
    except Exception as e:  # Redis down — don't 500 the whole logs page
        return {"dlq": [], "total": 0, "error": f"Redis unavailable: {e}"}
    import json as _json
    out = []
    for raw in items:
        try:
            out.append(_json.loads(raw))
        except Exception:
            out.append({"raw": raw[:200]})
    total = len(out)
    start = (page - 1) * page_size
    return {"dlq": out[start:start + page_size], "total": total,
            "page": page, "page_size": page_size}


@router.get("/export")
async def export_logs(campaign_id: int = Query(0),
                      outcome: str = Query(""),
                      db: AsyncSession = Depends(get_db),
                      user: User = Depends(get_current_user)):
    """CSV export of send attempts for a campaign (all pages)."""
    q = (select(SendAttempt, CampaignRecipient)
         .join(CampaignRecipient, SendAttempt.campaign_recipient_id == CampaignRecipient.id)
         .join(Campaign, Campaign.id == SendAttempt.campaign_id)
         .where(Campaign.user_id == user.id))
    if campaign_id:
        q = q.where(SendAttempt.campaign_id == campaign_id)
    if outcome:
        q = q.where(SendAttempt.outcome == outcome)
    rows = (await db.execute(q.order_by(SendAttempt.id.asc()))).all()

    def generate():
        buf = io.StringIO()
        w = csv.writer(buf)
        w.writerow(["attempt_id", "campaign_id", "email", "attempt_number", "outcome",
                    "error_class", "error_message", "message_id", "provider_response", "timestamp"])
        yield buf.getvalue()
        buf.seek(0); buf.truncate(0)
        for a, cr in rows:
            w.writerow([a.id, a.campaign_id, cr.email, a.attempt_number, a.outcome,
                        a.error_class, (a.error_message or "")[:500], a.message_id,
                        (a.provider_response or "")[:500],
                        a.created_at.isoformat() if a.created_at else ""])
            yield buf.getvalue()
            buf.seek(0); buf.truncate(0)

    return StreamingResponse(generate(), media_type="text/csv",
                             headers={"Content-Disposition":
                                      f"attachment; filename=send_logs{('_' + str(campaign_id)) if campaign_id else 's'}.csv"})


@router.get("/recipient/{cr_id}/history")
async def recipient_history(cr_id: int, db: AsyncSession = Depends(get_db),
                            user: User = Depends(get_current_user)):
    """Full attempt history for one campaign recipient (modal in Logs page)."""
    cr = await db.get(CampaignRecipient, cr_id)
    if not cr:
        raise HTTPException(status_code=404, detail="Recipient not found")
    camp = await db.get(Campaign, cr.campaign_id)
    if not camp or camp.user_id != user.id:
