from __future__ import annotations

import argparse
import asyncio
import json
import signal
from contextlib import suppress
import sys

from .client import DEFAULT_NODE_MAP, read_nodes_once_json_sync
from .server import (
    DEFAULT_BIND_ENDPOINT,
    DEFAULT_DEVICE_NAME,
    DEFAULT_ENDPOINT,
    DEFAULT_NAMESPACE_URI,
    DEFAULT_PH_NODE,
    DEFAULT_TEMP_NODE,
    DEFAULT_TIMESTAMP_NODE,
    run_server,
)

try:
    from cf_cli.registry import register_command
except Exception:  # pragma: no cover - optional dependency
    register_command = None


def _add_common_args(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--interval", type=float, default=1.0, help="Loop interval in seconds.")
    parser.add_argument("--duration", type=float, default=0.0, help="Total runtime in seconds (0=run until stopped).")
    parser.add_argument("--endpoint", type=str, default=DEFAULT_ENDPOINT, help="Client endpoint hint.")
    parser.add_argument("--bind-endpoint", type=str, default=DEFAULT_BIND_ENDPOINT, help="Server bind endpoint.")
    parser.add_argument("--namespace", type=str, default=DEFAULT_NAMESPACE_URI, help="Namespace URI.")
    parser.add_argument("--device-name", type=str, default=DEFAULT_DEVICE_NAME, help="Device object name.")
    parser.add_argument("--ph-node", type=str, default=DEFAULT_PH_NODE, help="pH node id string.")
    parser.add_argument("--temp-node", type=str, default=DEFAULT_TEMP_NODE, help="Temperature node id string.")
    parser.add_argument("--timestamp-node", type=str, default=DEFAULT_TIMESTAMP_NODE, help="Timestamp node id string.")


def _add_read_args(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--endpoint", type=str, default=DEFAULT_ENDPOINT, help="Client endpoint.")
    parser.add_argument(
        "--nodes",
        type=str,
        default=json.dumps(DEFAULT_NODE_MAP),
        help="JSON object mapping output keys to OPC UA node ids.",
    )
    parser.add_argument("--timeout", type=float, default=1.0, help="Client timeout in seconds.")
    parser.add_argument("--compact", action="store_true", help="Emit one compact JSON line for runtime consumers.")


def _parse_nodes_argument(raw_nodes: str) -> dict[str, str]:
    try:
        loaded = json.loads(raw_nodes)
    except json.JSONDecodeError as exc:
        raise ValueError(f"nodes must be valid JSON: {exc}") from exc

    if not isinstance(loaded, dict) or not loaded:
        raise ValueError("nodes must be a non-empty JSON object")

    normalized: dict[str, str] = {}
    for key, node_id in loaded.items():
        if not isinstance(key, str) or not key.strip():
            raise ValueError("all node mapping keys must be non-empty strings")
        if not isinstance(node_id, str) or not node_id.strip():
            raise ValueError(f"node id for '{key}' must be a non-empty string")
        normalized[key] = node_id
    return normalized


def _run_from_args(args: argparse.Namespace) -> int:
    cmd = getattr(args, "cmd", None) or "start"
    if cmd == "read":
        try:
            payload = read_nodes_once_json_sync(
                args.endpoint,
                _parse_nodes_argument(args.nodes),
                timeout=args.timeout,
                compact=args.compact,
            )
        except Exception as exc:
            print(f"OPC UA read failed: {exc}", file=sys.stderr)
            return 1
        print(payload)
        return 0
    if cmd != "start":
        return 2

    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    stop_event = asyncio.Event()

    def _request_stop() -> None:
        if not stop_event.is_set():
            print("\nStopping OPC UA server...")
            stop_event.set()

    for sig in (signal.SIGINT, signal.SIGTERM):
        with suppress(ValueError, NotImplementedError):
            loop.add_signal_handler(sig, _request_stop)

    async def _main() -> None:
        await run_server(
            loop_interval=args.interval,
            endpoint=args.endpoint,
            bind_endpoint=args.bind_endpoint,
            namespace_uri=args.namespace,
            device_name=args.device_name,
            ph_node=args.ph_node,
            temp_node=args.temp_node,
            timestamp_node=args.timestamp_node,
            duration=args.duration if args.duration and args.duration > 0 else None,
            stop_event=stop_event,
        )

    try:
        loop.run_until_complete(_main())
    finally:
        loop.close()
    return 0


def _register_cf_cli() -> None:
    if register_command is None:
        return

    def _add_server_parser(subparsers: argparse._SubParsersAction) -> argparse.ArgumentParser:
        parser = subparsers.add_parser("server", help="Virtual OPC-UA server")
        _add_common_args(parser)
        server_subparsers = parser.add_subparsers(dest="cmd", required=False)
        start_cmd = server_subparsers.add_parser("start", help="Start the virtual OPC-UA server.")
        _add_common_args(start_cmd)
        parser.set_defaults(_cf_handler=_run_from_args)
        start_cmd.set_defaults(_cf_handler=_run_from_args, cmd="start")
        return parser

    def _add_read_parser(subparsers: argparse._SubParsersAction) -> argparse.ArgumentParser:
        parser = subparsers.add_parser("read", help="Read OPC-UA node values once.")
        _add_read_args(parser)
        parser.set_defaults(_cf_handler=_run_from_args, cmd="read")
        return parser

    register_command(
        "opcua",
        "server",
        _add_server_parser,
        group_help="OPC-UA tools",
        command_help="Run the virtual OPC-UA server",
    )
    register_command(
        "opcua",
        "read",
        _add_read_parser,
        group_help="OPC-UA tools",
        command_help="Read OPC-UA node values once",
    )


def main() -> int:
    parser = argparse.ArgumentParser(description="Cogniflow OPC UA tools")
    subparsers = parser.add_subparsers(dest="cmd", required=False)

    start_cmd = subparsers.add_parser("start", help="Start the virtual OPC-UA server.")
    _add_common_args(start_cmd)
    read_cmd = subparsers.add_parser("read", help="Read OPC-UA node values once.")
    _add_read_args(read_cmd)

    args = parser.parse_args()
    if args.cmd is None:
        args = parser.parse_args(["start"])

    cmd = args.cmd

    if cmd not in {"start", "read"}:
        parser.print_help()
        return 0

    return _run_from_args(args)


if __name__ == "__main__":
    raise SystemExit(main())

_register_cf_cli()
