import uuid

from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils import timezone

from apps.accounts.permissions import assert_company_access, assert_task_assignee
from apps.audit.services import log_event
from .models import EmployeeRequest, ManagementNotice, NoticeRecipient, Notification, WorkflowTask


def notify(*, recipient, title, message, module="", reference_no=""):
    if not recipient:
        return None
    return Notification.objects.create(
        recipient=recipient,
        title=title,
        message=message,
        module=module,
        reference_no=reference_no,
    )


def _notify_employee(employee, *, title, message, module, reference_no):
    user = getattr(employee, "user", None)
    if user:
        notify(recipient=user, title=title, message=message, module=module, reference_no=reference_no)



@transaction.atomic
def publish_management_notice(*, cleaned_data, user, request=None):
    company = cleaned_data["company"]
    assert_company_access(user, company)
    notice = ManagementNotice.objects.create(
        company=company,
        title=cleaned_data["title"].strip(),
        message=cleaned_data["message"].strip(),
        publish_from=cleaned_data["publish_from"],
        publish_until=cleaned_data.get("publish_until"),
        audience=cleaned_data["audience"],
        created_by=user,
    )
    from apps.employees.models import Employee
    if notice.audience == "SELECTED":
        employees = list(cleaned_data.get("recipients") or [])
    else:
        employees = list(Employee.objects.filter(
            company=company, status__in=[Employee.Status.ACTIVE, Employee.Status.PROBATION, Employee.Status.CONFIRMED]
        ).select_related("user"))
    recipient_rows = [NoticeRecipient(notice=notice, employee=e) for e in employees]
    NoticeRecipient.objects.bulk_create(recipient_rows, ignore_conflicts=True)
    for employee in employees:
        if employee.user_id:
            notify(
                recipient=employee.user,
                title=f"Management Notice: {notice.title}",
                message=notice.message,
                module="NOTICE",
                reference_no=f"NOTICE-{notice.pk:06d}",
            )
    log_event(
        user=user, company=company, action="PUBLISH_NOTICE", module="workflow", object_id=notice.pk,
        object_repr=notice.title, new_values={"audience": notice.audience, "recipient_count": len(employees)}, request=request,
    )
    return notice

def make_request_reference(employee, request_type):
    from apps.organization.services import get_effective_policy
    policy = get_effective_policy(employee.company, timezone.localdate())
    base_prefix = (policy.request_reference_prefix or "REQ").strip().upper()
    prefix_map = {
        "RESIGNATION": "RES",
        "SALARY_ADVANCE": "ADV",
        "LOAN": "LOAN",
        "CERTIFICATE": "CERT",
        "PROFILE_CORRECTION": "PROF",
        "SALARY_DISPUTE": "PAY-DIS",
        "GENERAL_HR": "HR",
    }
    type_prefix = prefix_map.get(request_type, "REQ")
    prefix = f"{base_prefix}-{type_prefix}" if base_prefix != type_prefix else type_prefix
    stamp = timezone.localtime().strftime("%Y%m%d%H%M%S")
    suffix = uuid.uuid4().hex[:4].upper()
    return f"{prefix}-{stamp}-{employee.employee_code}-{suffix}"[:40]


def create_workflow_task_for_employee(*, employee, module, reference_no, title, source_type, source_id, priority="NORMAL", preferred_role="HR Admin", payload=None):
    assigned_to = None
    assigned_role = preferred_role
    manager = getattr(employee, "reporting_manager", None)
    manager_user = getattr(manager, "user", None) if manager else None
    if preferred_role == "Manager":
        if manager_user:
            assigned_to = manager_user
            assigned_role = ""
        else:
            assigned_role = "HR Admin"
    task = WorkflowTask.objects.create(
        company=employee.company,
        module=module,
        reference_no=reference_no,
        title=title,
        assigned_to=assigned_to,
        assigned_role=assigned_role,
        priority=priority,
        source_type=source_type,
        source_id=str(source_id),
        payload=payload or {},
    )
    if assigned_to:
        notify(recipient=assigned_to, title="New approval task", message=title, module=module, reference_no=reference_no)
    return task


@transaction.atomic
def create_employee_request(*, employee, cleaned_data, user=None, request=None):
    if user is not None:
        assert_company_access(user, employee.company_id)
    request_type = cleaned_data["request_type"]
    payload = {}
    for key in ("requested_amount", "installment_count", "required_date", "proposed_last_working_day", "certificate_type", "disputed_month"):
        value = cleaned_data.get(key)
        if value is not None and value != "":
            payload[key] = value.isoformat() if hasattr(value, "isoformat") else str(value)
    reference = make_request_reference(employee, request_type)
    item = EmployeeRequest.objects.create(
        employee=employee,
        request_type=request_type,
        subject=(cleaned_data.get("subject") or "").strip(),
        details=(cleaned_data.get("details") or "").strip(),
        payload=payload,
        attachment=cleaned_data.get("attachment"),
        reference_no=reference,
        status=EmployeeRequest.Status.PENDING,
    )
    route_role = "HR Admin"
    priority = "NORMAL"
    if request_type == "SALARY_DISPUTE":
        route_role = "Payroll Officer"
        priority = "HIGH"
    elif request_type in {"RESIGNATION", "LOAN", "SALARY_ADVANCE"}:
        route_role = "HR Admin"
        priority = "MEDIUM"
    create_workflow_task_for_employee(
        employee=employee,
        module="HR Request",
        reference_no=reference,
        title=f"{request_type.replace('_', ' ').title()}: {employee.full_name}",
        source_type="employee_request",
        source_id=item.pk,
        priority=priority,
        preferred_role=route_role,
        payload={"request_type": request_type},
    )
    log_event(
        user=user,
        action="SUBMIT",
        module="hr_request",
        object_id=item.pk,
        object_repr=reference,
        new_values={"request_type": request_type, "status": item.status, "payload": payload},
        reason=item.details,
        request=request,
    )
    return item



@transaction.atomic
def resubmit_employee_request(*, request_obj, cleaned_data, user=None, request=None):
    item = EmployeeRequest.objects.select_for_update().select_related("employee").get(pk=request_obj.pk)
    if item.status != EmployeeRequest.Status.RETURNED:
        raise ValidationError("Only a returned application can be resubmitted.")
    if user is not None:
        assert_company_access(user, item.employee.company_id)
        if getattr(item.employee, "user_id", None) != getattr(user, "id", None):
            raise ValidationError("Only the employee who submitted the application may resubmit it.")
    payload = {}
    for key in ("requested_amount", "installment_count", "required_date", "proposed_last_working_day", "certificate_type", "disputed_month"):
        value = cleaned_data.get(key)
        if value is not None and value != "":
            payload[key] = value.isoformat() if hasattr(value, "isoformat") else str(value)
    item.subject = (cleaned_data.get("subject") or "").strip()
    item.details = (cleaned_data.get("details") or "").strip()
    item.payload = payload
    if cleaned_data.get("attachment"):
        item.attachment = cleaned_data["attachment"]
    item.status = EmployeeRequest.Status.PENDING
    item.resolution_note = ""
    item.rejection_reason = ""
    item.save(update_fields=["subject", "details", "payload", "attachment", "status", "resolution_note", "rejection_reason", "updated_at"])
    route_role = "Payroll Officer" if item.request_type == "SALARY_DISPUTE" else "HR Admin"
    priority = "HIGH" if item.request_type == "SALARY_DISPUTE" else ("MEDIUM" if item.request_type in {"RESIGNATION", "LOAN", "SALARY_ADVANCE"} else "NORMAL")
    create_workflow_task_for_employee(
        employee=item.employee, module="HR Request", reference_no=item.reference_no,
        title=f"Resubmitted {item.request_type.replace('_', ' ').title()}: {item.employee.full_name}",
        source_type="employee_request", source_id=item.pk, priority=priority, preferred_role=route_role,
        payload={"request_type": item.request_type, "resubmitted": True},
    )
    log_event(user=user, action="RESUBMIT", module="hr_request", object_id=item.pk, object_repr=item.reference_no, new_values={"status": item.status, "payload": payload}, reason=item.details, request=request)
    return item

def _create_disbursement_task(loan):
    existing = WorkflowTask.objects.filter(
        source_type="loan_disbursement",
        source_id=str(loan.pk),
        status=WorkflowTask.Status.OPEN,
    ).first()
    if existing:
        return existing
    return WorkflowTask.objects.create(
        company=loan.employee.company,
        module="Loan",
        reference_no=f"LOAN-DISB-{loan.pk:06d}",
        title=f"Disburse {loan.get_loan_type_display()}: {loan.employee.full_name}",
        assigned_role="Accounts",
        priority="HIGH",
        source_type="loan_disbursement",
        source_id=str(loan.pk),
        payload={"loan_id": loan.pk, "request_reference": loan.request_reference},
    )


def _sync_employee_request(req, *, decision, note, user, decision_data):
    if decision == "REJECTED":
        req.status = EmployeeRequest.Status.REJECTED
        req.rejection_reason = note
        req.resolution_note = ""
        req.save(update_fields=["status", "resolution_note", "rejection_reason", "updated_at"])
        _notify_employee(req.employee, title="Application Rejected", message=note, module="HR_REQUEST", reference_no=req.reference_no)
        return
    if decision == "RETURNED":
        req.status = EmployeeRequest.Status.RETURNED
        req.rejection_reason = ""
        req.resolution_note = note
        req.save(update_fields=["status", "resolution_note", "rejection_reason", "updated_at"])
        _notify_employee(req.employee, title="Application Returned for Correction", message=note, module="HR_REQUEST", reference_no=req.reference_no)
        return

    if req.request_type in {"LOAN", "SALARY_ADVANCE"}:
        from apps.loans.services import create_loan_from_approved_request

        loan = create_loan_from_approved_request(
            req,
            sanctioned_amount=decision_data.get("sanctioned_amount"),
            installment_count=decision_data.get("approved_installment_count") or 1,
            recovery_start_month=decision_data.get("recovery_start_month"),
            user=user,
        )
        _create_disbursement_task(loan)
        req.status = EmployeeRequest.Status.APPROVED
        req.rejection_reason = ""
        req.resolution_note = (
            f"Approved: {loan.get_loan_type_display()} {loan.sanctioned_amount}; "
            f"{loan.installment_count} installment(s), recovery from {loan.start_month:%b %Y}. "
            f"Pending Accounts disbursement. {note}"
        ).strip()
        req.save(update_fields=["status", "resolution_note", "rejection_reason", "updated_at"])
        _notify_employee(
            req.employee,
            title="Loan / Advance Approved",
            message=req.resolution_note,
            module="LOAN",
            reference_no=req.reference_no,
        )
        return

    if req.request_type == "RESIGNATION":
        from apps.exits.services import create_exit_case_from_resignation
        case = create_exit_case_from_resignation(req, user=user)
        req.status = EmployeeRequest.Status.APPROVED
        req.resolution_note = (note + " " if note else "") + f"Exit case #{case.pk} created; department clearances assigned automatically."
        req.rejection_reason = ""
        req.save(update_fields=["status", "resolution_note", "rejection_reason", "updated_at"])
        _notify_employee(req.employee, title="Resignation Approved", message=req.resolution_note, module="EXIT", reference_no=req.reference_no)
        return

    req.status = EmployeeRequest.Status.APPROVED
    req.resolution_note = note
    req.rejection_reason = ""
    req.save(update_fields=["status", "resolution_note", "rejection_reason", "updated_at"])
    _notify_employee(
        req.employee,
        title="Application Approved",
        message=note or f"Your request {req.reference_no} is approved.",
        module="HR_REQUEST",
        reference_no=req.reference_no,
    )


def _assert_task_source_company(task, source_company_id):
    """Fail closed if a workflow task points to a source record in another tenant."""
    if not source_company_id or int(source_company_id) != int(task.company_id):
        raise ValidationError("Workflow source belongs to another company; decision blocked.")


def _sync_source(task, *, decision, note, user, decision_data=None):
    decision_data = decision_data or {}
    source_type = (task.source_type or "").lower().strip()
    source_id = task.source_id
    if not source_type or not source_id:
        return

    if source_type == "employee_request":
        req = EmployeeRequest.objects.select_for_update().select_related("employee__user").get(pk=source_id)
        _assert_task_source_company(task, req.employee.company_id)
        _sync_employee_request(req, decision=decision, note=note, user=user, decision_data=decision_data)
        return

    if source_type == "leave_request":
        from apps.leave.models import LeaveRequest
        from apps.leave.services import decide_leave_request
        req = LeaveRequest.objects.select_related("employee__user").get(pk=source_id)
        _assert_task_source_company(task, req.employee.company_id)
        decide_leave_request(req, decision=decision, user=user, note=note)
        req.refresh_from_db()
        _notify_employee(req.employee, title=f"Leave {req.status.title()}", message=note or f"Your leave request is now {req.status.lower()}.", module="LEAVE", reference_no=task.reference_no)
        return

    if source_type == "attendance_adjustment":
        from apps.attendance.models import AttendanceAdjustment
        from apps.attendance.services import apply_attendance_adjustment
        item = AttendanceAdjustment.objects.select_related(
            "attendance__employee__user", "requested_by_user"
        ).get(pk=source_id)
        employee = item.attendance.employee
        _assert_task_source_company(task, employee.company_id)
        apply_attendance_adjustment(item, decision=decision, note=note, user=user)
        message = note or "Attendance correction request was updated."
        if item.requested_by_user_id:
            notify(recipient=item.requested_by_user, title=f"Attendance correction {decision.title()}", message=message, module="ATTENDANCE", reference_no=task.reference_no)
            if employee.user_id and employee.user_id != item.requested_by_user_id:
                _notify_employee(employee, title=f"Attendance correction {decision.title()}", message=message, module="ATTENDANCE", reference_no=task.reference_no)
        else:
            _notify_employee(employee, title=f"Attendance correction {decision.title()}", message=message, module="ATTENDANCE", reference_no=task.reference_no)
        return

    if source_type == "overtime_entry":
        from apps.attendance.models import AttendancePeriod, AttendanceRecord, OvertimeEntry
        item = OvertimeEntry.objects.select_for_update().select_related("employee__user").get(pk=source_id)
        _assert_task_source_company(task, item.employee.company_id)
        if item.status != "PENDING":
            raise ValidationError("Only a pending overtime entry can be decided by this open task.")
        period = AttendancePeriod.objects.select_for_update().filter(
            company=item.employee.company, year=item.work_date.year, month=item.work_date.month
        ).first()
        if period and period.status == AttendancePeriod.Status.FINALIZED:
            raise ValidationError("Attendance is finalized for this OT month. Reopen attendance before deciding the pending OT entry.")
        item.status = decision
        item.approved_minutes = item.requested_minutes if decision == "APPROVED" else 0
        if note:
            item.reason = note
        item.save(update_fields=["status", "approved_minutes", "reason", "updated_at"])
        attendance = AttendanceRecord.objects.select_for_update().filter(employee=item.employee, work_date=item.work_date).first()
        if attendance:
            attendance.payable_ot_minutes = item.approved_minutes if decision == "APPROVED" else 0
            attendance.save(update_fields=["payable_ot_minutes", "updated_at"])
        _notify_employee(item.employee, title=f"Overtime {decision.title()}", message=note or "Overtime request updated.", module="OVERTIME", reference_no=task.reference_no)
        return

    if source_type == "bonus":
        from apps.payroll.models import Bonus, PayrollPeriod
        item = Bonus.objects.select_for_update().select_related("employee__user").get(pk=source_id)
        _assert_task_source_company(task, item.employee.company_id)
        if item.status != "PENDING":
            raise ValidationError("Only a pending bonus can be decided by this open task.")
        if decision == "APPROVED":
            period = PayrollPeriod.objects.select_for_update().filter(
                company=item.employee.company, year=item.payroll_month.year, month=item.payroll_month.month
            ).first()
            if period and period.status != PayrollPeriod.Status.DRAFT:
                raise ValidationError("Target payroll is already Reviewed/Approved/Locked. Use the controlled adjustment/arrear process instead of approving this bonus into a closed workflow state.")
        item.status = decision
        if note:
            item.reason = note
        item.save(update_fields=["status", "reason", "updated_at"])
        _notify_employee(item.employee, title=f"Bonus {decision.title()}", message=note or "Bonus request updated.", module="BONUS", reference_no=task.reference_no)
        return

    if source_type == "deduction":
        from apps.payroll.models import Deduction, PayrollPeriod
        item = Deduction.objects.select_for_update().select_related("employee__user").get(pk=source_id)
        _assert_task_source_company(task, item.employee.company_id)
        if item.status != "PENDING":
            raise ValidationError("Only a pending deduction can be decided by this open task.")
        if decision == "APPROVED":
            period = PayrollPeriod.objects.select_for_update().filter(
                company=item.employee.company, year=item.payroll_month.year, month=item.payroll_month.month
            ).first()
            if period and period.status != PayrollPeriod.Status.DRAFT:
                raise ValidationError("Target payroll is already Reviewed/Approved/Locked. Use the controlled adjustment/arrear process instead of approving this deduction into a closed workflow state.")
        item.status = decision
        if note:
            item.reason = note
        item.save(update_fields=["status", "reason", "updated_at"])
        _notify_employee(item.employee, title=f"Deduction {decision.title()}", message=note or "Deduction request updated.", module="DEDUCTION", reference_no=task.reference_no)
        return

    if source_type == "clearance_item":
        from apps.exits.models import ClearanceItem
        from apps.exits.services import record_clearance_decision
        item = ClearanceItem.objects.select_related("exit_case__employee__user").get(pk=source_id)
        _assert_task_source_company(task, item.exit_case.employee.company_id)
        record_clearance_decision(item, decision=decision, note=note, user=user)
        return

    if source_type == "final_settlement":
        from apps.exits.models import FinalSettlement
        from apps.exits.services import review_final_settlement
        settlement = FinalSettlement.objects.select_related("exit_case__employee").get(pk=source_id)
        _assert_task_source_company(task, settlement.exit_case.employee.company_id)
        review_final_settlement(settlement, decision=decision, user=user, note=note)
        return

    if source_type == "loan_disbursement":
        from apps.loans.models import LoanAccount
        from apps.loans.services import cancel_pending_loan, disburse_loan

        loan = LoanAccount.objects.select_for_update().select_related("employee__user").get(pk=source_id)
        _assert_task_source_company(task, loan.employee.company_id)
        if decision == "APPROVED":
            disburse_loan(
                loan,
                disbursement_date=decision_data.get("disbursement_date") or timezone.localdate(),
                payment_reference=decision_data.get("payment_reference", ""),
                note=note,
                user=user,
            )
            req = EmployeeRequest.objects.select_for_update().filter(reference_no=loan.request_reference).first()
            if req:
                req.resolution_note = (
                    f"Disbursed on {decision_data.get('disbursement_date') or timezone.localdate()}"
                    + (f" · Reference: {decision_data.get('payment_reference')}" if decision_data.get("payment_reference") else "")
                    + (f" · {note}" if note else "")
                )
                req.save(update_fields=["resolution_note", "updated_at"])
            _notify_employee(
                loan.employee,
                title="Loan / Advance Disbursed",
                message=note or "Your approved loan/advance has been disbursed and will enter payroll recovery from the approved month.",
                module="LOAN",
                reference_no=task.reference_no,
            )
        elif decision == "REJECTED":
            cancel_pending_loan(loan, reason=note, user=user)
            req = EmployeeRequest.objects.select_for_update().filter(reference_no=loan.request_reference).first()
            if req:
                req.status = EmployeeRequest.Status.REJECTED
                req.rejection_reason = note
                req.save(update_fields=["status", "rejection_reason", "updated_at"])
            _notify_employee(loan.employee, title="Loan / Advance Disbursement Cancelled", message=note, module="LOAN", reference_no=task.reference_no)
        return

    if source_type == "payroll_period":
        from apps.payroll.models import PayrollPeriod
        from apps.payroll.services import return_payroll_period_to_draft, transition_payroll_period
        period = PayrollPeriod.objects.select_for_update().get(pk=source_id)
        _assert_task_source_company(task, period.company_id)
        requested_action = str((task.payload or {}).get("payroll_action", "")).upper()
        if decision == "APPROVED" and requested_action in {"REVIEW", "APPROVE", "LOCK"}:
            transition_payroll_period(period, action=requested_action, user=user, reason=note)
        elif decision == "APPROVED" and requested_action == "PAY":
            transition_payroll_period(period, action="MARK_PAID", user=user, reason=(decision_data or {}).get("payment_reference", ""))
        elif decision == "REJECTED" and requested_action in {"APPROVE", "LOCK"}:
            # APPROVE/LOCK rejection means the reviewed/approved payroll needs correction.
            # Return it to Draft so Payroll can rebuild it; REVIEW rejection is already
            # Draft and PAY rejection intentionally leaves the Locked payroll unpaid.
            return_payroll_period_to_draft(period, user=user, reason=note)
        return


RETURNABLE_TASK_SOURCE_TYPES = {"employee_request", "leave_request", "attendance_adjustment", "final_settlement"}


@transaction.atomic
def decide_task(*, task, user, decision, note="", decision_data=None):
    task = WorkflowTask.objects.select_for_update().get(pk=task.pk)
    assert_task_assignee(user, task)
    decision = decision.upper().strip()
    if decision not in {"APPROVED", "REJECTED", "RETURNED"}:
        raise ValidationError("Unsupported task decision.")
    if task.status != WorkflowTask.Status.OPEN:
        raise ValidationError("This task has already been decided.")
    source_type = (task.source_type or "").lower().strip()
    if decision == "RETURNED" and source_type not in RETURNABLE_TASK_SOURCE_TYPES:
        raise ValidationError("Return for Correction is not supported for this task type; approve or reject it instead.")
    if decision in {"REJECTED", "RETURNED"} and not note.strip():
        raise ValidationError("Reason/comment is required for reject or return.")

    task.status = decision
    task.decision_note = note.strip()
    task.decided_by = user
    task.decided_at = timezone.now()
    task.save(update_fields=["status", "decision_note", "decided_by", "decided_at", "updated_at"])
    _sync_source(task, decision=decision, note=note.strip(), user=user, decision_data=decision_data or {})
    log_event(
        user=user,
        action=decision,
        module=task.module or "workflow",
        object_id=task.pk,
        object_repr=task.title,
        new_values={"task_status": task.status, "reference_no": task.reference_no},
        reason=note.strip(),
    )
    return task
