"""
In-app varslingssenter (FEATURE_BACKLOG.md "Varsler"-runden, 2026-07-25) +
e-post-fallback per type (2026-07-28 oppfølging, migrasjon 033) + push til
telefonens OS (2026-07-28, migrasjon 037, se app/push.py). Push har -- ulikt
e-post -- INGEN egen per-type opt-in: selve det å abonnere (gi
nettleser-tillatelse) ER samtykket, en abonnert enhet får push for ALLE
typer create_notification() skriver.

Eid av BRUKER (mottaker), ikke organisasjon -- plain_connection(), samme
mønster som personlig profil/HCP-historikk/venner. `create_notification()`
er den eneste skrivevegen inn -- kalt fra andre routere (friends.py,
rounds.py) ved konkrete hendelser, ikke noe generisk event-system.

Trygg standard (samme filosofi som visibility_mode='private' ellers i
appen): en bruker som ikke har gjort noe valg får ALDRI e-post for noen
type -- kun `user_notification_email_pref`-raden EN BRUKER selv setter
(via PUT /notifications/email-prefs) slår e-post PÅ for akkurat den typen.
E-post-utsendingen skjer INNI `create_notification()` selv (ikke i hvert
enkelt kallsted) -- driftsfeil i utsendingen skal ALDRI hindre selve
in-app-varselet fra å bli opprettet, samme mønster (SMTP_CONFIGURED-sjekk
+ try/except + traceback.print_exc()) som all annen e-post i appen
(app/routers/auth.py, organizations.py).
"""

from __future__ import annotations

import traceback
from typing import Literal

from fastapi import APIRouter, Depends
from pydantic import BaseModel

from ..auth import CurrentUser, get_current_user
from ..config import settings
from ..db import plain_connection
from ..email import send_notification_email
from ..errors import app_error
from ..push import send_push_to_user

router = APIRouter(tags=["notifications"])

NotificationType = Literal["friend", "tournament", "round", "result"]
_NOTIFICATION_TYPES: tuple[NotificationType, ...] = ("friend", "tournament", "round", "result")
_PUSH_TITLES: dict[str, str] = {
    "friend": "Venner",
    "round": "Runde",
    "result": "Resultat",
    "tournament": "Turnering",
}


async def create_notification(
    conn, *, user_id: str, type: NotificationType, message: str, link_path: str, allow_email: bool = True
) -> None:
    """Kalles fra andre routere sin egen `plain_connection()`/transaksjon --
    tar en allerede-åpen `conn`, åpner ikke en egen. Sender i tillegg en
    e-post-fallback hvis (og kun hvis) mottakeren selv har valgt inn for
    akkurat DENNE typen, OG et push-varsel til enhver enhet mottakeren har
    abonnert (uavhengig av e-post-valget -- se _push_for_notification).

    `allow_email=False` (2026-08-10) lar en enkelt kallsted permanent
    utelukke e-post for akkurat DEN hendelsen, selv om brukeren har krysset
    av for typen generelt -- brukt av "venn startet en runde du kan følge"
    i rounds.py, som deler `type="round"` med "lagt til som medspiller"
    (som fortsatt skal kunne e-postes). In-app-varselet og push forblir
    uendret uansett -- kun selve e-postutsendingen droppes."""
    await conn.execute(
        "INSERT INTO notification (user_id, type, message, link_path) VALUES ($1, $2, $3, $4)",
        user_id,
        type,
        message,
        link_path,
    )

    await _push_for_notification(conn, user_id=user_id, type=type, message=message, link_path=link_path)

    if not allow_email:
        return

    wants_email = await conn.fetchval(
        "SELECT EXISTS(SELECT 1 FROM user_notification_email_pref WHERE user_id = $1 AND type = $2)",
        user_id,
        type,
    )
    if not wants_email:
        return
    recipient = await conn.fetchrow(
        "SELECT email, preferred_locale FROM app_user WHERE id = $1 AND email IS NOT NULL",
        user_id,
    )
    if recipient is None:
        return
    if settings.SMTP_CONFIGURED:
        try:
            await send_notification_email(recipient["email"], message, link_path, recipient["preferred_locale"])
        except Exception:
            # Se modul-docstring: driftsfeil i utsendingen skal aldri
            # hindre selve in-app-varselet, som allerede er skrevet over.
            traceback.print_exc()
    elif settings.DEV_LOG_MAGIC_LINKS:
        print(f"[DEV] Varsel-e-post til {recipient['email']} ({type}): {message}", flush=True)


async def _push_for_notification(conn, *, user_id: str, type: NotificationType, message: str, link_path: str) -> None:
    """Kalt fra create_notification() -- egen liten funksjon KUN for at
    push (uavhengig av e-post-preferansen over) alltid forsøkes for enhver
    abonnert enhet. Feiler aldri synlig for kalleren, se app/push.py."""
    try:
        await send_push_to_user(conn, user_id, _PUSH_TITLES.get(type, "TeeCup"), message, link_path)
    except Exception:
        traceback.print_exc()


class NotificationEmailPrefsOut(BaseModel):
    types: list[str]


class NotificationEmailPrefsUpdate(BaseModel):
    types: list[NotificationType]


@router.get("/notifications/email-prefs", response_model=NotificationEmailPrefsOut)
async def get_email_prefs(user: CurrentUser = Depends(get_current_user)) -> NotificationEmailPrefsOut:
    async with plain_connection() as conn:
        rows = await conn.fetch(
            "SELECT type FROM user_notification_email_pref WHERE user_id = $1",
            user.user_id,
        )
    return NotificationEmailPrefsOut(types=[r["type"] for r in rows])


@router.put("/notifications/email-prefs", response_model=NotificationEmailPrefsOut)
async def set_email_prefs(
    body: NotificationEmailPrefsUpdate, user: CurrentUser = Depends(get_current_user)
) -> NotificationEmailPrefsOut:
    """Full-erstatning, samme mønster som PUT /friends/{id}/categories --
    sender med en tom liste slår e-post AV for alle typer igjen."""
    types = sorted(set(body.types))
    async with plain_connection() as conn, conn.transaction():
        await conn.execute("DELETE FROM user_notification_email_pref WHERE user_id = $1", user.user_id)
        for t in types:
            await conn.execute(
                "INSERT INTO user_notification_email_pref (user_id, type) VALUES ($1, $2)",
                user.user_id,
                t,
            )
    return NotificationEmailPrefsOut(types=types)


class NotificationOut(BaseModel):
    id: str
    type: str
    message: str
    link_path: str
    created_at: str
    read_at: str | None


@router.get("/notifications", response_model=list[NotificationOut])
async def list_notifications(user: CurrentUser = Depends(get_current_user)) -> list[NotificationOut]:
    async with plain_connection() as conn:
        rows = await conn.fetch(
            """
            SELECT id::text AS id, type, message, link_path, created_at, read_at
            FROM notification WHERE user_id = $1
            ORDER BY created_at DESC
            LIMIT 100
            """,
            user.user_id,
        )
    return [
        NotificationOut(
            id=r["id"],
            type=r["type"],
            message=r["message"],
            link_path=r["link_path"],
            created_at=r["created_at"].isoformat(),
            read_at=r["read_at"].isoformat() if r["read_at"] else None,
        )
        for r in rows
    ]


@router.get("/notifications/unread-count")
async def unread_count(user: CurrentUser = Depends(get_current_user)) -> dict[str, int]:
    async with plain_connection() as conn:
        count = await conn.fetchval(
            "SELECT COUNT(*) FROM notification WHERE user_id = $1 AND read_at IS NULL",
            user.user_id,
        )
    return {"count": count}


@router.post("/notifications/{notification_id}/read")
async def mark_read(notification_id: str, user: CurrentUser = Depends(get_current_user)) -> dict[str, bool]:
    async with plain_connection() as conn:
        await conn.execute(
            "UPDATE notification SET read_at = now() WHERE id = $1 AND user_id = $2 AND read_at IS NULL",
            notification_id,
            user.user_id,
        )
    return {"ok": True}


@router.post("/notifications/read-all")
async def mark_all_read(user: CurrentUser = Depends(get_current_user)) -> dict[str, bool]:
    async with plain_connection() as conn:
        await conn.execute(
            "UPDATE notification SET read_at = now() WHERE user_id = $1 AND read_at IS NULL",
            user.user_id,
        )
    return {"ok": True}


# ---------------------------------------------------------------------------
# Push-varsler til telefonens OS (Web Push/VAPID, migrasjon 037). Offentlig
# nøkkel er nettopp det -- offentlig, ingen auth nødvendig for GET-en (samme
# begrunnelse som ethvert annet "her er min offentlige nøkkel"-endepunkt).
# Abonner/avbryt krever derimot en ekte sesjon -- abonnementet er alltid
# knyttet til AKKURAT den innloggede brukeren.
# ---------------------------------------------------------------------------


class VapidPublicKeyOut(BaseModel):
    public_key: str | None
    configured: bool


@router.get("/push/vapid-public-key", response_model=VapidPublicKeyOut)
async def get_vapid_public_key() -> VapidPublicKeyOut:
    return VapidPublicKeyOut(public_key=settings.VAPID_PUBLIC_KEY, configured=settings.PUSH_CONFIGURED)


class PushSubscriptionKeys(BaseModel):
    p256dh: str
    auth: str


class PushSubscriptionCreate(BaseModel):
    endpoint: str
    keys: PushSubscriptionKeys


@router.post("/push/subscribe")
async def subscribe_push(
    body: PushSubscriptionCreate, user: CurrentUser = Depends(get_current_user)
) -> dict[str, bool]:
    if not settings.PUSH_CONFIGURED:
        raise app_error(400, "VALIDATION_FAILED", "Push er ikke konfigurert på serveren.")
    async with plain_connection() as conn:
        # Samme endpoint kan i prinsippet re-abonneres (f.eks. nøklene
        # rotert av nettleseren) -- oppdater i stedet for å feile på
        # unik-constrainten.
        await conn.execute(
            """
            INSERT INTO push_subscription (user_id, endpoint, p256dh, auth)
            VALUES ($1, $2, $3, $4)
            ON CONFLICT (endpoint) DO UPDATE SET user_id = $1, p256dh = $3, auth = $4
            """,
            user.user_id,
            body.endpoint,
            body.keys.p256dh,
            body.keys.auth,
        )
    return {"ok": True}


class PushUnsubscribe(BaseModel):
    endpoint: str


@router.delete("/push/subscribe")
async def unsubscribe_push(
    body: PushUnsubscribe, user: CurrentUser = Depends(get_current_user)
) -> dict[str, bool]:
    async with plain_connection() as conn:
        await conn.execute(
            "DELETE FROM push_subscription WHERE endpoint = $1 AND user_id = $2",
            body.endpoint,
            user.user_id,
        )
    return {"ok": True}
