"""Campaign logs and audit log endpoints: send attempts, events, errors with filters,
CSV export, per-recipient history. Combined logs view for the Logs page."""
import csv
import io
from datetime import datetime, timezone

from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from sqlalchemy import func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.api.deps import get_current_user, get_db
from app.models import (AuditLog, Campaign, CampaignRecipient, EmailEvent,
                        SendAttempt, SendJob, User)

router = APIRouter(prefix="/logs", tags=["logs"])


def _ser_attempt(a: SendAttempt, cr_id: int = None) -> dict:
    return {
        "id": a.id,
        "campaign_id": a.campaign_id,
        "campaign_recipient_id": cr_id if cr_id is not None else (a.send_job.campaign_recipient_id if a.send_job else None),
        "account_id": a.gmail_account_id,
        "attempt_number": (a.retry_no or 0) + 1,
        "outcome": a.outcome,
        "error_class": a.error_class,
        "error_message": (a.failure_reason or "")[:300],
        "message_id": a.message_id,
        "thread_id": getattr(a, "thread_id", "") or "",
        "method": a.method,
        "subject": (a.subject or "")[:160],
        "template_name": (a.template_name or "")[:160],
        "provider_response": (a.provider_response or "")[:300],
        "created_at": a.created_at.isoformat() if a.created_at else None,
    }


@router.get("/list")
async def list_logs(campaign_id: int = Query(0),
                    outcome: str = Query("", description="submitted|accepted|temp_failed|perm_failed|skipped"),
                    search: str = Query("", description="email or message id fragment"),
                    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)):
    """Unified send log: SendAttempt rows joined via SendJob to CampaignRecipient."""
    q = (select(SendAttempt, SendJob)
         .join(SendJob, SendAttempt.send_job_id == SendJob.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)
    if search:
        like = f"%{search}%"
        q = q.where(or_(SendAttempt.recipient_email.ilike(like),
                        SendAttempt.message_id.ilike(like)))
    total = (await db.execute(
        select(func.count()).select_from(q.subquery()))).scalar() or 0
    rows = (await db.execute(
        q.order_by(SendAttempt.id.desc()).offset((page - 1) * page_size).limit(page_size)
    )).all()
    out = []
    for a, job in rows:
        d = _ser_attempt(a, cr_id=job.campaign_recipient_id)
        d["email"] = a.recipient_email
        d["recipient_status"] = None  # filled cheaply below for joined rows
        out.append(d)
    if out:
        cr_ids = [d["campaign_recipient_id"] for d in out if d["campaign_recipient_id"]]
        if cr_ids:
            crs = (await db.execute(
                select(CampaignRecipient.id, CampaignRecipient.status)
                .where(CampaignRecipient.id.in_(cr_ids)))).all()
            smap = {r[0]: r[1] for r in crs}
            for d in out:
                if d["campaign_recipient_id"]:
                    d["recipient_status"] = smap.get(d["campaign_recipient_id"])
    return {"logs": out, "total": total, "page": page, "page_size": page_size}


@router.get("/events")
async def list_events(campaign_id: int = Query(0),
                      event_type: 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)):
    q = (select(EmailEvent, CampaignRecipient)
         .join(CampaignRecipient, EmailEvent.campaign_recipient_id == CampaignRecipient.id)
         .join(Campaign, Campaign.id == EmailEvent.campaign_id)
         .where(Campaign.user_id == user.id))
    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}


# Canonical filter values the application can actually write. The DB-distinct
# values seen so far are merged in at runtime by /logs/filters so dropdowns can
# never desync from reality again (bug #9).
_CANON_OUTCOMES = ["submitted", "accepted", "temp_failed", "perm_failed", "skipped"]
_CANON_EVENT_TYPES = ["sent", "failed", "perm_failed", "suppressed", "unsubscribed"]
_CANON_AUDIT_ACTIONS = sorted({
    "attachment.clear_all", "attachment.convert_pdf", "attachment.delete", "attachment.upload",
    "campaign.cancel", "campaign.clear_all", "campaign.create", "campaign.delete",
    "campaign.patch", "campaign.pause", "campaign.resume", "campaign.start",
    "gmail.authorized", "gmail.clear_all", "gmail.delete", "gmail.disconnect", "gmail.test",
    "oauth_config.imported",
    "phone.add", "phone.clear_all", "phone.delete", "phone.import", "phone.patch",
    "recipients.import", "recipients.manual_add",
    "settings.reset", "settings.reset_all", "settings.save", "settings.save_batch",
    "suppression.add", "suppression.bulk", "suppression.clear_all", "suppression.delete",
    "suppression.import",
    "test_send.error", "test_send.failed", "test_send.success",
    "user.register",
})


@router.get("/filters")
async def list_filter_values(db: AsyncSession = Depends(get_db),
                             user: User = Depends(get_current_user)):
    """Distinct filter values for the Logs page dropdowns: canonical values the
    app can write, merged with values actually present in this user's data."""
    async def _distinct(stmt):
        return sorted({v for (v,) in (await db.execute(stmt)).all() if v})

    own_campaign_ids = select(Campaign.id).where(Campaign.user_id == user.id)
    out_outcomes = _distinct(select(SendAttempt.outcome).where(
        SendAttempt.campaign_id.in_(own_campaign_ids)))
    out_events = _distinct(select(EmailEvent.event_type).where(
        EmailEvent.campaign_id.in_(own_campaign_ids)))
    out_audit = _distinct(select(AuditLog.action).where(AuditLog.user_id == user.id))

    def _merge(canon, actual):
        return sorted(set(canon) | set(actual))

    return {
        "outcomes": _merge(_CANON_OUTCOMES, await out_outcomes),
        "event_types": _merge(_CANON_EVENT_TYPES, await out_events),
        "audit_actions": _merge(_CANON_AUDIT_ACTIONS, await out_audit),
    }


@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)
         .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()))).scalars().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", "method",
                    "provider_response", "timestamp"])
        yield buf.getvalue()
        buf.seek(0); buf.truncate(0)
        for a in rows:
            w.writerow([a.id, a.campaign_id, a.recipient_email, (a.retry_no or 0) + 1, a.outcome,
                        a.error_class, (a.failure_reason or "")[:500], a.message_id, a.method,
                        (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:
        raise HTTPException(status_code=404, detail="Recipient not found")
    attempts = (await db.execute(
        select(SendAttempt)
        .join(SendJob, SendAttempt.send_job_id == SendJob.id)
        .where(SendJob.campaign_recipient_id == cr_id)
        .order_by(SendAttempt.id.asc()))).scalars().all()
    events = (await db.execute(
        select(EmailEvent).where(EmailEvent.campaign_recipient_id == cr_id)
        .order_by(EmailEvent.id.asc()))).scalars().all()
    return {
        "recipient": {
            "id": cr.id, "email": cr.email, "status": cr.status, "seq": cr.seq,
            "subject_id": cr.generated_subject_id, "template_id": cr.generated_template_id,
            "invoice_number": cr.generated_invoice_number,
            "amount": cr.generated_amount, "message_id": cr.msg_id,
            "last_error": cr.last_error,
        },
        "attempts": [_ser_attempt(a, cr_id=cr_id) for a in attempts],
        "events": [{
            "id": e.id, "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,
        } for e in events],
    }
