//! Control query implementations.
//!
//! Each function takes `&Node` and returns a `serde_json::Value`.
//! Query logic is kept separate from socket handling.

use crate::identity::{NodeAddr, PeerIdentity, encode_npub};
use crate::node::Node;
use crate::node::stats_history::{ALL_METRICS, ALL_PEER_METRICS, Granularity, Metric, PeerMetric};
use serde_json::{Value, json};
use std::str::FromStr;
use std::time::Duration;

/// Resolve an `npub1...` string to the corresponding `NodeAddr`.
fn parse_peer_npub(s: &str) -> Result<NodeAddr, String> {
    PeerIdentity::from_npub(s)
        .map(|p| *p.node_addr())
        .map_err(|e| format!("invalid peer npub: {e}"))
}

/// Helper: get current Unix time in milliseconds.
fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

/// Classify a DualEwma trend as "rising", "falling", or "stable".
fn trend_label(short: f64, long: f64) -> &'static str {
    if !short.is_finite() || !long.is_finite() || long == 0.0 {
        return "stable";
    }
    let ratio = short / long;
    if ratio > 1.05 {
        "rising"
    } else if ratio < 0.95 {
        "falling"
    } else {
        "stable"
    }
}

/// `show_status` — Node overview.
pub fn show_status(node: &Node) -> Value {
    let pid = std::process::id();
    let exe_path = std::env::current_exe()
        .map(|p| p.display().to_string())
        .unwrap_or_else(|_| "-".into());
    let uptime_secs = node.uptime().as_secs();
    let fwd = node.stats().snapshot().forwarding;

    // Inline last-N-second sparklines for dashboard rendering. Kept
    // short so the status payload stays compact; longer windows use
    // `show_stats_history`.
    const SPARK_N: usize = 30;
    let hist = node.stats_history();
    let sparklines = json!({
        "mesh_size": hist.recent(Metric::MeshSize, SPARK_N),
        "tree_depth": hist.recent(Metric::TreeDepth, SPARK_N),
        "peer_count": hist.recent(Metric::PeerCount, SPARK_N),
        "bytes_in": hist.recent(Metric::BytesIn, SPARK_N),
        "bytes_out": hist.recent(Metric::BytesOut, SPARK_N),
        "loss_rate": hist.recent(Metric::LossRate, SPARK_N),
    });

    json!({
        "version": crate::version::short_version(),
        "npub": node.npub(),
        "node_addr": hex::encode(node.node_addr().as_bytes()),
        "ipv6_addr": format!("{}", node.identity().address()),
        "state": format!("{}", node.state()),
        "is_leaf_only": node.is_leaf_only(),
        "peer_count": node.peer_count(),
        "session_count": node.session_count(),
        "link_count": node.link_count(),
        "transport_count": node.transport_count(),
        "connection_count": node.connection_count(),
        "tun_state": format!("{}", node.tun_state()),
        "tun_name": node.tun_name().unwrap_or("-"),
        "effective_ipv6_mtu": node.effective_ipv6_mtu(),
        "control_socket": &node.config().node.control.socket_path,
        "pid": pid,
        "exe_path": exe_path,
        "uptime_secs": uptime_secs,
        "estimated_mesh_size": node.estimated_mesh_size(),
        "forwarding": serde_json::to_value(&fwd).unwrap_or_default(),
        "sparklines": sparklines,
    })
}

/// `show_acl` — Loaded peer ACL state.
pub fn show_acl(node: &Node) -> Value {
    let status = node.peer_acl_status();

    json!({
        "allow_file": status.allow_file,
        "deny_file": status.deny_file,
        "enforcement_active": status.enforcement_active,
        "effective_mode": status.effective_mode,
        "default_decision": status.default_decision,
        "allow_all": status.allow_all,
        "deny_all": status.deny_all,
        "allow_file_entries": status.allow_file_entries,
        "deny_file_entries": status.deny_file_entries,
        "allow_entries": status.allow_entries,
        "deny_entries": status.deny_entries,
    })
}

/// `show_peers` — Authenticated peers.
pub fn show_peers(node: &Node) -> Value {
    let tree = node.tree_state();
    let my_addr = *tree.my_node_addr();
    let parent_id = *tree.my_declaration().parent_id();
    let is_root = tree.is_root();

    let peers: Vec<Value> = node
        .peers()
        .map(|peer| {
            let node_addr = *peer.node_addr();
            let addr_hex = hex::encode(node_addr.as_bytes());

            // Determine tree relationship
            let is_parent = !is_root && node_addr == parent_id;
            let is_child = tree
                .peer_declaration(&node_addr)
                .is_some_and(|decl| *decl.parent_id() == my_addr);

            let mut peer_json = json!({
                "node_addr": addr_hex,
                "npub": peer.npub(),
                "display_name": node.peer_display_name(&node_addr),
                "ipv6_addr": format!("{}", peer.address()),
                "connectivity": format!("{}", peer.connectivity()),
                "link_id": peer.link_id().as_u64(),
                "authenticated_at_ms": peer.authenticated_at(),
                "last_seen_ms": peer.last_seen(),
                "has_tree_position": peer.has_tree_position(),
                "has_bloom_filter": peer.filter_sequence() > 0,
                "filter_sequence": peer.filter_sequence(),
                "is_parent": is_parent,
                "is_child": is_child,
            });

            // Add transport address if available
            if let Some(addr) = peer.current_addr() {
                peer_json["transport_addr"] = json!(format!("{}", addr));
            }

            // Add link info (direction, transport type)
            let link_id = peer.link_id();
            if let Some(link) = node.get_link(&link_id) {
                peer_json["direction"] = json!(format!("{}", link.direction()));
                let transport_id = link.transport_id();
                if let Some(handle) = node.get_transport(&transport_id) {
                    peer_json["transport_type"] = json!(handle.transport_type().name);
                }
            }

            // Add tree depth if available
            if let Some(coords) = peer.coords() {
                peer_json["tree_depth"] = json!(coords.depth());
            }

            // Add link stats
            let stats = peer.link_stats();
            peer_json["stats"] = json!({
                "packets_sent": stats.packets_sent,
                "packets_recv": stats.packets_recv,
                "bytes_sent": stats.bytes_sent,
                "bytes_recv": stats.bytes_recv,
            });

            // Security signals
            peer_json["replay_suppressed"] = json!(peer.replay_suppressed_count());
            peer_json["consecutive_decrypt_failures"] = json!(peer.consecutive_decrypt_failures());

            // Noise session counters (rekey urgency, replay window state)
            if let Some(session) = peer.noise_session() {
                peer_json["noise"] = json!({
                    "send_counter": session.current_send_counter(),
                    "highest_recv_counter": session.highest_received_counter(),
                });
            }

            // Session indices (hijack detection)
            if let Some(idx) = peer.our_index() {
                peer_json["our_session_index"] = json!(format!("{:08x}", idx.as_u32()));
            }

            // Rekey state
            if peer.rekey_in_progress() {
                peer_json["rekey_in_progress"] = json!(true);
            }
            if peer.is_draining() {
                peer_json["rekey_draining"] = json!(true);
            }
            peer_json["current_k_bit"] = json!(peer.current_k_bit());

            // Add MMP metrics if available
            if let Some(mmp) = peer.mmp() {
                let mut mmp_json = json!({
                    "mode": format!("{}", mmp.mode()),
                });
                if let Some(srtt) = mmp.metrics.srtt_ms() {
                    mmp_json["srtt_ms"] = json!(srtt);
                }
                mmp_json["loss_rate"] = json!(mmp.metrics.loss_rate());
                mmp_json["etx"] = json!(mmp.metrics.etx);
                mmp_json["goodput_bps"] = json!(mmp.metrics.goodput_bps);
                mmp_json["delivery_ratio_forward"] = json!(mmp.metrics.delivery_ratio_forward);
                mmp_json["delivery_ratio_reverse"] = json!(mmp.metrics.delivery_ratio_reverse);
                if let Some(smoothed_loss) = mmp.metrics.smoothed_loss() {
                    mmp_json["smoothed_loss"] = json!(smoothed_loss);
                }
                if let Some(smoothed_etx) = mmp.metrics.smoothed_etx() {
                    mmp_json["smoothed_etx"] = json!(smoothed_etx);
                }
                if let Some(srtt) = mmp.metrics.srtt_ms()
                    && let Some(setx) = mmp.metrics.smoothed_etx()
                {
                    mmp_json["lqi"] = json!(setx * (1.0 + srtt / 100.0));
                }
                peer_json["mmp"] = mmp_json;
            }

            peer_json
        })
        .collect();

    json!({ "peers": peers })
}

/// `show_links` — Active links.
pub fn show_links(node: &Node) -> Value {
    let links: Vec<Value> = node
        .links()
        .map(|link| {
            let stats = link.stats();
            json!({
                "link_id": link.link_id().as_u64(),
                "transport_id": link.transport_id().as_u32(),
                "remote_addr": format!("{}", link.remote_addr()),
                "direction": format!("{}", link.direction()),
                "state": format!("{}", link.state()),
                "created_at_ms": link.created_at(),
                "stats": {
                    "packets_sent": stats.packets_sent,
                    "packets_recv": stats.packets_recv,
                    "bytes_sent": stats.bytes_sent,
                    "bytes_recv": stats.bytes_recv,
                    "last_recv_ms": stats.last_recv_ms,
                },
            })
        })
        .collect();

    json!({ "links": links })
}

/// `show_tree` — Spanning tree state.
pub fn show_tree(node: &Node) -> Value {
    let tree = node.tree_state();
    let my_coords = tree.my_coords();
    let decl = tree.my_declaration();

    // Build coords array as hex strings
    let coords: Vec<String> = my_coords
        .entries()
        .iter()
        .map(|e| hex::encode(e.node_addr.as_bytes()))
        .collect();

    // Build peer tree data
    let peers: Vec<Value> = tree
        .peer_ids()
        .map(|peer_id| {
            let mut peer_json = json!({
                "node_addr": hex::encode(peer_id.as_bytes()),
                "display_name": node.peer_display_name(peer_id),
            });
            if let Some(coords) = tree.peer_coords(peer_id) {
                let coord_path: Vec<String> = coords
                    .entries()
                    .iter()
                    .map(|e| hex::encode(e.node_addr.as_bytes()))
                    .collect();
                peer_json["depth"] = json!(coords.depth());
                peer_json["root"] = json!(hex::encode(coords.root_id().as_bytes()));
                peer_json["coords"] = json!(coord_path);
                peer_json["distance_to_us"] = json!(my_coords.distance_to(coords));
            }
            peer_json
        })
        .collect();

    // Determine parent display name
    let parent_addr = my_coords.parent_id();
    let parent_hex = hex::encode(parent_addr.as_bytes());
    let parent_display = node.peer_display_name(parent_addr);

    let tree_stats = node.stats().snapshot().tree;

    json!({
        "my_node_addr": hex::encode(tree.my_node_addr().as_bytes()),
        "root": hex::encode(tree.root().as_bytes()),
        "is_root": tree.is_root(),
        "depth": my_coords.depth(),
        "my_coords": coords,
        "parent": parent_hex,
        "parent_display_name": parent_display,
        "declaration_sequence": decl.sequence(),
        "declaration_signed": decl.is_signed(),
        "peer_tree_count": tree.peer_count(),
        "peers": peers,
        "stats": serde_json::to_value(&tree_stats).unwrap_or_default(),
    })
}

/// `show_sessions` — End-to-end sessions.
pub fn show_sessions(node: &Node) -> Value {
    let sessions: Vec<Value> = node
        .session_entries()
        .map(|(addr, entry)| {
            let state_str = if entry.is_established() {
                "established"
            } else if entry.is_initiating() {
                "initiating"
            } else if entry.is_awaiting_msg3() {
                "awaiting_msg3"
            } else {
                "unknown"
            };

            let mut session_json = json!({
                "remote_addr": hex::encode(addr.as_bytes()),
                "display_name": node.peer_display_name(addr),
                "state": state_str,
                "is_initiator": entry.is_initiator(),
                "last_activity_ms": entry.last_activity(),
            });

            // Derive npub from session's remote public key
            let (xonly, _parity) = entry.remote_pubkey().x_only_public_key();
            session_json["npub"] = json!(encode_npub(&xonly));

            // Traffic counters
            let (pkts_tx, pkts_rx, bytes_tx, bytes_rx) = entry.traffic_counters();
            session_json["stats"] = json!({
                "packets_sent": pkts_tx,
                "packets_recv": pkts_rx,
                "bytes_sent": bytes_tx,
                "bytes_recv": bytes_rx,
            });

            // Handshake health (visible during initiating/awaiting_msg3)
            if !entry.is_established() {
                session_json["resend_count"] = json!(entry.resend_count());
            }

            // Rekey and session health (visible when established)
            if entry.is_established() {
                session_json["session_start_ms"] = json!(entry.session_start_ms());
                session_json["current_k_bit"] = json!(entry.current_k_bit());
                session_json["coords_warmup_remaining"] = json!(entry.coords_warmup_remaining());
                session_json["is_draining"] = json!(entry.is_draining());
            }

            // Add session MMP if available
            if let Some(mmp) = entry.mmp() {
                let mut mmp_json = json!({
                    "mode": format!("{}", mmp.mode()),
                    "loss_rate": mmp.metrics.loss_rate(),
                    "etx": mmp.metrics.etx,
                    "goodput_bps": mmp.metrics.goodput_bps,
                    "delivery_ratio_forward": mmp.metrics.delivery_ratio_forward,
                    "delivery_ratio_reverse": mmp.metrics.delivery_ratio_reverse,
                    "path_mtu": mmp.path_mtu.current_mtu(),
                });
                if let Some(srtt) = mmp.metrics.srtt_ms() {
                    mmp_json["srtt_ms"] = json!(srtt);
                }
                if let Some(smoothed_loss) = mmp.metrics.smoothed_loss() {
                    mmp_json["smoothed_loss"] = json!(smoothed_loss);
                }
                if let Some(smoothed_etx) = mmp.metrics.smoothed_etx() {
                    mmp_json["smoothed_etx"] = json!(smoothed_etx);
                }
                if let Some(srtt) = mmp.metrics.srtt_ms()
                    && let Some(setx) = mmp.metrics.smoothed_etx()
                {
                    mmp_json["sqi"] = json!(setx * (1.0 + srtt / 100.0));
                }
                session_json["mmp"] = mmp_json;
            }

            session_json
        })
        .collect();

    json!({ "sessions": sessions })
}

/// `show_bloom` — Bloom filter state.
pub fn show_bloom(node: &Node) -> Value {
    let bloom = node.bloom_state();

    let leaf_deps: Vec<String> = bloom
        .leaf_dependents()
        .iter()
        .map(|addr| hex::encode(addr.as_bytes()))
        .collect();

    // Build per-peer filter info
    let peer_filters: Vec<Value> = node
        .peers()
        .map(|peer| {
            let addr = *peer.node_addr();
            let mut pf = json!({
                "peer": hex::encode(addr.as_bytes()),
                "display_name": node.peer_display_name(&addr),
                "has_filter": peer.filter_sequence() > 0,
                "filter_sequence": peer.filter_sequence(),
            });
            if let Some(filter) = peer.inbound_filter() {
                let max_fpr = node.config().node.bloom.max_inbound_fpr;
                pf["estimated_count"] = json!(filter.estimated_count(max_fpr));
                pf["set_bits"] = json!(filter.count_ones());
                pf["fill_ratio"] = json!(filter.fill_ratio());
            }
            pf
        })
        .collect();

    let bloom_stats = node.stats().snapshot().bloom;

    json!({
        "own_node_addr": hex::encode(node.node_addr().as_bytes()),
        "is_leaf_only": node.is_leaf_only(),
        "sequence": bloom.sequence(),
        "leaf_dependent_count": bloom.leaf_dependents().len(),
        "leaf_dependents": leaf_deps,
        "peer_filters": peer_filters,
        "stats": serde_json::to_value(&bloom_stats).unwrap_or_default(),
    })
}

/// `show_mmp` — MMP metrics summary.
pub fn show_mmp(node: &Node) -> Value {
    // Link-layer MMP per peer
    let peers: Vec<Value> = node.peers().filter_map(|peer| {
        let mmp = peer.mmp()?;
        let addr = *peer.node_addr();
        let metrics = &mmp.metrics;

        let mut link_layer = json!({
            "loss_rate": metrics.loss_rate(),
            "etx": metrics.etx,
            "goodput_bps": metrics.goodput_bps,
            "spin_bit_role": if mmp.spin_bit.is_initiator() { "initiator" } else { "responder" },
        });

        if let Some(smoothed_loss) = metrics.smoothed_loss() {
            link_layer["smoothed_loss"] = json!(smoothed_loss);
        }
        if let Some(smoothed_etx) = metrics.smoothed_etx() {
            link_layer["smoothed_etx"] = json!(smoothed_etx);
        }
        if let Some(srtt) = metrics.srtt_ms() {
            link_layer["srtt_ms"] = json!(srtt);
            if let Some(setx) = metrics.smoothed_etx() {
                link_layer["lqi"] = json!(setx * (1.0 + srtt / 100.0));
            }
        }

        // Trend indicators
        if metrics.rtt_trend.initialized() {
            link_layer["rtt_trend"] = json!(trend_label(metrics.rtt_trend.short(), metrics.rtt_trend.long()));
        }
        if metrics.loss_trend.initialized() {
            link_layer["loss_trend"] = json!(trend_label(metrics.loss_trend.short(), metrics.loss_trend.long()));
        }
        if metrics.goodput_trend.initialized() {
            link_layer["goodput_trend"] = json!(trend_label(metrics.goodput_trend.short(), metrics.goodput_trend.long()));
        }
        if metrics.jitter_trend.initialized() {
            link_layer["jitter_trend"] = json!(trend_label(metrics.jitter_trend.short(), metrics.jitter_trend.long()));
        }

        link_layer["delivery_ratio_forward"] = json!(metrics.delivery_ratio_forward);
        link_layer["delivery_ratio_reverse"] = json!(metrics.delivery_ratio_reverse);
        link_layer["ecn_ce_count"] = json!(metrics.last_ecn_ce_count());

        Some(json!({
            "peer": hex::encode(addr.as_bytes()),
            "display_name": node.peer_display_name(&addr),
            "mode": format!("{}", mmp.mode()),
            "link_layer": link_layer,
        }))
    }).collect();

    // Session-layer MMP
    let sessions: Vec<Value> = node
        .session_entries()
        .filter_map(|(addr, entry)| {
            let mmp = entry.mmp()?;
            let metrics = &mmp.metrics;

            let mut session_layer = json!({
                "loss_rate": metrics.loss_rate(),
                "etx": metrics.etx,
                "path_mtu": mmp.path_mtu.current_mtu(),
            });

            if let Some(smoothed_loss) = metrics.smoothed_loss() {
                session_layer["smoothed_loss"] = json!(smoothed_loss);
            }
            if let Some(smoothed_etx) = metrics.smoothed_etx() {
                session_layer["smoothed_etx"] = json!(smoothed_etx);
            }
            if let Some(srtt) = metrics.srtt_ms() {
                session_layer["srtt_ms"] = json!(srtt);
                if let Some(setx) = metrics.smoothed_etx() {
                    session_layer["sqi"] = json!(setx * (1.0 + srtt / 100.0));
                }
            }

            Some(json!({
                "remote": hex::encode(addr.as_bytes()),
                "display_name": node.peer_display_name(addr),
                "mode": format!("{}", mmp.mode()),
                "session_layer": session_layer,
            }))
        })
        .collect();

    json!({
        "peers": peers,
        "sessions": sessions,
    })
}

/// `show_cache` — Coordinate cache stats and entries.
pub fn show_cache(node: &Node) -> Value {
    let cache = node.coord_cache();
    let now = now_ms();
    let stats = cache.stats(now);

    // Include individual entries for route debugging
    let entries: Vec<Value> = cache
        .iter(now)
        .map(|(addr, entry)| {
            let fips_addr = crate::identity::FipsAddress::from_node_addr(addr);
            let coord_path: Vec<String> = entry
                .coords()
                .entries()
                .iter()
                .map(|e| hex::encode(e.node_addr.as_bytes()))
                .collect();
            let mut entry_json = json!({
                "node_addr": hex::encode(addr.as_bytes()),
                "display_name": node.peer_display_name(addr),
                "ipv6_addr": format!("{}", fips_addr),
                "depth": entry.coords().depth(),
                "coords": coord_path,
                "age_ms": now.saturating_sub(entry.created_at()),
                "last_used_ms": entry.last_used(),
            });
            if let Some(mtu) = entry.path_mtu() {
                entry_json["path_mtu"] = json!(mtu);
            }
            entry_json
        })
        .collect();

    json!({
        "count": stats.entries,
        "max_entries": stats.max_entries,
        "fill_ratio": stats.fill_ratio(),
        "default_ttl_ms": cache.default_ttl_ms(),
        "expired": stats.expired,
        "avg_age_ms": stats.avg_age_ms,
        "entries": entries,
    })
}

/// `show_connections` — Pending handshakes.
pub fn show_connections(node: &Node) -> Value {
    let now = now_ms();
    let connections: Vec<Value> = node
        .connections()
        .map(|conn| {
            let mut conn_json = json!({
                "link_id": conn.link_id().as_u64(),
                "direction": format!("{}", conn.direction()),
                "handshake_state": format!("{}", conn.handshake_state()),
                "started_at_ms": conn.started_at(),
                "idle_ms": now.saturating_sub(conn.last_activity()),
                "resend_count": conn.resend_count(),
            });

            if let Some(identity) = conn.expected_identity() {
                conn_json["expected_peer"] = json!(identity.npub());
            }

            conn_json
        })
        .collect();

    json!({ "connections": connections })
}

/// `show_transports` — Transport instances.
pub fn show_transports(node: &Node) -> Value {
    let transports: Vec<Value> = node
        .transport_ids()
        .map(|id| {
            let handle = node.get_transport(id).unwrap();
            let mut t_json = json!({
                "transport_id": id.as_u32(),
                "type": handle.transport_type().name,
                "state": format!("{}", handle.state()),
                "mtu": handle.mtu(),
            });

            if let Some(name) = handle.name() {
                t_json["name"] = json!(name);
            }
            if let Some(addr) = handle.local_addr() {
                t_json["local_addr"] = json!(format!("{}", addr));
            }

            // Tor-specific fields
            if let Some(mode) = handle.tor_mode() {
                t_json["tor_mode"] = json!(mode);
            }
            if let Some(onion) = handle.onion_address() {
                t_json["onion_address"] = json!(onion);
            }
            if let Some(monitoring) = handle.tor_monitoring() {
                t_json["tor_monitoring"] = serde_json::to_value(&monitoring).unwrap_or_default();
            }

            t_json["stats"] = handle.transport_stats();

            t_json
        })
        .collect();

    json!({ "transports": transports })
}

/// `show_routing` — Routing table summary and node statistics.
pub fn show_routing(node: &Node) -> Value {
    let cache = node.coord_cache();
    let now = now_ms();
    let cache_stats = cache.stats(now);
    let node_stats = node.stats().snapshot();

    // Pending discovery lookups (individual targets)
    let lookups: Vec<Value> = node
        .pending_lookups_iter()
        .map(|(addr, lookup)| {
            json!({
                "target": hex::encode(addr.as_bytes()),
                "display_name": node.peer_display_name(addr),
                "initiated_ms": lookup.initiated_ms,
                "last_sent_ms": lookup.last_sent_ms,
                "attempt": lookup.attempt,
                "age_ms": now.saturating_sub(lookup.initiated_ms),
            })
        })
        .collect();

    // Connection retry state
    let retries: Vec<Value> = node
        .retry_state_iter()
        .map(|(addr, state)| {
            json!({
                "node_addr": hex::encode(addr.as_bytes()),
                "display_name": node.peer_display_name(addr),
                "retry_count": state.retry_count,
                "retry_after_ms": state.retry_after_ms,
                "auto_reconnect": state.reconnect,
            })
        })
        .collect();

    json!({
        "coord_cache_entries": cache_stats.entries,
        "identity_cache_entries": node.identity_cache_len(),
        "pending_lookups": lookups,
        "pending_tun_destinations": node.pending_tun_destinations(),
        "pending_tun_packets": node.pending_tun_total_packets(),
        "recent_requests": node.recent_request_count(),
        "retries": retries,
        "forwarding": serde_json::to_value(&node_stats.forwarding).unwrap_or_default(),
        "discovery": serde_json::to_value(&node_stats.discovery).unwrap_or_default(),
        "error_signals": serde_json::to_value(&node_stats.errors).unwrap_or_default(),
        "congestion": serde_json::to_value(&node_stats.congestion).unwrap_or_default(),
    })
}

/// `show_identity_cache` — Known node identities.
///
/// Lists every node whose public key has been cached by this daemon.
/// Identities are learned from DNS resolution, peer handshakes, session
/// establishment, and configured peer npubs.  The cache uses LRU eviction
/// bounded by `node.cache.identity_size`.
pub fn show_identity_cache(node: &Node) -> Value {
    let now = now_ms();
    let entries: Vec<Value> = node
        .identity_cache_iter()
        .map(|(node_addr, pubkey, last_seen_ms)| {
            let (xonly, _parity) = pubkey.x_only_public_key();
            let fips_addr = crate::identity::FipsAddress::from_node_addr(node_addr);
            json!({
                "node_addr": hex::encode(node_addr.as_bytes()),
                "npub": encode_npub(&xonly),
                "display_name": node.peer_display_name(node_addr),
                "ipv6_addr": format!("{}", fips_addr),
                "last_seen_ms": last_seen_ms,
                "age_ms": now.saturating_sub(last_seen_ms),
            })
        })
        .collect();
    let count = entries.len();

    json!({
        "entries": entries,
        "count": count,
        "max_entries": node.identity_cache_max(),
    })
}

/// `show_stats_list` — Enumerate available history metrics and their units.
pub fn show_stats_list() -> Value {
    let metrics: Vec<Value> = ALL_METRICS
        .iter()
        .map(|m| {
            json!({
                "name": m.name(),
                "unit": m.unit(),
                "scope": "node",
            })
        })
        .chain(ALL_PEER_METRICS.iter().map(|m| {
            json!({
                "name": m.name(),
                "unit": m.unit(),
                "scope": "peer",
            })
        }))
        .collect();
    json!({
        "metrics": metrics,
        "fast_ring_seconds": crate::node::stats_history::FAST_RING_CAPACITY,
        "slow_ring_minutes": crate::node::stats_history::SLOW_RING_CAPACITY,
        "peer_retention_seconds": crate::node::stats_history::PEER_EVICTION_SECS,
    })
}

/// `show_stats_history` — Time-series samples for one metric.
///
/// Params:
/// - `metric` (required): metric name. Node-level metrics (e.g.
///   `mesh_size`) are resolved against `Metric`; per-peer metrics (e.g.
///   `srtt_ms`, `ecn_ce`) require the `peer` param and resolve against
///   `PeerMetric`.
/// - `peer` (optional): `npub1...` of the peer; required for per-peer
///   metrics.
/// - `window` (default `10m`): duration `<N>s`, `<N>m`, or `<N>h`.
/// - `granularity` (default `1s`): `1s` or `1m`.
pub fn show_stats_history(node: &Node, params: Option<&Value>) -> super::protocol::Response {
    use super::protocol::Response;
    let Some(params) = params else {
        return Response::error("missing params for show_stats_history");
    };

    let metric_name = match params.get("metric").and_then(|v| v.as_str()) {
        Some(v) => v,
        None => return Response::error("missing 'metric' parameter"),
    };

    let window_str = params
        .get("window")
        .and_then(|v| v.as_str())
        .unwrap_or("10m");
    let window = match parse_duration(window_str) {
        Ok(d) => d,
        Err(e) => return Response::error(e),
    };

    let granularity_str = params
        .get("granularity")
        .and_then(|v| v.as_str())
        .unwrap_or("1s");
    let granularity = match Granularity::from_str(granularity_str) {
        Ok(g) => g,
        Err(e) => return Response::error(e),
    };

    let peer_npub = params.get("peer").and_then(|v| v.as_str());
    let hist = node.stats_history();

    if let Some(npub) = peer_npub {
        let addr = match parse_peer_npub(npub) {
            Ok(a) => a,
            Err(e) => return Response::error(e),
        };
        let peer_metric = match PeerMetric::from_str(metric_name) {
            Ok(m) => m,
            Err(e) => return Response::error(e),
        };
        match hist.peer_query(&addr, peer_metric, window, granularity) {
            Some(series) => Response::ok(serde_json::to_value(&series).unwrap_or(Value::Null)),
            None => Response::error(format!(
                "peer not tracked in stats history: {}",
                node.peer_display_name(&addr)
            )),
        }
    } else {
        let metric = match Metric::from_str(metric_name) {
            Ok(m) => m,
            Err(e) => return Response::error(e),
        };
        let series = hist.query(metric, window, granularity);
        Response::ok(serde_json::to_value(&series).unwrap_or(Value::Null))
    }
}

/// Parse a duration of the form `<N>s`, `<N>m`, or `<N>h` into a `Duration`.
fn parse_duration(s: &str) -> Result<Duration, String> {
    if s.is_empty() {
        return Err("empty duration".to_string());
    }
    let (num_part, unit) = s.split_at(s.len() - 1);
    let n: u64 = num_part
        .parse()
        .map_err(|_| format!("invalid duration: {s}"))?;
    let secs = match unit {
        "s" => n,
        "m" => n * 60,
        "h" => n * 3600,
        _ => return Err(format!("unknown duration unit: {unit} (expected s, m, h)")),
    };
    Ok(Duration::from_secs(secs))
}

/// `show_stats_all_history` — Return a series for every tracked metric
/// in one round trip. Intended for the fipstop Graphs tab.
///
/// Without `peer`: returns the 10 node-level metrics.
/// With `peer` (npub): returns the 7 per-peer metrics for that peer.
///
/// Params: `{"peer": "<npub>"?, "window": "<dur>", "granularity": "<1s|1m>"}`.
pub fn show_stats_all_history(node: &Node, params: Option<&Value>) -> super::protocol::Response {
    use super::protocol::Response;
    let params = params.cloned().unwrap_or_else(|| json!({}));

    let window_str = params
        .get("window")
        .and_then(|v| v.as_str())
        .unwrap_or("10m");
    let window = match parse_duration(window_str) {
        Ok(d) => d,
        Err(e) => return Response::error(e),
    };

    let granularity_str = params
        .get("granularity")
        .and_then(|v| v.as_str())
        .unwrap_or("1s");
    let granularity = match Granularity::from_str(granularity_str) {
        Ok(g) => g,
        Err(e) => return Response::error(e),
    };

    let peer_npub = params.get("peer").and_then(|v| v.as_str());
    let hist = node.stats_history();

    let series: Vec<Value> = if let Some(npub) = peer_npub {
        let addr = match parse_peer_npub(npub) {
            Ok(a) => a,
            Err(e) => return Response::error(e),
        };
        if !hist.has_peer(&addr) {
            return Response::error(format!(
                "peer not tracked in stats history: {}",
                node.peer_display_name(&addr)
            ));
        }
        ALL_PEER_METRICS
            .iter()
            .map(|m| {
                let s = hist
                    .peer_query(&addr, *m, window, granularity)
                    .unwrap_or_else(|| {
                        // Unreachable: has_peer checked above, but degrade
                        // gracefully rather than panic.
                        crate::node::stats_history::Series {
                            metric: m.name(),
                            unit: m.unit(),
                            granularity_seconds: granularity.seconds(),
                            values: Vec::new(),
                        }
                    });
                serde_json::to_value(&s).unwrap_or(Value::Null)
            })
            .collect()
    } else {
        ALL_METRICS
            .iter()
            .map(|m| {
                let s = hist.query(*m, window, granularity);
                serde_json::to_value(&s).unwrap_or(Value::Null)
            })
            .collect()
    };

    Response::ok(json!({
        "granularity_seconds": granularity.seconds(),
        "window_seconds": window.as_secs(),
        "peer": peer_npub,
        "series": series,
    }))
}

/// `show_stats_peers` — Enumerate peers tracked in the stats history
/// with their lifecycle metadata. Used by operator tools to populate
/// peer selectors and to confirm a peer is in the retention window.
pub fn show_stats_peers(node: &Node) -> Value {
    let hist = node.stats_history();
    let now = std::time::Instant::now();

    let mut peers: Vec<Value> = hist
        .peers()
        .map(|(addr, rings)| {
            let last_contact_secs = now.duration_since(rings.last_contact()).as_secs();
            let first_seen_secs = now.duration_since(rings.first_seen()).as_secs();
            let is_active = node.peers().any(|p| p.node_addr() == addr);
            let npub = node
                .peers()
                .find(|p| p.node_addr() == addr)
                .map(|p| p.npub())
                .unwrap_or_else(|| hex::encode(addr.as_bytes()));
            json!({
                "npub": npub,
                "node_addr": hex::encode(addr.as_bytes()),
                "display_name": node.peer_display_name(addr),
                "is_active": is_active,
                "first_seen_secs_ago": first_seen_secs,
                "last_contact_secs_ago": last_contact_secs,
            })
        })
        .collect();

    // Stable display order: active peers first, then by display name.
    peers.sort_by(|a, b| {
        let a_active = a
            .get("is_active")
            .and_then(|v| v.as_bool())
            .unwrap_or(false);
        let b_active = b
            .get("is_active")
            .and_then(|v| v.as_bool())
            .unwrap_or(false);
        match (b_active, a_active) {
            (true, false) => std::cmp::Ordering::Greater,
            (false, true) => std::cmp::Ordering::Less,
            _ => a
                .get("display_name")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .cmp(b.get("display_name").and_then(|v| v.as_str()).unwrap_or("")),
        }
    });

    json!({ "peers": peers, "count": peers.len() })
}

/// `show_stats_history_all_peers` — One metric across every tracked
/// peer in one round trip. Backs the fipstop MetricByPeer grid view.
///
/// Params: `{"metric": "<name>", "window": "<dur>", "granularity": "<1s|1m>"}`.
/// `metric` must be a per-peer metric name (see `PeerMetric`).
pub fn show_stats_history_all_peers(
    node: &Node,
    params: Option<&Value>,
) -> super::protocol::Response {
    use super::protocol::Response;
    let Some(params) = params else {
        return Response::error("missing params for show_stats_history_all_peers");
    };

    let metric_name = match params.get("metric").and_then(|v| v.as_str()) {
        Some(v) => v,
        None => return Response::error("missing 'metric' parameter"),
    };
    let metric = match PeerMetric::from_str(metric_name) {
        Ok(m) => m,
        Err(e) => return Response::error(e),
    };

    let window_str = params
        .get("window")
        .and_then(|v| v.as_str())
        .unwrap_or("10m");
    let window = match parse_duration(window_str) {
        Ok(d) => d,
        Err(e) => return Response::error(e),
    };

    let granularity_str = params
        .get("granularity")
        .and_then(|v| v.as_str())
        .unwrap_or("1s");
    let granularity = match Granularity::from_str(granularity_str) {
        Ok(g) => g,
        Err(e) => return Response::error(e),
    };

    let hist = node.stats_history();
    let peer_addrs: Vec<NodeAddr> = hist.peer_addrs().copied().collect();

    let mut peers: Vec<Value> = peer_addrs
        .iter()
        .filter_map(|addr| {
            let s = hist.peer_query(addr, metric, window, granularity)?;
            let is_active = node.peers().any(|p| p.node_addr() == addr);
            Some(json!({
                "node_addr": hex::encode(addr.as_bytes()),
                "display_name": node.peer_display_name(addr),
                "is_active": is_active,
                "values": serde_json::to_value(&s.values).unwrap_or(Value::Null),
            }))
        })
        .collect();

    // Active peers first, then by display name.
    peers.sort_by(|a, b| {
        let a_active = a
            .get("is_active")
            .and_then(|v| v.as_bool())
            .unwrap_or(false);
        let b_active = b
            .get("is_active")
            .and_then(|v| v.as_bool())
            .unwrap_or(false);
        match (b_active, a_active) {
            (true, false) => std::cmp::Ordering::Greater,
            (false, true) => std::cmp::Ordering::Less,
            _ => a
                .get("display_name")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .cmp(b.get("display_name").and_then(|v| v.as_str()).unwrap_or("")),
        }
    });

    Response::ok(json!({
        "metric": metric.name(),
        "unit": metric.unit(),
        "granularity_seconds": granularity.seconds(),
        "window_seconds": window.as_secs(),
        "peers": peers,
    }))
}

/// Dispatch a command string to the appropriate query function.
pub fn dispatch(node: &Node, command: &str, params: Option<&Value>) -> super::protocol::Response {
    match command {
        "show_acl" => super::protocol::Response::ok(show_acl(node)),
        "show_status" => super::protocol::Response::ok(show_status(node)),
        "show_peers" => super::protocol::Response::ok(show_peers(node)),
        "show_links" => super::protocol::Response::ok(show_links(node)),
        "show_tree" => super::protocol::Response::ok(show_tree(node)),
        "show_sessions" => super::protocol::Response::ok(show_sessions(node)),
        "show_bloom" => super::protocol::Response::ok(show_bloom(node)),
        "show_mmp" => super::protocol::Response::ok(show_mmp(node)),
        "show_cache" => super::protocol::Response::ok(show_cache(node)),
        "show_connections" => super::protocol::Response::ok(show_connections(node)),
        "show_transports" => super::protocol::Response::ok(show_transports(node)),
        "show_routing" => super::protocol::Response::ok(show_routing(node)),
        "show_identity_cache" => super::protocol::Response::ok(show_identity_cache(node)),
        "show_stats_list" => super::protocol::Response::ok(show_stats_list()),
        "show_stats_history" => show_stats_history(node, params),
        "show_stats_all_history" => show_stats_all_history(node, params),
        "show_stats_peers" => super::protocol::Response::ok(show_stats_peers(node)),
        "show_stats_history_all_peers" => show_stats_history_all_peers(node, params),
        _ => super::protocol::Response::error(format!("unknown command: {}", command)),
    }
}
