"""Customer-coordinated address/contact workflows; no authorization decisions."""
from contextlib import contextmanager

from django.core.exceptions import ValidationError
from django.utils import timezone

from .detail_locks import locked_customer, persisted_pk, require_operational
from .models import Customer, CustomerAddress, CustomerContact


_UNSET = object()


@contextmanager
def _locked_detail(instance, model):
    pk = persisted_pk(instance, model)
    customer_id = model.objects.values_list("customer_id", flat=True).get(pk=pk)
    with locked_customer(customer_id) as customer:
        yield customer, model.objects.select_for_update().get(pk=pk)


def _primary(customer, record):
    require_operational(customer)
    if not record.is_active:
        raise ValidationError("Only an active record can become primary.")
    if record.is_primary:
        return record
    peers = type(record).objects.filter(customer_id=customer.pk, is_primary=True)
    if isinstance(record, CustomerContact):
        peers = peers.filter(contact_type=record.contact_type)
    # This controlled clear and following validated save share the Customer lock
    # and outer transaction. Failure rolls the complete previous primary back.
    list(peers.select_for_update().order_by("pk"))
    peers.update(is_primary=False, updated_at=timezone.now())
    record.is_primary = True
    record.save(update_fields=["is_primary"])
    return record


def create_customer_address(*, customer, address_type, address_line_1, city, country_code,
                            label="", address_line_2="", area="", district="", postal_code="",
                            is_primary=False):
    with locked_customer(persisted_pk(customer, Customer)) as current:
        require_operational(current)
        record = CustomerAddress(customer=current, address_type=address_type, address_line_1=address_line_1,
            city=city, country_code=country_code, label=label, address_line_2=address_line_2,
            area=area, district=district, postal_code=postal_code)
        record.save(force_insert=True)
        if is_primary:
            _primary(current, record)
        return record


def create_customer_contact(*, customer, contact_type, value, label="", is_primary=False):
    with locked_customer(persisted_pk(customer, Customer)) as current:
        require_operational(current)
        record = CustomerContact(customer=current, contact_type=contact_type, value=value, label=label)
        record.save(force_insert=True)
        if is_primary:
            _primary(current, record)
        return record


def _update(instance, model, changes):
    changes = {name: value for name, value in changes.items() if value is not _UNSET}
    with _locked_detail(instance, model) as (_, record):
        for name, value in changes.items():
            setattr(record, name, value)
        if changes:
            record.save(update_fields=changes)
        return record


def update_customer_address(*, address, address_type=_UNSET, address_line_1=_UNSET,
                            city=_UNSET, country_code=_UNSET, label=_UNSET,
                            address_line_2=_UNSET, area=_UNSET, district=_UNSET, postal_code=_UNSET):
    return _update(address, CustomerAddress, {
        "address_type": address_type, "address_line_1": address_line_1, "city": city,
        "country_code": country_code, "label": label, "address_line_2": address_line_2,
        "area": area, "district": district, "postal_code": postal_code,
    })


def update_customer_contact(*, contact, value=_UNSET, label=_UNSET):
    return _update(contact, CustomerContact, {"value": value, "label": label})


def set_primary_customer_address(*, address):
    with _locked_detail(address, CustomerAddress) as (customer, record):
        return _primary(customer, record)


def set_primary_customer_contact(*, contact):
    with _locked_detail(contact, CustomerContact) as (customer, record):
        return _primary(customer, record)


def _active(instance, model, active):
    with _locked_detail(instance, model) as (customer, record):
        if active:
            require_operational(customer)
        if record.is_active != active:
            record.is_active = active
            record.is_primary = False
            record.save(update_fields=["is_active", "is_primary"])
        return record


def deactivate_customer_address(*, address):
    return _active(address, CustomerAddress, False)


def reactivate_customer_address(*, address):
    return _active(address, CustomerAddress, True)


def deactivate_customer_contact(*, contact):
    return _active(contact, CustomerContact, False)


def reactivate_customer_contact(*, contact):
    return _active(contact, CustomerContact, True)
