"""
budget/views.py
===============
Function-based views for Budget Management (dashboard, bulk upload, settings,
delete endpoints).  CRUD list/form flows live in budget/cbv/.

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

import csv
import io
from datetime import datetime

from django.contrib import messages
from django.db import transaction
from django.db.models import ProtectedError
from django.http import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_http_methods

from horilla.decorators import login_required, permission_required

from .forms import BudgetBulkUploadForm, BudgetSettingsForm
from .models import (
    BudgetAllocation,
    BudgetHead,
    BudgetItem,
    BudgetSettings,
    BudgetSubHead,
)


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

@login_required
@permission_required("budget.view_budgethead")
def budget_dashboard(request):
    """
    High-level budget overview: totals, recent allocations, overrun alerts.
    """
    from django.db.models import Sum

    current_year = datetime.now().year

    heads = BudgetHead.objects.filter(is_active=True).prefetch_related(
        "sub_heads__items__allocations"
    )
    allocations = (
        BudgetAllocation.objects.filter(fiscal_year=current_year)
        .select_related("item__sub_head__head")
        .order_by("-allocated_amount")
    )

    totals = allocations.aggregate(
        total_allocated=Sum("allocated_amount"),
        total_disbursed=Sum("disbursed_amount"),
        total_approved=Sum("approved_amount"),
        total_committed=Sum("committed_amount"),
    )
    total_allocated = totals["total_allocated"] or 0
    total_disbursed = totals["total_disbursed"] or 0
    total_approved = totals["total_approved"] or 0
    total_committed = totals["total_committed"] or 0
    total_balance = total_allocated - total_disbursed
    overrun_items = [a for a in allocations if a.is_overrun]

    context = {
        "heads": heads,
        "allocations": allocations,
        "current_year": current_year,
        "total_allocated": total_allocated,
        "total_disbursed": total_disbursed,
        "total_approved": total_approved,
        "total_committed": total_committed,
        "total_balance": total_balance,
        "overrun_items": overrun_items,
        "overrun_count": len(overrun_items),
        "head_count": heads.count(),
        "item_count": BudgetItem.objects.filter(is_active=True).count(),
    }
    return render(request, "budget/dashboard.html", context)


# ---------------------------------------------------------------------------
# Settings
# ---------------------------------------------------------------------------

@login_required
@permission_required("budget.change_budgetsettings")
def budget_settings(request):
    obj = BudgetSettings.get_settings()
    form = BudgetSettingsForm(request.POST or None, instance=obj)
    if request.method == "POST" and form.is_valid():
        form.save()
        messages.success(request, _("Budget settings saved."))
        return HttpResponse(
            "<script>"
            "$('#genericModal').removeClass('oh-modal--show');"
            "$('#reloadMessagesButton').click();"
            "</script>"
        )
    return render(request, "budget/settings_form.html", {"form": form})


# ---------------------------------------------------------------------------
# Bulk Upload
# ---------------------------------------------------------------------------

@login_required
@permission_required("budget.add_budgetitem")
def budget_bulk_upload(request):
    """
    Upload an Excel (.xlsx) or CSV file to bulk create/update budget structure.

    Expected columns (case-insensitive, order-independent):
      head_name, sub_head_name, item_name, description (optional),
      fiscal_year (optional), period_type (optional), period_value (optional),
      allocated_amount (optional)
    """
    form = BudgetBulkUploadForm(request.POST or None, request.FILES or None)
    results = None

    if request.method == "POST" and form.is_valid():
        uploaded = form.cleaned_data["file"]
        try:
            results = _process_bulk_upload(uploaded)
            messages.success(
                request,
                _("Upload complete: {created} created, {updated} updated, {skipped} skipped.").format(
                    **results
                ),
            )
        except Exception as exc:
            messages.error(request, _("Upload failed: {}").format(str(exc)))

    return render(request, "budget/bulk_upload.html", {"form": form, "results": results})


def _process_bulk_upload(uploaded_file) -> dict:
    """
    Parse uploaded file and upsert BudgetHead / BudgetSubHead / BudgetItem
    (and optionally BudgetAllocation) records.
    Returns a summary dict: {created, updated, skipped, errors}.
    """
    name = uploaded_file.name.lower()
    rows = []

    if name.endswith(".csv"):
        text = uploaded_file.read().decode("utf-8-sig")
        reader = csv.DictReader(io.StringIO(text))
        rows = [
            {k.strip().lower(): v.strip() for k, v in row.items()}
            for row in reader
        ]
    else:
        # openpyxl for xlsx
        import openpyxl  # noqa: PLC0415

        wb = openpyxl.load_workbook(uploaded_file, read_only=True, data_only=True)
        ws = wb.active
        headers = None
        for row in ws.iter_rows(values_only=True):
            if headers is None:
                headers = [str(c).strip().lower() if c else "" for c in row]
                continue
            rows.append(
                {headers[i]: (str(v).strip() if v is not None else "") for i, v in enumerate(row)}
            )
        wb.close()

    created = updated = skipped = 0
    errors = []

    with transaction.atomic():
        for idx, row in enumerate(rows, start=2):  # row 2 = first data row (1 = header)
            head_name = row.get("head_name") or row.get("head name", "")
            sub_head_name = row.get("sub_head_name") or row.get("sub head name", "")
            item_name = row.get("item_name") or row.get("item name", "")

            if not head_name:
                errors.append(f"Row {idx}: missing head_name — skipped.")
                skipped += 1
                continue

            # Upsert Head
            head, head_created = BudgetHead.objects.get_or_create(
                name__iexact=head_name,
                defaults={"name": head_name, "description": row.get("head_description", "")},
            )
            if head_created:
                created += 1
            else:
                updated += 1

            if not sub_head_name:
                continue

            # Upsert SubHead
            sub_head, sh_created = BudgetSubHead.objects.get_or_create(
                head=head,
                name__iexact=sub_head_name,
                defaults={"name": sub_head_name, "description": row.get("sub_head_description", "")},
            )
            if sh_created:
                created += 1
            else:
                updated += 1

            if not item_name:
                continue

            # Upsert Item
            item, item_created = BudgetItem.objects.get_or_create(
                sub_head=sub_head,
                name__iexact=item_name,
                defaults={"name": item_name, "description": row.get("description", "")},
            )
            if item_created:
                created += 1
            else:
                if row.get("description"):
                    item.description = row["description"]
                    item.save(update_fields=["description"])
                updated += 1

            # Optional allocation row
            fiscal_year_str = row.get("fiscal_year", "")
            period_type = (row.get("period_type") or "annual").lower()
            period_value_str = row.get("period_value", "")
            allocated_str = row.get("allocated_amount", "")

            if fiscal_year_str and allocated_str:
                try:
                    fiscal_year = int(fiscal_year_str)
                    allocated_amount = float(allocated_str)
                    period_value = int(period_value_str) if period_value_str else None

                    alloc, alloc_created = BudgetAllocation.objects.get_or_create(
                        item=item,
                        fiscal_year=fiscal_year,
                        period_type=period_type,
                        period_value=period_value,
                        defaults={"allocated_amount": allocated_amount},
                    )
                    if alloc_created:
                        created += 1
                    else:
                        alloc.allocated_amount = allocated_amount
                        alloc.save(update_fields=["allocated_amount"])
                        updated += 1
                except (ValueError, TypeError) as exc:
                    errors.append(f"Row {idx}: allocation parse error — {exc}")
                    skipped += 1

    return {"created": created, "updated": updated, "skipped": skipped, "errors": errors}


# ---------------------------------------------------------------------------
# Delete endpoints (HTMX-friendly)
# ---------------------------------------------------------------------------

def _hx_delete_response(request):
    """Standard response after a soft/hard delete triggered from a list row."""
    # Reload the list and the messages area
    return HttpResponse(
        "<script>"
        "$('.filterButton').click();"
        "$('#reloadMessagesButton').click();"
        "$('#deleteConfirmation').removeClass('oh-modal--show');"
        "</script>"
    )


@login_required
@permission_required("budget.delete_budgethead")
@require_http_methods(["POST", "DELETE"])
def delete_budget_head(request, pk):
    head = get_object_or_404(BudgetHead, pk=pk)
    try:
        head.delete()
        messages.success(request, _("Budget head deleted."))
    except ProtectedError:
        messages.error(request, _("Budget head is in use and cannot be deleted."))
    return _hx_delete_response(request)


@login_required
@permission_required("budget.delete_budgetsubhead")
@require_http_methods(["POST", "DELETE"])
def delete_budget_sub_head(request, pk):
    sub_head = get_object_or_404(BudgetSubHead, pk=pk)
    try:
        sub_head.delete()
        messages.success(request, _("Budget sub-head deleted."))
    except ProtectedError:
        messages.error(request, _("Budget sub-head is in use and cannot be deleted."))
    return _hx_delete_response(request)


@login_required
@permission_required("budget.delete_budgetitem")
@require_http_methods(["POST", "DELETE"])
def delete_budget_item(request, pk):
    item = get_object_or_404(BudgetItem, pk=pk)
    try:
        item.delete()
        messages.success(request, _("Budget item deleted."))
    except ProtectedError:
        messages.error(request, _("Budget item is in use and cannot be deleted."))
    return _hx_delete_response(request)


@login_required
@permission_required("budget.delete_budgetallocation")
@require_http_methods(["POST", "DELETE"])
def delete_budget_allocation(request, pk):
    allocation = get_object_or_404(BudgetAllocation, pk=pk)
    try:
        allocation.delete()
        messages.success(request, _("Budget allocation deleted."))
    except ProtectedError:
        messages.error(request, _("Budget allocation is in use and cannot be deleted."))
    return _hx_delete_response(request)
