"""Browser push notification service using VAPID/pywebpush."""
import asyncio
import base64
import json
import logging
from datetime import datetime, timezone
from functools import lru_cache
from pathlib import Path

from sqlalchemy import delete, select

from fabledassistant.config import Config
from fabledassistant.models import async_session
from fabledassistant.models.push_subscription import PushSubscription

logger = logging.getLogger(__name__)

# Persisted alongside other app data so keys survive container restarts.
_VAPID_KEYS_FILE = Path(Config.IMAGE_CACHE_DIR).parent / "vapid_keys.json"


@lru_cache(maxsize=1)
def _get_webpush():
    """Lazy import to avoid startup errors if pywebpush is not installed."""
    try:
        from pywebpush import webpush
        return webpush
    except ImportError:
        return None


def vapid_enabled() -> bool:
    return bool(Config.VAPID_PRIVATE_KEY and Config.VAPID_PUBLIC_KEY)


def ensure_vapid_keys() -> None:
    """Load or auto-generate VAPID keys, storing them in the data volume.

    Called once at startup.  If VAPID_PRIVATE_KEY / VAPID_PUBLIC_KEY env vars
    are already set they take precedence and nothing is written to disk.
    """
    if vapid_enabled():
        logger.info("VAPID keys loaded from environment variables")
        return

    # Try to load previously generated keys from the data volume.
    if _VAPID_KEYS_FILE.exists():
        try:
            data = json.loads(_VAPID_KEYS_FILE.read_text())
            Config.VAPID_PRIVATE_KEY = data["private_key"]
            Config.VAPID_PUBLIC_KEY = data["public_key"]
            logger.info("VAPID keys loaded from %s", _VAPID_KEYS_FILE)
            return
        except Exception:
            logger.warning("Failed to load VAPID keys from %s — regenerating", _VAPID_KEYS_FILE, exc_info=True)

    # Generate a fresh key pair.
    try:
        from cryptography.hazmat.primitives import serialization
        from py_vapid import Vapid01

        v = Vapid01()
        v.generate_keys()

        # pywebpush expects the private key as a base64url-encoded DER blob
        # (passed to Vapid.from_string → from_der), NOT a PEM string.
        private_der = v.private_key.private_bytes(
            serialization.Encoding.DER,
            serialization.PrivateFormat.TraditionalOpenSSL,
            serialization.NoEncryption(),
        )
        private_b64 = base64.urlsafe_b64encode(private_der).rstrip(b"=").decode()

        pub_bytes = v.public_key.public_bytes(
            serialization.Encoding.X962,
            serialization.PublicFormat.UncompressedPoint,
        )
        public_b64 = base64.urlsafe_b64encode(pub_bytes).rstrip(b"=").decode()

        # Persist so they survive container restarts.
        _VAPID_KEYS_FILE.parent.mkdir(parents=True, exist_ok=True)
        _VAPID_KEYS_FILE.write_text(json.dumps({"private_key": private_b64, "public_key": public_b64}))

        Config.VAPID_PRIVATE_KEY = private_b64
        Config.VAPID_PUBLIC_KEY = public_b64
        logger.info("Generated new VAPID keys and saved to %s", _VAPID_KEYS_FILE)
    except Exception:
        logger.warning("Failed to generate VAPID keys — push notifications will be unavailable", exc_info=True)


async def regenerate_vapid_keys() -> bool:
    """Delete existing VAPID keys, clear all push subscriptions, and generate a fresh pair.

    All existing browser subscriptions are invalidated when keys rotate, so they
    must be cleared — users will need to re-enable notifications.
    """
    if _VAPID_KEYS_FILE.exists():
        _VAPID_KEYS_FILE.unlink()
    Config.VAPID_PRIVATE_KEY = ""
    Config.VAPID_PUBLIC_KEY = ""

    # Clear all push subscriptions — they are bound to the old public key.
    async with async_session() as session:
        await session.execute(delete(PushSubscription))
        await session.commit()

    ensure_vapid_keys()
    enabled = vapid_enabled()
    if enabled:
        logger.info("VAPID keys regenerated successfully")
    else:
        logger.error("VAPID key regeneration failed")
    return enabled


async def save_subscription(user_id: int, subscription_json: dict, user_agent: str | None = None) -> PushSubscription:
    """Upsert a push subscription by endpoint."""
    endpoint = subscription_json.get("endpoint", "")
    keys = subscription_json.get("keys", {})
    p256dh = keys.get("p256dh", "")
    auth = keys.get("auth", "")

    async with async_session() as session:
        result = await session.execute(
            select(PushSubscription).where(
                PushSubscription.user_id == user_id,
                PushSubscription.endpoint == endpoint,
            )
        )
        existing = result.scalars().first()
        if existing:
            existing.p256dh = p256dh
            existing.auth = auth
            existing.user_agent = user_agent
            existing.last_used = datetime.now(timezone.utc)
            await session.commit()
            await session.refresh(existing)
            return existing
        else:
            sub = PushSubscription(
                user_id=user_id,
                endpoint=endpoint,
                p256dh=p256dh,
                auth=auth,
                user_agent=user_agent,
            )
            session.add(sub)
            await session.commit()
            await session.refresh(sub)
            return sub


async def delete_subscription(user_id: int, endpoint: str) -> None:
    async with async_session() as session:
        await session.execute(
            delete(PushSubscription).where(
                PushSubscription.user_id == user_id,
                PushSubscription.endpoint == endpoint,
            )
        )
        await session.commit()


async def _remove_expired_subscription(user_id: int, endpoint: str) -> None:
    """Remove a subscription that returned 410 Gone."""
    try:
        await delete_subscription(user_id, endpoint)
        logger.info("Removed expired push subscription for user %d", user_id)
    except Exception:
        logger.warning("Failed to remove expired subscription", exc_info=True)


async def send_push_notification(
    user_id: int,
    title: str,
    body: str,
    url: str = "/",
) -> None:
    """Send a push notification to all subscriptions for a user.

    Fire-and-forget — wrap in asyncio.create_task() at call site.
    """
    if not vapid_enabled():
        logger.debug("VAPID not configured, skipping push notification")
        return

    webpush = _get_webpush()
    if webpush is None:
        logger.warning("pywebpush not installed, cannot send push notifications")
        return

    async with async_session() as session:
        result = await session.execute(
            select(PushSubscription).where(PushSubscription.user_id == user_id)
        )
        subscriptions = list(result.scalars().all())

    if not subscriptions:
        return

    payload = json.dumps({"title": title, "body": body, "url": url})
    vapid_claims = {
        "sub": Config.VAPID_CLAIMS_SUB,
    }

    def _send_sync(sub: PushSubscription) -> tuple[int, str]:
        """Synchronous send — runs in executor."""
        try:
            subscription_info = {
                "endpoint": sub.endpoint,
                "keys": {"p256dh": sub.p256dh, "auth": sub.auth},
            }
            response = webpush(
                subscription_info=subscription_info,
                data=payload,
                vapid_private_key=Config.VAPID_PRIVATE_KEY,
                vapid_claims=vapid_claims,
            )
            return response.status_code, sub.endpoint
        except Exception as e:
            logger.error("Push send failed for sub %d: %s", sub.id, e, exc_info=True)
            return 0, sub.endpoint

    loop = asyncio.get_event_loop()
    tasks = [loop.run_in_executor(None, _send_sync, sub) for sub in subscriptions]
    results = await asyncio.gather(*tasks, return_exceptions=True)

    for sub, result in zip(subscriptions, results):
        if isinstance(result, Exception):
            logger.error("Push gather exception for user %d: %s", user_id, result, exc_info=result)
            continue
        status_code, endpoint = result
        if status_code in (200, 201):
            logger.info("Push notification sent to sub %d (status %d)", sub.id, status_code)
        elif status_code == 410:
            asyncio.create_task(_remove_expired_subscription(user_id, endpoint))
        elif status_code:
            logger.error("Push returned unexpected status %d for user %d sub %d", status_code, user_id, sub.id)
