"""Authoritative registration, correction and lifecycle operations."""
from contextlib import contextmanager

from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction

from apps.catalog.models import ProductModel, ProductVariant
from .identification import TYPES, normalize_identifier, validate_configuration
from .locking import locked_catalog, persisted_pk, require_catalog_active
from .models import Device, DeviceIdentifier

_UNSET = object()


def _identifier_revision(record):
    return [str(record.pk), record.updated_at.isoformat()] if record else None


@contextmanager
def _atomic_write():
    try:
        with transaction.atomic():
            yield
    except IntegrityError as error:
        # Outside the savepoint: do not leave a broken caller transaction or leak
        # identifier values embedded in a PostgreSQL constraint error.
        raise ValidationError("Identifier conflict; the value may already be permanently reserved.") from error


@contextmanager
def _locked_device(device):
    pk = persisted_pk(device, Device)
    snapshot = Device.objects.get(pk=pk)
    with locked_catalog(snapshot.product_model_id, snapshot.product_variant_id) as (model, variant):
        current = Device.objects.select_for_update().get(pk=pk)
        if (current.product_model_id, current.product_variant_id) != (snapshot.product_model_id, snapshot.product_variant_id):
            raise ValidationError("Device catalog ownership changed unexpectedly.")
        yield current, model, variant


def _configuration(device):
    return {record.identifier_type: record.value for record in
            device.identifiers.filter(is_active=True).select_for_update().order_by("pk")}


def register_device(*, product_model, product_variant=None, imei1="", imei2="", serial=""):
    model_id = persisted_pk(product_model, ProductModel)
    variant_id = None if product_variant is None else persisted_pk(product_variant, ProductVariant)
    with _atomic_write(), locked_catalog(model_id, variant_id) as (model, variant):
        require_catalog_active(model, variant)
        values = validate_configuration(model, {"IMEI1": imei1, "IMEI2": imei2, "SERIAL": serial})
        device = Device(product_model=model, product_variant=variant)
        device._persist()
        # Common key ordering avoids reversed multi-identifier reservation waits.
        for kind, (value, _, key) in sorted(values.items(), key=lambda item: item[1][2]):
            DeviceIdentifier(device=device, identifier_type=kind, value=value)._persist()
        return device


def replace_device_identifier(*, device, identifier_type, new_value, expected_identifier_revision=_UNSET):
    if identifier_type not in TYPES:
        raise ValidationError("Unsupported identifier type.")
    _, _, key = normalize_identifier(identifier_type, new_value)
    with _atomic_write(), _locked_device(device) as (current, model, variant):
        # Corrections also repair inactive devices/configurations. Current catalog
        # and the complete resulting identifier set must nevertheless be valid.
        require_catalog_active(model, variant)
        configuration = _configuration(current)
        old = current.identifiers.filter(identifier_type=identifier_type, is_active=True).first()
        if expected_identifier_revision is not _UNSET and _identifier_revision(old) != expected_identifier_revision:
            raise ValidationError("Identifier changed; reload before correcting it.")
        configuration[identifier_type] = new_value
        validate_configuration(model, configuration)
        reserved = DeviceIdentifier.objects.filter(identity_key=key).first()
        if reserved and (reserved.device_id != current.pk or reserved.identifier_type != identifier_type):
            raise ValidationError("Identifier is permanently reserved to another device or slot.")
        if reserved and reserved.is_active:
            return current
        if old:
            old.is_active = False
            old._persist(update_fields=["is_active"])
        if reserved:
            # Restore the same row to its original slot, preserving its ownership
            # and original entered value. No duplicate historical row is created.
            reserved = DeviceIdentifier.objects.select_for_update().get(pk=reserved.pk)
            reserved.is_active = True
            reserved._persist(update_fields=["is_active"])
        else:
            DeviceIdentifier(device=current, identifier_type=identifier_type, value=new_value)._persist()
        return current


def deactivate_device(*, device):
    with _atomic_write(), _locked_device(device) as (current, _, __):
        if current.is_active:
            current.is_active = False
            current._persist(update_fields=["is_active"])
        return current


def reactivate_device(*, device):
    with _atomic_write(), _locked_device(device) as (current, model, variant):
        require_catalog_active(model, variant)
        validate_configuration(model, _configuration(current))
        if not current.is_active:
            current.is_active = True
            current._persist(update_fields=["is_active"])
        return current


from .evidence_services import (  # noqa: E402
    set_device_purchase_evidence, verify_device_purchase_evidence,
    reject_device_purchase_evidence, set_device_warranty_coverage,
    clear_device_warranty_coverage,
)
from .relationship_services import (  # noqa: E402
    assign_device_owner, end_device_ownership, transfer_device_ownership,
)
