"""Supported writes for retained global part masters and compatibility."""
from django.core.exceptions import ValidationError
from django.db import transaction

from apps.access.locks import share_rows
from .locking import lock_catalog, persisted_pk
from .models import PartCategory, SparePart, SparePartCompatibility

UNSET = object()


def _text(value, field, *, code=False):
    if not isinstance(value, str):
        raise ValidationError({field: "Supply text."})
    return value.strip().upper() if code else value.strip()


def revision(record):
    return record.updated_at.isoformat()


def _precondition(record, expected_revision):
    if expected_revision is not None and expected_revision != revision(record):
        raise ValidationError("This record changed. Reload and review before saving.")


def _get_locked(instance, model):
    pk = persisted_pk(instance, model)
    try:
        return model.objects.select_for_update().get(pk=pk)
    except model.DoesNotExist as error:
        raise ValidationError("The record no longer exists.") from error


def _category(category):
    pk = persisted_pk(category, PartCategory)
    share_rows(PartCategory, [pk])
    try:
        return PartCategory.objects.get(pk=pk)
    except PartCategory.DoesNotExist as error:
        raise ValidationError("The part category no longer exists.") from error


def _part(spare_part, category=UNSET):
    pk = persisted_pk(spare_part, SparePart)
    snapshot = SparePart.objects.filter(pk=pk).values_list("category_id", flat=True).first()
    if snapshot is None:
        raise ValidationError("The spare part no longer exists.")
    target = snapshot if category is UNSET else persisted_pk(category, PartCategory)
    share_rows(PartCategory, [snapshot, target])
    record = _get_locked(spare_part, SparePart)
    if record.category_id != snapshot:
        raise ValidationError("The category changed; reload and retry.")
    try:
        record.category = PartCategory.objects.get(pk=target)
    except PartCategory.DoesNotExist as error:
        raise ValidationError("The part category no longer exists.") from error
    return record


@transaction.atomic
def create_part_category(*, code, name, description=""):
    record = PartCategory(code=_text(code, "code", code=True), name=_text(name, "name"),
        description=_text(description, "description"))
    record._persist()
    return record


@transaction.atomic
def update_part_category(*, part_category, name=UNSET, description=UNSET, expected_revision=None):
    record = _get_locked(part_category, PartCategory)
    _precondition(record, expected_revision)
    for field, value in (("name", name), ("description", description)):
        if value is not UNSET:
            setattr(record, field, _text(value, field))
    record._persist()
    return record


@transaction.atomic
def _category_lifecycle(part_category, active, expected_revision):
    record = _get_locked(part_category, PartCategory)
    _precondition(record, expected_revision)
    record.is_active = active
    record._persist()
    return record


def deactivate_part_category(*, part_category, expected_revision=None):
    return _category_lifecycle(part_category, False, expected_revision)


def reactivate_part_category(*, part_category, expected_revision=None):
    return _category_lifecycle(part_category, True, expected_revision)


def _compatibility_inputs(product_models, product_variants):
    if product_models is UNSET and product_variants is UNSET:
        return None
    if product_models is UNSET or product_variants is UNSET:
        raise ValidationError("Supply both model and variant collections to replace compatibility.")
    return lock_catalog(product_models, product_variants)


def _replace_compatibility(record, ids):
    model_ids, variant_ids = ids
    existing = list(record.compatibilities.select_for_update().order_by("pk"))
    retained_models, retained_variants = set(), set()
    for mapping in existing:
        active = mapping.product_model_id in model_ids or mapping.product_variant_id in variant_ids
        if mapping.product_model_id:
            retained_models.add(mapping.product_model_id)
        else:
            retained_variants.add(mapping.product_variant_id)
        if mapping.is_active != active:
            mapping.is_active = active
            mapping._persist()
    for pk in sorted(model_ids - retained_models, key=str):
        SparePartCompatibility(spare_part=record, product_model_id=pk)._persist()
    for pk in sorted(variant_ids - retained_variants, key=str):
        SparePartCompatibility(spare_part=record, product_variant_id=pk)._persist()


@transaction.atomic
def create_spare_part(*, part_code, name, category, serialization_policy,
                      description="", manufacturer_part_number="", product_models=(), product_variants=()):
    ids = lock_catalog(product_models, product_variants)
    record = SparePart(part_code=_text(part_code, "part_code", code=True), name=_text(name, "name"),
        category=_category(category), serialization_policy=_text(serialization_policy, "serialization_policy"),
        description=_text(description, "description"),
        manufacturer_part_number=_text(manufacturer_part_number, "manufacturer_part_number"))
    record._persist()
    _replace_compatibility(record, ids)
    return record


@transaction.atomic
def update_spare_part(*, spare_part, name=UNSET, category=UNSET, serialization_policy=UNSET,
                      description=UNSET, manufacturer_part_number=UNSET, expected_revision=None,
                      product_models=UNSET, product_variants=UNSET):
    # Admin metadata + compatibility are one operation, maintaining dependency
    # lock order rather than acquiring catalog locks after a metadata save.
    ids = _compatibility_inputs(product_models, product_variants)
    record = _part(spare_part, category)
    _precondition(record, expected_revision)
    if serialization_policy is not UNSET and _text(serialization_policy, "serialization_policy") != record.serialization_policy:
        from apps.inventory.policy import require_policy_editable
        require_policy_editable(record)
    for field, value in (("name", name), ("description", description),
            ("serialization_policy", serialization_policy), ("manufacturer_part_number", manufacturer_part_number)):
        if value is not UNSET:
            setattr(record, field, _text(value, field))
    record._persist()
    if ids is not None:
        _replace_compatibility(record, ids)
    return record


def set_spare_part_compatibility(*, spare_part, product_models, product_variants, expected_revision=None):
    return update_spare_part(spare_part=spare_part, product_models=product_models,
        product_variants=product_variants, expected_revision=expected_revision)


@transaction.atomic
def _part_lifecycle(spare_part, active, expected_revision):
    record = _part(spare_part)
    _precondition(record, expected_revision)
    record.is_active = active
    record._persist()
    return record


def deactivate_spare_part(*, spare_part, expected_revision=None):
    return _part_lifecycle(spare_part, False, expected_revision)


def reactivate_spare_part(*, spare_part, expected_revision=None):
    return _part_lifecycle(spare_part, True, expected_revision)
