"""Issue to job custody, consume in an open repair, or return unused stock."""
import uuid
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import F, Q, Sum
from django.utils import timezone
from apps.parts.locking import persisted_pk
from apps.service.engineer_services import _actors, _eligibility_dependencies
from apps.service.engineer_queries import eligible_engineers_for_service_case
from apps.service.models import ServiceEngineerAssignment, ServiceRepairAction
from apps.service.repair_services import _open_execution, _validate_taxonomy
from .models import InventoryLocation, PartsRequest, PartsIssue, PartsDisposition, StockReservation, SerializedStockUnit
from .request_services import job_context, _event
from .locking import lock_positions
from .services import _post, check_revision, quantity_value, text_value
from .queries import USABLE_TYPES


@transaction.atomic
def issue_reserved_parts(*, actor, reservation, expected_revision, note=""):
    snapshot = StockReservation.objects.select_related("service_case", "spare_part", "location", "line").get(pk=persisted_pk(reservation, StockReservation))
    assignment = ServiceEngineerAssignment.objects.select_related("engineer").filter(service_case=snapshot.service_case, ended_at=None).first()
    if assignment is None:
        raise ValidationError("A current engineer assignment is required.")
    actor = _actors(actor, assignment.engineer)
    _eligibility_dependencies(snapshot.service_case_id, assignment.engineer)
    with job_context(actor=actor, service_case=snapshot.service_case, permission="inventory.issue_parts",
            parts=[snapshot.spare_part], locations=[snapshot.location]) as (actor, case, parts, locations):
        current = ServiceEngineerAssignment.objects.select_for_update().filter(service_case=case, ended_at=None).first()
        if current is None or current.pk != assignment.pk or not eligible_engineers_for_service_case(case).filter(pk=current.engineer_id).exists():
            raise ValidationError("The responsible engineer changed or is ineligible.")
        request = PartsRequest.objects.select_for_update().get(pk=snapshot.line.request_id)
        if request.status != "APPROVED":
            raise ValidationError("An approved request is required.")
        row = StockReservation.objects.select_for_update().get(pk=snapshot.pk)
        check_revision(row, expected_revision)
        if row.status != "ACTIVE":
            raise ValidationError("Only active reserved stock can be issued.")
        source, part = locations[row.location_id], parts[row.spare_part_id]
        custody = InventoryLocation(company_id=case.company_id, service_center_id=case.service_center_id,
            code=f"CUSTODY-{row.pk.hex.upper()}", name=f"Job custody: {case.job_number}", location_type="CUSTODY")
        custody._persist()
        lock_positions([source, custody], [part])
        links = list(row.units.select_for_update().order_by("pk"))
        units = list(SerializedStockUnit.objects.filter(pk__in=[link.unit_id for link in links]))
        row.status, row.ended_by, row.ended_at, row.reason = "ISSUED", actor, timezone.now(), "Issued to job custody."
        row._persist()
        for link in links:
            link.is_active = False
            link._persist()
        movement = _post(actor=actor, company=case.company, spare_part=part, source=source, destination=custody,
            quantity=row.quantity, units=units, reference=f"ISSUE-{row.pk}", idempotency_key=row.pk, note=note)
        issue = PartsIssue(reservation=row, service_case=case, engineer_assignment=current, recipient_id=current.engineer_id,
            issuer=actor, custody_location=custody, movement=movement, note=text_value(note))
        issue._persist()
        complete = not request.lines.annotate(issued=Sum("reservations__quantity",
            filter=Q(reservations__status="ISSUED"), default=0)).exclude(issued=F("quantity")).exists()
        if complete:
            request.status, request.decided_by, request.decided_at = "FULFILLED", actor, timezone.now()
            _event(request, actor, "FULFILLED")
        request._persist()
        return issue


def _issue_snapshot(issue):
    return PartsIssue.objects.select_related("service_case", "reservation__spare_part", "custody_location").get(pk=persisted_pk(issue, PartsIssue))


def usage_revision(issue):
    resolved = issue.dispositions.aggregate(value=Sum("quantity"))["value"] or 0
    return f"{issue.updated_at.isoformat()}:{resolved}"


def _dispose(*, actor, issue, quantity, units, destination, action, command_key, note, expected_revision):
    snapshot = _issue_snapshot(issue)
    quantity = quantity_value(quantity)
    consume = action is not None
    action_snapshot = ServiceRepairAction.objects.select_related("repair_execution").get(pk=persisted_pk(action, ServiceRepairAction)) if consume else None
    with job_context(actor=actor, service_case=snapshot.service_case,
            permission="inventory.consume_parts" if consume else "inventory.return_parts",
            parts=[snapshot.reservation.spare_part], locations=[snapshot.custody_location] + ([destination] if destination else []),
            operational=consume, taxonomy_ids=[action_snapshot.repair_action_id] if consume else ()) as (actor, case, parts, locations):
        if case.status == "CLOSED":
            raise ValidationError("Closed service history is terminal.")
        if consume:
            current = ServiceEngineerAssignment.objects.select_for_update().filter(service_case=case, ended_at=None).first()
            if current is None or current.engineer_id != actor.pk or current.pk != snapshot.engineer_assignment_id or not eligible_engineers_for_service_case(case).filter(pk=actor.pk).exists():
                raise ValidationError("Only the responsible eligible engineer can consume issued stock.")
            execution = _open_execution(action_snapshot.repair_execution, case, current)
            action = ServiceRepairAction.objects.select_for_update().get(pk=action_snapshot.pk)
            if not action.is_active or action.performed_at is None or action.repair_execution_id != execution.pk or action.repair_action_id != action_snapshot.repair_action_id:
                raise ValidationError("Consumption requires a performed active action in the open repair.")
            _validate_taxonomy(action.repair_action_id, case.device.product_model.category)
            from apps.commercial.gating import require_commercial_authorization
            require_commercial_authorization(service_case=case, actor=actor, operation="CONSUME", repair_execution=execution,
                action=action, spare_part=parts[snapshot.reservation.spare_part_id], quantity=quantity)
        row = PartsIssue.objects.select_for_update().get(pk=snapshot.pk)
        if expected_revision is not None and usage_revision(row) != expected_revision:
            raise ValidationError("Issued stock changed; reload and review remaining custody.")
        resolved = row.dispositions.aggregate(value=Sum("quantity"))["value"] or 0
        if resolved + quantity > snapshot.reservation.quantity:
            raise ValidationError("Disposition exceeds unresolved issued quantity.")
        source = locations[row.custody_location_id]
        destination = locations[destination.pk] if destination else None
        if destination and (destination.location_type not in USABLE_TYPES or not destination.is_active):
            raise ValidationError("Unused stock must return to an active usable location.")
        part = parts[snapshot.reservation.spare_part_id]
        lock_positions([source] + ([destination] if destination else []), [part])
        movement = _post(actor=actor, company=case.company, spare_part=part, source=source, destination=destination,
            quantity=quantity, units=units, reference=f"JOB-{row.pk}", idempotency_key=command_key, note=note,
            kind="CONSUME" if consume else "MOVE")
        disposition = PartsDisposition(issue=row, kind="CONSUMED" if consume else "RETURNED", quantity=quantity,
            movement=movement, repair_action=action if consume else None, actor=actor, note=text_value(note))
        disposition._persist()
        return disposition


def consume_issued_parts(*, actor, issue, repair_action, quantity, command_key, units=(), note="", expected_revision=None):
    return _dispose(actor=actor, issue=issue, quantity=quantity, units=units, destination=None, action=repair_action,
        command_key=command_key, note=note, expected_revision=expected_revision)


def return_unused_parts(*, actor, issue, destination, quantity, command_key, units=(), note="", expected_revision=None):
    return _dispose(actor=actor, issue=issue, quantity=quantity, units=units, destination=destination, action=None,
        command_key=command_key, note=note, expected_revision=expected_revision)


def require_inventory_resolved(service_case):
    """Caller must hold the ServiceCase update lock; do not acquire later locks."""
    if StockReservation.objects.filter(service_case=service_case, status="ACTIVE").exists():
        raise ValidationError("Active parts reservations must be released or resolved before closure.")
    from django.db.models import F
    if PartsIssue.objects.filter(service_case=service_case).annotate(resolved=Sum("dispositions__quantity", default=0)).filter(resolved__lt=F("reservation__quantity")).exists():
        raise ValidationError("Issued parts must be consumed or returned before closure.")


def recover_defective_component(*, actor, repair_action, location, component_description,
        command_key, quantity=1, observed_identifier="", replacement=None, note="", expected_action_revision=None):
    from .models import DefectiveRecovery
    from .services import normalized_identifier
    snapshot = ServiceRepairAction.objects.select_related("repair_execution__service_case").get(pk=persisted_pk(repair_action, ServiceRepairAction))
    if not isinstance(command_key, uuid.UUID):
        raise ValidationError("Supply an explicit UUID command key.")
    with job_context(actor=actor, service_case=snapshot.repair_execution.service_case,
            permission="inventory.recover_defective_parts", locations=[location], taxonomy_ids=[snapshot.repair_action_id]) as (actor, case, _, locations):
        current = ServiceEngineerAssignment.objects.select_for_update().filter(service_case=case, ended_at=None).first()
        if current is None or current.engineer_id != actor.pk or not eligible_engineers_for_service_case(case).filter(pk=actor.pk).exists():
            raise ValidationError("Recovery requires the responsible eligible engineer.")
        execution = _open_execution(snapshot.repair_execution, case, current)
        action = ServiceRepairAction.objects.select_for_update().get(pk=snapshot.pk)
        check_revision(action, expected_action_revision)
        if not action.is_active or action.performed_at is None or action.repair_action_id != snapshot.repair_action_id:
            raise ValidationError("Recovery requires a performed active action in the open repair.")
        _validate_taxonomy(action.repair_action_id, case.device.product_model.category)
        location = locations[location.pk]
        if not location.is_active or location.location_type not in ("QUARANTINE", "DEFECTIVE"):
            raise ValidationError("Removed components require an active quarantine or defective location.")
        if replacement is not None:
            replacement = PartsDisposition.objects.select_related("issue").get(pk=persisted_pk(replacement, PartsDisposition))
            if replacement.kind != "CONSUMED" or replacement.issue.service_case_id != case.pk or replacement.repair_action_id != action.pk:
                raise ValidationError("Replacement evidence must be consumption for this case and action.")
        row = DefectiveRecovery(company_id=case.company_id, service_case=case, device_id=case.device_id, repair_action=action,
            replacement=replacement, location=location, component_description=text_value(component_description, required=True, maximum=200),
            observed_identifier=normalized_identifier(observed_identifier) if observed_identifier else "", quantity=quantity_value(quantity),
            actor=actor, command_key=command_key, note=text_value(note))
        row._persist()
        return row
