"""Employee behavior profile scoring algorithms.

This module is deliberately pure: database services prepare metrics, while
the formula layer owns normalization, score composition, levels, and advice.
"""

from __future__ import annotations

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

ALGORITHM_VERSION = "employee_behavior_profile.v1"

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

PROFILE_LABELS = {
    "expense": "费用支出画像",
    "process_quality": "流程质量画像",
    "ai_usage": "AI 协作强度",
    "approval": "审批行为画像",
}

LEVEL_LABELS = {
    LEVEL_NORMAL: "正常",
    LEVEL_WATCH: "关注",
    LEVEL_REVIEW: "复核",
    LEVEL_ESCALATION: "升级关注",
}

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


@dataclass(slots=True)
class ProfileComponent:
    code: str
    label: str
    score: int
    value: Any = None
    unit: str = ""
    weight: Decimal = Decimal("0")
    detail: str = ""

    def as_dict(self) -> dict[str, Any]:
        return {
            "code": self.code,
            "label": self.label,
            "score": _clamp_score(self.score),
            "value": _format_value(self.value),
            "unit": self.unit,
            "weight": _format_decimal(self.weight),
            "detail": self.detail,
        }


@dataclass(slots=True)
class ProfileScoreResult:
    profile_type: str
    profile_score: int
    profile_level: str
    components: list[ProfileComponent] = field(default_factory=list)
    metrics: dict[str, Any] = field(default_factory=dict)

    @property
    def profile_label(self) -> str:
        return PROFILE_LABELS.get(self.profile_type, self.profile_type)

    @property
    def profile_level_label(self) -> str:
        return LEVEL_LABELS.get(self.profile_level, self.profile_level)

    def top_contributors(self, limit: int = 5) -> list[dict[str, Any]]:
        ranked = sorted(
            self.components,
            key=lambda item: (Decimal(_clamp_score(item.score)) * item.weight, item.score),
            reverse=True,
        )
        return [item.as_dict() for item in ranked[: max(0, limit)] if item.score > 0]

    def as_dict(self) -> dict[str, Any]:
        return {
            "profile_type": self.profile_type,
            "profile_label": self.profile_label,
            "profile_score": self.profile_score,
            "profile_level": self.profile_level,
            "profile_level_label": self.profile_level_label,
            "components": [item.as_dict() for item in self.components],
            "top_contributors": self.top_contributors(),
            "metrics": _json_safe(self.metrics),
        }


def normalize_by_peer_percentiles(value: Any, p50: Any, p90: Any) -> int:
    """Map a metric to 0-100 with peer P50 as zero and peer P90 as full score."""

    current = _to_decimal(value)
    median = _to_decimal(p50)
    high = _to_decimal(p90)
    if current <= median or high <= median:
        return 0
    raw_score = HUNDRED * (current - median) / (high - median)
    return _clamp_score(raw_score)


def score_by_bands(value: Any, bands: list[tuple[Any, int]]) -> int:
    """Piecewise linear score where each tuple is a threshold and score."""

    normalized = _to_decimal(value)
    if not bands:
        return 0

    points = [(_to_decimal(threshold), _clamp_score(score)) for threshold, score in bands]
    points.sort(key=lambda item: item[0])

    if normalized <= points[0][0]:
        return points[0][1]

    for index in range(1, len(points)):
        previous_threshold, previous_score = points[index - 1]
        next_threshold, next_score = points[index]
        if normalized > next_threshold:
            continue
        if next_threshold == previous_threshold:
            return next_score
        ratio = (normalized - previous_threshold) / (next_threshold - previous_threshold)
        interpolated = Decimal(previous_score) + ratio * Decimal(next_score - previous_score)
        return _clamp_score(interpolated)

    return points[-1][1]


def evaluate_weighted_profile(
    profile_type: str,
    components: list[ProfileComponent],
    metrics: dict[str, Any] | None = None,
) -> ProfileScoreResult:
    total_weight = sum((_to_decimal(item.weight) for item in components), ZERO)
    if total_weight <= ZERO:
        profile_score = max((_clamp_score(item.score) for item in components), default=0)
    else:
        weighted = (
            sum(Decimal(_clamp_score(item.score)) * _to_decimal(item.weight) for item in components)
            / total_weight
        )
        profile_score = _clamp_score(weighted)

    return ProfileScoreResult(
        profile_type=profile_type,
        profile_score=profile_score,
        profile_level=level_from_score(profile_score),
        components=components,
        metrics=metrics or {},
    )


def calculate_review_priority_score(
    *,
    expense_profile_score: Any,
    process_quality_score: Any,
) -> int:
    weighted = _to_decimal(expense_profile_score) * Decimal("0.70") + _to_decimal(
        process_quality_score
    ) * Decimal("0.30")
    return _clamp_score(weighted)


def build_review_suggestions(
    *,
    expense_profile_score: Any,
    process_quality_score: Any,
    requested_days: Any = None,
    peer_days_p75: Any = None,
    policy_limit: Any = None,
    peer_unit_amount_p75: Any = None,
) -> list[dict[str, Any]]:
    review_score = calculate_review_priority_score(
        expense_profile_score=expense_profile_score,
        process_quality_score=process_quality_score,
    )
    level = level_from_score(review_score)
    suggestions: list[dict[str, Any]] = []

    if _to_decimal(requested_days) > ZERO and _to_decimal(peer_days_p75) > ZERO:
        suggested_days = min(
            _to_decimal(requested_days),
            _to_decimal(peer_days_p75) * _level_factor(level),
        )
        if suggested_days < _to_decimal(requested_days):
            suggestions.append(
                {
                    "type": "review_travel_days",
                    "severity": _severity_from_level(level),
                    "message": "建议复核出差天数和业务必要性。",
                    "recommended_upper": _format_decimal(suggested_days),
                    "unit": "天",
                }
            )

    unit_amount_upper = _resolve_entertainment_unit_upper(
        level=level,
        policy_limit=policy_limit,
        peer_unit_amount_p75=peer_unit_amount_p75,
    )
    if unit_amount_upper is not None:
        suggestions.append(
            {
                "type": "review_entertainment_unit_amount",
                "severity": _severity_from_level(level),
                "message": "建议复核业务招待人均金额和客户招待必要性。",
                "recommended_upper": _format_decimal(unit_amount_upper),
                "unit": "元/人",
            }
        )

    if expense_profile_score and _to_decimal(expense_profile_score) >= Decimal("60"):
        suggestions.append(
            {
                "type": "review_expense_pattern",
                "severity": _severity_from_level(level),
                "message": "申请人近期费用节奏高于同组基准，建议核对费用标准和预算占用。",
            }
        )

    if process_quality_score and _to_decimal(process_quality_score) >= Decimal("60"):
        suggestions.append(
            {
                "type": "review_material_quality",
                "severity": "medium",
                "message": "申请人近期材料质量波动较高，建议重点核对附件、事由和票据一致性。",
            }
        )

    return suggestions


def level_from_score(score: Any) -> 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 percentile(values: list[Any], percent: Any) -> Decimal:
    normalized_values = sorted(_to_decimal(item) for item in values if _to_decimal(item) >= ZERO)
    if not normalized_values:
        return ZERO
    if len(normalized_values) == 1:
        return normalized_values[0]

    pct = max(ZERO, min(HUNDRED, _to_decimal(percent)))
    position = (Decimal(len(normalized_values) - 1) * pct) / HUNDRED
    lower_index = int(position.to_integral_value(rounding=ROUND_FLOOR))
    upper_index = int(position.to_integral_value(rounding=ROUND_CEILING))
    if lower_index == upper_index:
        return normalized_values[lower_index]

    fraction = position - Decimal(lower_index)
    return (
        normalized_values[lower_index]
        + (normalized_values[upper_index] - normalized_values[lower_index]) * fraction
    )


def _resolve_entertainment_unit_upper(
    *,
    level: str,
    policy_limit: Any,
    peer_unit_amount_p75: Any,
) -> Decimal | None:
    policy = _to_decimal(policy_limit)
    peer = _to_decimal(peer_unit_amount_p75)
    candidates = [item for item in (policy, peer * _level_factor(level)) if item > ZERO]
    if not candidates:
        return None
    return min(candidates)


def _level_factor(level: str) -> Decimal:
    if level == LEVEL_ESCALATION:
        return Decimal("0.90")
    if level == LEVEL_REVIEW:
        return Decimal("1.00")
    if level == LEVEL_WATCH:
        return Decimal("1.10")
    return Decimal("1.20")


def _severity_from_level(level: str) -> str:
    if level == LEVEL_ESCALATION:
        return "high"
    if level == LEVEL_REVIEW:
        return "medium"
    return "low"


def _clamp_score(value: Any) -> int:
    try:
        normalized = _to_decimal(value)
    except InvalidOperation:
        return 0
    bounded = max(ZERO, min(HUNDRED, normalized))
    return int(bounded.quantize(Decimal("1"), rounding=ROUND_HALF_UP))


def _to_decimal(value: Any) -> Decimal:
    if value is None:
        return ZERO
    if isinstance(value, Decimal):
        return value
    if isinstance(value, bool):
        return ONE if value else ZERO
    try:
        return Decimal(str(value).strip() or "0")
    except (InvalidOperation, ValueError):
        return ZERO


def _format_decimal(value: Any) -> str | None:
    if value is None:
        return None
    decimal_value = _to_decimal(value)
    return str(decimal_value.quantize(Decimal("0.0001")).normalize())


def _format_value(value: Any) -> Any:
    if isinstance(value, Decimal):
        return _format_decimal(value)
    if isinstance(value, dict):
        return {key: _format_value(item) for key, item in value.items()}
    if isinstance(value, list):
        return [_format_value(item) for item in value]
    return value


def _json_safe(value: Any) -> Any:
    return _format_value(value)
