"""Process KPI models — Phase 5.

Key Performance Indicators linked to organisational process definitions.

Model inventory
---------------
ProcessKPI          — a KPI definition tied to a process (KPI-111-01 …).
ProcessKPIMeasurement — a recorded measurement value for a KPI.
KPIAlert            — alert triggered when a measurement misses its target.

Design decisions
----------------
* ``ProcessKPI.instance`` on ProcessKPIMeasurement is a nullable FK that
  will be wired to ProcessInstance in Phase 6.  For now it accepts null
  so KPIs can be measured outside a formal instance.
* ``is_target_met`` is computed in Python (not a DB column) based on
  ``target_operator`` and ``target_value`` from the parent KPI.
* ``KPIAlert.notified_users`` is M2M so multiple stakeholders can be
  notified for the same breach.
* ``ProcessKPI.pcf_metric`` is a nullable soft FK to PCFMetric — allows
  mapping to APQC standard metrics without enforcing referential integrity
  across modules.
"""

from __future__ import annotations

import decimal
import operator as op_mod
from typing import ClassVar

from django.conf import settings
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

from simorgh.core.models import TimeStampedModel, UUIDModel

from simorgh.apps.bpm.models.pcf import PCFMetric
from simorgh.apps.bpm.models.process import ProcessDefinition
from simorgh.apps.bpm.models.raci import ProcessRole


# ---------------------------------------------------------------------------
# Choices
# ---------------------------------------------------------------------------

class KPICategory(models.TextChoices):
    EFFECTIVENESS = "effectiveness", _("Effectiveness / اثربخشی")
    EFFICIENCY    = "efficiency",    _("Efficiency / کارایی")
    COMPLIANCE    = "compliance",    _("Compliance / انطباق")
    QUALITY       = "quality",       _("Quality / کیفیت")


class KPITargetOperator(models.TextChoices):
    GTE = ">=", _("≥ (greater than or equal)")
    LTE = "<=", _("≤ (less than or equal)")
    EQ  = "=",  _("= (equal)")
    GT  = ">",  _("› (greater than)")
    LT  = "<",  _("‹ (less than)")


class KPIAlertType(models.TextChoices):
    BELOW_TARGET = "below_target", _("Below Target")
    ABOVE_TARGET = "above_target", _("Above Target")


# Mapping of operator string → Python callable
_OP_MAP: ClassVar[dict] = {
    ">=": op_mod.ge,
    "<=": op_mod.le,
    "=":  op_mod.eq,
    ">":  op_mod.gt,
    "<":  op_mod.lt,
}


# ---------------------------------------------------------------------------
# ProcessKPI
# ---------------------------------------------------------------------------

class ProcessKPI(UUIDModel, TimeStampedModel):
    """A Key Performance Indicator linked to a ProcessDefinition.

    ``code`` follows the pattern KPI-<hierarchy_id without dots>-<seq>,
    e.g. KPI-111-01 for the first KPI of process 1.1.1.
    """

    process = models.ForeignKey(
        ProcessDefinition,
        on_delete=models.CASCADE,
        related_name="kpis",
        verbose_name=_("process"),
    )
    code = models.CharField(_("code"), max_length=40)
    name = models.CharField(_("name"), max_length=255)
    name_fa = models.CharField(_("name (FA)"), max_length=255, blank=True)
    category = models.CharField(
        _("category"), max_length=20,
        choices=KPICategory.choices,
        default=KPICategory.EFFECTIVENESS,
    )
    formula = models.TextField(
        _("formula"), blank=True,
        help_text=_("Calculation formula or description."),
    )
    target_value = models.DecimalField(
        _("target value"), max_digits=18, decimal_places=4,
    )
    target_operator = models.CharField(
        _("target operator"), max_length=2,
        choices=KPITargetOperator.choices,
        default=KPITargetOperator.GTE,
    )
    unit = models.CharField(
        _("unit"), max_length=30, blank=True,
        help_text=_("%  |  day  |  count  |  score …"),
    )
    frequency = models.CharField(
        _("measurement frequency"), max_length=100, blank=True,
        help_text=_("How often to measure, e.g. 'سالانه', 'هر ارزیابی'."),
    )
    owner_role = models.ForeignKey(
        ProcessRole,
        on_delete=models.SET_NULL,
        null=True, blank=True,
        related_name="owned_kpis",
        verbose_name=_("owner role"),
    )
    # Soft FK to APQC PCF standard metric — no DB constraint
    pcf_metric = models.ForeignKey(
        PCFMetric,
        on_delete=models.SET_NULL,
        null=True, blank=True,
        related_name="process_kpis",
        verbose_name=_("PCF metric"),
    )
    description = models.TextField(_("description"), blank=True)

    class Meta:
        verbose_name = _("process KPI")
        verbose_name_plural = _("process KPIs")
        ordering = ("process", "category", "code")
        constraints = [
            models.UniqueConstraint(
                fields=("process", "code"),
                name="bpm_processkpi_process_code_uniq",
            ),
        ]

    def __str__(self) -> str:
        return f"{self.process.hierarchy_id} — {self.code} ({self.name})"

    def evaluate(self, value: decimal.Decimal) -> bool:
        """Return True if *value* satisfies the target."""
        comparator = _OP_MAP.get(self.target_operator, op_mod.ge)
        return comparator(value, self.target_value)


# ---------------------------------------------------------------------------
# ProcessKPIMeasurement
# ---------------------------------------------------------------------------

class ProcessKPIMeasurement(TimeStampedModel):
    """A recorded measurement value for a ProcessKPI.

    ``is_target_met`` is computed by comparing ``value`` against the KPI's
    ``target_value`` using ``target_operator``; it is **not** stored in DB.
    """

    kpi = models.ForeignKey(
        ProcessKPI,
        on_delete=models.CASCADE,
        related_name="measurements",
        verbose_name=_("KPI"),
    )
    # Will be linked to ProcessInstance in Phase 6
    process_instance_id = models.UUIDField(
        _("process instance ID"), null=True, blank=True,
        help_text=_("FK to ProcessInstance.public_id — wired in Phase 6."),
    )
    measured_at = models.DateTimeField(_("measured at"), default=timezone.now)
    value = models.DecimalField(
        _("value"), max_digits=18, decimal_places=4,
    )
    measured_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True, blank=True,
        related_name="bpm_kpi_measurements",
        verbose_name=_("measured by"),
    )
    notes = models.TextField(_("notes"), blank=True)

    class Meta:
        verbose_name = _("KPI measurement")
        verbose_name_plural = _("KPI measurements")
        ordering = ("-measured_at",)

    def __str__(self) -> str:
        return f"{self.kpi.code} @ {self.measured_at:%Y-%m-%d} = {self.value}"

    @property
    def is_target_met(self) -> bool:
        """True if this measurement satisfies the KPI target."""
        return self.kpi.evaluate(self.value)


# ---------------------------------------------------------------------------
# KPIAlert
# ---------------------------------------------------------------------------

class KPIAlert(TimeStampedModel):
    """Alert generated when a measurement misses its KPI target."""

    kpi = models.ForeignKey(
        ProcessKPI,
        on_delete=models.CASCADE,
        related_name="alerts",
        verbose_name=_("KPI"),
    )
    measurement = models.ForeignKey(
        ProcessKPIMeasurement,
        on_delete=models.CASCADE,
        related_name="alerts",
        verbose_name=_("measurement"),
    )
    alert_type = models.CharField(
        _("alert type"), max_length=20,
        choices=KPIAlertType.choices,
    )
    notified_users = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        blank=True,
        related_name="bpm_kpi_alerts",
        verbose_name=_("notified users"),
    )

    class Meta:
        verbose_name = _("KPI alert")
        verbose_name_plural = _("KPI alerts")
        ordering = ("-created_at",)

    def __str__(self) -> str:
        return f"Alert [{self.alert_type}] — {self.kpi.code}"
