"""
budget/models.py
================
Core models for the Budget Management module (Module 1 of EFMS).

Hierarchy:
  BudgetHead  →  BudgetSubHead  →  BudgetItem
                                       ↓
                               BudgetAllocation  (period-based allocation)

Budget code format (auto-generated):
  Head     : OPS          (abbreviation of name)
  SubHead  : OPS-UTL      (head code + sub-abbreviation)
  Item     : OPS-UTL-001  (subhead code + 3-digit sequence)
"""

import re

from django.core.exceptions import ValidationError
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 horilla.models import HorillaModel


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

def _make_abbreviation(name: str, max_len: int = 5) -> str:
    """
    Derive a short uppercase abbreviation from a name.
    Examples:
      "Operations"         → "OPS"
      "Human Resources"    → "HR"
      "Information Technology" → "IT"
    """
    words = re.sub(r"[^a-zA-Z0-9\s]", "", name).split()
    if not words:
        return "X"
    if len(words) == 1:
        abbr = words[0][:max_len].upper()
    else:
        abbr = "".join(w[0] for w in words).upper()[:max_len]
    return abbr or "X"


def _unique_code(base: str, model_class, field: str = "code", instance_pk=None) -> str:
    """
    Ensure a code is unique within `model_class`.  If `base` is taken,
    appends a numeric suffix: BASE2, BASE3, …
    """
    code = base
    counter = 2
    qs = model_class.objects.filter(**{field: code})
    if instance_pk:
        qs = qs.exclude(pk=instance_pk)
    while qs.exists():
        code = f"{base}{counter}"
        counter += 1
        qs = model_class.objects.filter(**{field: code})
        if instance_pk:
            qs = qs.exclude(pk=instance_pk)
    return code


# ---------------------------------------------------------------------------
# Period constants
# ---------------------------------------------------------------------------

PERIOD_ANNUAL = "annual"
PERIOD_QUARTERLY = "quarterly"
PERIOD_MONTHLY = "monthly"
PERIOD_WEEKLY = "weekly"

PERIOD_TYPE_CHOICES = [
    (PERIOD_ANNUAL, _("Annual")),
    (PERIOD_QUARTERLY, _("Quarterly")),
    (PERIOD_MONTHLY, _("Monthly")),
    (PERIOD_WEEKLY, _("Weekly")),
]


# ---------------------------------------------------------------------------
# BudgetSettings  (singleton config)
# ---------------------------------------------------------------------------

class BudgetSettings(HorillaModel):
    """
    Organisation-wide budget settings.
    There should only ever be one instance (enforced in clean()).
    """

    overrun_limit_percent = models.DecimalField(
        max_digits=5,
        decimal_places=2,
        default=0,
        verbose_name=_("Over-budget Limit (%)"),
        help_text=_(
            "Maximum percentage by which a request may exceed the remaining "
            "budget balance before it is blocked.  0 = no overrun allowed."
        ),
    )

    class Meta:
        verbose_name = _("Budget Settings")
        verbose_name_plural = _("Budget Settings")

    def clean(self):
        if self.overrun_limit_percent < 0:
            raise ValidationError(
                {"overrun_limit_percent": _("Limit cannot be negative.")}
            )
        # Only one settings record allowed
        qs = BudgetSettings.objects.exclude(pk=self.pk)
        if qs.exists():
            raise ValidationError(
                _("Only one Budget Settings record is allowed. Edit the existing one.")
            )

    def __str__(self):
        return f"Budget Settings (overrun limit: {self.overrun_limit_percent}%)"

    @classmethod
    def get_settings(cls) -> "BudgetSettings":
        obj, _ = cls.objects.get_or_create(pk=1, defaults={"overrun_limit_percent": 0})
        return obj


# ---------------------------------------------------------------------------
# BudgetHead
# ---------------------------------------------------------------------------

class BudgetHead(HorillaModel):
    """
    Top-level budget category (e.g. 'Operations', 'Human Resources').
    Code is auto-generated from the name abbreviation.
    """

    name = models.CharField(max_length=200, unique=True, verbose_name=_("Head Name"))
    code = models.CharField(
        max_length=20,
        unique=True,
        editable=False,
        verbose_name=_("Budget Code"),
    )
    description = models.TextField(blank=True, verbose_name=_("Description"))

    class Meta:
        verbose_name = _("Budget Head")
        verbose_name_plural = _("Budget Heads")
        ordering = ["name"]

    def save(self, *args, **kwargs):
        if not self.code:
            base = _make_abbreviation(self.name)
            self.code = _unique_code(base, BudgetHead, instance_pk=self.pk)
        super().save(*args, **kwargs)

    def __str__(self):
        return f"[{self.code}] {self.name}"

    # ------------------------------------------------------------------
    # Horilla URL helpers (used by HorillaListView actions)
    # ------------------------------------------------------------------

    def get_update_url(self):
        return reverse_lazy("budget-head-update", kwargs={"pk": self.pk})

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

    def get_delete_instance(self):
        return self.pk

    def get_nav_url(self):
        return reverse_lazy("budget-head-view")

    # ------------------------------------------------------------------
    # Horilla column helpers
    # ------------------------------------------------------------------

    def sub_head_count(self):
        count = self.sub_heads.filter(is_active=True).count()
        return format_html(
            '<span class="badge oh-badge oh-badge--primary">{}</span>', count
        )

    sub_head_count.short_description = _("Sub-Heads")


# ---------------------------------------------------------------------------
# BudgetSubHead
# ---------------------------------------------------------------------------

class BudgetSubHead(HorillaModel):
    """
    Second-level budget category (e.g. 'Utilities', 'Maintenance').
    Code is auto-generated as {HEAD_CODE}-{ABBREV}.
    """

    head = models.ForeignKey(
        BudgetHead,
        on_delete=models.CASCADE,
        related_name="sub_heads",
        verbose_name=_("Budget Head"),
    )
    name = models.CharField(max_length=200, verbose_name=_("Sub-Head Name"))
    code = models.CharField(
        max_length=30,
        unique=True,
        editable=False,
        verbose_name=_("Budget Code"),
    )
    description = models.TextField(blank=True, verbose_name=_("Description"))

    class Meta:
        verbose_name = _("Budget Sub-Head")
        verbose_name_plural = _("Budget Sub-Heads")
        ordering = ["head__name", "name"]
        unique_together = [["head", "name"]]

    def save(self, *args, **kwargs):
        if not self.code:
            abbr = _make_abbreviation(self.name)
            base = f"{self.head.code}-{abbr}"
            self.code = _unique_code(base, BudgetSubHead, instance_pk=self.pk)
        super().save(*args, **kwargs)

    def __str__(self):
        return f"[{self.code}] {self.name}"

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

    def get_update_url(self):
        return reverse_lazy("budget-sub-head-update", kwargs={"pk": self.pk})

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

    def get_delete_instance(self):
        return self.pk

    def get_nav_url(self):
        return reverse_lazy("budget-sub-head-view")

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

    def item_count(self):
        count = self.items.filter(is_active=True).count()
        return format_html(
            '<span class="badge oh-badge oh-badge--primary">{}</span>', count
        )

    item_count.short_description = _("Items")


# ---------------------------------------------------------------------------
# BudgetItem
# ---------------------------------------------------------------------------

class BudgetItem(HorillaModel):
    """
    Leaf-level budget line item (e.g. 'Generator Fuel', 'Electrical Repairs').
    Code is auto-generated as {SUBHEAD_CODE}-{3-digit-sequence}.
    """

    sub_head = models.ForeignKey(
        BudgetSubHead,
        on_delete=models.CASCADE,
        related_name="items",
        verbose_name=_("Budget Sub-Head"),
    )
    name = models.CharField(max_length=200, verbose_name=_("Item Name"))
    code = models.CharField(
        max_length=40,
        unique=True,
        editable=False,
        verbose_name=_("Budget Code"),
    )
    description = models.TextField(blank=True, verbose_name=_("Description"))

    class Meta:
        verbose_name = _("Budget Item")
        verbose_name_plural = _("Budget Items")
        ordering = ["sub_head__head__name", "sub_head__name", "name"]
        unique_together = [["sub_head", "name"]]

    def save(self, *args, **kwargs):
        if not self.code:
            # Sequence within this sub-head
            last = (
                BudgetItem.objects.filter(
                    sub_head=self.sub_head, code__startswith=self.sub_head.code
                )
                .order_by("code")
                .last()
            )
            if last:
                try:
                    last_seq = int(last.code.rsplit("-", 1)[-1])
                except ValueError:
                    last_seq = 0
            else:
                last_seq = 0
            self.code = f"{self.sub_head.code}-{last_seq + 1:03d}"
        super().save(*args, **kwargs)

    def __str__(self):
        return f"[{self.code}] {self.name}"

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

    def get_update_url(self):
        return reverse_lazy("budget-item-update", kwargs={"pk": self.pk})

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

    def get_delete_instance(self):
        return self.pk

    def get_nav_url(self):
        return reverse_lazy("budget-item-view")

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

    def allocation_count(self):
        count = self.allocations.count()
        return format_html(
            '<span class="badge oh-badge oh-badge--primary">{}</span>', count
        )

    allocation_count.short_description = _("Allocations")


# ---------------------------------------------------------------------------
# BudgetAllocation
# ---------------------------------------------------------------------------

class BudgetAllocation(HorillaModel):
    """
    Period-based allocation record for a BudgetItem.

    One record per (item, fiscal_year, period_type, period_value).
    The `period_value` semantics depend on `period_type`:
      - annual    → NULL (one per fiscal year)
      - quarterly → 1–4
      - monthly   → 1–12
      - weekly    → 1–53
    """

    item = models.ForeignKey(
        BudgetItem,
        on_delete=models.CASCADE,
        related_name="allocations",
        verbose_name=_("Budget Item"),
    )
    fiscal_year = models.PositiveSmallIntegerField(
        verbose_name=_("Fiscal Year"),
        help_text=_("e.g. 2025 for FY 2025 (Jan–Dec)"),
    )
    period_type = models.CharField(
        max_length=20,
        choices=PERIOD_TYPE_CHOICES,
        default=PERIOD_ANNUAL,
        verbose_name=_("Period Type"),
    )
    period_value = models.PositiveSmallIntegerField(
        null=True,
        blank=True,
        verbose_name=_("Period Value"),
        help_text=_("Quarter (1–4), Month (1–12), or Week (1–53). Leave blank for Annual."),
    )
    allocated_amount = models.DecimalField(
        max_digits=15,
        decimal_places=2,
        default=0,
        verbose_name=_("Allocated Amount (₦)"),
    )
    # Running totals — updated by signals from expense workflow
    committed_amount = models.DecimalField(
        max_digits=15,
        decimal_places=2,
        default=0,
        verbose_name=_("Committed Amount (₦)"),
        help_text=_("Total of pending/submitted expense requests."),
    )
    approved_amount = models.DecimalField(
        max_digits=15,
        decimal_places=2,
        default=0,
        verbose_name=_("Approved Amount (₦)"),
        help_text=_("Total of approved but not yet disbursed requests."),
    )
    disbursed_amount = models.DecimalField(
        max_digits=15,
        decimal_places=2,
        default=0,
        verbose_name=_("Disbursed Amount (₦)"),
        help_text=_("Total actually paid out."),
    )

    class Meta:
        verbose_name = _("Budget Allocation")
        verbose_name_plural = _("Budget Allocations")
        ordering = ["-fiscal_year", "period_type", "period_value"]
        unique_together = [["item", "fiscal_year", "period_type", "period_value"]]

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

    def get_update_url(self):
        return reverse_lazy("budget-allocation-update", kwargs={"pk": self.pk})

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

    def get_delete_instance(self):
        return self.pk

    def get_nav_url(self):
        return reverse_lazy("budget-allocation-view")

    # ------------------------------------------------------------------
    # Computed properties
    # ------------------------------------------------------------------

    @property
    def available_balance(self):
        """Remaining budget = allocated – disbursed."""
        return self.allocated_amount - self.disbursed_amount

    @property
    def utilization_percent(self):
        if self.allocated_amount == 0:
            return 0
        return round((self.disbursed_amount / self.allocated_amount) * 100, 1)

    @property
    def is_overrun(self):
        return self.disbursed_amount > self.allocated_amount

    @property
    def variance(self):
        """Positive = surplus, Negative = overrun."""
        return self.allocated_amount - self.disbursed_amount

    def check_overrun(self, requested_amount):
        """
        Returns (allowed: bool, reason: str).
        Validates whether a new request of `requested_amount` can proceed
        given the current balance and the configured overrun limit.
        """
        settings = BudgetSettings.get_settings()
        limit_pct = float(settings.overrun_limit_percent)
        balance = float(self.available_balance)
        req = float(requested_amount)

        if req <= balance:
            return True, ""

        if limit_pct == 0:
            return False, _(
                "Insufficient budget balance. Available: ₦{:.2f}, Requested: ₦{:.2f}."
            ).format(balance, req)

        max_allowed = float(self.allocated_amount) * (1 + limit_pct / 100)
        already_out = float(self.disbursed_amount) + float(self.approved_amount) + float(self.committed_amount)
        if already_out + req <= max_allowed:
            return True, _(
                "Warning: This request exceeds the budget balance but is within the "
                "{:.0f}% overrun limit."
            ).format(limit_pct)

        return False, _(
            "Request exceeds the budget balance and the configured {:.0f}% overrun limit. "
            "Available: ₦{:.2f}, Requested: ₦{:.2f}."
        ).format(limit_pct, balance, req)

    # ------------------------------------------------------------------
    # Display helpers for HorillaListView
    # ------------------------------------------------------------------

    def period_label(self):
        labels = {
            PERIOD_ANNUAL: _("Annual"),
            PERIOD_QUARTERLY: _("Q{}").format(self.period_value or ""),
            PERIOD_MONTHLY: _(
                "{month} {year}"
            ).format(
                month=[
                    "", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
                    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
                ][self.period_value or 0] if self.period_value else "-",
                year=self.fiscal_year,
            ),
            PERIOD_WEEKLY: _("Week {} / {}").format(self.period_value, self.fiscal_year),
        }
        return labels.get(self.period_type, self.period_type)

    period_label.short_description = _("Period")

    def balance_display(self):
        balance = self.available_balance
        colour = "oh-badge--danger" if balance < 0 else "oh-badge--success"
        return format_html(
            '<span class="badge oh-badge {}">{}</span>',
            colour,
            f"₦{balance:,.2f}",
        )

    balance_display.short_description = _("Available Balance")

    def overrun_indicator(self):
        if self.is_overrun:
            return format_html(
                '<span class="badge oh-badge oh-badge--danger">'
                '<ion-icon name="warning-outline"></ion-icon> Over-budget'
                "</span>"
            )
        return format_html(
            '<span class="badge oh-badge oh-badge--success">On Track</span>'
        )

    overrun_indicator.short_description = _("Status")

    def __str__(self):
        return (
            f"[{self.item.code}] {self.period_type.capitalize()} "
            f"FY{self.fiscal_year} — ₦{self.allocated_amount:,.2f}"
        )
