"""Engineer-owned technical work and explicit, attributed recovery."""
from contextlib import contextmanager

from django.core.exceptions import ValidationError
from django.utils import timezone

from apps.access.locks import share_rows
from apps.devices.locking import persisted_pk, locked_catalog, require_catalog_active
from apps.devices.models import Device
from apps.service_catalog.models import FaultDiagnosis, RootCause
from apps.service_catalog import queries as taxonomy
from .engineer_queries import eligible_engineers_for_service_case
from .engineer_services import _actors, _eligibility_dependencies, _expect
from .locking import share
from .models import ServiceCase, ServiceEngineerAssignment, ServiceDiagnosticAssessment, ServiceDiagnosticFinding
from .services import _UNSET, _write, _locked_case


def _revision(record, expected):
    if expected is not _UNSET and record.updated_at != expected:
        raise ValidationError("Diagnosis changed; reload and review it.")


def _case_for(assessment):
    return ServiceDiagnosticAssessment.objects.get(pk=persisted_pk(assessment, ServiceDiagnosticAssessment)).service_case


@contextmanager
def _technical(service_case, actor, faults=(), causes=()):
    case_id = persisted_pk(service_case, ServiceCase)
    with _write():
        actor = _actors(actor)
        _eligibility_dependencies(case_id, actor)
        device = Device.objects.get(pk=ServiceCase.objects.values_list("device_id", flat=True).get(pk=case_id))
        with locked_catalog(device.product_model_id, device.product_variant_id) as (model, variant):
            require_catalog_active(model, variant)
            fresh_device = share(Device, device.pk)
            if not fresh_device.is_active or (fresh_device.product_model_id, fresh_device.product_variant_id) != (device.product_model_id, device.product_variant_id):
                raise ValidationError("Device is inactive or its catalog identity changed; reload.")
            share_rows(FaultDiagnosis, faults)
            share_rows(RootCause, causes)
            case = _locked_case(service_case)
            current = ServiceEngineerAssignment.objects.select_for_update().filter(service_case=case, ended_at=None).first()
            if (case.status not in ("ASSIGNED", "DIAGNOSING") or current is None or current.engineer_id != actor.pk
                    or not eligible_engineers_for_service_case(case).filter(pk=actor.pk).exists()):
                raise ValidationError("Only the currently eligible assigned engineer can perform diagnosis.")
            yield case, current, actor, model.category


def _open_assessment(assessment, case, current, expected=_UNSET):
    row = ServiceDiagnosticAssessment.objects.select_for_update().get(pk=persisted_pk(assessment, ServiceDiagnosticAssessment))
    if not row.is_open or case.status != "DIAGNOSING" or row.service_case_id != case.pk or row.engineer_assignment_id != current.pk:
        raise ValidationError("Diagnosis is not open on the current engineer assignment.")
    _revision(row, expected)
    return row


def _validate_taxonomy(fault_id, root_id, category):
    fault = FaultDiagnosis.objects.get(pk=fault_id)
    if not taxonomy.fault_diagnosis_applies_to_category(diagnosis=fault, product_category=category):
        raise ValidationError("FaultDiagnosis must be active and applicable.")
    if root_id is not None:
        root = RootCause.objects.get(pk=root_id)
        if not taxonomy.root_cause_applies_to_category(root_cause=root, product_category=category):
            raise ValidationError("RootCause must be active and applicable when supplied.")


def begin_service_case_diagnosis(*, service_case, actor, expected_engineer_assignment_id=_UNSET, expected_updated_at=_UNSET):
    with _technical(service_case, actor) as (case, current, actor, category):
        _revision(case, expected_updated_at)
        _expect(current, expected_engineer_assignment_id)
        if case.status != "ASSIGNED" or ServiceDiagnosticAssessment.objects.filter(service_case=case, completed_at=None, abandoned_at=None).exists():
            raise ValidationError("Diagnosis can begin only once from ASSIGNED.")
        row = ServiceDiagnosticAssessment(service_case=case, engineer_assignment=current, started_by=actor)
        row._persist()
        case.status = "DIAGNOSING"
        case._persist()
        return row


def update_diagnostic_assessment(*, assessment, actor, technical_note, expected_updated_at=_UNSET):
    with _technical(_case_for(assessment), actor) as (case, current, actor, category):
        row = _open_assessment(assessment, case, current, expected_updated_at)
        row.technical_note = technical_note
        row._persist()
        return row


def add_diagnostic_finding(*, assessment, fault_diagnosis, actor, root_cause=None, note="", expected_updated_at=_UNSET):
    fault_id = persisted_pk(fault_diagnosis, FaultDiagnosis)
    root_id = persisted_pk(root_cause, RootCause) if root_cause is not None else None
    with _technical(_case_for(assessment), actor, [fault_id], [root_id] if root_id else []) as (case, current, actor, category):
        row = _open_assessment(assessment, case, current, expected_updated_at)
        _validate_taxonomy(fault_id, root_id, category)
        finding = ServiceDiagnosticFinding(assessment=row, fault_diagnosis_id=fault_id, root_cause_id=root_id, note=note)
        finding._persist()
        row._persist()  # Aggregate revision changes on every finding mutation.
        return finding


def update_diagnostic_finding(*, finding, actor, fault_diagnosis=_UNSET, root_cause=_UNSET, note=_UNSET, expected_updated_at=_UNSET, expected_assessment_updated_at=_UNSET):
    snapshot = ServiceDiagnosticFinding.objects.get(pk=persisted_pk(finding, ServiceDiagnosticFinding))
    fault_id = snapshot.fault_diagnosis_id if fault_diagnosis is _UNSET else persisted_pk(fault_diagnosis, FaultDiagnosis)
    root_id = snapshot.root_cause_id if root_cause is _UNSET else (persisted_pk(root_cause, RootCause) if root_cause is not None else None)
    with _technical(_case_for(snapshot.assessment), actor, [fault_id], [root_id] if root_id else []) as (case, current, actor, category):
        assessment = _open_assessment(snapshot.assessment, case, current, expected_assessment_updated_at)
        row = ServiceDiagnosticFinding.objects.select_for_update().get(pk=snapshot.pk)
        _revision(row, snapshot.updated_at)  # Never add new taxonomy locks behind case.
        _revision(row, expected_updated_at)
        if row.removed_at is not None:
            raise ValidationError("Removed finding cannot change.")
        _validate_taxonomy(fault_id, root_id, category)
        row.fault_diagnosis_id, row.root_cause_id = fault_id, root_id
        if note is not _UNSET:
            row.note = note
        row._persist()
        assessment._persist()
        return row


def remove_diagnostic_finding(*, finding, actor, expected_updated_at=_UNSET, expected_assessment_updated_at=_UNSET):
    snapshot = ServiceDiagnosticFinding.objects.get(pk=persisted_pk(finding, ServiceDiagnosticFinding))
    with _technical(_case_for(snapshot.assessment), actor) as (case, current, actor, category):
        assessment = _open_assessment(snapshot.assessment, case, current, expected_assessment_updated_at)
        row = ServiceDiagnosticFinding.objects.select_for_update().get(pk=snapshot.pk)
        _revision(row, expected_updated_at)
        if row.removed_at is not None:
            raise ValidationError("Finding was already removed.")
        row.removed_at = timezone.now()
        row._persist()
        assessment._persist()
        return row


def _finding_facts(assessment):
    return list(ServiceDiagnosticFinding.objects.filter(assessment=assessment, removed_at=None).order_by("pk").values_list(
        "pk", "fault_diagnosis_id", "root_cause_id", "updated_at"))


def complete_service_case_diagnosis(*, assessment, actor, technical_note=_UNSET, expected_updated_at=_UNSET):
    case = _case_for(assessment)
    facts = _finding_facts(assessment)
    with _technical(case, actor, [row[1] for row in facts], [row[2] for row in facts if row[2] is not None]) as (case, current, actor, category):
        row = _open_assessment(assessment, case, current, expected_updated_at)
        if facts != _finding_facts(row):
            raise ValidationError("Finding set changed; reload before completion.")
        if not facts:
            raise ValidationError("Completion requires at least one finding.")
        for _, fault_id, root_id, _ in facts:
            _validate_taxonomy(fault_id, root_id, category)
        if technical_note is not _UNSET:
            row.technical_note = technical_note
        row.completed_at = timezone.now()
        row.completed_by = actor
        row._persist()
        case.status = "DIAGNOSED"
        case._persist()
        return row


def _abandon(row, actor, reason, timestamp):
    if not isinstance(reason, str) or not reason.strip():
        raise ValidationError("Abandonment requires a reason.")
    row.abandoned_at, row.abandoned_by, row.abandon_reason = timestamp, actor, reason
    row._persist()


def abandon_service_case_diagnosis(*, assessment, actor, reason, expected_updated_at=_UNSET):
    # Explicit recovery, like cancellation: active actor, no engineer impersonation
    # or continued eligibility requirement. Application caller authorization remains separate.
    with _write():
        actor = _actors(actor)
        case = _locked_case(_case_for(assessment))
        current = ServiceEngineerAssignment.objects.select_for_update().filter(service_case=case, ended_at=None).first()
        if current is None:
            raise ValidationError("Current engineer assignment is missing.")
        row = _open_assessment(assessment, case, current, expected_updated_at)
        _abandon(row, actor, reason, timezone.now())
        case.status = "ASSIGNED"
        case._persist()
        return row
