"""Multiple-testing correction methods, mirroring R's stats::p.adjust."""
import numpy as np

_METHODS = {"holm", "hochberg", "hommel", "bonferroni", "BH", "fdr", "BY", "none"}


def p_adjust(p, method="holm"):
    """Adjust a vector of p-values for multiple testing (mirrors R's stats::p.adjust)."""
    if method not in _METHODS:
        raise ValueError(f"Unknown correction_method: {method!r}")
    p = np.asarray(p, dtype=float)
    n = p.size
    if n == 0:
        return p.copy()

    if method == "none":
        return p.copy()

    if method == "bonferroni":
        return np.minimum(p * n, 1.0)

    order = np.argsort(p)
    p_sorted = p[order]

    if method == "holm":
        # Step-down: cumulative max from left of (n-i+1)*p_(i), capped at 1.
        adj_sorted = np.maximum.accumulate((n - np.arange(n)) * p_sorted)
        adj_sorted = np.minimum(adj_sorted, 1.0)
    elif method == "hochberg":
        # Step-up: q_i = max from right of (n-i+1)*p_(i), then capped at 1
        rev = (n - np.arange(n)) * p_sorted
        adj_sorted = np.minimum.accumulate(rev[::-1])[::-1]
        adj_sorted = np.minimum(adj_sorted, 1.0)
    elif method == "hommel":
        adj_sorted = _hommel(p_sorted, n)
    elif method in {"BH", "fdr"}:
        # Benjamini-Hochberg
        rev_idx = np.arange(n, 0, -1)
        rev_p = p_sorted[::-1]
        cummin = np.minimum.accumulate(n / rev_idx * rev_p)
        adj_sorted = np.minimum(cummin, 1.0)[::-1]
    elif method == "BY":
        # Benjamini-Yekutieli adds the harmonic-number factor.
        cm = np.sum(1.0 / np.arange(1, n + 1))
        rev_idx = np.arange(n, 0, -1)
        rev_p = p_sorted[::-1]
        cummin = np.minimum.accumulate(cm * n / rev_idx * rev_p)
        adj_sorted = np.minimum(cummin, 1.0)[::-1]
    else:  # pragma: no cover - guarded above
        raise AssertionError("unreachable")

    out = np.empty_like(p)
    out[order] = adj_sorted
    return out


def _hommel(p_sorted, n):
    """Hommel adjustment (port of R's stats::p.adjust implementation)."""
    if n <= 1:
        return p_sorted.copy()
    q = np.full(n, p_sorted.min() * n, dtype=float)
    pa = q.copy()
    for j in range(n - 1, 1, -1):
        ij = np.arange(n - j + 1)
        i2 = np.arange(n - j + 1, n)
        q1 = np.min(j * p_sorted[i2] / np.arange(2, j + 1))
        q[ij] = np.minimum(j * p_sorted[ij], q1)
        q[i2] = q[n - j]
        pa = np.maximum(pa, q)
    return np.minimum(np.maximum(pa, p_sorted), 1.0)
