import json
from datetime import timedelta
from functools import wraps

from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Count, Sum
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.views.decorators.http import require_POST

from schools.access import allowed_columns, crm_can_assign, crm_level, visible_schools
from schools.cache import invalidate_finder_cache
from schools.pages import base_context

from . import services
from .constants import (CONTACT_TYPES, PRIORITIES, PRIORITY_LABELS, PRIORITY_RANK, STAGE_LABELS,
                        STAGES, TERMINAL)
from .forms import ActivityForm, ContactForm, LeadForm
from .models import Activity, Contact, Lead

User = get_user_model()
BULK_LIMIT = 5000
FACT_KEYS = ["headTeacherName", "contactNumber", "email", "organizationType", "districtName",
             "municipalityName", "ward", "organizationAddress", "total_students", "classRegisteredUpto"]


def crm_required(edit=False):
    """Login + CRM access (and 'edit' level for anything that changes data)."""
    def deco(view):
        @wraps(view)
        def wrapper(request, *a, **kw):
            level = crm_level(request.user)
            if level == "none" or (edit and level != "edit"):
                raise PermissionDenied
            return view(request, *a, **kw)
        return login_required(wrapper)
    return deco


def _scoped_school(request, org_id):
    """404 (not 403) for schools outside the user's rules, so their existence isn't revealed."""
    return get_object_or_404(visible_schools(request.user), organization_id=org_id)


def _scoped_leads(user):
    return Lead.objects.filter(school__in=visible_schools(user))


# ---------- school page ----------
def _render_school(request, school, lead_form=None, act_form=None, contact_form=None, status=200):
    user = request.user
    lead = Lead.objects.filter(school=school).select_related("owner").first()
    cols = set(allowed_columns(user))
    data = {k: v for k, v in school.data.items() if k in cols}
    facts = [(k, data[k]) for k in FACT_KEYS if data.get(k) not in (None, "")]
    classes = [(k, v) for k, v in data.items() if k.startswith("class_") and v not in (None, "")]
    others = [(k, v) for k, v in data.items()
              if k not in FACT_KEYS and not k.startswith("class_") and v not in (None, "")
              and k not in ("organizationName", "organizationId")]
    ctx = base_context(request, nav="schools")
    ctx.update({
        "school": school, "lead": lead, "data": data, "facts": facts, "classes": classes, "others": others,
        "lead_form": lead_form or LeadForm(instance=lead or Lead(school=school)),
        "act_form": act_form or ActivityForm.blank(),
        "contact_form": contact_form or ContactForm(),
        "activities": list(lead.activities.select_related("created_by")[:100]) if lead else [],
        "people": list(school.people.all()),
        "stage": lead.stage if lead else "new",
        "today": timezone.localdate(),
    })
    return render(request, "crm/school.html", ctx, status=status)


@crm_required()
def school_page(request, org_id):
    return _render_school(request, _scoped_school(request, org_id))


@crm_required(edit=True)
@require_POST
def save_lead(request, org_id):
    school = _scoped_school(request, org_id)
    lead = services.get_lead(school)
    form = LeadForm(request.POST)          # no instance: validating must not change the saved lead
    if not form.is_valid():
        return _render_school(request, school, lead_form=form, status=400)
    activity_kind = form.cleaned_data.get("activity_kind") or ""
    if form.cleaned_data["stage"] == "dnc" and activity_kind in CONTACT_TYPES:
        form.add_error("activity_kind", "This school is marked 'Do not contact'. Change the stage first, or log a note.")
        return _render_school(request, school, lead_form=form, status=400)
    try:
        with transaction.atomic():
            changes = services.update_lead(lead, request.user, form.cleaned_data)
            if activity_kind:
                services.log_activity(
                    lead,
                    request.user,
                    activity_kind,
                    form.cleaned_data.get("activity_outcome") or "",
                    form.cleaned_data.get("activity_notes") or "",
                    form.cleaned_data.get("activity_occurred_at") or timezone.now(),
                )
    except services.RuleError as e:
        form.add_error(None, str(e))
        return _render_school(request, school, lead_form=form, status=400)
    activity_saved = bool(activity_kind)
    if changes and activity_saved:
        message = "Lead updated and activity logged."
    elif changes:
        message = "Lead updated: " + "; ".join(changes)
    elif activity_saved:
        message = "Activity logged."
    else:
        message = "No changes to save."
    messages.success(request, message)
    return redirect("school_crm", org_id=org_id)


@crm_required(edit=True)
@require_POST
def add_activity(request, org_id):
    school = _scoped_school(request, org_id)
    lead = services.get_lead(school)
    form = ActivityForm(request.POST)
    if form.is_valid():
        d = form.cleaned_data
        if lead.stage == "dnc" and d["kind"] in CONTACT_TYPES:
            form.add_error(None, "This school is marked 'Do not contact'. Change the stage first, or log a note.")
        else:
            services.log_activity(lead, request.user, d["kind"], d["outcome"], d["notes"],
                                  d["occurred_at"], d["followup_date"], d["followup_action"])
            messages.success(request, "Activity logged.")
            return redirect("school_crm", org_id=org_id)
    return _render_school(request, school, act_form=form, status=400)


@crm_required(edit=True)
@require_POST
def add_contact(request, org_id):
    school = _scoped_school(request, org_id)
    form = ContactForm(request.POST)
    if not form.is_valid():
        return _render_school(request, school, contact_form=form, status=400)
    with transaction.atomic():
        c = form.save(commit=False)
        c.school = school
        if c.is_primary:
            school.people.update(is_primary=False)
        c.save()
    messages.success(request, f"Added {c.name}.")
    return redirect("school_crm", org_id=org_id)


@crm_required(edit=True)
@require_POST
def delete_contact(request, pk):
    contact = get_object_or_404(Contact, pk=pk, school__in=visible_schools(request.user))
    org_id = contact.school.organization_id
    contact.delete()
    messages.success(request, "Contact removed.")
    return redirect("school_crm", org_id=org_id)


# ---------- dashboard ----------
@crm_required()
def dashboard(request):
    user, today = request.user, timezone.localdate()
    schools = visible_schools(user)
    total = schools.count()
    leads = _scoped_leads(user)

    by_stage = {r["stage"]: r["n"] for r in leads.values("stage").annotate(n=Count("id"))}
    worked = sum(n for s, n in by_stage.items() if s != "new")
    by_stage["new"] = max(total - worked, 0)            # schools nobody has touched yet
    biggest = max(by_stage.values() or [1]) or 1
    funnel = [{"key": k, "label": lbl, "n": by_stage.get(k, 0),
               "pct": round(100 * by_stage.get(k, 0) / biggest)} for k, lbl in STAGES]

    open_leads = leads.exclude(stage__in=TERMINAL)
    month_start = timezone.localtime().replace(day=1, hour=0, minute=0, second=0, microsecond=0)
    week_ago = timezone.now() - timedelta(days=7)
    ctx = base_context(request, nav="dashboard")
    ctx.update({
        "total": total, "worked": worked,
        "contacted": leads.filter(last_contacted_at__isnull=False).count(),
        "overdue": open_leads.filter(next_followup__lt=today).count(),
        "due_today": open_leads.filter(next_followup=today).count(),
        "my_leads_total": leads.filter(owner=user).count(),
        "won_month": leads.filter(stage="won", stage_changed_at__gte=month_start).count(),
        "pipeline": open_leads.aggregate(s=Sum("deal_value"))["s"] or 0,
        "funnel": funnel,
        "my_due": open_leads.filter(owner=user, next_followup__lte=today)
                  .select_related("school", "owner").order_by("next_followup")[:10],
        "hot_untouched": open_leads.filter(priority="high", last_contacted_at__isnull=True)
                         .select_related("school", "owner")[:10],
        "recent": Activity.objects.filter(lead__school__in=schools).select_related("lead__school", "created_by")[:12],
        "team": (Activity.objects.filter(lead__school__in=schools, created_at__gte=week_ago)
                 .exclude(kind__in=["stage", "system"]).values("created_by__username")
                 .annotate(n=Count("id")).order_by("-n")[:8]),
        "today": today,
    })
    return render(request, "crm/dashboard.html", ctx)


# ---------- follow-ups ----------
@crm_required()
def followups(request):
    user, today = request.user, timezone.localdate()
    mode = "all" if request.GET.get("owner") == "all" else "mine"
    qs = (_scoped_leads(user).exclude(stage__in=TERMINAL).filter(next_followup__isnull=False)
          .select_related("school", "owner"))
    if mode == "mine":
        qs = qs.filter(owner=user)
    items = sorted(qs[:1000], key=lambda l: (l.next_followup, PRIORITY_RANK[l.priority]))
    week = today + timedelta(days=7)
    groups = [("Overdue", "overdue", [l for l in items if l.next_followup < today]),
              ("Today", "today", [l for l in items if l.next_followup == today]),
              ("Next 7 days", "week", [l for l in items if today < l.next_followup <= week]),
              ("Later", "later", [l for l in items if l.next_followup > week])]
    ctx = base_context(request, nav="followups")
    ctx.update({"groups": groups, "mode": mode, "today": today, "total_items": len(items)})
    return render(request, "crm/followups.html", ctx)


@crm_required()
def my_leads(request):
    user, today = request.user, timezone.localdate()
    leads = (_scoped_leads(user).filter(owner=user).select_related("school", "owner")
             .order_by("-priority", "next_followup", "school__name"))
    open_leads = leads.exclude(stage__in=TERMINAL)
    ctx = base_context(request, nav="myleads")
    ctx.update({
        "my_leads": list(leads[:500]),
        "active_leads": list(open_leads[:500]),
        "total_items": leads.count(),
        "active_items": open_leads.count(),
        "due_today": open_leads.filter(next_followup=today).count(),
        "overdue": open_leads.filter(next_followup__lt=today).count(),
        "won_count": leads.filter(stage="won").count(),
        "today": today,
    })
    return render(request, "crm/myleads.html", ctx)


# ---------- bulk update from the school list (JSON) ----------
@require_POST
def bulk_update(request):
    user = request.user
    if not user.is_authenticated:
        return JsonResponse({"detail": "Login required"}, status=401)
    if crm_level(user) != "edit":
        return JsonResponse({"detail": "You don't have permission to edit the CRM."}, status=403)
    try:
        body = json.loads(request.body or b"{}")
        ids = [int(i) for i in body.get("ids", [])]
    except (ValueError, TypeError):
        return JsonResponse({"detail": "Bad request."}, status=400)
    if not ids:
        return JsonResponse({"detail": "No schools selected."}, status=400)
    if len(ids) > BULK_LIMIT:
        return JsonResponse({"detail": f"Too many schools at once (max {BULK_LIMIT:,}). Narrow the filters."}, status=400)

    stage, priority, owner_in = body.get("stage") or None, body.get("priority") or None, body.get("owner")
    if stage and stage not in STAGE_LABELS:
        return JsonResponse({"detail": "Unknown stage."}, status=400)
    if priority and priority not in PRIORITY_LABELS:
        return JsonResponse({"detail": "Unknown priority."}, status=400)
    lost_reason = (body.get("lost_reason") or "").strip()
    if stage == "lost" and not lost_reason:
        return JsonResponse({"detail": "Say why these leads were lost."}, status=400)
    set_owner, new_owner = owner_in not in (None, ""), None
    if set_owner and owner_in != "none":
        new_owner = User.objects.filter(pk=owner_in, is_active=True).first()
        if not new_owner or crm_level(new_owner) != "edit":
            return JsonResponse({"detail": "That person can't own leads."}, status=400)
    if not (stage or priority or set_owner):
        return JsonResponse({"detail": "Choose something to change."}, status=400)

    schools = list(visible_schools(user).filter(organization_id__in=ids))   # only schools in scope
    existing = {l.school_id: l for l in Lead.objects.filter(school__in=schools).select_related("owner")}
    to_create = [Lead(school=s) for s in schools if s.pk not in existing]
    with transaction.atomic():
        Lead.objects.bulk_create(to_create)
        for l in Lead.objects.filter(school__in=[x.school for x in to_create]).select_related("owner"):
            existing[l.school_id] = l
        updated, skipped, logs = [], 0, []
        for s in schools:
            lead, notes = existing[s.pk], []
            if set_owner and new_owner != lead.owner:
                try:
                    services.check_owner_change(user, lead, new_owner)
                except services.RuleError:
                    skipped += 1
                    continue
                notes.append(f"Owner: {lead.owner or 'nobody'} → {new_owner or 'nobody'}")
                lead.owner = new_owner
            if priority and priority != lead.priority:
                notes.append(f"Priority: {PRIORITY_LABELS.get(lead.priority or '', '-')} → {PRIORITY_LABELS.get(priority or '', '-')}")
                lead.priority = priority
            if stage and stage != lead.stage:
                notes.append(f"Stage: {STAGE_LABELS[lead.stage]} → {STAGE_LABELS[stage]}")
                lead.stage, lead.stage_changed_at = stage, timezone.now()
                lead.lost_reason = lost_reason if stage == "lost" else ""
                if stage in TERMINAL:
                    lead.next_followup, lead.next_action = None, ""
            if notes:
                updated.append(lead)
                logs.append(Activity(lead=lead, kind="stage" if stage else "system", created_by=user,
                                     notes="Bulk update. " + "; ".join(notes)))
        Lead.objects.bulk_update(updated, ["owner", "priority", "stage", "stage_changed_at",
                                           "lost_reason", "next_followup", "next_action"])
        Activity.objects.bulk_create(logs)
        transaction.on_commit(invalidate_finder_cache)
    return JsonResponse({"updated": len(updated), "unchanged": len(schools) - len(updated) - skipped,
                         "skipped_not_allowed": skipped, "not_visible": len(set(ids)) - len(schools)})
