"""
Security module for authentication, JWT handling, and role/permission enforcement.
"""
from datetime import datetime, timedelta
from functools import wraps
from typing import Any, Callable, Dict, List, Optional, Union
import inspect
import uuid

from passlib.context import CryptContext
import importlib
from jose import JWTError, jwt
from fastapi import Depends, HTTPException, Request, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy.orm import Session

from app.core.config import settings
from app.core.database import get_db
from app.models.role import UserAuditLog
from app.models.token_blacklist import TokenBlacklist
from app.models.user import User

_preferred_schemes = ["bcrypt"]
try:
    _bcrypt_mod = importlib.import_module("bcrypt")
    # Some binary builds expose metadata in __about__; if missing, fallback
    if not hasattr(_bcrypt_mod, "__about__"):
        raise ImportError("bcrypt backend appears incompatible")
except Exception:
    _preferred_schemes = ["sha256_crypt"]

pwd_context = CryptContext(schemes=_preferred_schemes, deprecated="auto")
security = HTTPBearer(auto_error=False)

SECRET_KEY = settings.secret_key
ALGORITHM = settings.algorithm
ACCESS_TOKEN_EXPIRE_MINUTES = settings.access_token_expire_minutes

class DummyUser:
    """Pseudo-user for local development when no credentials are provided."""

    def __init__(self):
        self.id = "dev-user-id"
        self.username = "developer"
        self.email = "dev@localhost"
        self.role = "super_admin"
        self.branch_id = None
        self.company_id = None
        self.is_active = True


def verify_password(plain_password: str, hashed_password: str) -> bool:
    if not plain_password or not hashed_password:
        return False
    try:
        return pwd_context.verify(plain_password, hashed_password)
    except Exception:
        return False


def get_password_hash(password: str) -> str:
    return pwd_context.hash(password)


def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
    to_encode = data.copy()
    expire = datetime.utcnow() + (
        expires_delta
        if expires_delta is not None
        else timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    )
    to_encode.update({"exp": expire, "jti": str(uuid.uuid4())})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)


def verify_token(token: str) -> Optional[Dict[str, Any]]:
    try:
        return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
    except JWTError:
        return None


def _raise_unauthorized(detail: str = "Invalid authentication credentials") -> None:
    raise HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail=detail,
        headers={"WWW-Authenticate": "Bearer"},
    )


def _resolve_user_from_payload(payload: Dict[str, Any], db: Session) -> Optional[User]:
    if payload.get("cnp_support") is True:
        from app.core.cnp_support import support_user_from_payload
        return support_user_from_payload(payload, db)

    user_identifier = payload.get("sub")
    if not user_identifier:
        return None
    user = db.query(User).filter((User.id == user_identifier) | (User.username == user_identifier)).first()
    if not user:
        return None
    if payload.get("company_id") and not getattr(user, "company_id", None):
        user.company_id = payload.get("company_id")
    return user


def get_current_user(
    credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
    db: Session = Depends(get_db),
) -> User:
    if not credentials:
        _raise_unauthorized("Not authenticated")

    try:
        payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
        jti = payload.get("jti")
        if jti:
            blacklisted = db.query(TokenBlacklist).filter(TokenBlacklist.token_jti == jti).first()
            if blacklisted:
                _raise_unauthorized("Token has been revoked")
        user = _resolve_user_from_payload(payload, db)
        if not user:
            _raise_unauthorized("User not found or inactive")
        if not getattr(user, "is_active", False) and not getattr(user, "cnp_support", False):
            _raise_unauthorized("User not found or inactive")
        return user
    except JWTError:
        _raise_unauthorized()


def get_current_active_user(current_user: Union[User, DummyUser] = Depends(get_current_user)) -> Union[User, DummyUser]:
    if not getattr(current_user, "is_active", False):
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user")
    return current_user


def _requires_role_check(
    predicate: Callable[[Union[User, DummyUser]], bool],
    failure_message: str,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
        @wraps(func)
        async def wrapper(
            *args: Any,
            current_user: Union[User, DummyUser] = Depends(get_current_user),
            **kwargs: Any,
        ) -> Any:
            if not predicate(current_user):
                raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=failure_message)
            result = func(*args, **kwargs)
            if inspect.isawaitable(result):
                return await result
            return result

        return wrapper

    return decorator


def require_roles(*roles: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    if not roles:
        raise ValueError("At least one role is required")
    normalized_roles = {role.lower() for role in roles}
    return _requires_role_check(
        lambda user: getattr(user, "role", "").lower() in normalized_roles,
        "Insufficient role privileges",
    )


def require_any(*roles: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    if not roles:
        raise ValueError("At least one role is required")
    normalized_roles = {role.lower() for role in roles}
    return _requires_role_check(
        lambda user: getattr(user, "role", "").lower() in normalized_roles,
        "Insufficient role privileges",
    )


def forbid_roles(*roles: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    if not roles:
        raise ValueError("At least one role is required")
    normalized_roles = {role.lower() for role in roles}
    return _requires_role_check(
        lambda user: getattr(user, "role", "").lower() not in normalized_roles,
        "This role is forbidden from accessing this endpoint",
    )


def has_permission(user: Union[User, DummyUser], permission: str) -> bool:
    if not getattr(user, "role", None):
        return False
    if getattr(user, "role", "").lower() in {"super_admin", "admin"}:
        return True
    if getattr(user, "user_permissions", None):
        for user_permission in user.user_permissions:
            if getattr(user_permission.permission, "name", None) == permission:
                return True
    return False


def has_role(user: Union[User, DummyUser], role: str) -> bool:
    return getattr(user, "role", "").lower() == role.lower()


def require_permission(permission: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    if not permission:
        raise ValueError("Permission is required")
    # Dependency-style checker for use with `Depends(require_permission_dep(...))`
    def _permission_dep(current_user: Union[User, DummyUser] = Depends(get_current_user)) -> Union[User, DummyUser]:
        if not has_permission(current_user, permission):
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permission")
        return current_user

    # Decorator-style: keep previous behavior for direct use as @require_permission('x')
    def _decorator(func: Callable[..., Any]) -> Callable[..., Any]:
        @wraps(func)
        async def wrapper(
            *args: Any,
            current_user: Union[User, DummyUser] = Depends(get_current_user),
            **kwargs: Any,
        ) -> Any:
            if not has_permission(current_user, permission):
                raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permission")
            result = func(*args, **kwargs)
            if inspect.isawaitable(result):
                return await result
            return result

        return wrapper

    # Attach the dependency factory for explicit dependency use
    _decorator.dependency = _permission_dep  # type: ignore
    return _decorator


def require_permission_or_roles(permission: str, *roles: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    if not permission:
        raise ValueError("Permission is required")
    normalized_roles = {role.lower() for role in roles}
    def _dep(current_user: Union[User, DummyUser] = Depends(get_current_user)) -> Union[User, DummyUser]:
        if has_permission(current_user, permission) or getattr(current_user, "role", "").lower() in normalized_roles:
            return current_user
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permission or role")

    def _decorator(func: Callable[..., Any]) -> Callable[..., Any]:
        @wraps(func)
        async def wrapper(
            *args: Any,
            current_user: Union[User, DummyUser] = Depends(get_current_user),
            **kwargs: Any,
        ) -> Any:
            if has_permission(current_user, permission) or getattr(current_user, "role", "").lower() in normalized_roles:
                result = func(*args, **kwargs)
                if inspect.isawaitable(result):
                    return await result
                return result
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permission or role")

        return wrapper

    _decorator.dependency = _dep  # type: ignore
    return _decorator


def normalize_role(role: str) -> str:
    return role.lower() if isinstance(role, str) else role


def expand_roles(roles: List[str]) -> List[str]:
    return [normalize_role(role) for role in roles if isinstance(role, str)]


def enforce_branch_scope(user: Union[User, DummyUser], target_branch_id: Optional[str] = None) -> bool:
    if not getattr(user, "role", None):
        return False
    if getattr(user, "role", "").lower() in {"super_admin", "admin", "accountant"}:
        return True
    if target_branch_id is None:
        return True
    return str(getattr(user, "branch_id", "")) == str(target_branch_id)


def check_branch_access(user: Union[User, DummyUser], branch_id: str) -> bool:
    if not getattr(user, "role", None):
        return False
    if getattr(user, "role", "").lower() in {"super_admin", "admin", "accountant"}:
        return True
    return str(getattr(user, "branch_id", "")) == str(branch_id)


def require_branch_match(*args: Any, **kwargs: Any) -> Any:
    raise NotImplementedError("require_branch_match is not implemented as a decorator; use branch-specific checks in route logic")


def check_permission() -> bool:
    return True


def apply_branch_scope(query: Any, model_class: Any, user: Union[User, DummyUser] = None) -> Any:
    if user is None:
        return query
    branch_id = getattr(user, "branch_id", None)
    if branch_id is None:
        return query
    if hasattr(model_class, "branch_id"):
        return query.filter(getattr(model_class, "branch_id") == branch_id)
    return query


def log_user_action(
    db: Session,
    user: Union[User, DummyUser],
    action: str,
    module: str,
    resource_type: str,
    resource_id: str = None,
    details: dict = None,
    request: Optional[Request] = None,
) -> None:
    if not getattr(user, "id", None):
        return
    from app.core.cnp_support import CNP_SUPPORT_USER_ID, ensure_cnp_support_user, is_cnp_support_user

    user_id = user.id
    action_details = dict(details or {})
    if is_cnp_support_user(user):
        ensure_cnp_support_user(db)
        user_id = CNP_SUPPORT_USER_ID
        action_details["cnp_support_session"] = True
        action_details["support_context_company_id"] = getattr(user, "company_id", None)
        action_details["support_context_branch_id"] = getattr(user, "branch_id", None)
    audit_log = UserAuditLog(
        user_id=user_id,
        action=action,
        module=module,
        resource_type=resource_type,
        resource_id=resource_id,
        details=action_details if action_details else details,
        ip_address=request.client.host if request else None,
        user_agent=request.headers.get("user-agent") if request else None,
    )
    db.add(audit_log)
    db.commit()

# Constants for role checking
ALLOWED_EVERYTHING = ["super_admin", "admin", "administrator", "developer", "user", "staff", "manager", "accountant"]
UNIVERSAL_ROLES = ["super_admin", "admin", "accountant"]
