from __future__ import annotations

import asyncio
from datetime import date, datetime, time
from decimal import Decimal
import json
from typing import Any, Mapping

from .server import DEFAULT_PH_NODE, DEFAULT_TEMP_NODE, DEFAULT_TIMESTAMP_NODE

DEFAULT_NODE_MAP: dict[str, str] = {
    "pH": DEFAULT_PH_NODE,
    "temperature": DEFAULT_TEMP_NODE,
    "timestamp": DEFAULT_TIMESTAMP_NODE,
}


def _json_compatible(value: Any) -> Any:
    if value is None or isinstance(value, (bool, int, float, str)):
        return value
    if isinstance(value, Decimal):
        return float(value)
    if isinstance(value, bytes):
        return value.decode("utf-8", errors="replace")
    if isinstance(value, (datetime, date, time)):
        return value.isoformat()
    if isinstance(value, Mapping):
        return {str(key): _json_compatible(item) for key, item in value.items()}
    if isinstance(value, (list, tuple, set)):
        return [_json_compatible(item) for item in value]
    return str(value)


def _normalize_nodes(nodes: Mapping[str, str] | None) -> dict[str, str]:
    if nodes is None:
        return dict(DEFAULT_NODE_MAP)

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

    if not normalized:
        raise ValueError("at least one node mapping is required")
    return normalized


async def read_nodes_once(
    endpoint: str,
    nodes: Mapping[str, str] | None = None,
    *,
    timeout: float = 1.0,
) -> dict[str, Any]:
    try:
        from asyncua import Client
    except ImportError as exc:  # pragma: no cover
        raise RuntimeError("asyncua is required; install cf-opcua-server") from exc

    endpoint_text = endpoint.strip()
    if not endpoint_text:
        raise ValueError("endpoint must be a non-empty string")

    normalized_nodes = _normalize_nodes(nodes)
    client = Client(url=endpoint_text, timeout=timeout)

    async with client:
        payload: dict[str, Any] = {}
        for key, node_id in normalized_nodes.items():
            node = client.get_node(node_id)
            value = await node.read_value()
            payload[key] = _json_compatible(value)
        return payload


def read_nodes_once_sync(
    endpoint: str,
    nodes: Mapping[str, str] | None = None,
    *,
    timeout: float = 1.0,
) -> dict[str, Any]:
    return asyncio.run(read_nodes_once(endpoint, nodes, timeout=timeout))


async def read_nodes_once_json(
    endpoint: str,
    nodes: Mapping[str, str] | None = None,
    *,
    timeout: float = 1.0,
    compact: bool = True,
) -> str:
    payload = await read_nodes_once(endpoint, nodes, timeout=timeout)
    if compact:
        return json.dumps(payload, separators=(",", ":"), sort_keys=True)
    return json.dumps(payload, indent=2, sort_keys=True)


def read_nodes_once_json_sync(
    endpoint: str,
    nodes: Mapping[str, str] | None = None,
    *,
    timeout: float = 1.0,
    compact: bool = True,
) -> str:
    payload = read_nodes_once_sync(endpoint, nodes, timeout=timeout)
    if compact:
        return json.dumps(payload, separators=(",", ":"), sort_keys=True)
    return json.dumps(payload, indent=2, sort_keys=True)
