from django.contrib import messages
from django.core.exceptions import ValidationError
from django.core.paginator import Paginator
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect, render

from apps.accounts.permissions import (
    ROLE_HR_ADMIN, ROLE_PAYROLL_OFFICER, ROLE_SUPER_ADMIN, has_role, require_roles,
    accessible_company_ids, scope_queryset_by_company,
)
from apps.attendance.models import OvertimeEntry
from apps.audit.services import log_event
from apps.employees.models import Employee
from apps.workflow.models import WorkflowTask
from apps.workflow.services import create_workflow_task_for_employee
from .forms import BonusForm, DeductionForm, OvertimeEntryForm, PayrollAdjustmentForm, PayrollPeriodForm, PayrollUnlockForm, SalaryAssignmentForm
from .models import Bonus, Deduction, EmployeeSalaryAssignment, PayrollPeriod, PayrollRecord
from .services import build_payroll_period, transition_payroll_period


PAYROLL_ROLES = (ROLE_SUPER_ADMIN, ROLE_HR_ADMIN, ROLE_PAYROLL_OFFICER)


def _sync_pending_input_task(*, item, source_type, title):
    task = WorkflowTask.objects.select_for_update().filter(
        source_type=source_type, source_id=str(item.pk), status=WorkflowTask.Status.OPEN
    ).first()
    if not task:
        raise ValidationError("Open approval task is missing for this pending record; edit blocked until workflow integrity is restored.")
    if task.company_id != item.employee.company_id:
        raise ValidationError("Approval task company does not match the source employee company; edit blocked.")
    if task.title != title:
        task.title = title
        task.save(update_fields=["title", "updated_at"])
    return task


def _input_snapshot(item, fields):
    values = {}
    for field in fields:
        value = getattr(item, field)
        if hasattr(value, "isoformat"):
            value = value.isoformat()
        values[field] = str(value) if value is not None else ""
    return values


def _employee_queryset(user):
    qs = Employee.objects.filter(status__in=["ACTIVE", "PROBATION", "CONFIRMED"]).select_related("company", "department", "designation").order_by("employee_code")
    return scope_queryset_by_company(user, qs, "company")


def _create_approval_task(*, employee, module, title, source_type, source_id, reference_no, preferred_role="HR Admin", priority="NORMAL"):
    existing = WorkflowTask.objects.filter(source_type=source_type, source_id=str(source_id), status=WorkflowTask.Status.OPEN).first()
    if existing:
        return existing
    return create_workflow_task_for_employee(
        employee=employee,
        module=module,
        reference_no=reference_no,
        title=title,
        source_type=source_type,
        source_id=source_id,
        priority=priority,
        preferred_role=preferred_role,
    )


@require_roles(*PAYROLL_ROLES)
def salary_assignments(request):
    qs = scope_queryset_by_company(request.user, EmployeeSalaryAssignment.objects.select_related("employee", "structure"), "employee__company").order_by("employee__employee_code", "-effective_from")
    page_obj = Paginator(qs, 100).get_page(request.GET.get("page"))
    return render(request, "payroll/salary_assignments.html", {"items": page_obj.object_list, "page_obj": page_obj})


@require_roles(ROLE_SUPER_ADMIN, ROLE_HR_ADMIN)
def salary_assignment_create(request):
    company_ids = accessible_company_ids(request.user)
    form = SalaryAssignmentForm(request.POST or None, employee_queryset=_employee_queryset(request.user), company_ids=company_ids)
    if request.method == "POST" and form.is_valid():
        item = form.save()
        log_event(user=request.user, action="ASSIGN_SALARY", module="salary", object_id=item.pk, object_repr=str(item.employee), new_values={"gross_salary": str(item.gross_salary), "basic_salary": str(item.basic_salary), "effective_from": str(item.effective_from)})
        messages.success(request, "Salary assignment saved.")
        return redirect("payroll:salary_assignments")
    return render(request, "payroll/simple_form.html", {"form": form, "page_title": "Assign Employee Salary"})


@require_roles(*PAYROLL_ROLES)
def overtime_list(request):
    qs = scope_queryset_by_company(request.user, OvertimeEntry.objects.select_related("employee"), "employee__company").order_by("-work_date", "employee__employee_code")
    page_obj = Paginator(qs, 100).get_page(request.GET.get("page"))
    return render(request, "payroll/input_list.html", {"items": page_obj.object_list, "page_obj": page_obj, "kind": "Overtime"})


@require_roles(*PAYROLL_ROLES)
def overtime_create(request):
    form = OvertimeEntryForm(request.POST or None, employee_queryset=_employee_queryset(request.user).filter(ot_eligible=True))
    if request.method == "POST" and form.is_valid():
        item = form.save(commit=False)
        item.status = "PENDING"
        item.approved_minutes = 0
        item.save()
        ref = f"OT-{item.pk:06d}"
        _create_approval_task(employee=item.employee, module="Overtime", title=f"OT approval: {item.employee.full_name} / {item.work_date}", source_type="overtime_entry", source_id=item.pk, reference_no=ref, preferred_role="HR Admin", priority="MEDIUM")
        log_event(user=request.user, company=item.employee.company, action="SUBMIT", module="overtime", object_id=item.pk, object_repr=ref, new_values={"employee": item.employee_id, "work_date": str(item.work_date), "requested_minutes": item.requested_minutes, "rate_multiplier": str(item.rate_multiplier)}, request=request)
        messages.success(request, "OT entry submitted for approval.")
        return redirect("payroll:overtime")
    return render(request, "payroll/simple_form.html", {"form": form, "page_title": "Add Overtime Entry"})


@require_roles(*PAYROLL_ROLES)
def overtime_edit(request, pk):
    item = get_object_or_404(scope_queryset_by_company(request.user, OvertimeEntry.objects.select_related("employee", "employee__company"), "employee__company"), pk=pk)
    if item.status != "PENDING":
        messages.error(request, "Only pending overtime entries can be edited. Approved/rejected OT is immutable; use the controlled correction/adjustment process where applicable.")
        return redirect("payroll:overtime")
    form = OvertimeEntryForm(request.POST or None, instance=item, employee_queryset=_employee_queryset(request.user).filter(ot_eligible=True))
    if request.method == "POST" and form.is_valid():
        old_values = _input_snapshot(item, ["employee_id", "work_date", "requested_minutes", "rate_multiplier", "reason"])
        try:
            with transaction.atomic():
                updated = form.save(commit=False)
                updated.status = "PENDING"
                updated.approved_minutes = 0
                updated.save()
                _sync_pending_input_task(
                    item=updated, source_type="overtime_entry",
                    title=f"OT approval: {updated.employee.full_name} / {updated.work_date}",
                )
                log_event(
                    user=request.user, company=updated.employee.company, action="EDIT_PENDING", module="overtime",
                    object_id=updated.pk, object_repr=f"OT-{updated.pk:06d}", old_values=old_values,
                    new_values=_input_snapshot(updated, ["employee_id", "work_date", "requested_minutes", "rate_multiplier", "reason"]),
                    request=request,
                )
            messages.success(request, "Pending overtime entry updated; approval task context was refreshed.")
            return redirect("payroll:overtime")
        except ValidationError as exc:
            form.add_error(None, exc)
    return render(request, "payroll/simple_form.html", {"form": form, "page_title": "Edit Overtime Entry"})


@require_roles(*PAYROLL_ROLES)
def bonus_list(request):
    qs = scope_queryset_by_company(request.user, Bonus.objects.select_related("employee"), "employee__company").order_by("-payroll_month", "employee__employee_code")
    page_obj = Paginator(qs, 100).get_page(request.GET.get("page"))
    return render(request, "payroll/input_list.html", {"items": page_obj.object_list, "page_obj": page_obj, "kind": "Bonus"})


@require_roles(*PAYROLL_ROLES)
def bonus_create(request):
    form = BonusForm(request.POST or None, employee_queryset=_employee_queryset(request.user))
    if request.method == "POST" and form.is_valid():
        item = form.save(commit=False)
        item.status = "PENDING"
        item.save()
        ref = f"BONUS-{item.pk:06d}"
        _create_approval_task(employee=item.employee, module="Bonus", title=f"Bonus approval: {item.employee.full_name} / {item.bonus_type}", source_type="bonus", source_id=item.pk, reference_no=ref, preferred_role="HR Admin")
        log_event(user=request.user, company=item.employee.company, action="SUBMIT", module="bonus", object_id=item.pk, object_repr=ref, new_values={"employee": item.employee_id, "amount": str(item.amount), "payroll_month": str(item.payroll_month), "bonus_type": item.bonus_type}, request=request)
        messages.success(request, "Bonus submitted for approval.")
        return redirect("payroll:bonus")
    return render(request, "payroll/simple_form.html", {"form": form, "page_title": "Add Bonus"})


@require_roles(*PAYROLL_ROLES)
def bonus_edit(request, pk):
    item = get_object_or_404(scope_queryset_by_company(request.user, Bonus.objects.select_related("employee", "employee__company"), "employee__company"), pk=pk)
    if item.status != "PENDING":
        messages.error(request, "Only pending bonus entries can be edited. Approved/rejected bonus entries are immutable; use the controlled adjustment/arrear process where applicable.")
        return redirect("payroll:bonus")
    form = BonusForm(request.POST or None, instance=item, employee_queryset=_employee_queryset(request.user))
    if request.method == "POST" and form.is_valid():
        old_values = _input_snapshot(item, ["employee_id", "payroll_month", "bonus_type", "amount", "reason"])
        try:
            with transaction.atomic():
                updated = form.save(commit=False)
                updated.status = "PENDING"
                updated.save()
                _sync_pending_input_task(
                    item=updated, source_type="bonus",
                    title=f"Bonus approval: {updated.employee.full_name} / {updated.bonus_type}",
                )
                log_event(
                    user=request.user, company=updated.employee.company, action="EDIT_PENDING", module="bonus",
                    object_id=updated.pk, object_repr=f"BONUS-{updated.pk:06d}", old_values=old_values,
                    new_values=_input_snapshot(updated, ["employee_id", "payroll_month", "bonus_type", "amount", "reason"]),
                    request=request,
                )
            messages.success(request, "Pending bonus updated; approval task context was refreshed.")
            return redirect("payroll:bonus")
        except ValidationError as exc:
            form.add_error(None, exc)
    return render(request, "payroll/simple_form.html", {"form": form, "page_title": "Edit Bonus"})


@require_roles(*PAYROLL_ROLES)
def deduction_list(request):
    qs = scope_queryset_by_company(request.user, Deduction.objects.select_related("employee"), "employee__company").order_by("-payroll_month", "employee__employee_code")
    page_obj = Paginator(qs, 100).get_page(request.GET.get("page"))
    return render(request, "payroll/input_list.html", {"items": page_obj.object_list, "page_obj": page_obj, "kind": "Deduction"})


@require_roles(*PAYROLL_ROLES)
def deduction_create(request):
    form = DeductionForm(request.POST or None, employee_queryset=_employee_queryset(request.user))
    if request.method == "POST" and form.is_valid():
        item = form.save(commit=False)
        item.status = "PENDING"
        item.save()
        ref = f"DED-{item.pk:06d}"
        _create_approval_task(employee=item.employee, module="Deduction", title=f"Deduction approval: {item.employee.full_name} / {item.deduction_type}", source_type="deduction", source_id=item.pk, reference_no=ref, preferred_role="HR Admin")
        log_event(user=request.user, company=item.employee.company, action="SUBMIT", module="deduction", object_id=item.pk, object_repr=ref, new_values={"employee": item.employee_id, "amount": str(item.amount), "payroll_month": str(item.payroll_month), "deduction_type": item.deduction_type}, request=request)
        messages.success(request, "Deduction submitted for approval.")
        return redirect("payroll:deduction")
    return render(request, "payroll/simple_form.html", {"form": form, "page_title": "Add Deduction"})


@require_roles(*PAYROLL_ROLES)
def deduction_edit(request, pk):
    item = get_object_or_404(scope_queryset_by_company(request.user, Deduction.objects.select_related("employee", "employee__company"), "employee__company"), pk=pk)
    if item.status != "PENDING":
        messages.error(request, "Only pending deduction entries can be edited. Approved/rejected deductions are immutable; use the controlled adjustment/arrear process where applicable.")
        return redirect("payroll:deduction")
    form = DeductionForm(request.POST or None, instance=item, employee_queryset=_employee_queryset(request.user))
    if request.method == "POST" and form.is_valid():
        old_values = _input_snapshot(item, ["employee_id", "payroll_month", "deduction_type", "amount", "reason"])
        try:
            with transaction.atomic():
                updated = form.save(commit=False)
                updated.status = "PENDING"
                updated.save()
                _sync_pending_input_task(
                    item=updated, source_type="deduction",
                    title=f"Deduction approval: {updated.employee.full_name} / {updated.deduction_type}",
                )
                log_event(
                    user=request.user, company=updated.employee.company, action="EDIT_PENDING", module="deduction",
                    object_id=updated.pk, object_repr=f"DED-{updated.pk:06d}", old_values=old_values,
                    new_values=_input_snapshot(updated, ["employee_id", "payroll_month", "deduction_type", "amount", "reason"]),
                    request=request,
                )
            messages.success(request, "Pending deduction updated; approval task context was refreshed.")
            return redirect("payroll:deduction")
        except ValidationError as exc:
            form.add_error(None, exc)
    return render(request, "payroll/simple_form.html", {"form": form, "page_title": "Edit Deduction"})


@require_roles(*PAYROLL_ROLES)
def period_list(request):
    qs = scope_queryset_by_company(request.user, PayrollPeriod.objects.select_related("company"), "company").order_by("-year", "-month")
    page_obj = Paginator(qs, 60).get_page(request.GET.get("page"))
    return render(request, "payroll/period_list.html", {"periods": page_obj.object_list, "page_obj": page_obj})


@require_roles(*PAYROLL_ROLES)
def period_create(request):
    form = PayrollPeriodForm(request.POST or None, company_ids=accessible_company_ids(request.user))
    if request.method == "POST" and form.is_valid():
        period = form.save()
        log_event(user=request.user, action="CREATE", module="payroll_period", object_id=period.pk, object_repr=f"{period.year}-{period.month:02d}", new_values={"status": period.status})
        messages.success(request, "Payroll period created.")
        return redirect("payroll:period_detail", pk=period.pk)
    return render(request, "payroll/simple_form.html", {"form": form, "page_title": "Create Payroll Period"})


@require_roles(*PAYROLL_ROLES)
def period_detail(request, pk):
    period = get_object_or_404(scope_queryset_by_company(request.user, PayrollPeriod.objects.select_related("company"), "company"), pk=pk)
    records = period.records.select_related("employee", "salary_assignment").order_by("employee__employee_code")
    return render(request, "payroll/period_detail.html", {"period": period, "records": records})


@require_roles(ROLE_SUPER_ADMIN, ROLE_PAYROLL_OFFICER)
def payroll_adjustment_create(request, period_pk, record_pk):
    period = get_object_or_404(
        scope_queryset_by_company(request.user, PayrollPeriod.objects.all(), "company"),
        pk=period_pk,
    )
    payroll = get_object_or_404(
        PayrollRecord.objects.select_related("employee", "period"),
        pk=record_pk,
        period=period,
    )
    if period.status != PayrollPeriod.Status.DRAFT:
        messages.error(request, "Manual adjustment can only be added while payroll is in Draft status.")
        return redirect("payroll:period_detail", pk=period.pk)
    if request.method != "POST":
        return redirect("payroll:period_detail", pk=period.pk)

    form = PayrollAdjustmentForm(request.POST)
    if form.is_valid():
        item = form.save(commit=False)
        item.payroll = payroll
        item.save()
        log_event(
            user=request.user,
            action="ADD_PAYROLL_ADJUSTMENT",
            module="payroll",
            object_id=item.pk,
            object_repr=f"{payroll.employee} / {period.year}-{period.month:02d}",
            new_values={
                "adjustment_type": item.adjustment_type,
                "amount": str(item.amount),
                "reason": item.reason,
                "reference_no": item.reference_no,
            },
        )
        try:
            build_payroll_period(period, user=request.user)
            messages.success(request, "Payroll adjustment saved and Draft payroll recalculated.")
        except ValidationError as exc:
            messages.warning(request, f"Adjustment saved, but recalculation needs attention: {exc}")
    else:
        messages.error(request, "; ".join([str(e) for errors in form.errors.values() for e in errors]))
    return redirect("payroll:period_detail", pk=period.pk)


@require_roles(*PAYROLL_ROLES)
def period_build(request, pk):
    period = get_object_or_404(scope_queryset_by_company(request.user, PayrollPeriod.objects.all(), "company"), pk=pk)
    if not has_role(request.user, ROLE_SUPER_ADMIN, ROLE_PAYROLL_OFFICER):
        messages.error(request, "Payroll calculation must be prepared by Payroll Officer / Super Admin.")
        return redirect("payroll:period_detail", pk=pk)
    if request.method != "POST":
        return redirect("payroll:period_detail", pk=pk)
    if period.status != PayrollPeriod.Status.DRAFT:
        messages.error(request, "Payroll can only be calculated/recalculated while the period is in Draft status. Return it to Draft through the controlled workflow before rebuilding.")
        return redirect("payroll:period_detail", pk=pk)
    try:
        result = build_payroll_period(period, user=request.user)
        messages.success(request, f"Payroll recalculated: {result['created']} created, {result['updated']} updated, {result['skipped']} skipped outside the employment service period, {result.get('stale_removed', 0)} stale Draft record(s) removed.")
    except ValidationError as exc:
        messages.error(request, str(exc))
    return redirect("payroll:period_detail", pk=pk)


@require_roles(*PAYROLL_ROLES)
def period_action(request, pk, action):
    period = get_object_or_404(scope_queryset_by_company(request.user, PayrollPeriod.objects.all(), "company"), pk=pk)
    action = action.upper().strip()
    if request.method != "POST":
        return redirect("payroll:period_detail", pk=pk)
    if action == "UNLOCK":
        if not has_role(request.user, ROLE_SUPER_ADMIN, ROLE_HR_ADMIN):
            messages.error(request, "Only HR Admin / Super Admin may unlock payroll.")
            return redirect("payroll:period_detail", pk=pk)
        form = PayrollUnlockForm(request.POST)
        if form.is_valid():
            try:
                transition_payroll_period(period, action="UNLOCK", user=request.user, reason=form.cleaned_data["reason"])
                messages.success(request, "Payroll unlocked with audit reason.")
            except ValidationError as exc:
                messages.error(request, str(exc))
        else:
            messages.error(request, "A valid unlock reason is required.")
        return redirect("payroll:period_detail", pk=pk)

    if action not in {"REVIEW", "APPROVE", "LOCK", "PAY"}:
        messages.error(request, "Unsupported payroll action.")
        return redirect("payroll:period_detail", pk=pk)

    expected_status = {
        "REVIEW": PayrollPeriod.Status.DRAFT,
        "APPROVE": PayrollPeriod.Status.REVIEWED,
        "LOCK": PayrollPeriod.Status.APPROVED,
        "PAY": PayrollPeriod.Status.LOCKED,
    }[action]
    if period.status != expected_status:
        messages.error(request, f"Payroll must be {expected_status.title()} before creating the {action.title()} task.")
        return redirect("payroll:period_detail", pk=pk)

    if action == "PAY":
        if period.status != PayrollPeriod.Status.LOCKED:
            messages.error(request, "Payroll must be Locked before payment can be posted.")
            return redirect("payroll:period_detail", pk=pk)
        if period.payment_status == PayrollPeriod.PaymentStatus.PAID:
            messages.info(request, "Payroll payment has already been posted.")
            return redirect("payroll:period_detail", pk=pk)
        role = "Accounts"
    else:
        role = "Payroll Officer" if action == "REVIEW" else "HR Admin"
    existing = WorkflowTask.objects.filter(source_type="payroll_period", source_id=str(period.pk), status=WorkflowTask.Status.OPEN, payload__payroll_action=action).first()
    if existing:
        messages.info(request, "An open approval task already exists for this payroll action.")
        return redirect("payroll:period_detail", pk=pk)
    WorkflowTask.objects.create(
        company=period.company,
        module="Payroll",
        reference_no=f"PAY-{period.year}{period.month:02d}-{action}",
        title=f"Payroll {action.title()}: {period.year}-{period.month:02d}",
        assigned_role=role,
        priority="HIGH" if action in {"APPROVE", "LOCK", "PAY"} else "MEDIUM",
        source_type="payroll_period",
        source_id=str(period.pk),
        payload={"payroll_action": action},
    )
    messages.success(request, f"Payroll {action.lower()} task created for {role}.")
    return redirect("payroll:period_detail", pk=pk)
