"""Device-owned evidence writes; no catalog lock or claim-approval decision."""
from contextlib import contextmanager

from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import connection, transaction
from django.utils import timezone

from .locking import persisted_pk
from .models import Device, DevicePurchaseEvidence, DeviceWarrantyCoverage


_UNSET = object()
FACTS = ("purchase_date", "seller_name", "seller_reference", "invoice_number")


@contextmanager
def _device_lock(device):
    with transaction.atomic():
        yield Device.objects.select_for_update().get(pk=persisted_pk(device, Device))


def _expect_revision(current, expected):
    if expected is not _UNSET and (current.updated_at if current else None) != expected:
        raise ValidationError("Purchase evidence changed; reload before editing or reviewing.")


def _retire_purchase_coverage(device):
    current = DeviceWarrantyCoverage.objects.select_for_update().filter(
        device=device, is_active=True, coverage_source=DeviceWarrantyCoverage.Source.PURCHASE_EVIDENCE,
    ).first()
    if current:
        current.is_active = False
        current._persist(update_fields=["is_active"])


def set_device_purchase_evidence(*, device, purchase_date=_UNSET, seller_name=_UNSET,
                                 seller_reference=_UNSET, invoice_number=_UNSET,
                                 expected_updated_at=_UNSET):
    changes = {name: value for name, value in zip(FACTS, (purchase_date, seller_name, seller_reference, invoice_number))
               if value is not _UNSET}
    with _device_lock(device) as current_device:
        evidence = DevicePurchaseEvidence.objects.select_for_update().filter(device=current_device).first()
        _expect_revision(evidence, expected_updated_at)
        new = evidence is None
        if new:
            evidence = DevicePurchaseEvidence(device=current_device)
        before = tuple(getattr(evidence, field) for field in FACTS)
        for field, value in changes.items():
            setattr(evidence, field, value)
        # Normalize before comparing, so cosmetic outer whitespace does not
        # invalidate a review. Dates use Django DateField's date/ISO conversion.
        evidence.clean_fields()
        if new or before != tuple(getattr(evidence, field) for field in FACTS):
            evidence.evidence_status = DevicePurchaseEvidence.Status.UNVERIFIED
            evidence.reviewed_by = None
            evidence.reviewed_at = None
            evidence.verification_note = ""
            evidence._persist()
            _retire_purchase_coverage(current_device)
        return evidence


def _review(*, evidence, reviewed_by, note, status):
    evidence_pk = persisted_pk(evidence, DevicePurchaseEvidence)
    user_model = get_user_model()
    reviewer_pk = persisted_pk(reviewed_by, user_model)
    device_id = DevicePurchaseEvidence.objects.values_list("device_id", flat=True).get(pk=evidence_pk)
    with transaction.atomic():
        # User first: synchronize reviewer activity with ordinary User updates;
        # never acquire a catalog lock behind Device.
        table = connection.ops.quote_name(user_model._meta.db_table)
        with connection.cursor() as cursor:
            cursor.execute(f"SELECT is_active FROM {table} WHERE id = %s FOR SHARE", [reviewer_pk])
            row = cursor.fetchone()
        if not row or not row[0]:
            raise ValidationError("An active persisted User must review evidence.")
        Device.objects.select_for_update().get(pk=device_id)
        current = DevicePurchaseEvidence.objects.select_for_update().get(pk=evidence_pk)
        _expect_revision(current, evidence.updated_at)
        current.evidence_status = status
        current.reviewed_by_id = reviewer_pk
        current.reviewed_at = timezone.now()
        current.verification_note = note
        current._persist(update_fields=["evidence_status", "reviewed_by", "reviewed_at", "verification_note"])
        if status == DevicePurchaseEvidence.Status.REJECTED:
            _retire_purchase_coverage(current.device_id)
        return current


def verify_device_purchase_evidence(*, evidence, reviewed_by, note=""):
    return _review(evidence=evidence, reviewed_by=reviewed_by, note=note, status=DevicePurchaseEvidence.Status.VERIFIED)


def reject_device_purchase_evidence(*, evidence, reviewed_by, note=""):
    return _review(evidence=evidence, reviewed_by=reviewed_by, note=note, status=DevicePurchaseEvidence.Status.REJECTED)


def _expect_coverage(current, expected):
    if expected is not _UNSET and (current.pk if current else None) != expected:
        raise ValidationError("Active coverage changed; reload before replacing or clearing it.")


def set_device_warranty_coverage(*, device, coverage_start_date, coverage_end_date,
                                 coverage_source, reference="", note="", expected_current_id=_UNSET):
    with _device_lock(device) as current_device:
        if not current_device.is_active:
            raise ValidationError("New active coverage requires an active Device.")
        if coverage_source == DeviceWarrantyCoverage.Source.PURCHASE_EVIDENCE:
            evidence = DevicePurchaseEvidence.objects.select_for_update().filter(device=current_device).first()
            if evidence is None or evidence.evidence_status != DevicePurchaseEvidence.Status.VERIFIED:
                raise ValidationError("Purchase-based coverage requires currently verified purchase evidence.")
        current = DeviceWarrantyCoverage.objects.select_for_update().filter(device=current_device, is_active=True).first()
        _expect_coverage(current, expected_current_id)
        replacement = DeviceWarrantyCoverage(device=current_device, coverage_start_date=coverage_start_date,
            coverage_end_date=coverage_end_date, coverage_source=coverage_source, reference=reference, note=note)
        # Validate facts before retiring current coverage; uniqueness is validated
        # again by _persist after the atomic clear.
        replacement.full_clean(validate_constraints=False)
        if current:
            current.is_active = False
            current._persist(update_fields=["is_active"])
        replacement._persist()
        return replacement


def clear_device_warranty_coverage(*, device, expected_current_id=_UNSET):
    with _device_lock(device) as current_device:
        current = DeviceWarrantyCoverage.objects.select_for_update().filter(device=current_device, is_active=True).first()
        _expect_coverage(current, expected_current_id)
        if current:
            current.is_active = False
            current._persist(update_fields=["is_active"])
        return current
