"""Independent QC, immutable outcomes and explicit rework, under ordered locks."""
from contextlib import contextmanager

from django.core.exceptions import ValidationError
from django.utils import timezone

from apps.devices.locking import persisted_pk, locked_catalog, require_catalog_active
from apps.devices.models import Device
from .engineer_services import _actors, _eligibility_dependencies
from .locking import share
from .models import (ServiceCase, ServiceEngineerAssignment, ServiceRepairExecution, ServiceDiagnosticAssessment,
    ServiceCaseComplaint, ServiceQualityControl, ServiceQualityControlCheck, ServiceQualityControlComplaintCheck)
from .services import _write, _locked_case, _UNSET
from .quality_control_checklist import CHECKLIST_VERSION, definition
from .quality_control_queries import eligible_quality_control_inspectors


def _revision(row, expected):
    if expected != row.updated_at:
        raise ValidationError("QC changed; reload and review it.")


def _expected(row, expected):
    return row.updated_at if expected is _UNSET else expected


def _repair(case):
    assignment = ServiceEngineerAssignment.objects.select_for_update().filter(service_case=case, ended_at=None).first()
    repair = ServiceRepairExecution.objects.filter(service_case=case).order_by("-started_at", "-pk").first()
    if assignment is None or repair is None:
        raise ValidationError("Current engineer assignment and completed repair are required.")
    diagnosis = ServiceDiagnosticAssessment.objects.select_for_update().filter(pk=repair.diagnostic_assessment_id,
        service_case=case, engineer_assignment=assignment, completed_at__isnull=False, abandoned_at=None).first()
    repair = ServiceRepairExecution.objects.select_for_update().get(pk=repair.pk)
    if diagnosis is None or repair.engineer_assignment_id != assignment.pk or repair.status != "COMPLETED" or repair.outcome != "REPAIRED":
        raise ValidationError("Latest repair must be successfully completed on the current assignment.")
    return repair, assignment


@contextmanager
def _technical(service_case, inspector):
    case_id = persisted_pk(service_case, ServiceCase)
    with _write():
        inspector = _actors(inspector)
        _eligibility_dependencies(case_id, inspector)
        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 = share(Device, device.pk)
            if not fresh.is_active or (fresh.product_model_id, fresh.product_variant_id) != (device.product_model_id, device.product_variant_id):
                raise ValidationError("Device is inactive or catalog identity changed.")
            case = _locked_case(service_case)
            repair, assignment = _repair(case)
            if assignment.engineer_id == inspector.pk or not eligible_quality_control_inspectors(case).filter(pk=inspector.pk).exists():
                raise ValidationError("An independent, currently eligible QC inspector is required.")
            yield case, repair, inspector


def submit_for_quality_control(*, service_case, inspector, expected_updated_at=_UNSET):
    expected = _expected(service_case, expected_updated_at)
    with _technical(service_case, inspector) as (case, repair, inspector):
        _revision(case, expected)
        if case.status != "REPAIRED" or ServiceQualityControl.objects.filter(service_case=case, status="IN_PROGRESS").exists():
            raise ValidationError("Only REPAIRED cases may enter QC_PENDING.")
        case.status = "QC_PENDING"
        case._persist()
        return case


def begin_quality_control(*, service_case, inspector, expected_updated_at=_UNSET):
    expected = _expected(service_case, expected_updated_at)
    with _technical(service_case, inspector) as (case, repair, inspector):
        _revision(case, expected)
        if case.status != "QC_PENDING" or ServiceQualityControl.objects.filter(service_case=case, status="IN_PROGRESS").exists():
            raise ValidationError("QC must begin from QC_PENDING without an open attempt.")
        row = ServiceQualityControl(service_case=case, repair_execution=repair, inspector=inspector, checklist_version=CHECKLIST_VERSION)
        row._persist()
        for sequence, (code, name, allows_na) in enumerate(definition(row.checklist_version), 1):
            ServiceQualityControlCheck(quality_control=row, check_code=code, check_name=name,
                sequence=sequence, allows_not_applicable=allows_na)._persist()
        for complaint in ServiceCaseComplaint.objects.filter(service_case=case, removed_at=None).order_by("pk"):
            ServiceQualityControlComplaintCheck(quality_control=row, complaint=complaint)._persist()
        case.status = "QC_IN_PROGRESS"
        case._persist()
        return row


def _open(quality_control, case, repair, expected):
    row = ServiceQualityControl.objects.select_for_update().get(pk=persisted_pk(quality_control, ServiceQualityControl))
    if case.status != "QC_IN_PROGRESS" or row.status != "IN_PROGRESS" or row.service_case_id != case.pk or row.repair_execution_id != repair.pk:
        raise ValidationError("QC is not open on the current successful repair.")
    _revision(row, expected)
    return row


@contextmanager
def _attempt(quality_control, inspector, expected_updated_at):
    expected = _expected(quality_control, expected_updated_at)
    case = ServiceQualityControl.objects.get(pk=persisted_pk(quality_control, ServiceQualityControl)).service_case
    with _technical(case, inspector) as (case, repair, inspector):
        row = _open(quality_control, case, repair, expected)
        if row.inspector_id != inspector.pk:
            raise ValidationError("Only the responsible QC inspector may record technical results.")
        yield case, row


def update_quality_control(*, quality_control, inspector, summary, expected_updated_at=_UNSET):
    with _attempt(quality_control, inspector, expected_updated_at) as (case, row):
        row.summary = summary
        row._persist()
        return row


def set_quality_control_check(*, quality_control, inspector, check_code, result, note=_UNSET, expected_updated_at=_UNSET):
    if result not in ServiceQualityControlCheck.Result.values:
        raise ValidationError("Choose a controlled QC check result.")
    with _attempt(quality_control, inspector, expected_updated_at) as (case, row):
        check = ServiceQualityControlCheck.objects.select_for_update().filter(quality_control=row, check_code=check_code).first()
        if check is None:
            raise ValidationError("Select a check from this QC attempt.")
        check.result = result
        if note is not _UNSET:
            check.note = note
        check._persist()
        row._persist()
        return check


def set_quality_control_complaint_check(*, quality_control, inspector, complaint, result, note=_UNSET, expected_updated_at=_UNSET):
    if result not in ServiceQualityControlComplaintCheck.Result.values:
        raise ValidationError("Choose a controlled complaint verification result.")
    complaint_id = persisted_pk(complaint, ServiceCaseComplaint)
    with _attempt(quality_control, inspector, expected_updated_at) as (case, row):
        check = ServiceQualityControlComplaintCheck.objects.select_for_update().filter(quality_control=row, complaint_id=complaint_id).first()
        if check is None:
            raise ValidationError("Select an original active complaint from this QC attempt.")
        check.result = result
        if note is not _UNSET:
            check.note = note
        check._persist()
        row._persist()
        return check


def _checks(row):
    checks = list(ServiceQualityControlCheck.objects.select_for_update().filter(quality_control=row).order_by("pk"))
    complaints = list(ServiceQualityControlComplaintCheck.objects.select_for_update().filter(quality_control=row).order_by("pk"))
    return checks, complaints


def _finish(case, row, outcome):
    row.status, row.outcome, row.completed_at = "COMPLETED", outcome, timezone.now()
    row._persist()
    case.status = "QC_PASSED" if outcome == "PASSED" else "DIAGNOSED"
    case._persist()
    return row


def complete_quality_control_pass(*, quality_control, inspector, summary=_UNSET, expected_updated_at=_UNSET):
    with _attempt(quality_control, inspector, expected_updated_at) as (case, row):
        checks, complaints = _checks(row)
        actual = {(c.check_code, c.check_name, c.allows_not_applicable, c.sequence) for c in checks}
        required = {(code, name, na, sequence) for sequence, (code, name, na) in enumerate(definition(row.checklist_version), 1)}
        if actual != required or any(c.result != "PASS" and not (c.result == "NOT_APPLICABLE" and c.allows_not_applicable and c.note.strip()) for c in checks):
            raise ValidationError("Every required QC check must pass or have an allowed, explained NOT_APPLICABLE result.")
        relevant = set(ServiceCaseComplaint.objects.filter(service_case=case, removed_at=None).values_list("pk", flat=True))
        if {c.complaint_id for c in complaints} != relevant or any(c.result != "RESOLVED" for c in complaints):
            raise ValidationError("Every relevant original complaint must be verified RESOLVED.")
        if summary is not _UNSET:
            row.summary = summary
        return _finish(case, row, "PASSED")


def complete_quality_control_fail(*, quality_control, inspector, failure_note, expected_updated_at=_UNSET):
    if not isinstance(failure_note, str) or not failure_note.strip():
        raise ValidationError("QC failure requires an explicit failure note.")
    with _attempt(quality_control, inspector, expected_updated_at) as (case, row):
        checks, complaints = _checks(row)
        if not any(c.result == "FAIL" for c in checks) and not any(c.result in ("NOT_RESOLVED", "NOT_TESTABLE") for c in complaints):
            raise ValidationError("Record a failed technical or unresolved/untestable complaint check before failing QC.")
        row.failure_note = failure_note
        return _finish(case, row, "FAILED")


def abandon_quality_control(*, quality_control, actor, reason, expected_updated_at=_UNSET):
    if not isinstance(reason, str) or not reason.strip():
        raise ValidationError("QC abandonment requires a reason.")
    expected = _expected(quality_control, expected_updated_at)
    with _write():
        actor = _actors(actor)
        snapshot = ServiceQualityControl.objects.get(pk=persisted_pk(quality_control, ServiceQualityControl))
        case = _locked_case(snapshot.service_case)
        repair, assignment = _repair(case)
        row = _open(quality_control, case, repair, expected)
        row.status, row.abandoned_at, row.abandoned_by, row.abandon_reason = "ABANDONED", timezone.now(), actor, reason
        row._persist()
        case.status = "QC_PENDING"
        case._persist()
        return row
