from __future__ import annotations

import re
import sys
import time
from typing import Any, Callable

import grpc
import socketio  # type: ignore[import-untyped]
from socketio.exceptions import ConnectionError as SocketIoConnectionError  # type: ignore[import-untyped]

from remotivelabs.cli.broker.lib.client import BrokerException, Client, SignalIdentifier, SignalsInFrame
from remotivelabs.cli.settings import settings
from remotivelabs.cli.utils.console import print_generic_error, print_generic_message, print_success, print_unformatted_to_stderr

PP_CONNECT_APP_NAME = "RemotiveBridge"


def apply_signal_transform(name: str, expression: str | None) -> str:
    """
    Apply safe string transformations to signal names.

    Supports chained operations like: lower().replace(".", "_")
    Supported operations: lower(), upper(), replace("x", "y")
    """
    if expression is None:
        return name

    result = name
    # Split on ")." to handle chained method calls, keeping the closing paren
    # e.g., 'lower().replace(".", "_")' -> ['lower()', 'replace(".", "_")']
    parts = re.split(r"\)\.", expression)
    operations = [p if p.endswith(")") else p + ")" for p in parts if p]

    for operation in operations:
        op = operation.strip()
        if op == "lower()":
            result = result.lower()
        elif op == "upper()":
            result = result.upper()
        elif op.startswith("replace("):
            # Parse replace("x", "y") - handles both single and double quotes
            match = re.match(r'replace\(["\'](.+?)["\'],\s*["\'](.*)["\']\)', op)
            if match:
                old, new = match.groups()
                result = result.replace(old, new)
            else:
                print_generic_error(f"Invalid replace expression: {op}")
                print_generic_error('Expected format: replace("old", "new")')
                sys.exit(1)
        elif op:
            print_generic_error(f"Unsupported transform operation: {op}")
            print_generic_error('Supported operations: lower(), upper(), replace("x", "y")')
            sys.exit(1)

    return result


class ProtoPieBridge:
    """Bridge between RemotiveBroker signals and ProtoPie Connect."""

    def __init__(
        self,
        broker_url: str,
        pp_address: str,
        api_key: str | None = None,
        signal_mappings: dict[str, list[str]] | None = None,
        expression: str | None = None,
    ):
        self.broker_url = broker_url
        self.pp_address = pp_address
        self.api_key = self._resolve_api_key(api_key, broker_url)
        self.signal_mappings = signal_mappings or {}
        self.expression = expression

        self._io: socketio.Client = socketio.Client()
        self._is_connected = False
        self._has_received_signal = False
        self._subscription: Any = None

        self._setup_handlers()

    @staticmethod
    def _resolve_api_key(api_key: str | None, broker_url: str) -> str | None:
        """Resolve API key, reading from settings if needed for HTTPS connections."""
        if broker_url.startswith("https"):
            if api_key is None:
                print_generic_message("No --api-key, reading token from file")
                return settings.get_active_token()
            return api_key
        return api_key

    def _setup_handlers(self) -> None:
        """Configure socket.io event handlers."""

        @self._io.on("connect")  # type: ignore[misc]
        def on_connect() -> None:
            print_success("Connected to ProtoPie Connect")
            self._io.emit("ppBridgeApp", {"name": PP_CONNECT_APP_NAME})
            self._io.emit("PLUGIN_STARTED", {"name": PP_CONNECT_APP_NAME})
            self._is_connected = True

    def _emit_signal(self, signal_name: str, value: str) -> None:
        """Send a signal value to ProtoPie Connect."""
        transformed_name = apply_signal_transform(signal_name, self.expression)
        self._io.emit("ppMessage", {"messageId": transformed_name, "value": value})

    def _create_signal_callback(self) -> Callable[[SignalsInFrame], None]:
        """Create callback for handling incoming broker signals."""

        def on_signals(frame: SignalsInFrame) -> None:
            if not self._has_received_signal:
                print_generic_message("Bridge-app is properly receiving signals, you are good to go :thumbsup:")
                self._has_received_signal = True

            for signal in frame:
                value = str(signal.value())
                name = signal.name()

                # Check for mapTo in signal mappings
                mapped_names = self.signal_mappings.get(name)
                if mapped_names:
                    for mapped_name in mapped_names:
                        self._emit_signal(mapped_name, value)
                else:
                    self._emit_signal(name, value)

        return on_signals

    def _subscribe_to_broker(self, signals: list[SignalIdentifier], on_change_only: bool) -> None:
        """Connect to broker and subscribe to signals."""
        try:
            print_generic_message("Connecting and subscribing to broker...")

            client = Client(client_id="cli")
            client.connect(url=self.broker_url, api_key=self.api_key)
            client.on_signals = self._create_signal_callback()

            if not signals:
                print_generic_error("No signals to subscribe to")
                return

            self._subscription = client.subscribe(
                signals_to_subscribe_to=signals,
                changed_values_only=on_change_only,
            )
            print_generic_message("Subscription to broker completed")
            print_generic_message("Waiting for signals...")

            # Keep running until interrupted
            while True:
                time.sleep(1)

        except grpc.RpcError as e:
            print_generic_error("Problems connecting or subscribing")
            if isinstance(e, grpc.Call):
                print_generic_error(f"{e.code()} - {e.details()}")
            else:
                print_generic_error(str(e))

        except BrokerException as e:
            print_generic_error(str(e))
            self._cancel_subscription()

        except KeyboardInterrupt:
            print_generic_message("Keyboard interrupt received. Closing subscription.")
            self._cancel_subscription()

        except Exception as e:
            print_generic_error(str(e))

    def _cancel_subscription(self) -> None:
        """Cancel active subscription if present."""
        if self._subscription is not None:
            self._subscription.cancel()

    def connect(self, signals: list[SignalIdentifier], on_change_only: bool = True) -> None:
        """Connect to ProtoPie Connect and start bridging signals."""
        try:
            self._io.connect(self.pp_address)

            # Wait for ProtoPie connection
            while not self._is_connected:
                time.sleep(0.1)

            self._subscribe_to_broker(signals, on_change_only)

        except SocketIoConnectionError as e:
            print_generic_error("Failed to connect to ProtoPie Connect")
            print_unformatted_to_stderr(e)
            sys.exit(1)

        except Exception as e:
            print_generic_error("Unexpected error")
            print_unformatted_to_stderr(e)
            sys.exit(1)


def do_connect(  # noqa: PLR0913
    address: str,
    broker_url: str,
    api_key: str | None,
    signals: list[SignalIdentifier],
    signal_mappings: dict[str, list[str]] | None = None,
    expression: str | None = None,
    on_change_only: bool = True,
) -> None:
    """
    Create a ProtoPie bridge and start connecting signals.

    This is the main entry point for the protopie command.
    """
    bridge = ProtoPieBridge(
        broker_url=broker_url,
        pp_address=address,
        api_key=api_key,
        signal_mappings=signal_mappings,
        expression=expression,
    )
    bridge.connect(signals, on_change_only)
