"""Authorization decorators for role and permission gating.

Authentication (is the caller logged in?) is handled by Flask-JWT-Extended's
@jwt_required. Authorization (is the caller ALLOWED to do this?) is handled
here. The two are separate concerns; a route needs both on any state-changing
action.

These decorators call verify_jwt_in_request() themselves, so they work whether
or not a separate @jwt_required is also present. The admin role bypasses every
permission check (see User.haspermission), so an admin never needs individual
permissions granted.

Usage:

    @assets_bp.route('/<int:asset_id>', methods=['DELETE'])
    @jwt_required()
    @require_permission('assets.delete')
    def delete_asset(asset_id):
        ...
"""

from functools import wraps

from flask_jwt_extended import verify_jwt_in_request, current_user, get_jwt

from shopdb.utils.responses import error_response, ErrorCodes


def require_permission(permission_name: str):
    """Gate a route behind a single permission. Admin role bypasses.

    A scoped personal API token (patscopes claim present) does NOT bypass: it
    grants only the permissions in its scope list, intersected with what the
    owner actually holds. See shopdb/utils/apitoken_auth.py.
    """
    def decorator(view_func):
        @wraps(view_func)
        def wrapper(*args, **kwargs):
            # verify jwt first so current_user is loaded (idempotent if the
            # route also has @jwt_required)
            verify_jwt_in_request()
            if current_user is None:
                return error_response(
                    ErrorCodes.UNAUTHORIZED,
                    'Authentication required',
                    http_code=401
                )
            patscopes = get_jwt().get('patscopes')
            if patscopes is not None:
                # Scoped PAT: allowed only when this permission is in the scope
                # list AND the owner actually holds it. haspermission still
                # returns True for an admin owner (who legitimately holds
                # everything), so the scope list is the real limiter - the
                # admin bypass is suspended.
                allowed = (permission_name in patscopes
                           and current_user.haspermission(permission_name))
                if not allowed:
                    return error_response(
                        ErrorCodes.FORBIDDEN,
                        'This API token is not scoped for this action',
                        http_code=403
                    )
                return view_func(*args, **kwargs)
            if not current_user.haspermission(permission_name):
                return error_response(
                    ErrorCodes.FORBIDDEN,
                    'You do not have permission to perform this action',
                    http_code=403
                )
            return view_func(*args, **kwargs)
        return wrapper
    return decorator


def require_role(rolename: str):
    """Gate a route behind a single role (e.g. 'admin').

    A scoped personal API token (patscopes claim present) is ALWAYS denied here:
    scopes gate individual permissions, not roles, so role-gated admin surfaces
    require an unscoped token. See shopdb/utils/apitoken_auth.py.
    """
    def decorator(view_func):
        @wraps(view_func)
        def wrapper(*args, **kwargs):
            verify_jwt_in_request()
            if current_user is None:
                return error_response(
                    ErrorCodes.UNAUTHORIZED,
                    'Authentication required',
                    http_code=401
                )
            if get_jwt().get('patscopes') is not None:
                return error_response(
                    ErrorCodes.FORBIDDEN,
                    'Scoped API tokens cannot access role-gated endpoints',
                    http_code=403
                )
            if not current_user.hasrole(rolename):
                return error_response(
                    ErrorCodes.FORBIDDEN,
                    f'{rolename.capitalize()} access required',
                    http_code=403
                )
            return view_func(*args, **kwargs)
        return wrapper
    return decorator
