"""Lightweight learned ranking models for candidate prioritization."""
from __future__ import annotations

import json
from functools import lru_cache
from pathlib import Path
from typing import Any


def resolve_ranking_model_path(model_path: str) -> Path:
    path = Path(model_path)
    if path.is_absolute():
        return path
    return Path.cwd() / path


@lru_cache(maxsize=16)
def load_ranking_model(model_path: str) -> dict[str, Any]:
    path = resolve_ranking_model_path(model_path)
    return json.loads(path.read_text())


def compute_ranking_model_score(
    row: dict[str, Any],
    model_path: str | None,
) -> float | None:
    if not model_path:
        return None
    model = load_ranking_model(model_path)
    model_type = str(model.get("model_type", "")).lower()
    if model_type != "bucket_blend_v1":
        return None

    features = model.get("features", [])
    total_weight = 0.0
    weighted_sum = 0.0
    for feature in features:
        weight = float(feature.get("weight", 0.0))
        if weight <= 0:
            continue
        key = _resolve_feature_key(str(feature.get("name", "")), row)
        if key is None:
            continue
        value = feature.get("values", {}).get(key)
        if value is None:
            continue
        weighted_sum += weight * float(value)
        total_weight += weight

    if total_weight <= 0:
        fallback = model.get("global_mean")
        return float(fallback) if fallback is not None else None
    return weighted_sum / total_weight


def _resolve_feature_key(name: str, row: dict[str, Any]) -> str | None:
    if name == "event_type":
        return _safe_text(row.get("event_type"))
    if name == "direction_guidance_combo":
        direction = _safe_text(row.get("event_direction"))
        guidance = _safe_text(row.get("guidance_status"))
        if direction is None or guidance is None:
            return None
        return f"{direction}|{guidance}"
    if name == "reaction_bucket":
        return _bucketize(
            _safe_float(row.get("reaction_day_return")),
            [0.03, 0.05, 0.08, 0.12, 0.18, 0.25],
        )
    if name == "close_bucket":
        return _bucketize(
            _safe_float(row.get("close_location")),
            [0.45, 0.55, 0.65, 0.70, 0.75, 0.83],
        )
    if name == "volume_bucket":
        return _bucketize(
            _safe_float(row.get("volume_ratio_20d")),
            [1.0, 1.5, 2.0, 3.0, 4.0, 6.0],
        )
    if name == "gap_bucket":
        return _bucketize(
            _safe_float(row.get("gap_size")),
            [0.0, 0.02, 0.05, 0.08, 0.15],
        )
    if name == "document_bucket":
        return _bucketize(
            _safe_float(row.get("document_quality_score")),
            [0.60, 0.66, 0.70, 0.75, 0.80, 0.85],
        )
    if name == "confidence_bucket":
        return _bucketize(
            _safe_float(row.get("parse_confidence_overall")),
            [0.60, 0.64, 0.70, 0.75, 0.80, 0.90],
        )
    return None


def _bucketize(value: float | None, edges: list[float]) -> str | None:
    if value is None or not edges:
        return None
    if value < edges[0]:
        return f"lt:{edges[0]:.4f}"
    for low, high in zip(edges, edges[1:]):
        if low <= value < high:
            return f"{low:.4f}:{high:.4f}"
    return f"ge:{edges[-1]:.4f}"


def _safe_float(raw: Any) -> float | None:
    try:
        return float(raw)
    except (TypeError, ValueError):
        return None


def _safe_text(raw: Any) -> str | None:
    if raw is None:
        return None
    text = str(raw).strip().lower()
    return text or None
