import re

import rerun as rr

from common.message import LogLevel, Message
from common.message.rerun import RerunConvertible

CHANNEL_PATTERN = re.compile(r"^[a-zA-Z0-9](?:[a-zA-Z0-9_.\-]*(?:/[a-zA-Z0-9][a-zA-Z0-9_.\-]*)*)?$")

LEVEL_MAP: dict[LogLevel, rr.TextLogLevel] = {
    LogLevel.DEBUG: rr.TextLogLevel.DEBUG,
    LogLevel.INFO: rr.TextLogLevel.INFO,
    LogLevel.WARNING: rr.TextLogLevel.WARN,
    LogLevel.ERROR: rr.TextLogLevel.ERROR,
}


class Logger:
    """
    Logger that writes structured logs and telemetry to Rerun.

    Logs to the global default RecordingStream, which is set up by
    ``Scenario`` with the appropriate sinks (gRPC for live viewing, file
    for .rrd persistence). Time is tracked as a single ``time_us``
    timeline (logical execution time in microseconds).

    The logger is intentionally simple -- it knows nothing about
    recordings, sinks, or scenario lifecycle. Those concerns belong to
    ``Scenario``.
    """

    def __init__(
        self,
        base_channel: str | None = None,
        debug: bool = False,
        print_logs: bool = True,
    ) -> None:
        """
        Initialize the logger.

        :param base_channel: Optional base channel prefix for all entity paths.
        :param debug: Whether to emit debug-level logs.
        :param print_logs: Whether to also print logs to stdout.
        """

        self._base_channel = base_channel
        self._debug = debug
        self._print_logs = print_logs
        self._time_us: int = 0

    def set_time_us(self, time_us: int) -> None:
        """
        Set the logical execution time.

        Updates both the internal timestamp and the global Rerun
        ``sim_time`` timeline so that any subsequent ``rr.log()``
        call (whether through this logger or direct) lands at the
        correct time.

        :param time_us: Logical execution time in microseconds.
        """

        self._time_us = time_us
        self._set_time()

    def debug(self, message: str) -> None:
        """
        Log a debug message.

        :param message: Log message.
        """

        if self._debug:
            self._log(LogLevel.DEBUG, message)

    def info(self, message: str) -> None:
        """
        Log an info message.

        :param message: Log message.
        """

        self._log(LogLevel.INFO, message)

    def warning(self, message: str) -> None:
        """
        Log a warning message.

        :param message: Log message.
        """

        self._log(LogLevel.WARNING, message)

    def error(self, message: str) -> None:
        """
        Log an error message.

        :param message: Log message.
        """

        self._log(LogLevel.ERROR, message)

    def telemetry(self, channel: str, telemetry: Message | dict) -> None:
        """
        Record telemetry data directly to Rerun.

        If the message implements ``RerunConvertible``, its ``to_rerun()``
        archetype is logged. Otherwise, numeric dict values are logged as
        individual scalars and non-numeric values as text.

        :param channel: Entity path suffix (alphanumeric with slashes).
        :param telemetry: The message or dict to record.
        :raises ValueError: If channel format is invalid.
        """

        if not CHANNEL_PATTERN.match(channel):
            raise ValueError(
                f"Invalid channel '{channel}': must be alphanumeric with underscore/hyphen/period/slash, start with alphanumeric"
            )

        entity_path = f"{self._base_channel}/{channel}" if self._base_channel else channel
        self._set_time()

        # Typed messages with Rerun conversion
        if isinstance(telemetry, RerunConvertible):
            rerun_data = telemetry.to_rerun()
            if rerun_data is None:
                return
            # FrameTransforms returns a list of (suffix, archetype) tuples
            # that need to be logged at separate entity paths
            if isinstance(rerun_data, list):
                for suffix, archetype in rerun_data:
                    rr.log(f"{entity_path}/{suffix}", archetype)
            else:
                rr.log(entity_path, rerun_data)
            return

        # Dict telemetry: log numeric values as individual scalars
        if isinstance(telemetry, dict):
            logged_any = False
            for key, value in telemetry.items():
                if isinstance(value, (int, float)) and not isinstance(value, bool):
                    rr.log(f"{entity_path}/{key}", rr.Scalars(scalars=[float(value)]))
                    logged_any = True
            if logged_any:
                return
            rr.log(entity_path, rr.TextLog(str(telemetry)))
            return

        # Fallback: log as text
        rr.log(entity_path, rr.TextLog(str(telemetry)))

    def close(self) -> None:
        """
        No-op. Recording lifecycle is managed by Scenario.
        """

        pass

    def _log(self, level: LogLevel, message: str) -> None:
        """
        Log a text message to Rerun.

        :param level: The log level.
        :param message: The log message.
        """

        entity_path = f"{self._base_channel}/logs" if self._base_channel else "logs"
        self._set_time()
        rr.log(entity_path, rr.TextLog(message, level=LEVEL_MAP[level]))
        if self._print_logs:
            print(f"[{level.value.upper()}] {message}")

    def _set_time(self) -> None:
        """
        Set the Rerun timeline to the current logical execution time.

        Uses a single ``sim_time`` duration timeline. The internal
        ``_time_us`` value is in microseconds; Rerun's ``duration``
        parameter expects seconds.
        """

        rr.set_time("sim_time", duration=self._time_us / 1e6)
