import json
from datetime import timedelta
from decimal import Decimal, InvalidOperation

from django.contrib import messages
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import ValidationError
from django.core.mail import send_mail
from django.db.models import Count, Q, Sum
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from django.utils.crypto import get_random_string

from . import mail, records
from .models import (ActivityLog, EmailSettings, CalendarEvent, Client, Invoice, Lead,
                     LoginEvent, Message, Notification, OngoingService,
                     Payment, PortfolioCategory, PortfolioProject,
                     Milestone, PortfolioImage, PricingPackage, ProcessStep,
                     Project, ProjectFile, ProjectUpdate, QuoteItem,
                     Quote, Service, SiteSettings, Statistic, Task,
                     Testimonial, Ticket, TicketReply, Website,
                     WebsiteActivity, WebsiteCredential)

ZERO = Decimal("0")


# ─────────────────────────── helpers ───────────────────────────

def staff_required(view):
    return user_passes_test(lambda u: u.is_active and u.is_staff, login_url="admin_login")(view)


def get_client(request):
    client = getattr(request.user, "client", None)
    if client is None:
        raise Http404("No client account for this user.")
    return client


def post(request, name, default=""):
    return request.POST.get(name, default).strip()


def post_id(request, name):
    """Return a POSTed primary key, or None when the field was left blank."""
    value = post(request, name)
    return value or None


def to_decimal(value):
    try:
        return Decimal(str(value).replace(",", ""))
    except (InvalidOperation, ValueError):
        return None


def to_date(value):
    return value or None


def log(request, description):
    ActivityLog.objects.create(
        user=request.user if request.user.is_authenticated else None,
        description=description,
    )


def notify_owner(kind, body, url=""):
    for owner in User.objects.filter(is_staff=True, is_active=True):
        Notification.objects.create(user=owner, kind=kind, body=body, url=url)


def notify_client(client, kind, body, url=""):
    Notification.objects.create(user=client.user, kind=kind, body=body, url=url)


def sum_amount(queryset, field="amount"):
    return queryset.aggregate(total=Sum(field))["total"] or ZERO


# ─────────────────────────── public site ───────────────────────────

def home(request):
    return render(request, "public/home.html", {
        "nav": "home",
        "featured": PortfolioProject.objects.filter(published=True, featured=True)[:6],
        "services": Service.objects.filter(published=True),
        "stats": Statistic.objects.all(),
        "testimonials": Testimonial.objects.filter(published=True),
        "steps": ProcessStep.objects.all(),
        "websites": Website.objects.filter(status=Website.ONLINE)[:6],
    })


def about(request):
    return render(request, "public/about.html", {
        "nav": "about",
        "products": PortfolioProject.objects.filter(published=True, featured=True)[:4],
    })


def services(request):
    return render(request, "public/services.html", {
        "nav": "services",
        "services": Service.objects.filter(published=True),
    })


def pricing(request):
    return render(request, "public/pricing.html", {
        "nav": "pricing",
        "packages": PricingPackage.objects.filter(published=True),
        "ongoing": OngoingService.objects.all(),
    })


def portfolio(request):
    category = request.GET.get("cat", "")
    projects = PortfolioProject.objects.filter(published=True)
    if category:
        projects = projects.filter(category__slug=category)
    return render(request, "public/portfolio.html", {
        "nav": "portfolio",
        "projects": projects,
        "categories": PortfolioCategory.objects.all(),
        "active_category": category,
    })


def portfolio_detail(request, slug):
    project = get_object_or_404(PortfolioProject, slug=slug, published=True)
    return render(request, "public/portfolio_detail.html", {"nav": "portfolio", "project": project})


CONTACT_FIELDS = ["name", "email", "phone", "company", "project_type",
                  "has_website", "description", "budget", "timeline"]

WIZARD_FIELDS = ["name", "company", "email", "phone", "project_type", "description",
                 "features", "budget", "timeline", "references", "branding"]


def contact(request):
    values, errors = {}, {}
    if request.method == "POST":
        values = {f: post(request, f) for f in CONTACT_FIELDS}
        if not values["name"]:
            errors["name"] = "Tell me your name."
        if "@" not in values["email"]:
            errors["email"] = "Enter an email I can reply to."
        if not values["description"]:
            errors["description"] = "Describe what you need built."
        if not errors:
            lead = Lead.objects.create(source="contact", **values)
            notify_owner("Lead", f"New enquiry from {lead.name} ({lead.reference}).", "/admin/leads/")
            mail.notify(
                "new_lead",
                f"New enquiry — {lead.name}",
                f"{lead.name} ({lead.email}) asked about "
                f"{lead.project_type or 'a project'}.\n\n"
                f"Budget: {lead.budget or 'not given'}\n"
                f"Timeline: {lead.timeline or 'not given'}\n\n"
                f"{lead.description}\n\n"
                f"Reference {lead.reference}.",
                SiteSettings.load().contact_email,
                reply_to=lead.email,
            )
            return render(request, "public/contact.html", {"nav": "contact", "sent": lead})
    return render(request, "public/contact.html", {
        "nav": "contact", "values": values, "errors": errors,
    })


def start_project(request):
    values, errors = {}, {}
    if request.method == "POST":
        values = {f: post(request, f) for f in WIZARD_FIELDS}
        if not values["name"]:
            errors["name"] = "Tell me your name."
        if "@" not in values["email"]:
            errors["email"] = "Enter an email I can reply to."
        if not values["project_type"]:
            errors["project_type"] = "Pick what you want built."
        if not errors:
            lead = Lead.objects.create(
                source="start-project",
                target_date=to_date(post(request, "target_date")),
                **values,
            )
            notify_owner("Lead", f"New project request from {lead.name} ({lead.reference}).", "/admin/leads/")
            mail.notify(
                "new_lead",
                f"New project request — {lead.name}",
                f"{lead.name} ({lead.email}) wants {lead.project_type or 'a project'}.\n\n"
                f"Budget: {lead.budget or 'not given'}\n"
                f"Timeline: {lead.timeline or 'not given'}\n"
                f"Features: {lead.features or 'none listed'}\n\n"
                f"{lead.description}\n\n"
                f"Reference {lead.reference}.",
                SiteSettings.load().contact_email,
                reply_to=lead.email,
            )
            return render(request, "public/start_project.html", {"sent": lead})
    return render(request, "public/start_project.html", {"values": values, "errors": errors})


def register(request):
    values, errors = {}, {}
    if request.method == "POST":
        values = {f: post(request, f) for f in
                  ["first_name", "last_name", "email", "phone", "company"]}
        password = request.POST.get("password", "")
        confirm = request.POST.get("password_confirm", "")
        if not values["first_name"]:
            errors["first_name"] = "Enter your name."
        if "@" not in values["email"]:
            errors["email"] = "Enter a valid email."
        elif User.objects.filter(email__iexact=values["email"]).exists():
            errors["email"] = "An account already uses that email. Log in instead."
        if password != confirm:
            errors["password_confirm"] = "Both passwords must match."
        else:
            try:
                validate_password(password)
            except ValidationError as exc:
                errors["password"] = " ".join(exc.messages)
        if not errors:
            user = User.objects.create_user(
                username=values["email"],
                email=values["email"],
                password=password,
                first_name=values["first_name"],
                last_name=values["last_name"],
            )
            Client.objects.create(user=user, phone=values["phone"], company=values["company"])
            login(request, user)
            return redirect("client_dashboard")
    return render(request, "registration/register.html", {"values": values, "errors": errors})


# ─────────────────────────── client portal ───────────────────────────

@login_required
def client_dashboard(request):
    if not hasattr(request.user, "client") and request.user.is_staff:
        return redirect("admin_dashboard")
    client = get_client(request)
    projects = client.projects.all()
    active = [p for p in projects if p.status in Project.OPEN_STATUSES]
    invoices = client.invoices.exclude(status=Invoice.DRAFT)
    next_due = next((i for i in invoices.order_by("due_on") if i.balance > 0), None)
    return render(request, "client/dashboard.html", {
        "nav": "dashboard",
        "client": client,
        "projects": projects,
        "active_projects": active,
        "websites": client.websites.all(),
        "total_paid": sum((p.amount_paid for p in projects), ZERO),
        "total_balance": sum((p.balance for p in projects), ZERO),
        "updates": ProjectUpdate.objects.filter(project__client=client, client_visible=True)[:5],
        "open_tickets": client.tickets.exclude(status=Ticket.RESOLVED)[:5],
        "next_due": next_due,
    })


@login_required
def client_projects(request):
    client = get_client(request)
    return render(request, "client/projects.html", {
        "nav": "projects", "client": client, "projects": client.projects.all(),
    })


@login_required
def client_project(request, slug):
    client = get_client(request)
    project = get_object_or_404(Project, slug=slug, client=client)
    return render(request, "client/project.html", {
        "nav": "projects",
        "client": client,
        "project": project,
        "milestones": project.milestones.all(),
        "files": project.files.filter(client_visible=True),
        "updates": project.updates.filter(client_visible=True),
    })


@login_required
def client_websites(request):
    client = get_client(request)
    return render(request, "client/websites.html", {
        "nav": "websites", "client": client, "websites": client.websites.all(),
    })


@login_required
def client_invoices(request):
    client = get_client(request)
    invoices = client.invoices.exclude(status=Invoice.DRAFT)
    return render(request, "client/invoices.html", {
        "nav": "invoices",
        "client": client,
        "invoices": invoices,
        "total_paid": sum((i.amount_paid for i in invoices), ZERO),
        "total_outstanding": sum((i.balance for i in invoices), ZERO),
    })


@login_required
def client_invoice_pdf(request, number):
    client = get_client(request)
    invoice = get_object_or_404(Invoice, number=number, client=client)
    return render(request, "client/invoice_print.html", {
        "nav": "invoices", "invoice": invoice, "client": client,
    })


@login_required
def client_payments(request):
    client = get_client(request)
    projects = client.projects.all()
    errors = {}
    if request.method == "POST":
        project = projects.filter(pk=post_id(request, "project")).first() if post_id(request, "project") else None
        amount = to_decimal(post(request, "amount"))
        method = post(request, "method") or Payment.TRANSFER
        if project is None:
            errors["project"] = "Pick which project this payment is for."
        if amount is None or amount <= 0:
            errors["amount"] = "Enter an amount greater than zero."
        if not errors:
            payment = Payment.objects.create(
                client=client, project=project, amount=amount, method=method,
                reference="PAY-" + get_random_string(10).upper(), status=Payment.PENDING,
            )
            notify_owner("Finance",
                         f"{client.full_name} started a payment of {payment.amount} on {project.name}.")
            mail.notify(
                "payment_received",
                f"Payment started — {client.full_name}",
                f"{client.full_name} started a payment of {payment.amount} "
                f"on {project.name}.\nReference {payment.reference}.",
                SiteSettings.load().contact_email,
            )
            messages.success(request, "Payment started. You will get a receipt by email once it clears.")
            return redirect("client_payments")
    return render(request, "client/payments.html", {
        "nav": "payments",
        "client": client,
        "payments": client.payments.all(),
        "projects": projects,
        "methods": Payment.METHOD_CHOICES,
        "errors": errors,
        "total_cost": sum((p.budget for p in projects), ZERO),
        "total_paid": sum((p.amount_paid for p in projects), ZERO),
        "total_balance": sum((p.balance for p in projects), ZERO),
    })


@login_required
def client_messages(request):
    client = get_client(request)
    if request.method == "POST":
        body = post(request, "body")
        if body:
            Message.objects.create(
                client=client,
                project=client.projects.filter(pk=post_id(request, "project")).first() if post_id(request, "project") else None,
                sender=request.user,
                body=body,
                attachment=request.FILES.get("attachment"),
            )
            notify_owner("Messages", f"New message from {client.full_name}.")
            mail.notify(
                "new_message",
                f"Message from {client.full_name}",
                f"{body}\n\n—\nReply at /admin/messages/?client={client.pk}",
                SiteSettings.load().contact_email,
                reply_to=client.user.email,
            )
        return redirect("client_messages")
    client.messages.filter(read=False).exclude(sender=request.user).update(read=True)
    return render(request, "client/messages.html", {
        "nav": "messages",
        "client": client,
        "thread": client.messages.all(),
        "projects": client.projects.all(),
    })


@login_required
def client_support(request):
    client = get_client(request)
    values, errors = {}, {}
    if request.method == "POST":
        values = {"subject": post(request, "subject"), "message": post(request, "message")}
        priority = post(request, "priority") or Ticket.NORMAL
        if not values["subject"]:
            errors["subject"] = "Give the ticket a subject."
        if not values["message"]:
            errors["message"] = "Describe what went wrong."
        if not errors:
            ticket = Ticket.objects.create(
                client=client,
                project=client.projects.filter(pk=post_id(request, "project")).first() if post_id(request, "project") else None,
                subject=values["subject"],
                message=values["message"],
                priority=priority,
                attachment=request.FILES.get("attachment"),
            )
            notify_owner("Support", f"New {ticket.get_priority_display().lower()} ticket: {ticket.subject}.")
            mail.notify(
                "ticket_opened",
                f"[{ticket.get_priority_display()}] {ticket.reference} — {ticket.subject}",
                f"{client.full_name} at {client.company or 'no company'} opened a ticket.\n\n"
                f"{ticket.message}",
                SiteSettings.load().contact_email,
                reply_to=client.user.email,
            )
            messages.success(request,
                             f"Ticket {ticket.reference} created. You will get an email when it is answered.")
            return redirect("client_support")
    return render(request, "client/support.html", {
        "nav": "support",
        "client": client,
        "tickets": client.tickets.all(),
        "projects": client.projects.all(),
        "priorities": Ticket.PRIORITY_CHOICES,
        "values": values,
        "errors": errors,
    })


@login_required
def client_ticket(request, reference):
    client = get_client(request)
    ticket = get_object_or_404(Ticket, reference=reference, client=client)
    if request.method == "POST":
        body = post(request, "body")
        if body:
            TicketReply.objects.create(ticket=ticket, author=request.user, body=body)
            if ticket.status == Ticket.RESOLVED:
                ticket.status = Ticket.OPEN
                ticket.save()
        return redirect("client_ticket", reference=reference)
    return render(request, "client/ticket.html", {
        "nav": "support", "client": client, "ticket": ticket,
    })


@login_required
def client_files(request):
    client = get_client(request)
    return render(request, "client/files.html", {
        "nav": "files", "client": client, "projects": client.projects.prefetch_related("files"),
    })


PREF_FIELDS = ["notify_project_updates", "notify_invoices", "notify_payments",
               "notify_messages", "notify_tickets", "notify_renewals"]


@login_required
def client_account(request):
    client = get_client(request)
    errors = {}
    if request.method == "POST" and "save_profile" in request.POST:
        first_name = post(request, "first_name")
        email = post(request, "email")
        if not first_name:
            errors["first_name"] = "Enter your name."
        if "@" not in email:
            errors["email"] = "Enter a valid email."
        elif User.objects.filter(email__iexact=email).exclude(pk=client.user.pk).exists():
            errors["email"] = "Another account already uses that email."
        if not errors:
            user = client.user
            user.first_name = first_name
            user.last_name = post(request, "last_name")
            user.email = email
            user.save()
            client.company = post(request, "company")
            client.phone = post(request, "phone")
            client.address = post(request, "address")
            client.save()
            messages.success(request, "Details saved.")
            return redirect("client_account")
    elif request.method == "POST" and "save_prefs" in request.POST:
        for field in PREF_FIELDS:
            setattr(client, field, field in request.POST)
        client.save()
        messages.success(request, "Preferences saved.")
        return redirect("client_account")
    return render(request, "client/account.html", {
        "nav": "account", "client": client, "errors": errors,
    })


# ─────────────────────────── admin panel ───────────────────────────

@staff_required
def admin_dashboard(request):
    projects = Project.objects.select_related("client")
    active = projects.filter(status__in=Project.OPEN_STATUSES)
    invoices = Invoice.objects.exclude(status=Invoice.DRAFT)
    soon = timezone.localdate() + timedelta(days=60)
    return render(request, "adminpanel/dashboard.html", {
        "nav": "dashboard",
        "client_count": Client.objects.count(),
        "active_count": active.count(),
        "completed_count": projects.filter(status__in=[Project.COMPLETED, Project.MAINTENANCE]).count(),
        "website_count": Website.objects.count(),
        "revenue": sum_amount(Payment.objects.filter(status=Payment.CONFIRMED)),
        "outstanding": sum((i.balance for i in invoices), ZERO),
        "new_leads": Lead.objects.filter(status__in=[Lead.NEW, Lead.CONTACTED]).count(),
        "open_tickets": Ticket.objects.exclude(status=Ticket.RESOLVED).count(),
        "active_projects": active[:6],
        "leads": Lead.objects.all()[:5],
        "payments": Payment.objects.all()[:5],
        "tickets": Ticket.objects.exclude(status=Ticket.RESOLVED)[:5],
        "activity": ActivityLog.objects.all()[:6],
        "deadlines": projects.filter(deadline__isnull=False).exclude(
            status__in=[Project.COMPLETED, Project.CANCELLED]).order_by("deadline")[:5],
        "overdue_invoices": [i for i in invoices if i.is_overdue],
        "expiring": Website.objects.filter(domain_expiry__lte=soon).order_by("domain_expiry"),
    })


@staff_required
def admin_leads(request):
    status = request.GET.get("status", "")
    query = request.GET.get("q", "")
    leads = Lead.objects.all()
    if status:
        leads = leads.filter(status=status)
    if query:
        leads = leads.filter(Q(name__icontains=query) | Q(company__icontains=query)
                             | Q(email__icontains=query))
    return render(request, "adminpanel/leads.html", {
        "nav": "leads",
        "leads": leads,
        "statuses": Lead.STATUS_CHOICES,
        "active_status": status,
        "query": query,
        "counts": {
            "total": Lead.objects.count(),
            "new": Lead.objects.filter(status=Lead.NEW).count(),
            "progress": Lead.objects.filter(
                status__in=[Lead.CONTACTED, Lead.QUOTED, Lead.NEGOTIATING]).count(),
            "won": Lead.objects.filter(status=Lead.WON).count(),
        },
    })


@staff_required
def admin_lead(request, pk):
    lead = get_object_or_404(Lead, pk=pk)
    if request.method == "POST":
        lead.status = post(request, "status") or lead.status
        lead.notes = post(request, "notes")
        lead.save()
        log(request, f"Lead {lead.reference} moved to {lead.get_status_display()}.")
        messages.success(request, "Lead saved.")
        return redirect("admin_lead", pk=pk)
    return render(request, "adminpanel/lead.html", {
        "nav": "leads", "lead": lead, "statuses": Lead.STATUS_CHOICES,
    })


@staff_required
def admin_clients(request):
    query = request.GET.get("q", "")
    clients = Client.objects.select_related("user").annotate(
        project_count=Count("projects", distinct=True),
        website_count=Count("websites", distinct=True),
        ticket_count=Count("tickets", distinct=True),
    )
    if query:
        clients = clients.filter(
            Q(user__first_name__icontains=query) | Q(user__last_name__icontains=query)
            | Q(company__icontains=query) | Q(user__email__icontains=query))
    return render(request, "adminpanel/clients.html", {
        "nav": "clients", "clients": clients, "query": query,
    })


@staff_required
def admin_client(request, pk):
    client = get_object_or_404(Client, pk=pk)
    return render(request, "adminpanel/client.html", {
        "nav": "clients",
        "client": client,
        "projects": client.projects.all(),
        "websites": client.websites.all(),
        "invoices": client.invoices.all(),
        "tickets": client.tickets.all(),
    })


@staff_required
def admin_quotes(request):
    return render(request, "adminpanel/quotes.html", {
        "nav": "quotes", "quotes": Quote.objects.prefetch_related("items"),
    })


@staff_required
def admin_quote_accept(request, pk):
    quote = get_object_or_404(Quote, pk=pk)
    quote.status = Quote.ACCEPTED
    quote.save()
    log(request, f"Quotation {quote.number} marked accepted.")
    return redirect("admin_quotes")


@staff_required
def admin_projects(request):
    status = request.GET.get("status", "")
    query = request.GET.get("q", "")
    projects = Project.objects.select_related("client")
    if status:
        projects = projects.filter(status=status)
    if query:
        projects = projects.filter(Q(name__icontains=query) | Q(client__company__icontains=query))
    all_projects = Project.objects.all()
    return render(request, "adminpanel/projects.html", {
        "nav": "projects",
        "projects": projects,
        "statuses": Project.STATUS_CHOICES,
        "active_status": status,
        "query": query,
        "total_value": sum((p.budget for p in all_projects), ZERO),
        "total_balance": sum((p.balance for p in all_projects), ZERO),
        "in_progress": all_projects.filter(status__in=Project.OPEN_STATUSES).count(),
    })


@staff_required
def admin_project(request, slug):
    project = get_object_or_404(Project, slug=slug)
    if request.method == "POST":
        project.status = post(request, "status") or project.status
        progress = post(request, "progress")
        if progress.isdigit():
            project.progress = min(100, int(progress))
        project.deadline = to_date(post(request, "deadline"))
        project.internal_notes = post(request, "internal_notes")
        project.save()
        log(request, f"Project {project.reference} updated to {project.get_status_display()}.")
        notify_client(project.client, "Project",
                      f"{project.name} is now at {project.get_status_display()}.")
        mail.notify(
            "project_status",
            f"{project.name} — {project.get_status_display()}",
            f"Your project is now at {project.get_status_display()}, "
            f"{project.progress}% complete.\n\n"
            f"Track it here: /client/projects/{project.slug}/",
            project.client.user.email,
            client=project.client,
        )
        messages.success(request, "Project saved.")
        return redirect("admin_project", slug=slug)
    return render(request, "adminpanel/project.html", {
        "nav": "projects",
        "project": project,
        "statuses": Project.STATUS_CHOICES,
        "milestones": project.milestones.all(),
        "tasks": project.tasks.exclude(status=Task.COMPLETED),
        "files": project.files.all(),
        "updates": project.updates.all(),
    })


@staff_required
def admin_websites(request):
    query = request.GET.get("q", "")
    websites = Website.objects.select_related("client")
    if query:
        websites = websites.filter(Q(name__icontains=query) | Q(domain__icontains=query))
    soon = timezone.localdate() + timedelta(days=60)
    return render(request, "adminpanel/websites.html", {
        "nav": "websites",
        "websites": websites,
        "query": query,
        "online": Website.objects.filter(status=Website.ONLINE).count(),
        "on_maintenance": Website.objects.filter(maintenance_status=Website.MAINT_ACTIVE).count(),
        "renewals": Website.objects.filter(domain_expiry__lte=soon).count(),
    })


@staff_required
def admin_website(request, slug):
    website = get_object_or_404(Website, slug=slug)
    if request.method == "POST":
        website.maintenance_notes = post(request, "maintenance_notes")
        website.save()
        messages.success(request, "Maintenance notes saved.")
        return redirect(f"/admin/websites/{slug}/?tab=maintenance")
    return render(request, "adminpanel/website.html", {
        "nav": "websites", "website": website, "tab": request.GET.get("tab", "overview"),
    })


WEBSITE_TEXT_FIELDS = [
    "name", "slug", "domain", "status", "registrar", "dns_notes",
    "hosting_provider", "hosting_plan", "server_reference", "deployment_notes",
    "backend", "frontend", "database", "apis", "ssl_provider",
    "maintenance_status", "maintenance_notes",
]
WEBSITE_DATE_FIELDS = [
    "built_on", "last_deployment", "domain_expiry", "hosting_expiry",
    "ssl_expiry", "last_update", "next_maintenance",
]


def slugify_name(value):
    import re
    return re.sub(r"^-|-$", "", re.sub(r"[^a-z0-9]+", "-", value.lower().strip()))


@staff_required
def admin_website_form(request, slug=None):
    website = get_object_or_404(Website, slug=slug) if slug else None
    errors = {}

    if request.method == "POST":
        values = {f: post(request, f) for f in WEBSITE_TEXT_FIELDS}
        values.update({f: post(request, f) for f in WEBSITE_DATE_FIELDS})
        values["client"] = post(request, "client")
        values["project"] = post(request, "project")
        values["auto_renew"] = "auto_renew" in request.POST

        if not values["name"]:
            errors["name"] = "Give the website a name."
        client = Client.objects.filter(pk=values["client"]).first() if values["client"] else None
        if client is None:
            errors["client"] = "Pick which client this website belongs to."
        if not values["domain"]:
            errors["domain"] = "Enter the domain."

        candidate = values["slug"] or slugify_name(values["name"])
        clash = Website.objects.filter(slug=candidate)
        if website:
            clash = clash.exclude(pk=website.pk)
        if candidate and clash.exists():
            errors["slug"] = "Another website already uses that slug."
        values["slug"] = candidate

        if not errors:
            obj = website or Website()
            for field in WEBSITE_TEXT_FIELDS:
                setattr(obj, field, values[field])
            for field in WEBSITE_DATE_FIELDS:
                setattr(obj, field, to_date(values[field]))
            obj.client = client
            obj.project = Project.objects.filter(pk=values["project"]).first() if values["project"] else None
            obj.auto_renew = values["auto_renew"]
            obj.save()
            log(request, f"Website {obj.name} {'updated' if website else 'added'}.")
            messages.success(request, "Website saved." if website else f"{obj.name} added.")
            return redirect("admin_website", slug=obj.slug)

    elif website:
        values = {f: getattr(website, f) or "" for f in WEBSITE_TEXT_FIELDS}
        values.update({f: (getattr(website, f).isoformat() if getattr(website, f) else "")
                       for f in WEBSITE_DATE_FIELDS})
        values["client"] = str(website.client_id)
        values["project"] = str(website.project_id or "")
        values["auto_renew"] = website.auto_renew
    else:
        values = {f: "" for f in WEBSITE_TEXT_FIELDS + WEBSITE_DATE_FIELDS}
        values.update({"client": "", "project": "", "auto_renew": True,
                       "status": Website.ONLINE, "maintenance_status": Website.MAINT_NONE})

    return render(request, "adminpanel/website_form.html", {
        "nav": "websites",
        "website": website,
        "values": values,
        "errors": errors,
        "clients": Client.objects.select_related("user"),
        "projects": Project.objects.select_related("client"),
        "statuses": Website.STATUS_CHOICES,
        "maintenance_choices": Website.MAINT_CHOICES,
    })


@staff_required
def admin_website_credential(request, slug):
    """Add a credential reference from the website's Credentials tab."""
    website = get_object_or_404(Website, slug=slug)
    label = post(request, "label")
    reference = post(request, "vault_reference")
    if label:
        WebsiteCredential.objects.create(
            website=website, label=label, vault_reference=reference,
            held_by_client="held_by_client" in request.POST,
        )
        log(request, f"Credential reference added on {website.name}.")
        messages.success(request, "Credential reference saved. The secret itself stays in your vault.")
    return redirect(f"/admin/websites/{slug}/?tab=credentials")


@staff_required
def admin_website_activity(request, slug):
    """Log an activity entry from the website's Activity tab."""
    website = get_object_or_404(Website, slug=slug)
    description = post(request, "description")
    if description:
        WebsiteActivity.objects.create(
            website=website, description=description,
            happened_on=to_date(post(request, "happened_on")) or timezone.localdate(),
        )
        messages.success(request, "Activity logged.")
    return redirect(f"/admin/websites/{slug}/?tab=activity")


@staff_required
def admin_domains(request):
    soon = timezone.localdate() + timedelta(days=60)
    return render(request, "adminpanel/domains.html", {
        "nav": "domains",
        "websites": Website.objects.order_by("domain_expiry"),
        "expiring": Website.objects.filter(domain_expiry__lte=soon, auto_renew=False),
    })


@staff_required
def admin_hosting(request):
    return render(request, "adminpanel/hosting.html", {
        "nav": "hosting", "websites": Website.objects.order_by("hosting_expiry"),
    })


@staff_required
def admin_maintenance(request):
    active = Website.objects.filter(maintenance_status=Website.MAINT_ACTIVE)
    return render(request, "adminpanel/maintenance.html", {
        "nav": "maintenance",
        "websites": Website.objects.all(),
        "active_count": active.count(),
        "lapsed_count": Website.objects.exclude(maintenance_status=Website.MAINT_ACTIVE).count(),
        "recurring": active.count() * 25000,
    })


@staff_required
def admin_invoices(request):
    status = request.GET.get("status", "")
    invoices = Invoice.objects.select_related("client")
    if status:
        invoices = invoices.filter(status=status)
    issued = Invoice.objects.exclude(status=Invoice.DRAFT)
    return render(request, "adminpanel/invoices.html", {
        "nav": "invoices",
        "invoices": invoices,
        "statuses": Invoice.STATUS_CHOICES,
        "active_status": status,
        "collected": sum((i.amount_paid for i in issued), ZERO),
        "outstanding": sum((i.balance for i in issued), ZERO),
        "overdue": sum((i.balance for i in issued if i.is_overdue), ZERO),
    })


@staff_required
def admin_invoice(request, number):
    invoice = get_object_or_404(Invoice, number=number)
    return render(request, "adminpanel/invoice.html", {"nav": "invoices", "invoice": invoice})


@staff_required
def admin_payments(request):
    payments = Payment.objects.select_related("client", "project")
    month_start = timezone.localdate().replace(day=1)
    return render(request, "adminpanel/payments.html", {
        "nav": "payments",
        "payments": payments,
        "received": sum_amount(payments.filter(status=Payment.CONFIRMED)),
        "this_month": sum_amount(payments.filter(status=Payment.CONFIRMED, paid_on__gte=month_start)),
        "by_transfer": payments.filter(method=Payment.TRANSFER).count(),
        "failed": payments.filter(status=Payment.FAILED).count(),
    })


@staff_required
def admin_revenue(request):
    today = timezone.localdate()
    months = []
    for offset in range(5, -1, -1):
        month = (today.replace(day=1) - timedelta(days=offset * 30)).replace(day=1)
        total = sum_amount(Payment.objects.filter(
            status=Payment.CONFIRMED, paid_on__year=month.year, paid_on__month=month.month))
        months.append({"label": month.strftime("%b"), "total": total})
    peak = max([m["total"] for m in months], default=ZERO) or Decimal("1")
    for month in months:
        month["percent"] = int(month["total"] / peak * 100)
    collected = sum((m["total"] for m in months), ZERO)
    by_client = list(Payment.objects.filter(status=Payment.CONFIRMED)
                     .values("client__company", "client__user__first_name")
                     .annotate(total=Sum("amount")).order_by("-total"))
    for row in by_client:
        row["percent"] = int(row["total"] / (collected or Decimal("1")) * 100)
    return render(request, "adminpanel/revenue.html", {
        "nav": "revenue",
        "months": months,
        "total": collected,
        "best": max([m["total"] for m in months], default=ZERO),
        "average": collected / len(months) if months else ZERO,
        "by_client": by_client,
        "outstanding": [i for i in Invoice.objects.exclude(status=Invoice.DRAFT) if i.balance > 0],
    })


@staff_required
def admin_portfolio(request):
    return render(request, "adminpanel/portfolio.html", {
        "nav": "portfolio",
        "projects": PortfolioProject.objects.select_related("category"),
        "categories": PortfolioCategory.objects.annotate(count=Count("projects")),
    })


@staff_required
def admin_tickets(request):
    status = request.GET.get("status", "")
    tickets = Ticket.objects.select_related("client")
    if status:
        tickets = tickets.filter(status=status)
    return render(request, "adminpanel/tickets.html", {
        "nav": "tickets",
        "tickets": tickets,
        "statuses": Ticket.STATUS_CHOICES,
        "active_status": status,
        "open_count": Ticket.objects.filter(status=Ticket.OPEN).count(),
        "pending_count": Ticket.objects.filter(status=Ticket.PENDING).count(),
        "critical_count": Ticket.objects.filter(
            priority=Ticket.CRITICAL).exclude(status=Ticket.RESOLVED).count(),
        "resolved_count": Ticket.objects.filter(status=Ticket.RESOLVED).count(),
    })


@staff_required
def admin_ticket(request, reference):
    ticket = get_object_or_404(Ticket, reference=reference)
    if request.method == "POST":
        if "status" in request.POST:
            ticket.status = post(request, "status")
            ticket.save()
            notify_client(ticket.client, "Support",
                          f"Ticket {ticket.reference} is now {ticket.get_status_display().lower()}.")
        else:
            body = post(request, "body")
            if body:
                TicketReply.objects.create(ticket=ticket, author=request.user, body=body)
                notify_client(ticket.client, "Support",
                              f"Teniola replied to ticket {ticket.reference}.")
                mail.notify(
                    "ticket_reply",
                    f"Re: {ticket.reference} — {ticket.subject}",
                    f"{body}\n\n—\nReply in your portal: /client/support/{ticket.reference}/",
                    ticket.client.user.email,
                    client=ticket.client,
                )
        return redirect("admin_ticket", reference=reference)
    return render(request, "adminpanel/ticket.html", {"nav": "tickets", "ticket": ticket})


@staff_required
def admin_messages(request):
    clients = Client.objects.select_related("user")
    client = clients.filter(pk=request.GET.get("client")).first() or clients.first()
    if request.method == "POST" and client:
        body = post(request, "body")
        if body:
            Message.objects.create(
                client=client, sender=request.user, body=body,
                attachment=request.FILES.get("attachment"),
            )
            notify_client(client, "Messages", "Teniola sent you a message.")
            mail.notify(
                "new_message",
                "New message from Teniola",
                f"{body}\n\n—\nReply in your portal: /client/messages/",
                client.user.email,
                client=client,
            )
        return redirect(f"/admin/messages/?client={client.pk}")
    return render(request, "adminpanel/messages.html", {
        "nav": "messages",
        "clients": clients,
        "client": client,
        "thread": client.messages.all() if client else [],
    })


@staff_required
def admin_tasks(request):
    """The board. The quick-add box here takes a name, a project and a due date;
    anything more detailed goes through the full form."""
    if request.method == "POST":
        name = post(request, "name")
        if name:
            Task.objects.create(
                name=name,
                project=Project.objects.filter(pk=post_id(request, "project")).first()
                if post_id(request, "project") else None,
                priority=post(request, "priority") or Task.NORMAL,
                due_date=to_date(post(request, "due_date")),
            )
            messages.success(request, "Task added.")
        return redirect("admin_tasks")

    tasks = Task.objects.select_related("project", "project__client")
    return render(request, "adminpanel/tasks.html", {
        "nav": "tasks",
        "columns": [
            {"label": label, "value": value, "tasks": tasks.filter(status=value)}
            for value, label in Task.STATUS_CHOICES
        ],
        "statuses": Task.STATUS_CHOICES,
        "priorities": Task.PRIORITY_CHOICES,
        "projects": Project.objects.select_related("client"),
        "open_count": tasks.exclude(status=Task.COMPLETED).count(),
        "today": timezone.localdate(),
        "overdue": tasks.exclude(status=Task.COMPLETED).filter(
            due_date__lt=timezone.localdate()).count(),
    })


@staff_required
def admin_task_form(request, pk=None):
    task = get_object_or_404(Task, pk=pk) if pk else None
    return record_form(
        request, records.TASK_SPEC, Task, task,
        page_title="Edit task" if task else "New task",
        back_url=reverse("admin_tasks"),
        submit_label="Save task" if task else "Add task",
        success_url=lambda obj: reverse("admin_tasks"),
    )


@staff_required
def admin_task_move(request, pk):
    """Move a task to another column from the board."""
    task = get_object_or_404(Task, pk=pk)
    status = post(request, "status")
    if status in dict(Task.STATUS_CHOICES):
        task.status = status
        task.save()
    return redirect("admin_tasks")


@staff_required
def admin_task_delete(request, pk):
    task = get_object_or_404(Task, pk=pk)
    name = task.name
    task.delete()
    messages.success(request, f"Deleted “{name}”.")
    return redirect("admin_tasks")


@staff_required
def admin_calendar(request):
    import calendar as calmod

    today = timezone.localdate()
    year = int(request.GET.get("year", today.year))
    month = int(request.GET.get("month", today.month))
    events = CalendarEvent.objects.filter(date__year=year, date__month=month)
    grid = []
    for week in calmod.Calendar(firstweekday=0).monthdatescalendar(year, month):
        for day in week:
            grid.append({
                "date": day,
                "in_month": day.month == month,
                "today": day == today,
                "events": [e for e in events if e.date == day],
            })
    return render(request, "adminpanel/calendar.html", {
        "nav": "calendar",
        "grid": grid,
        "events": events,
        "label": f"{calmod.month_name[month]} {year}",
    })


@staff_required
def admin_files(request):
    client_id = request.GET.get("client", "")
    projects = Project.objects.select_related("client").prefetch_related("files")
    if client_id:
        projects = projects.filter(client_id=client_id)
    return render(request, "adminpanel/files.html", {
        "nav": "files",
        "projects": projects,
        "clients": Client.objects.select_related("user"),
        "active_client": client_id,
    })


@staff_required
def admin_notifications(request):
    if request.method == "POST":
        request.user.notifications.update(read=True)
        return redirect("admin_notifications")
    return render(request, "adminpanel/notifications.html", {
        "nav": "notifications", "notifications": request.user.notifications.all()[:50],
    })


@staff_required
def admin_activity(request):
    return render(request, "adminpanel/activity.html", {
        "nav": "activity",
        "activity": ActivityLog.objects.all()[:100],
        "logins": LoginEvent.objects.all()[:20],
    })


SITE_FIELDS = ["name", "tagline", "contact_email", "whatsapp", "location",
               "meta_description", "github_url", "linkedin_url", "x_url",
               "payment_provider", "payment_mode", "webhook_url"]


EMAIL_TEXT_FIELDS = ["provider", "host", "username", "from_name", "from_email", "reply_to"]
EMAIL_BOOL_FIELDS = ["enabled", "use_tls", "use_ssl", "on_new_lead", "on_ticket_opened",
                     "on_ticket_reply", "on_project_status", "on_invoice_issued",
                     "on_payment_received", "on_new_message"]


@staff_required
def admin_settings(request):
    site = SiteSettings.load()
    email = EmailSettings.load()
    tab = request.GET.get("tab", "website")

    if request.method == "POST":
        if "save_email" in request.POST:
            for field in EMAIL_TEXT_FIELDS:
                if field in request.POST:
                    setattr(email, field, post(request, field))
            for field in EMAIL_BOOL_FIELDS:
                setattr(email, field, field in request.POST)
            port = post(request, "port")
            email.port = int(port) if port.isdigit() else email.port
            # only overwrite the password when a new one is typed
            new_password = request.POST.get("password", "")
            if new_password and new_password != "********":
                email.password = new_password
            email.save()
            log(request, f"Email settings changed to {email.get_provider_display()}.")
            messages.success(request, "Email settings saved.")
            return redirect("/admin/settings/?tab=email")

        if "send_test" in request.POST:
            target = post(request, "test_to") or request.user.email
            try:
                sent = mail.send_now(
                    "Test from your Teniteno1 admin",
                    "If you are reading this, outgoing mail works.\n\n"
                    f"Provider: {email.get_provider_display()}\n"
                    f"Host: {email.host or 'not set'}\n",
                    target, fail_silently=False,
                )
                email.last_test_result = (f"Sent to {target}." if sent
                                          else f"Nothing sent to {target}.")
                messages.success(request, email.last_test_result)
            except Exception as exc:
                email.last_test_result = f"{type(exc).__name__}: {exc}"[:300]
                messages.error(request, f"Test failed — {email.last_test_result}")
            email.last_tested_at = timezone.now()
            email.save()
            return redirect("/admin/settings/?tab=email")

        for field in SITE_FIELDS:
            if field in request.POST:
                setattr(site, field, post(request, field))
        site.save()
        messages.success(request, "Settings saved.")
        return redirect(request.get_full_path())

    return render(request, "adminpanel/settings.html", {
        "nav": "settings",
        "site": site,
        "email": email,
        "providers": EmailSettings.PROVIDER_CHOICES,
        "presets_json": json.dumps(EmailSettings.PRESETS),
        "preset_hint": email.preset.get("hint", ""),
        "packages": PricingPackage.objects.all(),
        "services": Service.objects.all(),
        "tab": tab,
    })


# ─────────────────────── native record forms ───────────────────────
# Everything is added and edited inside this panel. Nothing sends you to
# Django's own admin.

def _querysets(spec):
    """Resolve the dropdowns a spec needs."""
    out = {}
    for field in records.flatten(spec):
        model = field.get("model")
        if model is not None:
            out[field["name"]] = model.objects.all()
    return out


def _resolvers(spec, values):
    """Turn posted primary keys into model instances."""
    out = {}
    for field in records.flatten(spec):
        model = field.get("model")
        if model is not None:
            pk = values.get(field["name"])
            out[field["name"]] = model.objects.filter(pk=pk).first() if pk else None
    return out


def record_form(request, spec, model, instance, page_title, back_url,
                submit_label="Save", extra_validate=None, after_save=None,
                success_url=None, template="adminpanel/record_form.html"):
    """One add/edit form, driven by a field spec. Used by every record type."""
    errors = {}
    if request.method == "POST":
        values = records.read_post(request, spec)
        errors = records.validate(values, spec)
        resolvers = _resolvers(spec, values)
        for field in records.flatten(spec):
            if field.get("required") and field.get("model") and not resolvers.get(field["name"]):
                errors[field["name"]] = f"Pick a {field['label'].lower()}."
        if extra_validate:
            extra_validate(values, errors, instance)
        if not errors:
            obj = instance or model()
            records.apply_fields(obj, values, spec, resolvers)
            obj.save()
            if after_save:
                after_save(obj, values, request)
            messages.success(request, f"{obj} saved.")
            return redirect(success_url(obj) if success_url else back_url)
    else:
        values = records.read_instance(instance, spec) if instance else {
            fld["name"]: (fld.get("default", False) if fld["kind"] == "checkbox" else "")
            for fld in records.flatten(spec)
        }

    return render(request, template, {
        "nav": request.resolver_match.url_name.split("_")[1] if request.resolver_match else "",
        "page_title": page_title,
        "back_url": back_url,
        "submit_label": submit_label,
        "sections": records.build_sections(spec, values, errors, _querysets(spec)),
        "object": instance,
    })


def _unique_slug(model, values, errors, instance, source="name"):
    candidate = values.get("slug") or records.slugify(values.get(source, ""))
    values["slug"] = candidate
    if not candidate:
        return
    clash = model.objects.filter(slug=candidate)
    if instance:
        clash = clash.exclude(pk=instance.pk)
    if clash.exists():
        errors["slug"] = "Another record already uses that slug."


@staff_required
def admin_lead_form(request, pk=None):
    lead = get_object_or_404(Lead, pk=pk) if pk else None
    return record_form(
        request, records.LEAD_SPEC, Lead, lead,
        page_title="Edit lead" if lead else "Add lead",
        back_url=reverse("admin_lead", args=[pk]) if lead else reverse("admin_leads"),
        submit_label="Save lead" if lead else "Add lead",
        success_url=lambda obj: reverse("admin_lead", args=[obj.pk]),
    )


@staff_required
def admin_client_form(request, pk=None):
    """Clients own a User, so this form writes both."""
    client = get_object_or_404(Client, pk=pk) if pk else None
    spec = records.CLIENT_SPEC
    errors = {}

    if request.method == "POST":
        values = records.read_post(request, spec)
        errors = records.validate(values, spec)
        existing = User.objects.filter(email__iexact=values["email"])
        if client:
            existing = existing.exclude(pk=client.user_id)
        if values["email"] and existing.exists():
            errors["email"] = "Another account already uses that email."
        if not errors:
            if client:
                user = client.user
            else:
                user = User(username=values["email"])
            user.username = values["email"]
            user.email = values["email"]
            user.first_name = values["first_name"]
            user.last_name = values["last_name"]
            if not client:
                password = get_random_string(12)
                user.set_password(password)
                messages.info(request, f"Temporary password for {values['email']}: {password}")
            user.save()
            obj = client or Client(user=user)
            obj.company = values["company"]
            obj.phone = values["phone"]
            obj.address = values["address"]
            obj.status = values["status"] or Client.ACTIVE
            obj.save()
            log(request, f"Client {obj.full_name} {'updated' if client else 'added'}.")
            messages.success(request, f"{obj.full_name} saved.")
            return redirect("admin_client", pk=obj.pk)
    elif client:
        values = {
            "first_name": client.user.first_name, "last_name": client.user.last_name,
            "email": client.user.email, "phone": client.phone,
            "company": client.company, "address": client.address, "status": client.status,
        }
    else:
        values = {fld["name"]: "" for fld in records.flatten(spec)}

    return render(request, "adminpanel/record_form.html", {
        "nav": "clients",
        "page_title": "Edit client" if client else "Add client",
        "back_url": reverse("admin_client", args=[pk]) if client else reverse("admin_clients"),
        "submit_label": "Save client" if client else "Add client",
        "sections": records.build_sections(spec, values, errors),
        "object": client,
    })


@staff_required
def admin_project_form(request, slug=None):
    project = get_object_or_404(Project, slug=slug) if slug else None
    return record_form(
        request, records.PROJECT_SPEC, Project, project,
        page_title="Edit project" if project else "New project",
        back_url=reverse("admin_project", args=[slug]) if project else reverse("admin_projects"),
        submit_label="Save project" if project else "Create project",
        extra_validate=lambda v, e, i: _unique_slug(Project, v, e, i),
        success_url=lambda obj: reverse("admin_project", args=[obj.slug]),
    )


@staff_required
def admin_invoice_form(request, number=None):
    invoice = get_object_or_404(Invoice, number=number) if number else None
    return record_form(
        request, records.INVOICE_SPEC, Invoice, invoice,
        page_title="Edit invoice" if invoice else "New invoice",
        back_url=reverse("admin_invoice", args=[number]) if invoice else reverse("admin_invoices"),
        submit_label="Save invoice" if invoice else "Create invoice",
        success_url=lambda obj: reverse("admin_invoice", args=[obj.number]),
    )


@staff_required
def admin_quote_form(request, pk=None):
    quote = get_object_or_404(Quote, pk=pk) if pk else None

    def save_items(obj, values, req):
        descriptions = req.POST.getlist("item_description")
        amounts = req.POST.getlist("item_amount")
        obj.items.all().delete()
        order = 0
        for description, amount in zip(descriptions, amounts):
            description, amount = description.strip(), amount.strip().replace(",", "")
            if not description or not amount:
                continue
            try:
                value = Decimal(amount)
            except InvalidOperation:
                continue
            order += 1
            QuoteItem.objects.create(quote=obj, description=description,
                                     amount=value, order=order)

    return record_form(
        request, records.QUOTE_SPEC, Quote, quote,
        page_title="Edit quotation" if quote else "New quotation",
        back_url=reverse("admin_quotes"),
        submit_label="Save quotation" if quote else "Create quotation",
        after_save=save_items,
        success_url=lambda obj: reverse("admin_quotes"),
        template="adminpanel/quote_form.html",
    )


@staff_required
def admin_portfolio_form(request, slug=None):
    project = get_object_or_404(PortfolioProject, slug=slug) if slug else None

    def save_image(obj, values, req):
        upload = req.FILES.get("image")
        if upload:
            PortfolioImage.objects.create(project=obj, image=upload)

    return record_form(
        request, records.PORTFOLIO_SPEC, PortfolioProject, project,
        page_title="Edit portfolio project" if project else "Add portfolio project",
        back_url=reverse("admin_portfolio"),
        submit_label="Save project" if project else "Add project",
        extra_validate=lambda v, e, i: _unique_slug(PortfolioProject, v, e, i),
        after_save=save_image,
        success_url=lambda obj: reverse("admin_portfolio"),
        template="adminpanel/portfolio_form.html",
    )


@staff_required
def admin_category_form(request, pk=None):
    category = get_object_or_404(PortfolioCategory, pk=pk) if pk else None
    return record_form(
        request, records.CATEGORY_SPEC, PortfolioCategory, category,
        page_title="Edit category" if category else "Add category",
        back_url=reverse("admin_portfolio"),
        submit_label="Save category" if category else "Add category",
        extra_validate=lambda v, e, i: _unique_slug(PortfolioCategory, v, e, i),
    )


@staff_required
def admin_service_form(request, pk=None):
    service = get_object_or_404(Service, pk=pk) if pk else None
    return record_form(
        request, records.SERVICE_SPEC, Service, service,
        page_title="Edit service" if service else "Add service",
        back_url=reverse("admin_settings") + "?tab=services",
        submit_label="Save service" if service else "Add service",
        extra_validate=lambda v, e, i: _unique_slug(Service, v, e, i, source="title"),
        success_url=lambda obj: reverse("admin_settings") + "?tab=services",
    )


@staff_required
def admin_package_form(request, pk=None):
    package = get_object_or_404(PricingPackage, pk=pk) if pk else None
    return record_form(
        request, records.PACKAGE_SPEC, PricingPackage, package,
        page_title="Edit package" if package else "Add package",
        back_url=reverse("admin_settings") + "?tab=pricing",
        submit_label="Save package" if package else "Add package",
        extra_validate=lambda v, e, i: _unique_slug(PricingPackage, v, e, i),
        success_url=lambda obj: reverse("admin_settings") + "?tab=pricing",
    )


@staff_required
def admin_project_milestone(request, slug):
    project = get_object_or_404(Project, slug=slug)
    title = post(request, "title")
    if title:
        Milestone.objects.create(
            project=project, title=title, detail=post(request, "detail"),
            status=post(request, "status") or Milestone.NOT_STARTED,
            due_date=to_date(post(request, "due_date")),
            order=project.milestones.count() + 1,
        )
        messages.success(request, "Milestone added.")
    return redirect("admin_project", slug=slug)


@staff_required
def admin_project_task(request, slug):
    project = get_object_or_404(Project, slug=slug)
    name = post(request, "name")
    if name:
        Task.objects.create(
            name=name, project=project,
            priority=post(request, "priority") or Task.NORMAL,
            due_date=to_date(post(request, "due_date")),
        )
        messages.success(request, "Task added.")
    return redirect("admin_project", slug=slug)


@staff_required
def admin_project_update(request, slug):
    project = get_object_or_404(Project, slug=slug)
    title = post(request, "title")
    body = post(request, "body")
    if title and body:
        ProjectUpdate.objects.create(
            project=project, title=title, body=body,
            client_visible="client_visible" in request.POST,
        )
        if "client_visible" in request.POST:
            notify_client(project.client, "Project", f"New update on {project.name}: {title}")
        messages.success(request, "Update posted.")
    return redirect("admin_project", slug=slug)


@staff_required
def admin_project_file(request, slug):
    project = get_object_or_404(Project, slug=slug)
    upload = request.FILES.get("file")
    if upload:
        ProjectFile.objects.create(
            project=project, file=upload, name=post(request, "name") or upload.name,
            folder=post(request, "folder") or ProjectFile.DOCUMENT,
            client_visible="client_visible" in request.POST,
        )
        messages.success(request, "File uploaded.")
    return redirect("admin_project", slug=slug)


def robots(request):
    return HttpResponse("User-agent: *\nDisallow: /client/\nDisallow: /admin/\n",
                        content_type="text/plain")
