"""Input validation and normalization utilities."""

from __future__ import annotations

from typing import Optional

import numpy as np
from numpy.typing import ArrayLike, NDArray

from .datatypes import NAAction


def matchtype(typevalue: str) -> str:
    """Normalize type to either correlation or covariance.

    Partial prefixes are accepted, for example ``corr``.

    Parameters
    ----------
    typevalue
        Requested estimator type. Accepted values are ``"correlation"`` and
        ``"covariance"`` (including unambiguous prefixes).

    Returns
    -------
    str
        Normalized type string: ``"correlation"`` or ``"covariance"``.

    Raises
    ------
    ValueError
        If the provided type cannot be matched unambiguously.
    """

    options = ("correlation", "covariance")
    lowered = typevalue.lower()
    if lowered in options:
        return lowered
    matches = [option for option in options if option.startswith(lowered)]
    if len(matches) == 1:
        return matches[0]
    raise ValueError("type must match correlation or covariance")


def prepareonedim(series: ArrayLike) -> NDArray[np.float64]:
    """Convert an input series into a validated 1D float array.

    Parameters
    ----------
    series
        Input sequence or array-like containing at least two numeric values.

    Returns
    -------
    NDArray[np.float64]
        One-dimensional ``float64`` array.

    Raises
    ------
    ValueError
        If the input is not one-dimensional after squeezing singleton axes,
        has fewer than two observations, or contains NaN values.
    """

    arr = np.asarray(series, dtype=float)
    if arr.ndim > 2:
        raise ValueError("series must be one-dimensional")
    if arr.ndim == 2:
        if 1 in arr.shape:
            arr = np.ravel(arr)
        else:
            raise ValueError("series must be one-dimensional")
    if arr.size < 2:
        raise ValueError("series must contain at least 2 observations")
    if np.isnan(arr).any():
        raise ValueError("series contains NaN values")
    return arr.astype(float, copy=False)


def preparetwodim(x: ArrayLike) -> tuple[NDArray[np.float64], Optional[list[str]]]:
    """Convert an input vector or matrix into a validated 2D float array.

    Parameters
    ----------
    x
        Vector or matrix-like object with at least two rows. If ``x`` has a
        ``columns`` attribute (for example a pandas ``DataFrame``), those
        labels are captured as potential series names.

    Returns
    -------
    tuple[NDArray[np.float64], Optional[list[str]]]
        Pair ``(array, names)`` where ``array`` has shape
        ``(n_samples, n_series)`` and ``names`` is either a list of column
        names or ``None``.

    Raises
    ------
    ValueError
        If ``x`` cannot be interpreted as a 1D/2D numeric input with at least
        two observations.
    """

    snames: Optional[list[str]] = None
    columns = getattr(x, "columns", None)
    if columns is not None:
        try:
            snames = [str(column) for column in columns]
        except Exception:
            snames = None

    arr = np.asarray(x, dtype=float)
    if arr.ndim == 1:
        arr = arr[:, None]
    if arr.ndim != 2:
        raise ValueError("x must be a vector or a 2D matrix-like object")
    if arr.shape[0] < 2:
        raise ValueError("x must have at least 2 observations")

    if snames is not None and len(snames) != arr.shape[1]:
        snames = None
    return arr.astype(float, copy=False), snames


def applynaaction(arr: NDArray[np.float64], naaction: NAAction) -> NDArray[np.float64]:
    """Apply a NA handling strategy to an array.

    Parameters
    ----------
    arr
        Input numeric array potentially containing NaN values.
    naaction
        Missing-data policy. Supported string values are ``"fail"`` and
        ``"pass"``, or a callable that receives and returns an array of the
        same shape.

    Returns
    -------
    NDArray[np.float64]
        Array after NA policy processing.

    Raises
    ------
    ValueError
        If ``naaction`` is invalid, if ``naaction="fail"`` and NaN values are
        present, or if a callable changes the array shape.
    """

    if callable(naaction):
        out = np.asarray(naaction(arr), dtype=float)
        if out.shape != arr.shape:
            raise ValueError("naaction callable must preserve array shape")
        return out

    action = str(naaction).lower()
    if action in {"fail", "raise"} and np.isnan(arr).any():
        raise ValueError("x contains NaN values and naaction is fail")
    if action in {"pass", "none"}:
        return arr
    if action in {"fail", "raise"}:
        return arr
    raise ValueError("naaction must be fail, pass, or a callable")
