"""Atomic intake and explicit edits; domain integrity, not caller authorization."""
from contextlib import contextmanager
from datetime import datetime, timezone as datetime_timezone

from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction
from django.utils import timezone

from apps.devices import queries as device_queries
from apps.devices.locking import persisted_pk, locked_catalog, require_catalog_active
from apps.devices.models import Device
from apps.service_catalog.models import ComplaintSymptom
from apps.service_catalog.queries import complaint_applies_to_category
from .locking import intake_context, share
from .models import (ServiceCase, ServiceCaseNumberSequence, ServiceCaseComplaint,
    ServiceCaseIntakeCondition, ServiceCaseAccessory, ServiceCaseWarrantySnapshot, MAX_JOB_NUMBER)

_UNSET = object()


@contextmanager
def _write():
    try:
        with transaction.atomic():
            yield
    except IntegrityError as error:
        raise ValidationError("Intake conflicts with existing records; reload and retry.") from error


def _received_at(value):
    now = timezone.now()
    if value is None:
        return now
    if not isinstance(value, datetime) or timezone.is_naive(value) or value > now:
        raise ValidationError("Received time must be timezone-aware and no later than now.")
    return value.astimezone(datetime_timezone.utc)


def _capture_warranty(case):
    covered = device_queries.device_has_recorded_warranty_coverage(device=case.device, on_date=case.received_at.date())
    facts = {}
    if covered:
        current = device_queries.current_warranty_coverage(case.device)
        facts = dict(coverage_source=current.coverage_source, coverage_start_date=current.coverage_start_date,
                     coverage_end_date=current.coverage_end_date, coverage_reference=current.reference)
    snapshot = ServiceCaseWarrantySnapshot(service_case=case, recorded_coverage=covered, **facts)
    snapshot._persist()
    return snapshot


def create_service_case(*, company, service_center, customer, device, created_by, intake_channel,
                        received_at=None, customer_reference="", intake_note=""):
    with _write(), intake_context(company=company, service_center=service_center, customer=customer,
                                 device=device, created_by=created_by) as (company, center, customer, device, actor):
        received_at = _received_at(received_at)
        sequence, _ = ServiceCaseNumberSequence.objects.select_for_update().get_or_create(service_center=center)
        if not 1 <= sequence.next_value <= MAX_JOB_NUMBER:
            raise ValidationError("Job number capacity exceeded.")
        number = f"JOB-{sequence.next_value:08d}"
        sequence.next_value += 1
        sequence.save(update_fields=["next_value"])
        case = ServiceCase(company=company, service_center=center, customer=customer, device=device,
            created_by=actor, intake_channel=intake_channel, received_at=received_at,
            customer_reference=customer_reference, intake_note=intake_note, job_number=number)
        case._persist()
        _capture_warranty(case)
        return case


def _locked_case(case, expected=_UNSET):
    current = ServiceCase.objects.select_for_update().get(pk=persisted_pk(case, ServiceCase))
    if expected is not _UNSET and expected != current.updated_at:
        raise ValidationError("ServiceCase changed; reload before continuing.")
    return current


def _editable(case):
    if case.status != ServiceCase.Status.RECEIVED:
        raise ValidationError("Only RECEIVED intake is editable.")


def update_service_case_intake(*, service_case, customer_reference=_UNSET, intake_note=_UNSET, expected_updated_at=_UNSET):
    with _write():
        current = _locked_case(service_case, expected_updated_at)
        _editable(current)
        for field, value in (("customer_reference", customer_reference), ("intake_note", intake_note)):
            if value is not _UNSET:
                setattr(current, field, value)
        current._persist()
        return current


def cancel_service_case(*, service_case, reason="", expected_updated_at=_UNSET, cancelled_by=None):
    from .engineer_services import _actors, _close
    from .models import ServiceEngineerAssignment, ServiceDiagnosticAssessment
    from .diagnostic_services import _abandon
    from .repair_services import _open_execution, _abandon as abandon_repair
    from .models import ServiceRepairExecution
    with _write():
        actor = _actors(cancelled_by) if cancelled_by is not None else None
        current = _locked_case(service_case, expected_updated_at)
        if current.status == ServiceCase.Status.CANCELLED:
            return current
        if current.status in ("REPAIRED", "QC_PENDING", "QC_IN_PROGRESS", "QC_PASSED", "READY_FOR_DELIVERY", "DELIVERED", "CLOSED"):
            raise ValidationError("Successfully repaired cases cannot be cancelled in this phase.")
        assignment = ServiceEngineerAssignment.objects.select_for_update().filter(service_case=current, ended_at__isnull=True).first()
        if (current.status in ("ASSIGNED", "DIAGNOSING", "DIAGNOSED", "REPAIRING")) != (assignment is not None):
            raise ValidationError("Case status and assignment history disagree.")
        assessment = ServiceDiagnosticAssessment.objects.select_for_update().filter(service_case=current, completed_at=None, abandoned_at=None).first()
        if (current.status == "DIAGNOSING") != (assessment is not None):
            raise ValidationError("Case status and diagnostic history disagree.")
        repair = ServiceRepairExecution.objects.filter(service_case=current, status="OPEN").first()
        if (current.status == "REPAIRING") != (repair is not None):
            raise ValidationError("Case status and repair history disagree.")
        now = timezone.now()
        if assignment is not None:
            if actor is None:
                raise ValidationError("An explicit cancellation actor is required for assigned cases.")
            if repair is not None:
                repair = _open_execution(repair, current, assignment)
                abandon_repair(repair, actor, "ServiceCase cancelled.", now)
            if assessment is not None:
                if assessment.engineer_assignment_id != assignment.pk:
                    raise ValidationError("Diagnosis does not belong to the current assignment.")
                _abandon(assessment, actor, "ServiceCase cancelled.", now)
            _close(assignment, actor, now, reason)
        current.status = ServiceCase.Status.CANCELLED
        current.cancelled_at = now
        current.cancellation_reason = reason
        current._persist()
        return current


def add_service_case_complaint(*, service_case, complaint_symptom, customer_description=""):
    case_id = persisted_pk(service_case, ServiceCase)
    symptom_id = persisted_pk(complaint_symptom, ComplaintSymptom)
    device = Device.objects.get(pk=ServiceCase.objects.values_list("device_id", flat=True).get(pk=case_id))
    with _write(), locked_catalog(device.product_model_id, device.product_variant_id) as (model, variant):
        require_catalog_active(model, variant)
        symptom = share(ComplaintSymptom, symptom_id)
        current = _locked_case(service_case)
        _editable(current)
        if not complaint_applies_to_category(complaint=symptom, product_category=model.category):
            raise ValidationError("Complaint is inactive or does not apply to this ProductCategory.")
        complaint = ServiceCaseComplaint(service_case=current, complaint_symptom=symptom, customer_description=customer_description)
        complaint._persist()
        return complaint


def remove_service_case_complaint(*, service_case, complaint):
    complaint_id = persisted_pk(complaint, ServiceCaseComplaint)
    with _write():
        current = _locked_case(service_case)
        _editable(current)
        record = ServiceCaseComplaint.objects.select_for_update().get(pk=complaint_id)
        if record.service_case_id != current.pk:
            raise ValidationError("Complaint belongs to another ServiceCase.")
        if record.removed_at is None:
            record.removed_at = timezone.now()
            record._persist()
        return record


def add_service_case_intake_condition(*, service_case, condition_type, description):
    with _write():
        current = _locked_case(service_case)
        _editable(current)
        record = ServiceCaseIntakeCondition(service_case=current, condition_type=condition_type, description=description)
        record._persist()
        return record


def add_service_case_accessory(*, service_case, accessory_type, description="", quantity=1):
    if type(quantity) is not int or quantity <= 0:
        raise ValidationError("Accessory quantity must be a positive integer.")
    with _write():
        current = _locked_case(service_case)
        _editable(current)
        record = ServiceCaseAccessory(service_case=current, accessory_type=accessory_type, description=description, quantity=quantity)
        record._persist()
        return record
