"""Marketplace device reputation scoring (Phase 5, issue 290).

Two layers:

1. **Simple helpers** (``compute_reputation_score``, ``reputation_tier``, etc.) —
   lightweight, synchronous, used by both Python and the frontend nodes endpoint
   for quick scoring when probe data is unavailable.

2. **ReputationEngine** — async class that pulls uptime, latency, and success-rate
   history from :class:`~llmhosts.marketplace.probes.ProbeStore` to produce a
   weighted score:

   * uptime_pct  (30-day success rate)     — weight 40%
   * avg_latency (lower is better, 0-100)  — weight 25%
   * success_rate (7-day success %)         — weight 25%
   * consecutive_successes bonus (streak)   — weight 10%

   Tier mapping: Unverified (<50), Standard (50-79), Verified (80-94), Elite (95-100).

Staleness: last_seen older than STALE_SECONDS is treated as not recent.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from typing import Any

logger = logging.getLogger(__name__)

STALE_SECONDS = 5 * 60  # 5 minutes

# ---------------------------------------------------------------------------
# Tier constants
# ---------------------------------------------------------------------------
REPUTATION_TIER_UNVERIFIED = "Unverified"
REPUTATION_TIER_STANDARD = "Standard"
REPUTATION_TIER_VERIFIED = "Verified"
REPUTATION_TIER_ELITE = "Elite"

# Minimum score thresholds per tier (for tier-based filtering).
TIER_MIN_SCORES: dict[str, int] = {
    REPUTATION_TIER_UNVERIFIED: 0,
    REPUTATION_TIER_STANDARD: 50,
    REPUTATION_TIER_VERIFIED: 80,
    REPUTATION_TIER_ELITE: 95,
}


# ---------------------------------------------------------------------------
# Simple helpers (synchronous, no probe data)
# ---------------------------------------------------------------------------


def compute_reputation_score(
    status: str | None,
    latency_ms: int | float | None,
    last_seen: datetime | str | None,
) -> int:
    """Compute 0-100 reputation score from device status, latency, and last_seen.

    Online + recent + low latency => up to 100; offline or stale => no online bonus.
    """
    now = datetime.now(timezone.utc)
    if last_seen is None:
        recent = False
    elif isinstance(last_seen, str):
        try:
            dt = datetime.fromisoformat(last_seen.replace("Z", "+00:00"))
            if dt.tzinfo is None:
                dt = dt.replace(tzinfo=timezone.utc)
            recent = (now - dt).total_seconds() <= STALE_SECONDS
        except (ValueError, TypeError):
            recent = False
    else:
        if last_seen.tzinfo is None:
            last_seen = last_seen.replace(tzinfo=timezone.utc)
        recent = (now - last_seen).total_seconds() <= STALE_SECONDS

    online = (status or "").strip().lower() == "online"
    online_bonus = 25 if (online and recent) else 0

    latency_bonus = 0
    if latency_ms is not None:
        try:
            lat = float(latency_ms)
            if lat < 100:
                latency_bonus = 25
            elif lat < 500:
                latency_bonus = 10
        except (TypeError, ValueError):
            pass

    score = 50 + online_bonus + latency_bonus
    return max(0, min(100, round(score)))


def reputation_score_from_node(node: dict[str, Any]) -> int:
    """Compute reputation from a catalog node dict (status, latency_ms, last_seen)."""
    return compute_reputation_score(
        status=node.get("status"),
        latency_ms=node.get("latency_ms"),
        last_seen=node.get("last_seen"),
    )


def reputation_tier(score: int | float) -> str:
    """Map 0-100 score to tier: Unverified (<50), Standard (50-79), Verified (80-94), Elite (95-100)."""
    s = round(score)
    if s < 50:
        return REPUTATION_TIER_UNVERIFIED
    if s < 80:
        return REPUTATION_TIER_STANDARD
    if s < 95:
        return REPUTATION_TIER_VERIFIED
    return REPUTATION_TIER_ELITE


def reputation_score_with_uptime(
    status: str | None,
    latency_ms: int | float | None,
    last_seen: datetime | str | None,
    uptime_pct: float | None,
) -> int:
    """Compute 0-100 score; when uptime_pct is available, blend with compute_reputation_score.

    Formula: 0.6 * base_score + 0.4 * uptime_pct (clamped 0-100). When uptime_pct
    is None, returns same as compute_reputation_score.
    """
    base = compute_reputation_score(status, latency_ms, last_seen)
    if uptime_pct is None:
        return base
    blended = 0.6 * base + 0.4 * uptime_pct
    return max(0, min(100, round(blended)))


def min_score_for_tier(tier: str) -> int:
    """Return the minimum score required to reach *tier*. Defaults to 0 for unknown tiers."""
    return TIER_MIN_SCORES.get(tier, 0)


# ---------------------------------------------------------------------------
# ReputationEngine (async, uses ProbeStore)
# ---------------------------------------------------------------------------

# Weight constants for the engine score formula
_W_UPTIME = 0.40
_W_LATENCY = 0.25
_W_SUCCESS = 0.25
_W_STREAK = 0.10

# Latency normalisation: 0 ms => 100 score, >= _MAX_LATENCY_MS => 0 score
_MAX_LATENCY_MS = 5000.0


@dataclass
class DeviceScore:
    """Computed reputation for a single device."""

    device_id: str
    score: int  # 0-100
    tier: str
    uptime_pct: float  # 0-100, from probe data
    avg_latency_ms: float  # average latency from probes
    success_rate: float  # 0-100, 7-day success rate
    consecutive_successes: int  # current streak


@dataclass
class ReputationEngine:
    """Async engine that computes reputation scores from ProbeStore history.

    Usage::

        from llmhosts.marketplace.probes import ProbeStore

        store = ProbeStore("/tmp/probes.db")
        await store.initialize()
        engine = ReputationEngine(probe_store=store)
        score = await engine.compute_score("device-1")
        scores = await engine.get_scores(["device-1", "device-2"])
    """

    probe_store: Any  # ProbeStore instance (duck-typed to avoid circular import)
    uptime_days: int = field(default=30)
    success_rate_days: int = field(default=7)

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    async def compute_score(self, device_id: str) -> DeviceScore:
        """Compute the full weighted reputation score for one device.

        Components (weights sum to 1.0):
          * uptime_pct  (30-day)       — 40%
          * avg_latency (normalised)   — 25%
          * success_rate (7-day)       — 25%
          * streak bonus               — 10%
        """
        uptime_pct = await self._get_uptime_pct(device_id)
        avg_latency = await self._get_avg_latency(device_id)
        success_rate = await self._get_success_rate(device_id)
        streak = await self._get_consecutive_successes(device_id)

        uptime_component = uptime_pct * _W_UPTIME

        # Latency: only contribute if we have successful probe data.
        # No data (None) => 0 contribution (not rewarded for missing data).
        if avg_latency is not None:
            latency_score = max(0.0, min(100.0, 100.0 * (1.0 - avg_latency / _MAX_LATENCY_MS)))
        else:
            latency_score = 0.0
        latency_component = latency_score * _W_LATENCY

        success_component = success_rate * _W_SUCCESS
        # Streak bonus: 100 consecutive => full 10 points; capped at 100
        streak_score = min(100.0, streak * 1.0)
        streak_component = streak_score * _W_STREAK

        raw = uptime_component + latency_component + success_component + streak_component
        score = max(0, min(100, round(raw)))
        tier = reputation_tier(score)

        return DeviceScore(
            device_id=device_id,
            score=score,
            tier=tier,
            uptime_pct=round(uptime_pct, 2),
            avg_latency_ms=round(avg_latency, 2) if avg_latency is not None else 0.0,
            success_rate=round(success_rate, 2),
            consecutive_successes=streak,
        )

    @staticmethod
    def get_tier(score: int | float) -> str:
        """Map 0-100 score to tier name. Convenience wrapper around :func:`reputation_tier`."""
        return reputation_tier(score)

    async def get_scores(self, device_ids: list[str]) -> dict[str, DeviceScore]:
        """Batch-compute reputation scores for multiple devices."""
        results: dict[str, DeviceScore] = {}
        for did in device_ids:
            results[did] = await self.compute_score(did)
        return results

    # ------------------------------------------------------------------
    # Internal data accessors (query ProbeStore)
    # ------------------------------------------------------------------

    async def _get_uptime_pct(self, device_id: str) -> float:
        """Return 30-day uptime percentage from probe data (0-100). Defaults to 0 if no data."""
        pct = await self.probe_store.get_uptime_pct(device_id, days=self.uptime_days)
        return pct if pct is not None else 0.0

    async def _get_avg_latency(self, device_id: str) -> float | None:
        """Return average latency_ms for successful probes over the uptime window. None if no data."""
        db = self.probe_store._db
        if db is None:
            return None
        since = (datetime.now(timezone.utc) - timedelta(days=self.uptime_days)).isoformat()
        sql = """
        SELECT AVG(latency_ms) FROM probe_results
        WHERE device_id = ? AND timestamp >= ? AND success = 1
        """
        async with db.execute(sql, (device_id, since)) as cursor:
            row = await cursor.fetchone()
        if row is None or row[0] is None:
            return None
        return float(row[0])

    async def _get_success_rate(self, device_id: str) -> float:
        """Return 7-day success rate (0-100). Defaults to 0 if no data."""
        pct = await self.probe_store.get_uptime_pct(device_id, days=self.success_rate_days)
        return pct if pct is not None else 0.0

    async def _get_consecutive_successes(self, device_id: str) -> int:
        """Return the number of consecutive successful probes (most recent first). 0 if none."""
        db = self.probe_store._db
        if db is None:
            return 0
        sql = """
        SELECT success FROM probe_results
        WHERE device_id = ?
        ORDER BY timestamp DESC
        LIMIT 200
        """
        streak = 0
        async with db.execute(sql, (device_id,)) as cursor:
            async for row in cursor:
                if row[0] == 1:
                    streak += 1
                else:
                    break
        return streak
