from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.exceptions import PermissionDenied


def get_active_organization(request):
    """Resolve the caller's current Organization.

    Uses request.session['active_org_id'] if set and the user is still a
    member; otherwise falls back to their first membership (this covers
    every user today, since each has exactly one org until the org-switcher
    UI ships). Raises PermissionDenied if they have no membership at all.
    """
    from apps.companies.models import Organization

    user = request.user
    active_org_id = request.session.get("active_org_id")
    if active_org_id:
        org = Organization.objects.filter(pk=active_org_id, members=user).first()
        if org:
            return org

    org = Organization.objects.filter(members=user).order_by("memberships__created_at").first()
    if org is None:
        raise PermissionDenied("User is not a member of any organization.")
    return org


def assert_same_organization(organization, *objs):
    """Raise PermissionDenied if any non-null obj belongs to a different org.

    Guards writable FK fields on a serializer (e.g. `account`, `vendor`,
    `invoice`) that DRF validates for existence but not tenant ownership.
    """
    for obj in objs:
        if obj is not None and obj.organization_id != organization.id:
            raise PermissionDenied(f"{obj.__class__.__name__} is outside your organization.")


def validate_same_organization_field(serializer, value):
    """Field-level validator: raise DRF's own "does not exist" shape if
    `value` belongs to a different organization than the request's active one.

    Use as `validate_<field> = lambda self, value: validate_same_organization_field(self, value)`
    or call directly from a `validate_<field>(self, value)` method. Requires
    `request` in the serializer's context — raises `AssertionError` if it's
    missing rather than silently skipping the check, since a missing request
    context is itself the bug this guards against (see PERF/BE-002 audit:
    several nested-action call sites omitted `context={"request": request}`
    entirely, which would make a context-optional check silently no-op).

    Indistinguishable from a genuinely invalid PK: a caller cannot use this
    to probe whether an ID exists in another organization.
    """
    from rest_framework import serializers as drf_serializers

    request = serializer.context.get("request")
    assert request is not None, (
        f"{serializer.__class__.__name__} must receive request in its "
        "context for cross-organization FK validation to run."
    )
    if value is not None and value.organization_id != get_active_organization(request).id:
        raise drf_serializers.ValidationError(
            f'Invalid pk "{value.pk}" - object does not exist.',
            code="does_not_exist",
        )
    return value


def organization_of(obj, _depth=0):
    """Walk forward FKs to find the Organization owning `obj`, or None.

    Two hops covers everything in this codebase: 13 models hold a direct
    organization FK, 16 reach one through a parent (EmployeeDocument -> employee),
    and the remainder need two (PRFAttachment -> prf -> organization). The depth
    cap keeps a cyclic FK graph from recursing forever.

    Generic relations are followed too (SubmissionVersion -> its FundReport or
    Deliverable), since a GenericForeignKey is the only link some models have to
    their parent and skipping it would leave those rows unownable.
    """
    from apps.companies.models import Organization

    if obj is None or _depth > 2:
        return None

    # An Organization owns itself. Without this, the tenant of a row that *is*
    # the tenant is unresolvable, since Organization has no `organization` field.
    if isinstance(obj, Organization):
        return obj

    org = getattr(obj, "organization", None)
    # Type-checked, not merely non-None: `organization` is not always the FK it
    # looks like. EventSpeaker.organization is a CharField naming the speaker's
    # employer, and returning that string would let a caller compare `.id` on a
    # str (AttributeError) or, worse, treat unrelated models as co-tenants.
    if isinstance(org, Organization):
        return org

    for field in obj._meta.get_fields():
        if not getattr(field, "many_to_one", False) or field.related_model is None:
            continue
        # Skip User: an uploader is not the owner. Authorizing by uploader would
        # let anyone who once touched a file keep reading it after leaving the org.
        if field.related_model._meta.label == settings.AUTH_USER_MODEL:
            continue
        try:
            parent = getattr(obj, field.name, None)
        except ObjectDoesNotExist:
            continue
        found = organization_of(parent, _depth + 1)
        if found is not None:
            return found

    for field in obj._meta.get_fields():
        if not isinstance(field, GenericForeignKey):
            continue
        try:
            parent = getattr(obj, field.name, None)
        except ObjectDoesNotExist:
            continue
        # A GenericForeignKey dereference is an extra query, so it runs only
        # after every concrete FK has failed to produce an organization.
        found = organization_of(parent, _depth + 1)
        if found is not None:
            return found
    return None


def assert_object_in_organization(obj, organization, *, allow_unowned=False):
    """Raise PermissionDenied unless `obj` resolves to `organization`.

    For generic relations (comments) where the target model is client-supplied
    and has no uniform organization FK, so `assert_same_organization` can't be
    used. Fails closed: an object whose organization cannot be derived is
    refused unless the caller opts into `allow_unowned`.
    """
    owner = organization_of(obj)
    if owner is None:
        if allow_unowned:
            return
        raise PermissionDenied("Cannot determine the owner of the target object.")
    if owner.id != organization.id:
        raise PermissionDenied("Target object is outside your organization.")


class TenantScopedMixin:
    """Filters queryset and tags new rows with the caller's active Organization.

    Apply to ViewSets whose model has a direct `organization` FK. For a
    child model scoped only via a parent's organization FK (no column of
    its own), set `organization_lookup = "parent__organization"` — the
    queryset filter joins through it, but perform_create still expects
    `organization` to be settable on the model (skip mixin perform_create /
    override it if the child has no such field to write).

    For ViewSets that already build a custom get_queryset() without calling
    super(), filter by get_active_organization(self.request) inline instead
    — this mixin only composes through the super() chain.
    """

    organization_lookup = "organization"

    def get_queryset(self):
        qs = super().get_queryset()
        return qs.filter(**{self.organization_lookup: get_active_organization(self.request)})

    def perform_create(self, serializer):
        serializer.save(organization=get_active_organization(self.request))
