from datetime import date

from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, render
from django.utils import timezone

from apps.accounts.permissions import (
    ROLE_ACCOUNTS,
    ROLE_HR_ADMIN,
    ROLE_PAYROLL_OFFICER,
    ROLE_SUPER_ADMIN,
    has_role,
    require_roles,
    accessible_company_ids,
    user_can_access_company,
)
from apps.attendance.models import AttendanceRecord, OvertimeEntry
from apps.audit.models import AuditLog
from apps.audit.services import log_event
from apps.employees.models import Employee, EmployeePaymentAccount
from apps.exits.models import FinalSettlement
from apps.leave.models import LeaveBalance, LeaveRequest
from apps.loans.models import LoanAccount
from apps.payroll.models import Bonus, Deduction, EmployeeSalaryAssignment, PayrollPeriod, PayrollRecord, SalaryStructureLine
from .services import csv_response, parse_iso_date, payslip_pdf_response, payroll_display_lines, payroll_calculation_summary

REPORT_ROLES = (ROLE_SUPER_ADMIN, ROLE_HR_ADMIN, ROLE_PAYROLL_OFFICER, ROLE_ACCOUNTS)


def _own_employee(user):
    return getattr(user, "employee_profile", None)


def _can_view_record(user, record):
    if has_role(user, *REPORT_ROLES):
        return user_can_access_company(user, record.period.company_id)
    own = _own_employee(user)
    return bool(own and own.pk == record.employee_id and record.period.status == PayrollPeriod.Status.LOCKED)


def _date_window(request):
    today = timezone.localdate()
    first = today.replace(day=1)
    start = parse_iso_date(request.GET.get("start"), first)
    end = parse_iso_date(request.GET.get("end"), today)
    if start > end:
        start, end = end, start
    return start, end


@login_required
def index(request):
    own = _own_employee(request.user)
    is_report_user = has_role(request.user, *REPORT_ROLES)
    if not is_report_user and not own:
        raise PermissionDenied("No report access is assigned to this account.")

    own_payslips = PayrollRecord.objects.none()
    if own:
        own_payslips = PayrollRecord.objects.filter(
            employee=own,
            period__status=PayrollPeriod.Status.LOCKED,
        ).select_related("period", "employee").order_by("-period__year", "-period__month")[:24]

    periods = PayrollPeriod.objects.none()
    if is_report_user:
        periods = PayrollPeriod.objects.filter(company_id__in=accessible_company_ids(request.user)).select_related("company").order_by("-year", "-month")[:36]
    return render(request, "reports/index.html", {
        "is_report_user": is_report_user,
        "own_payslips": own_payslips,
        "periods": periods,
    })


@login_required
def payslip_detail(request, pk):
    record = get_object_or_404(
        PayrollRecord.objects.select_related(
            "period__company", "employee__department", "employee__designation", "employee__branch"
        ).prefetch_related("line_items"),
        pk=pk,
    )
    if not _can_view_record(request.user, record):
        raise PermissionDenied("You may only view your own locked payslips.")
    lines = payroll_display_lines(record)
    earnings = sum((amount for _, kind, amount in lines if kind == "EARNING"), start=0)
    deductions = sum((amount for _, kind, amount in lines if kind == "DEDUCTION"), start=0)
    return render(request, "reports/payslip.html", {
        "record": record,
        "lines": lines,
        "earnings": earnings,
        "deductions": deductions,
        "calculation_summary": payroll_calculation_summary(record),
    })


@login_required
def payslip_pdf(request, pk):
    record = get_object_or_404(
        PayrollRecord.objects.select_related(
            "period__company", "employee__department", "employee__designation"
        ).prefetch_related("line_items"),
        pk=pk,
    )
    if not _can_view_record(request.user, record):
        raise PermissionDenied("You may only download your own locked payslips.")
    log_event(user=request.user, action="EXPORT_PDF", module="payslip", object_id=record.pk, object_repr=f"{record.employee.employee_code}/{record.period.year}-{record.period.month:02d}")
    return payslip_pdf_response(record)


@require_roles(*REPORT_ROLES)
def payroll_csv(request, period_id):
    period = get_object_or_404(PayrollPeriod.objects.filter(company_id__in=accessible_company_ids(request.user)).select_related("company"), pk=period_id)
    records = period.records.select_related("employee__department", "employee__designation").order_by("employee__employee_code")
    rows = []
    for r in records:
        rows.append([
            r.employee.employee_code, r.employee.full_name, str(r.employee.department), str(r.employee.designation),
            r.gross_salary, r.overtime_amount, r.bonus_amount, r.other_earnings,
            r.attendance_deduction, r.deduction_amount, r.loan_recovery, r.net_payable, period.status,
        ])
    log_event(user=request.user, action="EXPORT_CSV", module="report_payroll", object_id=period.pk, object_repr=f"{period.year}-{period.month:02d}")
    return csv_response(
        f"payroll-{period.year}-{period.month:02d}.csv",
        ["Employee ID", "Employee", "Department", "Designation", "Gross", "OT", "Bonus", "Other Earnings", "Attendance Deduction", "Other Deduction", "Loan Recovery", "Net Payable", "Payroll Status"],
        rows,
    )


@require_roles(*REPORT_ROLES)
def attendance_csv(request):
    start, end = _date_window(request)
    qs = AttendanceRecord.objects.filter(employee__company_id__in=accessible_company_ids(request.user), work_date__range=(start, end)).select_related("employee__department").order_by("work_date", "employee__employee_code")
    rows = [[
        x.work_date, x.employee.employee_code, x.employee.full_name, str(x.employee.department), x.status,
        x.check_in.isoformat(sep=" ", timespec="minutes") if x.check_in else "",
        x.check_out.isoformat(sep=" ", timespec="minutes") if x.check_out else "",
        x.worked_minutes, x.late_minutes, x.early_exit_minutes, x.payable_ot_minutes, x.finalized,
    ] for x in qs]
    log_event(user=request.user, action="EXPORT_CSV", module="report_attendance", object_repr=f"{start}..{end}")
    return csv_response(
        f"attendance-{start}-to-{end}.csv",
        ["Date", "Employee ID", "Employee", "Department", "Status", "Check In", "Check Out", "Worked Minutes", "Late Minutes", "Early Exit Minutes", "Payable OT Minutes", "Finalized"], rows,
    )


@require_roles(*REPORT_ROLES)
def leave_csv(request):
    start, end = _date_window(request)
    qs = LeaveRequest.objects.filter(employee__company_id__in=accessible_company_ids(request.user), start_date__lte=end, end_date__gte=start).select_related("employee__department", "leave_type").order_by("-start_date")
    from apps.leave.services import count_leave_days
    rows = []
    for x in qs:
        window_start = max(start, x.start_date)
        window_end = min(end, x.end_date)
        half_day = bool(x.days == 0.5 and x.start_date == x.end_date)
        days_in_window = count_leave_days(x.employee, window_start, window_end, half_day=half_day)
        rows.append([
            x.employee.employee_code, x.employee.full_name, str(x.employee.department), x.leave_type.name,
            x.start_date, x.end_date, x.days, days_in_window, x.status, x.reviewer_note or x.rejection_reason,
        ])
    log_event(user=request.user, action="EXPORT_CSV", module="report_leave", object_repr=f"{start}..{end}")
    return csv_response(
        f"leave-{start}-to-{end}.csv",
        ["Employee ID", "Employee", "Department", "Leave Type", "From", "To", "Request Total Days", "Days in Selected Window", "Status", "Reviewer Note / Rejection Reason"],
        rows,
    )


@require_roles(*REPORT_ROLES)
def overtime_csv(request):
    start, end = _date_window(request)
    qs = OvertimeEntry.objects.filter(employee__company_id__in=accessible_company_ids(request.user), work_date__range=(start, end)).select_related("employee__department").order_by("work_date", "employee__employee_code")
    rows = [[x.work_date, x.employee.employee_code, x.employee.full_name, str(x.employee.department), x.requested_minutes, x.approved_minutes, x.rate_multiplier, x.status, x.reason] for x in qs]
    log_event(user=request.user, action="EXPORT_CSV", module="report_overtime", object_repr=f"{start}..{end}")
    return csv_response(f"overtime-{start}-to-{end}.csv", ["Date", "Employee ID", "Employee", "Department", "Requested Minutes", "Approved Minutes", "Rate Multiplier", "Status", "Reason"], rows)


@require_roles(*REPORT_ROLES)
def loans_csv(request):
    qs = LoanAccount.objects.filter(employee__company_id__in=accessible_company_ids(request.user)).select_related("employee__department").order_by("employee__employee_code", "status", "start_month")
    rows = [[x.employee.employee_code, x.employee.full_name, str(x.employee.department), x.loan_type, x.request_reference, x.sanctioned_amount, x.installment_amount, x.installment_count, x.start_month, x.disbursement_date or "", x.outstanding_balance, x.status] for x in qs]
    log_event(user=request.user, action="EXPORT_CSV", module="report_loans", object_repr="loan-register")
    return csv_response("loan-advance-register.csv", ["Employee ID", "Employee", "Department", "Type", "Request Reference", "Sanctioned Amount", "Installment Amount", "Installments", "Recovery Start", "Disbursement Date", "Outstanding", "Status"], rows)


@require_roles(*REPORT_ROLES)
def employee_master_csv(request):
    qs = Employee.objects.filter(company_id__in=accessible_company_ids(request.user)).select_related(
        "company", "branch", "department", "designation", "shift", "reporting_manager"
    ).order_by("company__name", "employee_code")
    rows = [[
        x.company.code, x.employee_code, x.full_name, x.get_status_display(), x.employee_type,
        x.branch.name if x.branch_id else "", x.department.name if x.department_id else "",
        x.designation.name if x.designation_id else "", x.shift.name if x.shift_id else "",
        x.reporting_manager.employee_code if x.reporting_manager_id else "", x.joining_date,
        x.confirmation_date or "", x.employment_end_date or "", x.mobile, x.email, x.ot_eligible,
    ] for x in qs]
    log_event(user=request.user, action="EXPORT_CSV", module="report_employee_master", object_repr="employee-master")
    return csv_response("employee-master.csv", [
        "Company", "Employee ID", "Employee", "Status", "Employee Type", "Branch", "Department",
        "Designation", "Shift", "Reporting Manager ID", "Joining Date", "Confirmation Date",
        "Employment End Date", "Mobile", "Email", "OT Eligible",
    ], rows)


@require_roles(*REPORT_ROLES)
def leave_balance_csv(request):
    year_raw = request.GET.get("year")
    try:
        year = int(year_raw) if year_raw else timezone.localdate().year
    except (TypeError, ValueError):
        year = timezone.localdate().year
    qs = LeaveBalance.objects.filter(
        employee__company_id__in=accessible_company_ids(request.user), year=year
    ).select_related("employee__department", "leave_type").order_by("employee__employee_code", "leave_type__code")
    rows = [[
        x.employee.employee_code, x.employee.full_name, str(x.employee.department), x.leave_type.code,
        x.leave_type.name, x.year, x.opening, x.allocated, x.adjusted, x.used, x.available,
    ] for x in qs]
    log_event(user=request.user, action="EXPORT_CSV", module="report_leave_balance", object_repr=str(year))
    return csv_response(f"leave-balance-{year}.csv", [
        "Employee ID", "Employee", "Department", "Leave Code", "Leave Type", "Year", "Opening",
        "Allocated", "Adjusted", "Used", "Available",
    ], rows)


@require_roles(*REPORT_ROLES)
def salary_structure_csv(request):
    assignments = EmployeeSalaryAssignment.objects.filter(
        employee__company_id__in=accessible_company_ids(request.user)
    ).select_related("employee__department", "structure").order_by("employee__employee_code", "-effective_from")
    rows = []
    for a in assignments:
        component_labels = []
        for line in a.structure.lines.select_related("component").order_by("component__kind", "component__code"):
            component_labels.append(
                f"{line.component.code}:{line.component.kind}:{line.component.calculation_type}:"
                f"amount={line.amount}:pct={line.percentage}:formula={line.formula or ''}"
            )
        rows.append([
            a.employee.employee_code, a.employee.full_name, str(a.employee.department), a.structure.code,
            a.structure.name, a.effective_from, a.effective_to or "", a.gross_salary, a.basic_salary,
            a.ot_eligible, a.ot_rate_multiplier, " | ".join(component_labels),
        ])
    log_event(user=request.user, action="EXPORT_CSV", module="report_salary_structure", object_repr="salary-structure-register")
    return csv_response("salary-structure-register.csv", [
        "Employee ID", "Employee", "Department", "Structure Code", "Structure", "Effective From",
        "Effective To", "Gross", "Basic", "OT Eligible", "OT Multiplier", "Component Rules",
    ], rows)


@require_roles(*REPORT_ROLES)
def variable_pay_csv(request):
    start, end = _date_window(request)
    company_ids = accessible_company_ids(request.user)
    bonus_qs = Bonus.objects.filter(employee__company_id__in=company_ids, payroll_month__range=(start, end)).select_related("employee__department")
    deduction_qs = Deduction.objects.filter(employee__company_id__in=company_ids, payroll_month__range=(start, end)).select_related("employee__department")
    rows = []
    for x in bonus_qs.order_by("payroll_month", "employee__employee_code"):
        rows.append([x.payroll_month, x.employee.employee_code, x.employee.full_name, str(x.employee.department), "BONUS", x.bonus_type, x.amount, x.status, x.reason])
    for x in deduction_qs.order_by("payroll_month", "employee__employee_code"):
        rows.append([x.payroll_month, x.employee.employee_code, x.employee.full_name, str(x.employee.department), "DEDUCTION", x.deduction_type, x.amount, x.status, x.reason])
    rows.sort(key=lambda r: (str(r[0]), str(r[1]), str(r[4])))
    log_event(user=request.user, action="EXPORT_CSV", module="report_variable_pay", object_repr=f"{start}..{end}")
    return csv_response(f"bonus-deduction-{start}-to-{end}.csv", [
        "Payroll Month", "Employee ID", "Employee", "Department", "Kind", "Type", "Amount", "Status", "Reason",
    ], rows)


@require_roles(*REPORT_ROLES)
def final_settlement_csv(request):
    qs = FinalSettlement.objects.filter(
        exit_case__employee__company_id__in=accessible_company_ids(request.user)
    ).select_related("exit_case__employee__department", "exit_case__employee", "prepared_by", "approved_by").order_by("-created_at")
    rows = [[
        x.exit_case.employee.employee_code, x.exit_case.employee.full_name, str(x.exit_case.employee.department),
        x.exit_case.final_working_day or "", x.get_status_display(), x.payroll_due_amount, x.leave_encashment,
        x.other_earnings, x.loan_outstanding_recovery, x.other_deductions, x.net_payable,
        x.prepared_by.get_username() if x.prepared_by_id else "", x.approved_by.get_username() if x.approved_by_id else "",
        x.payment_reference, x.paid_at or "", x.reviewer_note,
    ] for x in qs]
    log_event(user=request.user, action="EXPORT_CSV", module="report_final_settlement", object_repr="final-settlement-register")
    return csv_response("final-settlement-register.csv", [
        "Employee ID", "Employee", "Department", "Final Working Day", "Status", "Payroll Due", "Leave Encashment",
        "Other Earnings", "Loan Recovery", "Other Deductions", "Net Payable", "Prepared By", "Approved By",
        "Payment Reference", "Paid At", "Reviewer Note",
    ], rows)


@require_roles(ROLE_SUPER_ADMIN, ROLE_HR_ADMIN)
def audit_csv(request):
    start, end = _date_window(request)
    qs = AuditLog.objects.filter(
        Q(company_id__in=accessible_company_ids(request.user)) | Q(company__isnull=True),
        created_at__date__range=(start, end),
    ).select_related("company", "user").order_by("-created_at")
    if not request.user.is_superuser:
        qs = qs.filter(company_id__in=accessible_company_ids(request.user))
    rows = [[
        x.created_at, x.company.code if x.company_id else "GLOBAL", x.user.get_username() if x.user_id else "SYSTEM",
        x.action, x.module, x.object_id, x.object_repr, x.reason, x.ip_address or "",
    ] for x in qs]
    log_event(user=request.user, action="EXPORT_CSV", module="report_audit", object_repr=f"{start}..{end}")
    return csv_response(f"audit-{start}-to-{end}.csv", [
        "Date/Time", "Company", "User", "Action", "Module", "Object ID", "Object", "Reason", "IP",
    ], rows)


@require_roles(ROLE_SUPER_ADMIN, ROLE_PAYROLL_OFFICER, ROLE_ACCOUNTS)
def bank_advice_csv(request, period_id):
    period = get_object_or_404(
        PayrollPeriod.objects.filter(company_id__in=accessible_company_ids(request.user)).select_related("company"),
        pk=period_id,
    )
    if period.status != PayrollPeriod.Status.LOCKED:
        return HttpResponse("Bank Advice is available only after payroll is Locked.", status=409, content_type="text/plain; charset=utf-8")
    records = list(period.records.select_related("employee", "employee__department").order_by("employee__employee_code"))
    employee_ids = [r.employee_id for r in records]
    accounts = {
        x.employee_id: x for x in EmployeePaymentAccount.objects.filter(
            employee_id__in=employee_ids,
            employee__company=period.company,
            payment_method=EmployeePaymentAccount.Method.BANK,
            is_active=True,
        )
    }
    missing = [r.employee.employee_code for r in records if r.employee_id not in accounts]
    if missing:
        return HttpResponse(
            "Bank Advice blocked: active bank-transfer account is missing for: " + ", ".join(missing[:50]),
            status=409,
            content_type="text/plain; charset=utf-8",
        )
    rows = []
    for r in records:
        account = accounts[r.employee_id]
        rows.append([
            r.employee.employee_code, r.employee.full_name, str(r.employee.department), account.account_name,
            account.bank_name, account.bank_branch, account.account_number, account.routing_number,
            r.net_payable, f"{period.year}-{period.month:02d}",
        ])
    log_event(
        user=request.user, company=period.company, action="EXPORT_BANK_ADVICE", module="report_bank_advice",
        object_id=period.pk, object_repr=f"{period.year}-{period.month:02d}",
        new_values={"employees": len(rows), "total": str(sum((r.net_payable for r in records), 0))}, request=request,
    )
    return csv_response(
        f"bank-advice-{period.year}-{period.month:02d}.csv",
        ["Employee ID", "Employee", "Department", "Account Name", "Bank", "Branch", "Account Number", "Routing Number", "Net Payable", "Payroll Period"],
        rows,
    )
