"""CSA indexer — sparse top-k selection from compressed KV cache.

Paper §2.3.1, eq. 13–17:
  c_Q = h_t · W_DQ    (shared with main queries)
  q^I_t = c_Q · W_IUQ  (low-rank indexer queries)
  w^I_t = h_t · W_w    (per-head weights)
  I[t,s] = Σ_h w^I_t,h · ReLU(q^I_t,h · K^IComp[s])
  Selected = TopK(I[t,:])

The indexer only exists in CSA layers. HCA and SWA layers don't have
an indexer (they do dense attention).
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import torch

if TYPE_CHECKING:
    from dsv4.model.config import DSV4Config
    from dsv4.cache.handle import LayerCacheHandle


class CSAIndexer:
    """Lightning indexer for CSA layers.

    Composed by AttentionSubBlock when layer is CSA. Owns W_IUQ and W_w.
    The shared c_Q comes from the main query path; this class does NOT
    own W_DQ.
    """

    def __init__(self, config: "DSV4Config"):
        self.config = config
        self._runner_id = None

    def __call__(
        self,
        c_Q: torch.Tensor,     # [T, d_c] BF16 — shared latent
        h_t: torch.Tensor,     # [T, d] BF16 — hidden states
        cache: "LayerCacheHandle",
    ) -> torch.Tensor:
        """Return top-k compressed-block indices per query token.

        Returns [T, csa_top_k] int32 indices into the compressed pool.
        """
        from dsv4.kernels.indexer.score_topk import run_indexer_score_topk

        # Kernel A: indexer query up-projection (c_Q -> q_I)
        # For now, use a simple torch linear; will swap to Nvfp4Linear
        # with FP4 output in Phase 2.
        if not hasattr(self, '_q_up_weight'):
            # Lazy init — weights would be loaded from checkpoint
            d_c = self.config.query_compression_dim
            n_ih = self.config.indexer_num_heads
            c_i = self.config.indexer_head_dim
            self._q_up_weight = torch.randn(
                d_c, n_ih * c_i, dtype=torch.bfloat16, device='cuda') * 0.02
            self._w_head_weight = torch.randn(
                self.config.hidden_size, n_ih, dtype=torch.bfloat16, device='cuda') * 0.02

        q_I = torch.nn.functional.linear(c_Q, self._q_up_weight.T)  # [T, n_ih * c_i] BF16
        w_h = torch.nn.functional.linear(h_t, self._w_head_weight.T).float()  # [T, n_ih] FP32

        view = cache.read_indexer_view()
        return run_indexer_score_topk(
            q_I=q_I,
            w_h=w_h,
            indexer_view=view,
            num_heads=self.config.indexer_num_heads,
            head_dim=self.config.indexer_head_dim,
            top_k=self.config.csa_top_k,
            entries_per_block=cache.paged.schema.entries_per_block,
        )
