from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timezone
from math import exp
from typing import Any, Iterable, Mapping, MutableMapping, Optional


@dataclass(frozen=True)
class EvidenceItem:
    evidence_type: str
    seen_at: Optional[datetime]
    weight: Optional[float] = None


@dataclass(frozen=True)
class ContradictionRecord:
    severity: float
    detected_at: Optional[datetime] = None


def _normalize_positive(
    profile: Mapping[str, Any],
) -> MutableMapping[str, MutableMapping[str, float]]:
    positive = {}
    raw_positive = profile.get("positive")
    if raw_positive:
        for key, cfg in raw_positive.items():
            base = cfg.get("base", 0.0)
            positive[key] = {
                "base": float(base),
                "half_life_days": cfg.get("half_life_days"),
                "cap_at": cfg.get("cap_at"),
            }
    else:
        for key, value in profile.get("weights", {}).items():
            positive[key] = {
                "base": float(value),
                "half_life_days": profile.get("half_life_days", {}).get(key),
                "cap_at": profile.get("caps", {}).get(key),
            }
    return positive


def _normalize_negative(
    profile: Mapping[str, Any],
) -> MutableMapping[str, MutableMapping[str, float]]:
    negative = {}
    raw_negative = profile.get("negative")
    if raw_negative:
        for key, cfg in raw_negative.items():
            negative[key] = {
                "base": float(cfg.get("base", 0.0)),
                "half_life_days": cfg.get("half_life_days"),
            }
    else:
        for key, value in profile.get("negative_signals", {}).items():
            negative[key] = {
                "base": float(value),
                "half_life_days": profile.get("half_life_days", {}).get(key),
            }
    if "contradiction" not in negative:
        negative["contradiction"] = {"base": -0.20, "half_life_days": 365}
    return negative


def _decay(
    base: float, seen_at: Optional[datetime], half_life_days: Optional[float]
) -> float:
    if not seen_at or not half_life_days:
        return base
    now = datetime.now(timezone.utc)
    age_seconds = (now - seen_at).total_seconds()
    if age_seconds <= 0:
        return base
    age_days = age_seconds / 86400.0
    return base * (0.5 ** (age_days / half_life_days))


def _clamp(value: float, lo: float, hi: float) -> float:
    return max(lo, min(hi, value))


def score_entity(
    evidence_items: Iterable[EvidenceItem],
    contradictions: Iterable[ContradictionRecord],
    profile: Mapping[str, Any],
) -> float:
    positive_cfg = _normalize_positive(profile)
    negative_cfg = _normalize_negative(profile)
    raw_score = float(profile.get("bias", -0.6))

    for item in evidence_items:
        key = item.evidence_type
        cfg = positive_cfg.get(key) or negative_cfg.get(key)
        if not cfg:
            continue
        base = item.weight if item.weight is not None else cfg.get("base", 0.0)
        decayed = _decay(float(base), item.seen_at, cfg.get("half_life_days"))
        cap_at = cfg.get("cap_at")
        if cap_at is not None:
            decayed = min(decayed, float(cap_at))
        raw_score += decayed

    contradiction_cfg = negative_cfg.get(
        "contradiction", {"base": -0.2, "half_life_days": 365}
    )
    for contradiction in contradictions:
        base = contradiction_cfg.get("base", -0.2) * contradiction.severity
        raw_score += _decay(
            float(base),
            contradiction.detected_at,
            contradiction_cfg.get("half_life_days"),
        )

    raw_floor = float(profile.get("raw_floor", -2.0))
    raw_score = max(raw_score, raw_floor)

    sigmoid_cfg = profile.get("sigmoid", {"k": 1.4, "offset": 0.0})
    k = float(sigmoid_cfg.get("k", 1.4))
    offset = float(sigmoid_cfg.get("offset", 0.0))
    score = 1.0 / (1.0 + exp(-k * (raw_score - offset)))
    return _clamp(score, 0.0, 1.0)
