import hashlib
import json
import math
from pathlib import Path
from typing import Any, Mapping


def require(d: Mapping[str, Any], key: str, ctx: str) -> Any:
    if key not in d:
        raise KeyError(f"Missing required key '{key}' at {ctx}")
    return d[key]


def parse_mu_disp_mode(ethics_cfg: Mapping[str, Any]) -> str:
    mode = str(ethics_cfg.get("mu_disp_mode", "mu_hat")).lower()
    if mode not in ("mu_hat", "risk"):
        raise ValueError(f"ethics.mu_disp_mode must be one of: mu_hat, risk (got {mode})")
    return mode


def require_unit_interval_closed_open(x: float, name: str) -> float:
    xx = float(x)
    if xx < 0.0 or xx >= 1.0:
        raise ValueError(f"{name} must be in [0, 1)")
    return xx


def require_unit_interval_open_closed(x: float, name: str) -> float:
    xx = float(x)
    if xx <= 0.0 or xx > 1.0:
        raise ValueError(f"{name} must be in (0, 1]")
    return xx


def validate_unit_interval(x: float, name: str, closed_open: bool = True) -> float:
    if closed_open:
        return require_unit_interval_closed_open(x, name)
    else:
        return require_unit_interval_open_closed(x, name)


def clamp(x: float, lo: float, hi: float) -> float:
    if x < lo:
        return lo
    if x > hi:
        return hi
    return x


def sigmoid(x: float) -> float:
    if x >= 0:
        z = math.exp(-x)
        return 1.0 / (1.0 + z)
    z = math.exp(x)
    return z / (1.0 + z)


def smoothstep01(x: float) -> float:
    x = clamp(x, 0.0, 1.0)
    return x * x * (3.0 - 2.0 * x)


def map_risk_to_target_u(risk: float, pi_cfg: Mapping[str, Any]) -> float:
    typ = str(require(pi_cfg, "type", "controller.pi"))
    if typ == "smoothstep":
        lo = float(require(pi_cfg, "lo", "controller.pi"))
        hi = float(require(pi_cfg, "hi", "controller.pi"))
        if hi <= lo:
            raise ValueError("controller.pi.hi must be > controller.pi.lo")
        return smoothstep01((risk - lo) / (hi - lo))
    if typ == "logistic":
        k = float(require(pi_cfg, "k", "controller.pi"))
        x0 = float(require(pi_cfg, "x0", "controller.pi"))
        return sigmoid(k * (risk - x0))
    raise ValueError(f"Unsupported controller.pi.type: {typ}")


def canonical_json(obj: Any) -> str:
    return json.dumps(obj, sort_keys=True, ensure_ascii=False, separators=(",", ":"))


def sha256_hex(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8")).hexdigest()


def resolve_path(base_file: Path, maybe_rel: str) -> Path:
    p = Path(maybe_rel)
    if p.is_absolute():
        return p
    return (base_file.parent / p).resolve()
