"""fastapi-users wiring: password hashing, cookie sessions, the auth dependency.

We use the library end-to-end (no hand-rolled crypto): pwdlib hashing under the
hood, an HttpOnly cookie transport, and a *database* session strategy (tokens live
in the access_token table) so logins survive a restart and can be revoked.
"""
import os
import secrets
import uuid

from fastapi import Depends, Request
from fastapi.concurrency import run_in_threadpool
from fastapi_users import BaseUserManager, FastAPIUsers, InvalidPasswordException, UUIDIDMixin
from fastapi_users.authentication import AuthenticationBackend, CookieTransport
from fastapi_users.authentication.strategy.db import DatabaseStrategy
from fastapi_users_db_sqlalchemy import SQLAlchemyUserDatabase
from fastapi_users_db_sqlalchemy.access_token import SQLAlchemyAccessTokenDatabase
from sqlalchemy.ext.asyncio import AsyncSession

from accounts.db import get_async_session
from accounts.models import AccessToken, User
from core import audit
from notifications import mailer

BASE = "/" + os.environ.get("THERMOGRAPH_BASE", "/thermograph").strip("/")
# The public origin, for links in emailed tokens — same var + default indexnow.py
# and discord.py already use for exactly this purpose (a link that has to resolve
# from wherever the recipient opens it, so it can't be relative to a request).
BASE_URL = os.environ.get("THERMOGRAPH_BASE_URL", "https://thermograph.org").rstrip("/")
# Signs verification tokens. THERMOGRAPH_AUTH_SECRET must be pinned before any
# such token is mailed — a per-boot random fallback would invalidate every
# outstanding link (and every session) on restart.
SECRET = os.environ.get("THERMOGRAPH_AUTH_SECRET") or secrets.token_urlsafe(32)
SESSION_TTL_SECONDS = int(os.environ.get("THERMOGRAPH_SESSION_TTL_DAYS", "30")) * 86400


def _cookie_secure() -> bool:
    # The app serves plain HTTP on the LAN by default; a Secure cookie would never
    # be sent over HTTP, silently breaking auth. So default OFF and let the TLS/VPS
    # deploy opt in with THERMOGRAPH_COOKIE_SECURE=1. Accepted tradeoff: LAN traffic
    # is unencrypted on a trusted network.
    v = os.environ.get("THERMOGRAPH_COOKIE_SECURE", "").strip().lower()
    return v in ("1", "true", "yes", "always", "on")


_COOKIE_SECURE = _cookie_secure()
# Default "lax" (today's exact hardcoded behavior): blocks cross-site form posts
# (CSRF), allows top-level nav, and needs no Secure. "none" is what genuine
# cross-origin fetch (frontend and backend on different origins -- repo-split
# Stage 5) requires -- browsers refuse to store a SameSite=None cookie without
# Secure at all, so that combination fails loud here instead of manifesting
# only as "login mysteriously doesn't persist" once deployed.
COOKIE_SAMESITE = os.environ.get("THERMOGRAPH_COOKIE_SAMESITE", "lax").strip().lower()
if COOKIE_SAMESITE == "none" and not _COOKIE_SECURE:
    raise RuntimeError(
        "THERMOGRAPH_COOKIE_SAMESITE=none requires THERMOGRAPH_COOKIE_SECURE=1 "
        "(browsers drop SameSite=None cookies that aren't also Secure)."
    )


async def get_user_db(session: AsyncSession = Depends(get_async_session)):
    yield SQLAlchemyUserDatabase(session, User)


async def get_access_token_db(session: AsyncSession = Depends(get_async_session)):
    yield SQLAlchemyAccessTokenDatabase(session, AccessToken)


class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
    reset_password_token_secret = SECRET
    verification_token_secret = SECRET

    async def validate_password(self, password: str, user) -> None:
        if len(password) < 8:
            raise InvalidPasswordException(reason="Password must be at least 8 characters.")
        if len(password) > 1024:
            # Guard against a scrypt/argon DoS from a megabyte "password".
            raise InvalidPasswordException(reason="Password is too long.")

    async def on_after_register(self, user: User, request: "Request | None" = None) -> None:
        audit.log_activity("auth.register", {"user_id": str(user.id)})
        # Kick off the same token-generation path a manual "resend verification"
        # request would use, so there is exactly one place that builds the email
        # (on_after_request_verify below) rather than two copies drifting apart.
        await self.request_verify(user, request)

    async def on_after_login(self, user: User, request: "Request | None" = None,
                             response=None) -> None:
        audit.log_activity("auth.login", {"user_id": str(user.id), "verified": user.is_verified})

    async def on_after_verify(self, user: User, request: "Request | None" = None) -> None:
        audit.log_activity("auth.verify", {"user_id": str(user.id)})

    async def on_after_request_verify(
        self, user: User, token: str, request: "Request | None" = None
    ) -> None:
        # mailer.send() already no-ops on the console/disabled backends (dev,
        # tests) — the enabled() check here just skips building the message.
        if not mailer.enabled():
            return
        link = f"{BASE_URL}{BASE}/?verify_token={token}"
        # mailer.send() opens a blocking SMTP connection — fires on every
        # register/resend-verification request, so keep it off the event loop.
        await run_in_threadpool(
            mailer.send,
            user.email,
            "Confirm your Thermograph account",
            "Welcome to Thermograph! Confirm your email address to finish setting up "
            f"your account:\n\n{link}\n\n"
            "If you didn't create this account, you can safely ignore this email.",
        )


async def get_user_manager(user_db=Depends(get_user_db)):
    yield UserManager(user_db)


cookie_transport = CookieTransport(
    cookie_name="tg_session",
    cookie_max_age=SESSION_TTL_SECONDS,
    cookie_path=BASE,          # scope the cookie to /thermograph, not the whole domain
    cookie_secure=_COOKIE_SECURE,
    cookie_httponly=True,      # unreadable by JS -> XSS can't steal the session
    cookie_samesite=COOKIE_SAMESITE,
)


def get_database_strategy(access_token_db=Depends(get_access_token_db)) -> DatabaseStrategy:
    return DatabaseStrategy(access_token_db, lifetime_seconds=SESSION_TTL_SECONDS)


auth_backend = AuthenticationBackend(
    name="cookie",
    transport=cookie_transport,
    get_strategy=get_database_strategy,
)

fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [auth_backend])

# Dependencies handlers use to require / peek at the logged-in user.
current_active_user = fastapi_users.current_user(active=True)
current_user_optional = fastapi_users.current_user(active=True, optional=True)
