"""Internal support module.

Conceptually, this module centralizes the low-level coercion helpers used to
unwrap nested scalar payloads and normalize heterogeneous object arrays.

It exists as a separate unit so scalar handling and dict-like conversions stay
consistent across loaders, figure helpers, and analysis modules.

It connects raw saved payloads to the typed arrays and plain Python objects
expected by higher-level helpers.
"""

from __future__ import annotations

from typing import Any, Dict, List

import numpy as np


def unwrap_scalar_obj(x: Any) -> Any:
    """Unwrap nested NumPy scalar-object containers."""

    while isinstance(x, np.ndarray) and x.shape == ():
        x = x.item()
    return x


def to_1d(x: Any, dtype=None) -> np.ndarray:
    """Convert arbitrary input to a one-dimensional NumPy array.

    Parameters
    ----------
    x:
        Arbitrary scalar, array-like, or nested object payload.
    dtype:
        Optional dtype applied after flattening.

    Returns
    -------
    np.ndarray
        One-dimensional flattened array.
    """

    arr = np.asarray(unwrap_scalar_obj(x)).reshape(-1)
    if dtype is not None:
        arr = arr.astype(dtype)
    return arr


def to_1d_str(x: Any) -> np.ndarray:
    """Convert arbitrary values to a stripped string vector.

    Parameters
    ----------
    x:
        Scalar or array-like payload containing strings, bytes, or arbitrary
        scalar values.

    Returns
    -------
    np.ndarray
        One-dimensional object array of stripped strings.
    """

    arr = to_1d(x)
    out: List[str] = []
    for v in arr:
        if isinstance(v, bytes):
            s = v.decode("utf-8", errors="ignore").strip()
        else:
            s = str(v).strip()
        out.append(s)
    return np.asarray(out, dtype=object)


def normalize_raw_entries(raw_arr: np.ndarray) -> List[Dict[str, Any]]:
    """Normalize object-loaded entry payloads into a list of plain dicts.

    Parameters
    ----------
    raw_arr:
        Raw object or structured array loaded from saved processed data.

    Returns
    -------
    list[dict[str, Any]]
        Plain entry dictionaries with one element per record.
    """

    entries: List[Dict[str, Any]] = []
    for el in np.asarray(raw_arr).reshape(-1):
        el = unwrap_scalar_obj(el)

        if el is None:
            continue
        if isinstance(el, dict):
            entries.append(el)
            continue
        if isinstance(el, np.void) and el.dtype.names:
            entries.append({name: el[name] for name in el.dtype.names})
            continue
        if isinstance(el, np.ndarray) and el.dtype.names and el.size == 1:
            rec = el.reshape(-1)[0]
            entries.append({name: rec[name] for name in rec.dtype.names})
            continue

        raise TypeError(f"Unsupported psth_mat element type: {type(el)}")
    return entries


def normalize_entries(raw_arr: Any) -> List[Dict[str, Any]]:
    """Backward-compatible wrapper around `normalize_raw_entries`."""

    return normalize_raw_entries(np.asarray(raw_arr, dtype=object))


def deep_scalar_obj(x: Any) -> Any:
    """Recursively unwrap nested one-element object arrays and scalars."""

    x = unwrap_scalar_obj(x)
    if isinstance(x, np.ndarray) and x.size == 1:
        return deep_scalar_obj(x.reshape(-1)[0])
    return x


def to_session_id_str(x: Any) -> str:
    """Convert nested session-id payloads to a clean string."""

    x = deep_scalar_obj(x)
    if isinstance(x, bytes):
        return x.decode("utf-8", errors="ignore").strip()
    return str(x).strip()


def clean_str(v: Any) -> str:
    """Convert a scalar-like payload to a stripped string."""

    return str(unwrap_scalar_obj(v)).strip()


def last_numeric(v: Any) -> float:
    """Return the last finite-ready numeric value from a scalar-like payload.

    Parameters
    ----------
    v:
        Scalar or array-like payload containing numeric values.

    Returns
    -------
    float
        Last flattened numeric value, or `np.nan` when the payload is empty.
    """

    arr = np.asarray(unwrap_scalar_obj(v), dtype=float).reshape(-1)
    if arr.size == 0:
        return np.nan
    return float(arr[-1])


def to_1d_bool(x: Any) -> np.ndarray:
    """Convert an arbitrary nested payload to a one-dimensional boolean array.

    Parameters
    ----------
    x:
        Scalar or array-like payload containing booleans, numeric flags, or
        string-like truth values.

    Returns
    -------
    np.ndarray
        Boolean vector with one element per flattened input value.
    """

    arr = np.asarray(unwrap_scalar_obj(x))
    if arr.dtype == bool:
        return arr.reshape(-1)
    if np.issubdtype(arr.dtype, np.number):
        return arr.reshape(-1).astype(float) != 0
    # Accept common textual truthy values so flags loaded from object arrays
    # remain usable without per-caller normalization.
    norm = np.asarray([str(v).strip().lower() for v in arr.reshape(-1)], dtype=object)
    return np.isin(norm, ["1", "true", "t", "yes", "y"])


def to_dict_like(v: Any) -> Dict[str, Any]:
    """Convert nested record-like payloads into a plain Python dict.

    Parameters
    ----------
    v:
        Dict-like payload such as a plain dict, scalar structured array, or
        scalar object carrying named fields.

    Returns
    -------
    dict[str, Any]
        Plain dictionary with string keys.
    """

    vv = unwrap_scalar_obj(v)
    if isinstance(vv, dict):
        return vv
    if isinstance(vv, np.void) and vv.dtype.names:
        return {str(k): vv[k] for k in vv.dtype.names}
    if isinstance(vv, np.ndarray):
        if vv.dtype.names and vv.size == 1:
            rec = vv.reshape(-1)[0]
            return {str(k): rec[k] for k in vv.dtype.names}
        if vv.size == 1:
            return to_dict_like(vv.reshape(-1)[0])
    if hasattr(vv, "_fieldnames"):
        return {str(k): getattr(vv, k) for k in (vv._fieldnames or [])}
    raise TypeError(f"Expected dict-like object, got {type(vv)}")
