from typing import Any, Optional

from vicentin.kernels import Kernel
from vicentin.utils import Dispatcher

smo_dispatcher = Dispatcher()

try:
    from .smo_np import SMO as SMO_np

    smo_dispatcher.register("numpy", SMO_np)
except ImportError:
    pass

try:
    from .smo_torch import SMO as SMO_torch

    smo_dispatcher.register("torch", SMO_torch)
except ImportError:
    pass


def SMO(
    X: Any,
    y: Any,
    kernel: Kernel,
    C: float,
    tol: float = 1e-3,
    max_passes: int = 5,
    is_l2: bool = False,
    backend: Optional[str] = None,
) -> tuple[Any, float]:
    """
    Solves the SVM dual problem using Sequential Minimal Optimization (SMO).

    Parameters:
    -----------
    X : Any
        Training features of shape (n, d).
    y : Any
        Training labels of shape (n,) in \\{-1, 1\\}.
    kernel : Kernel
        Kernel object for inner product evaluation.
    C : float
        Regularization parameter (box constraint limit).
    tol : float, optional (default=1e-3)
        Numerical tolerance for KKT condition violations.
    max_passes : int, optional (default=5)
        Number of iterations over the dataset without alpha changes before terminating.
    backend : str, optional (default=None)
        Explicitly specify the backend ('numpy', 'torch').
    is_l2 : bool, optional (default=False)
        If True, alters the objective to solve the L2-SVM dual problem by removing
        the upper bound and shifting the kernel diagonal.

    Returns:
    --------
    weights : Any
        The optimal dual weights formatted as $\\alpha_i y_i$. This format
        is intentionally returned (instead of raw $\\alpha$ values) to match
        the exact QP solver's output and feed directly into the `predict` function.
    b : float
        The bias (intercept) term.

    Notes:
    ------
    For L2-SVM (`is_l2=True`), the algorithm performs lazy evaluation of the
    diagonal kernel shift ($K_{ii} + \\frac{1}{2C}$) to avoid instantiating
    a dense $N \\times N$ matrix, preventing Out-Of-Memory (OOM) errors.
    """

    ctx = smo_dispatcher.detect_backend(X, backend)
    X, y = smo_dispatcher.cast_values(ctx, X, y)

    return smo_dispatcher(ctx, X, y, kernel, C, tol, max_passes, is_l2)
