"""
expense/views.py
================
Function-based views for expense request submission, approval actions,
dashboard, and AJAX helpers.

NOTE: ``horilla.decorators`` (NOT ``horilla_views.cbv_methods``) must be
used here — the latter expects a CBV ``self`` first-argument.
"""

from datetime import datetime

from django.contrib import messages
from django.contrib.auth.models import Group
from django.db import transaction
from django.db.models import Count, Q, Sum
from django.http import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_http_methods, require_POST

from notifications.signals import notify

from base.models import Department
from budget.models import BudgetItem, BudgetSubHead
from employee.models import Employee
from horilla.decorators import login_required, permission_required

from .forms import (
    ApproveForm,
    ExpenseApprovalStageForm,
    ExpenseApprovalWorkflowForm,
    ExpenseDocumentFormSet,
    ExpenseRequestForm,
    KIVForm,
    RejectForm,
    ResubmitForm,
)
from .models import (
    ACTION_APPROVED,
    ACTION_CLOSED,
    ACTION_DISBURSED,
    ACTION_KIV,
    ACTION_RECALLED,
    ACTION_REJECTED,
    ACTION_RESUBMITTED,
    ACTION_SUBMITTED,
    STATUS_APPROVED,
    STATUS_CLOSED,
    STATUS_DISBURSED,
    STATUS_KIV,
    STATUS_PENDING,
    STATUS_REJECTED,
    STATUS_RESUBMITTED,
    ExpenseApprovalLog,
    ExpenseApprovalStage,
    ExpenseApprovalWorkflow,
    ExpenseRequest,
    KIVRecord,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _get_employee(request):
    try:
        return request.user.employee_get
    except Exception:
        return None


def _log(expense, stage, action, actor, comment=""):
    ExpenseApprovalLog.objects.create(
        expense=expense,
        stage=stage,
        action=action,
        actor=actor,
        comment=comment,
    )


def _notify_stage(expense, stage, verb):
    """Send in-app notification to all members of the stage group."""
    if not stage:
        return
    for user in stage.group.user_set.all():
        notify.send(
            expense.employee.employee_user_id,
            recipient=user,
            verb=verb,
            description=f"{expense.reference_number}: {expense.title}",
        )


def _notify_requester(expense, verb):
    notify.send(
        expense.employee.employee_user_id,
        recipient=expense.employee.employee_user_id,
        verb=verb,
        description=expense.reference_number,
    )


def _add_to_budget(expense):
    """Add expense amount to budget committed bucket on submission."""
    alloc = expense.get_current_allocation()
    if not alloc:
        return
    from django.db.models import F
    alloc.committed_amount = F("committed_amount") + expense.amount
    alloc.save(update_fields=["committed_amount"])


def _close_modal_reload():
    """Standard HTMX response to close the open modal and reload the page."""
    return HttpResponse(
        "<script>"
        "$('#genericModal').removeClass('oh-modal--show');"
        "$('#deleteConfirmation').removeClass('oh-modal--show');"
        "$('.reload-record').click();"
        "$('#reloadMessagesButton').click();"
        "location.reload();"
        "</script>"
    )


# ---------------------------------------------------------------------------
# Dashboard
# ---------------------------------------------------------------------------


@login_required
def expense_dashboard(request):
    employee = _get_employee(request)

    my_requests = (
        ExpenseRequest.objects.filter(employee=employee).order_by("-submitted_at")[:10]
        if employee
        else ExpenseRequest.objects.none()
    )

    user_group_ids = list(request.user.groups.values_list("id", flat=True))

    approval_queue = (
        ExpenseRequest.objects.filter(
            status__in=[STATUS_PENDING, STATUS_RESUBMITTED],
            current_stage__group_id__in=user_group_ids,
        )
        .select_related("employee", "budget_item", "current_stage")
        .order_by("-submitted_at")
    )

    kiv_due = (
        ExpenseRequest.objects.filter(
            status=STATUS_KIV,
            kiv_records__recalled_at__isnull=True,
            kiv_records__revisit_date__lte=timezone.now().date(),
            current_stage__group_id__in=user_group_ids,
        )
        .distinct()
    )

    # Aggregate counts to avoid multiple queries
    my_counts = (
        ExpenseRequest.objects.filter(employee=employee)
        .aggregate(
            my_pending=Count(
                "id", filter=Q(status__in=[STATUS_PENDING, STATUS_RESUBMITTED, STATUS_KIV])
            ),
        )
        if employee
        else {"my_pending": 0}
    )

    context = {
        "my_requests": my_requests,
        "approval_queue": approval_queue,
        "kiv_due": kiv_due,
        "pending_count": approval_queue.count(),
        "kiv_count": kiv_due.count(),
        "my_pending": my_counts["my_pending"],
    }
    return render(request, "expense/dashboard.html", context)


# ---------------------------------------------------------------------------
# Submit new expense request
# ---------------------------------------------------------------------------


@login_required
@permission_required("expense.add_expenserequest")
def submit_expense_request(request):
    employee = _get_employee(request)
    if not employee:
        messages.error(request, _("Your user account is not linked to an employee profile."))
        return redirect("expense-dashboard")

    workflow = ExpenseApprovalWorkflow.get_default()
    if not workflow:
        messages.error(
            request,
            _("No active approval workflow configured. Contact your administrator."),
        )
        return redirect("expense-dashboard")

    first_stage = workflow.stages.filter(is_active=True).order_by("order").first()
    if not first_stage:
        messages.error(request, _("The approval workflow has no active stages."))
        return redirect("expense-dashboard")

    form = ExpenseRequestForm(request.POST or None, employee=employee)
    doc_formset = ExpenseDocumentFormSet(request.POST or None, request.FILES or None)

    if request.method == "POST" and form.is_valid() and doc_formset.is_valid():
        with transaction.atomic():
            expense = form.save(commit=False)
            expense.employee = employee
            expense.workflow = workflow
            expense.current_stage = first_stage
            expense.status = STATUS_PENDING

            # Auto-fill department from employee work info
            try:
                expense.department = employee.employee_work_info.department
            except Exception:
                pass

            # Budget overrun check
            alloc = expense.get_current_allocation()
            if alloc:
                allowed, reason = alloc.check_overrun(expense.amount)
                if not allowed:
                    messages.error(request, reason)
                    return render(
                        request,
                        "expense/request_form.html",
                        {"form": form, "doc_formset": doc_formset},
                    )
                if reason:
                    messages.warning(request, reason)

            expense.save()

            # Track in budget
            _add_to_budget(expense)

            # Save documents
            doc_formset.instance = expense
            for doc_form in doc_formset:
                if doc_form.cleaned_data and doc_form.cleaned_data.get("file"):
                    doc = doc_form.save(commit=False)
                    doc.expense = expense
                    doc.uploaded_by = request.user
                    doc.save()

            # Log submission
            _log(expense, first_stage, ACTION_SUBMITTED, request.user)

            # Notify first stage approvers
            _notify_stage(
                expense,
                first_stage,
                f"New expense request requires your approval: {expense.reference_number}",
            )

        messages.success(
            request,
            _("Expense request %(ref)s submitted successfully.") % {"ref": expense.reference_number},
        )
        return redirect("expense-my-requests")

    return render(
        request,
        "expense/request_form.html",
        {"form": form, "doc_formset": doc_formset},
    )


# ---------------------------------------------------------------------------
# My Requests list (employee view) — searchable via HTMX
# ---------------------------------------------------------------------------


@login_required
def my_expense_requests(request):
    employee = _get_employee(request)
    qs = (
        ExpenseRequest.objects.filter(employee=employee)
        .select_related("budget_item__sub_head__head", "current_stage", "workflow")
        .order_by("-submitted_at")
    )
    return render(request, "expense/my_requests.html", {"requests": qs})


# ---------------------------------------------------------------------------
# Request detail (loaded inside #genericModal)
# ---------------------------------------------------------------------------


@login_required
def expense_request_detail(request, pk):
    expense = get_object_or_404(ExpenseRequest, pk=pk)
    employee = _get_employee(request)

    user_group_ids = list(request.user.groups.values_list("id", flat=True))
    is_approver = bool(
        expense.current_stage and expense.current_stage.group_id in user_group_ids
    )
    is_owner = bool(employee and expense.employee == employee)

    if not (
        is_approver
        or is_owner
        or request.user.is_superuser
        or request.user.has_perm("expense.view_expenserequest")
    ):
        return HttpResponse(status=403)

    logs = expense.approval_logs.select_related("stage", "actor").order_by("created_at")
    docs = expense.documents.all()
    kiv = expense.kiv_records.filter(recalled_at__isnull=True).first()

    context = {
        "expense": expense,
        "logs": logs,
        "docs": docs,
        "kiv": kiv,
        "is_approver": is_approver,
        "is_owner": is_owner,
        "can_act": is_approver
        and expense.status in (STATUS_PENDING, STATUS_RESUBMITTED),
        "can_recall": expense.status == STATUS_KIV
        and expense.can_recall_kiv(request.user),
    }
    return render(request, "expense/request_detail.html", context)


# ---------------------------------------------------------------------------
# Action modal partials — loaded into #genericModal when an action button is clicked
# ---------------------------------------------------------------------------


@login_required
def action_modal(request, pk, action):
    """Render the requested action modal (approve/reject/kiv/resubmit/recall)."""
    expense = get_object_or_404(ExpenseRequest, pk=pk)
    employee = _get_employee(request)

    user_group_ids = list(request.user.groups.values_list("id", flat=True))
    is_approver = bool(
        expense.current_stage and expense.current_stage.group_id in user_group_ids
    )
    is_owner = bool(employee and expense.employee == employee)

    template_map = {
        "approve": "expense/partials/approve_modal.html",
        "reject": "expense/partials/reject_modal.html",
        "kiv": "expense/partials/kiv_modal.html",
        "resubmit": "expense/partials/resubmit_modal.html",
        "recall": "expense/partials/recall_modal.html",
    }
    if action not in template_map:
        return HttpResponse(status=404)

    # Permission gate per action
    if action in ("approve", "reject", "kiv") and not (
        is_approver and expense.status in (STATUS_PENDING, STATUS_RESUBMITTED)
    ):
        return HttpResponse(status=403)
    if action == "resubmit" and not (is_owner and expense.can_resubmit()):
        return HttpResponse(status=403)
    if action == "recall" and not expense.can_recall_kiv(request.user):
        return HttpResponse(status=403)

    context = {
        "expense": expense,
        "approve_form": ApproveForm(),
        "reject_form": RejectForm(),
        "kiv_form": KIVForm(),
        "resubmit_form": ResubmitForm(),
    }
    return render(request, template_map[action], context)


# ---------------------------------------------------------------------------
# Approval actions
# ---------------------------------------------------------------------------


def _check_approver(request, expense):
    """Return (True, None) or (False, error_response)."""
    if not expense.is_approvable_by(request.user):
        return False, HttpResponse(
            "<div class='oh-alert oh-alert--danger'>"
            + str(_("You do not have permission to act on this request."))
            + "</div>",
            status=403,
        )
    return True, None


@login_required
@require_POST
def approve_expense(request, pk):
    expense = get_object_or_404(ExpenseRequest, pk=pk)
    ok, err = _check_approver(request, expense)
    if not ok:
        return err

    form = ApproveForm(request.POST)
    if not form.is_valid():
        return HttpResponse(status=400)

    comment = form.cleaned_data.get("comment", "")
    stage = expense.current_stage

    with transaction.atomic():
        _log(expense, stage, ACTION_APPROVED, request.user, comment)

        next_stage = stage.get_next_stage()

        if next_stage:
            expense.current_stage = next_stage
            # Transition to APPROVED before disbursement stage
            if stage.is_disbursement_stage is False and next_stage.is_disbursement_stage:
                expense.status = STATUS_APPROVED
            expense.save()
            _notify_stage(
                expense,
                next_stage,
                f"Expense {expense.reference_number} requires your action.",
            )
        else:
            # Final stage
            if stage.is_disbursement_stage:
                expense.status = STATUS_DISBURSED
                _log(expense, stage, ACTION_DISBURSED, request.user, comment)
            else:
                expense.status = STATUS_APPROVED
            expense.current_stage = None
            expense.save()
            _notify_requester(
                expense,
                f"Your expense request {expense.reference_number} has been {expense.get_status_display()}.",
            )

    messages.success(request, _("Request approved and forwarded."))
    return _close_modal_reload()


@login_required
@require_POST
def reject_expense(request, pk):
    expense = get_object_or_404(ExpenseRequest, pk=pk)
    ok, err = _check_approver(request, expense)
    if not ok:
        return err

    form = RejectForm(request.POST)
    if not form.is_valid():
        return render(
            request,
            "expense/partials/reject_modal.html",
            {"form": form, "expense": expense},
        )

    reason = form.cleaned_data["reason"]

    with transaction.atomic():
        if expense.resubmit_count >= 1:
            expense.status = STATUS_CLOSED
            _log(expense, expense.current_stage, ACTION_CLOSED, request.user, reason)
        else:
            expense.status = STATUS_REJECTED
            _log(expense, expense.current_stage, ACTION_REJECTED, request.user, reason)

        expense.current_stage = None
        expense.save()
        _notify_requester(
            expense,
            f"Your expense request {expense.reference_number} was {expense.get_status_display()}.",
        )

    messages.warning(request, _("Request rejected."))
    return _close_modal_reload()


@login_required
@require_POST
def kiv_expense(request, pk):
    expense = get_object_or_404(ExpenseRequest, pk=pk)
    ok, err = _check_approver(request, expense)
    if not ok:
        return err

    form = KIVForm(request.POST)
    if not form.is_valid():
        return render(
            request,
            "expense/partials/kiv_modal.html",
            {"form": form, "expense": expense},
        )

    with transaction.atomic():
        KIVRecord.objects.create(
            expense=expense,
            stage=expense.current_stage,
            set_by=request.user,
            reason=form.cleaned_data["reason"],
            revisit_date=form.cleaned_data["revisit_date"],
            status_before_kiv=expense.status,
        )
        expense.status = STATUS_KIV
        expense.save()
        _log(
            expense,
            expense.current_stage,
            ACTION_KIV,
            request.user,
            form.cleaned_data["reason"],
        )
        _notify_requester(
            expense,
            f"Your expense request {expense.reference_number} has been kept in view until {form.cleaned_data['revisit_date']}.",
        )

    messages.info(request, _("Request placed on KIV."))
    return _close_modal_reload()


@login_required
@require_POST
def recall_kiv(request, pk):
    expense = get_object_or_404(ExpenseRequest, pk=pk)

    if not expense.can_recall_kiv(request.user):
        return HttpResponse("Forbidden", status=403)

    kiv = expense.kiv_records.filter(recalled_at__isnull=True).first()
    if not kiv:
        messages.error(request, _("No active KIV record found."))
        return _close_modal_reload()

    with transaction.atomic():
        kiv.recalled_at = timezone.now()
        kiv.recalled_by = request.user
        kiv.save()
        expense.status = kiv.status_before_kiv
        expense.save()
        _log(expense, expense.current_stage, ACTION_RECALLED, request.user)
        _notify_stage(
            expense,
            expense.current_stage,
            f"KIV recalled: {expense.reference_number} is pending your action again.",
        )

    messages.success(request, _("KIV recalled. Request is active again."))
    return _close_modal_reload()


@login_required
@require_POST
def resubmit_expense(request, pk):
    expense = get_object_or_404(ExpenseRequest, pk=pk)
    employee = _get_employee(request)

    if not (employee and expense.employee == employee):
        return HttpResponse("Forbidden", status=403)

    if not expense.can_resubmit():
        messages.error(request, _("This request cannot be resubmitted."))
        return _close_modal_reload()

    form = ResubmitForm(request.POST)
    if not form.is_valid():
        messages.error(request, _("Invalid form."))
        return _close_modal_reload()

    first_stage = expense.workflow.stages.filter(is_active=True).order_by("order").first()
    if not first_stage:
        messages.error(request, _("Workflow has no active stages."))
        return _close_modal_reload()

    with transaction.atomic():
        expense.resubmit_count += 1
        expense.current_stage = first_stage
        expense.status = STATUS_RESUBMITTED
        expense.save()
        _log(
            expense,
            first_stage,
            ACTION_RESUBMITTED,
            request.user,
            form.cleaned_data.get("note", ""),
        )
        _notify_stage(
            expense,
            first_stage,
            f"Resubmitted expense {expense.reference_number} requires your review.",
        )

    messages.success(request, _("Request resubmitted successfully."))
    return _close_modal_reload()


# ---------------------------------------------------------------------------
# Approval queue (approver view) — searchable via HTMX
# ---------------------------------------------------------------------------


@login_required
def approval_queue(request):
    user_group_ids = list(request.user.groups.values_list("id", flat=True))
    queue = (
        ExpenseRequest.objects.filter(
            status__in=[STATUS_PENDING, STATUS_RESUBMITTED],
            current_stage__group_id__in=user_group_ids,
        )
        .select_related(
            "employee", "budget_item__sub_head__head", "current_stage", "department"
        )
        .order_by("-submitted_at")
    )
    return render(request, "expense/approval_queue.html", {"queue": queue})


# ---------------------------------------------------------------------------
# AJAX helpers for cascading budget selects
# ---------------------------------------------------------------------------


@login_required
def ajax_sub_heads(request):
    head_id = request.GET.get("budget_head")
    if not head_id:
        return HttpResponse("<option value=''>---------</option>")
    subs = BudgetSubHead.objects.filter(head_id=head_id, is_active=True).order_by("name")
    html = "<option value=''>---------</option>" + "".join(
        f"<option value='{s.pk}'>{s.name}</option>" for s in subs
    )
    return HttpResponse(html)


@login_required
def ajax_budget_items(request):
    sub_id = request.GET.get("budget_sub_head")
    if not sub_id:
        return HttpResponse("<option value=''>---------</option>")
    items = BudgetItem.objects.filter(sub_head_id=sub_id, is_active=True).order_by("name")
    html = "<option value=''>---------</option>" + "".join(
        f"<option value='{i.pk}'>[{i.code}] {i.name}</option>" for i in items
    )
    return HttpResponse(html)


# ---------------------------------------------------------------------------
# Workflow configuration (admin views)
# ---------------------------------------------------------------------------


@login_required
@permission_required("expense.add_expenseapprovalworkflow")
def workflow_config(request):
    workflows = ExpenseApprovalWorkflow.objects.prefetch_related("stages__group").order_by(
        "name"
    )
    return render(request, "expense/workflow_config.html", {"workflows": workflows})


@login_required
@permission_required("expense.change_expenseapprovalworkflow")
def workflow_form(request, pk=None):
    instance = get_object_or_404(ExpenseApprovalWorkflow, pk=pk) if pk else None
    form = ExpenseApprovalWorkflowForm(request.POST or None, instance=instance)
    if request.method == "POST" and form.is_valid():
        form.save()
        messages.success(request, _("Workflow saved."))
        return _close_modal_reload()
    return render(
        request,
        "expense/partials/workflow_form.html",
        {"form": form, "is_create": instance is None},
    )


@login_required
@permission_required("expense.change_expenseapprovalstage")
def stage_form(request, workflow_pk, pk=None):
    workflow = get_object_or_404(ExpenseApprovalWorkflow, pk=workflow_pk)
    instance = get_object_or_404(ExpenseApprovalStage, pk=pk) if pk else None
    form = ExpenseApprovalStageForm(
        request.POST or None, instance=instance, initial={"workflow": workflow}
    )
    if request.method == "POST" and form.is_valid():
        form.save()
        messages.success(request, _("Stage saved."))
        return _close_modal_reload()
    return render(
        request,
        "expense/partials/stage_form.html",
        {"form": form, "workflow": workflow, "is_create": instance is None},
    )
