"""
Pure domain service for clinical reference range selection and result evaluation.
"""

from datetime import UTC, date, datetime
from decimal import Decimal, InvalidOperation

from app.models.patient import Gender
from app.models.test import TestParameter, TestParameterRange


def normalize_numeric_input(raw: str) -> str:
    """Strip thousands separators/whitespace so '10,000' parses as 10000.

    Nepal uses period decimals with comma thousand-groups; a raw comma must
    never silently truncate a value (parseFloat/Decimal stop at the comma).
    """
    return raw.strip().replace(",", "").replace(" ", "").replace("\u00a0", "")


def store_value(raw: str) -> str:
    """Canonical stored reading: normalized when numeric, raw-stripped otherwise."""
    normalized = normalize_numeric_input(raw)
    try:
        Decimal(normalized)
        return normalized
    except InvalidOperation:
        return raw.strip()


class ClinicalEngine:
    """Clinical interpretation engine for lab diagnostic parameters."""

    @staticmethod
    def calculate_patient_age(
        date_of_birth: date | None,
        fallback_age: int | None = None,
    ) -> int | None:
        """
        Calculate patient's current age in whole years.
        Prefers DOB if present, otherwise returns fallback_age.
        """
        if date_of_birth:
            today = datetime.now(UTC).date()
            years = today.year - date_of_birth.year
            if (today.month, today.day) < (date_of_birth.month, date_of_birth.day):
                years -= 1
            return max(0, years)
        return fallback_age

    @staticmethod
    def select_reference_range(
        ranges: list[TestParameterRange],
        patient_gender: Gender | str | None,
        patient_age: int | None,
    ) -> TestParameterRange | None:
        """
        Select the most specific biological reference range for a parameter
        matching the patient's biological sex and age in years.

        Specificity ranking (higher score = more specific):
          1. Exact gender match vs unisex (gender is None)
          2. Age range bound (both min and max set > one bound set > no age bounds)
        """
        applicable_ranges: list[tuple[int, TestParameterRange]] = []

        for r in ranges:
            # 1. Gender check
            if r.gender is not None and patient_gender is not None and r.gender != patient_gender:
                continue

            # 2. Age check
            if patient_age is not None:
                if r.age_min is not None and patient_age < r.age_min:
                    continue
                if r.age_max is not None and patient_age > r.age_max:
                    continue

            # 3. Calculate specificity score
            score = 0
            if r.gender is not None and r.gender == patient_gender:
                score += 10
            if r.age_min is not None and r.age_max is not None:
                score += 4
            elif r.age_min is not None or r.age_max is not None:
                score += 2

            applicable_ranges.append((score, r))

        if not applicable_ranges:
            return None

        # Sort descending by specificity score
        applicable_ranges.sort(key=lambda item: item[0], reverse=True)
        return applicable_ranges[0][1]

    @staticmethod
    def evaluate_value(
        raw_value: str,
        parameter: TestParameter,
        ref_range: TestParameterRange | None,
    ) -> tuple[str, bool]:
        """
        Evaluate an entered parameter value against its normal reference range.

        Returns:
            tuple[flag, is_abnormal]
            flag: "N" (Normal), "L" (Low), "H" (High), "ABNORMAL" (Text mismatch)
        """
        clean_val = normalize_numeric_input(raw_value)
        raw_stripped = raw_value.strip()

        # 1. Attempt numeric evaluation
        try:
            numeric_val = Decimal(clean_val)
            if parameter.calc_factor:
                numeric_val = numeric_val * parameter.calc_factor

            if ref_range:
                if ref_range.numeric_low is not None and numeric_val < ref_range.numeric_low:
                    return "L", True
                if ref_range.numeric_high is not None and numeric_val > ref_range.numeric_high:
                    return "H", True

            return "N", False

        except (InvalidOperation, TypeError):
            # Non-numeric string value
            pass

        # 2. Text evaluation (against the raw stripped text, not the numeric-normalized form)
        if ref_range and ref_range.text_normal:
            expected = ref_range.text_normal.strip().lower()
            if raw_stripped.lower() == expected:
                return "N", False
            return "ABNORMAL", True

        return "N", False
