import functools
import threading
import requests
from .utils import generate_prediction_id, utc_timestamp


class DataNeurusMonitor:
    """
    Client for sending prediction telemetry to the DataNeurus drift monitoring backend.

    Usage::

        monitor = DataNeurusMonitor(
            api_url="http://localhost:8000/api/telemetry",
            api_key="dnm_...",
            model_id="my-fraud-model",
        )

        # As a decorator on a Flask route:
        @app.route("/predict", methods=["POST"])
        @monitor.monitor_prediction
        def predict():
            features = request.get_json()
            prediction = model.predict(features)
            return jsonify({"prediction": prediction})

        # Or call manually:
        monitor.log_prediction(features={"age": 32}, prediction=0.87)
    """

    def __init__(
        self,
        api_url: str,
        api_key: str,
        model_id: str,
        model_version_id: str = None,
        timeout: int = 5,
        async_send: bool = True,
    ):
        self.api_url = api_url.rstrip("/")
        self.api_key = api_key
        self.model_id = model_id
        self.model_version_id = model_version_id
        self.timeout = timeout
        self.async_send = async_send

    # ------------------------------------------------------------------
    # Core logging methods
    # ------------------------------------------------------------------

    def log_prediction(
        self,
        features: dict,
        prediction,
        prediction_id: str = None,
        timestamp: str = None,
        metadata: dict = None,
    ):
        """Log a single prediction synchronously. Raises on HTTP error."""
        payload = {
            "prediction_id": prediction_id or generate_prediction_id(),
            "model_id": self.model_id,
            "timestamp": timestamp or utc_timestamp(),
            "features": features if isinstance(features, dict) else {},
            "prediction": prediction,
            "metadata": metadata or {},
        }
        if self.model_version_id:
            payload["model_version_id"] = self.model_version_id

        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
        }
        url = f"{self.api_url}/predictions"
        resp = requests.post(url, json=payload, headers=headers, timeout=self.timeout)
        if resp.status_code >= 400:
            raise RuntimeError(f"Telemetry ingestion failed: {resp.status_code} {resp.text}")
        return resp.json()

    def log_ground_truth(
        self,
        entity_id: str,
        actual_value,
        timestamp: str = None,
        prediction_log_id: str = None,
    ):
        """Log a ground truth label for a previously logged prediction."""
        payload = {
            "records": [
                {
                    "entity_id": entity_id,
                    "actual_value": actual_value,
                    "timestamp": timestamp or utc_timestamp(),
                    "prediction_log_id": prediction_log_id,
                }
            ]
        }
        # Ground truth goes to the models router, not telemetry
        base = self.api_url.replace("/api/telemetry", "/api")
        url = f"{base}/models/{self.model_id}/ground-truth"
        params = {}
        if self.model_version_id:
            params["model_version_id"] = self.model_version_id

        headers = {"Authorization": f"Bearer {self.api_key}"}
        resp = requests.post(url, json=payload, params=params, headers=headers, timeout=self.timeout)
        if resp.status_code >= 400:
            raise RuntimeError(f"Ground truth ingestion failed: {resp.status_code} {resp.text}")
        return resp.json()

    # ------------------------------------------------------------------
    # Decorator
    # ------------------------------------------------------------------

    def monitor_prediction(self, func):
        """
        Decorator that automatically logs the prediction after each call.

        Works with Flask routes (reads flask.request.get_json() for features)
        and plain Python functions (uses the first positional argument).

        Errors are swallowed so monitoring never breaks your app.
        """
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # --- capture features ---
            features = {}
            try:
                from flask import request as _flask_req
                features = _flask_req.get_json(silent=True) or {}
            except (ImportError, RuntimeError):
                if args and isinstance(args[0], dict):
                    features = args[0]

            result = func(*args, **kwargs)

            # --- extract prediction value ---
            prediction = None
            try:
                if hasattr(result, "get_json"):
                    # Flask Response object
                    data = result.get_json(force=True)
                    prediction = data.get("prediction", data) if isinstance(data, dict) else data
                elif isinstance(result, dict):
                    prediction = result.get("prediction", result)
                else:
                    prediction = result
            except Exception:
                prediction = str(result)

            # --- send telemetry (never block the response) ---
            def _send():
                try:
                    self.log_prediction(features=features, prediction=prediction)
                except Exception:
                    pass  # monitoring must never crash the app

            if self.async_send:
                threading.Thread(target=_send, daemon=True).start()
            else:
                _send()

            return result

        return wrapper
