"""Core simulation orchestrator for the discrete-event simulation engine.

This module implements the main simulation loop using a pop-invoke-push pattern:
events are popped from a priority heap, invoked to perform their work, and any
resulting events are pushed back for future processing.
"""

import logging

from happysimulator.core.entity import Entity
from happysimulator.core.event import Event
from happysimulator.core.protocols import Simulatable
from happysimulator.core.event_heap import EventHeap
from happysimulator.core.clock import Clock
from happysimulator.core.temporal import Instant
from happysimulator.load.source import Source
from happysimulator.instrumentation.recorder import TraceRecorder, NullTraceRecorder

logger = logging.getLogger(__name__)


class Simulation:
    """Orchestrates discrete-event simulation execution.

    The simulation maintains a central event heap and advances time by processing
    events in chronological order. It supports two termination modes:

    - Explicit: Run until `end_time` is reached
    - Auto: Run until no primary (non-daemon) events remain, allowing probes
      to keep running without blocking termination

    All entities share a common Clock instance, ensuring consistent time views
    across the simulation. Sources and probes are bootstrapped during initialization,
    priming the heap with their first events.

    Args:
        start_time: When the simulation begins. Defaults to Instant.Epoch (t=0).
        end_time: When to stop processing. Defaults to Instant.Infinity (auto-terminate).
        sources: Load generators that produce events at specified intervals.
        entities: Simulation actors that respond to events.
        probes: Measurement sources that run as daemons (won't block termination).
        trace_recorder: Optional recorder for debugging/visualization.
    """
    def __init__(
        self,
        start_time: Instant = None,
        end_time: Instant = None,
        sources: list[Source] = None,
        entities: list[Simulatable] = None,
        probes: list[Source] = None,
        trace_recorder: TraceRecorder | None = None,
    ):
        self._start_time = start_time
        if self._start_time is None:
            self._start_time = Instant.Epoch
        
        self._end_time = end_time
        if self._end_time is None:
            self._end_time = Instant.Infinity
            
        self._clock = Clock(self._start_time)
        
        self._entities = entities or []
        self._sources = sources or []
        self._probes = probes or []
        
        all_components = self._entities + self._sources + self._probes
        for component in all_components:
            if isinstance(component, Simulatable):
                component.set_clock(self._clock)
        
        self._trace = trace_recorder or NullTraceRecorder()
        self._event_heap = EventHeap(trace_recorder=self._trace)
        
        logger.info(
            "Simulation initialized: start=%r, end=%r, sources=%d, entities=%d, probes=%d",
            self._start_time, self._end_time,
            len(self._sources), len(self._entities), len(self._probes),
        )
        
        self._trace.record(
            time=self._start_time,
            kind="simulation.init",
            num_sources=len(self._sources),
            num_entities=len(self._entities),
            num_probes=len(self._probes),
        )
        
        for source in self._sources:
            # The source calculates its first event and returns it
            initial_events = source.start(self._start_time)
            logger.debug("Source '%s' produced %d initial event(s)", source.name, len(initial_events))
            
            # We push it to the heap to prime the simulation
            for event in initial_events:
                self._event_heap.push(event)
        
        for probe in self._probes:
            initial_events = probe.start(self._start_time)
            logger.debug("Probe '%s' produced %d initial event(s)", probe.name, len(initial_events))
            for event in initial_events:
                self._event_heap.push(event)
        
        logger.debug("Initialization complete, heap size: %d", self._event_heap.size())

    @property
    def trace_recorder(self) -> TraceRecorder:
        """Access the trace recorder for inspection after simulation."""
        return self._trace

    def schedule(self, events: Event | list[Event]) -> None:
        """Inject events into the simulation from outside the event loop.

        Useful for adding events programmatically after initialization but before
        or during simulation execution.
        """
        self._event_heap.push(events)

    def run(self) -> None:
        """Execute the simulation until termination.

        Implements the core pop-invoke-push loop:
        1. Pop the earliest event from the heap
        2. Advance simulation time to that event's timestamp
        3. Invoke the event (calling its target entity or callback)
        4. Push any resulting events back onto the heap
        5. Repeat until termination condition is met

        Termination occurs when:
        - The event heap is exhausted, or
        - Current time exceeds end_time, or
        - Auto-terminate mode with no primary events remaining
        """
        current_time = self._start_time
        self._event_heap.set_current_time(current_time)
        
        logger.info("Simulation starting at %r with %d event(s) in heap", current_time, self._event_heap.size())
        
        if not self._event_heap.has_events():
            logger.warning("Simulation started with empty event heap")
        
        self._trace.record(
            time=current_time,
            kind="simulation.start",
            heap_size=self._event_heap.size(),
        )
        
        events_processed = 0
        
        while self._event_heap.has_events() and self._end_time >= current_time:
            
            # TERMINATION CHECK:
            # If we rely on auto-termination (end_time is Infinity),
            # and we have no primary events left (only probes), STOP.
            if self._end_time == Instant.Infinity and not self._event_heap.has_primary_events():
                logger.info(
                    "Auto-terminating at %r: no primary events remaining (only daemon/probe events)",
                    current_time,
                )
                self._trace.record(
                    time=current_time,
                    kind="simulation.auto_terminate",
                    reason="no_primary_events",
                )
                break
            
            # 1. Pop
            event = self._event_heap.pop()

            if event.time < current_time:
                logger.warning(
                    "Time travel detected: next event scheduled at %r, but current simulation time is %r. "
                    "event_type=%s event_id=%s",
                    event.time,
                    current_time,
                    event.event_type,
                    event.context.get("id"),
                )
            current_time = event.time  # Advance clock
            self._clock.update(current_time)
            self._event_heap.set_current_time(current_time)
            events_processed += 1
            
            logger.debug(
                "Processing event #%d: %r",
                events_processed, event,
            )
            
            self._trace.record(
                time=current_time,
                kind="simulation.dequeue",
                event_id=event.context.get("id"),
                event_type=event.event_type,
            )
            
            # 2. Invoke
            # The event itself knows how to run and what to return
            new_events = event.invoke()
            
            # 3. Push
            if new_events:
                logger.debug(
                    "Event %r produced %d new event(s)",
                    event.event_type, len(new_events),
                )
                for new_event in new_events:
                    logger.debug(
                        "  Scheduling %r for %r",
                        new_event.event_type, new_event.time,
                    )
                    self._trace.record(
                        time=current_time,
                        kind="simulation.schedule",
                        event_id=new_event.context.get("id"),
                        event_type=new_event.event_type,
                        scheduled_time=new_event.time,
                    )
                self._event_heap.push(new_events)
        
        # Determine why loop ended
        if not self._event_heap.has_events():
            logger.info("Simulation ended at %r: event heap exhausted", current_time)
        elif self._end_time < current_time:
            logger.info(
                "Simulation ended: current time %r exceeded end_time %r",
                current_time, self._end_time,
            )
        
        logger.info(
            "Simulation complete: processed %d events, final time %r, %d event(s) remaining in heap",
            events_processed, current_time, self._event_heap.size(),
        )
        
        if self._event_heap.size() > 0:
            logger.debug("Unprocessed events remain in heap (scheduled past end_time)")
        
        self._trace.record(
            time=current_time,
            kind="simulation.end",
            final_heap_size=self._event_heap.size(),
        )
        
        return

