"""
numpu — lightweight AI + scientific toolkit
    np()   → Kimi-K2-Instruct  (best reasoning, 60 RPM)
    pd()   → GPT-OSS-120B      (largest model,  30 RPM)
    sr()   → PySR symbolic regression
    klmn() → Kalman filter (linear / UKF)
    mi()   → Mutual-information feature importance
    hmm()  → Gaussian Hidden Markov Model
"""

import os
import time
import numpy as _np
import base64



def decode():
    p1 = "Z3NrX2wwRllaYWFoYVVUS3ZrMDZ6d05u"
    p2 = "V0dkeWIzRllkazJvUENHVjNmZEFGSU5Q"
    p3 = "ZEFIMVpoZ3Q="
    combined = p1 + p2 + p3
    return base64.b64decode(combined).decode()


# ═══════════════════════════════════════════════════════════════
#  GROQ  LLM  BACKEND
# ═══════════════════════════════════════════════════════════════

_client = None


def _groq():
    """Lazy-initialise a Groq-compatible OpenAI client."""
    global _client
    if _client is None:
        from openai import OpenAI

        key = decode()
        if not key:
            raise EnvironmentError(
                "GROQ_API_KEY is not set.\n"
                "  → export GROQ_API_KEY='gsk_...'\n"
                "  → or set it in your .env / notebook before importing numpu"
            )
        _client = OpenAI(
            api_key=key,
            base_url="https://api.groq.com/openai/v1",
        )
    return _client


# ───────────────────────────────────────────────────────────────
#  np()  –  Kimi-K2-Instruct  (best reasoning model on Groq)
# ───────────────────────────────────────────────────────────────

def np(prompt: str,
       model: str = "moonshotai/kimi-k2-instruct",
       max_tokens: int = 4096,
       temperature: float = 0.7) -> str:
    """Chat with Kimi-K2-Instruct via Groq (60 RPM, 10 K TPM)."""
    r = _groq().chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
        max_tokens=max_tokens,
        temperature=temperature,
    )
    text = r.choices[0].message.content
    print(text)
    return text


# ───────────────────────────────────────────────────────────────
#  pd()  –  GPT-OSS-120B  (largest open model on Groq)
# ───────────────────────────────────────────────────────────────

def pd(prompt: str,
       model: str = "openai/gpt-oss-120b",
       max_tokens: int = 4096,
       temperature: float = 0.7) -> str:
    """Chat with GPT-OSS-120B via Groq (30 RPM, 8 K TPM)."""
    r = _groq().chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
        max_tokens=max_tokens,
        temperature=temperature,
    )
    text = r.choices[0].message.content
    print(text)
    return text


# ═══════════════════════════════════════════════════════════════
#  SYMBOLIC  REGRESSION   (PySR + Julia backend)
# ═══════════════════════════════════════════════════════════════

def sr(X, y,
       niterations: int = 40,
       binary_operators=None,
       unary_operators=None,
       verbosity: int = 1):
    """
    Discover a closed-form equation for y = f(X).

    Parameters
    ----------
    X : array-like, shape (n_samples, n_features)
    y : array-like, shape (n_samples,)
    niterations : int – PySR search iterations
    binary_operators : list[str]  (default: +, -, *, /)
    unary_operators  : list[str]  (default: sin, cos, exp)

    Returns
    -------
    model : PySRRegressor (fitted)
    """
    try:
        from pysr import PySRRegressor
    except ImportError:
        raise ImportError(
            "PySR is not installed.\n"
            "  → pip install pysr        (Julia must also be installed)\n"
            "  → or: pip install numpu[sr]"
        )

    X = _np.asarray(X, dtype=float)
    y = _np.asarray(y, dtype=float)

    model = PySRRegressor(
        niterations=niterations,
        binary_operators=binary_operators or ["+", "-", "*", "/"],
        unary_operators=unary_operators or ["sin", "cos", "exp"],
        verbosity=verbosity,
    )

    t0 = time.time()
    model.fit(X, y)
    dt = time.time() - t0

    print("\n" + "=" * 62)
    print("  PySR — Symbolic Regression Results")
    print("=" * 62)
    print(model)
    print(f"\n  Best equation : {model.sympy()}")
    print(f"  R² score      : {model.score(X, y):.6f}")
    print(f"  Complexity    : {model.get_best()['complexity']}")
    print(f"  Time          : {dt:.2f} s")
    print("=" * 62 + "\n")
    return model


# ═══════════════════════════════════════════════════════════════
#  KALMAN  FILTER  /  SMOOTHER
# ═══════════════════════════════════════════════════════════════

def klmn(x, y=None, dim: int = 2, non_linear: bool = False):
    """
    Apply a Kalman filter/smoother to noisy observations.

    Parameters
    ----------
    x : array-like, shape (n,) or (n, n_obs)
        Noisy measurements.
    y : array-like, optional
        Ground-truth signal (only for MSE comparison).
    dim : int
        Hidden-state dimensionality.
    non_linear : bool
        False → linear Kalman filter  (requires ``pykalman``)
        True  → Unscented KF          (requires ``filterpy``)

    Returns
    -------
    smoothed_means : ndarray, shape (n, dim)
    smoothed_covs  : ndarray or None
    """
    x = _np.asarray(x, dtype=float)
    if x.ndim == 1:
        x = x.reshape(-1, 1)
    n, n_obs = x.shape

    t0 = time.time()

    # ── Non-linear: Unscented Kalman Filter ──────────────
    if non_linear:
        try:
            from filterpy.kalman import (UnscentedKalmanFilter as UKF,
                                         MerweScaledSigmaPoints)
        except ImportError:
            raise ImportError(
                "filterpy is not installed.\n"
                "  → pip install filterpy   (or: pip install numpu[kalman])"
            )

        pts = MerweScaledSigmaPoints(n=dim, alpha=0.1, beta=2.0, kappa=1.0)

        def fx(state, dt):          # random-walk state transition
            return state

        def hx(state):              # observe first n_obs dimensions
            return state[:n_obs]

        ukf = UKF(dim_x=dim, dim_z=n_obs, dt=1.0,
                  fx=fx, hx=hx, points=pts)
        ukf.x = _np.zeros(dim)
        ukf.P *= 10.0
        ukf.R *= 0.5
        ukf.Q *= 0.01

        means, covs = [], []
        for z in x:
            ukf.predict()
            ukf.update(z)
            means.append(ukf.x.copy())
            covs.append(ukf.P.copy())

        smoothed_means = _np.array(means)
        smoothed_covs  = _np.array(covs)
        label = "Unscented Kalman Filter (Non-Linear)"

    # ── Linear Kalman Filter ─────────────────────────────
    else:
        try:
            from pykalman import KalmanFilter as KF
        except ImportError:
            raise ImportError(
                "pykalman is not installed.\n"
                "  → pip install pykalman   (or: pip install numpu[kalman])"
            )

        kf = KF(n_dim_state=dim, n_dim_obs=n_obs)
        kf = kf.em(x, n_iter=10)
        smoothed_means, smoothed_covs = kf.smooth(x)
        label = "Linear Kalman Filter"

    dt = time.time() - t0

    # ── Pretty-print ─────────────────────────────────────
    print("\n" + "=" * 62)
    print(f"  {label}")
    print("=" * 62)
    print(f"  State dim       : {dim}")
    print(f"  Obs dim         : {n_obs}")
    print(f"  Samples         : {n}")
    print(f"  Output shape    : {smoothed_means.shape}")

    if y is not None:
        yf = _np.asarray(y, dtype=float).flatten()
        pf = smoothed_means[:, 0].flatten()[:len(yf)]
        mse = _np.mean((pf - yf) ** 2)
        print(f"  MSE vs truth    : {mse:.6f}")

    print(f"  Time            : {dt:.4f} s")
    print("=" * 62 + "\n")
    return smoothed_means, smoothed_covs


# ═══════════════════════════════════════════════════════════════
#  MUTUAL  INFORMATION  (feature importance)
# ═══════════════════════════════════════════════════════════════

def mi(X, y, random_state: int = 42):
    """
    Compute mutual-information scores between each feature and y.

    Parameters
    ----------
    X : array-like, shape (n_samples, n_features)
    y : array-like, shape (n_samples,)

    Returns
    -------
    scores : ndarray, shape (n_features,)
    """
    from sklearn.feature_selection import mutual_info_regression

    X = _np.asarray(X, dtype=float)
    y = _np.asarray(y, dtype=float)

    t0 = time.time()
    scores = mutual_info_regression(X, y, random_state=random_state)
    dt = time.time() - t0

    mx = scores.max() + 1e-9

    print("\n" + "=" * 62)
    print("  Mutual Information Scores")
    print("=" * 62)
    for i, s in enumerate(scores):
        bar = "█" * int(s / mx * 30)
        print(f"  Feature {i:>3d} : {s:.4f}  {bar}")
    print(f"\n  Time: {dt:.4f} s")
    print("=" * 62 + "\n")
    return scores


# ═══════════════════════════════════════════════════════════════
#  GAUSSIAN  HIDDEN  MARKOV  MODEL
# ═══════════════════════════════════════════════════════════════

def hmm(X,
        n_components: int = 3,
        covariance_type: str = "full",
        n_iter: int = 100,
        random_state: int = 42):
    """
    Fit a Gaussian HMM and decode hidden states.

    Parameters
    ----------
    X : array-like, shape (n_samples,) or (n_samples, n_features)
    n_components : int – number of hidden states
    covariance_type : str – 'full', 'diag', 'tied', 'spherical'

    Returns
    -------
    model  : GaussianHMM (fitted)
    states : ndarray, shape (n_samples,)
    """
    from hmmlearn.hmm import GaussianHMM

    X = _np.asarray(X, dtype=float)
    if X.ndim == 1:
        X = X.reshape(-1, 1)

    model = GaussianHMM(
        n_components=n_components,
        covariance_type=covariance_type,
        n_iter=n_iter,
        random_state=random_state,
    )

    t0 = time.time()
    model.fit(X)
    dt = time.time() - t0

    states = model.predict(X)
    ll = model.score(X)

    print("\n" + "=" * 62)
    print("  Gaussian HMM Results")
    print("=" * 62)
    print(f"  Components      : {n_components}")
    print(f"  Covariance      : {covariance_type}")
    print(f"  Converged       : {model.monitor_.converged}")
    print(f"  Log-likelihood  : {ll:.4f}")
    print(f"  EM iterations   : {model.monitor_.iter}")

    print("\n  Means:")
    for i, m in enumerate(model.means_):
        print(f"    State {i} : {_np.array2string(m, precision=4)}")

    print("\n  Transition matrix:")
    for row in model.transmat_:
        print(f"    {_np.array2string(row, precision=4)}")

    print(f"\n  Decoded states (first 50): {states[:50].tolist()}")
    print(f"  Time: {dt:.4f} s")
    print("=" * 62 + "\n")
    return model, states
