"""
Replay RINGMLAPrefillBF16Kernel cases from the performance database on Ascend NPU.

Purpose:
  Read RINGMLAPrefillBF16Kernel rows from
  profiling_database/data/{device}/vllm_ascend/{version}/RINGMLAPrefillBF16Kernel.csv,
  rebuild the recorded tensor inputs, then execute torch_npu.atb.npu_ring_mla().

Notes:
  - This kernel maps to the vLLM-Ascend MLA prefill ATB path.
  - The current dataset contains two row families:
      * first-ring masked prefill: q/k lengths match, mask is present,
        pre_out and prev_lse are absent
      * incremental/default ring step: pre_out and prev_lse are present,
        mask tensor is not recorded
  - qk_scale is inferred from q_nope_dim + q_rope_dim, which matches the MLA
    attention head dimension construction in vLLM-Ascend.
"""

from __future__ import annotations

from math import ceil
from math import sqrt

try:
    from .common import (
        build_input_tensor,
        build_standard_argparser,
        ensure_npu_available,
        get_replay_repeat_count,
        get_runtime_modules,
        get_target_data_dir,
        init_runtime,
        normalize_dtype_name,
        parse_shape,
        print_invalid_replay_summary,
        process_replay_csvs,
        resolve_runtime_dtype,
    )
except ImportError:
    from common import (
        build_input_tensor,
        build_standard_argparser,
        ensure_npu_available,
        get_replay_repeat_count,
        get_runtime_modules,
        get_target_data_dir,
        init_runtime,
        normalize_dtype_name,
        parse_shape,
        print_invalid_replay_summary,
        process_replay_csvs,
        resolve_runtime_dtype,
    )


RING_MASK_SIZE = 512

def pick_dtype_name(*candidates: str, fallback: str = "DT_FLOAT") -> str:
    for candidate in candidates:
        if candidate and candidate.strip():
            return candidate
    return fallback

def parse_optional_shapes(raw_value: str) -> list[tuple[int, ...] | None]:
    values = []
    cleaned = raw_value.strip().strip('"')
    for item in cleaned.split(";"):
        item = item.strip()
        upper = item.upper()
        if upper in {"UNDEFINED", "DT_UNDEFINED", "N/A", "NA", "NULL"}:
            values.append(None)
        elif not item:
            values.append(None)
        else:
            values.append(parse_shape(item))
    return values


def parse_optional_text(raw_value: str) -> list[str]:
    values = []
    cleaned = raw_value.strip().strip('"')
    for item in cleaned.split(";"):
        item = item.strip()
        upper = item.upper()
        values.append("" if upper in {"", "UNDEFINED", "DT_UNDEFINED", "N/A", "NA", "NULL"} else item)
    return values


def build_mask_tensor(mask_shape: tuple[int, ...] | None, dtype_name: str):
    runtime_torch, _ = get_runtime_modules()
    if mask_shape is None:
        mask_shape = (RING_MASK_SIZE, RING_MASK_SIZE)
    dtype = resolve_runtime_dtype(pick_dtype_name(dtype_name))
    mask_value = float("-inf") if dtype == runtime_torch.float16 else 1
    mask = runtime_torch.zeros(mask_shape, dtype=dtype, device="npu")
    upper = runtime_torch.triu(
        runtime_torch.ones(mask_shape, dtype=runtime_torch.bool, device="npu"),
        diagonal=1,
    )
    return mask.masked_fill(upper, mask_value)


def infer_default_mask_shape(
    explicit_mask_shape: tuple[int, ...] | None,
    q_len: int,
    kv_len: int,
) -> tuple[int, ...]:
    if explicit_mask_shape is not None:
        return explicit_mask_shape
    return (RING_MASK_SIZE, RING_MASK_SIZE)


def build_lse_tensor(shape: tuple[int, ...], dtype_name: str):
    runtime_torch, _ = get_runtime_modules()
    dtype = resolve_runtime_dtype(dtype_name)
    return runtime_torch.empty(shape, dtype=dtype, device="npu")


def build_seqlen_tensor(*lengths: int):
    runtime_torch, _ = get_runtime_modules()
    if len(lengths) == 1:
        return runtime_torch.tensor([lengths[0]], dtype=runtime_torch.int32, device="cpu")
    return runtime_torch.tensor([[length] for length in lengths], dtype=runtime_torch.int32, device="cpu")


def split_lengths(total: int, slots: int, max_chunk: int) -> list[int]:
    remaining = total
    chunks = []
    for _ in range(slots):
        chunk = min(remaining, max_chunk)
        chunks.append(chunk)
        remaining -= chunk
    return chunks


def build_default_seqlen_candidates(q_len: int, kv_len: int):
    candidates = [build_seqlen_tensor(q_len, kv_len)]
    chunk_slots = max(ceil(q_len / RING_MASK_SIZE), ceil(kv_len / RING_MASK_SIZE))
    if chunk_slots > 1:
        runtime_torch, _ = get_runtime_modules()
        q_chunks = split_lengths(q_len, chunk_slots, RING_MASK_SIZE)
        kv_chunks = split_lengths(kv_len, chunk_slots, RING_MASK_SIZE)
        candidates.append(runtime_torch.tensor([q_chunks, kv_chunks], dtype=runtime_torch.int32, device="cpu"))
    return candidates


def build_row_case(row: dict[str, str]):
    init_runtime()
    input_shapes = parse_optional_shapes(row["Input Shapes"])
    input_formats = parse_optional_text(row["Input Formats"])
    input_dtypes = parse_optional_text(row["Input Data Types"])
    output_shapes = parse_optional_shapes(row["Output Shapes"])
    output_dtypes = parse_optional_text(row["Output Data Types"])

    if len(input_shapes) < 5:
        raise ValueError(
            "RINGMLAPrefillBF16Kernel expects at least five recorded tensor inputs"
        )

    q_nope = build_input_tensor(
        shape=input_shapes[0],
        input_format=input_formats[0],
        dtype_name=normalize_dtype_name(input_dtypes[0]),
    )
    q_rope = build_input_tensor(
        shape=input_shapes[1],
        input_format=input_formats[1],
        dtype_name=normalize_dtype_name(input_dtypes[1]),
    )
    k_nope = build_input_tensor(
        shape=input_shapes[2],
        input_format=input_formats[2],
        dtype_name=normalize_dtype_name(input_dtypes[2]),
    )
    k_rope = build_input_tensor(
        shape=input_shapes[3],
        input_format=input_formats[3],
        dtype_name=normalize_dtype_name(input_dtypes[3]),
    )
    value = build_input_tensor(
        shape=input_shapes[4],
        input_format=input_formats[4],
        dtype_name=normalize_dtype_name(input_dtypes[4]),
    )

    mask_shape = input_shapes[5] if len(input_shapes) > 5 else None
    pre_out_shape = input_shapes[13] if len(input_shapes) > 13 else None
    prev_lse_shape = input_shapes[14] if len(input_shapes) > 14 else None
    pre_out_dtype = input_dtypes[13] if len(input_dtypes) > 13 else input_dtypes[0]
    prev_lse_dtype = input_dtypes[14] if len(input_dtypes) > 14 else "DT_FLOAT"

    has_prefix_state = pre_out_shape is not None and prev_lse_shape is not None
    output_shape = output_shapes[0] if output_shapes and output_shapes[0] is not None else pre_out_shape
    if output_shape is None:
        raise ValueError("Unable to infer output shape for RINGMLAPrefillBF16Kernel")

    lse_shape = None
    if len(output_shapes) > 1 and output_shapes[1] is not None:
        lse_shape = output_shapes[1]
    elif prev_lse_shape is not None:
        lse_shape = prev_lse_shape
    else:
        lse_shape = (q_nope.shape[1], q_nope.shape[0])

    if has_prefix_state:
        q_len = q_nope.shape[0]
        kv_len = k_nope.shape[0]
        output_dtype_name = normalize_dtype_name(
            pick_dtype_name(output_dtypes[0] if output_dtypes else "", pre_out_dtype, input_dtypes[0])
        )
        lse_dtype_name = pick_dtype_name(
            output_dtypes[1] if len(output_dtypes) > 1 else "",
            prev_lse_dtype,
            "DT_FLOAT",
        )
        pre_out = build_input_tensor(
            shape=pre_out_shape,
            input_format=input_formats[13],
            dtype_name=normalize_dtype_name(pre_out_dtype),
        )
        prev_lse = build_input_tensor(
            shape=prev_lse_shape,
            input_format=input_formats[14],
            dtype_name=normalize_dtype_name(pick_dtype_name(prev_lse_dtype, input_dtypes[0])),
        )
        mask = build_mask_tensor(
            infer_default_mask_shape(mask_shape, q_len, kv_len),
            pick_dtype_name(input_dtypes[0]),
        )
        mask_type = "no_mask"
        calc_type = "calc_type_default"
        seqlen = build_seqlen_tensor(q_len, kv_len)
        seqlen_candidates = build_default_seqlen_candidates(q_len, kv_len)
        output = pre_out
        softmax_lse = prev_lse
    else:
        output_dtype_name = normalize_dtype_name(
            pick_dtype_name(output_dtypes[0] if output_dtypes else "", input_dtypes[0])
        )
        lse_dtype_name = pick_dtype_name(
            output_dtypes[1] if len(output_dtypes) > 1 else "",
            prev_lse_dtype,
            "DT_FLOAT",
        )
        pre_out = None
        prev_lse = None
        mask = build_mask_tensor(
            mask_shape,
            pick_dtype_name(
                input_dtypes[5] if len(input_dtypes) > 5 else "",
                input_dtypes[0],
            ),
        )
        mask_type = "mask_type_triu"
        calc_type = "calc_type_first_ring"
        seqlen = build_seqlen_tensor(q_nope.shape[0])
        seqlen_candidates = [seqlen]
        output = build_input_tensor(
            shape=output_shape,
            input_format="ND",
            dtype_name=output_dtype_name,
        )
        softmax_lse = build_lse_tensor(
            shape=lse_shape,
            dtype_name=lse_dtype_name,
        )

    head_num = q_nope.shape[1]
    qk_scale = 1.0 / sqrt(q_nope.shape[-1] + q_rope.shape[-1])

    return {
        "q_nope": q_nope,
        "q_rope": q_rope,
        "k_nope": k_nope,
        "k_rope": k_rope,
        "value": value,
        "mask": mask,
        "seqlen": seqlen,
        "seqlen_candidates": seqlen_candidates,
        "has_prefix_state": has_prefix_state,
        "head_num": head_num,
        "kv_head_num": head_num,
        "pre_out": pre_out,
        "prev_lse": prev_lse,
        "qk_scale": qk_scale,
        "mask_type": mask_type,
        "calc_type": calc_type,
        "output": output,
        "softmax_lse": softmax_lse,
        "output_shape": output_shape,
        "lse_shape": lse_shape,
        "output_dtype_name": output_dtype_name,
        "lse_dtype_name": lse_dtype_name,
        "input_dtype_name": pick_dtype_name(input_dtypes[0]),
    }


def build_argparser():
    return build_standard_argparser(
        description=(
            "Run RINGMLAPrefillBF16Kernel workload replay on Ascend NPU.\n"
            "The script reads RINGMLAPrefillBF16Kernel.csv under the selected\n"
            "device and vllm_ascend version directory, reconstructs q/k/v MLA\n"
            "inputs from the recorded CSV rows, infers ATB scalar arguments,\n"
            "then runs torch_npu.atb.npu_ring_mla()."
        ),
        usage_examples=[
            "py -3 tools/perf_data_collection/op_replay/RINGMLAPrefillBF16Kernel_run.py "
            "--device ATLAS_800_A3_752T_128G_DIE --vllm-version 0.13.0",
        ],
        version_help="vLLM-Ascend version, e.g. 0.13.0.",
    )


def run_row(csv_path, row_index: int, row: dict[str, str]) -> None:
    runtime_torch, runtime_torch_npu = get_runtime_modules()
    case = build_row_case(row)

    default_configs = [
        {
            "name": "default_nomask_alias_2d",
            "mask_type": "no_mask",
            "mask": case["mask"],
            "alias_output": True,
        },
        {
            "name": "default_nomask_separate_2d",
            "mask_type": "no_mask",
            "mask": case["mask"],
            "alias_output": False,
        },
        {
            "name": "default_triu_alias_3d",
            "mask_type": "mask_type_triu",
            "mask": build_mask_tensor((1, RING_MASK_SIZE, RING_MASK_SIZE), case["input_dtype_name"]),
            "alias_output": True,
        },
        {
            "name": "default_triu_separate_3d",
            "mask_type": "mask_type_triu",
            "mask": build_mask_tensor((1, RING_MASK_SIZE, RING_MASK_SIZE), case["input_dtype_name"]),
            "alias_output": False,
        },
    ]
    configs = default_configs if case["has_prefix_state"] else [{
        "name": "first_ring",
        "mask_type": case["mask_type"],
        "mask": case["mask"],
        "alias_output": False,
    }]

    last_exc = None
    attempts = []
    for config in configs:
        for seqlen in case["seqlen_candidates"]:
            try:
                if config["alias_output"]:
                    output = case["pre_out"]
                    softmax_lse = case["prev_lse"]
                else:
                    output = build_input_tensor(
                        shape=case["output_shape"],
                        input_format="ND",
                        dtype_name=case["output_dtype_name"],
                    )
                    softmax_lse = build_lse_tensor(
                        shape=case["lse_shape"],
                        dtype_name=case["lse_dtype_name"],
                    )

                output, softmax_lse = runtime_torch_npu.atb.npu_ring_mla(
                    q_nope=case["q_nope"],
                    q_rope=case["q_rope"],
                    k_nope=case["k_nope"],
                    k_rope=case["k_rope"],
                    value=case["value"],
                    mask=config["mask"],
                    seqlen=seqlen,
                    head_num=case["head_num"],
                    kv_head_num=case["kv_head_num"],
                    pre_out=case["pre_out"],
                    prev_lse=case["prev_lse"],
                    qk_scale=case["qk_scale"],
                    kernel_type="kernel_type_high_precision",
                    mask_type=config["mask_type"],
                    input_layout="type_bsnd",
                    calc_type=case["calc_type"],
                    output=output,
                    softmax_lse=softmax_lse,
                )
                runtime_torch.npu.synchronize()
                case["seqlen"] = seqlen
                case["mask_type"] = config["mask_type"]
                case["output"] = output
                case["softmax_lse"] = softmax_lse
                case["selected_config"] = config["name"]
                break
            except RuntimeError as exc:
                last_exc = exc
                attempts.append(f"{config['name']} seqlen={seqlen.tolist()}")
        else:
            continue
        break
    else:
        raise RuntimeError(
            "All RINGMLAPrefillBF16Kernel replay attempts failed: "
            + ", ".join(attempts)
        ) from last_exc

    print(
        f"[OK] {csv_path}:{row_index} "
        f"q={tuple(case['q_nope'].shape)} q_rope={tuple(case['q_rope'].shape)} "
        f"k={tuple(case['k_nope'].shape)} v={tuple(case['value'].shape)} "
        f"output={tuple(output.shape)} lse={tuple(softmax_lse.shape)} "
        f"mask_type={case['mask_type']} calc_type={case['calc_type']} "
        f"seqlen={case['seqlen'].tolist()} config={case['selected_config']}"
    )


def main() -> None:
    args = build_argparser().parse_args()
    repeat_count = get_replay_repeat_count(args.repeat_count)
    ensure_npu_available()

    target_data_dir = get_target_data_dir(
        device=args.device,
        vllm_ascend_version=args.vllm_version,
        database_path=args.database_path,
        torch_version=args.torch_version,
        cann_version=args.cann_version,
    )
    csv_paths = sorted(target_data_dir.rglob("RINGMLAPrefillBF16Kernel.csv"))
    if not csv_paths:
        raise FileNotFoundError(
            f"No RINGMLAPrefillBF16Kernel.csv found under {target_data_dir}"
        )

    total_rows, invalid_rows, _, skipped_rows = process_replay_csvs(
        kernel_type="RINGMLAPrefillBF16Kernel",
        csv_paths=csv_paths,
        repeat_count=repeat_count,
        run_row_fn=run_row,
        update_mode=args.update_mode,
    )

    print(
        "Processed "
        f"{total_rows} RINGMLAPrefillBF16Kernel rows from {len(csv_paths)} "
        f"csv file(s) under {target_data_dir}."
    )
    if args.update_mode == "missing-only":
        print(f"[SUMMARY] RINGMLAPrefillBF16Kernel: skipped {skipped_rows} row(s) due to missing-only mode.")
    print_invalid_replay_summary(invalid_rows, label="RINGMLAPrefillBF16Kernel")


if __name__ == "__main__":
    main()