# Copyright (c) 2026, HUAWEI CORPORATION.  All rights reserved.
import torch
from contextlib import contextmanager


@contextmanager
def set_num_threads(num_thread):
    old = torch.get_num_threads()
    torch.set_num_threads(num_thread)
    try:
        yield
    finally:
        torch.set_num_threads(old)


def get_abs_err(x, y):
    return (x.detach() - y.detach()).flatten().abs().max().item()


def get_err_ratio(x, y):
    err = (x.detach() - y.detach()).flatten().square().mean().sqrt().item()
    base = (x.detach()).flatten().square().mean().sqrt().item()
    return err / (base + 1e-8)


def assert_close(prefix, ref, tri, ratio, warning=False, err_atol=1e-6):
    abs_atol = get_abs_err(ref, tri)
    msg = f"{prefix:>16} diff: {abs_atol:.6f} ratio: {get_err_ratio(ref, tri):.6f}"
    error_rate = get_err_ratio(ref, tri)
    if abs_atol <= err_atol:
        return
    else:
        assert error_rate < ratio, msg


def get_mare(actual, golden, eps=1e-7):
    """Max Absolute Relative Error: max(|actual - golden| / (|golden| + eps)).

    Computation is moved to CPU and performed in float64 to avoid
    low-precision reduction errors and NPU double-type limitations.
    """
    actual = actual.detach().cpu().flatten().double()
    golden = golden.detach().cpu().flatten().double()
    return (torch.abs(actual - golden) / (torch.abs(golden) + eps)).max().item()


def get_mere(actual, golden, eps=1e-7):
    """Mean Absolute Relative Error: mean(|actual - golden| / (|golden| + eps)).

    Computation is moved to CPU and performed in float64 to avoid
    low-precision reduction errors and NPU double-type limitations.
    """
    actual = actual.detach().cpu().flatten().double()
    golden = golden.detach().cpu().flatten().double()
    return (torch.abs(actual - golden) / (torch.abs(golden) + eps)).mean().item()


def get_rmse(actual, golden, eps=None):
    """Root Mean Square Error: sqrt(sum((actual - golden)^2) / N).

    Computation is moved to CPU and performed in float64 to avoid
    low-precision reduction errors and NPU double-type limitations.
    `eps` is accepted for API consistency with get_mare/get_mere but is ignored.
    """
    diff = actual.detach().cpu().flatten().double() - golden.detach().cpu().flatten().double()
    return torch.sqrt(torch.mean(diff * diff)).item()


DUAL_BENCH_LEVELS = {
    "L0": {"MARE": 10.0, "MERE": 2.0, "RMSE": 2.0},
    "L1": {"MARE": 5.0, "MERE": 1.5, "RMSE": 1.5},
    "L2": {"MARE": 2.0, "MERE": 1.2, "RMSE": 1.2},
}


def assert_dual_bench(prefix, actual, ref, golden, level="L0", eps=1e-7, zero_guard=None):
    """Assert dual-bench ratios are within the specified level thresholds.

    Thresholds (MARE / MERE / RMSE):
        L0: 10.0 / 2.0 / 2.0
        L1:  5.0 / 1.5 / 1.5
        L2:  2.0 / 1.2 / 1.2
    """
    if level not in DUAL_BENCH_LEVELS:
        raise ValueError(f"Unknown dual-bench level: {level}. Choose from {list(DUAL_BENCH_LEVELS.keys())}.")
    thresholds = DUAL_BENCH_LEVELS[level]
    if zero_guard is None:
        zero_guard = get_zero_guard(actual.dtype)

    exceeded = []
    for metric_name, metric_fn in (("MARE", get_mare), ("MERE", get_mere), ("RMSE", get_rmse)):
        err_actual = metric_fn(actual, golden, eps=eps)
        err_ref = metric_fn(ref, golden, eps=eps)
        ratio = err_actual / max(err_ref, zero_guard)
        if ratio > thresholds[metric_name]:
            exceeded.append(
                f"{metric_name} ratio={ratio:.6f} > {level} threshold={thresholds[metric_name]} "
                f"(actual={err_actual:.6e}, ref={err_ref:.6e})"
            )

    if exceeded:
        diagnose_dual_bench(actual, ref, golden, name=prefix, eps=eps)
        msg = f"{prefix:>16} dual-bench assertion failed:\n" + "\n".join(f"  {e}" for e in exceeded)
        assert False, msg


def diagnose_dual_bench(actual, ref, golden, name="", eps=1e-7):
    """Locate worst relative-error elements for both actual and ref against golden."""
    actual = actual.detach().cpu().flatten().double()
    ref = ref.detach().cpu().flatten().double()
    golden = golden.detach().cpu().flatten().double()

    rel_actual = (actual - golden).abs() / (golden.abs() + eps)
    rel_ref = (ref - golden).abs() / (golden.abs() + eps)

    idx_actual = rel_actual.argmax().item()
    idx_ref = rel_ref.argmax().item()

    print(
        f"[{name}] actual worst idx={idx_actual}, "
        f"golden={golden[idx_actual]:.6e}, actual={actual[idx_actual]:.6e}, ref={ref[idx_actual]:.6e}, "
        f"rel_actual={rel_actual[idx_actual]:.6f}, rel_ref={rel_ref[idx_actual]:.6f}"
    )
    print(
        f"[{name}] ref    worst idx={idx_ref}, "
        f"golden={golden[idx_ref]:.6e}, actual={actual[idx_ref]:.6e}, ref={ref[idx_ref]:.6e}, "
        f"rel_actual={rel_actual[idx_ref]:.6f}, rel_ref={rel_ref[idx_ref]:.6f}"
    )


def get_zero_guard(dtype):
    """Return a dtype-aware floor for the reference error in dual-bench ratios."""
    if dtype == torch.float16:
        return 2e-11
    if dtype == torch.bfloat16:
        return 2e-8
    if dtype == torch.float32:
        return 2e-14
    return 1e-7


def dual_bench_ratio(actual, ref, golden, metric_fn=get_mare, eps=1e-7, zero_guard=None):
    """Return actual's error relative to ref's error against golden.

    ratio = metric_fn(actual, golden) / max(metric_fn(ref, golden), zero_guard)

    This is useful when `ref` is a trusted baseline (e.g. PyTorch native) and
    `actual` is the implementation under test (e.g. Triton).  The ratio tells
    how many times larger the actual error is compared with the baseline error.

    If `zero_guard` is None, it is auto-selected from `actual.dtype` via
    `get_zero_guard`.
    """
    if zero_guard is None:
        zero_guard = get_zero_guard(actual.dtype)
    err_actual = metric_fn(actual, golden, eps=eps)
    err_ref = metric_fn(ref, golden, eps=eps)
    return err_actual / max(err_ref, zero_guard)


def print_dual_bench(name, actual, ref, golden, eps=1e-7, zero_guard=None):
    """Print MARE/MERE/RMSE for actual and ref against golden, plus their ratios.

    If `zero_guard` is None, it is auto-selected from `actual.dtype` via
    `get_zero_guard`.
    """
    if zero_guard is None:
        zero_guard = get_zero_guard(actual.dtype)
    for metric_name, metric_fn in (("MARE", get_mare), ("MERE", get_mere), ("RMSE", get_rmse)):
        err_actual = metric_fn(actual, golden, eps=eps)
        err_ref = metric_fn(ref, golden, eps=eps)
        ratio = err_actual / max(err_ref, zero_guard)
        print(f"[{name}] {metric_name:4}: actual={err_actual:.6e}, ref={err_ref:.6e}, ratio={ratio:.6f}")


def pairwise(parameters):
    """Generate pairwise test combinations.

    Given N parameter lists, produces a minimal set of tuples such that every
    pair of values from any two different lists appears together in at least
    one tuple.  This drastically reduces the number of test cases compared to
    the full Cartesian product while still covering all two-way interactions.

    Usage with pytest::

        from tests.utils import pairwise

        B = [1, 2, 4]
        T = [1024, 4096, 16384]
        H = [1, 4, 16]

        CASES = pairwise([B, T, H])  # 9 cases instead of 27

        @pytest.mark.parametrize("params", CASES, ids=lambda p: str(p))
        def test_something(params):
            b, t, h = params
            ...

    Args:
        parameters: List of parameter value lists.  Must contain at least 2
            non-empty lists.

    Returns:
        List of tuples, one per test case.
    """
    if len(parameters) < 2:
        raise ValueError("pairwise requires at least 2 parameter lists")
    for p in parameters:
        if not p:
            raise ValueError("each parameter list must be non-empty")

    n = len(parameters)
    sizes = [len(p) for p in parameters]
    uncovered = set()
    for i in range(n):
        for j in range(i + 1, n):
            for vi in range(sizes[i]):
                for vj in range(sizes[j]):
                    uncovered.add((i, vi, j, vj))

    result = []
    while uncovered:
        seed = next(iter(uncovered))
        pi, vi, pj, vj = seed
        row = [None] * n
        row[pi] = vi
        row[pj] = vj
        filled = {pi, pj}
        remaining = sorted(
            [pos for pos in range(n) if pos not in filled],
            key=lambda x: sizes[x],
            reverse=True,
        )
        for pos in remaining:
            best_val, best_count = 0, -1
            for val in range(sizes[pos]):
                count = 0
                for other in filled:
                    if other < pos:
                        pair = (other, row[other], pos, val)
                    else:
                        pair = (pos, val, other, row[other])
                    if pair in uncovered:
                        count += 1
                if count > best_count:
                    best_count = count
                    best_val = val
            row[pos] = best_val
            filled.add(pos)
        for i in range(n):
            for j in range(i + 1, n):
                uncovered.discard((i, row[i], j, row[j]))
        result.append(tuple(parameters[i][row[i]] for i in range(n)))
    return result


def print_diff(name, ref, tri, atol=0.005):
    abs_diff = torch.abs(ref - tri)
    max_abs_diff = abs_diff.max().item()
    print(f"[{name}] Max absolute difference: {max_abs_diff:.6f}")
    if max_abs_diff > atol:
        print(f"Exceeds tolerance ({atol})!")