"""
expense/models.py
=================
Models for Module 2 (Expense Management) + Module 3 (Approval Workflow Engine).

Status lifecycle:
  submit → PENDING → [HOD → Director → MD → Accountant] → APPROVED → [Disbursement] → DISBURSED
                  ↘ KIV ↗ (recall)
                  ↘ REJECTED → (resubmit once) → RESUBMITTED → re-enters chain
                  ↘ CLOSED (after 2nd rejection)
"""

import os
from datetime import date

from django.contrib.auth.models import Group, User
from django.core.exceptions import ValidationError
from django.core.validators import FileExtensionValidator
from django.db import models
from django.urls import reverse_lazy
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _

from base.models import Department
from budget.models import BudgetAllocation, BudgetItem
from employee.models import Employee
from horilla.models import HorillaModel

# ---------------------------------------------------------------------------
# Status constants
# ---------------------------------------------------------------------------

STATUS_PENDING = "pending"
STATUS_APPROVED = "approved"
STATUS_REJECTED = "rejected"
STATUS_KIV = "kiv"
STATUS_RESUBMITTED = "resubmitted"
STATUS_DISBURSED = "disbursed"
STATUS_CLOSED = "closed"

STATUS_CHOICES = [
    (STATUS_PENDING, _("Pending Approval")),
    (STATUS_APPROVED, _("Approved")),
    (STATUS_REJECTED, _("Rejected")),
    (STATUS_KIV, _("Kept In View (KIV)")),
    (STATUS_RESUBMITTED, _("Resubmitted")),
    (STATUS_DISBURSED, _("Disbursed")),
    (STATUS_CLOSED, _("Closed")),
]

# Statuses that are "active" (approver can act on them)
ACTIVE_STATUSES = [STATUS_PENDING, STATUS_RESUBMITTED, STATUS_KIV]

STATUS_BADGE_CLASSES = {
    STATUS_PENDING: "oh-badge--warning",
    STATUS_APPROVED: "oh-badge--success",
    STATUS_REJECTED: "oh-badge--danger",
    STATUS_KIV: "oh-badge--secondary",
    STATUS_RESUBMITTED: "oh-badge--info",
    STATUS_DISBURSED: "oh-badge--primary",
    STATUS_CLOSED: "oh-badge--dark",
}

# ---------------------------------------------------------------------------
# Approval Workflow Engine (Module 3)
# ---------------------------------------------------------------------------


class ExpenseApprovalWorkflow(HorillaModel):
    """
    Named approval workflow (e.g. 'Expense Approval').
    An organisation typically has one default workflow.
    """

    name = models.CharField(max_length=200, unique=True, verbose_name=_("Workflow Name"))
    description = models.TextField(blank=True, verbose_name=_("Description"))

    class Meta:
        verbose_name = _("Approval Workflow")
        verbose_name_plural = _("Approval Workflows")
        ordering = ["name"]

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse_lazy("expense-workflow-config")

    def get_update_url(self):
        return reverse_lazy("expense-workflow-edit", kwargs={"pk": self.pk})

    @classmethod
    def get_default(cls):
        """Return the first active workflow (used as default for new requests)."""
        return cls.objects.filter(is_active=True).order_by("id").first()


class ExpenseApprovalStage(HorillaModel):
    """
    One stage in an approval chain.
    Members of the linked Django Group are the approvers at this stage.
    """

    workflow = models.ForeignKey(
        ExpenseApprovalWorkflow,
        on_delete=models.CASCADE,
        related_name="stages",
        verbose_name=_("Workflow"),
    )
    name = models.CharField(max_length=200, verbose_name=_("Stage Name"))
    group = models.ForeignKey(
        Group,
        on_delete=models.PROTECT,
        verbose_name=_("Approver Group"),
        help_text=_("Members of this Django Group can approve at this stage."),
    )
    order = models.PositiveSmallIntegerField(
        verbose_name=_("Order"),
        help_text=_("Execution order (1 = first). Must be unique within workflow."),
    )
    is_disbursement_stage = models.BooleanField(
        default=False,
        verbose_name=_("Is Disbursement Stage"),
        help_text=_(
            "When this stage approves, the request is marked DISBURSED instead of APPROVED."
        ),
    )

    class Meta:
        verbose_name = _("Approval Stage")
        verbose_name_plural = _("Approval Stages")
        ordering = ["workflow", "order"]
        unique_together = [["workflow", "order"]]

    def __str__(self):
        return f"{self.workflow.name} — Stage {self.order}: {self.name}"

    def get_update_url(self):
        return reverse_lazy(
            "expense-stage-edit",
            kwargs={"workflow_pk": self.workflow_id, "pk": self.pk},
        )

    def get_next_stage(self):
        """Return the next stage in this workflow, or None if this is the last."""
        return (
            ExpenseApprovalStage.objects.filter(
                workflow=self.workflow, order__gt=self.order, is_active=True
            )
            .order_by("order")
            .first()
        )

    def get_previous_stage(self):
        return (
            ExpenseApprovalStage.objects.filter(
                workflow=self.workflow, order__lt=self.order, is_active=True
            )
            .order_by("-order")
            .first()
        )


# ---------------------------------------------------------------------------
# Expense Request (Module 2)
# ---------------------------------------------------------------------------


def _generate_reference():
    """Auto-generate reference number: EXP-YYYY-NNNNNN"""
    year = date.today().year
    prefix = f"EXP-{year}-"
    last = (
        ExpenseRequest.objects.filter(reference_number__startswith=prefix)
        .order_by("reference_number")
        .last()
    )
    if last:
        try:
            seq = int(last.reference_number.split("-")[-1]) + 1
        except (ValueError, IndexError):
            seq = 1
    else:
        seq = 1
    return f"{prefix}{seq:06d}"


def _expense_doc_upload_path(instance, filename):
    ext = os.path.splitext(filename)[1].lower()
    return f"expense/documents/{instance.expense.reference_number}/{instance.pk or 'new'}{ext}"


def _validate_document(file):
    allowed_ext = ["pdf", "jpg", "jpeg", "png", "doc", "docx"]
    ext = os.path.splitext(file.name)[1].lstrip(".").lower()
    if ext not in allowed_ext:
        raise ValidationError(
            _("Unsupported file type '.%(ext)s'. Allowed: %(allowed)s.")
            % {"ext": ext, "allowed": ", ".join(allowed_ext)}
        )
    if file.size > 5 * 1024 * 1024:
        raise ValidationError(_("File size must not exceed 5 MB."))


class ExpenseRequest(HorillaModel):
    """
    A single expense request submitted by an employee.
    """

    reference_number = models.CharField(
        max_length=30,
        unique=True,
        editable=False,
        verbose_name=_("Reference No."),
    )
    employee = models.ForeignKey(
        Employee,
        on_delete=models.CASCADE,
        related_name="expense_requests",
        verbose_name=_("Employee"),
    )
    department = models.ForeignKey(
        Department,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        verbose_name=_("Department"),
        help_text=_("Auto-filled from employee profile."),
    )
    budget_item = models.ForeignKey(
        BudgetItem,
        on_delete=models.PROTECT,
        related_name="expense_requests",
        verbose_name=_("Budget Item"),
    )
    title = models.CharField(max_length=300, verbose_name=_("Request Title"))
    amount = models.DecimalField(
        max_digits=15,
        decimal_places=2,
        verbose_name=_("Amount (₦)"),
    )
    justification = models.TextField(verbose_name=_("Purpose / Justification"))

    status = models.CharField(
        max_length=20,
        choices=STATUS_CHOICES,
        default=STATUS_PENDING,
        verbose_name=_("Status"),
    )
    workflow = models.ForeignKey(
        ExpenseApprovalWorkflow,
        on_delete=models.PROTECT,
        verbose_name=_("Approval Workflow"),
    )
    current_stage = models.ForeignKey(
        ExpenseApprovalStage,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="pending_requests",
        verbose_name=_("Current Stage"),
    )

    submitted_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Submitted At"))
    resubmit_count = models.PositiveSmallIntegerField(
        default=0,
        verbose_name=_("Resubmit Count"),
    )

    class Meta:
        verbose_name = _("Expense Request")
        verbose_name_plural = _("Expense Requests")
        ordering = ["-submitted_at"]

    def save(self, *args, **kwargs):
        if not self.reference_number:
            self.reference_number = _generate_reference()
        super().save(*args, **kwargs)

    def __str__(self):
        return f"{self.reference_number} — {self.title} (₦{self.amount:,.2f})"

    # ------------------------------------------------------------------
    # Horilla URL helpers
    # ------------------------------------------------------------------

    def get_absolute_url(self):
        return reverse_lazy("expense-request-detail", kwargs={"pk": self.pk})

    def get_detail_url(self):
        return reverse_lazy("expense-request-detail", kwargs={"pk": self.pk})

    def get_update_url(self):
        """Expense requests cannot be edited directly — link to the detail view."""
        return self.get_detail_url()

    def get_delete_url(self):
        return reverse_lazy("generic-delete")

    def get_delete_instance(self):
        return self.pk

    def get_action_url(self, action):
        """Return the URL of an action modal (approve/reject/kiv/resubmit/recall)."""
        return reverse_lazy(
            "expense-action-modal",
            kwargs={"pk": self.pk, "action": action},
        )

    def get_approve_url(self):
        return reverse_lazy("expense-approve", kwargs={"pk": self.pk})

    def get_reject_url(self):
        return reverse_lazy("expense-reject", kwargs={"pk": self.pk})

    def get_kiv_url(self):
        return reverse_lazy("expense-kiv", kwargs={"pk": self.pk})

    def get_recall_kiv_url(self):
        return reverse_lazy("expense-recall-kiv", kwargs={"pk": self.pk})

    def get_resubmit_url(self):
        return reverse_lazy("expense-resubmit", kwargs={"pk": self.pk})

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    def can_resubmit(self):
        return self.status == STATUS_REJECTED and self.resubmit_count < 1

    def can_recall_kiv(self, user):
        """Original requester or any member of the current stage group can recall."""
        if self.status != STATUS_KIV:
            return False
        if self.employee.employee_user_id == user:
            return True
        if self.current_stage and user.groups.filter(id=self.current_stage.group_id).exists():
            return True
        return False

    def is_approvable_by(self, user):
        """True if the user is in the current stage's group and status is active."""
        if self.status not in [STATUS_PENDING, STATUS_RESUBMITTED]:
            return False
        if not self.current_stage:
            return False
        return user.groups.filter(id=self.current_stage.group_id).exists()

    def get_current_allocation(self):
        """Find the best matching BudgetAllocation for tracking amounts."""
        today = date.today()
        year = today.year
        # Annual first
        alloc = BudgetAllocation.objects.filter(
            item=self.budget_item, fiscal_year=year, period_type="annual"
        ).first()
        if alloc:
            return alloc
        # Monthly
        alloc = BudgetAllocation.objects.filter(
            item=self.budget_item, fiscal_year=year, period_type="monthly",
            period_value=today.month
        ).first()
        if alloc:
            return alloc
        # Quarterly
        quarter = (today.month - 1) // 3 + 1
        return BudgetAllocation.objects.filter(
            item=self.budget_item, fiscal_year=year, period_type="quarterly",
            period_value=quarter
        ).first()

    # ------------------------------------------------------------------
    # Column display helpers
    # ------------------------------------------------------------------

    def status_badge(self):
        css = STATUS_BADGE_CLASSES.get(self.status, "oh-badge--secondary")
        label = self.get_status_display()
        return format_html(
            '<span class="badge oh-badge {}">{}</span>', css, label
        )

    status_badge.short_description = _("Status")

    def amount_display(self):
        return format_html("₦{:,.2f}", self.amount)

    amount_display.short_description = _("Amount")


# ---------------------------------------------------------------------------
# Supporting Documents
# ---------------------------------------------------------------------------


class ExpenseDocument(HorillaModel):
    """Supporting documents attached to an expense request."""

    expense = models.ForeignKey(
        ExpenseRequest,
        on_delete=models.CASCADE,
        related_name="documents",
        verbose_name=_("Expense Request"),
    )
    file = models.FileField(
        upload_to=_expense_doc_upload_path,
        validators=[_validate_document],
        verbose_name=_("File"),
    )
    file_name = models.CharField(max_length=255, editable=False)
    uploaded_by = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        null=True,
        editable=False,
        related_name="expense_documents_uploaded",
        verbose_name=_("Uploaded By"),
    )

    class Meta:
        verbose_name = _("Expense Document")
        verbose_name_plural = _("Expense Documents")

    def save(self, *args, **kwargs):
        if self.file and not self.file_name:
            self.file_name = os.path.basename(self.file.name)
        super().save(*args, **kwargs)

    def __str__(self):
        return self.file_name or str(self.file)


# ---------------------------------------------------------------------------
# Immutable Approval Audit Log
# ---------------------------------------------------------------------------

ACTION_SUBMITTED = "submitted"
ACTION_APPROVED = "approved"
ACTION_REJECTED = "rejected"
ACTION_KIV = "kiv"
ACTION_RECALLED = "recalled"
ACTION_RESUBMITTED = "resubmitted"
ACTION_DISBURSED = "disbursed"
ACTION_CLOSED = "closed"

ACTION_CHOICES = [
    (ACTION_SUBMITTED, _("Submitted")),
    (ACTION_APPROVED, _("Approved")),
    (ACTION_REJECTED, _("Rejected")),
    (ACTION_KIV, _("Kept In View")),
    (ACTION_RECALLED, _("Recalled from KIV")),
    (ACTION_RESUBMITTED, _("Resubmitted")),
    (ACTION_DISBURSED, _("Disbursed")),
    (ACTION_CLOSED, _("Closed")),
]


class ExpenseApprovalLog(models.Model):
    """
    Immutable audit log. Never updated or deleted — only appended to.
    Does NOT extend HorillaModel to prevent soft-delete and modification tracking.
    """

    expense = models.ForeignKey(
        ExpenseRequest,
        on_delete=models.CASCADE,
        related_name="approval_logs",
        verbose_name=_("Expense Request"),
    )
    stage = models.ForeignKey(
        ExpenseApprovalStage,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        verbose_name=_("Workflow Stage"),
    )
    action = models.CharField(
        max_length=20,
        choices=ACTION_CHOICES,
        verbose_name=_("Action"),
    )
    actor = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        null=True,
        verbose_name=_("Actor"),
    )
    comment = models.TextField(blank=True, verbose_name=_("Comment / Reason"))
    created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Timestamp"))

    class Meta:
        verbose_name = _("Approval Log Entry")
        verbose_name_plural = _("Approval Log Entries")
        ordering = ["expense", "created_at"]

    def __str__(self):
        return f"{self.expense.reference_number} — {self.action} by {self.actor} at {self.created_at:%Y-%m-%d %H:%M}"


# ---------------------------------------------------------------------------
# KIV Record
# ---------------------------------------------------------------------------


class KIVRecord(models.Model):
    """
    Tracks a 'Kept In View' hold on an expense request.
    A request can be KIV'd multiple times (one active at a time).
    """

    expense = models.ForeignKey(
        ExpenseRequest,
        on_delete=models.CASCADE,
        related_name="kiv_records",
        verbose_name=_("Expense Request"),
    )
    stage = models.ForeignKey(
        ExpenseApprovalStage,
        on_delete=models.SET_NULL,
        null=True,
        verbose_name=_("Stage at KIV"),
    )
    set_by = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        null=True,
        related_name="kiv_set_by",
        verbose_name=_("Set By"),
    )
    reason = models.TextField(verbose_name=_("KIV Reason"))
    revisit_date = models.DateField(verbose_name=_("Revisit Date"))
    status_before_kiv = models.CharField(
        max_length=20,
        choices=STATUS_CHOICES,
        default=STATUS_PENDING,
        verbose_name=_("Status Before KIV"),
    )
    recalled_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Recalled At"))
    recalled_by = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="kiv_recalled_by",
        verbose_name=_("Recalled By"),
    )

    class Meta:
        verbose_name = _("KIV Record")
        verbose_name_plural = _("KIV Records")
        ordering = ["-revisit_date"]

    @property
    def is_active(self):
        return self.recalled_at is None

    def __str__(self):
        status = "Active" if self.is_active else "Recalled"
        return f"KIV [{status}] — {self.expense.reference_number} revisit {self.revisit_date}"
