from rest_framework import permissions
from django.contrib.auth import get_user_model
from typing import Any

User = get_user_model()


class IsRecruiterOrAdmin(permissions.BasePermission):
    """Recruiters and admins only, and recruiters need a candidate permission.

    Views using this class operate on candidate data, so a recruiter must hold
    candidates.view_candidate at minimum. Being *entitled to the specific
    candidate* is enforced separately by get_scoped_candidate /
    scope_candidates in the views — this class only covers condition 1.
    """

    def has_permission(self, request: Any, view: Any) -> bool:
        from .access import is_privileged

        if not (request.user and request.user.is_authenticated):
            return False
        if is_privileged(request.user):
            return True
        if (getattr(request.user, "role", "") or "").upper() != "RECRUITER":
            return False
        if not request.user.has_perm("candidates.view_candidate"):
            self.message = (
                "You do not have permission to perform this action on candidates."
            )
            return False
        return True


class IsAdmin(permissions.BasePermission):
    """Permission check for admins only (e.g. the soft-deleted 'Draft' view)."""

    def has_permission(self, request: Any, view: Any) -> bool:
        return bool(
            request.user
            and request.user.is_authenticated
            and request.user.role == "ADMIN"
        )


# Django permission required for each HTTP method on candidate endpoints.
METHOD_PERMISSIONS = {
    "GET": "candidates.view_candidate",
    "HEAD": "candidates.view_candidate",
    "OPTIONS": "candidates.view_candidate",
    "POST": "candidates.add_candidate",
    "PUT": "candidates.change_candidate",
    "PATCH": "candidates.change_candidate",
    "DELETE": "candidates.delete_candidate",
}


class IsCandidateOwnerOrStaff(permissions.BasePermission):
    """
    Permission check. Staff access requires BOTH conditions:
    - the matching candidates.* Django permission for the HTTP method, AND
    - a connection to the candidate (they applied to a JD assigned to this
      recruiter) — enforced object-level via apps.candidates.access.

    Holding candidates.view_candidate therefore grants a recruiter access to
    *their* candidates, never to every candidate in the system.

    Admins and managers are unrestricted. Candidate/standard users can only
    read/write/delete their own profile. All must be authenticated.
    """

    def has_permission(self, request: Any, view: Any) -> bool:
        from .access import is_privileged

        # User must be authenticated
        if not (request.user and request.user.is_authenticated):
            return False

        # Admins/managers are unrestricted.
        if is_privileged(request.user):
            return True

        role = (getattr(request.user, "role", "") or "").upper()
        if role == "CANDIDATE":
            # Candidates manage their own profile via object-level checks, and
            # must never reach the general list endpoint.
            if request.method == "GET" and view.__class__.__name__ == "CandidateListCreateAPIView":
                return False
            return True

        # Other staff roles (recruiters, interviewers, …) need the real Django
        # permission for this method. Condition 2 — being assigned to the JD the
        # candidate applied to — is enforced in has_object_permission and by
        # scope_candidates on list endpoints.
        required = METHOD_PERMISSIONS.get(request.method)
        if required and not request.user.has_perm(required):
            self.message = (
                "You do not have permission to perform this action on candidates."
            )
            return False
        return True

    def has_object_permission(self, request: Any, view: Any, obj: Any) -> bool:
        from .access import can_view_candidate, is_privileged

        # Admins/managers are unrestricted.
        if is_privileged(request.user):
            return True

        # Recruiters are scoped: a candidate is only reachable if they applied
        # to one of this recruiter's JDs (see apps.candidates.access). This is
        # what stops a forwarded "View Candidate" email from working for a
        # recruiter the application doesn't belong to.
        candidate = obj if hasattr(obj, "applications") else getattr(obj, "candidate", None)
        if candidate is not None:
            if can_view_candidate(request.user, candidate):
                return True
            # DRF renders `self.message` as the 403 body, so the API returns the
            # exact wording the UI shows instead of a generic denial.
            from .access import CANDIDATE_FORBIDDEN_MESSAGE
            self.message = CANDIDATE_FORBIDDEN_MESSAGE
            return False

        # Fallback owner check for objects with no candidate relation.
        if hasattr(obj, "user"):
            return obj.user == request.user
        return False
