"""Authentication routes: /login, /verify, /verify/resend, /logout.

Cassandra is passwordless. Single auth flow:

    GET  /login          → enter email
    POST /login          → get_or_create_user → issue OTP → send → 303 /verify
    GET  /verify         → enter 6-digit code (email shown from pending cookie)
    POST /verify         → validate → set session → 303 /
    POST /verify/resend  → reissue OTP (rate-limited)

Signup and login are intentionally the same path — typing your email is
sign-in if you've been here before, sign-up otherwise. No UI signal
distinguishes the two, which also masks user-enumeration.

The /signup endpoints from the previous auth scheme are gone. Anything
that linked to /signup should now link to /login.
"""
from __future__ import annotations

from urllib.parse import urlparse

from fastapi import APIRouter, Depends, Form, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy.ext.asyncio import AsyncSession

from app.auth import (
    PENDING_COOKIE_NAME,
    PENDING_TTL_SECONDS,
    SESSION_COOKIE_NAME,
    SESSION_TTL_SECONDS,
    sign_pending,
    sign_session,
    verify_pending,
)
from app.config import get_settings
from app.db import get_session, utcnow
from app.logging import get_logger
from app.services.auth_service import AuthError, get_or_create_user, get_user
from app.services import otp_service, referral_service
from app.services.email_service import EmailSendError, send_otp, send_welcome_email
from app.templates_env import templates


log = get_logger("auth_router")

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


def _safe_next(next_value: str | None) -> str:
    """Only allow same-origin relative paths to prevent open-redirect."""
    if not next_value or not next_value.startswith("/") or next_value.startswith("//"):
        return "/"
    if urlparse(next_value).netloc:
        return "/"
    return next_value


def _set_session_cookie(response: RedirectResponse, user_id: int) -> None:
    response.set_cookie(
        key=SESSION_COOKIE_NAME,
        value=sign_session(user_id),
        max_age=SESSION_TTL_SECONDS,
        httponly=True,
        samesite="lax",
        secure=False,
        path="/",
    )


def _set_pending_cookie(
    response: RedirectResponse,
    email: str,
    user_id: int,
    ref: str | None = None,
) -> None:
    response.set_cookie(
        key=PENDING_COOKIE_NAME,
        value=sign_pending(email, user_id, ref=ref),
        max_age=PENDING_TTL_SECONDS,
        httponly=True,
        samesite="lax",
        secure=False,
        path="/",
    )


def _clear_pending_cookie(response) -> None:
    response.delete_cookie(PENDING_COOKIE_NAME, path="/")


async def _issue_and_send_otp(session: AsyncSession, email: str) -> bool:
    """Generate a code, persist its hash, send the email. Returns True on
    success. Returns False (and logs) if SMTP submission fails — the OTP
    row is still created so the user can hit /verify/resend."""
    code = await otp_service.issue(session, email, purpose="auth")
    try:
        await send_otp(email, code, otp_service.OTP_TTL_MINUTES)
        return True
    except EmailSendError:
        return False


# ---------------------------------------------------------------------------
# Login (email entry)
# ---------------------------------------------------------------------------


@router.get("/login", response_class=HTMLResponse)
async def login_page(
    request: Request,
    next: str | None = None,
    error: str | None = None,
    ref: str | None = None,
    session: AsyncSession = Depends(get_session),
):
    # If a valid referral code is supplied, surface a small "invited"
    # banner. We resolve it server-side so the banner can show the
    # referrer's actual greeting (and a bad code silently degrades).
    ref_norm = referral_service.normalise_code(ref) if ref else None
    referrer = (
        await referral_service.lookup_referrer(session, ref_norm)
        if ref_norm else None
    )
    return templates.TemplateResponse(
        request, "login.html",
        {
            "next_path": _safe_next(next),
            "error": error,
            "ref": ref_norm if referrer else None,
            "referrer_present": referrer is not None,
        },
    )


@router.post("/login")
async def login_submit(
    request: Request,
    email: str = Form(...),
    next: str | None = Form(default=None),
    ref: str | None = Form(default=None),
    session: AsyncSession = Depends(get_session),
):
    s = get_settings()
    # Look up the referrer up front so a bad code doesn't pollute the
    # rest of the flow. Self-referral protection lives in
    # referral_service.link_new_user.
    ref_norm = referral_service.normalise_code(ref) if ref else None
    referrer = (
        await referral_service.lookup_referrer(session, ref_norm)
        if ref_norm else None
    )

    # Track whether THIS request creates the user row (i.e. a referral
    # capture window). Cleanest way: probe for existence first.
    from app.services.auth_service import get_user_by_email
    was_new = (await get_user_by_email(session, email)) is None

    try:
        user = await get_or_create_user(
            session, email, create_if_missing=s.CASSANDRA_SIGNUP_ENABLED,
        )
    except AuthError as e:
        return templates.TemplateResponse(
            request, "login.html",
            {"next_path": _safe_next(next), "error": str(e), "email": email,
             "ref": ref_norm if referrer else None,
             "referrer_present": referrer is not None},
            status_code=400,
        )

    # First-time signup with a valid referrer → persist the linkage now.
    # We do this BEFORE OTP-verify because the row is already created;
    # if the user abandons OTP we'll have an orphan link but that's
    # harmless audit data.
    if was_new and referrer is not None:
        await referral_service.link_new_user(session, user, referrer)

    # Issue OTP only if cooldown allows; if a fresh one was sent in the
    # last 60s we just reuse the existing one (silently) to avoid
    # spamming the user's inbox on a refreshed form submit.
    allowed, _ = await otp_service.can_request_new(session, user.email)
    if allowed:
        await _issue_and_send_otp(session, user.email)

    resp = RedirectResponse(url="/verify", status_code=303)
    # Stash the referral code on the pending cookie too — handy for
    # showing the "invited" badge on the /verify page so the friend
    # knows the discount is still tracking.
    _set_pending_cookie(
        resp, user.email, user.id,
        ref=ref_norm if referrer is not None else None,
    )
    return resp


# ---------------------------------------------------------------------------
# Verify (code entry)
# ---------------------------------------------------------------------------


@router.get("/verify", response_class=HTMLResponse)
async def verify_page(request: Request, error: str | None = None, sent: str | None = None):
    cookie = request.cookies.get(PENDING_COOKIE_NAME)
    pending = verify_pending(cookie) if cookie else None
    if pending is None:
        return RedirectResponse(url="/login", status_code=303)
    return templates.TemplateResponse(
        request, "verify.html",
        {"email": pending["email"], "error": error, "sent": sent,
         "ttl_minutes": otp_service.OTP_TTL_MINUTES,
         "resend_cooldown": otp_service.RESEND_COOLDOWN_SECONDS},
    )


@router.post("/verify")
async def verify_submit(
    request: Request,
    code: str = Form(...),
    session: AsyncSession = Depends(get_session),
):
    cookie = request.cookies.get(PENDING_COOKIE_NAME)
    pending = verify_pending(cookie) if cookie else None
    if pending is None:
        return RedirectResponse(url="/login", status_code=303)

    email = pending["email"]
    try:
        await otp_service.verify(session, email, code)
    except otp_service.OTPError as e:
        return templates.TemplateResponse(
            request, "verify.html",
            {"email": email, "error": str(e),
             "ttl_minutes": otp_service.OTP_TTL_MINUTES,
             "resend_cooldown": otp_service.RESEND_COOLDOWN_SECONDS},
            status_code=400,
        )

    user = await get_user(session, pending["uid"])
    if user is None:
        # User row vanished between cookie issue and verify. Restart flow.
        return RedirectResponse(url="/login", status_code=303)
    is_first_login = user.last_login_at is None
    user.last_login_at = utcnow()
    # Default opt-in is set on User row creation; we don't touch it here.
    # The one-time welcome email below explains the digest and the Settings
    # opt-out path — re-applying a checkbox state on every login would
    # silently re-subscribe users who explicitly opted out later.
    await session.commit()
    log.info("user.login", user_id=user.id, email=email)

    # First-login welcome email — best effort. SMTP failure must not block
    # the login itself; we log and continue. Idempotent because we commit
    # last_login_at above before this point, so a retried verify won't
    # re-trigger send.
    if is_first_login:
        try:
            await send_welcome_email(email)
        except Exception as e:  # noqa: BLE001
            log.warning("welcome_email.send_failed",
                        user_id=user.id, error=str(e)[:200])

    resp = RedirectResponse(url="/", status_code=303)
    _set_session_cookie(resp, user.id)
    _clear_pending_cookie(resp)
    return resp


@router.post("/verify/resend")
async def verify_resend(
    request: Request,
    session: AsyncSession = Depends(get_session),
):
    cookie = request.cookies.get(PENDING_COOKIE_NAME)
    pending = verify_pending(cookie) if cookie else None
    if pending is None:
        return RedirectResponse(url="/login", status_code=303)

    email = pending["email"]
    allowed, wait = await otp_service.can_request_new(session, email)
    if not allowed:
        return RedirectResponse(
            url=f"/verify?error=Please+wait+{wait}s+before+requesting+another+code",
            status_code=303,
        )
    ok = await _issue_and_send_otp(session, email)
    msg = "A new code has been sent" if ok else "Could not send email — try again shortly"
    return RedirectResponse(url=f"/verify?sent={msg}", status_code=303)


# ---------------------------------------------------------------------------
# Logout
# ---------------------------------------------------------------------------


_LOGOUT_HTML = """<!doctype html><html lang="en"><head>
<meta charset="utf-8">
<title>Signing out…</title>
<meta http-equiv="refresh" content="0;url=/login">
<script>
// Wipe per-user browser state before the redirect. Keeps `cassandra.theme`
// (cosmetic, no privacy concern) so the next user's first paint isn't a
// white-flash. The meta-refresh above is the no-JS fallback for the redirect;
// without JS, localStorage isn't cleared, but base.html's user-mismatch
// guard catches the next authenticated page load.
(function() {
  try {
    var theme = localStorage.getItem('cassandra.theme');
    localStorage.clear();
    if (theme) localStorage.setItem('cassandra.theme', theme);
    sessionStorage.clear();
  } catch (e) {}
  window.location.replace('/login');
})();
</script>
</head><body>Signing out&hellip;</body></html>"""


@router.post("/logout")
async def logout(request: Request):
    resp = HTMLResponse(content=_LOGOUT_HTML)
    resp.delete_cookie(SESSION_COOKIE_NAME, path="/")
    _clear_pending_cookie(resp)
    return resp


@router.get("/logout")
async def logout_get(request: Request):
    return await logout(request)
