"""PE_SCHEDULER: plan generation + tile dispatch (ADR-0021 D2).

Receives PeInternalTxn from PE_CPU, routes to engines:
  - Simple commands (DmaReadCmd, GemmCmd, etc.) → direct dispatch to engine
  - CompositeCmd → generate TilePlan, feed tiles via _feed_loop

Composite pipeline uses token self-routing (ADR-0021 D4):
  Scheduler only does initial dispatch + completion tracking.
  Tiles chain through components based on their plan's stage sequence.
"""
from __future__ import annotations

from collections.abc import Generator
from typing import TYPE_CHECKING, Any

import simpy

from kernbench.components.base import ComponentBase

if TYPE_CHECKING:
    from kernbench.common.pe_commands import PeInternalTxn
    from kernbench.components.context import ComponentContext
    from kernbench.topology.types import Node


class PeSchedulerComponent(ComponentBase):
    """PE_SCHEDULER: sole dispatcher inside a PE (ADR-0014 D1, ADR-0021 D2).

    Simple commands are forwarded to the appropriate engine.
    CompositeCmd creates a TilePlan and feeds tiles into the pipeline.

    Single _feed_loop process per scheduler ensures FIFO command ordering.
    """

    TILE_M = 32
    TILE_K = 64
    TILE_N = 32

    _CMD_DISPATCH: dict[type, str] = {}

    @classmethod
    def _ensure_dispatch_table(cls) -> None:
        if cls._CMD_DISPATCH:
            return
        from kernbench.common.pe_commands import DmaReadCmd, DmaWriteCmd, GemmCmd, MathCmd
        cls._CMD_DISPATCH = {
            DmaReadCmd: "pe_dma",
            DmaWriteCmd: "pe_dma",
            GemmCmd: "pe_gemm",
            MathCmd: "pe_math",
        }

    def __init__(self, node: Node, ctx: ComponentContext | None = None) -> None:
        super().__init__(node, ctx)
        self._pe_prefix = node.id.rsplit(".", 1)[0]
        self._ensure_dispatch_table()
        self._pending_feeds: simpy.Store | None = None
        self._pipeline_counter = 0

    def start(self, env: simpy.Environment) -> None:
        self._pending_feeds = simpy.Store(env)
        super().start(env)
        env.process(self._feed_loop(env))

    def run(self, env: simpy.Environment, nbytes: int) -> Generator:
        overhead_ns = float(self.node.attrs.get("overhead_ns", 0.0))
        yield env.timeout(overhead_ns)

    def _worker(self, env: simpy.Environment) -> Generator:
        from kernbench.common.pe_commands import PeInternalTxn

        while True:
            msg: Any = yield self._inbox.get()
            if isinstance(msg, PeInternalTxn):
                env.process(self._dispatch(env, msg))
            else:
                yield from self._forward_txn(env, msg)

    def _dispatch(self, env: simpy.Environment, pe_txn: PeInternalTxn) -> Generator:
        from kernbench.common.pe_commands import CompositeCmd, PeCpuOverheadCmd

        yield from self.run(env, 0)  # scheduler overhead

        cmd = pe_txn.command

        # Simple command dispatch
        engine_suffix = self._CMD_DISPATCH.get(type(cmd))
        if engine_suffix is not None:
            yield self.out_ports[f"{self._pe_prefix}.{engine_suffix}"].put(pe_txn)
            return

        # CompositeCmd: generate plan and feed
        if isinstance(cmd, CompositeCmd):
            yield from self._dispatch_composite(env, pe_txn, cmd)
            return

        if isinstance(cmd, PeCpuOverheadCmd):
            yield env.timeout(cmd.cycles)
            pe_txn.done.succeed()
            return

        pe_txn.done.succeed()

    def _dispatch_composite(
        self, env: simpy.Environment, pe_txn: Any, cmd: Any,
    ) -> Generator:
        """Generate plan and enqueue to feeder. Non-blocking (ADR-0021 D4)."""
        from kernbench.components.builtin.pe_types import PipelineContext

        plan = self._generate_plan(cmd)

        self._pipeline_counter += 1
        ctx = PipelineContext(
            id=f"p{self._pipeline_counter}",
            total_tiles=len(plan.tiles),
            done_event=pe_txn.done,
        )

        # Enqueue to feeder — scheduler worker returns immediately
        assert self._pending_feeds is not None
        yield self._pending_feeds.put((plan, ctx))

    def _feed_loop(self, env: simpy.Environment) -> Generator:
        """Single feeder process: FIFO command ordering (ADR-0021 D2).

        No tile feed interleaving between commands.
        Queue full → only this process blocks.
        """
        from kernbench.components.builtin.pe_types import TileToken

        assert self._pending_feeds is not None
        while True:
            plan, ctx = yield self._pending_feeds.get()
            for tile in plan.tiles:
                first_stage = tile.stages[0]
                token = TileToken(
                    tile_id=tile.tile_id,
                    pipeline_ctx=ctx,
                    plan=tile,
                    stage_idx=0,
                    params=first_stage.params,
                )
                yield self.out_ports[first_stage.component].put(token)

    def _generate_plan(self, cmd: Any) -> Any:
        """Generate a PipelinePlan from CompositeCmd."""
        from kernbench.components.builtin.tiling import (
            generate_gemm_plan,
            generate_math_plan,
        )

        pp = self._pe_prefix
        bpe = 2  # default bytes per element (f16)

        if cmd.op == "gemm" and cmd.b is not None:
            a = cmd.a
            b = cmd.b
            M, K = a.shape[-2], a.shape[-1]
            N = b.shape[-1]
            # When CompositeCmd.ops is populated, ops[0] is the head and
            # ops[1:] is the epilogue spec list. Empty ops → legacy path.
            epi_specs = tuple(cmd.ops[1:]) if cmd.ops else ()
            return generate_gemm_plan(
                M=M, K=K, N=N,
                tile_m=self.TILE_M, tile_k=self.TILE_K, tile_n=self.TILE_N,
                bytes_per_element=bpe,
                A_addr=a.addr, B_addr=b.addr, C_addr=cmd.out_addr,
                pe_prefix=pp,
                a_pinned=getattr(a, "pinned", False),
                b_pinned=getattr(b, "pinned", False),
                epilogue_specs=epi_specs,
            )
        else:
            # Math composite
            a = cmd.a
            M = a.shape[-2] if len(a.shape) >= 2 else a.shape[0]
            N = a.shape[-1] if len(a.shape) >= 2 else 1
            return generate_math_plan(
                M=M, N=N,
                tile_m=self.TILE_M, tile_n=self.TILE_N,
                bytes_per_element=bpe,
                math_op=cmd.math_op or "identity",
                src_addr=a.addr, dst_addr=cmd.out_addr,
                pe_prefix=pp,
            )
