"""Internal-user authentication and permission introspection routes.

Frontends should call ``GET /api/access/me`` to discover which permission keys
the current user has, then use those keys to hide/show navigation items.
**Visibility is not security** — every privileged backend route must depend on
``require_permission`` (or one of its siblings) directly.
"""
from __future__ import annotations

from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.orm import Session, selectinload

from app.core.access import (
    INTERNAL_USER_SUBJECT,
    INTERNAL_USER_TENANT_ID,
    get_current_user,
    get_user_permissions,
    permissions_to_module_map,
    require_permission,
)
from app.core.config import settings
from app.core.security import hash_password, issue_token, verify_password
from app.db.session import get_db
from app.models.access import Permission, Role, User


router = APIRouter(prefix="/api/access", tags=["access"])


class LoginRequest(BaseModel):
    email: str
    password: str


class UserSession(BaseModel):
    # Mirrors the existing `LoginResponse` shape so the frontend's `AppSession`
    # store can consume this response without a separate type. `permissions`
    # is the new permission-key array; `module_permissions` is the legacy
    # module→access-level map for nav gating.
    user_id: int
    email: str
    name: str
    role: str
    role_name: str | None = None
    is_active: bool
    tenant_id: str | None = None
    client_role: str | None = None
    client_account_id: int | None = None
    module_permissions: dict[str, str] = {}
    permissions: list[str]
    token: str | None = None


class RoleRead(BaseModel):
    id: int
    name: str
    description: str | None
    permissions: list[str]


class UserRead(BaseModel):
    id: int
    email: str
    name: str
    is_active: bool
    role: str | None


def _serialize_session(user: User, *, include_token: bool = False) -> UserSession:
    permission_set = get_user_permissions(user)
    permissions = sorted(permission_set)
    module_permissions = permissions_to_module_map(permission_set)
    role_name = user.role.name if user.role else None
    token = None
    if include_token:
        token = issue_token({"sub": INTERNAL_USER_SUBJECT, "user_id": user.id, "email": user.email})
    # role="internal" is a marker the shared auth deps recognise so internal
    # users can hit the same routes as client-portal users without being
    # confused with them. Display name lives in role_name / client_role.
    return UserSession(
        user_id=user.id,
        email=user.email,
        name=user.name,
        role="internal",
        role_name=role_name,
        is_active=user.is_active,
        tenant_id=INTERNAL_USER_TENANT_ID,
        client_role=role_name,
        client_account_id=None,
        module_permissions=module_permissions,
        permissions=permissions,
        token=token,
    )


@router.post("/login", response_model=UserSession)
def login(payload: LoginRequest, db: Session = Depends(get_db)):
    """Internal-user login.

    Authenticates against a shared internal password (``ADMIN_PASSWORD``) and
    looks up the user by email. Inactive or unknown users are rejected with
    a generic 401 to avoid leaking which emails are valid.
    """
    if payload.password != settings.admin_password:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password")

    email = payload.email.strip().lower()
    user = db.scalar(
        select(User)
        .where(User.email == email)
        .options(selectinload(User.role).selectinload(Role.permissions))
    )
    if user is None or not user.is_active:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or password")

    return _serialize_session(user, include_token=True)


@router.get("/me", response_model=UserSession)
def read_me(user: User = Depends(get_current_user)):
    """Return the current user with permission keys for UI navigation gating."""
    return _serialize_session(user)


@router.get("/me/permissions", response_model=list[str])
def read_my_permissions(user: User = Depends(get_current_user)):
    return sorted(get_user_permissions(user))


class UpdateMeRequest(BaseModel):
    name: str | None = None
    email: str | None = None
    current_password: str | None = None
    new_password: str | None = None


@router.patch("/me", response_model=UserSession)
def update_me(
    payload: UpdateMeRequest,
    db: Session = Depends(get_db),
    user: User = Depends(get_current_user),
):
    """Allow an internal user to update their own name, email, or password."""
    if payload.new_password:
        # Require current password verification before allowing a password change.
        # Users who have never set a personal password must supply the shared
        # admin password as the current credential.
        current_ok = (
            verify_password(payload.current_password or "", user.password_hash)
            if user.password_hash
            else (payload.current_password or "") == settings.admin_password
        )
        if not current_ok:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Current password is incorrect",
            )
        if len(payload.new_password) < 8:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
                detail="New password must be at least 8 characters",
            )
        user.password_hash = hash_password(payload.new_password)

    if payload.name is not None:
        name = payload.name.strip()
        if not name:
            raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Name cannot be empty")
        user.name = name

    if payload.email is not None:
        email = payload.email.strip().lower()
        if not email or "@" not in email:
            raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Invalid email address")
        existing = db.scalar(select(User).where(User.email == email, User.id != user.id))
        if existing:
            raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Email is already in use")
        user.email = email

    db.commit()
    db.refresh(user)
    return _serialize_session(user, include_token=True)


# Permission-enforced administrative endpoints. Route bodies should not check
# role names — every gate is the require_permission(...) dependency.

@router.get("/users", response_model=list[UserRead])
def list_users(
    db: Session = Depends(get_db),
    _: User = Depends(require_permission("view_users")),  # gated by permission key
):
    users = db.scalars(select(User).options(selectinload(User.role))).all()
    return [
        UserRead(
            id=user.id,
            email=user.email,
            name=user.name,
            is_active=user.is_active,
            role=user.role.name if user.role else None,
        )
        for user in users
    ]


@router.get("/roles", response_model=list[RoleRead])
def list_roles(
    db: Session = Depends(get_db),
    _: User = Depends(require_permission("manage_permissions")),  # gated by permission key
):
    roles = db.scalars(
        select(Role).options(selectinload(Role.permissions)).order_by(Role.name)
    ).all()
    return [
        RoleRead(
            id=role.id,
            name=role.name,
            description=role.description,
            permissions=sorted(p.key for p in role.permissions),
        )
        for role in roles
    ]


@router.get("/permissions", response_model=list[str])
def list_permissions(
    db: Session = Depends(get_db),
    _: User = Depends(require_permission("manage_permissions")),  # gated by permission key
):
    return sorted(p.key for p in db.scalars(select(Permission)).all())
