import json
import pickle
import hashlib
from dataclasses import dataclass, asdict
from typing import Any, Dict, List
import numpy as np


@dataclass
class ModelMetadata:
    model_name: str
    version: str
    training_data_hash: str
    hyperparameters: Dict[str, Any]
    eval_metrics: Dict[str, float]


class ModelRegistry:
    """A minimal, file-backed model registry sufficient to
    demonstrate the versioning/lineage concepts of section 41.1
    without any external database dependency."""

    def __init__(self):
        self.models: Dict[str, Dict[str, Any]] = {}

    def register(self, model, metadata: ModelMetadata):
        key = f"{metadata.model_name}:{metadata.version}"
        self.models[key] = {"model": model, "metadata": metadata}
        return key

    def get(self, model_name: str, version: str):
        key = f"{model_name}:{version}"
        if key not in self.models:
            raise KeyError(f"No such model/version: {key}")
        return self.models[key]

    def list_versions(self, model_name: str) -> List[str]:
        return [
            k.split(":")[1] for k in self.models
            if k.startswith(f"{model_name}:")
        ]


def hash_dataset(X: np.ndarray, y: np.ndarray) -> str:
    combined = np.concatenate([X.flatten(), y.flatten()])
    return hashlib.sha256(combined.tobytes()).hexdigest()[:16]


class PredictionService:
    """A minimal in-process stand-in for a real REST API layer,
    isolating the request-handling logic (validation, monitoring,
    response formatting) from any specific web framework."""

    def __init__(self, model, expected_n_features: int):
        self.model = model
        self.expected_n_features = expected_n_features
        self.request_count = 0
        self.error_count = 0
        self.prediction_log: List[Dict[str, Any]] = []

    def handle_request(self, payload: Dict[str, Any]) -> Dict[str, Any]:
        self.request_count += 1
        try:
            features = np.array(payload["features"], dtype=float)
            if features.shape[-1] != self.expected_n_features:
                raise ValueError(
                    f"Expected {self.expected_n_features} features, "
                    f"got {features.shape[-1]}"
                )
            features = features.reshape(1, -1)
            prediction = self.model.predict(features)[0]
            record = {
                "features": payload["features"],
                "prediction": float(prediction),
            }
            self.prediction_log.append(record)
            return {"status": "ok", "prediction": float(prediction)}
        except Exception as e:
            self.error_count += 1
            return {"status": "error", "message": str(e)}

    def health_report(self) -> Dict[str, Any]:
        error_rate = (
            self.error_count / self.request_count
            if self.request_count > 0 else 0.0
        )
        return {
            "request_count": self.request_count,
            "error_count": self.error_count,
            "error_rate": error_rate,
        }


def detect_feature_drift(
    training_feature: np.ndarray, live_feature: np.ndarray, n_bins: int = 10
) -> float:
    """Population Stability Index: a standard, simple drift metric
    comparing two distributions via a binned histogram comparison."""
    bin_edges = np.histogram_bin_edges(training_feature, bins=n_bins)
    train_counts, _ = np.histogram(training_feature, bins=bin_edges)
    live_counts, _ = np.histogram(live_feature, bins=bin_edges)

    train_pct = np.clip(train_counts / train_counts.sum(), 1e-6, 1)
    live_pct = np.clip(live_counts / live_counts.sum(), 1e-6, 1)

    psi = np.sum((live_pct - train_pct) * np.log(live_pct / train_pct))
    return float(psi)
