import os
import datetime
import csv
import io
from datetime import timedelta
import uuid
import json
from django.conf import settings
from django.db import models, transaction
from django.core.mail import send_mail
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User
from django.views.decorators.http import require_POST
from django.http import JsonResponse, HttpResponse
from django.core.paginator import Paginator
from django.core.cache import cache
from django.db.models import Sum, Count
from django.db.models.functions import TruncWeek, TruncDate
from django.shortcuts import render, get_object_or_404, redirect
from .forms import ServiceForm, BlogPostForm, CareerPositionForm
from .models import Service, BlogPost, CareerPosition
from django.utils import timezone
def _apply_preset_filters(request, section: str, params: dict):
    """Apply quick date presets (today / this_week / this_month / last_7 / last_30) for staff list pages.

    This project uses namespaced staff pages and passes filters via querystring.
    Templates set `preset` via JS (data-preset). This helper converts that preset into
    `from` and `to` (YYYY-MM-DD) string values compatible with `__date__gte/lte` filters.
    """
    preset = (request.GET.get("preset") or "").strip()
    if not preset:
        return "", params

    today = timezone.localdate()

    def fmt(d):
        return d.strftime("%Y-%m-%d")

    if preset == "today":
        params["from"] = fmt(today)
        params["to"] = fmt(today)
    elif preset in {"this_week", "week"}:
        # Week starts on Monday (0)
        start = today - datetime.timedelta(days=today.weekday())
        params["from"] = fmt(start)
        params["to"] = fmt(today)
    elif preset in {"this_month", "month"}:
        start = today.replace(day=1)
        params["from"] = fmt(start)
        params["to"] = fmt(today)
    elif preset in {"last_7", "last7"}:
        start = today - datetime.timedelta(days=6)
        params["from"] = fmt(start)
        params["to"] = fmt(today)
    elif preset in {"last_30", "last30"}:
        start = today - datetime.timedelta(days=29)
        params["from"] = fmt(start)
        params["to"] = fmt(today)
    else:
        # Unknown preset: do nothing but return it for UI
        return preset, params

    return preset, params
from django.utils.text import slugify
from django.views.generic import ListView, DetailView
from django.views.decorators.csrf import csrf_exempt
from django.contrib.admin.views.decorators import staff_member_required as django_staff_member_required
from functools import wraps
from django.contrib.auth import authenticate, login
from django.utils.http import url_has_allowed_host_and_scheme

def staff_member_required(view_func):
    return django_staff_member_required(view_func, login_url="website:staff_login")

def staff_access_required(view_func):
    @wraps(view_func)
    def wrapper(request, *args, **kwargs):
        if not request.user.is_authenticated or not request.user.is_active or not request.user.is_staff:
            from django.urls import reverse
            from urllib.parse import urlencode
            return redirect(reverse("website:staff_login") + "?" + urlencode({"next": request.get_full_path()}))
        return view_func(request, *args, **kwargs)
    return wrapper

def staff_login(request):
    from django.contrib.auth.forms import AuthenticationForm
    from django.urls import reverse
    next_url = request.POST.get("next") or request.GET.get("next") or reverse("website:staff_dashboard")
    if not url_has_allowed_host_and_scheme(next_url, allowed_hosts={request.get_host()}, require_https=request.is_secure()):
        next_url = reverse("website:staff_dashboard")
    if request.user.is_authenticated and request.user.is_active and request.user.is_staff:
        return redirect(next_url)
    form = AuthenticationForm(request, data=request.POST or None)
    if request.method == "POST" and form.is_valid():
        user = form.get_user()
        if user.is_active and user.is_staff:
            login(request, user)
            return redirect(next_url)
        form.add_error(None, "This account does not have staff portal access.")
    return render(request, "website/staff/login.html", {"form": form, "next": next_url, "page_title": "Staff Sign In"})

from .models import (
    Service, Testimonial, BlogPost, CareerPosition, Enquiry, CareerApplication,
    CareRequest, CaregiverProfile, CareAssignment,
    SupportTicket, SupportMessage, StaffNotification,
    TelehealthBooking, TelehealthProvider, TelehealthAvailabilitySlot,
    PaymentTransaction, Patient, EMRNote, ActivityLog,
)
from .whatsapp import send_whatsapp_text, route_number
from .paystack import paystack_enabled, initialize_transaction, verify_transaction
from .forms import (
    EnquiryForm, CareerApplicationForm,
    CareRequestStep1Form, CareRequestStep2Form, CareRequestStep3Form,
    CaregiverAvailabilityForm, AssignmentCreateForm, SupportTicketForm, TelehealthBookingForm, TelehealthSlotBookingForm, EMRNoteForm,
    TelehealthBookingStaffForm,
    TelehealthAvailabilitySlotForm,
    CareRequestStaffForm,
    CareerApplicationStaffForm,
    PaymentTransactionStaffForm,
    StaffUserForm,
    StaffUserCreateForm,
)
def notify_admin(subject: str, message: str) -> None:
    """Email notification to ADMIN_NOTIFY_EMAIL (best-effort)."""
    try:
        send_mail(
            subject=subject,
            message=message,
            from_email=getattr(settings, "DEFAULT_FROM_EMAIL", None),
            recipient_list=[getattr(settings, "ADMIN_NOTIFY_EMAIL", "holisticcaregiverservicesltd@gmail.com")],
            fail_silently=True,
        )
    except Exception:
        pass
def log_activity(action: str, summary: str, **meta):
    try:
        ActivityLog.objects.create(action=action, summary=summary[:240], meta=meta or {})
    except Exception:
        pass
def get_or_create_patient(full_name: str, phone: str, email: str = "") -> Patient:
    phone_key = (phone or "").strip()
    patient = None
    if phone_key:
        patient = Patient.objects.filter(phone=phone_key).first()
    if not patient and email:
        patient = Patient.objects.filter(email=email).first()
    if not patient:
        patient = Patient.objects.create(full_name=full_name[:140], phone=phone_key, email=email or "")
    else:
        # Keep latest name/email
        updates = {}
        if full_name and patient.full_name != full_name:
            updates["full_name"] = full_name[:140]
        if email and patient.email != email:
            updates["email"] = email
        if updates:
            for k,vv in updates.items():
                setattr(patient,k,vv)
            patient.save(update_fields=list(updates.keys()))
    return patient
def home(request):
    services = Service.objects.filter(is_featured=True)[:6]
    testimonials = Testimonial.objects.filter(is_published=True)[:6]
    return render(request, "website/pages/home.html", {
        "services": services,
        "testimonials": testimonials,
        "page_title": "Professional Telehealth and Home Care Services You Can Trust",
        "page_description": "Holistica Care & Telehealth Services provides integrated caregiving and virtual healthcare support that bridges online consultation with compassionate, coordinated care delivered beyond traditional hospital walls.",
    })
def about(request):
    return render(request, "website/pages/about.html", {"page_title": "About Us"})
def services(request):
    all_services = Service.objects.all()
    return render(request, "website/pages/services.html", {"services": all_services, "page_title": "Our Services"})
def care_process(request):
    return render(request, "website/pages/care_process.html", {"page_title": "Our Care Process"})
def why_choose_us(request):
    return render(request, "website/pages/why_choose_us.html", {"page_title": "Why Choose Us"})
def testimonials(request):
    items = Testimonial.objects.filter(is_published=True)
    return render(request, "website/pages/testimonials.html", {"testimonials": items, "page_title": "Testimonials"})
def careers(request):
    positions = CareerPosition.objects.filter(is_open=True)
    form = CareerApplicationForm()
    if request.method == "POST":
        form = CareerApplicationForm(request.POST, request.FILES)
        if form.is_valid():
            app = form.save()
            log_activity("career_application", f"New application: {app.full_name}", application_id=app.id, phone=app.phone, email=app.email)
            notify_admin("New Career Application", f"New application from {app.full_name} ({app.email}).\nPhone: {app.phone}")
            send_mail(
                subject="New Career Application",
                message=f"New application from {app.full_name} ({app.email}).",
                from_email=settings.DEFAULT_FROM_EMAIL,
                recipient_list=[getattr(settings, "ADMIN_NOTIFY_EMAIL", "admin@example.com")],
                fail_silently=True,
            )
            return redirect("website:careers_success")
    return render(request, "website/pages/careers.html", {
        "positions": positions,
        "form": form,
        "page_title": "Careers",
    })
def careers_success(request):
    return render(request, "website/pages/careers_success.html", {"page_title": "Application Received"})
def contact(request):
    form = EnquiryForm()
    if request.method == "POST":
        form = EnquiryForm(request.POST)
        if form.is_valid():
            enquiry = form.save()
            log_activity("enquiry", f"New enquiry: {enquiry.full_name}", enquiry_id=enquiry.id, phone=enquiry.phone, email=enquiry.email)
            notify_admin(f"New Enquiry: {enquiry.get_enquiry_type_display()}", f"From: {enquiry.full_name}\nPhone: {enquiry.phone}\nEmail: {enquiry.email}\n\n{enquiry.message}")
            send_mail(
                subject=f"New Enquiry: {enquiry.get_enquiry_type_display()}",
                message=f"From: {enquiry.full_name}\nPhone: {enquiry.phone}\nEmail: {enquiry.email}\nLocation: {enquiry.location}\nPreferred time: {enquiry.preferred_contact_time}\n\n{enquiry.message}",
                from_email=settings.DEFAULT_FROM_EMAIL,
                recipient_list=[getattr(settings, "ADMIN_NOTIFY_EMAIL", "admin@example.com")],
                fail_silently=True,
            )
            wa_to = route_number(enquiry.enquiry_type)
            send_whatsapp_text(wa_to, f"New Enquiry ({enquiry.get_enquiry_type_display()}):\n{enquiry.full_name} | {enquiry.phone}\nLocation: {enquiry.location}\n\n{enquiry.message[:700]}")
            return redirect("website:contact_success")
    return render(request, "website/pages/contact.html", {
        "form": form,
        "page_title": "Contact Us",
    })
def contact_success(request):
    return render(request, "website/pages/contact_success.html", {"page_title": "Thank You"})
def telehealth_slots_api(request):
    from .slot_automation import ensure_consultation_slots
    """Return available consultation slots as JSON for the booking calendar."""
    ensure_consultation_slots(days=30)
    provider_id = request.GET.get("provider")
    date_from = request.GET.get("from")
    date_to = request.GET.get("to")
    qs = TelehealthAvailabilitySlot.objects.filter(is_active=True, start_at__gt=timezone.now(), provider__is_active=True, provider__is_verified=True).select_related("provider")
    if provider_id and str(provider_id).isdigit():
        qs = qs.filter(provider_id=int(provider_id))
    # Reject malformed calendar dates rather than raising a database validation error.
    for raw_date in (date_from, date_to):
        if raw_date:
            try:
                datetime.date.fromisoformat(raw_date)
            except (ValueError, TypeError):
                return JsonResponse({"slots": [], "error": "Invalid date. Use YYYY-MM-DD."}, status=400)
    if date_from:
        qs = qs.filter(start_at__date__gte=date_from)
    if date_to:
        qs = qs.filter(start_at__date__lte=date_to)
    # only open slots
    qs = [s for s in qs if not s.is_full and s.provider.is_active and s.provider.is_verified]
    data = []
    for s in qs:
        data.append({
            "id": s.id,
            "provider": s.provider.full_name,
            "provider_id": s.provider_id,
            "role": s.provider.role,
            "specialization": s.provider.specialization,
            "start_at": s.start_at.isoformat(),
            "end_at": s.end_at.isoformat(),
        })
    return JsonResponse({"slots": data})
def telehealth_booking(request):
    from .slot_automation import ensure_consultation_slots
    ensure_consultation_slots(days=30)
    # Booking with staff-approved provider availability
    providers = TelehealthProvider.objects.filter(is_active=True, is_verified=True).order_by("sort_order", "full_name")
    form = TelehealthSlotBookingForm()
    if request.method == "POST":
        form = TelehealthSlotBookingForm(request.POST)
        if form.is_valid():
            slot_id = form.cleaned_data["slot_id"]
            # Lock the slot while reserving capacity to prevent simultaneous overbooking.
            with transaction.atomic():
                slot = TelehealthAvailabilitySlot.objects.select_for_update().select_related("provider").filter(pk=slot_id, is_active=True).first()
                if not slot or not slot.provider.is_active or not slot.provider.is_verified or slot.start_at <= timezone.now() or slot.booked_count >= slot.capacity:
                    form.add_error(None, "This appointment is no longer available. Please select another time.")
                    booking = None
                else:
                    booking = TelehealthBooking.objects.create(
                        full_name=form.cleaned_data["full_name"],
                        phone=form.cleaned_data["phone"],
                        email=form.cleaned_data.get("email", ""),
                        consultation_mode=form.cleaned_data["consultation_mode"],
                        symptoms_or_reason=form.cleaned_data.get("symptoms_or_reason", ""),
                        slot=slot,
                        preferred_date=timezone.localtime(slot.start_at).date(),
                        preferred_time=f"{timezone.localtime(slot.start_at):%I:%M %p} – {timezone.localtime(slot.end_at):%I:%M %p}",
                    )
                    patient = get_or_create_patient(booking.full_name, booking.phone, booking.email)
                    booking.patient = patient
                    booking.fee_ngn = int(getattr(settings, "PAYSTACK_TELEHEALTH_FEE_NGN", 0) or 0)
                    booking.save(update_fields=["patient", "fee_ngn"])
                    TelehealthAvailabilitySlot.objects.filter(pk=slot.pk).update(booked_count=models.F("booked_count") + 1)
            if booking is not None:
                log_activity("telehealth_booking", f"Telehealth booking #{booking.id} received", booking_id=booking.id, slot_id=slot.id)
                # Email notify (optional)
                send_mail(
                    subject=f"New Telehealth Booking: {booking.get_consultation_mode_display()}",
                    message=(
                        f"Name: {booking.full_name}\n"
                        f"Phone: {booking.phone}\n"
                        f"Slot: {slot.start_at:%Y-%m-%d %I:%M %p} – {slot.end_at:%I:%M %p}\n"
                        f"Provider: {slot.provider.full_name}\n\n"
                        f"Reason: {booking.symptoms_or_reason}"
                    ),
                    from_email=settings.DEFAULT_FROM_EMAIL,
                    recipient_list=[getattr(settings, "ADMIN_NOTIFY_EMAIL", "admin@example.com")],
                    fail_silently=True,
                )
                # WhatsApp notify (optional)
                wa_to = route_number("telehealth")
                send_whatsapp_text(
                    wa_to,
                    f"New Telehealth Booking\n"
                    f"{booking.full_name} | {booking.phone}\n"
                    f"Slot: {slot.start_at:%Y-%m-%d %I:%M %p} – {slot.end_at:%I:%M %p}\n"
                    f"Provider: {slot.provider.full_name}\n"
                    f"Mode: {booking.get_consultation_mode_display()}\n"
                    f"Reason: {booking.symptoms_or_reason[:500]}",
                )
                # If Paystack fee is configured, take payment before success.
                if paystack_enabled() and booking.fee_ngn > 0:
                    return redirect("website:paystack_start_telehealth", booking_id=booking.id)
                return redirect("website:telehealth_booking_success")
    return render(request, "website/pages/telehealth_booking.html", {
        "form": form,
        "providers": providers,
        "page_title": "Book a Telehealth Consultation",
        "page_description": "Pick an available slot and book a virtual consultation with our licensed clinical team.",
    })
def telehealth_booking_success(request):
    return render(request, "website/pages/telehealth_booking_success.html", {"page_title": "Booking Submitted"})
# -----------------------------------------------------------------------------
# PAYSTACK (Telehealth)
# -----------------------------------------------------------------------------
def paystack_start_telehealth(request, booking_id: int):
    booking = get_object_or_404(TelehealthBooking, pk=booking_id)
    if not paystack_enabled() or booking.fee_ngn <= 0:
        return redirect("website:telehealth_booking_success")
    # If already paid, skip
    if booking.is_paid:
        return redirect("website:telehealth_booking_success")
    callback_url = request.build_absolute_uri(
        # Paystack will redirect here with ?reference=...
        "/paystack/callback/"
    )
    reference = f"TEL_{uuid.uuid4().hex[:24]}"
    # Create local transaction
    tx = PaymentTransaction.objects.create(
        purpose="telehealth",
        amount_ngn=booking.fee_ngn,
        email=booking.email or getattr(settings, "ADMIN_NOTIFY_EMAIL", ""),
        reference=reference,
        telehealth_booking=booking,
    )
    try:
        resp = initialize_transaction(
            email=tx.email,
            amount_ngn=tx.amount_ngn,
            callback_url=callback_url,
            metadata={"purpose": "telehealth", "booking_id": booking.id, "reference": reference},
        )
        tx.raw_response = resp
        tx.save(update_fields=["raw_response"])
        if resp.get("status") is True and resp.get("data", {}).get("authorization_url"):
            return redirect(resp["data"]["authorization_url"])
    except Exception as e:
        tx.status = "failed"
        tx.raw_response = {"error": str(e)}
        tx.save(update_fields=["status", "raw_response"])
    notify_admin(
        "Paystack init failed (Telehealth)",
        f"Booking #{booking.id} for {booking.full_name}. Please check Paystack keys / connectivity.",
    )
    return redirect("website:telehealth_booking_success")
def paystack_callback(request):
    reference = request.GET.get("reference") or ""
    if not reference:
        return redirect("website:home")
    tx = PaymentTransaction.objects.filter(reference=reference).select_related("telehealth_booking").first()
    if not tx:
        return redirect("website:home")
    try:
        resp = verify_transaction(reference)
        tx.raw_response = resp
        ok = resp.get("status") is True and resp.get("data", {}).get("status") == "success"
        if ok:
            tx.status = "success"
            tx.paid_at = timezone.now()
            tx.save(update_fields=["status", "paid_at", "raw_response"])
            booking = tx.telehealth_booking
            if booking:
                booking.is_paid = True
                booking.paystack_reference = reference
                booking.save(update_fields=["is_paid", "paystack_reference"])
                log_activity("payment_success", f"Telehealth payment success for {booking.full_name}", reference=reference, amount=tx.amount_ngn)
                notify_admin(
                    "Payment received (Telehealth)",
                    f"Payment SUCCESS for {booking.full_name} | {booking.phone}\n"
                    f"Amount: ₦{tx.amount_ngn}\nReference: {reference}",
                )
        else:
            tx.status = "failed"
            tx.save(update_fields=["status", "raw_response"])
    except Exception as e:
        tx.status = "failed"
        tx.raw_response = {"error": str(e)}
        tx.save(update_fields=["status", "raw_response"])
    return redirect("website:telehealth_booking_success")
class BlogListView(ListView):
    template_name = "website/pages/blog_list.html"
    context_object_name = "posts"
    paginate_by = 9
    def get_queryset(self):
        return BlogPost.objects.filter(is_published=True)
class BlogDetailView(DetailView):
    template_name = "website/pages/blog_detail.html"
    context_object_name = "post"
    def get_object(self):
        return get_object_or_404(BlogPost, slug=self.kwargs["slug"], is_published=True)
def offline(request):
    return render(request, 'website/pages/offline.html', {'page_title': 'Offline'})
def _auto_assign_staff():
    """Round-robin assignment among staff users (excluding superusers optional)."""
    from django.contrib.auth import get_user_model
    User = get_user_model()
    staff = list(User.objects.filter(is_staff=True, is_active=True).order_by("id"))
    if not staff:
        return None
    # pick the staff with least open tickets
    from .models import SupportTicket
    best = None
    best_count = None
    for u in staff:
        c = SupportTicket.objects.filter(status__in=["open","pending"], assigned_to=u).count()
        if best is None or c < best_count:
            best, best_count = u, c
    return best
def _make_staff_notifications(title, body, url=""):
    from django.contrib.auth import get_user_model
    User = get_user_model()
    staff = User.objects.filter(is_staff=True, is_active=True)
    for u in staff:
        StaffNotification.objects.create(user=u, title=title[:140], body=body or "", url=url or "")
def _set_ticket_sla(ticket):
    try:
        mins = getattr(settings, "SUPPORT_SLA_MINUTES", 15)
    except Exception:
        mins = 15
    ticket.sla_minutes = mins
    ticket.due_at = ticket.created_at + timedelta(minutes=mins)
# -----------------------------------------------------------------------------
# Care Request Workflow (Step-by-step intake)
# -----------------------------------------------------------------------------
def _wizard_get(request):
    return request.session.get("care_wizard", {})
def _wizard_set(request, data):
    request.session["care_wizard"] = data
    request.session.modified = True
def care_request_step1(request):
    data = _wizard_get(request)
    form = CareRequestStep1Form(initial=data.get("step1"))
    if request.method == "POST":
        form = CareRequestStep1Form(request.POST)
        if form.is_valid():
            data["step1"] = form.cleaned_data
            _wizard_set(request, data)
            return redirect("website:care_request_step2")
    return render(request, "website/pages/care_request_step1.html", {"form": form, "page_title": "Request Care (Step 1/4)"})
def care_request_step2(request):
    data = _wizard_get(request)
    if not data.get("step1"):
        return redirect("website:care_request_step1")
    form = CareRequestStep2Form(initial=data.get("step2"))
    if request.method == "POST":
        form = CareRequestStep2Form(request.POST)
        if form.is_valid():
            data["step2"] = form.cleaned_data
            _wizard_set(request, data)
            return redirect("website:care_request_step3")
    return render(request, "website/pages/care_request_step2.html", {"form": form, "page_title": "Request Care (Step 2/4)"})
def care_request_step3(request):
    data = _wizard_get(request)
    if not data.get("step1") or not data.get("step2"):
        return redirect("website:care_request_step1")
    form = CareRequestStep3Form(initial=data.get("step3"))
    if request.method == "POST":
        form = CareRequestStep3Form(request.POST)
        if form.is_valid():
            # Django's default JSON session serializer cannot store date objects.
            # Store ISO text; DateField/DateInput can parse it on the return visit.
            step3 = form.cleaned_data.copy()
            if step3.get("preferred_start_date"):
                step3["preferred_start_date"] = step3["preferred_start_date"].isoformat()
            data["step3"] = step3
            _wizard_set(request, data)
            return redirect("website:care_request_review")
    return render(request, "website/pages/care_request_step3.html", {"form": form, "page_title": "Request Care (Step 3/4)"})
def care_request_review(request):
    data = _wizard_get(request)
    if not data.get("step1") or not data.get("step2"):
        return redirect("website:care_request_step1")
    if not data.get("step3"):
        return redirect("website:care_request_step3")
    if request.method == "POST":
        s1 = data.get("step1", {})
        s2 = data.get("step2", {})
        s3 = data.get("step3", {})
        cr = CareRequest.objects.create(
            full_name=s1.get("full_name",""),
            phone=s1.get("phone",""),
            email=s1.get("email","") or "",
            location=s1.get("location","") or "",
            request_type=s2.get("request_type","home_care"),
            patient_age=s2.get("patient_age"),
            care_needs=s2.get("care_needs",""),
            schedule_notes=s2.get("schedule_notes","") or "",
            preferred_start_date=s3.get("preferred_start_date"),
            preference_notes=s3.get("preference_notes","") or "",
            risk_notes=s3.get("risk_notes","") or "",
            status="new",
        )
        # Link to patient (EMR)
        patient = get_or_create_patient(cr.full_name, cr.phone, cr.email or "")
        log_activity("care_request", f"New care request: {cr.full_name}", care_request_id=cr.id, phone=cr.phone)
        notify_admin("New Care Request", f"Name: {cr.full_name}\nPhone: {cr.phone}\nLocation: {cr.location}\nType: {cr.get_request_type_display()}\n\nNeeds: {cr.care_needs}")
        # Email notify
        send_mail(
            subject=f"New Care Request: {cr.get_request_type_display()}",
            message=f"Name: {cr.full_name}\nPhone: {cr.phone}\nLocation: {cr.location}\n\nNeeds: {cr.care_needs}\nSchedule: {cr.schedule_notes}\nStart: {cr.preferred_start_date}\n\nPreferences: {cr.preference_notes}\nRisks: {cr.risk_notes}",
            from_email=settings.DEFAULT_FROM_EMAIL,
            recipient_list=[getattr(settings, "ADMIN_NOTIFY_EMAIL", "admin@example.com")],
            fail_silently=True,
        )
        # WhatsApp routing (optional)
        wa_to = route_number(cr.request_type)
        send_whatsapp_text(wa_to, f"New Care Request ({cr.get_request_type_display()}):\n{cr.full_name} | {cr.phone}\nLocation: {cr.location}\nNeeds: {cr.care_needs[:500]}")
        # Clear wizard
        request.session.pop("care_wizard", None)
        return redirect("website:care_request_success")
    return render(request, "website/pages/care_request_review.html", {
        "data": data,
        "page_title": "Request Care (Review & Submit)",
    })
def care_request_success(request):
    return render(request, "website/pages/care_request_success.html", {"page_title": "Request Submitted"})
# -----------------------------------------------------------------------------
# WhatsApp Business API Webhook (Cloud API)
# -----------------------------------------------------------------------------
def whatsapp_webhook(request):
    # Verification (GET)
    if request.method == "GET":
        mode = request.GET.get("hub.mode")
        token = request.GET.get("hub.verify_token")
        challenge = request.GET.get("hub.challenge")
        if mode == "subscribe" and token == getattr(settings, "WHATSAPP_VERIFY_TOKEN", ""):
            return HttpResponse(challenge or "", content_type="text/plain")
        return HttpResponse("Verification failed", status=403)
    # Incoming messages (POST)
    if request.method == "POST":
        try:
            payload = json.loads(request.body.decode("utf-8") or "{}")
        except Exception:
            payload = {}
        # Minimal acknowledgement; store/route can be expanded
        return JsonResponse({"status": "ok"})
    return JsonResponse({"detail": "Method not allowed"}, status=405)
# -----------------------------------------------------------------------------
# Caregiver Dashboard (Availability + Assignments)
# -----------------------------------------------------------------------------
def _is_staff(user):
    return user.is_authenticated and user.is_staff
@login_required
def caregiver_portal(request):
    # Caregivers (non-staff) can view their own assignments if they have a profile
    profile, _ = CaregiverProfile.objects.get_or_create(user=request.user)
    assignments = profile.assignments.select_related("care_request").all()[:50]
    form = CaregiverAvailabilityForm(instance=profile)
    if request.method == "POST":
        form = CaregiverAvailabilityForm(request.POST, instance=profile)
        if form.is_valid():
            form.save()
            return redirect("website:caregiver_portal")
    return render(request, "website/pages/caregiver_portal.html", {
        "profile": profile,
        "assignments": assignments,
        "form": form,
        "page_title": "Caregiver Portal",
    })
@staff_access_required
def staff_care_dashboard(request):
    # Staff dashboard for intake + matching
    care_requests = CareRequest.objects.all().order_by("-created_at")[:50]
    caregivers = CaregiverProfile.objects.filter(is_active=True).select_related("user").order_by("user__first_name")[:100]
    assignments = CareAssignment.objects.select_related("care_request", "caregiver", "caregiver__user").all()[:50]
    return render(request, "website/pages/staff_care_dashboard.html", {
        "care_requests": care_requests,
        "caregivers": caregivers,
        "assignments": assignments,
        "page_title": "Staff Care Dashboard",
    })
@staff_access_required
def staff_assignment_create(request):
    form = AssignmentCreateForm()
    if request.method == "POST":
        form = AssignmentCreateForm(request.POST)
        if form.is_valid():
            assign = form.save()
            # Update care request status
            cr = assign.care_request
            if cr.status in ("new", "assessment"):
                cr.status = "matched"
                cr.save(update_fields=["status"])
            # Optional WhatsApp notify (internal)
            wa_to = getattr(settings, "WHATSAPP_DEFAULT_ROUTE", "")
            send_whatsapp_text(wa_to, f"Care Assignment created: {assign.caregiver} assigned to {assign.care_request.full_name} ({assign.care_request.get_request_type_display()}).")
            return redirect("website:staff_care_dashboard")
    return render(request, "website/pages/staff_assignment_create.html", {"form": form, "page_title": "Create Assignment"})
def legal_privacy(request):
    return render(request, "website/pages/legal_privacy.html", {"page_title": "Privacy Policy"})
def legal_terms(request):
    return render(request, "website/pages/legal_terms.html", {"page_title": "Terms & Conditions"})
def legal_disclaimer(request):
    return render(request, "website/pages/legal_disclaimer.html", {"page_title": "Disclaimer"})
# -----------------------------------------------------------------------------
# Support Tickets
# -----------------------------------------------------------------------------
def support_ticket_create(request):
    """Create a support ticket from the chat widget or full page."""
    from .forms import SupportTicketForm
    from .models import SupportTicket, SupportMessage
    if request.method == "POST":
        form = SupportTicketForm(request.POST)
        if form.is_valid():
            data = form.cleaned_data
            public_id = SupportTicket.new_id()
            secret = str(uuid.uuid4())
            ticket = SupportTicket.objects.create(
                public_id=public_id,
                secret=secret,
                full_name=data.get("full_name",""),
                email=data.get("email","") or "",
                phone=data.get("phone","") or "",
                subject=(data.get("subject") or "Support Request")[:180],
                status="open",
            )
            # Auto-assign staff + set SLA
            ticket.assigned_to = _auto_assign_staff()
            _set_ticket_sla(ticket)
            ticket.save()
            SupportMessage.objects.create(
                ticket=ticket,
                sender="client",
                message=data.get("message","").strip(),
            )
            ticket.last_client_message_at = ticket.created_at
            ticket.save(update_fields=["last_client_message_at"])
            # Notify staff (in-app) + WhatsApp
            _make_staff_notifications(
                title=f"New support ticket #{ticket.public_id}",
                body=f"{ticket.full_name} • {ticket.subject}",
                url=f"/staff/support/{ticket.public_id}/",
            )
            # Optional: route to WhatsApp too (keep it separate from floating button)
            try:
                wa_to = route_number("home_care")
                send_whatsapp_text(wa_to, f"New Support Ticket #{ticket.public_id}\n{ticket.full_name} | {ticket.phone}\n{ticket.subject}\n\n{data.get('message','')[:700]}")
            except Exception:
                pass
            # Store last ticket in session for widget preview
            try:
                request.session["last_support_ticket"] = {"public_id": ticket.public_id, "secret": ticket.secret}
            except Exception:
                pass
            # If submitted from the widget, respond with JSON for instant success UI
            is_ajax = request.headers.get("x-requested-with") == "XMLHttpRequest" or request.POST.get("from_widget") == "1"
            if is_ajax:
                return JsonResponse({
                    "ok": True,
                    "ticket_public_id": ticket.public_id,
                    "ticket_url": f"/support/t/{ticket.public_id}/{ticket.secret}/",
                })
            return redirect("website:support_ticket_thanks", public_id=ticket.public_id, secret=ticket.secret)
    else:
        form = SupportTicketForm()
    return render(request, "website/pages/support_ticket_new.html", {"form": form, "page_title": "Support"})
def support_panel_preview(request):
    """Secure preview:
    - Preferred: phone + ticket public_id must match a real ticket (prevents random phone lookup).
    - Fallback: session last_support_ticket (same-device convenience).
    """
    from .models import SupportTicket, SupportMessage
    phone = (request.GET.get("phone") or "").strip()
    ticket_id = (request.GET.get("ticket") or "").strip()
    ticket = None
    # Secure path: require BOTH phone and ticket id
    if phone and ticket_id:
        ticket = SupportTicket.objects.filter(public_id=ticket_id, phone=phone).first()
    # Fallback: same-device session
    if not ticket:
        data = request.session.get("last_support_ticket") or {}
        public_id = (data.get("public_id") or "").strip()
        secret = (data.get("secret") or "").strip()
        if public_id and secret:
            ticket = SupportTicket.objects.filter(public_id=public_id, secret=secret).first()
    if not ticket:
        return JsonResponse({"ok": False, "messages": []})
    qs = SupportMessage.objects.filter(ticket=ticket).order_by("-created_at")[:3]
    msgs = []
    for m in reversed(list(qs)):
        msgs.append({
            "sender": m.sender,
            "message": m.message,
            "created_at": m.created_at.isoformat() if getattr(m, "created_at", None) else "",
        })
    return JsonResponse({
        "ok": True,
        "ticket_public_id": ticket.public_id,
        "ticket_url": f"/support/t/{ticket.public_id}/{ticket.secret}/",
        "messages": msgs,
    })
    qs = SupportMessage.objects.filter(ticket=ticket).order_by("-created_at")[:3]
    msgs = []
    for m in reversed(list(qs)):
        msgs.append({
            "sender": m.sender,
            "message": m.message,
            "created_at": m.created_at.isoformat() if getattr(m, "created_at", None) else "",
        })
    return JsonResponse({
        "ok": True,
        "ticket_public_id": ticket.public_id,
        "ticket_url": f"/support/t/{ticket.public_id}/{ticket.secret}/",
        "messages": msgs,
    })
def support_ticket_thanks(request, public_id, secret):
    ticket = SupportTicket.objects.filter(public_id=public_id, secret=secret).first()
    if not ticket:
        return redirect("website:home")
    return render(request, "website/pages/support_ticket_thanks.html", {"ticket": ticket, "page_title":"Ticket Created"})
def support_ticket_view(request, public_id, secret):
    ticket = SupportTicket.objects.filter(public_id=public_id, secret=secret).first()
    if not ticket:
        return redirect("website:home")
    if request.method == "POST":
        msg = (request.POST.get("message") or "").strip()
        if msg:
            SupportMessage.objects.create(ticket=ticket, sender="client", message=msg)
            ticket.last_client_message_at = timezone.now()
            ticket.save(update_fields=["last_client_message_at"])
            _make_staff_notifications(
                title=f"New reply on ticket #{ticket.public_id}",
                body=(msg[:180] + ("…" if len(msg)>180 else "")),
                url=f"/staff/support/{ticket.public_id}/",
            )
            try:
                wa_to = getattr(settings, 'WHATSAPP_SUPPORT_ROUTE', '') or route_number('home_care')
                send_whatsapp_text(
                    wa_to,
                    (
                        f"New client reply • Ticket #{ticket.public_id}\n"
                        f"{ticket.full_name} | {ticket.phone}\n\n"
                        f"{msg[:700]}"
                    ),
                )
            except Exception:
                pass
            return redirect("website:support_ticket_view", public_id=ticket.public_id, secret=ticket.secret)
    return render(request, "website/pages/support_ticket_view.html", {"ticket": ticket, "page_title": f"Ticket {ticket.public_id}"})
def _paginate(request, qs, per_page=25):
    page_number = request.GET.get("page") or 1
    paginator = Paginator(qs, per_page)
    return paginator.get_page(page_number)
def _apply_sorting(request, qs, allowed_map, default="-created_at"):
    """Return (qs, sort, direction)"""
    sort = request.GET.get("sort","").strip()
    direction = request.GET.get("dir","desc").strip().lower()
    if sort not in allowed_map:
        # default may be like "-created_at"
        base = default.lstrip("-")
        sort = base if base in allowed_map else next(iter(allowed_map.keys()))
        direction = "desc" if default.startswith("-") else "asc"
    field = allowed_map.get(sort)
    if not field:
        return qs, sort, direction
    order = f"-{field}" if direction == "desc" else field
    return qs.order_by(order), sort, direction
def _export_csv(title, columns, rows):
    resp = HttpResponse(content_type="text/csv")
    resp["Content-Disposition"] = f'attachment; filename="{slugify(title)}.csv"'
    out = io.StringIO()
    w = csv.writer(out)
    w.writerow(columns)
    for r in rows:
        w.writerow(r)
    resp.write(out.getvalue())
    return resp
def _export_pdf(title, columns, rows):
    """
    Server-side PDF export with branding (logo) + signature footer.
    Uses ReportLab (already in requirements).
    """
    from reportlab.lib.pagesizes import letter
    from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
    from reportlab.lib import colors
    from reportlab.lib.styles import getSampleStyleSheet
    from reportlab.lib.units import inch
    buf = io.BytesIO()
    # Slightly larger top/bottom margins to fit branded header/footer
    doc = SimpleDocTemplate(
        buf,
        pagesize=letter,
        leftMargin=24,
        rightMargin=24,
        topMargin=56,
        bottomMargin=56,
        title=title,
        author=getattr(settings, "SITE_NAME", "") if "settings" in globals() else ""
    )
    styles = getSampleStyleSheet()
    def _brand_canvas(canv, doc_):
        """Draw logo + header title and a signature footer on every page."""
        canv.saveState()
        # Logo (try static path first, then collected staticfiles)
        logo_candidates = []
        try:
            base_dir = getattr(settings, "BASE_DIR", None)
            if base_dir:
                logo_candidates += [
                    os.path.join(base_dir, "website", "static", "website", "img", "logo.png"),
                    os.path.join(base_dir, "website", "static", "website", "img", "logo.jpg"),
                    os.path.join(base_dir, "staticfiles", "website", "img", "logo.png"),
                    os.path.join(base_dir, "staticfiles", "website", "img", "logo.jpg"),
                ]
        except Exception:
            pass
        logo_path = next((p for p in logo_candidates if p and os.path.exists(p)), None)
        page_w, page_h = letter
        # Header band (logo + full letterhead)
        site_name = getattr(settings, "SITE_NAME", "Telehealthscare Services")
        site_phone = getattr(settings, "SITE_PHONE", "")
        site_email = getattr(settings, "SITE_EMAIL", "")
        site_address = getattr(settings, "SITE_ADDRESS", "")
        watermark = getattr(settings, "SITE_WATERMARK", getattr(settings, "SITE_SHORT", "Holistica care"))
        # Watermark (behind content)
        try:
            canv.saveState()
            canv.setFillAlpha(0.08)
            canv.setFont("Helvetica-Bold", 54)
            canv.setFillColor(colors.grey)
            canv.translate(page_w/2.0, page_h/2.0)
            canv.rotate(35)
            canv.drawCentredString(0, 0, watermark)
            canv.restoreState()
        except Exception:
            # Fallback when alpha is not available
            canv.saveState()
            canv.setFont("Helvetica-Bold", 54)
            canv.setFillColor(colors.lightgrey)
            canv.translate(page_w/2.0, page_h/2.0)
            canv.rotate(35)
            canv.drawCentredString(0, 0, watermark)
            canv.restoreState()
        # Logo
        logo_w = 34
        logo_h = 34
        left_x = 24
        top_y = page_h - 32
        if logo_path:
            try:
                canv.drawImage(logo_path, left_x, page_h - 60, width=logo_w, height=logo_h, preserveAspectRatio=True, mask='auto')
            except Exception:
                pass
        text_x = left_x + (logo_w + 10 if logo_path else 0)
        canv.setFont("Helvetica-Bold", 13)
        canv.drawString(text_x, top_y, site_name)
        canv.setFont("Helvetica", 9)
        canv.setFillColor(colors.grey)
        meta_parts = []
        if site_address:
            meta_parts.append(site_address)
        if site_phone:
            meta_parts.append(f"Tel: {site_phone}")
        if site_email:
            meta_parts.append(site_email)
        meta_line = " • ".join(meta_parts)
        if meta_line:
            canv.drawString(text_x, top_y - 14, meta_line)
        canv.setFillColor(colors.black)
        canv.setFont("Helvetica-Bold", 12)
        canv.drawString(text_x, top_y - 30, title)
        # Divider line
        canv.setStrokeColor(colors.lightgrey)
        canv.setLineWidth(1)
        canv.line(24, page_h - 70, page_w - 24, page_h - 70)
        # Footer: signature line + generated date
        canv.setStrokeColor(colors.lightgrey)
        canv.setLineWidth(1)
        canv.line(24, 56, page_w - 24, 56)
        canv.setFont("Helvetica", 9)
        canv.setFillColor(colors.grey)
        today = datetime.date.today().strftime("%Y-%m-%d")
        canv.drawString(24, 40, f"Generated: {today}")
        canv.drawRightString(page_w - 24, 40, "Authorized Signature: ____________________________")
        canv.setFillColor(colors.black)
        canv.restoreState()
    story = []
    story.append(Spacer(1, 6))
    # Table
    data = [columns] + rows
    table = Table(data, repeatRows=1)
    table.setStyle(TableStyle([
        ("BACKGROUND",(0,0),(-1,0),colors.lightgrey),
        ("GRID",(0,0),(-1,-1),0.25,colors.grey),
        ("FONTNAME",(0,0),(-1,0),"Helvetica-Bold"),
        ("FONTSIZE",(0,0),(-1,-1),8),
        ("VALIGN",(0,0),(-1,-1),"TOP"),
        ("ROWBACKGROUNDS",(0,1),(-1,-1),[colors.whitesmoke, colors.white]),
    ]))
    story.append(table)
    doc.build(story, onFirstPage=_brand_canvas, onLaterPages=_brand_canvas)
    pdf = buf.getvalue()
    buf.close()
    resp = HttpResponse(content_type="application/pdf")
    resp["Content-Disposition"] = f'attachment; filename="{slugify(title)}.pdf"'
    resp.write(pdf)
    return resp
@staff_member_required
def staff_support_dashboard(request):
    params = {
        "q": request.GET.get("q","").strip(),
        "status": request.GET.get("status","").strip(),
        "assigned": request.GET.get("assigned","").strip(),
        "from": request.GET.get("from","").strip(),
        "to": request.GET.get("to","").strip(),
    }
    preset, params = _apply_preset_filters(request, "support", params)
    q = params["q"]
    status = params["status"]
    assigned = params["assigned"]
    date_from = params["from"]
    date_to = params["to"]
    qs = SupportTicket.objects.all().select_related("assigned_to")
    if status in {"open","pending","closed"}:
        qs = qs.filter(status=status)
    if q:
        qs = qs.filter(Q(subject__icontains=q) | Q(email__icontains=q) | Q(full_name__icontains=q) | Q(public_id__icontains=q))
    if assigned:
        if assigned == "unassigned":
            qs = qs.filter(assigned_to__isnull=True)
        else:
            try:
                qs = qs.filter(assigned_to_id=int(assigned))
            except Exception:
                pass
    if date_from:
        try:
            qs = qs.filter(created_at__date__gte=date_from)
        except Exception:
            pass
    if date_to:
        try:
            qs = qs.filter(created_at__date__lte=date_to)
        except Exception:
            pass
    sort_map = {
        "id": "public_id",
        "client": "full_name",
        "status": "status",
        "assigned": "assigned_to__username",
        "created": "created_at",
    }
    qs, sort, direction = _apply_sorting(request, qs, sort_map, default="-created_at")
    export = request.GET.get("export","").strip().lower()
    if export in {"csv","pdf"}:
        rows = []
        for t in qs[:2000]:
            rows.append([
                t.public_id,
                t.full_name,
                t.subject,
                t.status,
                (t.assigned_to.get_full_name() or t.assigned_to.username) if t.assigned_to else "",
                t.created_at.strftime("%Y-%m-%d %H:%M"),
            ])
        title = "Support Tickets"
        cols = ["Ticket","Client","Subject","Status","Assigned to","Created"]
        return _export_csv(title, cols, rows) if export=="csv" else _export_pdf(title, cols, rows)
    staff_users = User.objects.filter(is_staff=True, is_active=True).order_by("first_name","username")
    page_obj = _paginate(request, qs, per_page=25)
    return render(request, "website/staff/support_dashboard.html", {
        "tickets": page_obj.object_list,
        "page_obj": page_obj,
        "status": status,
        "q": q,
        "assigned": assigned,
        "date_from": date_from,
        "date_to": date_to,
        "staff_users": staff_users,
        "preset": preset,
        "sort": sort,
        "dir": direction,
        "page_title": "Support Tickets"
    })
def staff_support_ticket(request, public_id):
    ticket = SupportTicket.objects.filter(public_id=public_id).first()
    if not ticket:
        return redirect("website:staff_support_dashboard")
    if request.method == "POST":
        action = request.POST.get("action")
        if action in {"open", "pending", "closed"}:
            ticket.status = action
            ticket.save()
        msg = (request.POST.get("staff_message") or "").strip()
        if msg:
            SupportMessage.objects.create(ticket=ticket, sender="staff", message=msg)
            now = timezone.now()
            if not ticket.first_response_at:
                ticket.first_response_at = now
            ticket.last_staff_message_at = now
            ticket.save(update_fields=["first_response_at", "last_staff_message_at"])
            # Notify client on WhatsApp (if phone provided)
            try:
                client_number = (ticket.phone or "").strip()
                if client_number:
                    base = getattr(settings, "SITE_BASE_URL", "") or ""
                    link = (
                        f"{base}/support/t/{ticket.public_id}/{ticket.secret}/"
                        if base else f"/support/t/{ticket.public_id}/{ticket.secret}/"
                    )
                    message = (
                        f"Holistica Caregiver Support ✅\n\n"
                        f"We replied to your ticket #{ticket.public_id}.\n\n"
                        f"Message:\n{msg[:1200]}\n\n"
                        f"Follow up here:\n{link}"
                    )
                    send_whatsapp_text(client_number, message)
            except Exception:
                pass
        return redirect("website:staff_support_ticket", public_id=ticket.public_id)
    return render(
        request,
        "website/staff/support_ticket.html",
        {"ticket": ticket, "page_title": f"Support {ticket.public_id}"},
    )
@staff_member_required
@staff_member_required
def staff_support_ticket_quick_action(request, public_id):
    if request.method != "POST":
        return redirect("website:staff_support_dashboard")
    t = get_object_or_404(SupportTicket, public_id=public_id)
    action = request.POST.get("action","")
    if action == "close":
        t.status = "closed"
        t.save()
    elif action == "reopen":
        t.status = "open"
        t.save()
    elif action == "assign":
        uid = request.POST.get("assigned_to","").strip()
        if uid == "":
            t.assigned_to = None
        else:
            try:
                t.assigned_to_id = int(uid)
            except Exception:
                pass
        t.save()
    ActivityLog.objects.create(actor=request.user, action="support_ticket_quick_action", message=f"Support {t.public_id}: {action}")
    return redirect(request.META.get("HTTP_REFERER","/staff/support/"))
def staff_support_analytics(request):
    from django.db.models import Avg, Count
    from django.utils import timezone
    from django.utils.text import slugify
    now = timezone.now()
    tickets = SupportTicket.objects.all()
    open_count = tickets.filter(status="open").count()
    pending_count = tickets.filter(status="pending").count()
    closed_count = tickets.filter(status="closed").count()
    # avg first response (minutes)
    resp_mins = []
    for t in tickets.exclude(first_response_at__isnull=True):
        resp_mins.append((t.first_response_at - t.created_at).total_seconds()/60.0)
    avg_first_response = round(sum(resp_mins)/len(resp_mins),2) if resp_mins else None
    sla_missed = tickets.filter(status__in=["open","pending"], due_at__isnull=False, due_at__lt=now, first_response_at__isnull=True).count()
    recent = tickets.order_by("-created_at")[:12]
    return render(request, "website/staff/support_analytics.html", {
        "open_count": open_count,
        "pending_count": pending_count,
        "closed_count": closed_count,
        "avg_first_response": avg_first_response,
        "sla_missed": sla_missed,
        "recent": recent,
        "page_title": "Support Analytics",
    })
@staff_member_required
def staff_notifications_poll(request):
    qs = StaffNotification.objects.filter(user=request.user, is_read=False)[:20]
    data = [{
        "id": n.id,
        "title": n.title,
        "body": n.body,
        "url": n.url,
        "created_at": n.created_at.isoformat(),
    } for n in qs]
    return JsonResponse({"items": data})
@staff_member_required
def staff_notifications_mark_read(request):
    ids = request.POST.get("ids","")
    try:
        id_list = [int(x) for x in ids.split(",") if x.strip().isdigit()]
    except Exception:
        id_list = []
    if id_list:
        StaffNotification.objects.filter(user=request.user, id__in=id_list).update(is_read=True)
    return JsonResponse({"ok": True})
@staff_member_required
def staff_dashboard(request):
    # High-level monitoring dashboard
    stats = {
        "telehealth_bookings": TelehealthBooking.objects.count(),
        "telehealth_paid": TelehealthBooking.objects.filter(is_paid=True).count(),
        "care_requests": CareRequest.objects.count(),
        "career_applications": CareerApplication.objects.count(),
        "enquiries": Enquiry.objects.count(),
        "patients": Patient.objects.count(),
        "payments": PaymentTransaction.objects.count(),
        "support_tickets": SupportTicket.objects.count(),
    }
    recent = {
        "bookings": TelehealthBooking.objects.select_related("slot", "patient").order_by("-created_at")[:8],
        "applications": CareerApplication.objects.order_by("-created_at")[:8],
        "care_requests": CareRequest.objects.order_by("-created_at")[:8],
        "payments": PaymentTransaction.objects.order_by("-created_at")[:8],
        "activity": ActivityLog.objects.order_by("-created_at")[:15],
    }
    # Charts (last 8 weeks)
    booking_series = (TelehealthBooking.objects
        .annotate(w=TruncWeek("created_at"))
        .values("w")
        .annotate(c=Count("id"))
        .order_by("w")
    )
    booking_labels = [x["w"].strftime("%Y-%m-%d") if x["w"] else "" for x in booking_series][-8:]
    booking_values = [x["c"] for x in booking_series][-8:]
    revenue_series = (PaymentTransaction.objects
        .filter(status="success") if hasattr(PaymentTransaction, "status") else PaymentTransaction.objects.all()
    )
    revenue_series = (revenue_series
        .annotate(w=TruncWeek("created_at"))
        .values("w")
        .annotate(amt=Sum("amount_ngn") if hasattr(PaymentTransaction, "amount_ngn") else Sum("amount"))
        .order_by("w")
    )
    revenue_labels = [x["w"].strftime("%Y-%m-%d") if x["w"] else "" for x in revenue_series][-8:]
    revenue_values = [int(x["amt"] or 0) for x in revenue_series][-8:]
    # Top services by request type (last 30 days)
    top_services_qs = CareRequest.objects
    top_services = (top_services_qs
        .values("request_type")
        .annotate(c=Count("id"))
        .order_by("-c")[:8]
    )
    type_map = dict(CareRequest.REQUEST_TYPES)
    top_services_labels = [type_map.get(x["request_type"], x["request_type"]) for x in top_services]
    top_services_values = [x["c"] for x in top_services]
    # Lab volume per week (last 8 weeks)
    lab_labels, lab_values = [], []
    try:
        from emr.models import LabResult
        lab_series = (LabResult.objects
            .annotate(w=TruncWeek("created_at"))
            .values("w")
            .annotate(c=Count("id"))
            .order_by("w")
        )
        lab_labels = [x["w"].strftime("%Y-%m-%d") if x["w"] else "" for x in lab_series][-8:]
        lab_values = [x["c"] for x in lab_series][-8:]
    except Exception:
        pass
    charts = {
        "bookings": {"labels": booking_labels, "values": booking_values},
        "revenue": {"labels": revenue_labels, "values": revenue_values},
        "top_services": {"labels": top_services_labels, "values": top_services_values},
        "lab_volume": {"labels": lab_labels, "values": lab_values},
    }
    return render(request, "website/staff/dashboard.html", {
        "page_title": "Staff Dashboard",
        "stats": stats,
        "charts_json": json.dumps(charts),
        **recent,
    })
def staff_emr_patients(request):
    q = (request.GET.get("q") or "").strip()
    patients = Patient.objects.all().order_by("-updated_at")
    if q:
        patients = patients.filter(models.Q(full_name__icontains=q) | models.Q(phone__icontains=q) | models.Q(email__icontains=q))
    return render(request, "website/staff/emr_patients.html", {"page_title": "EMR – Patients", "patients": patients[:200], "q": q})
@staff_member_required
def staff_emr_patient_detail(request, patient_id: int):
    patient = get_object_or_404(Patient, pk=patient_id)
    form = EMRNoteForm()
    if request.method == "POST":
        form = EMRNoteForm(request.POST)
        if form.is_valid():
            note = form.save(commit=False)
            note.patient = patient
            note.created_by = request.user
            note.save()
            log_activity("emr_note", f"EMR note added for {patient.full_name}", patient_id=patient.id)
            notify_admin("New EMR Note Added", f"Patient: {patient.full_name} ({patient.phone})\nNote: {note.title or ''}")
            return redirect("website:staff_emr_patient_detail", patient_id=patient.id)
    notes = patient.notes.select_related("created_by").order_by("-created_at")[:100]
    return render(request, "website/staff/emr_patient_detail.html", {
        "page_title": f"EMR – {patient.full_name}",
        "patient": patient,
        "notes": notes,
        "form": form,
    })
# ======================================================
# Staff Admin (Custom UI replacing Django admin)
# ======================================================
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.contrib import messages
from django.core.paginator import Paginator
from django.core.cache import cache
from django.db.models import Sum, Count
from django.db.models.functions import TruncWeek, TruncDate
from django.shortcuts import get_object_or_404
from django.db.models import Q
User = get_user_model()
@staff_member_required
def staff_telehealth_booking_list(request):
    params = {
        "q": request.GET.get("q","").strip(),
        "status": request.GET.get("status","").strip(),
        "paid": request.GET.get("paid","").strip(),
        "provider": request.GET.get("provider","").strip(),
        "from": request.GET.get("from","").strip(),
        "to": request.GET.get("to","").strip(),
    }
    preset, params = _apply_preset_filters(request, "bookings", params)
    q = params["q"]
    status = params["status"]
    paid = params["paid"]
    provider_id = params["provider"]
    date_from = params["from"]
    date_to = params["to"]
    # TelehealthBooking links to provider through the selected availability slot.
    # (TelehealthBooking has: slot, patient. TelehealthAvailabilitySlot has: provider)
    qs = TelehealthBooking.objects.all().select_related("slot", "slot__provider", "patient")
    if q:
        qs = qs.filter(
            Q(full_name__icontains=q) |
            Q(email__icontains=q) |
            Q(phone__icontains=q) |
            Q(paystack_reference__icontains=q)
        )
    if status:
        qs = qs.filter(status=status)
    if paid in {"yes","no"}:
        qs = qs.filter(is_paid=(paid=="yes"))
    if provider_id:
        # Provider is on the slot, not directly on booking
        try:
            qs = qs.filter(slot__provider_id=int(provider_id))
        except Exception:
            pass
    if date_from:
        try:
            qs = qs.filter(created_at__date__gte=date_from)
        except Exception:
            pass
    if date_to:
        try:
            qs = qs.filter(created_at__date__lte=date_to)
        except Exception:
            pass
    sort_map = {
        "id": "id",
        "client": "full_name",
        "status": "status",
        "paid": "is_paid",
        "created": "created_at",
    }
    qs, sort, direction = _apply_sorting(request, qs, sort_map, default="-created_at")
    # Export
    export = request.GET.get("export","").strip().lower()
    if export in {"csv","pdf"}:
        rows = []
        for b in qs[:2000]:
            rows.append([
                b.id,
                b.full_name,
                getattr(b, "mode", "") or getattr(b, "booking_mode", ""),
                b.status,
                "Yes" if getattr(b, "is_paid", False) else "No",
                getattr(b.slot, "start_at", "") if getattr(b, "slot_id", None) else "",
                b.created_at.strftime("%Y-%m-%d %H:%M"),
            ])
        title = "Telehealth Bookings"
        cols = ["ID","Client","Mode","Status","Paid","Slot","Created"]
        return _export_csv(title, cols, rows) if export=="csv" else _export_pdf(title, cols, rows)
    providers = TelehealthProvider.objects.all().order_by("sort_order","full_name") if "TelehealthProvider" in globals() else []
    page_obj = _paginate(request, qs, per_page=25)
    return render(request, "website/staff/bookings_list.html", {
        "bookings": page_obj.object_list,
        "page_obj": page_obj,
        "q": q,
        "status": status,
        "paid": paid,
        "provider_id": provider_id,
        "date_from": date_from,
        "date_to": date_to,
        "providers": providers,
        "preset": preset,
        "sort": sort,
        "dir": direction,
    })
def staff_telehealth_booking_detail(request, booking_id):
    booking = get_object_or_404(TelehealthBooking, pk=booking_id)
    if request.method == "POST":
        form = TelehealthBookingStaffForm(request.POST, instance=booking)
        if form.is_valid():
            form.save()
            messages.success(request, "Booking updated.")
            return redirect("website:staff_telehealth_booking_detail", booking_id=booking.id)
    else:
        form = TelehealthBookingStaffForm(instance=booking)
    return render(request, "website/staff/booking_detail.html", {"booking": booking, "form": form})
@staff_member_required
@staff_member_required
def staff_booking_quick_action(request, booking_id):
    if request.method != "POST":
        return redirect("website:staff_telehealth_booking_list")
    b = get_object_or_404(TelehealthBooking, id=booking_id)
    action = request.POST.get("action","")
    if action == "mark_paid":
        b.is_paid = True
    elif action == "mark_unpaid":
        b.is_paid = False
    elif action.startswith("status:"):
        st = action.split(":",1)[1]
        if st in {"new","confirmed","completed","cancelled"}:
            b.status = st
    elif action.startswith("assign_provider:"):
        pid = action.split(":",1)[1]
        if pid.isdigit() and hasattr(b, "provider_id"):
            b.provider_id = int(pid)
    b.save()
    ActivityLog.objects.create(
        actor=request.user,
        action="booking_quick_action",
        message=f"Booking #{b.id}: {action}",
    )
    return redirect(request.META.get("HTTP_REFERER","/staff/bookings/"))
@staff_member_required
def staff_provider_list(request):
    providers = TelehealthProvider.objects.all().order_by('sort_order','full_name')
    return render(request,'website/staff/providers_list.html',{'providers':providers})

@staff_member_required
def staff_provider_create(request):
    from .forms import StaffTelehealthProviderForm
    form = StaffTelehealthProviderForm(request.POST or None)
    if request.method == 'POST' and form.is_valid():
        provider = form.save(commit=False)
        provider.is_active = False
        provider.is_verified = False
        provider.auto_schedule = False
        provider.save()
        messages.success(request,'Clinician registered. A superuser must verify and activate availability.')
        return redirect('website:staff_provider_list')
    return render(request,'website/staff/provider_form.html',{'form':form,'heading':'Register clinician'})

@staff_member_required
def staff_provider_edit(request,provider_id):
    from .forms import StaffTelehealthProviderForm
    provider = get_object_or_404(TelehealthProvider,pk=provider_id)
    form = StaffTelehealthProviderForm(request.POST or None,instance=provider)
    if request.method == 'POST' and form.is_valid():
        form.save()
        # Changing license/schedule requires reapproval; existing bookings are not deleted.
        if not request.user.is_superuser:
            provider.is_verified = False
            provider.auto_schedule = False
            provider.is_active = False
            provider.save(update_fields=['is_verified','auto_schedule','is_active'])
        messages.success(request,'Clinician details updated.')
        return redirect('website:staff_provider_list')
    return render(request,'website/staff/provider_form.html',{'form':form,'heading':'Edit clinician','provider':provider})

@staff_member_required
@require_POST
def staff_provider_approval(request,provider_id):
    if not request.user.is_superuser:
        from django.core.exceptions import PermissionDenied
        raise PermissionDenied('Only superusers may approve or activate clinicians.')
    provider = get_object_or_404(TelehealthProvider,pk=provider_id)
    action = request.POST.get('action')
    if action == 'approve':
        if not provider.license_number.strip() or not provider.license_authority.strip():
            messages.error(request,'Record the professional license number and issuing authority before approval.')
        else:
            provider.is_verified = True
            provider.is_active = True
            provider.auto_schedule = True
            provider.save(update_fields=['is_verified','is_active','auto_schedule'])
            messages.success(request,'Clinician approved and automated availability enabled. Confirm credentials independently before approving.')
    elif action == 'suspend':
        provider.is_active = False
        provider.auto_schedule = False
        provider.save(update_fields=['is_active','auto_schedule'])
        messages.success(request,'Clinician suspended; existing bookings remain for staff follow-up.')
    elif action == 'resume' and provider.is_verified:
        provider.is_active = True
        provider.auto_schedule = True
        provider.save(update_fields=['is_active','auto_schedule'])
        messages.success(request,'Verified clinician reactivated.')
    return redirect('website:staff_provider_list')

@staff_member_required
def staff_slot_list(request):
    q = request.GET.get("q","").strip()
    provider_id = request.GET.get("provider","").strip()
    active = request.GET.get("active","").strip()
    date_from = request.GET.get("from","").strip()
    date_to = request.GET.get("to","").strip()
    qs = TelehealthAvailabilitySlot.objects.all().select_related("provider").order_by("-start_at")
    if q:
        qs = qs.filter(Q(provider__full_name__icontains=q) | Q(provider__specialization__icontains=q))
    if provider_id.isdigit():
        qs = qs.filter(provider_id=int(provider_id))
    if active == "yes":
        qs = qs.filter(is_active=True)
    elif active == "no":
        qs = qs.filter(is_active=False)
    if date_from:
        try: qs = qs.filter(start_at__date__gte=date_from)
        except Exception: pass
    if date_to:
        try: qs = qs.filter(start_at__date__lte=date_to)
        except Exception: pass
    providers = TelehealthProvider.objects.all().order_by("sort_order","full_name") if "TelehealthProvider" in globals() else []
    page_obj = _paginate(request, qs, per_page=25)
    return render(request, "website/staff/slots_list.html", {"slots": page_obj.object_list, "page_obj": page_obj, "q": q, "provider_id": provider_id, "active": active, "date_from": date_from, "date_to": date_to, "providers": providers})
def staff_slot_create(request):
    if request.method == "POST":
        form = TelehealthAvailabilitySlotForm(request.POST)
        if form.is_valid():
            form.save()
            messages.success(request, "Slot created.")
            return redirect("website:staff_slot_list")
    else:
        form = TelehealthAvailabilitySlotForm()
    return render(request, "website/staff/slot_form.html", {"form": form, "mode": "create"})
@staff_member_required
def staff_slot_edit(request, slot_id):
    slot = get_object_or_404(TelehealthAvailabilitySlot, pk=slot_id)
    if request.method == "POST":
        form = TelehealthAvailabilitySlotForm(request.POST, instance=slot)
        if form.is_valid():
            form.save()
            messages.success(request, "Slot updated.")
            return redirect("website:staff_slot_list")
    else:
        form = TelehealthAvailabilitySlotForm(instance=slot)
    return render(request, "website/staff/slot_form.html", {"form": form, "mode": "edit", "slot": slot})
@staff_member_required
def staff_slot_delete(request, slot_id):
    slot = get_object_or_404(TelehealthAvailabilitySlot, pk=slot_id)
    if request.method == "POST":
        slot.delete()
        messages.success(request, "Slot deleted.")
        return redirect("website:staff_slot_list")
    return render(request, "website/staff/slot_delete.html", {"slot": slot})
@staff_member_required
def staff_slot_bulk_generate(request):
    providers = TelehealthProvider.objects.filter(is_active=True, is_verified=True).order_by("sort_order", "full_name")
    if request.method == "POST":
        try:
            provider = providers.get(pk=request.POST.get("provider"))
            days = int(request.POST.get("days", 7))
            first_day = datetime.strptime(request.POST.get("first_day") or timezone.localdate().isoformat(), "%Y-%m-%d").date()
            if first_day < timezone.localdate() or first_day > timezone.localdate() + timedelta(days=365):
                raise ValueError("Choose a start date within the next year.")
            capacity = int(request.POST.get("capacity", 1))
            interval = int(request.POST.get("interval", 60))
            start = datetime.strptime(request.POST.get("start_time", "09:00"), "%H:%M").time()
            end = datetime.strptime(request.POST.get("end_time", "17:00"), "%H:%M").time()
            if days not in (7, 14, 30) or not 1 <= capacity <= 100 or not 10 <= interval <= 240 or start >= end:
                raise ValueError("Choose valid dates, capacity, interval and a start time before the end time.")
            created = 0
            for i in range(days):
                day = first_day + timedelta(days=i)
                cursor = datetime.combine(day, start)
                finish = datetime.combine(day, end)
                while cursor + timedelta(minutes=interval) <= finish:
                    start_at = timezone.make_aware(cursor, timezone.get_current_timezone())
                    end_at = timezone.make_aware(cursor + timedelta(minutes=interval), timezone.get_current_timezone())
                    if end_at > timezone.now():
                        _, was_created = TelehealthAvailabilitySlot.objects.get_or_create(
                            provider=provider, start_at=start_at, end_at=end_at,
                            defaults={"capacity": capacity, "is_active": True})
                        created += int(was_created)
                    cursor += timedelta(minutes=interval)
            messages.success(request, f"Created {created} available appointment slots. Existing slots were skipped.")
            return redirect("website:staff_slot_list")
        except (ValueError, TypeError, TelehealthProvider.DoesNotExist) as exc:
            messages.error(request, f"Unable to generate slots: {exc}")
    return render(request, "website/staff/slot_bulk_generate.html", {"providers": providers})
@staff_member_required
def staff_care_request_list(request):
    params = {
        "q": request.GET.get("q","").strip(),
        "status": request.GET.get("status","").strip(),
        "type": request.GET.get("type","").strip(),
        "assigned": request.GET.get("assigned","").strip(),
        "from": request.GET.get("from","").strip(),
        "to": request.GET.get("to","").strip(),
    }
    preset, params = _apply_preset_filters(request, "care", params)
    q = params["q"]
    status = params["status"]
    req_type = params["type"]
    assigned = params["assigned"]
    date_from = params["from"]
    date_to = params["to"]
    qs = CareRequest.objects.all().select_related("assigned_to")
    if q:
        qs = qs.filter(Q(full_name__icontains=q) | Q(phone__icontains=q) | Q(email__icontains=q))
    if status in dict(CareRequest.STATUS_CHOICES):
        qs = qs.filter(status=status)
    if req_type:
        qs = qs.filter(request_type=req_type)
    if assigned:
        if assigned == "unassigned":
            qs = qs.filter(assigned_to__isnull=True)
        else:
            try:
                qs = qs.filter(assigned_to_id=int(assigned))
            except Exception:
                pass
    if date_from:
        try:
            qs = qs.filter(created_at__date__gte=date_from)
        except Exception:
            pass
    if date_to:
        try:
            qs = qs.filter(created_at__date__lte=date_to)
        except Exception:
            pass
    sort_map = {
        "id": "id",
        "client": "full_name",
        "type": "request_type",
        "status": "status",
        "assigned": "assigned_to__username",
        "created": "created_at",
    }
    qs, sort, direction = _apply_sorting(request, qs, sort_map, default="-created_at")
    export = request.GET.get("export","").strip().lower()
    if export in {"csv","pdf"}:
        rows = []
        for r in qs[:2000]:
            rows.append([
                r.id,
                r.full_name,
                r.get_request_type_display(),
                r.get_status_display(),
                (r.assigned_to.get_full_name() or r.assigned_to.username) if r.assigned_to else "",
                r.created_at.strftime("%Y-%m-%d %H:%M") if hasattr(r,"created_at") else "",
            ])
        title = "Care Requests"
        cols = ["ID","Client","Type","Status","Assigned to","Created"]
        return _export_csv(title, cols, rows) if export=="csv" else _export_pdf(title, cols, rows)
    staff_users = User.objects.filter(is_staff=True, is_active=True).order_by("first_name","username")
    page_obj = _paginate(request, qs, per_page=25)
    return render(request, "website/staff/care_requests_list.html", {
        "requests": page_obj.object_list,
        "page_obj": page_obj,
        "q": q,
        "status": status,
        "req_type": req_type,
        "assigned": assigned,
        "date_from": date_from,
        "date_to": date_to,
        "request_types": CareRequest.REQUEST_TYPES,
        "status_choices": CareRequest.STATUS_CHOICES,
        "staff_users": staff_users,
        "preset": preset,
        "sort": sort,
        "dir": direction,
    })
def staff_care_request_detail(request, request_id):
    care = get_object_or_404(CareRequest, pk=request_id)
    if request.method == "POST":
        form = CareRequestStaffForm(request.POST, instance=care)
        if form.is_valid():
            form.save()
            messages.success(request, "Care request updated.")
            return redirect("website:staff_care_request_detail", request_id=care.id)
    else:
        form = CareRequestStaffForm(instance=care)
    return render(request, "website/staff/care_request_detail.html", {"care": care, "form": form})
@staff_member_required
@staff_member_required
def staff_care_request_quick_action(request, request_id):
    if request.method != "POST":
        return redirect("website:staff_care_request_list")
    r = get_object_or_404(CareRequest, id=request_id)
    action = request.POST.get("action","")
    if action.startswith("status:"):
        st = action.split(":",1)[1]
        if st in dict(CareRequest.STATUS_CHOICES):
            r.status = st
            r.save()
    elif action == "assign":
        uid = request.POST.get("assigned_to","").strip()
        if uid == "":
            r.assigned_to = None
        else:
            try:
                r.assigned_to_id = int(uid)
            except Exception:
                pass
        r.save()
    ActivityLog.objects.create(actor=request.user, action="care_request_quick_action", message=f"CareRequest #{r.id}: {action}")
    return redirect(request.META.get("HTTP_REFERER","/staff/care/requests/"))
def staff_career_application_list(request):
    q = request.GET.get("q","").strip()
    qs = CareerApplication.objects.all().select_related("position").order_by("-created_at")
    if q:
        qs = qs.filter(Q(full_name__icontains=q) | Q(phone__icontains=q) | Q(email__icontains=q))
    return render(request, "website/staff/applications_list.html", {"applications": qs[:500], "q": q})
@staff_member_required
def staff_career_application_detail(request, application_id):
    app = get_object_or_404(CareerApplication, pk=application_id)
    if request.method == "POST":
        form = CareerApplicationStaffForm(request.POST, instance=app)
        if form.is_valid():
            form.save()
            messages.success(request, "Application updated.")
            return redirect("website:staff_career_application_detail", application_id=app.id)
    else:
        form = CareerApplicationStaffForm(instance=app)
    return render(request, "website/staff/application_detail.html", {"app": app, "form": form})
@staff_member_required
def staff_payment_list(request):
    params = {
        "q": request.GET.get("q","").strip(),
        "status": request.GET.get("status","").strip(),
        "from": request.GET.get("from","").strip(),
        "to": request.GET.get("to","").strip(),
    }
    preset, params = _apply_preset_filters(request, "payments", params)
    q = params["q"]
    status = params["status"]
    date_from = params["from"]
    date_to = params["to"]
    qs = PaymentTransaction.objects.all()
    if q:
        qs = qs.filter(Q(reference__icontains=q) | Q(email__icontains=q) | Q(phone__icontains=q))
    if status in {"pending","success","failed"} and hasattr(PaymentTransaction, "status"):
        qs = qs.filter(status=status)
    if date_from:
        try: qs = qs.filter(created_at__date__gte=date_from)
        except Exception: pass
    if date_to:
        try: qs = qs.filter(created_at__date__lte=date_to)
        except Exception: pass
    sort_map = {
        "ref": "reference",
        "email": "email",
        "status": "status",
        "amount": "amount",
        "created": "created_at",
    }
    qs, sort, direction = _apply_sorting(request, qs, sort_map, default="-created_at")
    export = request.GET.get("export","").strip().lower()
    if export in {"csv","pdf"}:
        rows=[]
        for p in qs[:2000]:
            rows.append([
                getattr(p,"reference",""),
                getattr(p,"email",""),
                getattr(p,"phone",""),
                getattr(p,"status",""),
                getattr(p,"amount",""),
                p.created_at.strftime("%Y-%m-%d %H:%M") if hasattr(p,"created_at") else "",
            ])
        title="Payments"
        cols=["Reference","Email","Phone","Status","Amount","Created"]
        return _export_csv(title, cols, rows) if export=="csv" else _export_pdf(title, cols, rows)
    page_obj = _paginate(request, qs, per_page=25)
    return render(request, "website/staff/payments_list.html", {
        "payments": page_obj.object_list,
        "page_obj": page_obj,
        "q": q,
        "status": status,
        "date_from": date_from,
        "date_to": date_to,
        "preset": preset,
        "sort": sort,
        "dir": direction,
    })
def staff_payment_detail(request, tx_id):
    tx = get_object_or_404(PaymentTransaction, pk=tx_id)
    if request.method == "POST":
        form = PaymentTransactionStaffForm(request.POST, instance=tx)
        if form.is_valid():
            form.save()
            messages.success(request, "Payment updated.")
            return redirect("website:staff_payment_detail", tx_id=tx.id)
    else:
        form = PaymentTransactionStaffForm(instance=tx)
    return render(request, "website/staff/payment_detail.html", {"tx": tx, "form": form})
def owner_manager_required(view_func):
    """Only designated managers can administer staff identities."""
    @wraps(view_func)
    @staff_member_required
    def restricted(request, *args, **kwargs):
        from django.core.exceptions import PermissionDenied
        if not (request.user.is_superuser or request.user.groups.filter(name__in=["Admin", "HR Manager"]).exists()):
            raise PermissionDenied("Staff account management requires administrator authorization.")
        return view_func(request, *args, **kwargs)
    return restricted

@owner_manager_required
def staff_user_list(request):
    q = request.GET.get("q","").strip()
    role = request.GET.get("role","").strip()
    qs = User.objects.filter(is_staff=True)
    if not request.user.is_superuser:
        qs = qs.filter(is_superuser=False).exclude(groups__name="Admin")
    if q:
        qs = qs.filter(Q(username__icontains=q) | Q(email__icontains=q) | Q(first_name__icontains=q) | Q(last_name__icontains=q))
    if role in {"Doctor", "Nurse", "Caregiver", "Care Coordinator", "Receptionist", "HR Manager", "Admin"}:
        qs = qs.filter(groups__name=role).distinct()
    sort_map = {
        "username": "username",
        "email": "email",
        "role": "groups__name",
        "name": "first_name",
        "staff": "is_staff",
    }
    qs, sort, direction = _apply_sorting(request, qs, sort_map, default="username")
    export = request.GET.get("export","").strip().lower()
    if export in {"csv","pdf"}:
        rows=[]
        for u in qs[:2000]:
            roles=", ".join(u.groups.values_list("name", flat=True))
            rows.append([u.id, u.username, u.email, u.get_full_name(), roles, "Yes" if u.is_staff else "No"])
        title="Users"
        cols=["ID","Username","Email","Name","Roles","Is staff"]
        return _export_csv(title, cols, rows) if export=="csv" else _export_pdf(title, cols, rows)
    page_obj = _paginate(request, qs, per_page=25)
    return render(request, "website/staff/users_list.html", {"users": page_obj.object_list, "page_obj": page_obj, "q": q, "role": role, "sort": sort, "dir": direction})
@owner_manager_required
def staff_user_create(request):
    if request.method == "POST":
        form = StaffUserCreateForm(request.POST)
        if form.is_valid():
            form.save()
            messages.success(request, "Staff user created.")
            return redirect("website:staff_user_list")
    else:
        form = StaffUserCreateForm()
    return render(request, "website/staff/user_form.html", {"form": form, "mode":"create"})
@owner_manager_required
def staff_user_edit(request, user_id):
    user_obj = get_object_or_404(User, pk=user_id, is_staff=True)
    if not request.user.is_superuser and (user_obj.is_superuser or user_obj.groups.filter(name="Admin").exists()):
        from django.core.exceptions import PermissionDenied
        raise PermissionDenied("Only the owner can modify administrator accounts.")
    if request.method == "POST":
        form = StaffUserForm(request.POST, instance=user_obj)
        if form.is_valid():
            form.save()
            messages.success(request, "User updated.")
            return redirect("website:staff_user_list")
    else:
        form = StaffUserForm(instance=user_obj)
    return render(request, "website/staff/user_form.html", {"form": form, "mode":"edit", "user_obj": user_obj})
# ======================================================
# Staff UI - Content CRUD (Services / Blog / Positions)
# ======================================================
@staff_member_required
def staff_service_list(request):
    q = request.GET.get("q","").strip()
    qs = Service.objects.all()
    if q:
        qs = qs.filter(Q(title__icontains=q) | Q(short_description__icontains=q))
    sort_map = {
        "title": "title",
        "order": "sort_order",
        "created": "created_at" if hasattr(Service, "created_at") else "id",
    }
    # Default: sort_order then title
    qs, sort, direction = _apply_sorting(request, qs, sort_map, default="sort_order")
    export = request.GET.get("export","").strip().lower()
    if export in {"csv","pdf"}:
        rows=[]
        for s in qs[:2000]:
            rows.append([s.id, s.title, getattr(s,"sort_order",""), getattr(s,"is_active","") if hasattr(s,"is_active") else "", ])
        title="Services"
        cols=["ID","Title","Sort order","Active"]
        return _export_csv(title, cols, rows) if export=="csv" else _export_pdf(title, cols, rows)
    page_obj = _paginate(request, qs, per_page=25)
    return render(request, "website/staff/services_list.html", {"services": page_obj.object_list, "page_obj": page_obj, "q": q, "sort": sort, "dir": direction})
def staff_service_create(request):
    if request.method == "POST":
        form = ServiceForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect("website:staff_service_list")
    else:
        form = ServiceForm()
    return render(request, "website/staff/service_form.html", {"form": form, "title": "New Service"})
@staff_member_required
def staff_service_edit(request, service_id: int):
    obj = get_object_or_404(Service, id=service_id)
    if request.method == "POST":
        form = ServiceForm(request.POST, instance=obj)
        if form.is_valid():
            form.save()
            return redirect("website:staff_service_list")
    else:
        form = ServiceForm(instance=obj)
    return render(request, "website/staff/service_form.html", {"form": form, "title": "Edit Service"})
@staff_member_required
def staff_service_delete(request, service_id: int):
    obj = get_object_or_404(Service, id=service_id)
    if request.method == "POST":
        obj.delete()
        return redirect("website:staff_service_list")
    return render(
        request,
        "website/staff/delete_confirm_staff.html",
        {"obj": obj, "message": "This will remove the service from the website.", "cancel_url": redirect("website:staff_service_list").url},
    )
@staff_member_required
def staff_blog_list(request):
    q = request.GET.get("q","").strip()
    qs = BlogPost.objects.all()
    if q:
        qs = qs.filter(Q(title__icontains=q) | Q(excerpt__icontains=q) | Q(content__icontains=q))
    sort_map = {
        "title": "title",
        "status": "status" if hasattr(BlogPost,"status") else "id",
        "created": "created_at" if hasattr(BlogPost,"created_at") else "id",
    }
    qs, sort, direction = _apply_sorting(request, qs, sort_map, default="-created_at" if hasattr(BlogPost,"created_at") else "-id")
    export = request.GET.get("export","").strip().lower()
    if export in {"csv","pdf"}:
        rows=[]
        for p in qs[:2000]:
            rows.append([p.id, p.title, getattr(p,"status",""), getattr(p,"published_at","") or "", ])
        title="Blog Posts"
        cols=["ID","Title","Status","Published"]
        return _export_csv(title, cols, rows) if export=="csv" else _export_pdf(title, cols, rows)
    page_obj = _paginate(request, qs, per_page=25)
    return render(request, "website/staff/blog_list_staff.html", {"posts": page_obj.object_list, "page_obj": page_obj, "q": q, "sort": sort, "dir": direction})
def staff_blog_create(request):
    if request.method == "POST":
        form = BlogPostForm(request.POST, request.FILES)
        if form.is_valid():
            form.save()
            return redirect("website:staff_blog_list")
    else:
        form = BlogPostForm()
    return render(request, "website/staff/blog_form_staff.html", {"form": form, "title": "New Blog Post"})
@staff_member_required
def staff_blog_edit(request, post_id: int):
    obj = get_object_or_404(BlogPost, id=post_id)
    if request.method == "POST":
        form = BlogPostForm(request.POST, request.FILES, instance=obj)
        if form.is_valid():
            form.save()
            return redirect("website:staff_blog_list")
    else:
        form = BlogPostForm(instance=obj)
    return render(request, "website/staff/blog_form_staff.html", {"form": form, "title": "Edit Blog Post"})
@staff_member_required
def staff_blog_delete(request, post_id: int):
    obj = get_object_or_404(BlogPost, id=post_id)
    if request.method == "POST":
        obj.delete()
        return redirect("website:staff_blog_list")
    return render(
        request,
        "website/staff/delete_confirm_staff.html",
        {"obj": obj, "message": "This will permanently delete the blog post.", "cancel_url": redirect("website:staff_blog_list").url},
    )
@staff_member_required
def staff_position_list(request):
    q = request.GET.get("q","").strip()
    qs = CareerPosition.objects.all()
    if q:
        qs = qs.filter(Q(title__icontains=q) | Q(location__icontains=q) | Q(description__icontains=q))
    sort_map = {
        "title": "title",
        "location": "location",
        "created": "created_at" if hasattr(CareerPosition,"created_at") else "id",
        "active": "is_active" if hasattr(CareerPosition,"is_active") else "id",
    }
    qs, sort, direction = _apply_sorting(request, qs, sort_map, default="-created_at" if hasattr(CareerPosition,"created_at") else "-id")
    export = request.GET.get("export","").strip().lower()
    if export in {"csv","pdf"}:
        rows=[]
        for p in qs[:2000]:
            rows.append([p.id, p.title, getattr(p,"location",""), getattr(p,"is_active","") if hasattr(p,"is_active") else "", getattr(p,"created_at","") ])
        title="Career Positions"
        cols=["ID","Title","Location","Active","Created"]
        return _export_csv(title, cols, rows) if export=="csv" else _export_pdf(title, cols, rows)
    page_obj = _paginate(request, qs, per_page=25)
    return render(request, "website/staff/positions_list_staff.html", {"positions": page_obj.object_list, "page_obj": page_obj, "q": q, "sort": sort, "dir": direction})
def staff_position_create(request):
    if request.method == "POST":
        form = CareerPositionForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect("website:staff_position_list")
    else:
        form = CareerPositionForm()
    return render(request, "website/staff/position_form_staff.html", {"form": form, "title": "New Career Position"})
@staff_member_required
def staff_position_edit(request, position_id: int):
    obj = get_object_or_404(CareerPosition, id=position_id)
    if request.method == "POST":
        form = CareerPositionForm(request.POST, instance=obj)
        if form.is_valid():
            form.save()
            return redirect("website:staff_position_list")
    else:
        form = CareerPositionForm(instance=obj)
    return render(request, "website/staff/position_form_staff.html", {"form": form, "title": "Edit Career Position"})
@staff_member_required
def staff_position_delete(request, position_id: int):
    obj = get_object_or_404(CareerPosition, id=position_id)
    if request.method == "POST":
        obj.delete()
        return redirect("website:staff_position_list")
    return render(
        request,
        "website/staff/delete_confirm_staff.html",
        {"obj": obj, "message": "This will remove the position from the careers page.", "cancel_url": redirect("website:staff_position_list").url},
    )
@staff_member_required
def staff_live_counts(request):
    """Lightweight endpoint for live badge counts in sidebar/header."""
    data = {
        "bookings": TelehealthBooking.objects.count(),
        "care_requests": CareRequest.objects.count(),
        "support_tickets": SupportTicket.objects.count(),
        "payments": PaymentTransaction.objects.count(),
        "patients": Patient.objects.count() if "Patient" in globals() else 0,
    }
    return JsonResponse(data)
@staff_member_required
@require_POST
def staff_bulk_action(request, module: str):
    """Perform bulk actions for list tables (bookings, payments, support, care)."""
    try:
        payload = json.loads(request.body.decode("utf-8"))
    except Exception:
        return JsonResponse({"ok": False, "error": "Invalid JSON payload"}, status=400)
    action = (payload.get("action") or "").strip()
    ids = payload.get("ids") or []
    if not action or not isinstance(ids, list) or not ids:
        return JsonResponse({"ok": False, "error": "Missing action or ids"}, status=400)
    # Permissions by role
    is_admin = request.user.is_superuser or request.user.groups.filter(name__in=["Admin", "HR Manager"]).exists()
    is_doctor = is_admin or request.user.groups.filter(name="Doctor").exists()
    is_nurse = is_admin or request.user.groups.filter(name="Nurse").exists()
    if module == "bookings":
        qs = TelehealthBooking.objects.filter(id__in=ids)
        if action == "mark_paid" and is_admin:
            qs.update(is_paid=True)
        elif action == "mark_unpaid" and is_admin:
            qs.update(is_paid=False)
        elif action.startswith("status:"):
            status = action.split(":", 1)[1]
            if is_admin or is_nurse or is_doctor:
                qs.update(status=status)
        else:
            return JsonResponse({"ok": False, "error": "Action not allowed"}, status=403)
    elif module == "payments":
        qs = PaymentTransaction.objects.filter(id__in=ids)
        if not is_admin:
            return JsonResponse({"ok": False, "error": "Admin only"}, status=403)
        if action in {"mark_success","mark_failed","mark_pending"}:
            status = action.replace("mark_","")
            qs.update(status=status)
        else:
            return JsonResponse({"ok": False, "error": "Unknown action"}, status=400)
    elif module == "care":
        qs = CareRequest.objects.filter(id__in=ids)
        if action.startswith("status:") and (is_admin or is_nurse or is_doctor):
            status = action.split(":",1)[1]
            qs.update(status=status)
        elif action.startswith("assign:") and is_admin:
            user_id = action.split(":",1)[1]
            qs.update(assigned_to_id=user_id)
            _ws_notify_user(int(user_id), {"type":"assigned","message":"New care request assigned to you"})
        else:
            return JsonResponse({"ok": False, "error": "Action not allowed"}, status=403)
    elif module == "support":
        qs = SupportTicket.objects.filter(id__in=ids)
        if action == "close" and (is_admin or is_nurse or is_doctor):
            qs.update(status="closed")
        elif action == "reopen" and (is_admin or is_nurse or is_doctor):
            qs.update(status="open")
        elif action.startswith("assign:") and is_admin:
            user_id = action.split(":",1)[1]
            qs.update(assigned_to_id=user_id)
            _ws_notify_user(int(user_id), {"type":"assigned","message":"New care request assigned to you"})
        else:
            return JsonResponse({"ok": False, "error": "Action not allowed"}, status=403)
    else:
        return JsonResponse({"ok": False, "error": "Unknown module"}, status=404)
    _ws_broadcast({"type":"counts","counts": {
    "bookings": TelehealthBooking.objects.count(),
    "care_requests": CareRequest.objects.count(),
    "support_tickets": SupportTicket.objects.count(),
    "payments": PaymentTransaction.objects.count(),
    "patients": Patient.objects.count() if "Patient" in globals() else 0,
    }})
    return JsonResponse({"ok": True, "updated": len(ids)})
@staff_member_required
def staff_patient_timeline(request, patient_id: int):
    """Unified patient timeline: encounters, vitals, diagnoses, prescriptions, labs, and audit events."""
    from emr.models import Encounter, Vital, Diagnosis, Prescription, LabResult
    patient = get_object_or_404(Patient, id=patient_id)
    events = []
    for e in Encounter.objects.filter(patient=patient).order_by("-created_at")[:50]:
        events.append({"ts": e.created_at, "type":"Encounter", "title": f"Encounter #{e.id}", "detail": getattr(e, "chief_complaint", "") or ""})
    for v in Vital.objects.filter(encounter__patient=patient).select_related("encounter").order_by("-created_at")[:80]:
        detail = f"BP {v.bp_systolic}/{v.bp_diastolic} • Temp {v.temperature} • Pulse {v.pulse}"
        events.append({"ts": v.created_at, "type":"Vitals", "title": "Vitals recorded", "detail": detail})
    for d in Diagnosis.objects.filter(encounter__patient=patient).select_related("encounter").order_by("-created_at")[:80]:
        events.append({"ts": d.created_at, "type":"Diagnosis", "title": d.title or "Diagnosis", "detail": d.code or ""})
    for p in Prescription.objects.filter(encounter__patient=patient).select_related("encounter").order_by("-created_at")[:80]:
        events.append({"ts": p.created_at, "type":"Prescription", "title": p.medication_name, "detail": f"{p.dosage} • {p.frequency} • {p.duration}"})
    for l in LabResult.objects.filter(encounter__patient=patient).select_related("encounter").order_by("-created_at")[:80]:
        events.append({"ts": l.created_at, "type":"Lab", "title": l.test_name, "detail": l.status})
    # Optional: audit log if present
    try:
        from emr.models import AuditLog
        for a in AuditLog.objects.filter(patient_id=patient.id).order_by("-created_at")[:120]:
            who = getattr(a, "actor", None)
            events.append({"ts": a.created_at, "type":"Audit", "title": f"{a.action}", "detail": f"by {who}" if who else ""})
    except Exception:
        pass
    events.sort(key=lambda x: x["ts"] or datetime.datetime.min, reverse=True)
    return render(request, "website/staff/emr/patient_timeline.html", {
        "patient": patient,
        "events": events[:200],
        "page_title": "Patient Timeline",
    })


@staff_member_required
@require_POST
def staff_bulk_export(request, module: str):
    """Export selected rows as CSV/PDF (POST ids list)."""
    fmt = (request.GET.get("format") or "csv").lower()
    try:
        payload = json.loads(request.body.decode("utf-8"))
    except Exception:
        payload = {}
    ids = payload.get("ids") or []
    if not isinstance(ids, list) or not ids:
        return JsonResponse({"ok": False, "error": "No rows selected"}, status=400)

    # Admin-only for sensitive exports (payments/users). Others allowed for staff.
    is_admin = request.user.is_superuser or request.user.groups.filter(name__in=["Admin", "HR Manager"]).exists()
    is_doctor = is_admin or request.user.groups.filter(name="Doctor").exists()
    is_nurse = is_admin or request.user.groups.filter(name="Nurse").exists()

    title = f"{module.title()} export"
    columns = []
    rows = []

    if module == "bookings":
        qs = TelehealthBooking.objects.filter(id__in=ids).select_related("slot","patient").order_by("-created_at")
        columns = ["ID","Patient","Service","Status","Paid","Created"]
        for b in qs:
            rows.append([b.id, getattr(b.patient,'full_name',str(b.patient)), getattr(b,'service_type', '') or "", b.status, "YES" if b.is_paid else "NO", b.created_at])
    elif module == "payments":
        if not is_admin:
            return JsonResponse({"ok": False, "error": "Admin only"}, status=403)
        qs = PaymentTransaction.objects.filter(id__in=ids).order_by("-created_at")
        columns = ["ID","Reference","Amount","Status","Created"]
        for p in qs:
            rows.append([p.id, p.reference, p.amount, p.status, p.created_at])
    elif module == "care":
        qs = CareRequest.objects.filter(id__in=ids).select_related("assigned_to").order_by("-created_at")
        columns = ["ID","Name","Type","Status","Assigned To","Created"]
        for r in qs:
            rows.append([r.id, getattr(r,'full_name',''), r.request_type, r.status, getattr(r.assigned_to,'username',''), r.created_at])
    elif module == "support":
        qs = SupportTicket.objects.filter(id__in=ids).select_related("assigned_to").order_by("-created_at")
        columns = ["ID","Public ID","Subject","Status","Assigned To","Created"]
        for t in qs:
            rows.append([t.id, t.public_id, t.subject, t.status, getattr(t.assigned_to,'username',''), t.created_at])
    else:
        return JsonResponse({"ok": False, "error": "Unknown module"}, status=404)

    if fmt == "pdf":
        return _export_pdf(title, columns, rows)
    return _export_csv(title, columns, rows)


@staff_member_required
def staff_inbox(request):
    """Staff inbox showing items assigned to the current user with SLA countdown."""
    now = timezone.now()
    # SLA defaults (minutes)
    sla_support_mins = int(getattr(settings, "SLA_SUPPORT_MINS", 60))
    sla_care_mins = int(getattr(settings, "SLA_CARE_MINS", 120))

    my_support = SupportTicket.objects.filter(assigned_to=request.user).order_by("-created_at")[:200]
    my_care = CareRequest.objects.filter(assigned_to=request.user).order_by("-created_at")[:200]

    def due_for(created_at, mins):
        try:
            return created_at + datetime.timedelta(minutes=mins)
        except Exception:
            return None

    support_rows = []
    for t in my_support:
        due_at = getattr(t, "due_at", None) or due_for(t.created_at, sla_support_mins)
        support_rows.append({"obj": t, "due_at": due_at})

    care_rows = []
    for r in my_care:
        due_at = due_for(r.created_at, sla_care_mins)
        care_rows.append({"obj": r, "due_at": due_at})

    return render(request, "website/staff/inbox.html", {
        "support_rows": support_rows,
        "care_rows": care_rows,
        "now": now,
        "page_title": "My Inbox",
    })


def _ws_notify_user(user_id: int, payload: dict):
    """Send a websocket event to a staff user group (no-op if channels not configured)."""
    try:
        from asgiref.sync import async_to_sync
        from channels.layers import get_channel_layer
        channel_layer = get_channel_layer()
        if not channel_layer:
            return
        async_to_sync(channel_layer.group_send)(
            f"staff_user_{user_id}",
            {"type":"staff.event", "payload": payload},
        )
    except Exception:
        return

def _ws_broadcast(payload: dict):
    try:
        from asgiref.sync import async_to_sync
        from channels.layers import get_channel_layer
        channel_layer = get_channel_layer()
        if not channel_layer:
            return
        async_to_sync(channel_layer.group_send)(
            "staff_broadcast",
            {"type":"staff.event", "payload": payload},
        )
    except Exception:
        return


@staff_member_required
def staff_assignment_queue(request):
    """Unassigned queue for support tickets + care requests."""
    unassigned_support = SupportTicket.objects.filter(assigned_to__isnull=True).exclude(status="closed").order_by("-created_at")[:300]
    unassigned_care = CareRequest.objects.filter(assigned_to__isnull=True).exclude(status="completed").order_by("-created_at")[:300]
    # eligible staff for assignment (staff users in Admin/Doctor/Nurse)
    staff_users = User.objects.filter(is_staff=True, is_active=True).order_by("id")
    return render(request, "website/staff/queue.html", {
        "unassigned_support": unassigned_support,
        "unassigned_care": unassigned_care,
        "staff_users": staff_users,
        "page_title": "Assignment Queue",
    })

def _round_robin_next_user(cache_key: str, eligible_users):
    users = list(eligible_users)
    if not users:
        return None
    last_id = cache.get(cache_key)
    ids = [u.id for u in users]
    if last_id in ids:
        idx = ids.index(last_id)
        next_idx = (idx + 1) % len(users)
    else:
        next_idx = 0
    next_user = users[next_idx]
    cache.set(cache_key, next_user.id, 60 * 60 * 24 * 7)  # 7 days
    return next_user

@staff_member_required
@require_POST
def staff_auto_assign(request):
    """Auto-assign unassigned items using round-robin."""
    try:
        payload = json.loads(request.body.decode("utf-8"))
    except Exception:
        payload = {}
    kind = (payload.get("kind") or "all").lower()  # support/care/all
    limit = int(payload.get("limit") or 25)

    eligible = User.objects.filter(is_staff=True, is_active=True).order_by("id")
    assigned = {"support": 0, "care": 0}

    if kind in ("support","all"):
        qs = SupportTicket.objects.filter(assigned_to__isnull=True).exclude(status="closed").order_by("created_at")[:limit]
        for t in qs:
            user = _round_robin_next_user("rr_support_last", eligible)
            if not user:
                break
            t.assigned_to = user
            # set due_at if missing
            if getattr(t, "due_at", None) is None:
                mins = int(getattr(t, "sla_minutes", 15) or 15)
                t.due_at = t.created_at + datetime.timedelta(minutes=mins)
            t.save(update_fields=["assigned_to","due_at"])
            StaffNotification.objects.create(
                user=user, kind="support",
                title="New Support Ticket Assigned",
                body=f"Ticket #{t.public_id}: {t.subject}",
                url="/staff/support/"
            )
            _ws_notify_user(user.id, {"type":"assigned","message":f"New support ticket assigned: #{t.public_id}"})
            assigned["support"] += 1

    if kind in ("care","all"):
        qs = CareRequest.objects.filter(assigned_to__isnull=True).exclude(status="completed").order_by("created_at")[:limit]
        for r in qs:
            user = _round_robin_next_user("rr_care_last", eligible)
            if not user:
                break
            r.assigned_to = user
            r.save(update_fields=["assigned_to"])
            StaffNotification.objects.create(
                user=user, kind="care",
                title="New Care Request Assigned",
                body=f"Request #{r.id}: {r.full_name} ({r.request_type})",
                url="/staff/care/requests/"
            )
            _ws_notify_user(user.id, {"type":"assigned","message":f"New care request assigned: #{r.id}"})
            assigned["care"] += 1

    # broadcast counts
    _ws_broadcast({"type":"counts","counts": {
        "bookings": TelehealthBooking.objects.count(),
        "care_requests": CareRequest.objects.count(),
        "support_tickets": SupportTicket.objects.count(),
        "payments": PaymentTransaction.objects.count(),
        "patients": Patient.objects.count() if "Patient" in globals() else 0,
    }})
    return JsonResponse({"ok": True, "assigned": assigned})

def _notify_sla_breach(subject: str, message: str):
    # Email
    admin_email = getattr(settings, "ADMIN_NOTIFY_EMAIL", "") or getattr(settings, "DEFAULT_FROM_EMAIL", "")
    if admin_email:
        try:
            send_mail(subject, message, getattr(settings, "DEFAULT_FROM_EMAIL", admin_email), [admin_email], fail_silently=True)
        except Exception:
            pass
    # WhatsApp (optional)
    try:
        wa_to = getattr(settings, "ADMIN_WHATSAPP_NUMBER", "") or ""
        if wa_to:
            send_whatsapp_text(wa_to, message)
    except Exception:
        pass

@staff_member_required
def staff_sla_breaches(request):
    """Show SLA breaches and optionally trigger notifications."""
    now = timezone.now()
    support_breaches = SupportTicket.objects.filter(
        status__in=["open","pending"],
        due_at__isnull=False,
        first_response_at__isnull=True,
        due_at__lt=now
    ).order_by("due_at")[:300]

    # Care: if model has due_at field, use it; else compute from created + SLA_CARE_MINS
    care_breaches = []
    try:
        care_qs = CareRequest.objects.filter(status__in=["new","in_progress"]).order_by("-created_at")[:400]
        sla_care_mins = int(getattr(settings, "SLA_CARE_MINS", 120))
        for r in care_qs:
            due = getattr(r, "due_at", None)
            if due is None:
                due = r.created_at + datetime.timedelta(minutes=sla_care_mins)
            if due < now and getattr(r, "assigned_to_id", None):
                care_breaches.append((r, due))
    except Exception:
        care_breaches = []

    if request.GET.get("notify") == "1":
        sent = 0
        # prevent spamming using cache keys
        for t in support_breaches:
            key = f"sla_notified_support_{t.id}"
            if cache.get(key):
                continue
            _notify_sla_breach(
                "SLA Breach: Support Ticket",
                f"SLA Breach! Ticket #{t.public_id} ({t.subject}) is overdue. Assigned to: {getattr(t.assigned_to,'username','Unassigned')}"
            )
            cache.set(key, True, 60*60*6)  # 6 hours
            sent += 1

        for r,due in care_breaches:
            key = f"sla_notified_care_{r.id}"
            if cache.get(key):
                continue
            _notify_sla_breach(
                "SLA Breach: Care Request",
                f"SLA Breach! Care request #{r.id} for {r.full_name} is overdue. Assigned to: {getattr(r.assigned_to,'username','Unassigned')}"
            )
            cache.set(key, True, 60*60*6)
            sent += 1

        messages.success(request, f"SLA breach notifications triggered ({sent} sent).")
        return redirect("/staff/sla/")

    return render(request, "website/staff/sla_breaches.html", {
        "support_breaches": support_breaches,
        "care_breaches": care_breaches,
        "now": now,
        "page_title": "SLA Breaches",
    })

@staff_member_required
def staff_performance_dashboard(request):
    """Staff performance: avg response time, SLA hit rate, assigned workload."""
    now = timezone.now()
    staff_users = User.objects.filter(is_staff=True, is_active=True).order_by("username")
    rows = []
    for u in staff_users:
        # Support metrics
        sup = SupportTicket.objects.filter(assigned_to=u)
        total_assigned = sup.count()
        responded = sup.exclude(first_response_at__isnull=True)
        resp_count = responded.count()

        # avg response minutes
        avg_resp = None
        if resp_count:
            # compute in python (portable)
            deltas = []
            for t in responded.only("created_at","first_response_at"):
                if t.first_response_at:
                    deltas.append((t.first_response_at - t.created_at).total_seconds()/60.0)
            if deltas:
                avg_resp = sum(deltas)/len(deltas)

        # SLA hit rate
        sla_hit = 0
        sla_total = 0
        for t in responded.only("first_response_at","due_at","created_at","sla_minutes"):
            due = t.due_at
            if due is None:
                mins = int(getattr(t,"sla_minutes",15) or 15)
                due = t.created_at + datetime.timedelta(minutes=mins)
            if t.first_response_at:
                sla_total += 1
                if t.first_response_at <= due:
                    sla_hit += 1
        sla_rate = (sla_hit/sla_total*100.0) if sla_total else None

        # Care metrics
        care = CareRequest.objects.filter(assigned_to=u)
        care_assigned = care.count()

        rows.append({
            "user": u,
            "support_assigned": total_assigned,
            "support_responded": resp_count,
            "avg_response_mins": avg_resp,
            "sla_rate": sla_rate,
            "care_assigned": care_assigned,
        })

    return render(request, "website/staff/performance.html", {
        "rows": rows,
        "generated_at": now,
        "page_title": "Staff Performance",
    })