"""Serialized front-desk transitions, scoped authorization and existing intake reuse."""
from contextlib import contextmanager
from datetime import datetime

from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils import timezone

from apps.access.authorization import require_permission
from apps.access.locks import share_rows
from apps.access.models import Role, UserRoleAssignment
from apps.customers.models import Customer
from apps.devices.locking import persisted_pk
from apps.organization.models import Company, ServiceCenter, UserOrganizationAssignment
from apps.service.locking import share, intake_context
from apps.service.services import create_service_case
from .models import AppointmentSlot, Appointment, QueueSequence, QueueEntry, FrontdeskEvent


@contextmanager
def center_context(actor, center, permission):
    center_id = persisted_pk(center, ServiceCenter)
    with transaction.atomic():
        actor = share(get_user_model(), persisted_pk(actor, get_user_model()))
        snapshot = ServiceCenter.objects.get(pk=center_id)
        company = share(Company, snapshot.company_id)
        paths = UserOrganizationAssignment.objects.filter(user=actor, company=company)
        share_rows(UserOrganizationAssignment, paths.values_list("pk", flat=True))
        grants = UserRoleAssignment.objects.filter(user=actor, organization_assignment__in=paths)
        share_rows(Role, grants.values_list("role_id", flat=True))
        share_rows(UserRoleAssignment, grants.values_list("pk", flat=True))
        center = ServiceCenter.objects.select_for_update().get(pk=center_id)
        require_permission(user=actor, permission=permission, target=center)
        if (not actor.is_active or not company.is_active or not center.is_active
                or center.company_id != company.pk or not center.region.is_active
                or center.region.company_id != company.pk):
            raise ValidationError("An active, consistent service-center hierarchy is required.")
        yield actor, center


def _event(record, actor, action, reason=""):
    field = {Appointment: "appointment", QueueEntry: "queue_entry", AppointmentSlot: "slot"}[type(record)]
    FrontdeskEvent(**{field: record}, actor=actor, action=action, reason=reason)._persist()


def _parties(actor, center, customer, device):
    customer = share(Customer, persisted_pk(customer, Customer))
    if customer.company_id != center.company_id or not customer.is_active:
        raise ValidationError("Select an active customer in this company.")
    if device is not None:
        # Reuse all frozen catalog, identifier and historical company-affinity checks.
        with intake_context(company=center.company, service_center=center, customer=customer,
                            device=device, created_by=actor) as (_, _, customer, device, _):
            pass
    return customer, device


def _end(slot):
    return timezone.make_aware(datetime.combine(slot.date, slot.end_time), timezone.get_default_timezone())


def create_slot(*, actor, service_center, date, start_time, end_time, capacity):
    with center_context(actor, service_center, "frontdesk.manage_slots") as (actor, center):
        slot = AppointmentSlot(service_center=center, date=date, start_time=start_time,
                               end_time=end_time, capacity=capacity, created_by=actor)
        slot.full_clean()
        if _end(slot) <= timezone.now():
            raise ValidationError("Availability must end in the future.")
        if AppointmentSlot.objects.filter(service_center=center, date=date,
                                          start_time__lt=end_time, end_time__gt=start_time).exists():
            raise ValidationError("Slots must not overlap, including inactive slots.")
        slot._persist()
        _event(slot, actor, "CREATED", f"Capacity {capacity}")
        return slot


def configure_slot(*, actor, slot, capacity, is_active):
    with center_context(actor, slot.service_center, "frontdesk.manage_slots") as (actor, center):
        current = AppointmentSlot.objects.select_for_update().get(pk=slot.pk, service_center=center)
        used = current.appointments.exclude(status__in=["CANCELLED", "NO_SHOW"]).count()
        if type(capacity) is not int or capacity < max(1, used) or type(is_active) is not bool:
            raise ValidationError("Capacity must be positive and cover existing reservations.")
        previous = f"Capacity {current.capacity}, active {current.is_active}"
        current.capacity, current.is_active = capacity, is_active
        current._persist()
        _event(current, actor, "CONFIGURED", f"{previous} -> capacity {capacity}, active {is_active}")
        return current


def book_appointment(*, actor, slot, customer, device=None, reason="", channel="DESK"):
    with center_context(actor, slot.service_center, "frontdesk.create_appointment") as (actor, center):
        current = AppointmentSlot.objects.select_for_update().get(pk=slot.pk, service_center=center)
        if not current.is_active or _end(current) <= timezone.now():
            raise ValidationError("This slot is unavailable.")
        if current.appointments.exclude(status__in=["CANCELLED", "NO_SHOW"]).count() >= current.capacity:
            raise ValidationError("This slot is full.")
        customer, device = _parties(actor, center, customer, device)
        record = Appointment(company=center.company, service_center=center, slot=current, customer=customer,
                             device=device, reason=reason, channel=channel, created_by=actor)
        record._persist()
        _event(record, actor, "SCHEDULED")
        return record


def end_appointment(*, actor, appointment, reason, no_show=False):
    if not reason.strip():
        raise ValidationError("An explicit reason is required.")
    with center_context(actor, appointment.service_center, "frontdesk.cancel_appointment") as (actor, center):
        current = Appointment.objects.select_for_update().get(pk=appointment.pk, service_center=center, company_id=center.company_id)
        target = "NO_SHOW" if no_show else "CANCELLED"
        if current.status == target:
            return current
        if current.status != "SCHEDULED":
            raise ValidationError("Only scheduled appointments can be cancelled or marked no-show here.")
        if no_show and _end(current.slot) > timezone.now():
            raise ValidationError("No-show requires the scheduled slot to have ended.")
        current.status, current.ended_at = target, timezone.now()
        current._persist()
        _event(current, actor, target, reason)
        return current


def _arrival(actor, center, customer, device, reason, appointment=None):
    customer, device = _parties(actor, center, customer, device)
    now = timezone.now()
    date = timezone.localdate(now, timezone.get_default_timezone())
    # Center UPDATE lock coordinates first creation of the dated sequence too.
    sequence, _ = QueueSequence.objects.select_for_update().get_or_create(service_center=center, business_date=date)
    token = sequence.next_value
    sequence.next_value += 1
    sequence.save(update_fields=["next_value"])
    row = QueueEntry(company=center.company, service_center=center, customer=customer, device=device,
                     business_date=date, token=token, appointment=appointment, arrived_at=now,
                     created_by=actor, reason=reason)
    row._persist()
    _event(row, actor, "ARRIVED")
    return row


def add_walk_in(*, actor, service_center, customer, device=None, reason=""):
    with center_context(actor, service_center, "frontdesk.manage_queue") as (actor, center):
        return _arrival(actor, center, customer, device, reason)


def check_in(*, actor, appointment):
    with center_context(actor, appointment.service_center, "frontdesk.check_in_appointment") as (actor, center):
        current = Appointment.objects.select_for_update().get(pk=appointment.pk, service_center=center, company_id=center.company_id)
        existing = QueueEntry.objects.filter(appointment=current).first()
        if existing is not None:
            return existing  # Retry never rewrites original actor/timestamps.
        if current.status != "SCHEDULED" or current.slot.date != timezone.localdate(timezone=timezone.get_default_timezone()):
            raise ValidationError("Check-in requires a scheduled appointment for today.")
        row = _arrival(actor, center, current.customer, current.device, current.reason, current)
        current.status, current.checked_in_at = "CHECKED_IN", row.arrived_at
        current._persist()
        _event(current, actor, "CHECKED_IN")
        return row


def _transition(row, actor, target, reason=""):
    if row.status == target:
        return row
    previous = {"CALLED": "WAITING", "SERVING": "CALLED", "COMPLETED": "SERVING"}
    if target == "CANCELLED":
        if row.status in ("COMPLETED", "CANCELLED") or row.service_case_id or not reason.strip():
            raise ValidationError("Only an unfinished visit without intake can be cancelled, with a reason.")
    elif target not in previous or row.status != previous[target]:
        raise ValidationError("Invalid queue transition.")
    row.status = target
    setattr(row, {"CALLED": "called_at", "SERVING": "serving_at", "COMPLETED": "ended_at", "CANCELLED": "ended_at"}[target], timezone.now())
    row._persist()
    _event(row, actor, target, reason)
    if row.appointment_id and target in ("COMPLETED", "CANCELLED"):
        appointment = Appointment.objects.select_for_update().get(pk=row.appointment_id)
        appointment.status, appointment.ended_at = target, row.ended_at
        appointment._persist()
        _event(appointment, actor, target, reason)
    return row


def transition_queue(*, actor, queue_entry, target, reason=""):
    with center_context(actor, queue_entry.service_center, "frontdesk.manage_queue") as (actor, center):
        row = QueueEntry.objects.select_for_update().get(pk=queue_entry.pk, service_center=center, company_id=center.company_id)
        return _transition(row, actor, target, reason)


def call_next(*, actor, service_center):
    with center_context(actor, service_center, "frontdesk.manage_queue") as (actor, center):
        row = QueueEntry.objects.select_for_update().filter(service_center=center, company_id=center.company_id, status="WAITING",
            business_date=timezone.localdate(timezone=timezone.get_default_timezone())).order_by("token").first()
        return _transition(row, actor, "CALLED") if row else None


def start_intake(*, actor, queue_entry, device=None):
    with center_context(actor, queue_entry.service_center, "frontdesk.manage_queue") as (actor, center):
        require_permission(user=actor, permission="service.add_servicecase", target=center)
        row = QueueEntry.objects.select_for_update().get(pk=queue_entry.pk, service_center=center, company_id=center.company_id)
        if row.service_case_id:
            if device is not None and device.pk != row.device_id:
                raise ValidationError("Existing intake uses a different device.")
            return row.service_case
        if row.status != "SERVING":
            raise ValidationError("Start serving before creating intake.")
        if row.device_id and device is not None and row.device_id != device.pk:
            raise ValidationError("The recorded visit device cannot be replaced.")
        device = device or row.device
        if device is None:
            raise ValidationError("Select a registered device before intake.")
        case = create_service_case(company=center.company, service_center=center, customer=row.customer,
                                   device=device, created_by=actor, intake_channel="WALK_IN", intake_note=row.reason)
        row.device, row.service_case = device, case
        row._persist()
        _event(row, actor, "INTAKE_CREATED")
        return case
