"""Explicit SLA writes. No service lifecycle mutation and no provider dispatch."""
from datetime import timedelta
from django.core.exceptions import ValidationError
from django.db import transaction, connection
from django.db.models import Q
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.catalog.models import ProductCategory, ProductModel
from apps.communications.models import NotificationTemplate
from apps.communications import services as communications
from apps.organization.models import Company, ServiceCenter, UserOrganizationAssignment
from apps.service.models import ServiceCase
from apps.service.engineer_services import _actors, _eligibility_dependencies
from apps.service.locking import share
from .models import SlaPolicy, ServiceSla, SlaEscalation, EscalationCommunication
from . import queries


def save_policy(*, actor, company, code, name, effective_from, target_minutes, warning_minutes,
                service_center=None, product_category=None, effective_until=None, is_active=True,
                due_soon_template=None, overdue_template=None, policy=None):
    with transaction.atomic():
        actor = _actors(actor)
        company = Company.objects.select_for_update().get(pk=company.pk)
        paths = UserOrganizationAssignment.objects.filter(user=actor, company=company)
        share_rows(UserOrganizationAssignment, paths.values_list("pk", flat=True))
        share_rows(Role, UserRoleAssignment.objects.filter(user=actor, organization_assignment__in=paths).values_list("role_id", flat=True))
        require_permission(user=actor, permission="sla.manage_slapolicy", target=company)
        row = SlaPolicy.objects.select_for_update().get(pk=policy.pk, company=company) if policy else SlaPolicy(company=company)
        if policy and row.code != code:
            raise ValidationError("Policy company and code are immutable.")
        center = ServiceCenter.objects.get(pk=service_center.pk) if service_center else None
        category = share(ProductCategory, product_category.pk) if product_category else None
        templates = [NotificationTemplate.objects.get(pk=t.pk) if t else None for t in (due_soon_template, overdue_template)]
        if is_active and (not company.is_active or center and not center.is_active or category and not category.is_active):
            raise ValidationError("An active policy requires active dimensions.")
        if any(t and (not t.is_active or t.company_id != company.pk) for t in templates):
            raise ValidationError("Choose active notification templates in this company.")
        for key, value in dict(code=code, name=name, effective_from=effective_from, effective_until=effective_until,
            target_minutes=target_minutes, warning_minutes=warning_minutes, service_center=center,
            product_category=category, is_active=is_active, due_soon_template=templates[0], overdue_template=templates[1]).items():
            setattr(row, key, value)
        row.updated_by, row.updated_at = actor, timezone.now()
        row.full_clean()
        overlaps = SlaPolicy.objects.filter(company=company, service_center=center, product_category=category,
            is_active=True).exclude(pk=row.pk).filter(Q(effective_until=None) | Q(effective_until__gte=effective_from))
        if effective_until:
            overlaps = overlaps.filter(effective_from__lte=effective_until)
        if is_active and overlaps.exists():
            raise ValidationError("Active policies with this exact scope must not overlap effective dates.")
        row._persist()
        return row


def _case(actor, case, *, exclusive=False):
    actor = _actors(actor)
    company_id = ServiceCase.objects.values_list("company_id", flat=True).get(pk=case.pk)
    if exclusive:
        Company.objects.select_for_update().get(pk=company_id)
    _eligibility_dependencies(case.pk, actor)
    row = ServiceCase.objects.select_for_update().get(pk=case.pk)
    require_permission(user=actor, permission="sla.monitor_sla", target=row.service_center)
    if row.company_id != row.service_center.company_id:
        raise ValidationError("Case organizational scope is inconsistent.")
    return actor, row


def enroll(*, actor, service_case):
    with transaction.atomic():
        actor, case = _case(actor, service_case, exclusive=True)
        existing = ServiceSla.objects.filter(service_case=case).first()
        if existing:
            return existing
        if case.status in ("CANCELLED", "READY_FOR_DELIVERY", "DELIVERED", "CLOSED"):
            return None
        day = timezone.localdate(case.received_at, timezone.get_default_timezone())
        category_id = share(ProductModel, case.device.product_model_id).category_id
        candidates = list(SlaPolicy.objects.filter(company_id=case.company_id, is_active=True,
            effective_from__lte=day).filter(Q(effective_until=None) | Q(effective_until__gte=day)).filter(
            Q(service_center=None) | Q(service_center_id=case.service_center_id)).filter(
            Q(product_category=None) | Q(product_category_id=category_id)))
        if not candidates:
            return None
        rank = lambda p: 2 * bool(p.service_center_id) + bool(p.product_category_id)
        best = max(map(rank, candidates))
        winners = [p for p in candidates if rank(p) == best]
        if len(winners) != 1:
            raise ValidationError("Ambiguous SLA policy configuration.")
        policy = winners[0]
        start = queries.aware(case.received_at)
        due = start + timedelta(minutes=policy.target_minutes)
        snapshot = dict(code=policy.code, name=policy.name, target_minutes=policy.target_minutes,
            warning_minutes=policy.warning_minutes, effective_from=policy.effective_from.isoformat(),
            effective_until=policy.effective_until.isoformat() if policy.effective_until else None,
            company_id=str(policy.company_id), service_center_id=str(policy.service_center_id) if policy.service_center_id else None,
            product_category_id=str(policy.product_category_id) if policy.product_category_id else None,
            due_soon_template_id=str(policy.due_soon_template_id) if policy.due_soon_template_id else None,
            overdue_template_id=str(policy.overdue_template_id) if policy.overdue_template_id else None,
            clock="RECEIVED_TO_READY_ELAPSED", policy_updated_at=policy.updated_at.isoformat())
        row = ServiceSla(service_case=case, policy=policy, product_category_id=category_id,
            policy_snapshot=snapshot, started_at=start, due_at=due,
            warning_at=due-timedelta(minutes=policy.warning_minutes), enrolled_by=actor)
        row._persist()
        return row


def record_escalation(*, actor, sla, origin="MANUAL", at=None):
    with transaction.atomic():
        actor, case = _case(actor, sla.service_case)
        row = ServiceSla.objects.select_for_update().get(pk=sla.pk, service_case=case)
        at = queries.aware(at)
        if at > timezone.now():
            raise ValidationError("Escalation cannot be recorded in the future.")
        observed = queries.describe(queries.annotated(ServiceSla.objects.filter(pk=row.pk), at).get(), at)
        state = observed["state"]
        if origin not in ("MANUAL", "MONITOR"):
            raise ValidationError("Unsupported escalation origin.")
        if state not in ("DUE_SOON", "OVERDUE"):
            return None, False
        existing = SlaEscalation.objects.filter(sla=row, kind=state).first()
        if existing:
            return existing, False
        event = SlaEscalation(sla=row, kind=state, actor=actor, origin=origin, occurred_at=at,
            context=dict(case_reference=case.job_number, started_at=row.started_at.isoformat(), due_at=row.due_at.isoformat(),
                         observed_at=at.isoformat(), state=state, policy=row.policy_snapshot),
            notification_template_id=row.policy_snapshot[state.lower()+"_template_id"])
        event._persist()
        return event, True


def queue_escalation(*, actor, escalation):
    """Queue historical alert evidence; retry failures, never duplicate successes.

    Call after recording commits. The monitor enforces that boundary. A nested
    savepoint rolls back communications failures without rolling back SLA evidence.
    """
    with transaction.atomic():
        actor, case = _case(actor, escalation.sla.service_case, exclusive=True)
        event = SlaEscalation.objects.select_for_update().get(pk=escalation.pk)
        if not event.notification_template_id:
            return None
        previous = EscalationCommunication.objects.filter(escalation=event, notification__isnull=False).first()
        if previous:
            return previous
        # The event is historical: this never promises the case is still overdue.
        try:
            with transaction.atomic():
                notification = communications.request_notification(actor=actor, template=event.notification_template,
                    customer=case.customer, service_center=case.service_center, event="SLA_"+event.kind,
                    related_id=case.pk, related_reference=case.job_number, event_key="SLA:"+str(event.pk),
                    context={"service_case_reference": case.job_number, "device_name": str(case.device.product_model)})
        except Exception:
            result = EscalationCommunication(escalation=event, actor=actor, error_code="NOTIFICATION_REQUEST_FAILED")
        else:
            result = EscalationCommunication(escalation=event, actor=actor, notification=notification)
        result._persist()
        return result


def monitor(*, actor, company_id=None, service_center_id=None, case_id=None, origin="MONITOR"):
    if connection.in_atomic_block:
        raise ValidationError("Run monitoring outside an enclosing transaction so SLA evidence commits before notification requests.")
    scope = queries.cases(actor, "sla.monitor_sla")
    if company_id:
        scope = scope.filter(company_id=company_id)
    if service_center_id:
        scope = scope.filter(service_center_id=service_center_id)
    if case_id:
        scope = scope.filter(pk=case_id)
    counts = dict(enrolled=0, unmatched=0, escalations=0, queued=0, communication_failures=0)
    for case in scope.order_by("pk").iterator(chunk_size=200):
        existing = ServiceSla.objects.filter(service_case=case).first()
        row = existing or enroll(actor=actor, service_case=case)
        if row is None:
            if case.status not in ("CANCELLED", "READY_FOR_DELIVERY", "DELIVERED", "CLOSED"):
                counts["unmatched"] += 1
            continue
        counts["enrolled"] += int(existing is None)
        event, created = record_escalation(actor=actor, sla=row, origin=origin)
        counts["escalations"] += int(created)
        state = queries.describe(queries.annotated(ServiceSla.objects.filter(pk=row.pk)).get())["state"]
        counts[state] = counts.get(state, 0) + 1
        # Retry unqueued historical events as well as today's newly observed one.
        for pending in row.escalations.filter(notification_template__isnull=False).exclude(communication_attempts__notification__isnull=False):
            result = queue_escalation(actor=actor, escalation=pending)
            if result:
                counts["queued" if result.notification_id else "communication_failures"] += 1
    return counts
