"""Example: subscribe to a RouterOS event stream (listener).

Python port of examples/listen/main.go.

Supports two modes that mirror the Go original:

* **implicit-async** (default) – ``listen()`` auto-enables async mode internally.
* **explicit-async** (``--async``) – caller enables async mode first.

Usage::

    uv run python -m routeros.examples.listen.main --help

    # Default: implicit async, listen to address-list events, cancel after 10 s
    uv run python -m routeros.examples.listen.main \\
        --address 192.168.1.1:8728 \\
        --username admin \\
        --password ""

    # Explicit async mode
    uv run python -m routeros.examples.listen.main --async

    # Different command
    uv run python -m routeros.examples.listen.main \\
        --command "/interface/listen"

    # Longer timeout
    uv run python -m routeros.examples.listen.main --timeout 30

    # TLS + debug
    uv run python -m routeros.examples.listen.main --tls --debug \\
        --address 192.168.1.1:8729

Press Ctrl+C to stop at any time (in addition to the auto-timeout).
"""

from __future__ import annotations

import argparse
import logging
import signal
import ssl
import sys
import threading

import routeros


def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(
        description="Subscribe to a RouterOS event stream.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    p.add_argument("--debug",    action="store_true",                         help="Enable DEBUG logging")
    p.add_argument("--address",  default="127.0.0.1:8728",                    help="RouterOS address:port")
    p.add_argument("--username", default="admin",                              help="Username")
    p.add_argument("--password", default="",                                   help="Password")
    p.add_argument("--command",  default="/ip/firewall/address-list/listen",   help="RouterOS listen command")
    p.add_argument("--timeout",  type=float, default=10.0,                     help="Auto-cancel after N seconds (0 = no timeout)")
    p.add_argument("--async",    dest="use_async", action="store_true",        help="Enable explicit async mode")
    p.add_argument("--tls",      action="store_true",                          help="Use TLS/SSL connection")
    return p


def dial(args: argparse.Namespace, log: logging.Logger) -> routeros.Client:
    if args.tls:
        ctx = ssl.create_default_context()
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE
        return routeros.dial_tls(args.address, args.username, args.password,
                                 tls_context=ctx, logger=log)
    return routeros.dial(args.address, args.username, args.password, logger=log)


# ---------------------------------------------------------------------------
# Implicit-async mode (mirrors implicitAsync in Go)
# listener itself triggers async mode automatically
# ---------------------------------------------------------------------------

def implicit_async(c: routeros.Client, args: argparse.Namespace, log: logging.Logger) -> None:
    words = args.command.split()
    listener = c.listen_args(words, queue_size=100)

    stop_event = threading.Event()

    def _cancel_after_timeout():
        stop_event.wait(timeout=args.timeout)
        if not stop_event.is_set():
            log.debug("Cancelling the RouterOS command (timeout)...")
            listener.cancel()

    if args.timeout > 0:
        t = threading.Thread(target=_cancel_after_timeout, daemon=True)
        t.start()

    log.info("Waiting for events (Ctrl+C or timeout to stop)...")
    try:
        for sentence in listener:
            log.info("Update: %s", sentence)
    finally:
        stop_event.set()

    if listener.err:
        log.error("received an error: %s", listener.err)
        sys.exit(2)


# ---------------------------------------------------------------------------
# Explicit-async mode (mirrors explicitAsync in Go)
# caller calls async_start() first; listener is created in a separate thread
# ---------------------------------------------------------------------------

def explicit_async(c: routeros.Client, args: argparse.Namespace, log: logging.Logger) -> None:
    log.debug("Running explicit_async mode...")
    c.async_start()

    done_event = threading.Event()
    error_holder: list[Exception] = []

    def _listen_thread():
        words = args.command.split()
        try:
            listener = c.listen_args(words, queue_size=100)
        except routeros.RouterOSError as exc:
            log.error("could not listen: %s", exc)
            error_holder.append(exc)
            done_event.set()
            return

        def _cancel_after_timeout():
            import time
            time.sleep(args.timeout)
            log.debug("Cancelling the RouterOS command...")
            try:
                listener.cancel()
            except Exception as exc:
                log.error("could not cancel: %s", exc)
            done_event.set()

        if args.timeout > 0:
            threading.Thread(target=_cancel_after_timeout, daemon=True).start()

        log.info("Waiting for !re events...")
        for sentence in listener:
            log.info("Update: %s", sentence)

        if listener.err:
            error_holder.append(listener.err)

        done_event.set()

    threading.Thread(target=_listen_thread, daemon=True).start()
    done_event.wait()

    if error_holder:
        log.error("received an error: %s", error_holder[0])
        sys.exit(2)


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main() -> None:
    args = build_parser().parse_args()

    level = logging.DEBUG if args.debug else logging.INFO
    logging.basicConfig(
        level=level,
        format="%(asctime)s %(levelname)-8s %(name)s  %(message)s",
        datefmt="%Y-%m-%dT%H:%M:%S",
    )
    log = logging.getLogger("routeros")

    # Graceful Ctrl+C handling (mirrors signal.NotifyContext in Go).
    interrupted = threading.Event()

    def _on_sigint(sig, frame):
        log.info("Interrupted – shutting down.")
        interrupted.set()

    signal.signal(signal.SIGINT, _on_sigint)

    try:
        c = dial(args, log)
    except Exception as exc:
        log.error("could not dial: %s", exc)
        sys.exit(2)

    with c:
        if args.use_async:
            explicit_async(c, args, log)
        else:
            implicit_async(c, args, log)


if __name__ == "__main__":
    main()
