"""Dependency-first locks shared by all inventory commands."""
from contextlib import contextmanager

from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import transaction

from apps.access.authorization import require_permission
from apps.access.locks import share_rows
from apps.access.models import Role, UserRoleAssignment
from apps.organization.assignments import UserOrganizationAssignment
from apps.organization.models import Company, ServiceCenter
from apps.parts.locking import persisted_pk
from apps.parts.models import PartCategory, SparePart
from .models import InventoryLocation, StockPosition


@contextmanager
def inventory_context(*, actor, company, permission, locations=(), parts=(), exclusive_locations=False, authorization_target=None, require_active_parts=True):
    """User -> Company -> assignment/role -> part category/part -> locations.

    StockPosition and units/documents are locked by the caller afterwards.
    Multiple rows in a lock group are always acquired in UUID order.
    """
    user_model = get_user_model()
    actor_id = persisted_pk(actor, user_model)
    company_id = persisted_pk(company, Company)
    location_ids = {persisted_pk(obj, InventoryLocation) for obj in locations}
    part_ids = {persisted_pk(obj, SparePart) for obj in parts}
    with transaction.atomic():
        share_rows(user_model, [actor_id])
        actor = user_model.objects.filter(pk=actor_id, is_active=True).first()
        share_rows(Company, [company_id])
        company = Company.objects.filter(pk=company_id, is_active=True).first()
        if actor is None or company is None:
            raise ValidationError("An active actor and company are required.")
        paths = UserOrganizationAssignment.objects.filter(user=actor, company=company)
        share_rows(UserOrganizationAssignment, paths.values_list("pk", flat=True))
        roles = UserRoleAssignment.objects.filter(user=actor, organization_assignment__in=paths)
        share_rows(Role, roles.values_list("role_id", flat=True))
        if not location_ids:
            target = authorization_target or company
            if authorization_target is not None and (not isinstance(target, ServiceCenter) or target.company_id != company.pk):
                raise ValidationError("Invalid inventory authorization target.")
            require_permission(user=actor, permission=permission, target=target)
        snapshot = dict(SparePart.objects.filter(pk__in=part_ids).values_list("pk", "category_id"))
        if len(snapshot) != len(part_ids):
            raise ValidationError("A spare part no longer exists.")
        share_rows(PartCategory, snapshot.values())
        share_rows(SparePart, part_ids)
        parts = {obj.pk: obj for obj in SparePart.objects.filter(pk__in=part_ids).select_related("category")}
        if {pk: obj.category_id for pk, obj in parts.items()} != snapshot:
            raise ValidationError("Part category changed; reload and retry.")
        if require_active_parts and any(not obj.is_active or not obj.category.is_active for obj in parts.values()):
            raise ValidationError("Active parts and part categories are required.")
        if exclusive_locations:
            locations = list(InventoryLocation.objects.filter(pk__in=location_ids).select_for_update().order_by("pk"))
        else:
            share_rows(InventoryLocation, location_ids)
            locations = list(InventoryLocation.objects.filter(pk__in=location_ids))
        if len(locations) != len(location_ids):
            raise ValidationError("An inventory location no longer exists.")
        for location in locations:
            if location.company_id != company.pk:
                raise ValidationError("Inventory cannot cross company boundaries.")
            target = company
            if location.service_center_id:
                target = ServiceCenter.objects.select_related("region").get(pk=location.service_center_id)
                if target.company_id != company.pk or target.region.company_id != company.pk or not target.is_active or not target.region.is_active:
                    raise ValidationError("An active consistent service-center hierarchy is required.")
            require_permission(user=actor, permission=permission, target=target)
        yield actor, company, {obj.pk: obj for obj in locations}, parts


def lock_positions(locations, spare_parts):
    # First insertion is also serialized by the database unique constraint.
    # The anchor has no quantity; ledger SUM is the authoritative balance.
    result = []
    for location in sorted(locations, key=lambda obj: str(obj.pk)):
        for part in sorted(spare_parts, key=lambda obj: str(obj.pk)):
            row = StockPosition.objects.filter(location=location, spare_part=part).first()
            if row is None:
                # get_or_create cannot use the guarded model.save(). Do the insert
                # in a savepoint and recover only the expected unique race.
                from django.db import IntegrityError
                try:
                    with transaction.atomic():
                        row = StockPosition(location=location, spare_part=part)
                        row._persist()
                except (IntegrityError, ValidationError):
                    row = StockPosition.objects.filter(location=location, spare_part=part).first()
                    if row is None:
                        raise
            result.append(StockPosition.objects.select_for_update().get(pk=row.pk))
    return result
