from __future__ import annotations

from dataclasses import dataclass

from fastapi import Depends, HTTPException, Request, status
from sqlalchemy import select
from sqlalchemy.orm import Session, selectinload

from app.core.access import (
    INTERNAL_USER_SUBJECT,
    INTERNAL_USER_TENANT_ID,
    get_user_permissions,
    permissions_to_module_map,
)
from app.core.http import ADMIN_AUTH_COOKIE, CLIENT_AUTH_COOKIE, get_bearer_or_cookie_token
from app.core.security_logging import log_security_event
from app.core.security import verify_token
from app.db.session import get_db
from app.models.access import Role, User
from app.models.client_access import ClientFeatureAccess, ClientUser
from app.services.client_access_service import has_access_level, module_access_map


@dataclass(frozen=True)
class AuthSession:
    role: str
    email: str
    name: str
    tenant_id: str | None = None
    client_role: str | None = None
    user_id: int | None = None
    client_account_id: int | None = None
    module_permissions: dict[str, str] | None = None


def _build_internal_auth_session(db: Session, payload: dict) -> AuthSession:
    """Translate an internal-user token into an AuthSession the shared route
    dependencies can consume. Internal users present `role="internal"` so
    `require_client_module_access` can spot them and skip the ClientUser DB
    lookup, deriving their module_permissions from the role-permission table.
    """
    user_id = payload.get("user_id")
    if not isinstance(user_id, int):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication token")

    user = db.scalar(
        select(User)
        .where(User.id == user_id)
        .options(selectinload(User.role).selectinload(Role.permissions))
    )
    if user is None:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User no longer exists")
    if not user.is_active:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User is inactive")

    return AuthSession(
        role="internal",
        email=user.email,
        name=user.name,
        tenant_id=INTERNAL_USER_TENANT_ID,
        client_role=user.role.name if user.role else None,
        user_id=user.id,
        client_account_id=None,
        module_permissions=permissions_to_module_map(get_user_permissions(user)),
    )


def get_auth_session(
    request: Request,
    db: Session = Depends(get_db),
) -> AuthSession:
    token = get_bearer_or_cookie_token(request, cookie_name=CLIENT_AUTH_COOKIE.name) or get_bearer_or_cookie_token(
        request, cookie_name=ADMIN_AUTH_COOKIE.name
    )
    if token is None:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required")

    payload = verify_token(token)

    # Internal Hunter Stock Feeds users get an auth session derived from the
    # role/permission tables rather than the client-portal ClientUser tables.
    if payload.get("sub") == INTERNAL_USER_SUBJECT:
        return _build_internal_auth_session(db, payload)

    return AuthSession(
        role=str(payload.get("role", "")),
        email=str(payload.get("email", "")),
        name=str(payload.get("name", "")),
        tenant_id=payload.get("tenant_id"),
        client_role=payload.get("client_role"),
        user_id=payload.get("user_id"),
        client_account_id=payload.get("client_account_id"),
        module_permissions={},
    )


def require_client_session(session: AuthSession = Depends(get_auth_session)) -> AuthSession:
    # Internal Hunter Stock Feeds users share the workspace with client users
    # but don't have a ClientUser row, so we accept them here and let
    # `require_client_module_access` enforce the per-module checks.
    if session.role == "internal":
        if not session.tenant_id or not session.user_id:
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Internal user context is missing")
        return session
    if session.role != "client":
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Client access required")
    if not session.tenant_id:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Client tenant is missing")
    if not session.user_id or not session.client_account_id:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Client user context is missing")
    return session


def require_admin_session(session: AuthSession = Depends(get_auth_session)) -> AuthSession:
    if session.role != "admin":
        log_security_event("authz.denied", role=session.role, required="admin")
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
    return session


def load_current_client_user(db: Session, session: AuthSession) -> ClientUser:
    user = db.scalar(
        select(ClientUser)
        .where(
            ClientUser.id == session.user_id,
            ClientUser.client_account_id == session.client_account_id,
            ClientUser.tenant_id == session.tenant_id,
        )
        .options(selectinload(ClientUser.module_permissions))
    )
    if user is None:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Client user access is no longer valid")
    if user.status == "suspended":
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Client user access is suspended")
    return user


def require_client_module_access(module_key: str, minimum_level: str = "view"):
    def dependency(
        session: AuthSession = Depends(require_client_session),
        db: Session = Depends(get_db),
    ) -> AuthSession:
        # Internal users have their permissions baked into the AuthSession at
        # token-resolve time; skip the ClientUser/feature DB lookups and check
        # the in-memory module_permissions map directly.
        if session.role == "internal":
            permissions = session.module_permissions or {}
            if not has_access_level(permissions.get(module_key), minimum_level):
                log_security_event("authz.denied", role=session.role, module=module_key, access_level=minimum_level)
                raise HTTPException(
                    status_code=status.HTTP_403_FORBIDDEN,
                    detail=f"{module_key} access is not permitted",
                )
            return session

        user = load_current_client_user(db, session)
        feature = db.scalar(
            select(ClientFeatureAccess).where(
                ClientFeatureAccess.client_account_id == user.client_account_id,
                ClientFeatureAccess.tenant_id == user.tenant_id,
                ClientFeatureAccess.feature_key == module_key,
            )
        )
        if feature is not None and not feature.enabled:
            log_security_event("authz.denied", role=session.role, module=module_key, reason="feature_disabled")
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"{module_key} is disabled for this client")

        permissions = module_access_map(user)
        if not has_access_level(permissions.get(module_key), minimum_level):
            log_security_event("authz.denied", role=session.role, module=module_key, access_level=minimum_level)
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=f"{module_key} access is not permitted")

        return AuthSession(
            role=session.role,
            email=session.email,
            name=session.name,
            tenant_id=session.tenant_id,
            client_role=user.role,
            user_id=user.id,
            client_account_id=user.client_account_id,
            module_permissions=permissions,
        )

    return dependency


def require_ordering_admin_session(
    session: AuthSession = Depends(get_auth_session),
    db: Session = Depends(get_db),
) -> AuthSession:
    """Internal-side authorization for managing the ordering portal.

    Accepts the single Lean admin (``role == "admin"``) or an internal Hunter
    user holding ``manage`` on the ordering module. Client/customer users are
    rejected — they use the customer-facing ``require_client_module_access``
    dependency instead.
    """
    if session.role == "admin":
        return session
    if session.role == "internal":
        permissions = session.module_permissions or {}
        if not has_access_level(permissions.get("ordering"), "manage"):
            log_security_event("authz.denied", role=session.role, module="ordering", access_level="manage")
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Ordering administration requires manage access")
        return session
    log_security_event("authz.denied", role=session.role, module="ordering", required="admin")
    raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Ordering administration requires internal admin access")


def require_client_access_manager_session(
    session: AuthSession = Depends(get_auth_session),
    db: Session = Depends(get_db),
) -> AuthSession:
    if session.role == "admin":
        return session
    if session.role == "internal":
        permissions = session.module_permissions or {}
        if not has_access_level(permissions.get("client_access"), "manage"):
            log_security_event("authz.denied", role=session.role, module="client_access", access_level="manage")
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Client access management requires Lean access")
        return session
    if session.role != "client":
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Client access management requires admin or superadmin access")

    user = load_current_client_user(db, require_client_session(session))
    permissions = module_access_map(user)
    if user.role != "superadmin" or not has_access_level(permissions.get("client_access"), "manage"):
        log_security_event("authz.denied", role=session.role, module="client_access", access_level="manage")
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Superadmin client access is required")

    return AuthSession(
        role=session.role,
        email=session.email,
        name=session.name,
        tenant_id=session.tenant_id,
        client_role=user.role,
        user_id=user.id,
        client_account_id=user.client_account_id,
        module_permissions=permissions,
    )
