"""
Sessions: each sign-in, recorded so people can see where they're signed in
and so a session — a person's every session, or everyone's — can be signed
out before its token expires.

A sign-in creates a user_sessions row; its token carries the row's id
("sid"). Every request (app/core/database.py's get_db) checks:
- that the session hasn't been signed out, and
- that the token wasn't issued before users.tokens_valid_after — which also
  catches tokens with no session (issued before sessions existed).

Signing out:
- yourself: your current session, or all your others;
- a password change: your other sessions; a reset or an administrator
  setting the password: all of that person's sessions;
- an administrator: any session, everything a person has, or everyone in
  the workspace but themselves.
"""

import uuid
from datetime import datetime, timedelta, timezone

from fastapi import HTTPException, status
from pydantic import BaseModel
from sqlalchemy import select, text, update
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.observability import client_info
from app.core.security import AuthContext
from app.models.identity import User, UserSession

# last_seen_at is updated at most this often per session.
_SEEN_EVERY = timedelta(minutes=5)


def _now() -> datetime:
    return datetime.now(timezone.utc)


class SessionRead(BaseModel):
    id: uuid.UUID
    user_id: uuid.UUID
    user_name: str | None = None
    user_email: str | None = None
    portal: bool = False
    created_at: datetime
    last_seen_at: datetime
    expires_at: datetime
    ip: str | None
    user_agent: str | None
    amr: list[str]
    # The session making this request.
    current: bool = False


async def start(db: AsyncSession, user: User, amr: list[str], lifetime: timedelta) -> UserSession:
    """Records a sign-in. `db` must be scoped to the user's tenant."""
    ip, agent = client_info()
    session = UserSession(
        tenant_id=user.tenant_id, user_id=user.id, created_at=_now(), last_seen_at=_now(), expires_at=_now() + lifetime,
        ip=(ip or None) and ip[:64], user_agent=(agent or None) and agent[:400], amr=amr,
    )
    db.add(session)
    await db.flush()
    return session


_SIGNED_OUT = HTTPException(
    status_code=status.HTTP_401_UNAUTHORIZED, detail="You've been signed out — please sign in again.", headers={"WWW-Authenticate": "Bearer"}
)


async def ensure_live(db: AsyncSession, ctx: AuthContext) -> None:
    """401 when this token's session was signed out. For every request."""
    if ctx.user_id is None:
        return
    row = (
        await db.execute(
            text(
                "SELECT u.tokens_valid_after, s.id AS sid, s.revoked_at, s.last_seen_at "
                "FROM users u LEFT JOIN user_sessions s ON s.id = :sid AND s.user_id = u.id WHERE u.id = :uid"
            ),
            {"uid": str(ctx.user_id), "sid": str(ctx.session_id) if ctx.session_id else None},
        )
    ).one_or_none()
    if row is None:
        return  # no such account here; the route decides what that means
    # Inclusive: a token from the same second as the sign-out is turned away too.
    if row.tokens_valid_after is not None and (ctx.issued_at is None or ctx.issued_at <= int(row.tokens_valid_after.timestamp())):
        raise _SIGNED_OUT
    if ctx.session_id is None:
        return
    if row.sid is None or row.revoked_at is not None:
        raise _SIGNED_OUT
    if _now() - row.last_seen_at > _SEEN_EVERY:
        await db.execute(update(UserSession).where(UserSession.id == ctx.session_id).values(last_seen_at=_now()))


async def live(db: AsyncSession, tenant_id: uuid.UUID, *, user_id: uuid.UUID | None = None, current: uuid.UUID | None = None) -> list[SessionRead]:
    """Sessions not signed out or expired, most recently active first."""
    stmt = (
        select(UserSession, User.name, User.email, User.vendor_id, User.tokens_valid_after)
        .join(User, User.id == UserSession.user_id)
        .where(UserSession.tenant_id == tenant_id, UserSession.revoked_at.is_(None), UserSession.expires_at > _now())
        .order_by(UserSession.last_seen_at.desc())
    )
    if user_id is not None:
        stmt = stmt.where(UserSession.user_id == user_id)
    result = []
    for s, name, email, vendor_id, valid_after in (await db.execute(stmt)).all():
        if valid_after is not None and s.created_at < valid_after:
            continue  # signed out everywhere since
        result.append(
            SessionRead(
                id=s.id, user_id=s.user_id, user_name=name, user_email=email, portal=vendor_id is not None, created_at=s.created_at,
                last_seen_at=s.last_seen_at, expires_at=s.expires_at, ip=s.ip, user_agent=s.user_agent, amr=list(s.amr or []), current=s.id == current,
            )
        )
    return result


async def revoke(db: AsyncSession, tenant_id: uuid.UUID, session_id: uuid.UUID, *, user_id: uuid.UUID | None = None) -> bool:
    """Signs one session out — only `user_id`'s, when given. False if there was none."""
    stmt = update(UserSession).where(UserSession.tenant_id == tenant_id, UserSession.id == session_id, UserSession.revoked_at.is_(None))
    if user_id is not None:
        stmt = stmt.where(UserSession.user_id == user_id)
    return (await db.execute(stmt.values(revoked_at=_now()))).rowcount > 0


async def revoke_others(db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID, keep: uuid.UUID | None) -> int:
    """Signs out every session of `user_id` but `keep`."""
    stmt = update(UserSession).where(UserSession.tenant_id == tenant_id, UserSession.user_id == user_id, UserSession.revoked_at.is_(None))
    if keep is not None:
        stmt = stmt.where(UserSession.id != keep)
    return (await db.execute(stmt.values(revoked_at=_now()))).rowcount


async def sign_out_everywhere(db: AsyncSession, user: User) -> None:
    """Every session and token `user` has, including ones from before sessions existed."""
    user.tokens_valid_after = _now()
    await revoke_others(db, user.tenant_id, user.id, keep=None)


async def sign_out_everyone(db: AsyncSession, tenant_id: uuid.UUID, keep_user: uuid.UUID | None) -> int:
    """Everyone in the workspace but `keep_user`. Returns how many people."""
    now = _now()
    stmt = update(User).where(User.tenant_id == tenant_id)
    if keep_user is not None:
        stmt = stmt.where(User.id != keep_user)
    people = (await db.execute(stmt.values(tokens_valid_after=now))).rowcount
    sessions = update(UserSession).where(UserSession.tenant_id == tenant_id, UserSession.revoked_at.is_(None))
    if keep_user is not None:
        sessions = sessions.where(UserSession.user_id != keep_user)
    await db.execute(sessions.values(revoked_at=now))
    return people
