# -*- coding: utf-8 -*-
"""Erlang node status cache and probes."""

from .shared import *
from .erlang import _ensure_epmd_daemon, get_erl_cmd, get_local_ip

class NodeStatusCache:
    """节点状态缓存
    
    全局缓存所有已查询节点的状态，避免重复查询
    """
    _instance = None
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._cache = {}  # {node_name: {'online': bool, 'timestamp': float}}
            cls._instance._initialized = False
        return cls._instance
    
    def get(self, node_name: str) -> Optional[bool]:
        """获取节点状态（从缓存）
        
        Args:
            node_name: 节点名称
        
        Returns:
            True=在线, False=离线, None=未缓存
        """
        if node_name in self._cache:
            return self._cache[node_name]['online']
        return None
    
    def set(self, node_name: str, online: bool):
        """设置节点状态
        
        Args:
            node_name: 节点名称
            online: 是否在线
        """
        self._cache[node_name] = {
            'online': online,
            'timestamp': time.time()
        }
    
    def set_online(self, node_name: str):
        """设置节点为在线状态"""
        self.set(node_name, True)
    
    def set_offline(self, node_name: str):
        """设置节点为离线状态"""
        self.set(node_name, False)
    
    def batch_set(self, results: Dict[str, bool]):
        """批量设置节点状态
        
        Args:
            results: {node_name: is_online}
        """
        for node_name, online in results.items():
            self.set(node_name, online)
    
    def get_all(self) -> Dict[str, bool]:
        """获取所有缓存的节点状态
        
        Returns:
            {node_name: is_online}
        """
        return {name: data['online'] for name, data in self._cache.items()}
    
    def has_local_nodes(self, local_nodes: List[str]) -> bool:
        """检查是否有本地节点的缓存
        
        Args:
            local_nodes: 本地节点名称列表
        
        Returns:
            True 如果所有本地节点都有缓存
        """
        for node in local_nodes:
            if node not in self._cache:
                return False
        return len(local_nodes) > 0
    
    def clear(self):
        """清空缓存"""
        self._cache.clear()
    
    def is_initialized(self) -> bool:
        """检查是否已初始化（是否有缓存数据）"""
        return self._initialized
    
    def set_initialized(self, value: bool = True):
        """设置初始化状态"""
        self._initialized = value


# 全局缓存实例
_node_status_cache = NodeStatusCache()


def get_node_status_cache() -> NodeStatusCache:
    """获取全局节点状态缓存实例"""
    return _node_status_cache


def check_node_status(server_name: str, cookie: str, target_ip: str = None,
                      erl_path: str = None) -> bool:
    """检查单个 Erlang 节点是否在线
    
    使用 net_adm:ping 来检查节点状态
    
    Args:
        server_name: 服务器名称（完整节点名如 xxx@ip 或仅名称）
        cookie: Erlang cookie
        target_ip: 目标IP（如果 server_name 不包含 @ip 时使用）
        erl_path: Erlang 安装路径
    
    Returns:
        True 表示节点在线，False 表示离线
    """
    results = _check_nodes_status_via_ping([server_name], cookie, target_ip, erl_path=erl_path)
    return results.get(server_name, False)


_NODE_STATUS_BATCH_SIZE = 40
_NODE_STATUS_MAX_EVAL_CHARS = 8000
_NODE_STATUS_MAX_WORKERS = 4
_NODE_STATUS_FAST_TIMEOUT = 0.8
_NODE_STATUS_FAST_MAX_WORKERS = 48
_EPMD_PORT = 4369


def _split_node_status_batches(target_nodes: List[str]) -> List[List[str]]:
    """按数量和命令长度拆分节点检查批次。"""
    batches: List[List[str]] = []
    current_batch: List[str] = []
    current_chars = 0

    for target_node in target_nodes:
        node_chars = len(target_node) + 4  # 单引号、逗号等额外字符
        should_split = (
            current_batch and (
                len(current_batch) >= _NODE_STATUS_BATCH_SIZE
                or current_chars + node_chars > _NODE_STATUS_MAX_EVAL_CHARS
            )
        )
        if should_split:
            batches.append(current_batch)
            current_batch = []
            current_chars = 0

        current_batch.append(target_node)
        current_chars += node_chars

    if current_batch:
        batches.append(current_batch)

    return batches


def _check_nodes_status_batch(
    target_nodes: List[str],
    target_mapping: Dict[str, List[str]],
    cookie: str,
    erl_path: str,
    local_ip: str,
) -> Dict[str, bool]:
    """执行单批节点状态检查。"""
    if not target_nodes:
        return {}

    erl = get_erl_cmd(erl_path)
    ping_node = f"ping_{os.getpid()}_{time.time_ns() % 100000000}"
    nodes_str = "[" + ",".join(f"'{node}'" for node in target_nodes) + "]"
    short_name_mapping: Dict[str, List[str]] = {}
    for full_node, original_names in target_mapping.items():
        short_name = full_node.split('@', 1)[0]
        short_name_mapping.setdefault(short_name, []).extend(original_names)
    eval_code = (
        f'Nodes = {nodes_str}, '
        'Parent = self(), '
        'Collector = fun F([], Acc) -> Acc; '
        '    F(Pending, Acc) -> '
        '        receive '
        '            {node_status, N, R} -> F(lists:delete(N, Pending), [{N, R} | Acc]) '
        '        after 8000 -> '
        '            [{N, pang} || N <- Pending] ++ Acc '
        '        end '
        'end, '
        'lists:foreach(fun(N) -> spawn(fun() -> Parent ! {node_status, N, net_adm:ping(N)} end) end, Nodes), '
        'Results = Collector(Nodes, []), '
        'Output = string:join([atom_to_list(N) ++ "::" ++ atom_to_list(R) || {N, R} <- lists:reverse(Results)], ","), '
        f'io:format("~s", [Output]), '
        f'halt(0)'
    )
    args = [
        erl, "-noshell",
        "-name", f"{ping_node}@{local_ip}",
        "-setcookie", cookie,
        "-eval", eval_code,
    ]

    batch_results: Dict[str, bool] = {}

    try:
        result = subprocess.run(
            args,
            capture_output=True,
            text=True,
            timeout=max(12, 8 + len(target_nodes)),
            creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0) if IS_WINDOWS else 0,
        )
    except (subprocess.TimeoutExpired, Exception):
        return batch_results

    if result.returncode != 0:
        return batch_results

    output = ",".join(
        part.strip()
        for part in ((result.stdout or ""), (result.stderr or ""))
        if part and part.strip()
    )
    if not output:
        return batch_results

    for item in output.split(','):
        if '::' not in item:
            continue
        node, status = item.rsplit('::', 1)
        node = node.strip()
        status = status.strip()
        original_names = target_mapping.get(node)
        if original_names is None:
            short_name = node.split('@', 1)[0]
            original_names = short_name_mapping.get(short_name)
        if not original_names:
            continue
        is_online = (status == 'pong')
        for original_name in original_names:
            batch_results[original_name] = is_online

    return batch_results


def _check_nodes_status_via_ping(server_names: List[str], cookie: str, target_ip: str = None,
                                 erl_path: str = None) -> Dict[str, bool]:
    """使用 net_adm:ping 严格检查节点状态。"""
    if not server_names:
        return {}

    local_ip = get_local_ip()
    cookie_arg = (cookie or "").strip() or "ddxq2-node"
    results = {name: False for name in server_names}
    target_mapping: Dict[str, List[str]] = {}
    target_nodes: List[str] = []

    for server_name in server_names:
        raw_name = server_name or ""
        normalized_name = raw_name.strip()
        if not normalized_name:
            continue
        if '@' in normalized_name:
            target_node = normalized_name
        else:
            node_ip = target_ip if target_ip else local_ip
            target_node = f"{normalized_name}@{node_ip}"
        if target_node not in target_mapping:
            target_mapping[target_node] = []
            target_nodes.append(target_node)
        target_mapping[target_node].append(raw_name)

    if not target_nodes:
        return results

    _ensure_epmd_daemon(erl_path)
    batches = _split_node_status_batches(target_nodes)

    if len(batches) == 1:
        results.update(
            _check_nodes_status_batch(
                batches[0], target_mapping, cookie_arg, erl_path, local_ip
            )
        )
        return results

    max_workers = min(_NODE_STATUS_MAX_WORKERS, len(batches))
    with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = [
            executor.submit(
                _check_nodes_status_batch,
                batch,
                target_mapping,
                cookie_arg,
                erl_path,
                local_ip,
            )
            for batch in batches
        ]
        for future in concurrent.futures.as_completed(futures):
            try:
                results.update(future.result())
            except Exception:
                continue

    return results


def _query_epmd_registered_names(host: str, timeout: float = _NODE_STATUS_FAST_TIMEOUT) -> List[str]:
    """查询指定主机 epmd 上已注册的 Erlang 短节点名。"""
    sock: Optional[socket.socket] = None
    try:
        sock = socket.create_connection((host, _EPMD_PORT), timeout=timeout)
        sock.settimeout(timeout)
        sock.sendall(struct.pack(">HB", 1, ord('n')))

        chunks = []
        while True:
            chunk = sock.recv(4096)
            if not chunk:
                break
            chunks.append(chunk)

        if not chunks:
            return []

        payload = b"".join(chunks)
        if len(payload) >= 4:
            payload = payload[4:]
        text = payload.decode("latin-1", errors="ignore")

        names: List[str] = []
        for line in text.splitlines():
            match = re.search(r"\bname\s+([^\s]+)\s+at\s+port\b", line)
            if match:
                names.append(match.group(1).strip())
        return names
    except Exception:
        return []
    finally:
        if sock is not None:
            try:
                sock.close()
            except OSError:
                pass


def check_nodes_status(server_names: List[str], cookie: str, target_ip: str = None,
                       erl_path: str = None) -> Dict[str, bool]:
    """批量检查多个 Erlang 节点是否在线
    
    批量模式优先按主机并发查询 epmd 已注册节点名，速度远快于逐个分布式 ping。
    这适合服务器列表/状态面板的在线展示；若需要严格校验 cookie 与分布式握手，
    请使用 ``check_node_status``。
    
    Args:
        server_names: 服务器名称列表（完整节点名如 xxx@ip 或仅名称）
        cookie: Erlang cookie（批量快速模式下仅保留参数兼容，不参与 epmd 查询）
        target_ip: 目标IP（如果 server_name 不包含 @ip 时使用）
        erl_path: Erlang 安装路径（批量快速模式下保留参数兼容）
    
    Returns:
        字典 {server_name: is_online}
    """
    if not server_names:
        return {}

    local_ip = get_local_ip()
    results = {name: False for name in server_names}
    host_mapping: Dict[str, Dict[str, List[str]]] = {}

    for server_name in server_names:
        raw_name = server_name or ""
        normalized_name = raw_name.strip()
        if not normalized_name:
            continue

        if '@' in normalized_name:
            short_name, host = normalized_name.split('@', 1)
        else:
            short_name = normalized_name
            host = target_ip if target_ip else local_ip

        if not short_name or not host:
            continue

        host_bucket = host_mapping.setdefault(host, {})
        host_bucket.setdefault(short_name, []).append(raw_name)

    if not host_mapping:
        return results

    max_workers = min(_NODE_STATUS_FAST_MAX_WORKERS, len(host_mapping))
    with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
        future_to_host = {
            executor.submit(_query_epmd_registered_names, host): host
            for host in host_mapping.keys()
        }
        for future in concurrent.futures.as_completed(future_to_host):
            host = future_to_host[future]
            try:
                registered_names = set(future.result())
            except Exception:
                registered_names = set()

            short_name_mapping = host_mapping.get(host, {})
            for short_name, original_names in short_name_mapping.items():
                is_online = short_name in registered_names
                for original_name in original_names:
                    results[original_name] = is_online

    return results
