"""
FastAPI push notification API — subscription management and send endpoint.

Provides Web Push subscription storage so the gateway can deliver
push notifications when an AI employee responds and the user's
WebSocket is not connected.

Endpoints:
  POST   /api/portal/push/subscribe   — store browser push subscription
  DELETE /api/portal/push/unsubscribe — remove subscription by endpoint
  POST   /api/portal/push/send        — internal: send push to user (called by WS handler)

Authentication:
  subscribe / unsubscribe: require portal user headers (X-Portal-User-Id)
  send: internal endpoint — requires same portal headers but is called by
        the gateway WebSocket handler when user is offline

Push delivery:
  Uses pywebpush for VAPID-signed Web Push delivery.
  Handles 410 Gone responses by deleting stale subscriptions.
"""

from __future__ import annotations

import json
import logging
import os
import uuid

from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy import delete, select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.ext.asyncio import AsyncSession

from shared.api.rbac import PortalCaller, get_portal_caller
from shared.db import get_session
from shared.models.push import PushSubscription, PushSubscriptionCreate, PushSubscriptionOut, PushSendRequest

logger = logging.getLogger(__name__)

push_router = APIRouter(prefix="/api/portal/push", tags=["push"])

# ---------------------------------------------------------------------------
# VAPID config (read from environment at import time)
# ---------------------------------------------------------------------------

VAPID_PRIVATE_KEY: str = os.environ.get("VAPID_PRIVATE_KEY", "")
VAPID_PUBLIC_KEY: str = os.environ.get("NEXT_PUBLIC_VAPID_PUBLIC_KEY", "")
VAPID_CLAIMS_EMAIL: str = os.environ.get("VAPID_CLAIMS_EMAIL", "admin@konstruct.dev")


# ---------------------------------------------------------------------------
# Helper — send a single push notification via pywebpush
# ---------------------------------------------------------------------------


async def _send_push(subscription: PushSubscription, payload: dict[str, object]) -> bool:
    """
    Send a Web Push notification to a single subscription.

    Returns True on success, False if the subscription is stale (410 Gone).
    Raises on other errors so the caller can decide how to handle them.
    """
    if not VAPID_PRIVATE_KEY:
        logger.warning("VAPID_PRIVATE_KEY not set — skipping push notification")
        return True

    try:
        from pywebpush import WebPusher, webpush, WebPushException  # type: ignore[import]

        subscription_info = {
            "endpoint": subscription.endpoint,
            "keys": {
                "p256dh": subscription.p256dh,
                "auth": subscription.auth,
            },
        }

        webpush(
            subscription_info=subscription_info,
            data=json.dumps(payload),
            vapid_private_key=VAPID_PRIVATE_KEY,
            vapid_claims={
                "sub": f"mailto:{VAPID_CLAIMS_EMAIL}",
            },
        )
        return True

    except Exception as exc:
        # Check for 410 Gone — subscription is no longer valid
        exc_str = str(exc)
        if "410" in exc_str or "Gone" in exc_str or "expired" in exc_str.lower():
            logger.info("Push subscription stale (410 Gone): %s", subscription.endpoint[:40])
            return False
        logger.error("Push delivery failed: %s", exc_str)
        raise


# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------


@push_router.post("/subscribe", status_code=status.HTTP_201_CREATED, response_model=PushSubscriptionOut)
async def subscribe(
    body: PushSubscriptionCreate,
    caller: PortalCaller = Depends(get_portal_caller),
    session: AsyncSession = Depends(get_session),
) -> PushSubscriptionOut:
    """
    Store a browser push subscription for the authenticated user.

    Uses INSERT ... ON CONFLICT (user_id, endpoint) DO UPDATE so
    re-subscribing the same browser updates the keys without creating
    a duplicate row.
    """
    stmt = (
        pg_insert(PushSubscription)
        .values(
            user_id=caller.user_id,
            tenant_id=uuid.UUID(body.tenant_id) if body.tenant_id else None,
            endpoint=body.endpoint,
            p256dh=body.p256dh,
            auth=body.auth,
        )
        .on_conflict_do_update(
            constraint="uq_push_user_endpoint",
            set_={
                "p256dh": body.p256dh,
                "auth": body.auth,
                "tenant_id": uuid.UUID(body.tenant_id) if body.tenant_id else None,
            },
        )
        .returning(PushSubscription)
    )
    result = await session.execute(stmt)
    row = result.scalar_one()
    await session.commit()

    return PushSubscriptionOut(
        id=str(row.id),
        endpoint=row.endpoint,
        created_at=row.created_at,
    )


class UnsubscribeRequest(BaseModel):
    endpoint: str


@push_router.delete("/unsubscribe", status_code=status.HTTP_204_NO_CONTENT)
async def unsubscribe(
    body: UnsubscribeRequest,
    caller: PortalCaller = Depends(get_portal_caller),
    session: AsyncSession = Depends(get_session),
) -> None:
    """Remove a push subscription for the authenticated user."""
    await session.execute(
        delete(PushSubscription).where(
            PushSubscription.user_id == caller.user_id,
            PushSubscription.endpoint == body.endpoint,
        )
    )
    await session.commit()


@push_router.post("/send", status_code=status.HTTP_200_OK)
async def send_push(
    body: PushSendRequest,
    caller: PortalCaller = Depends(get_portal_caller),
    session: AsyncSession = Depends(get_session),
) -> dict[str, object]:
    """
    Internal endpoint — send a push notification to all subscriptions for a user.

    Called by the gateway WebSocket handler when the agent responds but
    the user's WebSocket is no longer connected.

    Handles 410 Gone by deleting stale subscriptions.
    Returns counts of delivered and stale subscriptions.
    """
    try:
        target_user_id = uuid.UUID(body.user_id)
    except ValueError as exc:
        raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid user_id") from exc

    # Fetch all subscriptions for this user
    result = await session.execute(
        select(PushSubscription).where(PushSubscription.user_id == target_user_id)
    )
    subscriptions = result.scalars().all()

    if not subscriptions:
        return {"delivered": 0, "stale": 0, "total": 0}

    payload = {
        "title": body.title,
        "body": body.body,
        "data": {
            "conversationId": body.conversation_id,
        },
    }

    delivered = 0
    stale_endpoints: list[str] = []

    for sub in subscriptions:
        try:
            ok = await _send_push(sub, payload)
            if ok:
                delivered += 1
            else:
                stale_endpoints.append(sub.endpoint)
        except Exception as exc:
            logger.error("Push send error for user %s: %s", body.user_id, exc)

    # Delete stale subscriptions
    if stale_endpoints:
        await session.execute(
            delete(PushSubscription).where(
                PushSubscription.user_id == target_user_id,
                PushSubscription.endpoint.in_(stale_endpoints),
            )
        )
        await session.commit()

    return {
        "delivered": delivered,
        "stale": len(stale_endpoints),
        "total": len(subscriptions),
    }
