"""Explicit assignment history transitions; actor attribution is not authorization."""
from contextlib import contextmanager

from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.utils import timezone

from apps.access.locks import share_rows
from apps.access.models import Role, UserRoleAssignment
from apps.devices.locking import persisted_pk
from apps.organization.models import Company, UserOrganizationAssignment
from .engineer_queries import eligible_engineers_for_service_case
from .locking import share
from .models import ServiceCase, ServiceEngineerAssignment, ServiceDiagnosticAssessment
from .services import _UNSET, _write, _locked_case


def _actors(actor, engineer=None):
    User = get_user_model()
    actor_id = persisted_pk(actor, User)
    ids = [actor_id]
    if engineer is not None:
        ids.append(persisted_pk(engineer, User))
    share_rows(User, ids)
    users = {user.pk: user for user in User.objects.filter(pk__in=ids)}
    if any(pk not in users or not users[pk].is_active for pk in ids):
        raise ValidationError("Actors and candidate engineers must be active existing Users.")
    return users[actor_id]


@contextmanager
def _transition(service_case, actor, engineer=None):
    case_id = persisted_pk(service_case, ServiceCase)
    with _write():
        actor = _actors(actor, engineer)
        if engineer is not None:
            _eligibility_dependencies(case_id, engineer)
        case = _locked_case(service_case)
        if case.status not in (ServiceCase.Status.RECEIVED, ServiceCase.Status.ASSIGNED):
            raise ValidationError("Engineer changes require RECEIVED or ASSIGNED state; abandon open diagnosis first.")
        if ServiceDiagnosticAssessment.objects.filter(service_case=case, completed_at=None, abandoned_at=None).exists():
            raise ValidationError("An open diagnosis prevents engineer changes.")
        if engineer is not None and not eligible_engineers_for_service_case(case).filter(pk=engineer.pk).exists():
            raise ValidationError("Engineer is not currently eligible for this ServiceCenter.")
        current = ServiceEngineerAssignment.objects.select_for_update().filter(service_case=case, ended_at__isnull=True).first()
        if (case.status == ServiceCase.Status.ASSIGNED) != (current is not None):
            raise ValidationError("Case status and assignment history disagree.")
        yield case, current, actor


def _eligibility_dependencies(case_id, engineer):
    company_id = ServiceCase.objects.values_list("company_id", flat=True).get(pk=case_id)
    share(Company, company_id)
    paths = UserOrganizationAssignment.objects.filter(user_id=engineer.pk, company_id=company_id)
    share_rows(UserOrganizationAssignment, paths.values_list("pk", flat=True))
    roles = UserRoleAssignment.objects.filter(user_id=engineer.pk, organization_assignment__in=paths)
    share_rows(Role, roles.values_list("role_id", flat=True))


def _expect(current, expected):
    actual = str(current.pk) if current else None
    if expected is not _UNSET and (str(expected) if expected is not None else None) != actual:
        raise ValidationError("Current assignment changed; reload and review it.")


def _close(current, actor, timestamp, reason):
    current.ended_at = timestamp
    current.ended_by = actor
    current.end_reason = reason
    current._persist()


def _open(case, engineer, actor, timestamp, note):
    assignment = ServiceEngineerAssignment(service_case=case, engineer=engineer, assigned_by=actor, assigned_at=timestamp, note=note)
    assignment._persist()
    case.status = ServiceCase.Status.ASSIGNED
    case._persist()
    return assignment


def assign_service_case_engineer(*, service_case, engineer, assigned_by, note="", expected_updated_at=_UNSET):
    persisted_pk(engineer, get_user_model())
    with _transition(service_case, assigned_by, engineer) as (case, current, actor):
        if expected_updated_at is not _UNSET and case.updated_at != expected_updated_at:
            raise ValidationError("ServiceCase changed; reload and review it.")
        if current is not None or case.status != ServiceCase.Status.RECEIVED:
            raise ValidationError("Use explicit reassignment for an assigned case.")
        return _open(case, engineer, actor, timezone.now(), note)


def reassign_service_case_engineer(*, service_case, new_engineer, reassigned_by, reason="", note="", expected_current_assignment_id=_UNSET):
    persisted_pk(new_engineer, get_user_model())
    with _transition(service_case, reassigned_by, new_engineer) as (case, current, actor):
        _expect(current, expected_current_assignment_id)
        if current is None:
            raise ValidationError("No current engineer to reassign.")
        if current.engineer_id == new_engineer.pk:
            raise ValidationError("Choose a different engineer.")
        now = timezone.now()
        _close(current, actor, now, reason)
        return _open(case, new_engineer, actor, now, note)


def unassign_service_case_engineer(*, service_case, unassigned_by, reason="", expected_current_assignment_id=_UNSET):
    with _transition(service_case, unassigned_by) as (case, current, actor):
        _expect(current, expected_current_assignment_id)
        if current is None:
            raise ValidationError("No current engineer to unassign.")
        _close(current, actor, timezone.now(), reason)
        case.status = ServiceCase.Status.RECEIVED
        case._persist()
        return current
