# Some code borrowed from the awesome work: https://github.com/zhuzilin/ring-flash-attention
# Copyright (c) ModelScope Contributors. All rights reserved.
import torch
from functools import cache

from .ring_utils import RingComm

_NPU_BLOCK_MASK_SIZE = 2048
_NPU_FULL_TOKENS = 2147483647
_NPU_TND_SOFTMAX_STAT_REPEAT = 8


def is_npu_tensor(tensor: torch.Tensor) -> bool:
    return tensor.device.type == 'npu'


def _cu_seqlens_to_actual_seq(cu_seqlens: torch.Tensor) -> tuple[int, ...]:
    return tuple(int(x) for x in cu_seqlens[1:].detach().cpu().tolist())


@cache
def _get_npu_causal_mask_cpu() -> torch.Tensor:
    return torch.triu(torch.ones((_NPU_BLOCK_MASK_SIZE, _NPU_BLOCK_MASK_SIZE), dtype=torch.bool), diagonal=1)


def _get_npu_causal_mask(device: torch.device) -> torch.Tensor:
    return _get_npu_causal_mask_cpu().to(device=device)


def _normalize_window_size(window_size):
    if window_size is None:
        return -1, -1
    return window_size


def _get_npu_sparse_params(causal: bool, window_size, device: torch.device) -> dict:
    window_size = _normalize_window_size(window_size)
    if window_size != (-1, -1):
        left, right = window_size
        left = _NPU_FULL_TOKENS if left < 0 else int(left)
        right = _NPU_FULL_TOKENS if right < 0 else int(right)
        if causal:
            right = 0
        return {
            'atten_mask': _get_npu_causal_mask(device),
            'sparse_mode': 4,
            'pre_tockens': left,
            'next_tockens': right,
        }
    if causal:
        return {
            'atten_mask': _get_npu_causal_mask(device),
            'sparse_mode': 3,
            'pre_tockens': _NPU_FULL_TOKENS,
            'next_tockens': _NPU_FULL_TOKENS,
        }
    return {
        'atten_mask': None,
        'sparse_mode': 0,
        'pre_tockens': _NPU_FULL_TOKENS,
        'next_tockens': _NPU_FULL_TOKENS,
    }


def _reshape_npu_lse(lse: torch.Tensor, seqlen_q: int, num_heads: int) -> torch.Tensor:
    """Normalize Ascend softmax stats to flash-attn's [num_heads, seqlen] layout."""
    if lse.dim() == 2:
        if lse.shape == (num_heads, seqlen_q):
            return lse.contiguous()
        if lse.shape == (seqlen_q, num_heads):
            return lse.transpose(0, 1).contiguous()
    elif lse.dim() == 3:
        # Some CANN versions return an extra trailing size-8 axis with repeated
        # stats. Ring merge only needs one copy of each token/head lse.
        if lse.shape[-1] == 8:
            lse = lse[..., 0]
            if lse.shape == (seqlen_q, num_heads):
                return lse.transpose(0, 1).contiguous()
            if lse.shape == (num_heads, seqlen_q):
                return lse.contiguous()
        if lse.shape[0] == seqlen_q:
            return lse.permute(1, 2, 0).reshape(num_heads, seqlen_q).contiguous()
        if lse.shape[1] == seqlen_q:
            return lse.permute(0, 2, 1).reshape(num_heads, seqlen_q).contiguous()
    raise RuntimeError(f'Unexpected NPU lse shape {tuple(lse.shape)} for seqlen_q={seqlen_q}, num_heads={num_heads}')


def _get_npu_attention_common_kwargs(
    q: torch.Tensor,
    *,
    cu_seqlens_q: torch.Tensor,
    cu_seqlens_kv: torch.Tensor,
    softmax_scale: float,
    dropout_p: float,
    causal: bool,
    window_size,
    deterministic: bool,
) -> dict:
    sparse_params = _get_npu_sparse_params(causal, window_size, q.device)
    return {
        'head_num': q.shape[1],
        'input_layout': 'TND',
        'scale_value': softmax_scale or q.shape[-1]**(-0.5),
        'keep_prob': 1. - dropout_p,
        'actual_seq_qlen': _cu_seqlens_to_actual_seq(cu_seqlens_q),
        'actual_seq_kvlen': _cu_seqlens_to_actual_seq(cu_seqlens_kv),
        'sync': bool(deterministic and dropout_p > 0),
        **sparse_params,
    }


def _call_npu_fusion_attention(
    q: torch.Tensor,
    k: torch.Tensor,
    v: torch.Tensor,
    *,
    cu_seqlens_q: torch.Tensor,
    cu_seqlens_kv: torch.Tensor,
    softmax_scale: float,
    dropout_p: float,
    causal: bool,
    window_size,
    deterministic: bool,
):
    import torch_npu

    common_kwargs = _get_npu_attention_common_kwargs(
        q,
        cu_seqlens_q=cu_seqlens_q,
        cu_seqlens_kv=cu_seqlens_kv,
        softmax_scale=softmax_scale,
        dropout_p=dropout_p,
        causal=causal,
        window_size=window_size,
        deterministic=deterministic,
    )
    params = {
        'query': q,
        'key': k,
        'value': v,
        'scale': common_kwargs['scale_value'],
        'softmax_layout': 'TND',
    }
    params.update(common_kwargs)
    params.pop('scale_value')
    return torch_npu.npu_fusion_attention(**params)


def _call_npu_fusion_attention_grad(
    dout: torch.Tensor,
    q: torch.Tensor,
    k: torch.Tensor,
    v: torch.Tensor,
    *,
    attention_out: torch.Tensor,
    softmax_lse: torch.Tensor,
    cu_seqlens_q: torch.Tensor,
    cu_seqlens_kv: torch.Tensor,
    softmax_scale: float,
    dropout_p: float,
    causal: bool,
    window_size,
    deterministic: bool,
):
    import torch_npu

    if not hasattr(torch_npu, 'npu_fusion_attention_grad'):
        raise AttributeError('torch_npu.npu_fusion_attention_grad is not available')
    # Dropout backward needs the exact seed/offset from the original forward,
    # which this ring ctx does not save. Fail instead of using a wrong mask.
    if dropout_p != 0.0:
        raise NotImplementedError('NPU ring attention native backward currently requires dropout_p=0.')

    common_kwargs = _get_npu_attention_common_kwargs(
        q,
        cu_seqlens_q=cu_seqlens_q,
        cu_seqlens_kv=cu_seqlens_kv,
        softmax_scale=softmax_scale,
        dropout_p=dropout_p,
        causal=causal,
        window_size=window_size,
        deterministic=deterministic,
    )
    softmax_max, softmax_sum = _npu_softmax_stats_from_global_lse(
        softmax_lse,
        q_tokens=q.shape[0],
        num_heads=q.shape[1],
    )

    params = {
        'query': q,
        'key': k,
        'value': v,
        'dy': dout,
        'head_num': common_kwargs['head_num'],
        'input_layout': common_kwargs['input_layout'],
        'atten_mask': common_kwargs['atten_mask'],
        'softmax_max': softmax_max,
        'softmax_sum': softmax_sum,
        'softmax_in': None,
        'attention_in': (attention_out if torch.is_tensor(attention_out) and attention_out.numel() > 0 else None),
        'scale_value': common_kwargs['scale_value'],
        'keep_prob': common_kwargs['keep_prob'],
        'pre_tockens': common_kwargs['pre_tockens'],
        'next_tockens': common_kwargs['next_tockens'],
        'seed': 0,
        'offset': 0,
        'numels': 0,
        'actual_seq_qlen': common_kwargs['actual_seq_qlen'],
        'actual_seq_kvlen': common_kwargs['actual_seq_kvlen'],
        'sparse_mode': common_kwargs['sparse_mode'],
        'sync': common_kwargs['sync'],
        'softmax_layout': 'TND',
    }
    return torch_npu.npu_fusion_attention_grad(**params)


def _normalize_flash_attn_lse(softmax_lse: torch.Tensor, total_len: int) -> torch.Tensor:
    """Normalize flash-attn lse to [num_heads, total_len]."""
    lse = softmax_lse
    if lse.dim() == 3 and lse.shape[0] == 1:
        lse = lse.squeeze(0)
    if lse.dim() != 2:
        raise RuntimeError(f'Unexpected softmax_lse shape: {tuple(softmax_lse.shape)}')
    if lse.shape[1] != total_len:
        lse = lse.transpose(0, 1).contiguous()
    if lse.shape[1] != total_len:
        raise RuntimeError(f'Unexpected softmax_lse shape: {tuple(softmax_lse.shape)} for total_len={total_len}')
    return lse


def _npu_softmax_stats_from_global_lse(
    softmax_lse_global: torch.Tensor,
    q_tokens: int,
    num_heads: int,
) -> tuple[torch.Tensor, torch.Tensor]:
    lse_h_t = _normalize_flash_attn_lse(softmax_lse_global, q_tokens)
    if lse_h_t.shape[0] != num_heads:
        raise RuntimeError(f'Unexpected global lse shape: {tuple(softmax_lse_global.shape)} '
                           f'for q_tokens={q_tokens}, num_heads={num_heads}')

    # With softmax_layout='TND', Ascend returns softmax stats as [T, N, 8].
    # The split-attention backward only needs logsumexp; max=lse and sum=1
    # encode the same value without replaying the block forward.
    lse_t_h = lse_h_t.transpose(0, 1).contiguous().to(torch.float32)
    softmax_max = lse_t_h.unsqueeze(-1).expand(
        q_tokens,
        num_heads,
        _NPU_TND_SOFTMAX_STAT_REPEAT,
    ).contiguous()
    return softmax_max, torch.ones_like(softmax_max)


def _get_second_half_lse(softmax_lse: torch.Tensor, cu_seqlens: torch.Tensor) -> torch.Tensor:
    total_len = int(cu_seqlens[-1].item())
    lse = _normalize_flash_attn_lse(softmax_lse, total_len)

    # The step > rank branch only differentiates q[half_index1]. Slice the final
    # merged lse per sequence so the native grad ctx sees the same query span.
    second_half_lse = torch.empty((lse.shape[0], lse.shape[1] // 2), dtype=lse.dtype, device=lse.device)
    for i in range(len(cu_seqlens) - 1):
        start, end = cu_seqlens[i].item(), cu_seqlens[i + 1].item()
        new_start, new_end = start // 2, end // 2
        start += (end - start) // 2
        second_half_lse[:, new_start:new_end] = lse[:, start:end]
    return second_half_lse


def _npu_block_backward_with_global_stats(
    block_dout,
    block_q,
    block_k,
    block_v,
    block_out_global,
    block_lse_global,
    block_causal,
    cu_seqlens_q,
    cu_seqlens_kv,
    softmax_scale,
    dropout_p,
    window_size,
    deterministic,
):
    """Run one native NPU block backward using the final merged ring stats."""
    return _call_npu_fusion_attention_grad(
        block_dout.to(block_q.dtype),
        block_q,
        block_k,
        block_v,
        attention_out=block_out_global,
        softmax_lse=block_lse_global,
        cu_seqlens_q=cu_seqlens_q,
        cu_seqlens_kv=cu_seqlens_kv,
        softmax_scale=softmax_scale,
        dropout_p=dropout_p,
        causal=block_causal,
        window_size=window_size,
        deterministic=deterministic,
    )[:3]


def _squeeze_batch(*tensors):
    squeezed = []
    for tensor in tensors:
        if tensor.shape[0] == 1:
            squeezed.append(tensor.squeeze(0))
        else:
            squeezed.append(tensor)
    return tuple(squeezed)


def npu_backward(
        process_group,
        dout,
        q,
        k,
        v,
        out,
        softmax_lse,
        cu_seqlens,
        max_seqlen,
        half_index0,
        half_index1,
        softmax_scale,
        dropout_p=0.0,
        window_size=(-1, -1),
        deterministic=False,
):
    kv_comm = RingComm(process_group)
    d_kv_comm = RingComm(process_group)
    dout, q, k, v, out, softmax_lse = _squeeze_batch(dout, q, k, v, out, softmax_lse)
    cu_seqlens = cu_seqlens // kv_comm.world_size
    del max_seqlen

    half_cu_seqlens = cu_seqlens // 2
    q1 = q[half_index1]
    dout1 = dout[half_index1]
    out1 = out[half_index1]
    softmax_lse1 = _get_second_half_lse(softmax_lse, cu_seqlens)

    dq = torch.zeros_like(q, dtype=torch.float32)
    current_step_dk = torch.empty_like(k, dtype=torch.float32)
    current_step_dv = torch.empty_like(v, dtype=torch.float32)
    next_dk = next_dv = None

    for step in range(kv_comm.world_size):
        current_step_dk.zero_()
        current_step_dv.zero_()
        if step == 0:
            bdq, bdk, bdv = _npu_block_backward_with_global_stats(
                dout,
                q,
                k,
                v,
                out,
                softmax_lse,
                True,
                cu_seqlens,
                cu_seqlens,
                softmax_scale,
                dropout_p,
                window_size,
                deterministic,
            )
            dq += bdq.to(torch.float32)
            current_step_dk += bdk.to(torch.float32)
            current_step_dv += bdv.to(torch.float32)
        elif step <= kv_comm.rank:
            k0 = k[half_index0]
            v0 = v[half_index0]
            bdq, bdk, bdv = _npu_block_backward_with_global_stats(
                dout,
                q,
                k0,
                v0,
                out,
                softmax_lse,
                False,
                cu_seqlens,
                half_cu_seqlens,
                softmax_scale,
                dropout_p,
                window_size,
                deterministic,
            )
            dq += bdq.to(torch.float32)
            current_step_dk[half_index0] += bdk.to(torch.float32)
            current_step_dv[half_index0] += bdv.to(torch.float32)
        else:
            bdq, bdk, bdv = _npu_block_backward_with_global_stats(
                dout1,
                q1,
                k,
                v,
                out1,
                softmax_lse1,
                False,
                half_cu_seqlens,
                cu_seqlens,
                softmax_scale,
                dropout_p,
                window_size,
                deterministic,
            )
            dq[half_index1] += bdq.to(torch.float32)
            current_step_dk += bdk.to(torch.float32)
            current_step_dv += bdv.to(torch.float32)

        # K/V gradients are owned by the rank that originally held that shard.
        # Rotate the accumulated gradients in the opposite ring direction until
        # each owner receives its final dk/dv.
        if step == 0:
            dk = current_step_dk
            dv = current_step_dv
        else:
            dk = next_dk
            dv = next_dv
            dk += current_step_dk
            dv += current_step_dv

        next_dk, next_dv = d_kv_comm.send_recv_kv(dk, dv)
        d_kv_comm.wait()

        if step + 1 != kv_comm.world_size:
            next_k, next_v = kv_comm.send_recv_kv(k, v)
            kv_comm.wait()
            k, v = next_k, next_v

    return dq.to(q.dtype).unsqueeze(0), next_dk.to(q.dtype).unsqueeze(0), next_dv.to(q.dtype).unsqueeze(0)


def npu_forward(
    q,
    k,
    v,
    causal,
    cu_seqlens_q,
    cu_seqlens_kv,
    dropout_p,
    softmax_scale,
    deterministic,
    window_size,
):
    outputs = _call_npu_fusion_attention(
        q,
        k,
        v,
        cu_seqlens_q=cu_seqlens_q,
        cu_seqlens_kv=cu_seqlens_kv,
        softmax_scale=softmax_scale,
        dropout_p=dropout_p,
        causal=causal,
        window_size=window_size,
        deterministic=deterministic,
    )
    block_out, softmax_max, softmax_sum = outputs[:3]
    block_lse = softmax_max.to(torch.float32) + torch.log(softmax_sum.to(torch.float32))
    block_lse = _reshape_npu_lse(block_lse, q.shape[0], q.shape[1])
    return block_out, block_lse
