"""Model + machine parameters for the analytical visualization tool.

All numbers are per-rank / per-PE unless noted. bytes are counted in
bytes (b), FLOPs in raw ops, bandwidth in bytes/second, latency in
seconds. Convert to μs / GB / GFLOP at display time only.
"""
from __future__ import annotations

from dataclasses import dataclass, field


@dataclass
class ModelConfig:
    """Transformer model dimensions."""
    name: str = "Qwen 3 8B"
    hidden: int = 4096
    ffn_dim: int = 12288
    h_q: int = 32               # Q heads
    h_kv: int = 8               # KV heads
    d_head: int = 128
    layers: int = 36
    bytes_per_elem: int = 2     # bf16

    @property
    def group_size(self) -> int:
        """GQA group size = H_q / H_kv."""
        return self.h_q // self.h_kv

    @property
    def head_dim_total_q(self) -> int:
        return self.h_q * self.d_head

    @property
    def head_dim_total_kv(self) -> int:
        return self.h_kv * self.d_head


@dataclass
class TopologyConfig:
    """Physical mapping — DP × PP × CP × TP (× EP for MoE)."""
    cp: int = 4                 # inter-cube ring size (sequence shard)
    tp: int = 8                 # PEs per cube (head/hidden shard)
    pp: int = 1                 # pipeline stages (layer shard)
    dp: int = 1                 # data-parallel replicas
    ep: int = 1                 # expert-parallel degree (MoE only)
    s_kv: int = 4096
    mode: str = "decode"        # "decode" or "prefill"
    kv_shard_mode: str = "split"  # "split" or "replicate" (used when TP > H_kv)
    # FFN sharding scope: default TP+CP (across cube group) trades a small
    # extra AllReduce for lower per-PE weight bytes.
    ffn_shard_scope: str = "TP+CP"   # "TP" | "TP+CP" | "TP+CP+DP"
    # Inter-SIP interconnect: how physical SIPs are wired together.
    # "ring" (1D wrap), "mesh2d" (grid, no wrap), "torus2d" (grid + wrap).
    sip_topology: str = "ring"
    # Placement of parallelism dims on the hierarchy.
    # "pe"   -> dim lives on PEs within a single cube (intra-cube).
    # "cube" -> dim lives across cubes (inter-cube).
    tp_placement: str = "pe"     # default: TP fills PEs of a cube
    cp_placement: str = "cube"   # default: CP fills cubes of a SIP
    # CP ring variant: what gets passed around the CP ring each hop.
    # "kv"    -> K and V shards rotate (Q,O,m,l stay local). Bytes/hop scales
    #            with S_local * H_kv * d_h. Good for prefill (large T_q).
    # "qoml"  -> Q and running (O, m, l) rotate; K,V stay local. Bytes/hop
    #            scales with T_q * H_q * d_h. Cheap for decode (T_q=1).
    cp_ring_variant: str = "kv"
    pes_per_cube_hw: int = 8    # SIP hardware constant
    cubes_per_sip_hw: int = 16

    @property
    def pes_per_stage(self) -> int:
        """PEs per pipeline stage (all stages have the same size)."""
        return self.cp * self.tp

    @property
    def pes_per_replica(self) -> int:
        """PEs per one full model replica (CP*TP*PP)."""
        return self.cp * self.tp * self.pp

    @property
    def total_pes(self) -> int:
        return self.pes_per_replica * self.dp

    # ── Placement-aware layout ────────────────────────────────
    @property
    def intra_cube_dims(self) -> int:
        """Product of dims placed on PE level (must fit in one cube, else spills)."""
        n = 1
        if self.tp_placement == "pe":
            n *= self.tp
        if self.cp_placement == "pe":
            n *= self.cp
        return n

    @property
    def inter_cube_dims(self) -> int:
        """Product of dims placed at cube level = distinct cube groups per stage."""
        n = 1
        if self.tp_placement == "cube":
            n *= self.tp
        if self.cp_placement == "cube":
            n *= self.cp
        return n

    @property
    def pes_per_cube_used(self) -> int:
        """How many PEs are live in each cube."""
        return min(self.intra_cube_dims, self.pes_per_cube_hw)

    @property
    def cubes_per_stage(self) -> int:
        """Cubes needed per PP stage under current placement.
        If intra-cube demand exceeds PEs/cube, the intra dim spills to cubes."""
        spill = max(1, (self.intra_cube_dims + self.pes_per_cube_hw - 1)
                    // self.pes_per_cube_hw)
        return self.inter_cube_dims * spill

    @property
    def cubes_used(self) -> int:
        """Physical cubes used across all PP stages and DP replicas."""
        return self.cubes_per_stage * self.pp * self.dp

    @property
    def sips_used(self) -> int:
        """Number of SIPs used (each SIP = pes_per_cube_hw × cubes_per_sip_hw)."""
        cubes_per_sip = self.cubes_per_sip_hw
        return max(1, (self.cubes_used + cubes_per_sip - 1) // cubes_per_sip)

    @property
    def placement_valid(self) -> bool:
        """False when intra-cube demand exceeds one cube (dim spills to cubes)."""
        return self.intra_cube_dims <= self.pes_per_cube_hw

    @property
    def tp_spans_cubes(self) -> int:
        """How many cubes one TP group spans (>=1). Depends on placement:
        - tp_placement=pe: 1 if TP fits in a cube, else spill.
        - tp_placement=cube: TP itself is the number of cubes per TP group.
        """
        if self.tp_placement == "cube":
            return max(1, self.tp)
        return max(1, (self.tp + self.pes_per_cube_hw - 1) // self.pes_per_cube_hw)

    @property
    def cp_intra_sip_hops(self) -> int:
        """CP ring hops that stay within one SIP."""
        if self.cp <= 1:
            return 0
        sips_in_ring = self.sips_used  # one PP stage's CP ring may span SIPs
        return max(0, (self.cp - 1) - (sips_in_ring - 1))

    @property
    def cp_inter_sip_hops(self) -> int:
        """CP ring hops that cross SIP boundaries."""
        if self.cp <= 1:
            return 0
        # Approximation: one inter-SIP hop per SIP boundary in the ring.
        # For CP=32 across 2 SIPs → 1 inter-SIP + 30 intra-SIP hops.
        return max(0, self.sips_used - 1)

    @property
    def T_q(self) -> int:
        return 1 if self.mode == "decode" else self.s_kv // self.cp

    @property
    def s_local(self) -> int:
        return self.s_kv // self.cp

    @property
    def layers_per_stage(self) -> str:
        """Layers per PP stage. Requires model.layers to render — leave as fn."""
        return "N_layers / PP"

    def tp_link_tier(self) -> str:
        """Return which BW tier a TP AllReduce uses: 'intra' | 'inter' | 'intersip'.
        - tp_placement=pe and TP fits in one cube -> 'intra'
        - tp_placement=cube and TP fits in one SIP -> 'inter'
        - otherwise -> 'intersip'
        """
        if self.tp_placement == "pe":
            if self.tp <= self.pes_per_cube_hw:
                return "intra"
            # spilled to multiple cubes: cross-cube
            if self.tp_spans_cubes <= self.cubes_per_sip_hw:
                return "inter"
            return "intersip"
        else:  # tp_placement == "cube"
            if self.tp <= self.cubes_per_sip_hw:
                return "inter"
            return "intersip"

    def cp_link_tier(self) -> str:
        """Return BW tier for the CP ring: 'intra' | 'inter' | 'intersip'."""
        if self.cp_placement == "pe":
            return "intra"
        # cp_placement == "cube"
        if self.cp <= self.cubes_per_sip_hw:
            return "inter"
        return "intersip"


@dataclass
class MachineParams:
    """SIP hardware parameters — all user-adjustable via sliders.

    Bandwidth tiers, from fastest to slowest:
      HBM (per-PE local memory)
        > intra-cube (PE↔PE on same die)
        > inter-cube (UCIe D2D between dies on same SIP)
        > inter-SIP (C2C / RDMA-class, across SIPs / servers)
    """
    # Per-PE compute peak (TFLOPs f16).
    peak_tflops_f16: float = 8.0
    # Per-PE HBM budget (GB) — the memory ceiling per PE.
    pe_hbm_gb: float = 6.0
    # HBM per-PE bandwidth (GB/s).
    bw_hbm_gbs: float = 256.0
    # Intra-cube PE↔PE link BW (GB/s).
    bw_intra_gbs: float = 512.0
    # Inter-cube (UCIe D2D) BW (GB/s).
    bw_inter_gbs: float = 128.0
    # Inter-SIP (C2C / RDMA) BW (GB/s).
    bw_intersip_gbs: float = 50.0
    # Per-hop latencies (ns).
    alpha_intra_ns: float = 20.0
    alpha_inter_ns: float = 100.0
    alpha_intersip_ns: float = 1000.0
    # Achievable utilization for compute-bound stages (0-1).
    compute_util: float = 0.8

    @property
    def peak_flops(self) -> float:
        return self.peak_tflops_f16 * 1e12

    @property
    def bw_hbm(self) -> float:
        return self.bw_hbm_gbs * 1e9

    @property
    def bw_intra(self) -> float:
        return self.bw_intra_gbs * 1e9

    @property
    def bw_inter(self) -> float:
        return self.bw_inter_gbs * 1e9

    @property
    def bw_intersip(self) -> float:
        return self.bw_intersip_gbs * 1e9

    @property
    def alpha_intra(self) -> float:
        return self.alpha_intra_ns * 1e-9

    @property
    def alpha_inter(self) -> float:
        return self.alpha_inter_ns * 1e-9

    @property
    def alpha_intersip(self) -> float:
        return self.alpha_intersip_ns * 1e-9

    @property
    def pe_budget_bytes(self) -> int:
        return int(self.pe_hbm_gb * 1e9)


@dataclass
class FullConfig:
    """Bundled config for one analysis run."""
    model: ModelConfig = field(default_factory=ModelConfig)
    topo: TopologyConfig = field(default_factory=TopologyConfig)
    machine: MachineParams = field(default_factory=MachineParams)

    @property
    def h_q_per_pe(self) -> int:
        return max(1, self.model.h_q // self.topo.tp)

    @property
    def h_kv_per_pe(self) -> float:
        """Fractional if TP > H_kv (head-dim split needed)."""
        return self.model.h_kv / self.topo.tp

    @property
    def d_per_pe(self) -> int:
        return self.model.hidden // self.topo.tp

    @property
    def ffn_per_pe(self) -> int:
        return self.model.ffn_dim // self.topo.tp

    @property
    def kv_replication_needed(self) -> bool:
        """True if TP > H_kv (each KV head shared across TP/H_kv ranks)."""
        return self.topo.tp > self.model.h_kv

    @property
    def head_dim_split_factor(self) -> int:
        """Number of TP ranks sharing one head (via d_head split)."""
        return max(1, self.topo.tp // self.model.h_kv)

    @property
    def ffn_shard_divisor(self) -> int:
        """How much the FFN dim is sharded across ranks."""
        scope = self.topo.ffn_shard_scope
        d = self.topo.tp
        if "CP" in scope:
            d *= self.topo.cp
        if "DP" in scope:
            d *= self.topo.dp
        return max(1, d)
