"""Push notifications via Firebase Cloud Messaging (FCM HTTP v1).

Two endpoints:

  • POST /api/devices/register-token
      Called by the Android app on every cold start (Capacitor's
      pushNotificationsRegistration listener fires once after the
      user grants permission). Saves / refreshes the FCM token
      against the device id so future broadcasts reach this device.

  • POST /api/admin/broadcast-notification
      Admin-only. Pushes a {title, body, route} payload to every
      registered token using firebase_admin.messaging.send_each().
      Stale tokens (NOT_FOUND / INVALID_ARGUMENT errors) are pruned
      automatically so the next broadcast is faster.

The Firebase Admin SDK is initialised lazily so the service starts
even if the credentials file is missing — the broadcast endpoint
will then return a 503 with a clear message instead of crashing the
whole backend.
"""
from __future__ import annotations

import logging
import os
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
from pydantic import BaseModel, Field

from core.auth import get_current_user, get_db, require_admin

router = APIRouter(prefix="/api", tags=["push-notifications"])
log = logging.getLogger(__name__)

# ── Firebase Admin SDK lazy initialisation ────────────────────────
_FIREBASE_READY = False
_FIREBASE_ERROR: Optional[str] = None


def _ensure_firebase() -> None:
    """Initialise firebase_admin once. Errors are captured (not raised)."""
    global _FIREBASE_READY, _FIREBASE_ERROR
    if _FIREBASE_READY:
        return
    try:
        import firebase_admin
        from firebase_admin import credentials

        cred_path = os.environ.get(
            "FIREBASE_SERVICE_ACCOUNT",
            os.path.join(
                os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
                "secrets",
                "firebase-service-account.json",
            ),
        )
        if not os.path.exists(cred_path):
            _FIREBASE_ERROR = f"Service-account file not found at {cred_path}"
            return
        if not firebase_admin._apps:
            firebase_admin.initialize_app(credentials.Certificate(cred_path))
        _FIREBASE_READY = True
        log.info("firebase_admin initialised from %s", cred_path)
    except Exception as exc:  # noqa: BLE001 - report any init error
        _FIREBASE_ERROR = f"{type(exc).__name__}: {exc}"
        log.warning("firebase_admin init failed: %s", _FIREBASE_ERROR)


# ── Request / response models ─────────────────────────────────────
class RegisterTokenIn(BaseModel):
    token: str = Field(..., min_length=10, max_length=4096)
    device_id: str = Field(..., min_length=1, max_length=128)
    platform: str = Field(default="android", pattern="^(android|ios|web)$")
    app_version: Optional[str] = None


class BroadcastIn(BaseModel):
    title: str = Field(..., min_length=1, max_length=200)
    body: str = Field(..., min_length=1, max_length=1000)
    # Optional deep-link route — when set, tapping the notification
    # opens the app on this React route (e.g. "/live-tv").
    route: Optional[str] = Field(default=None, max_length=200)
    # Free-form key/values forwarded to the device as data payload.
    data: Optional[Dict[str, str]] = None
    # Optional rich image URL (Android/iOS notification style support).
    image_url: Optional[str] = Field(default=None, max_length=2000)
    # Optional CTA button label to show in-app when opened from push.
    action_label: Optional[str] = Field(default=None, max_length=60)
    # Optional single-user targeting (admin panel recipient picker).
    target_user_id: Optional[str] = Field(default=None, max_length=128)


class BroadcastOut(BaseModel):
    queued: bool
    total_tokens: int
    sent: int = 0
    failed: int = 0
    pruned: int = 0
    error: Optional[str] = None


# ── Token register endpoint ───────────────────────────────────────
@router.post("/devices/register-token")
async def register_token(body: RegisterTokenIn, user=Depends(get_current_user)):
    """Upsert an FCM device token for the current user."""
    db = get_db()
    now = datetime.now(timezone.utc).isoformat()
    doc = {
        "token": body.token,
        "device_id": body.device_id,
        "platform": body.platform,
        "app_version": body.app_version,
        "user_id": user["id"],
        "is_active": True,
        "updated_at": now,
    }
    await db.fcm_tokens.update_one(
        {"device_id": body.device_id},
        {"$set": doc, "$setOnInsert": {"created_at": now}},
        upsert=True,
    )
    return {"registered": True, "device_id": body.device_id}


# ── Admin broadcast endpoint ──────────────────────────────────────
@router.post("/admin/broadcast-notification", response_model=BroadcastOut)
async def broadcast_notification(
    body: BroadcastIn,
    bg: BackgroundTasks,
    _admin=Depends(require_admin),
):
    """Send a notification to every registered device."""
    _ensure_firebase()
    if not _FIREBASE_READY:
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail=f"Firebase not configured: {_FIREBASE_ERROR}",
        )
    db = get_db()
    token_query: Dict[str, Any] = {"is_active": True}
    if body.target_user_id:
        token_query["user_id"] = body.target_user_id

    cursor = db.fcm_tokens.find(
        token_query, {"_id": 0, "token": 1, "device_id": 1, "user_id": 1}
    )
    docs: List[Dict[str, Any]] = await cursor.to_list(length=None)
    if not docs:
        return BroadcastOut(queued=False, total_tokens=0, error="No registered devices")

    # Run the actual send in a background task so the HTTP response
    # comes back instantly even when there are 10k+ devices.
    bg.add_task(_send_broadcast, body.model_dump(), docs)
    return BroadcastOut(queued=True, total_tokens=len(docs))


@router.post("/admin/notify-user", response_model=BroadcastOut)
async def notify_single_user(
    body: BroadcastIn,
    bg: BackgroundTasks,
    _admin=Depends(require_admin),
):
    """Compatibility endpoint used by frontend when a specific user is selected."""
    if not body.target_user_id:
        raise HTTPException(status_code=400, detail="target_user_id is required")
    return await broadcast_notification(body=body, bg=bg, _admin=_admin)


async def _send_broadcast(payload: Dict[str, Any], docs: List[Dict[str, Any]]) -> None:
    """Background task: chunk + send + prune stale tokens."""
    from firebase_admin import messaging

    db = get_db()
    title = payload["title"]
    body_text = payload["body"]
    route = payload.get("route") or ""
    extra = payload.get("data") or {}
    image_url = (payload.get("image_url") or "").strip()
    action_label = (payload.get("action_label") or "").strip()
    target_user_id = (payload.get("target_user_id") or "").strip() or None

    # Build the data payload — FCM data values must be strings.
    data: Dict[str, str] = {str(k): str(v) for k, v in extra.items()}
    if route:
        data["route"] = route
    if action_label:
        data["action_label"] = action_label
    # Help React side know where this came from
    data["fromNotification"] = "1"
    data.setdefault("click_action", "FLUTTER_NOTIFICATION_CLICK")

    # Android-specific tweaks: high priority for live match alerts,
    # default channel id for the system notification (see Android side).
    android_notification = messaging.AndroidNotification(
        title=title,
        body=body_text,
        channel_id="nadiplayer_default",
        click_action="FLUTTER_NOTIFICATION_CLICK",
    )
    if image_url:
        android_notification.image = image_url

    android_cfg = messaging.AndroidConfig(
        priority="high",
        notification=android_notification,
    )

    apns_cfg = messaging.APNSConfig(
        headers={"apns-priority": "10"},
        payload=messaging.APNSPayload(
            aps=messaging.Aps(sound="default", content_available=True)
        ),
        fcm_options=messaging.APNSFCMOptions(image=image_url) if image_url else None,
    )

    webpush_cfg = messaging.WebpushConfig(
        notification=messaging.WebpushNotification(
            title=title,
            body=body_text,
            icon=image_url or None,
        ),
        data=data,
    )

    sent = failed = pruned = 0
    stale_device_ids: List[str] = []

    # FCM HTTP v1 send_each tolerates up to 500 messages per call.
    CHUNK = 500
    for start in range(0, len(docs), CHUNK):
        chunk = docs[start : start + CHUNK]
        messages = [
            messaging.Message(
                token=d["token"],
                notification=messaging.Notification(title=title, body=body_text),
                data=data,
                android=android_cfg,
                apns=apns_cfg,
                webpush=webpush_cfg,
            )
            for d in chunk
        ]
        try:
            resp = messaging.send_each(messages)
        except Exception as exc:  # noqa: BLE001
            log.exception("FCM batch send crashed: %s", exc)
            failed += len(chunk)
            continue

        sent += resp.success_count
        failed += resp.failure_count

        for i, r in enumerate(resp.responses):
            if r.success:
                continue
            err_code = (
                r.exception.code if hasattr(r.exception, "code") else str(r.exception)
            )
            # Tokens that are gone or invalid → mark for pruning.
            if str(err_code) in {
                "registration-token-not-registered",
                "invalid-argument",
                "invalid-registration-token",
                "NOT_FOUND",
                "UNREGISTERED",
            }:
                stale_device_ids.append(chunk[i]["device_id"])

    if stale_device_ids:
        await db.fcm_tokens.update_many(
            {"device_id": {"$in": stale_device_ids}},
            {"$set": {"is_active": False, "deactivated_at": datetime.now(timezone.utc).isoformat()}},
        )
        pruned = len(stale_device_ids)

    # Audit log so admin can see results in the future history view.
    await db.fcm_broadcasts.insert_one(
        {
            "title": title,
            "body": body_text,
            "route": route or None,
            "image_url": image_url or None,
            "action_label": action_label or None,
            "target_user_id": target_user_id,
            "total_tokens": len(docs),
            "sent": sent,
            "failed": failed,
            "pruned": pruned,
            "created_at": datetime.now(timezone.utc).isoformat(),
        }
    )
    log.info(
        "Broadcast complete: %d sent, %d failed, %d pruned of %d total",
        sent,
        failed,
        pruned,
        len(docs),
    )


@router.get("/admin/broadcasts")
async def list_broadcasts(_admin=Depends(require_admin), limit: int = 20):
    """Recent broadcast history (admin only)."""
    db = get_db()
    cursor = (
        db.fcm_broadcasts.find({}, {"_id": 0})
        .sort("created_at", -1)
        .limit(max(1, min(limit, 200)))
    )
    return {"broadcasts": await cursor.to_list(length=None)}
