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

from django.contrib import messages
from django.db import transaction
from django.db.models import Count, Q, Sum
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.utils.dateparse import parse_date, parse_time
from django.views.decorators.http import require_POST

from reportlab.lib import colors
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle

from accounts.branch_utils import get_active_branch, get_branch_context, resolve_write_branch, scope
from accounts.permissions import company_admin_required
from . import services
from .forms import (
    AdvanceForm, EmployeeForm, HolidayForm, PayForm, PayrollSettingForm, PayslipEditForm,
)
from .models import WEEKDAY_CHOICES, Attendance, Employee, Holiday, Payslip, SalaryAdvance

ZERO = Decimal("0")
MONTH_NAMES = ["", "January", "February", "March", "April", "May", "June", "July",
               "August", "September", "October", "November", "December"]


def _company(request):
    return request.user.company


def _default_branch(request):
    """Branch preselected in payroll forms: the selected one, else main."""
    ctx = get_branch_context(request)
    return resolve_write_branch(request) or ctx.main


def _month_context(year, month):
    py, pm = services.shift_month(year, month, -1)
    ny, nm = services.shift_month(year, month, 1)
    return {
        "year": year, "month": month, "month_value": f"{year}-{month:02d}",
        "month_label": f"{MONTH_NAMES[month]} {year}",
        "prev_month": f"{py}-{pm:02d}", "next_month": f"{ny}-{nm:02d}",
    }


def _sum(qs, field):
    return qs.aggregate(t=Sum(field))["t"] or ZERO


# ---------------------------------------------------------------------------
# Payroll dashboard
# ---------------------------------------------------------------------------

@company_admin_required
def payroll_dashboard(request):
    company = _company(request)
    ctx = get_branch_context(request)
    branch = ctx.active
    today = timezone.now().date()
    year, month = today.year, today.month
    m_start, m_end, _ = services.month_bounds(year, month)

    emps = services.employees_in_scope(company, branch)
    active_emps = emps.filter(status=Employee.STATUS_ACTIVE)
    att_today = services.attendance_for_day(company, branch, today)
    setting = services.get_setting(company)
    is_weekly_off = today.weekday() in setting.weekly_off_set
    is_holiday = bool(services.holiday_dates(company, branch, today, today))

    payslips = scope(Payslip.objects.filter(company=company, year=year, month=month), branch)
    gross_total = _sum(payslips, "gross_salary")
    net_total = _sum(payslips, "net_salary")
    paid_total = _sum(payslips.filter(status=Payslip.PAID), "net_salary")
    status_counts = {r["status"]: r["n"] for r in payslips.values("status").annotate(n=Count("id"))}
    expected_gross = sum((e.gross_salary for e in active_emps), ZERO)

    adv_qs = scope(SalaryAdvance.objects.filter(company=company), branch)
    adv_outstanding = _sum(adv_qs, "amount") - _sum(adv_qs, "recovered")

    # Six-month payroll trend (cash paid) + net payable
    labels, paid_series, payable_series = [], [], []
    for i in range(5, -1, -1):
        y, m = services.shift_month(year, month, -i)
        s, e, _ = services.month_bounds(y, m)
        labels.append(f"{MONTH_NAMES[m][:3]} {y}")
        paid_series.append(float(services.payroll_cost(company, s, e, branch)))
        payable_series.append(float(_sum(
            scope(Payslip.objects.filter(company=company, year=y, month=m), branch), "net_salary"
        )))

    # Daily attendance for the current month
    emp_ids = list(active_emps.values_list("pk", flat=True))
    daily = {}
    for r in Attendance.objects.filter(
        company=company, date__range=[m_start, today], employee_id__in=emp_ids
    ).values("date", "status").annotate(n=Count("id")):
        daily.setdefault(r["date"], {})[r["status"]] = r["n"]
    att_labels, att_present, att_absent, att_leave = [], [], [], []
    for d in range(1, today.day + 1):
        day = date(year, month, d)
        row = daily.get(day, {})
        att_labels.append(str(d))
        att_present.append(row.get("present", 0) + row.get("late", 0) + row.get("half_day", 0))
        att_absent.append(row.get("absent", 0))
        att_leave.append(row.get("paid_leave", 0) + row.get("unpaid_leave", 0))

    branch_rows = []
    if branch is None and ctx.is_multi:
        for b in ctx.branches:
            b_att = services.attendance_for_day(company, b, today)
            b_slips = scope(Payslip.objects.filter(company=company, year=year, month=month), b)
            branch_rows.append({
                "branch": b,
                "staff": services.employees_in_scope(company, b, active_only=True).count(),
                "present": b_att["present"] + b_att["late"] + b_att["half_day"],
                "absent": b_att["absent"],
                "net": _sum(b_slips, "net_salary"),
                "paid": _sum(b_slips.filter(status=Payslip.PAID), "net_salary"),
            })

    return render(request, "payroll/dashboard.html", {
        "today": today, "month_label": f"{MONTH_NAMES[month]} {year}", "month_value": f"{year}-{month:02d}",
        "total_staff": emps.count(), "active_staff": len(emp_ids),
        "att_today": att_today, "present_today": att_today["present"] + att_today["late"] + att_today["half_day"],
        "on_leave_today": att_today["paid_leave"] + att_today["unpaid_leave"],
        "is_weekly_off": is_weekly_off, "is_holiday": is_holiday,
        "expected_gross": expected_gross, "gross_total": gross_total, "net_total": net_total,
        "paid_total": paid_total, "pending_total": net_total - paid_total,
        "status_counts": status_counts, "payslip_count": sum(status_counts.values()),
        "adv_outstanding": adv_outstanding,
        "trend_labels": json.dumps(labels), "trend_paid": json.dumps(paid_series),
        "trend_payable": json.dumps(payable_series),
        "att_labels": json.dumps(att_labels), "att_present": json.dumps(att_present),
        "att_absent": json.dumps(att_absent), "att_leave": json.dumps(att_leave),
        "branch_rows": branch_rows,
        "recent_employees": emps.order_by("-created_at")[:5],
    })


# ---------------------------------------------------------------------------
# Staff management
# ---------------------------------------------------------------------------

def _suggestions(company):
    qs = Employee.objects.filter(company=company)
    return {
        "designations": sorted({d for d in qs.values_list("designation", flat=True) if d}),
        "departments": sorted({d for d in qs.values_list("department", flat=True) if d}),
    }


@company_admin_required
def employee_list(request):
    company = _company(request)
    branch = get_active_branch(request)
    employees = services.employees_in_scope(company, branch)
    q = request.GET.get("q", "").strip()
    status = request.GET.get("status", "active")
    department = request.GET.get("department", "").strip()
    if q:
        employees = employees.filter(
            Q(full_name__icontains=q) | Q(employee_code__icontains=q)
            | Q(phone__icontains=q) | Q(designation__icontains=q)
        )
    if status in dict(Employee.STATUS_CHOICES):
        employees = employees.filter(status=status)
    if department:
        employees = employees.filter(department=department)
    employees = list(employees.order_by("employee_code"))
    return render(request, "payroll/employee_list.html", {
        "employees": employees, "q": q, "status": status, "department": department,
        "status_choices": Employee.STATUS_CHOICES,
        "departments": _suggestions(company)["departments"],
        "total_gross": sum((e.gross_salary for e in employees if e.status == Employee.STATUS_ACTIVE), ZERO),
    })


@company_admin_required
def employee_create(request):
    company = _company(request)
    if request.method == "POST":
        form = EmployeeForm(request.POST, company=company)
        if form.is_valid():
            emp = form.save(commit=False)
            emp.company = company
            if not emp.employee_code:
                emp.employee_code = services.next_employee_code(company)
            emp.save()
            messages.success(request, f"{emp.full_name} added as {emp.employee_code}.")
            return redirect("employee_detail", pk=emp.pk)
    else:
        form = EmployeeForm(
            company=company, default_branch=_default_branch(request),
            initial={"join_date": timezone.now().date(), "status": Employee.STATUS_ACTIVE},
        )
    return render(request, "payroll/employee_form.html", {
        "form": form, "title": "Add Employee", "next_code": services.next_employee_code(company),
        **_suggestions(company),
    })


@company_admin_required
def employee_update(request, pk):
    company = _company(request)
    emp = get_object_or_404(Employee, pk=pk, company=company)
    if request.method == "POST":
        form = EmployeeForm(request.POST, instance=emp, company=company)
        if form.is_valid():
            emp = form.save(commit=False)
            if not emp.employee_code:
                emp.employee_code = services.next_employee_code(company)
            emp.save()
            messages.success(request, "Employee updated. New salary figures apply to salary sheets "
                                      "generated from now on; approved / paid sheets are unchanged.")
            return redirect("employee_detail", pk=emp.pk)
    else:
        form = EmployeeForm(instance=emp, company=company)
    return render(request, "payroll/employee_form.html", {
        "form": form, "title": f"Edit {emp.full_name}", "employee": emp, **_suggestions(company),
    })


@company_admin_required
def employee_detail(request, pk):
    company = _company(request)
    emp = get_object_or_404(Employee, pk=pk, company=company)
    today = timezone.now().date()
    setting = services.get_setting(company)
    start, end, _ = services.month_bounds(today.year, today.month)
    att = {a.date: a for a in Attendance.objects.filter(employee=emp, date__range=[start, end])}
    breakdown = services.attendance_breakdown(
        emp, today.year, today.month, setting, att,
        services.employee_holidays(company, emp, start, end),
    )
    advances = list(emp.advances.all()[:10])
    return render(request, "payroll/employee_detail.html", {
        "employee": emp, "breakdown": breakdown, "month_label": f"{MONTH_NAMES[today.month]} {today.year}",
        "payslips": emp.payslips.order_by("-year", "-month")[:12],
        "advances": advances,
        "advance_outstanding": sum((a.outstanding for a in advances), ZERO),
    })


@company_admin_required
@require_POST
def employee_delete(request, pk):
    company = _company(request)
    emp = get_object_or_404(Employee, pk=pk, company=company)
    if emp.payslips.exists() or emp.advances.exists():
        messages.error(
            request,
            f"{emp.full_name} has salary / advance history and cannot be deleted. "
            "Change the status to Resigned or Inactive instead.",
        )
        return redirect("employee_detail", pk=emp.pk)
    name = emp.full_name
    emp.delete()
    messages.success(request, f"{name} deleted.")
    return redirect("employee_list")


# ---------------------------------------------------------------------------
# Attendance
# ---------------------------------------------------------------------------

def _parse_day(value, default):
    return parse_date(value or "") or default


def _parse_ot(text):
    text = (text or "").strip()
    if not text:
        return ZERO
    try:
        value = Decimal(text)
    except InvalidOperation:
        return None
    return value if ZERO <= value <= 24 else None


@company_admin_required
def attendance_daily(request):
    company = _company(request)
    ctx = get_branch_context(request)
    branch = ctx.active
    today = timezone.now().date()
    src = request.POST if request.method == "POST" else request.GET
    day = _parse_day(src.get("date"), today)

    employees = list(
        services.employees_in_scope(company, branch, active_only=True)
        .filter(join_date__lte=day)
        .filter(Q(left_date__isnull=True) | Q(left_date__gte=day))
        .order_by("branch_id", "employee_code")
    )
    existing = {
        a.employee_id: a
        for a in Attendance.objects.filter(company=company, date=day, employee__in=employees)
    }
    locked = services.month_locked_employee_ids(company, day.year, day.month)
    valid_status = dict(Attendance.STATUS_CHOICES)

    if request.method == "POST":
        errors, changes = [], []
        for emp in employees:
            if emp.pk in locked:
                continue
            key = str(emp.pk)
            # The form always submits every row. A row that is missing from the
            # submission is left untouched - only an explicitly empty status
            # ("not marked") clears an existing record.
            if f"status_{key}" not in request.POST:
                continue
            status = request.POST.get(f"status_{key}", "")
            if status and status not in valid_status:
                errors.append(f"{emp.full_name}: invalid status.")
                continue
            ot = _parse_ot(request.POST.get(f"ot_{key}"))
            if ot is None:
                errors.append(f"{emp.full_name}: overtime must be between 0 and 24 hours.")
                continue
            check_in = parse_time(request.POST.get(f"in_{key}", "") or "")
            check_out = parse_time(request.POST.get(f"out_{key}", "") or "")
            changes.append((emp, status, check_in, check_out, ot, request.POST.get(f"note_{key}", "").strip()[:200]))
        if errors:
            messages.error(request, " ".join(errors))
        else:
            saved = cleared = 0
            with transaction.atomic(using=company.db_alias):
                for emp, status, cin, cout, ot, note in changes:
                    rec = existing.get(emp.pk)
                    if not status:
                        if rec:
                            rec.delete()
                            cleared += 1
                        continue
                    if rec:
                        rec.status, rec.check_in, rec.check_out = status, cin, cout
                        rec.overtime_hours, rec.note = ot, note
                        rec.save()
                    else:
                        Attendance.objects.create(
                            company=company, employee=emp, date=day, status=status,
                            check_in=cin, check_out=cout, overtime_hours=ot, note=note,
                            created_by=request.user,
                        )
                    saved += 1
            msg = f"Attendance saved for {day:%d %b %Y}: {saved} record(s)"
            if cleared:
                msg += f", {cleared} cleared"
            if locked & {e.pk for e in employees}:
                msg += ". Staff with an approved / paid salary for this month were left unchanged"
            messages.success(request, msg + ".")
            return redirect(f"{request.path}?date={day.isoformat()}")

    rows = [{"emp": e, "rec": existing.get(e.pk), "locked": e.pk in locked} for e in employees]
    setting = services.get_setting(company)
    hol = services.holiday_dates(company, branch, day, day)
    return render(request, "payroll/attendance_daily.html", {
        "day": day, "prev_day": day - timedelta(days=1), "next_day": day + timedelta(days=1),
        "rows": rows, "status_choices": Attendance.STATUS_CHOICES,
        "is_weekly_off": day.weekday() in setting.weekly_off_set, "is_holiday": bool(hol),
        "summary": services.attendance_for_day(company, branch, day),
    })


def _attendance_matrix(company, branch, year, month):
    start, end, days = services.month_bounds(year, month)
    setting = services.get_setting(company)
    off_days = setting.weekly_off_set
    today = timezone.now().date()
    employees = list(services.eligible_employees(company, branch, year, month).order_by("employee_code"))
    ids = [e.pk for e in employees]
    recs = {}
    for a in Attendance.objects.filter(company=company, date__range=[start, end], employee_id__in=ids):
        recs[(a.employee_id, a.date)] = a
    all_holidays = list(Holiday.objects.filter(company=company, date__range=[start, end]))

    header = []
    for d in range(1, days + 1):
        day = date(year, month, d)
        header.append({"day": d, "wd": day.strftime("%a"), "today": day == today, "off": day.weekday() in off_days,
                       "holiday": any(h.date == day and (h.branch_id is None or branch is None or h.branch_id == branch.pk) for h in all_holidays)})

    rows = []
    for emp in employees:
        hol = {h.date for h in all_holidays if h.branch_id is None or h.branch_id == emp.branch_id}
        cells = []
        t = {"P": 0, "L": 0, "A": 0, "H": 0, "PL": 0, "UL": 0, "OFF": 0, "OT": ZERO}
        for d in range(1, days + 1):
            day = date(year, month, d)
            rec = recs.get((emp.pk, day))
            is_off, is_hol = day.weekday() in off_days, day in hol
            if day < emp.join_date or (emp.left_date and day > emp.left_date):
                cells.append({"code": "", "css": "na", "title": "Not employed"})
                continue
            if rec:
                t["OT"] += rec.overtime_hours or ZERO
            if rec and not ((is_off or is_hol) and rec.status not in (Attendance.PRESENT, Attendance.LATE, Attendance.HALF_DAY)):
                code = Attendance.CODES[rec.status]
                cells.append({"code": code, "css": rec.status, "title": rec.get_status_display()})
                key = {"present": "P", "late": "L", "absent": "A", "half_day": "H",
                       "paid_leave": "PL", "unpaid_leave": "UL"}[rec.status]
                t[key] += 1
            elif is_off:
                cells.append({"code": "W", "css": "off", "title": "Weekly off"}); t["OFF"] += 1
            elif is_hol:
                cells.append({"code": "H", "css": "holiday", "title": "Holiday"}); t["OFF"] += 1
            else:
                cells.append({"code": "·", "css": "unmarked" if day <= today else "future",
                              "title": "Not marked"})
        rows.append({"emp": emp, "cells": cells, "t": t,
                     "attended": t["P"] + t["L"] + t["H"] * 1})
    return header, rows


@company_admin_required
def attendance_sheet(request):
    company = _company(request)
    branch = get_active_branch(request)
    year, month = services.parse_month(request.GET.get("month"))
    header, rows = _attendance_matrix(company, branch, year, month)
    return render(request, "payroll/attendance_sheet.html", {
        "header": header, "rows": rows, **_month_context(year, month),
    })


@company_admin_required
def attendance_sheet_export(request, fmt):
    company = _company(request)
    ctx = get_branch_context(request)
    year, month = services.parse_month(request.GET.get("month"))
    header, rows = _attendance_matrix(company, ctx.active, year, month)
    title = f"Attendance Sheet - {MONTH_NAMES[month]} {year} ({ctx.active_label})"
    head = ["Code", "Employee"] + [str(h["day"]) for h in header] + ["P", "L", "A", "1/2", "PL", "UL", "Off", "OT hrs"]
    body = []
    for r in rows:
        t = r["t"]
        body.append([r["emp"].employee_code, r["emp"].full_name] + [c["code"].replace("\u00bd", "1/2") for c in r["cells"]]
                    + [t["P"], t["L"], t["A"], t["H"], t["PL"], t["UL"], t["OFF"], f"{t['OT']:.1f}"])
    filename = f"attendance_{year}-{month:02d}"
    if fmt == "csv":
        resp = HttpResponse(content_type="text/csv")
        resp["Content-Disposition"] = f'attachment; filename="{filename}.csv"'
        w = csv.writer(resp)
        w.writerow([title])
        w.writerow(head)
        w.writerows(body)
        return resp
    resp = HttpResponse(content_type="application/pdf")
    resp["Content-Disposition"] = f'attachment; filename="{filename}.pdf"'
    doc = SimpleDocTemplate(resp, pagesize=landscape(A4), leftMargin=18, rightMargin=18, topMargin=24, bottomMargin=24)
    styles = getSampleStyleSheet()
    table = Table([head] + body, repeatRows=1)
    table.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1f2937")),
        ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
        ("FONTSIZE", (0, 0), (-1, -1), 6),
        ("GRID", (0, 0), (-1, -1), 0.25, colors.grey),
        ("ALIGN", (2, 0), (-1, -1), "CENTER"),
    ]))
    doc.build([Paragraph(f"{company.name} - {title}", styles["Title"]), Spacer(1, 6), table])
    return resp


# ---------------------------------------------------------------------------
# Monthly salary sheet
# ---------------------------------------------------------------------------

def _sheet_payslips(company, branch, year, month):
    return scope(
        Payslip.objects.filter(company=company, year=year, month=month).select_related("employee"),
        branch,
    ).order_by("employee__employee_code")


@company_admin_required
def salary_sheet(request):
    company = _company(request)
    branch = get_active_branch(request)
    year, month = services.parse_month(request.GET.get("month"))
    payslips = list(_sheet_payslips(company, branch, year, month))
    eligible = services.eligible_employees(company, branch, year, month)
    have = {p.employee_id for p in payslips}
    missing = [e for e in eligible if e.pk not in have]

    def total(field, only=None):
        return sum((getattr(p, field) for p in payslips if only is None or p.status == only), ZERO)

    totals = {
        "gross": total("gross_salary"), "overtime": total("overtime_amount"), "bonus": total("bonus"),
        "absence": total("absence_deduction"), "advance": total("advance_deduction"),
        "other": total("other_deduction"), "net": total("net_salary"),
        "paid": total("net_salary", Payslip.PAID),
    }
    totals["pending"] = totals["net"] - totals["paid"]
    counts = {s: sum(1 for p in payslips if p.status == s) for s, _ in Payslip.STATUS_CHOICES}
    return render(request, "payroll/salary_sheet.html", {
        "payslips": payslips, "totals": totals, "counts": counts, "missing_count": len(missing),
        "pay_form": PayForm(initial={"paid_date": timezone.now().date(), "payment_method": "cash"}),
        **_month_context(year, month),
    })


@company_admin_required
@require_POST
def salary_generate(request):
    company = _company(request)
    year, month = services.parse_month(request.POST.get("month"))
    result = services.generate_payroll(company, get_active_branch(request), year, month, request.user)
    if not any(result.values()):
        messages.warning(request, "No eligible employees found. Add staff first, or check joining dates.")
    else:
        msg = f"Salary sheet for {MONTH_NAMES[month]} {year}: {result['created']} created, {result['updated']} refreshed"
        if result["skipped"]:
            msg += f", {result['skipped']} approved/paid sheet(s) left unchanged"
        messages.success(request, msg + ".")
    return redirect(f"/payroll/salary/?month={year}-{month:02d}")


@company_admin_required
@require_POST
def salary_bulk(request):
    company = _company(request)
    year, month = services.parse_month(request.POST.get("month"))
    action = request.POST.get("action")
    back = redirect(f"/payroll/salary/?month={year}-{month:02d}")
    payslips = list(_sheet_payslips(company, get_active_branch(request), year, month))
    try:
        with transaction.atomic(using=company.db_alias):
            if action == "approve_all":
                targets = [p for p in payslips if p.status == Payslip.DRAFT]
                for p in targets:
                    services.approve_payslip(p)
                messages.success(request, f"{len(targets)} payslip(s) approved.")
            elif action == "pay_all":
                form = PayForm(request.POST)
                if not form.is_valid():
                    messages.error(request, "Enter a valid payment date and method.")
                    return back
                targets = [p for p in payslips if p.status != Payslip.PAID]
                for p in targets:
                    services.pay_payslip(
                        company, p, form.cleaned_data["paid_date"],
                        form.cleaned_data["payment_method"], form.cleaned_data["reference"],
                    )
                messages.success(request, f"{len(targets)} payslip(s) marked as paid.")
            else:
                messages.error(request, "Unknown action.")
    except ValueError as exc:
        messages.error(request, str(exc))
    return back


def _salary_rows(payslips, ctx):
    head = ["Code", "Employee", "Branch", "Gross", "Absent/Ded. days", "Absence ded.", "Overtime",
            "Bonus", "Advance", "Other ded.", "Net salary", "Status"]
    body = []
    for p in payslips:
        body.append([
            p.employee.employee_code, p.employee.full_name,
            ctx.label_map.get(str(p.branch_id), "-"), f"{p.gross_salary:.2f}", f"{p.deduction_days:.1f}",
            f"{p.absence_deduction:.2f}", f"{p.overtime_amount:.2f}", f"{p.bonus:.2f}",
            f"{p.advance_deduction:.2f}", f"{p.other_deduction:.2f}", f"{p.net_salary:.2f}",
            p.get_status_display(),
        ])
    return head, body


@company_admin_required
def salary_sheet_export(request, fmt):
    company = _company(request)
    ctx = get_branch_context(request)
    year, month = services.parse_month(request.GET.get("month"))
    payslips = list(_sheet_payslips(company, ctx.active, year, month))
    head, body = _salary_rows(payslips, ctx)
    total_net = sum((p.net_salary for p in payslips), ZERO)
    title = f"Salary Sheet - {MONTH_NAMES[month]} {year} ({ctx.active_label})"
    filename = f"salary_sheet_{year}-{month:02d}"
    if fmt == "csv":
        resp = HttpResponse(content_type="text/csv")
        resp["Content-Disposition"] = f'attachment; filename="{filename}.csv"'
        w = csv.writer(resp)
        w.writerow([title])
        w.writerow(head)
        w.writerows(body)
        w.writerow([""] * 9 + ["TOTAL NET", f"{total_net:.2f}"])
        return resp
    resp = HttpResponse(content_type="application/pdf")
    resp["Content-Disposition"] = f'attachment; filename="{filename}.pdf"'
    doc = SimpleDocTemplate(resp, pagesize=landscape(A4), leftMargin=24, rightMargin=24, topMargin=28, bottomMargin=28)
    styles = getSampleStyleSheet()
    table = Table([head] + body + [[""] * 9 + ["TOTAL", f"{total_net:.2f}", ""]], repeatRows=1)
    table.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1f2937")),
        ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
        ("FONTSIZE", (0, 0), (-1, -1), 7),
        ("GRID", (0, 0), (-1, -1), 0.25, colors.grey),
        ("FONTNAME", (0, -1), (-1, -1), "Helvetica-Bold"),
        ("ALIGN", (3, 0), (-2, -1), "RIGHT"),
    ]))
    doc.build([Paragraph(f"{company.name} - {title}", styles["Title"]), Spacer(1, 6), table])
    return resp


# ---------------------------------------------------------------------------
# Payslips
# ---------------------------------------------------------------------------

def _get_payslip(request, pk):
    return get_object_or_404(
        Payslip.objects.select_related("employee"), pk=pk, company=_company(request)
    )


@company_admin_required
def payslip_detail(request, pk):
    company = _company(request)
    ctx = get_branch_context(request)
    p = _get_payslip(request, pk)
    return render(request, "payroll/payslip_detail.html", {
        "p": p, "company": company, "branch": ctx.get(p.branch_id),
        "month_label": f"{MONTH_NAMES[p.month]} {p.year}",
        "pay_form": PayForm(initial={"paid_date": timezone.now().date(), "payment_method": p.employee.payment_method}),
    })


@company_admin_required
def payslip_edit(request, pk):
    company = _company(request)
    p = _get_payslip(request, pk)
    if p.is_locked:
        messages.error(request, "Approved / paid payslips cannot be edited. Re-open it first.")
        return redirect("payslip_detail", pk=p.pk)
    adv = sum((a.outstanding for a in SalaryAdvance.objects.filter(company=company, employee=p.employee)), ZERO)
    if request.method == "POST":
        form = PayslipEditForm(request.POST, instance=p, outstanding_advance=adv)
        if form.is_valid():
            obj = form.save(commit=False)
            services.recalc_net(obj)
            obj.save()
            messages.success(request, "Payslip updated.")
            return redirect(f"/payroll/salary/?month={p.year}-{p.month:02d}")
    else:
        form = PayslipEditForm(instance=p, outstanding_advance=adv)
    return render(request, "payroll/payslip_form.html", {
        "form": form, "p": p, "month_label": f"{MONTH_NAMES[p.month]} {p.year}",
    })


@company_admin_required
@require_POST
def payslip_action(request, pk, action):
    company = _company(request)
    p = _get_payslip(request, pk)
    back = redirect(f"/payroll/salary/?month={p.year}-{p.month:02d}")
    try:
        with transaction.atomic(using=company.db_alias):
            if action == "approve":
                services.approve_payslip(p)
                messages.success(request, f"{p.employee.full_name}: approved.")
            elif action == "reopen":
                services.reopen_payslip(p)
                messages.success(request, f"{p.employee.full_name}: re-opened as draft.")
            elif action == "pay":
                form = PayForm(request.POST)
                if not form.is_valid():
                    messages.error(request, "Enter a valid payment date and method.")
                    return back
                services.pay_payslip(company, p, form.cleaned_data["paid_date"],
                                     form.cleaned_data["payment_method"], form.cleaned_data["reference"])
                messages.success(request, f"{p.employee.full_name}: marked as paid.")
            elif action == "revert":
                services.revert_payment(company, p)
                messages.success(request, f"{p.employee.full_name}: payment reverted to approved.")
            elif action == "delete":
                if p.is_locked:
                    raise ValueError("Only draft payslips can be deleted.")
                p.delete()
                messages.success(request, "Draft payslip deleted.")
            else:
                messages.error(request, "Unknown action.")
    except ValueError as exc:
        messages.error(request, str(exc))
    return back


# ---------------------------------------------------------------------------
# Salary advances
# ---------------------------------------------------------------------------

@company_admin_required
def advance_list(request):
    company = _company(request)
    branch = get_active_branch(request)
    advances = list(
        scope(SalaryAdvance.objects.filter(company=company).select_related("employee"), branch)
    )
    return render(request, "payroll/advance_list.html", {
        "advances": advances,
        "total_given": sum((a.amount for a in advances), ZERO),
        "total_outstanding": sum((a.outstanding for a in advances), ZERO),
    })


@company_admin_required
def advance_create(request):
    company = _company(request)
    branch = get_active_branch(request)
    employees = services.employees_in_scope(company, branch, active_only=True).order_by("employee_code")
    if request.method == "POST":
        form = AdvanceForm(request.POST, employees=employees)
        if form.is_valid():
            adv = form.save(commit=False)
            adv.company = company
            adv.branch_id = adv.employee.branch_id
            adv.created_by = request.user
            adv.save()
            messages.success(request, f"Advance of {adv.amount:.2f} recorded for {adv.employee.full_name}.")
            return redirect("advance_list")
    else:
        form = AdvanceForm(employees=employees, initial={"date": timezone.now().date()})
    return render(request, "payroll/advance_form.html", {"form": form})


@company_admin_required
@require_POST
def advance_delete(request, pk):
    adv = get_object_or_404(SalaryAdvance, pk=pk, company=_company(request))
    if adv.recovered > 0:
        messages.error(request, "This advance is already (partly) recovered from a salary and cannot be deleted.")
    else:
        adv.delete()
        messages.success(request, "Advance deleted.")
    return redirect("advance_list")


# ---------------------------------------------------------------------------
# Holidays and settings
# ---------------------------------------------------------------------------

@company_admin_required
def holiday_list(request):
    company = _company(request)
    year = int(request.GET.get("year") or timezone.now().year)
    if request.method == "POST":
        form = HolidayForm(request.POST, company=company)
        if form.is_valid():
            h = form.save(commit=False)
            h.company = company
            h.save()
            messages.success(request, "Holiday added.")
            return redirect(f"/payroll/holidays/?year={h.date.year}")
    else:
        form = HolidayForm(company=company)
    holidays = Holiday.objects.filter(company=company, date__year=year).order_by("date")
    setting = services.get_setting(company)
    names = dict(WEEKDAY_CHOICES)
    return render(request, "payroll/holiday_list.html", {
        "form": form, "holidays": holidays, "year": year,
        "weekly_off_names": ", ".join(names[str(d)] for d in sorted(setting.weekly_off_set)) or "none",
    })


@company_admin_required
@require_POST
def holiday_delete(request, pk):
    h = get_object_or_404(Holiday, pk=pk, company=_company(request))
    year = h.date.year
    h.delete()
    messages.success(request, "Holiday removed.")
    return redirect(f"/payroll/holidays/?year={year}")


@company_admin_required
def payroll_settings(request):
    company = _company(request)
    setting = services.get_setting(company)
    if request.method == "POST":
        form = PayrollSettingForm(request.POST, instance=setting)
        if form.is_valid():
            form.save()
            messages.success(request, "Payroll settings saved. Refresh a draft salary sheet to apply them.")
            return redirect("payroll_settings")
    else:
        form = PayrollSettingForm(instance=setting)
    return render(request, "payroll/settings.html", {"form": form})
