"""Admin routes: login, list/view applications, NID image serve, approve/reject,
coupons CRUD, settings get/update, dashboard stats."""
import os
import json
import logging
from pathlib import Path
from datetime import datetime, timezone
from fastapi import APIRouter, HTTPException, Depends, Query, UploadFile, File
from fastapi.responses import FileResponse, Response
from typing import Optional, List

from models import (
    AdminLoginRequest,
    TokenResponse,
    Settings,
    SettingsUpdate,
    Coupon,
    CouponCreate,
    CouponUpdate,
    Application,
    ApplicationAdminUpdate,
    RejectRequest,
)
from pydantic import BaseModel, EmailStr  # noqa: E402
from auth import (
    create_admin_token,
    verify_password,
    require_admin,
    hash_password,
)
from email_service import (
    send_application_approved_email,
    send_application_rejected_email,
    send_test_email,
)
from pdf_service import build_application_pdf  # noqa: E402
from db import get_db

logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/admin", tags=["admin"])

UPLOAD_DIR = Path(os.environ.get("UPLOAD_DIR", "/app/backend/uploads"))


# ============== AUTH ==============
@router.post("/login", response_model=TokenResponse)
async def admin_login(req: AdminLoginRequest, db=Depends(get_db)):
    admin_doc = await db.admins.find_one({"email": req.email.lower()}, {"_id": 0})
    if not admin_doc:
        raise HTTPException(status_code=401, detail="Invalid credentials")
    if not verify_password(req.password, admin_doc.get("password_hash", "")):
        raise HTTPException(status_code=401, detail="Invalid credentials")
    token = create_admin_token(req.email.lower())
    return TokenResponse(access_token=token, admin_email=req.email.lower())


@router.get("/me")
async def admin_me(admin_email: str = Depends(require_admin)):
    return {"email": admin_email, "role": "admin"}


# ============== STATS ==============
@router.get("/stats")
async def stats(_: str = Depends(require_admin), db=Depends(get_db)):
    total = await db.applications.count_documents({})
    pending_payment = await db.applications.count_documents({"status": "pending_payment"})
    paid = await db.applications.count_documents({"status": "paid"})
    approved = await db.applications.count_documents({"status": "approved"})
    rejected = await db.applications.count_documents({"status": "rejected"})

    # Total revenue (sum of price_amount for paid+approved)
    pipeline = [
        {"$match": {"status": {"$in": ["paid", "approved"]}}},
        {"$group": {"_id": None, "total": {"$sum": "$price_amount"}}},
    ]
    cur = db.applications.aggregate(pipeline)
    revenue = 0.0
    async for r in cur:
        revenue = float(r.get("total", 0))

    coupons_active = await db.coupons.count_documents({"active": True})

    return {
        "total_applications": total,
        "pending_payment": pending_payment,
        "paid": paid,
        "approved": approved,
        "rejected": rejected,
        "revenue_usd": round(revenue, 2),
        "active_coupons": coupons_active,
    }


# ============== APPLICATIONS ==============
@router.get("/applications")
async def list_applications(
    status: Optional[str] = Query(None),
    search: Optional[str] = Query(None),
    limit: int = Query(50, le=200),
    skip: int = Query(0, ge=0),
    _: str = Depends(require_admin),
    db=Depends(get_db),
):
    query = {}
    if status and status != "all":
        query["status"] = status
    if search:
        s = search.strip()
        query["$or"] = [
            {"full_name": {"$regex": s, "$options": "i"}},
            {"email": {"$regex": s, "$options": "i"}},
            {"username": {"$regex": s, "$options": "i"}},
            {"phone": {"$regex": s, "$options": "i"}},
            {"telegram_username": {"$regex": s, "$options": "i"}},
            {"id": s},
        ]

    cursor = db.applications.find(query, {"_id": 0}).sort("created_at", -1).skip(skip).limit(limit)
    items = await cursor.to_list(limit)
    total = await db.applications.count_documents(query)
    return {"items": items, "total": total, "limit": limit, "skip": skip}


@router.get("/applications/pdf-bulk")
async def download_bulk_pdfs(
    status: Optional[str] = Query(None, description="Filter: pending_payment / paid / approved / rejected / all"),
    _: str = Depends(require_admin),
    db=Depends(get_db),
):
    """Bulk download — all applications matching status as a ZIP of PDFs."""
    import zipfile
    import io as _io
    query = {}
    if status and status != "all":
        if status not in ("pending_payment", "paid", "approved", "rejected"):
            raise HTTPException(status_code=400, detail="Invalid status filter")
        query["status"] = status
    docs = await db.applications.find(query, {"_id": 0}).sort("created_at", -1).to_list(1000)
    if not docs:
        raise HTTPException(status_code=404, detail="No applications match the filter")
    settings = await db.settings.find_one({"id": "global"}, {"_id": 0}) or {}
    site_name = settings.get("site_name", "Maxlife88 Agent Portal")

    buf = _io.BytesIO()
    with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
        for d in docs:
            try:
                pdf_bytes = build_application_pdf(d, site_name=site_name)
                short = d.get("id", "")[:8]
                name = f"{(d.get('status') or 'app')}-{(d.get('full_name','agent') or 'agent').replace(' ', '_')}-{short}.pdf"
                zf.writestr(name, pdf_bytes)
            except Exception as e:
                logger.error("Bulk PDF: skipped %s — %s", d.get("id"), e)
    label = status or "all"
    filename = f"applications-{label}-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}.zip"
    return Response(
        content=buf.getvalue(),
        media_type="application/zip",
        headers={"Content-Disposition": f'attachment; filename="{filename}"'},
    )


@router.get("/applications/{app_id}")
async def get_application(app_id: str, _: str = Depends(require_admin), db=Depends(get_db)):
    doc = await db.applications.find_one({"id": app_id}, {"_id": 0})
    if not doc:
        raise HTTPException(status_code=404, detail="Application not found")
    return doc


@router.get("/applications/{app_id}/pdf")
async def download_application_pdf(app_id: str, _: str = Depends(require_admin), db=Depends(get_db)):
    """One-click PDF receipt/certificate for a single application."""
    doc = await db.applications.find_one({"id": app_id}, {"_id": 0})
    if not doc:
        raise HTTPException(status_code=404, detail="Application not found")
    settings = await db.settings.find_one({"id": "global"}, {"_id": 0}) or {}
    site_name = settings.get("site_name", "Maxlife88 Agent Portal")
    try:
        pdf_bytes = build_application_pdf(doc, site_name=site_name)
    except Exception as e:
        logger.error("PDF generation failed for %s: %s", app_id, e)
        raise HTTPException(status_code=500, detail="PDF generation failed")
    status = doc.get("status") or "application"
    short = doc.get("id", "")[:8]
    filename = f"{status}-{doc.get('full_name','agent').replace(' ', '_')}-{short}.pdf"
    return Response(
        content=pdf_bytes,
        media_type="application/pdf",
        headers={"Content-Disposition": f'attachment; filename="{filename}"'},
    )


@router.get("/applications/{app_id}/nid/{kind}")
async def get_nid_image(
    app_id: str,
    kind: str,
    _: str = Depends(require_admin),
    db=Depends(get_db),
):
    if kind not in ("nid_front", "nid_back", "nid_selfie"):
        raise HTTPException(status_code=400, detail="Invalid NID kind")
    doc = await db.applications.find_one({"id": app_id}, {"_id": 0})
    if not doc:
        raise HTTPException(status_code=404, detail="Application not found")
    rel = doc.get(f"{kind}_path")
    if not rel:
        raise HTTPException(status_code=404, detail="Image not uploaded")
    full = UPLOAD_DIR / rel
    if not full.exists():
        raise HTTPException(status_code=404, detail="Image file missing on server")
    return FileResponse(str(full))


@router.put("/applications/{app_id}")
async def admin_update_application(
    app_id: str,
    req: ApplicationAdminUpdate,
    _: str = Depends(require_admin),
    db=Depends(get_db),
):
    """Admin can edit applicant details (typos, telegram handle, etc.) before approval."""
    doc = await db.applications.find_one({"id": app_id}, {"_id": 0})
    if not doc:
        raise HTTPException(status_code=404, detail="Application not found")

    update = {k: v for k, v in req.model_dump(exclude_unset=True).items() if v is not None}
    if "telegram_username" in update:
        update["telegram_username"] = str(update["telegram_username"]).lstrip("@").strip()
    if "email" in update:
        update["email"] = str(update["email"]).lower().strip()
    if not update:
        raise HTTPException(status_code=400, detail="No fields to update")
    update["updated_at"] = datetime.now(timezone.utc).isoformat()
    await db.applications.update_one({"id": app_id}, {"$set": update})
    fresh = await db.applications.find_one({"id": app_id}, {"_id": 0})
    return fresh


@router.post("/applications/{app_id}/mark-paid")
async def admin_mark_paid(
    app_id: str,
    _: str = Depends(require_admin),
    db=Depends(get_db),
):
    """Manually mark an application as paid (skip payment gateway).

    Useful for: cash/manual payments, free signups, comp accounts, gateway issues.
    Idempotent — if already paid/approved, returns current status without changes.
    """
    doc = await db.applications.find_one({"id": app_id}, {"_id": 0})
    if not doc:
        raise HTTPException(status_code=404, detail="Application not found")
    if doc.get("status") in ("paid", "approved"):
        return {"ok": True, "status": doc["status"], "message": "Application is already paid/approved"}
    if doc.get("status") not in ("pending_payment", "rejected"):
        raise HTTPException(status_code=400, detail=f"Cannot mark from status '{doc.get('status')}'")
    now_iso = datetime.now(timezone.utc).isoformat()
    from bd_time import now_bd_str
    bd_now = now_bd_str("%d %b %Y · %I:%M %p")
    await db.applications.update_one(
        {"id": app_id},
        {"$set": {
            "status": "paid",
            "payment_status": "manual",
            "paid_at": doc.get("paid_at") or now_iso,
            "updated_at": now_iso,
            "rejection_reason": None,
            "admin_notes": (doc.get("admin_notes") or "") + f"\n[{bd_now} BDT] Marked as paid by admin (no gateway payment)",
        }},
    )
    return {"ok": True, "status": "paid", "message": "Application marked as paid"}


@router.post("/applications/{app_id}/approve")
async def approve_application(
    app_id: str,
    _: str = Depends(require_admin),
    db=Depends(get_db),
):
    doc = await db.applications.find_one({"id": app_id}, {"_id": 0})
    if not doc:
        raise HTTPException(status_code=404, detail="Application not found")
    if doc.get("status") != "paid":
        raise HTTPException(status_code=400, detail=f"Cannot approve from status '{doc.get('status')}' (must be 'paid')")
    now_iso = datetime.now(timezone.utc).isoformat()
    await db.applications.update_one(
        {"id": app_id},
        {"$set": {"status": "approved", "decided_at": now_iso, "updated_at": now_iso}},
    )
    settings_doc = await db.settings.find_one({"id": "global"}, {"_id": 0}) or {}
    telegram_url = settings_doc.get("telegram_manager_url", "https://t.me/maxlife88_support")
    site_name = settings_doc.get("site_name", "Maxlife88 Agent Portal")
    # Generate PDF certificate
    updated = await db.applications.find_one({"id": app_id}, {"_id": 0})
    try:
        pdf_bytes = build_application_pdf(updated, site_name=site_name)
    except Exception as e:
        logger.error("PDF generation failed: %s", e)
        pdf_bytes = None
    try:
        await send_application_approved_email(
            to=doc["email"],
            full_name=doc["full_name"],
            telegram_url=telegram_url,
            application_id=doc["id"],
            pdf_bytes=pdf_bytes,
        )
    except Exception as e:
        logger.error("approval email failed: %s", e)
    return {"ok": True, "status": "approved"}


@router.post("/applications/{app_id}/reject")
async def reject_application(
    app_id: str,
    req: RejectRequest,
    _: str = Depends(require_admin),
    db=Depends(get_db),
):
    doc = await db.applications.find_one({"id": app_id}, {"_id": 0})
    if not doc:
        raise HTTPException(status_code=404, detail="Application not found")
    if doc.get("status") not in ("paid", "pending_payment"):
        raise HTTPException(status_code=400, detail=f"Cannot reject from status '{doc.get('status')}'")
    now_iso = datetime.now(timezone.utc).isoformat()
    await db.applications.update_one(
        {"id": app_id},
        {"$set": {
            "status": "rejected",
            "rejection_reason": req.reason,
            "decided_at": now_iso,
            "updated_at": now_iso,
        }},
    )
    settings_doc = await db.settings.find_one({"id": "global"}, {"_id": 0}) or {}
    telegram_url = settings_doc.get("telegram_manager_url", "https://t.me/maxlife88_support")
    site_name = settings_doc.get("site_name", "Maxlife88 Agent Portal")
    # Build re-upload link from settings (admin-configurable) → env → empty
    site_base = (
        (settings_doc.get("nowpayments_public_url") or "").strip()
        or os.environ.get("PUBLIC_URL", "").strip()
    ).rstrip("/")
    reupload_url = None
    if site_base:
        from urllib.parse import quote
        reupload_url = f"{site_base}/reupload/{doc['id']}?email={quote(doc.get('email', ''))}"
    # Generate PDF report including rejection reason
    updated = await db.applications.find_one({"id": app_id}, {"_id": 0})
    try:
        pdf_bytes = build_application_pdf(updated, site_name=site_name)
    except Exception as e:
        logger.error("PDF generation failed: %s", e)
        pdf_bytes = None
    try:
        await send_application_rejected_email(
            to=doc["email"],
            full_name=doc["full_name"],
            reason=req.reason,
            telegram_url=telegram_url,
            application_id=doc["id"],
            pdf_bytes=pdf_bytes,
            reupload_url=reupload_url,
        )
    except Exception as e:
        logger.error("rejection email failed: %s", e)
    return {"ok": True, "status": "rejected"}


# ============== COUPONS ==============
@router.get("/coupons")
async def list_coupons(_: str = Depends(require_admin), db=Depends(get_db)):
    items = await db.coupons.find({}, {"_id": 0}).sort("created_at", -1).to_list(500)
    return {"items": items}


@router.post("/coupons", response_model=Coupon)
async def create_coupon(req: CouponCreate, _: str = Depends(require_admin), db=Depends(get_db)):
    code_up = req.code.strip().upper()
    if await db.coupons.find_one({"code": code_up}):
        raise HTTPException(status_code=409, detail="Coupon code already exists")
    coupon = Coupon(
        code=code_up,
        discount_percent=req.discount_percent,
        max_uses=req.max_uses,
        expires_at=req.expires_at,
        active=req.active,
        description=req.description,
    )
    await db.coupons.insert_one(coupon.model_dump())
    return coupon


@router.put("/coupons/{coupon_id}")
async def update_coupon(coupon_id: str, req: CouponUpdate, _: str = Depends(require_admin), db=Depends(get_db)):
    update = {k: v for k, v in req.model_dump(exclude_unset=True).items() if v is not None}
    if not update:
        raise HTTPException(status_code=400, detail="No fields to update")
    res = await db.coupons.update_one({"id": coupon_id}, {"$set": update})
    if res.matched_count == 0:
        raise HTTPException(status_code=404, detail="Coupon not found")
    doc = await db.coupons.find_one({"id": coupon_id}, {"_id": 0})
    return doc


@router.delete("/coupons/{coupon_id}")
async def delete_coupon(coupon_id: str, _: str = Depends(require_admin), db=Depends(get_db)):
    res = await db.coupons.delete_one({"id": coupon_id})
    if res.deleted_count == 0:
        raise HTTPException(status_code=404, detail="Coupon not found")
    return {"ok": True}


# ============== SETTINGS ==============
SMTP_PWD_MASK = "********"
SECRET_FIELDS = ("smtp_password", "nowpayments_api_key", "nowpayments_ipn_secret")


def _mask_settings(doc: dict) -> dict:
    """Return settings with secret fields masked for safe transit to admin UI."""
    out = dict(doc)
    for k in SECRET_FIELDS:
        if out.get(k):
            out[k] = SMTP_PWD_MASK
    return out


@router.get("/settings")
async def get_settings_admin(_: str = Depends(require_admin), db=Depends(get_db)):
    doc = await db.settings.find_one({"id": "global"}, {"_id": 0})
    if not doc:
        s = Settings()
        await db.settings.insert_one(s.model_dump())
        return _mask_settings(s.model_dump())
    # Ensure all current schema fields are present (back-fill defaults)
    merged = {**Settings().model_dump(), **doc}
    return _mask_settings(merged)


@router.put("/settings")
async def update_settings(req: SettingsUpdate, _: str = Depends(require_admin), db=Depends(get_db)):
    update = {k: v for k, v in req.model_dump(exclude_unset=True).items() if v is not None}
    # If admin UI sent the mask back unchanged, don't overwrite the real secret
    for k in SECRET_FIELDS:
        if update.get(k) == SMTP_PWD_MASK:
            update.pop(k, None)
    if update:
        update["updated_at"] = datetime.now(timezone.utc).isoformat()
        await db.settings.update_one({"id": "global"}, {"$set": update}, upsert=True)
    doc = await db.settings.find_one({"id": "global"}, {"_id": 0})
    if not doc:
        s = Settings()
        await db.settings.insert_one(s.model_dump())
        return _mask_settings(s.model_dump())
    merged = {**Settings().model_dump(), **doc}
    return _mask_settings(merged)


class TestEmailRequest(BaseModel):
    to: EmailStr


@router.post("/settings/test-email")
async def admin_test_email(req: TestEmailRequest, _: str = Depends(require_admin)):
    """Send a one-off test email using current SMTP/Resend settings.

    Returns the result so admin can see if SMTP credentials work.
    """
    result = await send_test_email(req.to)
    if result.get("success"):
        return {"ok": True, "provider": result.get("provider"), "message": f"Test email sent to {req.to}"}
    if result.get("skipped"):
        raise HTTPException(status_code=400, detail="No email provider configured. Fill in SMTP host/user/password and save first.")
    raise HTTPException(status_code=502, detail=f"Email send failed: {result.get('error', 'unknown error')}")


# ============== BACKUP / RESTORE ==============
@router.get("/settings/backup")
async def download_backup(_: str = Depends(require_admin), db=Depends(get_db)):
    """One-click full config snapshot — settings + coupons + admin emails.

    Excludes sensitive smtp_password and JWT data. The downloaded JSON can be
    re-uploaded via /settings/restore on this or another deployment.
    """
    settings = await db.settings.find_one({"id": "global"}, {"_id": 0}) or {}
    # Mask all secrets in backup file (safer)
    settings = {**settings}
    for k in SECRET_FIELDS:
        if settings.get(k):
            settings[k] = ""
    coupons = await db.coupons.find({}, {"_id": 0}).to_list(1000)
    admins = await db.admins.find({}, {"_id": 0, "password_hash": 0}).to_list(100)
    snapshot = {
        "version": 1,
        "exported_at": datetime.now(timezone.utc).isoformat(),
        "settings": settings,
        "coupons": coupons,
        "admins": admins,
    }
    body = json.dumps(snapshot, indent=2, ensure_ascii=False)
    filename = f"maxlife88-backup-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}.json"
    return Response(
        content=body,
        media_type="application/json",
        headers={"Content-Disposition": f'attachment; filename="{filename}"'},
    )


@router.post("/settings/restore")
async def upload_restore(
    file: UploadFile = File(...),
    _: str = Depends(require_admin),
    db=Depends(get_db),
):
    """Restore settings + coupons from a backup JSON.

    Existing smtp_password is preserved (backup files don't carry passwords).
    Coupons are upserted by `code`. Existing applications are NOT touched.
    """
    if not file.filename.endswith(".json"):
        raise HTTPException(status_code=400, detail="Backup file must be .json")
    raw = await file.read()
    try:
        snap = json.loads(raw.decode("utf-8"))
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid JSON file")

    summary = {"settings_restored": False, "coupons_restored": 0, "errors": []}

    # Restore settings — preserve current secrets if blank in incoming
    current = await db.settings.find_one({"id": "global"}, {"_id": 0}) or {}
    incoming = snap.get("settings") or {}
    if incoming:
        # Don't overwrite live secrets with blanks from the backup
        for k in SECRET_FIELDS:
            if not incoming.get(k) and current.get(k):
                incoming[k] = current[k]
        incoming["id"] = "global"
        incoming["updated_at"] = datetime.now(timezone.utc).isoformat()
        await db.settings.update_one({"id": "global"}, {"$set": incoming}, upsert=True)
        summary["settings_restored"] = True

    # Restore coupons
    for c in (snap.get("coupons") or []):
        try:
            code = c.get("code")
            if not code:
                continue
            await db.coupons.update_one({"code": code}, {"$set": c}, upsert=True)
            summary["coupons_restored"] += 1
        except Exception as e:
            summary["errors"].append(f"coupon {c.get('code')}: {str(e)[:100]}")

    return {"ok": True, **summary}




# ============== EMAIL LOGS ==============
@router.get("/email-logs")
async def list_email_logs(
    email_type: Optional[str] = Query(None),
    status: Optional[str] = Query(None),
    search: Optional[str] = Query(None),
    limit: int = Query(50, le=200),
    skip: int = Query(0, ge=0),
    _: str = Depends(require_admin),
    db=Depends(get_db),
):
    query = {}
    if email_type and email_type != "all":
        query["email_type"] = email_type
    if status and status != "all":
        query["status"] = status
    if search:
        s = search.strip()
        query["$or"] = [
            {"to": {"$regex": s, "$options": "i"}},
            {"subject": {"$regex": s, "$options": "i"}},
            {"application_id": s},
        ]
    cursor = db.email_logs.find(query, {"_id": 0}).sort("sent_at", -1).skip(skip).limit(limit)
    items = await cursor.to_list(limit)
    total = await db.email_logs.count_documents(query)
    # aggregate counts respecting the same filter so UI counters match the visible list
    by_status = {}
    pipeline = []
    if query:
        pipeline.append({"$match": query})
    pipeline.append({"$group": {"_id": "$status", "count": {"$sum": 1}}})
    async for r in db.email_logs.aggregate(pipeline):
        by_status[r["_id"]] = r["count"]
    return {"items": items, "total": total, "by_status": by_status, "limit": limit, "skip": skip}


@router.post("/email-logs/{log_id}/retry")
async def retry_email(log_id: str, _: str = Depends(require_admin), db=Depends(get_db)):
    """Re-send an email that failed/was skipped earlier (best-effort, recreates from application context)."""
    from email_service import (
        send_application_approved_email,
        send_application_rejected_email,
        send_payment_received_email,
        send_admin_new_application_email,
    )
    log = await db.email_logs.find_one({"id": log_id}, {"_id": 0})
    if not log:
        raise HTTPException(status_code=404, detail="Email log not found")
    app_id = log.get("application_id")
    if not app_id:
        raise HTTPException(status_code=400, detail="Cannot retry: no application linked")
    app_doc = await db.applications.find_one({"id": app_id}, {"_id": 0})
    if not app_doc:
        raise HTTPException(status_code=404, detail="Linked application not found")

    settings_doc = await db.settings.find_one({"id": "global"}, {"_id": 0}) or {}
    telegram_url = settings_doc.get("telegram_manager_url", "https://t.me/maxlife88_support")
    et = log.get("email_type")
    to = log.get("to") or app_doc.get("email")

    if et == "approved":
        await send_application_approved_email(to=to, full_name=app_doc["full_name"], telegram_url=telegram_url, application_id=app_id)
    elif et == "rejected":
        site_base = (
            (settings_doc.get("nowpayments_public_url") or "").strip()
            or os.environ.get("PUBLIC_URL", "").strip()
        ).rstrip("/")
        reupload_url = None
        if site_base:
            from urllib.parse import quote
            reupload_url = f"{site_base}/reupload/{app_id}?email={quote(app_doc.get('email', ''))}"
        await send_application_rejected_email(to=to, full_name=app_doc["full_name"], reason=app_doc.get("rejection_reason") or "—", telegram_url=telegram_url, application_id=app_id, reupload_url=reupload_url)
    elif et == "payment_received":
        await send_payment_received_email(to=to, full_name=app_doc["full_name"], amount=float(app_doc.get("price_amount", 0)), payment_id=str(app_doc.get("payment_id", "")), application_id=app_id)
    elif et == "admin_alert":
        notify_to = settings_doc.get("notification_email") or os.environ.get("ADMIN_EMAIL") or to
        await send_admin_new_application_email(notify_to, app_doc)
    else:
        raise HTTPException(status_code=400, detail=f"Cannot retry email type '{et}'")
    # Track retry count + timestamp on the original log row
    await db.email_logs.update_one(
        {"id": log_id},
        {"$inc": {"retry_count": 1}, "$set": {"last_retried_at": datetime.now(timezone.utc).isoformat()}},
    )
    return {"ok": True, "retried": et}
