"""Applicant expense profile scoring algorithm.

The module is intentionally pure and framework-free. Service layers can build the
input snapshot from database records, while this module only owns the formula,
scores, thresholds, and explainable result.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
from typing import Any

ZERO = Decimal("0")
ONE = Decimal("1")
HUNDRED = Decimal("100")

LEVEL_NORMAL = "normal"
LEVEL_WATCH = "watch"
LEVEL_REVIEW = "review"
LEVEL_ESCALATION = "escalation"


@dataclass(slots=True)
class ApplicantExpenseProfileInput:
    """Inputs for applicant expense behavior scoring.

    Values should be pre-aggregated by a comparable peer group, such as
    department + role + expense type + city grade + project type.
    """

    applicant_claim_count_90d: int = 0
    peer_claim_count_p75_90d: Any = ZERO
    applicant_amount_90d: Any = ZERO
    available_peer_budget_90d: Any = ZERO
    amount_percentile: Any = ZERO
    peer_amount_median_90d: Any = ZERO
    adjusted_or_returned_count_180d: int = 0
    approved_claim_count_180d: int = 0
    requested_days: Any = ZERO
    peer_travel_days_p75: Any = ZERO
    business_buffer_days: Any = ONE
    claim_amount: Any = ZERO
    peer_daily_cost_baseline: Any = ZERO
    tolerance_factor: Any = Decimal("1.20")
    entertainment_amount: Any = ZERO
    attendee_count: int = 0
    entertainment_standard_cap: Any = ZERO
    same_customer_frequency_90d: int = 0
    applicant_entertainment_percentile: Any = ZERO
    hard_rule_score: int = 0


@dataclass(slots=True)
class ApplicantExpenseProfileResult:
    profile_score: int
    profile_level: str
    recommendation_score: int
    recommendation_level: str
    frequency_score: int
    amount_occupancy_score: int
    peer_deviation_score: int
    adjustment_history_score: int
    current_claim_deviation_score: int
    travel_days_deviation_score: int
    daily_cost_deviation_score: int
    entertainment_deviation_score: int
    frequency_ratio: Decimal
    budget_share_ratio: Decimal
    peer_deviation_ratio: Decimal
    adjustment_ratio: Decimal
    travel_days_ratio: Decimal
    daily_cost_ratio: Decimal
    per_capita_entertainment_ratio: Decimal
    suggested_days: Decimal | None
    suggested_amount_cap: Decimal | None
    basis_codes: list[str] = field(default_factory=list)

    def as_dict(self) -> dict[str, Any]:
        return {
            "profile_score": self.profile_score,
            "profile_level": self.profile_level,
            "recommendation_score": self.recommendation_score,
            "recommendation_level": self.recommendation_level,
            "scores": {
                "frequency_score": self.frequency_score,
                "amount_occupancy_score": self.amount_occupancy_score,
                "peer_deviation_score": self.peer_deviation_score,
                "adjustment_history_score": self.adjustment_history_score,
                "current_claim_deviation_score": self.current_claim_deviation_score,
                "travel_days_deviation_score": self.travel_days_deviation_score,
                "daily_cost_deviation_score": self.daily_cost_deviation_score,
                "entertainment_deviation_score": self.entertainment_deviation_score,
            },
            "metrics": {
                "frequency_ratio": _format_decimal(self.frequency_ratio),
                "budget_share_ratio": _format_decimal(self.budget_share_ratio),
                "peer_deviation_ratio": _format_decimal(self.peer_deviation_ratio),
                "adjustment_ratio": _format_decimal(self.adjustment_ratio),
                "travel_days_ratio": _format_decimal(self.travel_days_ratio),
                "daily_cost_ratio": _format_decimal(self.daily_cost_ratio),
                "per_capita_entertainment_ratio": _format_decimal(
                    self.per_capita_entertainment_ratio
                ),
                "suggested_days": _format_decimal(self.suggested_days),
                "suggested_amount_cap": _format_decimal(self.suggested_amount_cap),
            },
            "basis_codes": list(self.basis_codes),
        }


def evaluate_applicant_expense_profile(
    payload: ApplicantExpenseProfileInput,
) -> ApplicantExpenseProfileResult:
    frequency_ratio = _ratio(payload.applicant_claim_count_90d, payload.peer_claim_count_p75_90d)
    frequency_score = _score_frequency_ratio(frequency_ratio)

    budget_share_ratio = _ratio(payload.applicant_amount_90d, payload.available_peer_budget_90d)
    amount_percentile_score = _score_percentile(_to_decimal(payload.amount_percentile))
    budget_share_score = _score_budget_share_ratio(budget_share_ratio)
    amount_occupancy_score = max(amount_percentile_score, budget_share_score)

    peer_deviation_ratio = _ratio(payload.applicant_amount_90d, payload.peer_amount_median_90d)
    peer_deviation_score = _score_peer_deviation_ratio(peer_deviation_ratio)

    adjustment_ratio = _ratio(
        payload.adjusted_or_returned_count_180d,
        payload.approved_claim_count_180d,
    )
    adjustment_history_score = _score_adjustment_ratio(adjustment_ratio)

    travel_days_ratio = _ratio(payload.requested_days, payload.peer_travel_days_p75)
    travel_days_deviation_score = _score_travel_days_ratio(travel_days_ratio)

    requested_days = _to_decimal(payload.requested_days)
    daily_cost = _ratio(payload.claim_amount, requested_days)
    daily_cost_ratio = _ratio(daily_cost, payload.peer_daily_cost_baseline)
    daily_cost_deviation_score = _score_daily_cost_ratio(daily_cost_ratio)

    per_capita_amount = _ratio(payload.entertainment_amount, payload.attendee_count)
    per_capita_entertainment_ratio = _ratio(
        per_capita_amount,
        payload.entertainment_standard_cap,
    )
    entertainment_deviation_score = max(
        _score_entertainment_per_capita_ratio(per_capita_entertainment_ratio),
        _score_same_customer_frequency(payload.same_customer_frequency_90d),
        _score_percentile(_to_decimal(payload.applicant_entertainment_percentile)),
    )

    current_claim_deviation_score = max(
        travel_days_deviation_score,
        daily_cost_deviation_score,
        entertainment_deviation_score,
    )

    profile_score = _weighted_profile_score(
        frequency_score=frequency_score,
        amount_occupancy_score=amount_occupancy_score,
        peer_deviation_score=peer_deviation_score,
        adjustment_history_score=adjustment_history_score,
        current_claim_deviation_score=current_claim_deviation_score,
    )

    hard_rule_score = _clamp_score(payload.hard_rule_score)
    recommendation_score = max(profile_score, current_claim_deviation_score, hard_rule_score)

    suggested_days = _suggest_days(
        requested_days=requested_days,
        baseline_days=_to_decimal(payload.peer_travel_days_p75),
        business_buffer_days=_to_decimal(payload.business_buffer_days),
    )
    suggested_amount_cap = _suggest_amount_cap(
        suggested_days=suggested_days,
        daily_cost_baseline=_to_decimal(payload.peer_daily_cost_baseline),
        tolerance_factor=_to_decimal(payload.tolerance_factor),
    )

    basis_codes = _build_basis_codes(
        frequency_ratio=frequency_ratio,
        amount_percentile=_to_decimal(payload.amount_percentile),
        budget_share_ratio=budget_share_ratio,
        peer_deviation_ratio=peer_deviation_ratio,
        adjustment_ratio=adjustment_ratio,
        travel_days_ratio=travel_days_ratio,
        daily_cost_ratio=daily_cost_ratio,
        per_capita_entertainment_ratio=per_capita_entertainment_ratio,
        same_customer_frequency_90d=payload.same_customer_frequency_90d,
        applicant_entertainment_percentile=_to_decimal(payload.applicant_entertainment_percentile),
        hard_rule_score=hard_rule_score,
    )

    return ApplicantExpenseProfileResult(
        profile_score=profile_score,
        profile_level=level_from_score(profile_score),
        recommendation_score=recommendation_score,
        recommendation_level=level_from_score(recommendation_score),
        frequency_score=frequency_score,
        amount_occupancy_score=amount_occupancy_score,
        peer_deviation_score=peer_deviation_score,
        adjustment_history_score=adjustment_history_score,
        current_claim_deviation_score=current_claim_deviation_score,
        travel_days_deviation_score=travel_days_deviation_score,
        daily_cost_deviation_score=daily_cost_deviation_score,
        entertainment_deviation_score=entertainment_deviation_score,
        frequency_ratio=frequency_ratio,
        budget_share_ratio=budget_share_ratio,
        peer_deviation_ratio=peer_deviation_ratio,
        adjustment_ratio=adjustment_ratio,
        travel_days_ratio=travel_days_ratio,
        daily_cost_ratio=daily_cost_ratio,
        per_capita_entertainment_ratio=per_capita_entertainment_ratio,
        suggested_days=suggested_days,
        suggested_amount_cap=suggested_amount_cap,
        basis_codes=basis_codes,
    )


def level_from_score(score: int) -> str:
    normalized = _clamp_score(score)
    if normalized >= 80:
        return LEVEL_ESCALATION
    if normalized >= 60:
        return LEVEL_REVIEW
    if normalized >= 40:
        return LEVEL_WATCH
    return LEVEL_NORMAL


def _weighted_profile_score(
    *,
    frequency_score: int,
    amount_occupancy_score: int,
    peer_deviation_score: int,
    adjustment_history_score: int,
    current_claim_deviation_score: int,
) -> int:
    weighted = (
        Decimal(frequency_score) * Decimal("0.20")
        + Decimal(amount_occupancy_score) * Decimal("0.25")
        + Decimal(peer_deviation_score) * Decimal("0.25")
        + Decimal(adjustment_history_score) * Decimal("0.15")
        + Decimal(current_claim_deviation_score) * Decimal("0.15")
    )
    return _clamp_score(int(weighted.quantize(Decimal("1"), rounding=ROUND_HALF_UP)))


def _score_frequency_ratio(ratio: Decimal) -> int:
    return _score_ratio(
        ratio,
        [(Decimal("1.0"), 0), (Decimal("1.2"), 30), (Decimal("1.5"), 60), (Decimal("2.0"), 80)],
    )


def _score_budget_share_ratio(ratio: Decimal) -> int:
    return _score_ratio(ratio, [(Decimal("0.10"), 0), (Decimal("0.20"), 40), (Decimal("0.35"), 70)])


def _score_peer_deviation_ratio(ratio: Decimal) -> int:
    return _score_ratio(ratio, [(Decimal("1.0"), 0), (Decimal("1.3"), 40), (Decimal("1.8"), 70)])


def _score_adjustment_ratio(ratio: Decimal) -> int:
    return _score_ratio(ratio, [(Decimal("0.05"), 0), (Decimal("0.15"), 40), (Decimal("0.30"), 70)])


def _score_travel_days_ratio(ratio: Decimal) -> int:
    return _score_ratio(ratio, [(Decimal("1.2"), 0), (Decimal("1.5"), 40), (Decimal("2.0"), 70)])


def _score_daily_cost_ratio(ratio: Decimal) -> int:
    return _score_ratio(ratio, [(Decimal("1.1"), 0), (Decimal("1.3"), 40), (Decimal("1.6"), 70)])


def _score_entertainment_per_capita_ratio(ratio: Decimal) -> int:
    return _score_ratio(ratio, [(Decimal("1.0"), 0), (Decimal("1.2"), 40), (Decimal("1.5"), 70)])


def _score_same_customer_frequency(frequency: int) -> int:
    if frequency >= 5:
        return 100
    if frequency >= 3:
        return 70
    if frequency >= 2:
        return 40
    return 0


def _score_percentile(percentile: Decimal) -> int:
    normalized = max(ZERO, min(HUNDRED, percentile))
    if normalized <= Decimal("75"):
        return 0
    if normalized <= Decimal("85"):
        return 40
    if normalized <= Decimal("95"):
        return 70
    return 100


def _score_ratio(ratio: Decimal, bands: list[tuple[Decimal, int]]) -> int:
    if ratio <= ZERO:
        return 0
    for upper_bound, score in bands:
        if ratio <= upper_bound:
            return score
    return 100


def _suggest_days(
    *,
    requested_days: Decimal,
    baseline_days: Decimal,
    business_buffer_days: Decimal,
) -> Decimal | None:
    if requested_days <= ZERO:
        return None
    if baseline_days <= ZERO:
        return _quantize(requested_days)
    return _quantize(min(requested_days, baseline_days + max(ZERO, business_buffer_days)))


def _suggest_amount_cap(
    *,
    suggested_days: Decimal | None,
    daily_cost_baseline: Decimal,
    tolerance_factor: Decimal,
) -> Decimal | None:
    if suggested_days is None or suggested_days <= ZERO or daily_cost_baseline <= ZERO:
        return None
    factor = tolerance_factor if tolerance_factor > ZERO else ONE
    return _quantize_money(suggested_days * daily_cost_baseline * factor)


def _build_basis_codes(
    *,
    frequency_ratio: Decimal,
    amount_percentile: Decimal,
    budget_share_ratio: Decimal,
    peer_deviation_ratio: Decimal,
    adjustment_ratio: Decimal,
    travel_days_ratio: Decimal,
    daily_cost_ratio: Decimal,
    per_capita_entertainment_ratio: Decimal,
    same_customer_frequency_90d: int,
    applicant_entertainment_percentile: Decimal,
    hard_rule_score: int,
) -> list[str]:
    basis_codes: list[str] = []

    if frequency_ratio > Decimal("1.5"):
        basis_codes.append("applicant.frequency.ratio_review")
    elif frequency_ratio > Decimal("1.2"):
        basis_codes.append("applicant.frequency.ratio_watch")

    if amount_percentile > Decimal("95"):
        basis_codes.append("applicant.amount_percentile.p95")
    elif amount_percentile > Decimal("85"):
        basis_codes.append("applicant.amount_percentile.p85")

    if budget_share_ratio > Decimal("0.35"):
        basis_codes.append("applicant.budget_share.high")
    elif budget_share_ratio > Decimal("0.20"):
        basis_codes.append("applicant.budget_share.watch")

    if peer_deviation_ratio > Decimal("1.8"):
        basis_codes.append("applicant.peer_deviation.escalation")
    elif peer_deviation_ratio > Decimal("1.3"):
        basis_codes.append("applicant.peer_deviation.review")

    if adjustment_ratio > Decimal("0.30"):
        basis_codes.append("applicant.adjustment_history.escalation")
    elif adjustment_ratio > Decimal("0.15"):
        basis_codes.append("applicant.adjustment_history.review")

    if travel_days_ratio > Decimal("2.0"):
        basis_codes.append("travel.days_ratio.escalation")
    elif travel_days_ratio > Decimal("1.5"):
        basis_codes.append("travel.days_ratio.review")
    elif travel_days_ratio > Decimal("1.2"):
        basis_codes.append("travel.days_ratio.watch")

    if daily_cost_ratio > Decimal("1.6"):
        basis_codes.append("travel.daily_cost_ratio.escalation")
    elif daily_cost_ratio > Decimal("1.3"):
        basis_codes.append("travel.daily_cost_ratio.review")
    elif daily_cost_ratio > Decimal("1.1"):
        basis_codes.append("travel.daily_cost_ratio.watch")

    if per_capita_entertainment_ratio > Decimal("1.5"):
        basis_codes.append("entertainment.per_capita.escalation")
    elif per_capita_entertainment_ratio > Decimal("1.2"):
        basis_codes.append("entertainment.per_capita.review")

    if same_customer_frequency_90d >= 5:
        basis_codes.append("entertainment.same_customer.frequency_escalation")
    elif same_customer_frequency_90d >= 3:
        basis_codes.append("entertainment.same_customer.frequency_review")

    if applicant_entertainment_percentile > Decimal("95"):
        basis_codes.append("entertainment.amount_percentile.p95")
    elif applicant_entertainment_percentile > Decimal("85"):
        basis_codes.append("entertainment.amount_percentile.p85")

    if hard_rule_score >= 80:
        basis_codes.append("hard_rule.escalation")
    elif hard_rule_score >= 60:
        basis_codes.append("hard_rule.review")

    return basis_codes


def _ratio(numerator: Any, denominator: Any) -> Decimal:
    divisor = _to_decimal(denominator)
    if divisor <= ZERO:
        return ZERO
    return _quantize(_to_decimal(numerator) / divisor)


def _to_decimal(value: Any) -> Decimal:
    if value is None:
        return ZERO
    if isinstance(value, Decimal):
        return value
    try:
        return Decimal(str(value))
    except (InvalidOperation, ValueError):
        return ZERO


def _quantize(value: Decimal) -> Decimal:
    return value.quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)


def _quantize_money(value: Decimal) -> Decimal:
    return value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)


def _format_decimal(value: Decimal | None) -> str | None:
    if value is None:
        return None
    return format(value.normalize(), "f")


def _clamp_score(score: int) -> int:
    return max(0, min(100, int(score)))
