#!/usr/bin/env bash

# Copyright (c) 2026 Luca Camardo
# SPDX-License-Identifier: MIT

set -Eeuo pipefail
IFS=$'\n\t'

readonly VERSION="0.5.6"

VIEW="network"
COMPACT=0

# Network database, populated by build_network_database().
declare -a NET_KEYS=()
declare -A NET_SEEN=()
declare -A NET_L3=()
declare -A NET_HOSTS=()

# Diagnostics state.
declare -a DIAG_LINES=()
DIAG_COUNT=0


# -----------------------------------------------------------------------------
# Usage and errors
# -----------------------------------------------------------------------------

usage() {
    cat <<'USAGE'
Usage:
  netns-tree [OPTION]

Views:
  --network      Show IPv4 networks and their members (default).
  --summary      Show a compact network summary.
  --diagnose     Check topology and routing for common problems.
  --topology     Show namespace / bridge / veth topology.
  --interfaces   Show detailed interface information.
  --routes       Show routing tables.
  --all          Show interfaces and routing tables.

Modifiers:
  --compact      Condense the selected view.

Other:
  --help, -h     Show this help.
  --version, -V  Show version.

Examples:
  sudo netns-tree
  sudo netns-tree --network
  sudo netns-tree --summary
  sudo netns-tree --diagnose
  sudo netns-tree --topology
  sudo netns-tree --interfaces
  sudo netns-tree --routes
  sudo netns-tree --all
  sudo netns-tree --compact

Security:
  Read-only. No networking configuration changes, downloads,
  or arbitrary code execution are performed.
USAGE
}


die() {
    printf 'netns-tree: error: %s\n' "$*" >&2
    exit 1
}


# -----------------------------------------------------------------------------
# Tool discovery
# -----------------------------------------------------------------------------

find_ip() {
    local path

    for path in /usr/sbin/ip /sbin/ip; do
        if [[ -x "$path" ]]; then
            printf '%s\n' "$path"
            return 0
        fi
    done

    return 1
}


find_jq() {
    local path

    for path in /usr/bin/jq /bin/jq; do
        if [[ -x "$path" ]]; then
            printf '%s\n' "$path"
            return 0
        fi
    done

    return 1
}


IP="$(find_ip)" || die "ip command not found"
JQ="$(find_jq)" || die "jq command not found"


# -----------------------------------------------------------------------------
# Arguments
# -----------------------------------------------------------------------------

parse_args() {
    local view_count=0

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --help|-h)
                usage
                exit 0
                ;;

            --version|-V)
                printf 'netns-tree %s\n' "$VERSION"
                exit 0
                ;;

            --network)
                VIEW="network"
                view_count=$((view_count + 1))
                ;;

            --summary)
                VIEW="summary"
                view_count=$((view_count + 1))
                ;;

            --diagnose)
                VIEW="diagnose"
                view_count=$((view_count + 1))
                ;;

            --topology)
                VIEW="topology"
                view_count=$((view_count + 1))
                ;;

            --interfaces)
                VIEW="interfaces"
                view_count=$((view_count + 1))
                ;;

            --routes)
                VIEW="routes"
                view_count=$((view_count + 1))
                ;;

            --all)
                VIEW="all"
                view_count=$((view_count + 1))
                ;;

            --compact)
                COMPACT=1
                ;;

            --)
                shift

                if [[ $# -ne 0 ]]; then
                    die "unexpected argument: $1"
                fi

                break
                ;;

            *)
                die "unknown argument: $1"
                ;;
        esac

        shift
    done

    if (( view_count > 1 )); then
        die "choose only one view: --network, --summary, --diagnose, --topology, --interfaces, --routes, or --all"
    fi
}


parse_args "$@"

[[ "$EUID" -eq 0 ]] ||
    die "root privileges are required; run with: sudo netns-tree"


# -----------------------------------------------------------------------------
# Tree helpers
# -----------------------------------------------------------------------------

print_branch() {
    local indent="$1"
    local last="$2"
    local text="$3"

    if [[ "$last" == "1" ]]; then
        printf '%s└── %s\n' "$indent" "$text"
    else
        printf '%s├── %s\n' "$indent" "$text"
    fi
}


child_indent() {
    local indent="$1"
    local last="$2"

    if [[ "$last" == "1" ]]; then
        printf '%s    ' "$indent"
    else
        printf '%s│   ' "$indent"
    fi
}


# -----------------------------------------------------------------------------
# Namespace / JSON helpers
# -----------------------------------------------------------------------------

get_namespaces() {
    "$IP" netns list |
        sed 's/ (id:.*$//' |
        sed '/^[[:space:]]*$/d' |
        sort -V
}


get_link_json() {
    "$IP" -n "$1" -j -d link show
}


get_addr_json() {
    "$IP" -n "$1" -j addr show
}


get_route_json() {
    "$IP" -n "$1" -j route show table all
}


get_ipv4_route_json() {
    "$IP" -n "$1" -j -4 route show table all
}


# -----------------------------------------------------------------------------
# Interface helpers
# -----------------------------------------------------------------------------

get_interface() {
    local link_json="$1"
    local ifname="$2"

    "$JQ" -c \
        --arg ifname "$ifname" \
        '.[] | select(.ifname == $ifname)' \
        <<<"$link_json"
}


get_kind() {
    "$JQ" -r '.linkinfo.info_kind // "unknown"' <<<"$1"
}


get_state() {
    "$JQ" -r '.operstate // "UNKNOWN"' <<<"$1"
}


get_mtu() {
    "$JQ" -r '.mtu // "?"' <<<"$1"
}


get_mac() {
    "$JQ" -r '.address // "?"' <<<"$1"
}


has_lower_up() {
    "$JQ" -e '
        ((.flags // []) | index("LOWER_UP")) != null
    ' <<<"$1" >/dev/null 2>&1
}


get_addr_summary() {
    local addr_json="$1"
    local ifname="$2"

    "$JQ" -r \
        --arg ifname "$ifname" '
        [
            .[]
            | select(.ifname == $ifname)
            | .addr_info[]?
            | if .family == "inet" then
                  "\(.local)/\(.prefixlen)"
              elif .family == "inet6" then
                  "\(.local)/\(.prefixlen)"
              else
                  empty
              end
        ]
        | join(", ")
        ' <<<"$addr_json"
}


get_addr_lines() {
    local addr_json="$1"
    local ifname="$2"

    "$JQ" -r \
        --arg ifname "$ifname" '
        .[]
        | select(.ifname == $ifname)
        | .addr_info[]?
        | if .family == "inet" then
              "IPv4 \(.local)/\(.prefixlen)"
          elif .family == "inet6" then
              "IPv6 \(.local)/\(.prefixlen)"
          else
              empty
          end
        ' <<<"$addr_json"
}


get_ipv4_addresses() {
    local addr_json="$1"

    "$JQ" -r '
        .[]
        | select(.ifname != "lo")
        | .ifname as $ifname
        | .addr_info[]?
        | select(.family == "inet")
        | [$ifname, .local, .prefixlen]
        | @tsv
        ' <<<"$addr_json"
}


get_root_interfaces() {
    local link_json="$1"

    "$JQ" -r '
        .[]
        | select(.ifname != "lo")
        | select(
            .linkinfo.info_kind == "bridge"
            or .master? == null
            or .master? == ""
        )
        | .ifname
        ' <<<"$link_json" |
        sort -V
}


# -----------------------------------------------------------------------------
# Bridge helpers
# -----------------------------------------------------------------------------

get_bridge_ports() {
    local link_json="$1"
    local bridge="$2"
    local bridge_ifindex="$3"

    "$JQ" -r \
        --arg bridge "$bridge" \
        --arg idx "$bridge_ifindex" '
        .[]
        | select(.ifname != "lo")
        | select(
            ((.master? // "") | tostring) == $bridge
            or
            ((.master? // "") | tostring) == $idx
        )
        | .ifname
        ' <<<"$link_json"
}


bridge_port_count() {
    get_bridge_ports "$1" "$2" "$3" |
        sed '/^[[:space:]]*$/d' |
        wc -l |
        tr -d ' '
}


# -----------------------------------------------------------------------------
# Veth helpers
# -----------------------------------------------------------------------------

get_peer_namespace() {
    local ns="$1"
    local nsid="$2"

    "$IP" -n "$ns" netns list-id 2>/dev/null |
        sed -n \
            "s/^nsid[[:space:]]*${nsid}[[:space:]]*(iproute2 netns name: \([^)]*\)).*$/\1/p"
}


get_peer_interface() {
    local peer_ns="$1"
    local ifindex="$2"

    "$IP" -n "$peer_ns" -j link show 2>/dev/null |
        "$JQ" -r \
            --argjson idx "$ifindex" '
            .[]
            | select(.ifindex == $idx)
            | .ifname
            '
}


get_peer_description() {
    local ns="$1"
    local obj="$2"

    local nsid
    local ifindex
    local peer_ns
    local peer_ifname

    nsid="$(
        "$JQ" -r '.link_netnsid // empty' <<<"$obj"
    )"

    ifindex="$(
        "$JQ" -r '.link_index // empty' <<<"$obj"
    )"

    [[ "$ifindex" =~ ^[0-9]+$ ]] || return 1

    if [[ "$nsid" =~ ^[0-9]+$ ]]; then

        peer_ns="$(
            get_peer_namespace "$ns" "$nsid" ||
            true
        )"

        [[ -n "$peer_ns" ]] || return 1

        peer_ifname="$(
            get_peer_interface \
                "$peer_ns" \
                "$ifindex" ||
                true
        )"

        [[ -n "$peer_ifname" ]] || return 1

        printf '%s:%s\n' \
            "$peer_ns" \
            "$peer_ifname"

        return 0
    fi

    peer_ifname="$(
        get_peer_interface \
            "$ns" \
            "$ifindex" ||
            true
    )"

    [[ -n "$peer_ifname" ]] || return 1

    printf '%s:%s\n' \
        "$ns" \
        "$peer_ifname"
}


get_peer_state() {
    local peer="$1"

    [[ -n "$peer" ]] || return 0
    [[ "$peer" == *:* ]] || return 0

    local peer_ns="${peer%:*}"
    local peer_ifname="${peer##*:}"

    [[ -n "$peer_ns" ]] || return 0
    [[ -n "$peer_ifname" ]] || return 0

    local peer_link_json
    local peer_obj

    peer_link_json="$(
        get_link_json "$peer_ns" 2>/dev/null ||
        true
    )"

    [[ -n "$peer_link_json" ]] || return 0

    peer_obj="$(
        get_interface \
            "$peer_link_json" \
            "$peer_ifname" ||
            true
    )"

    [[ -n "$peer_obj" ]] || return 0

    get_state "$peer_obj"
}


canonical_pair_key() {
    local first="$1"
    local second="$2"

    if [[ "$first" < "$second" ]]; then
        printf '%s|%s\n' "$first" "$second"
    else
        printf '%s|%s\n' "$second" "$first"
    fi
}


# -----------------------------------------------------------------------------
# IPv4 calculations
# -----------------------------------------------------------------------------

ipv4_to_int() {
    local ip="$1"
    local a
    local b
    local c
    local d

    IFS=. read -r a b c d <<<"$ip"

    [[ "$a" =~ ^[0-9]+$ ]] || return 1
    [[ "$b" =~ ^[0-9]+$ ]] || return 1
    [[ "$c" =~ ^[0-9]+$ ]] || return 1
    [[ "$d" =~ ^[0-9]+$ ]] || return 1

    (( a <= 255 && b <= 255 && c <= 255 && d <= 255 )) ||
        return 1

    printf '%u\n' \
        "$(( (a << 24) | (b << 16) | (c << 8) | d ))"
}


int_to_ipv4() {
    local value="$1"

    printf '%d.%d.%d.%d\n' \
        "$(( (value >> 24) & 255 ))" \
        "$(( (value >> 16) & 255 ))" \
        "$(( (value >> 8) & 255 ))" \
        "$(( value & 255 ))"
}


ipv4_network() {
    local ip="$1"
    local prefix="$2"

    local ip_int
    local mask
    local network_int

    [[ "$prefix" =~ ^[0-9]+$ ]] || return 1
    (( prefix <= 32 )) || return 1

    ip_int="$(ipv4_to_int "$ip")" || return 1

    if (( prefix == 0 )); then
        mask=0
    else
        mask=$(( (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF ))
    fi

    network_int=$(( ip_int & mask ))

    printf '%s/%s\n' \
        "$(int_to_ipv4 "$network_int")" \
        "$prefix"
}


# -----------------------------------------------------------------------------
# Network database
# -----------------------------------------------------------------------------

reset_network_database() {
    NET_KEYS=()
    NET_SEEN=()
    NET_L3=()
    NET_HOSTS=()
}


build_network_database() {
    reset_network_database

    local ns
    local link_json
    local addr_json

    local ifname
    local ip
    local prefix
    local network
    local obj
    local state
    local kind
    local ifindex
    local ports
    local record

    while IFS= read -r ns; do

        [[ -z "$ns" ]] && continue

        link_json="$(get_link_json "$ns")"
        addr_json="$(get_addr_json "$ns")"

        while IFS=$'\t' read -r \
            ifname \
            ip \
            prefix; do

            [[ -z "$ifname" ]] && continue
            [[ -z "$ip" ]] && continue
            [[ -z "$prefix" ]] && continue

            network="$(
                ipv4_network \
                    "$ip" \
                    "$prefix"
            )" || continue

            obj="$(
                get_interface \
                    "$link_json" \
                    "$ifname"
            )"

            [[ -n "$obj" ]] || continue

            state="$(get_state "$obj")"
            kind="$(get_kind "$obj")"

            if [[ "$kind" == "bridge" ]]; then

                ifindex="$(
                    "$JQ" -r '.ifindex' <<<"$obj"
                )"

                ports="$(
                    bridge_port_count \
                        "$link_json" \
                        "$ifname" \
                        "$ifindex"
                )"

                record="$ns|$ifname|$ip/$prefix|$state|$ports"

                NET_L3["$network"]+="$record"$'\n'

            else

                record="$ns|$ifname|$ip/$prefix|$state"

                NET_HOSTS["$network"]+="$record"$'\n'
            fi

            if [[ -z "${NET_SEEN[$network]+x}" ]]; then
                NET_SEEN["$network"]=1
                NET_KEYS+=("$network")
            fi

        done < <(
            get_ipv4_addresses "$addr_json"
        )

    done < <(
        get_namespaces
    )

    if (( ${#NET_KEYS[@]} > 0 )); then
        mapfile -t NET_KEYS < <(
            printf '%s\n' "${NET_KEYS[@]}" |
                sort -V -u
        )
    fi
}


# -----------------------------------------------------------------------------
# Network view
# -----------------------------------------------------------------------------

render_network_view() {

    build_network_database

    printf 'NETWORKS (IPv4)\n\n'

    local network_count=${#NET_KEYS[@]}

    if (( network_count == 0 )); then
        printf '└── none detected\n'
        return 0
    fi

    local network_index=0
    local network
    local network_last
    local indent

    local -a gateways=()
    local -a hosts=()

    local record
    local ns
    local ifname
    local ip
    local state
    local ports
    local label
    local member_count
    local member_index
    local child_last

    for network in "${NET_KEYS[@]}"; do

        network_index=$((network_index + 1))

        if (( network_index == network_count )); then
            network_last=1
        else
            network_last=0
        fi

        print_branch \
            "" \
            "$network_last" \
            "$network"

        indent="$(
            child_indent \
                "" \
                "$network_last"
        )"

        mapfile -t gateways < <(
            printf '%s' "${NET_L3[$network]-}" |
                sed '/^$/d' |
                sort -t'|' -k3,3V -k1,1V
        )

        mapfile -t hosts < <(
            printf '%s' "${NET_HOSTS[$network]-}" |
                sed '/^$/d' |
                sort -t'|' -k3,3V -k1,1V
        )

        member_count=$(
            printf '%s\n' \
                "${gateways[@]}" \
                "${hosts[@]}" |
            sed '/^$/d' |
            wc -l |
            tr -d ' '
        )

        member_index=0

        if (( member_count == 0 )); then
            print_branch "$indent" 1 "no members"
            continue
        fi

        for record in "${gateways[@]}"; do

            [[ -z "$record" ]] && continue

            IFS='|' read -r \
                ns \
                ifname \
                ip \
                state \
                ports <<<"$record"

            member_index=$((member_index + 1))

            if (( member_index == member_count )); then
                child_last=1
            else
                child_last=0
            fi

            if (( COMPACT )); then
                label="$ip $ns:$ifname [$state]"
            else
                label="gateway $ip $ns:$ifname [$state,bridge] ports=$ports"
            fi

            print_branch \
                "$indent" \
                "$child_last" \
                "$label"
        done

        for record in "${hosts[@]}"; do

            [[ -z "$record" ]] && continue

            IFS='|' read -r \
                ns \
                ifname \
                ip \
                state <<<"$record"

            member_index=$((member_index + 1))

            if (( member_index == member_count )); then
                child_last=1
            else
                child_last=0
            fi

            if (( COMPACT )); then
                label="$ns $ip [$state]"
            else
                label="$ns $ip [$state] dev=$ifname"
            fi

            print_branch \
                "$indent" \
                "$child_last" \
                "$label"
        done
    done
}


# -----------------------------------------------------------------------------
# Summary
# -----------------------------------------------------------------------------

get_veth_pair_counts() {

    local -A pairs=()
    local -A degraded=()

    local ns
    local link_json
    local ifname
    local obj
    local state
    local peer
    local endpoint
    local key

    while IFS= read -r ns; do

        [[ -z "$ns" ]] && continue

        link_json="$(
            get_link_json "$ns"
        )"

        while IFS= read -r ifname; do

            [[ -z "$ifname" ]] && continue

            obj="$(
                get_interface \
                    "$link_json" \
                    "$ifname"
            )"

            [[ -n "$obj" ]] || continue

            endpoint="$ns:$ifname"
            state="$(get_state "$obj")"

            peer="$(
                get_peer_description \
                    "$ns" \
                    "$obj" ||
                    true
            )"

            if [[ -n "$peer" ]]; then
                key="$(
                    canonical_pair_key \
                        "$endpoint" \
                        "$peer"
                )"
            else
                key="$endpoint|MISSING-PEER"
            fi

            pairs["$key"]=1

            if [[ "$state" != "UP" ]] ||
                ! has_lower_up "$obj"; then

                degraded["$key"]=1
            fi

        done < <(
            "$JQ" -r '
                .[]
                | select(.ifname != "lo")
                | select(.linkinfo.info_kind == "veth")
                | .ifname
            ' <<<"$link_json" |
            sort -V
        )

    done < <(
        get_namespaces
    )

    printf '%s\t%s\n' \
        "${#pairs[@]}" \
        "${#degraded[@]}"
}


render_summary() {

    local -a namespaces=()

    mapfile -t namespaces < <(
        get_namespaces
    )

    local namespace_count=${#namespaces[@]}

    local bridge_count=0
    local veth_count=0
    local up_count=0
    local down_count=0

    local ns
    local link_json
    local stats
    local bridges
    local veths
    local ups
    local downs

    for ns in "${namespaces[@]}"; do

        [[ -z "$ns" ]] && continue

        link_json="$(
            get_link_json "$ns"
        )"

        stats="$(
            "$JQ" -r '
                [
                    (
                        [
                            .[]
                            | select(
                                .ifname != "lo"
                                and .linkinfo.info_kind == "bridge"
                            )
                        ]
                        | length
                    ),
                    (
                        [
                            .[]
                            | select(
                                .ifname != "lo"
                                and .linkinfo.info_kind == "veth"
                            )
                        ]
                        | length
                    ),
                    (
                        [
                            .[]
                            | select(
                                .ifname != "lo"
                                and .operstate == "UP"
                            )
                        ]
                        | length
                    ),
                    (
                        [
                            .[]
                            | select(
                                .ifname != "lo"
                                and .operstate == "DOWN"
                            )
                        ]
                        | length
                    )
                ]
                | @tsv
            ' <<<"$link_json"
        )"

        IFS=$'\t' read -r \
            bridges \
            veths \
            ups \
            downs <<<"$stats"

        bridge_count=$((bridge_count + bridges))
        veth_count=$((veth_count + veths))
        up_count=$((up_count + ups))
        down_count=$((down_count + downs))
    done

    local pair_counts
    local veth_pairs
    local degraded_pairs

    pair_counts="$(
        get_veth_pair_counts
    )"

    IFS=$'\t' read -r \
        veth_pairs \
        degraded_pairs <<<"$pair_counts"

    build_network_database

    printf 'NETWORK SUMMARY\n\n'

    printf '├── namespaces: %s\n' \
        "$namespace_count"

    printf '├── IPv4 networks: %s\n' \
        "${#NET_KEYS[@]}"

    printf '├── bridges: %s\n' \
        "$bridge_count"

    printf '├── veth pairs: %s\n' \
        "$veth_pairs"

    printf '├── interfaces UP: %s\n' \
        "$up_count"

    printf '├── interfaces DOWN: %s\n' \
        "$down_count"

    printf '└── degraded veth pairs: %s\n' \
        "$degraded_pairs"
}


# -----------------------------------------------------------------------------
# Topology view
# -----------------------------------------------------------------------------

render_topology_interface() {

    local ns="$1"
    local link_json="$2"
    local addr_json="$3"
    local ifname="$4"
    local indent="$5"
    local last="$6"

    local obj
    local kind
    local state
    local addr
    local label
    local next_indent

    obj="$(
        get_interface \
            "$link_json" \
            "$ifname"
    )"

    [[ -n "$obj" ]] || return 0

    kind="$(get_kind "$obj")"
    state="$(get_state "$obj")"
    addr="$(
        get_addr_summary \
            "$addr_json" \
            "$ifname"
    )"

    if [[ "$kind" == "bridge" ]]; then

        label="$ifname [bridge,$state]"

        [[ -n "$addr" ]] &&
            label+=" $addr"

        print_branch \
            "$indent" \
            "$last" \
            "$label"

        next_indent="$(
            child_indent \
                "$indent" \
                "$last"
        )"

        local bridge_ifindex

        bridge_ifindex="$(
            "$JQ" -r '.ifindex' <<<"$obj"
        )"

        mapfile -t ports < <(
            get_bridge_ports \
                "$link_json" \
                "$ifname" \
                "$bridge_ifindex" |
                sort -V
        )

        if (( ${#ports[@]} > 0 )); then

            print_branch \
                "$next_indent" \
                1 \
                "ports (${#ports[@]})"

            local ports_indent

            ports_indent="$(
                child_indent \
                    "$next_indent" \
                    1
            )"

            local port_index=0
            local port_count=${#ports[@]}
            local port
            local port_obj
            local port_state
            local port_peer
            local port_label
            local port_last

            for port in "${ports[@]}"; do

                port_index=$((port_index + 1))

                if (( port_index == port_count )); then
                    port_last=1
                else
                    port_last=0
                fi

                port_obj="$(
                    get_interface \
                        "$link_json" \
                        "$port"
                )"

                port_state="$(
                    get_state "$port_obj"
                )"

                port_label="$port [$port_state]"

                if [[ "$(get_kind "$port_obj")" == "veth" ]]; then

                    port_peer="$(
                        get_peer_description \
                            "$ns" \
                            "$port_obj" ||
                            true
                    )"

                    [[ -n "$port_peer" ]] &&
                        port_label+=" → $port_peer"
                fi

                print_branch \
                    "$ports_indent" \
                    "$port_last" \
                    "$port_label"
            done
        fi

        return 0
    fi

    label="$ifname [$state]"

    [[ -n "$addr" ]] &&
        label+=" $addr"

    if [[ "$kind" == "veth" ]]; then

        local peer

        peer="$(
            get_peer_description \
                "$ns" \
                "$obj" ||
                true
        )"

        [[ -n "$peer" ]] &&
            label+=" → $peer"
    fi

    print_branch \
        "$indent" \
        "$last" \
        "$label"
}


render_topology_namespace() {

    local ns="$1"
    local link_json="$2"
    local addr_json="$3"
    local indent="$4"
    local last="$5"

    print_branch \
        "$indent" \
        "$last" \
        "$ns"

    local ns_indent

    ns_indent="$(
        child_indent \
            "$indent" \
            "$last"
    )"

    print_branch \
        "$ns_indent" \
        1 \
        "interfaces"

    local interfaces_indent

    interfaces_indent="$(
        child_indent \
            "$ns_indent" \
            1
    )"

    mapfile -t interfaces < <(
        get_root_interfaces "$link_json"
    )

    local count=${#interfaces[@]}
    local index=0
    local ifname
    local interface_last

    if (( count == 0 )); then

        print_branch \
            "$interfaces_indent" \
            1 \
            "none"

        return 0
    fi

    for ifname in "${interfaces[@]}"; do

        index=$((index + 1))

        if (( index == count )); then
            interface_last=1
        else
            interface_last=0
        fi

        render_topology_interface \
            "$ns" \
            "$link_json" \
            "$addr_json" \
            "$ifname" \
            "$interfaces_indent" \
            "$interface_last"
    done
}


# -----------------------------------------------------------------------------
# Detailed interfaces view
# -----------------------------------------------------------------------------

render_detailed_interface() {

    local ns="$1"
    local link_json="$2"
    local addr_json="$3"
    local ifname="$4"
    local indent="$5"
    local last="$6"

    local obj
    local state
    local kind
    local mtu
    local mac

    obj="$(
        get_interface \
            "$link_json" \
            "$ifname"
    )"

    [[ -n "$obj" ]] || return 0

    state="$(get_state "$obj")"
    kind="$(get_kind "$obj")"
    mtu="$(get_mtu "$obj")"
    mac="$(get_mac "$obj")"

    print_branch \
        "$indent" \
        "$last" \
        "$ifname [$state] mtu=$mtu mac=$mac type=$kind"

    local next_indent

    next_indent="$(
        child_indent \
            "$indent" \
            "$last"
    )"

    mapfile -t children < <(
        get_addr_lines \
            "$addr_json" \
            "$ifname"
    )

    if [[ "$kind" == "veth" ]]; then

        local peer

        peer="$(
            get_peer_description \
                "$ns" \
                "$obj" ||
                true
        )"

        [[ -n "$peer" ]] &&
            children+=("peer → $peer")
    fi

    local bridge_ifindex
    local -a ports=()

    if [[ "$kind" == "bridge" ]]; then

        bridge_ifindex="$(
            "$JQ" -r '.ifindex' <<<"$obj"
        )"

        mapfile -t ports < <(
            get_bridge_ports \
                "$link_json" \
                "$ifname" \
                "$bridge_ifindex" |
                sort -V
        )

        if (( ${#ports[@]} > 0 )); then
            children+=("ports (${#ports[@]})")
        fi
    fi

    local child_count=${#children[@]}
    local child_index=0
    local child
    local child_last

    for child in "${children[@]}"; do

        child_index=$((child_index + 1))

        if (( child_index == child_count )); then
            child_last=1
        else
            child_last=0
        fi

        print_branch \
            "$next_indent" \
            "$child_last" \
            "$child"
    done

    if [[ "$kind" == "bridge" && ${#ports[@]} -gt 0 ]]; then

        local ports_indent

        ports_indent="$(
            child_indent \
                "$next_indent" \
                1
        )"

        local port_count=${#ports[@]}
        local port_index=0
        local port
        local port_obj
        local port_state
        local port_kind
        local port_peer
        local port_label
        local port_last

        for port in "${ports[@]}"; do

            port_index=$((port_index + 1))

            if (( port_index == port_count )); then
                port_last=1
            else
                port_last=0
            fi

            port_obj="$(
                get_interface \
                    "$link_json" \
                    "$port"
            )"

            port_state="$(
                get_state "$port_obj"
            )"

            port_kind="$(
                get_kind "$port_obj"
            )"

            port_label="$port [$port_state] type=$port_kind"

            if [[ "$port_kind" == "veth" ]]; then

                port_peer="$(
                    get_peer_description \
                        "$ns" \
                        "$port_obj" ||
                        true
                )"

                [[ -n "$port_peer" ]] &&
                    port_label+=" → $port_peer"
            fi

            print_branch \
                "$ports_indent" \
                "$port_last" \
                "$port_label"
        done
    fi
}


render_interfaces_namespace() {

    local ns="$1"
    local link_json="$2"
    local addr_json="$3"
    local indent="$4"
    local last="$5"

    print_branch \
        "$indent" \
        "$last" \
        "$ns"

    local ns_indent
    local interfaces_indent

    ns_indent="$(
        child_indent \
            "$indent" \
            "$last"
    )"

    print_branch \
        "$ns_indent" \
        1 \
        "interfaces"

    interfaces_indent="$(
        child_indent \
            "$ns_indent" \
            1
    )"

    mapfile -t interfaces < <(
        "$JQ" -r '
            .[]
            | select(.ifname != "lo")
            | .ifname
        ' <<<"$link_json" |
        sort -V
    )

    local count=${#interfaces[@]}
    local index=0
    local ifname
    local interface_last

    if (( count == 0 )); then

        print_branch \
            "$interfaces_indent" \
            1 \
            "none"

        return 0
    fi

    for ifname in "${interfaces[@]}"; do

        index=$((index + 1))

        if (( index == count )); then
            interface_last=1
        else
            interface_last=0
        fi

        render_detailed_interface \
            "$ns" \
            "$link_json" \
            "$addr_json" \
            "$ifname" \
            "$interfaces_indent" \
            "$interface_last"
    done
}


# -----------------------------------------------------------------------------
# Compact technical view
# -----------------------------------------------------------------------------

render_compact_interface() {

    local ns="$1"
    local link_json="$2"
    local addr_json="$3"
    local ifname="$4"
    local indent="$5"
    local last="$6"

    local obj
    local state
    local kind
    local addr
    local label

    obj="$(
        get_interface \
            "$link_json" \
            "$ifname"
    )"

    [[ -n "$obj" ]] || return 0

    state="$(get_state "$obj")"
    kind="$(get_kind "$obj")"
    addr="$(
        get_addr_summary \
            "$addr_json" \
            "$ifname"
    )"

    label="$ifname [$state]"

    [[ -n "$addr" ]] &&
        label+=" $addr"

    if [[ "$kind" == "bridge" ]]; then

        local bridge_ifindex
        local port_count

        bridge_ifindex="$(
            "$JQ" -r '.ifindex' <<<"$obj"
        )"

        port_count="$(
            bridge_port_count \
                "$link_json" \
                "$ifname" \
                "$bridge_ifindex"
        )"

        label+=" [bridge,ports=$port_count]"

    elif [[ "$kind" == "veth" ]]; then

        local peer

        peer="$(
            get_peer_description \
                "$ns" \
                "$obj" ||
                true
        )"

        [[ -n "$peer" ]] &&
            label+=" → $peer"
    fi

    print_branch \
        "$indent" \
        "$last" \
        "$label"
}


render_compact_namespace() {

    local ns="$1"
    local link_json="$2"
    local addr_json="$3"
    local indent="$4"
    local last="$5"

    print_branch \
        "$indent" \
        "$last" \
        "$ns"

    local ns_indent

    ns_indent="$(
        child_indent \
            "$indent" \
            "$last"
    )"

    mapfile -t interfaces < <(
        get_root_interfaces "$link_json"
    )

    local count=${#interfaces[@]}
    local index=0
    local ifname
    local interface_last

    if (( count == 0 )); then

        print_branch \
            "$ns_indent" \
            1 \
            "no interfaces"

        return 0
    fi

    for ifname in "${interfaces[@]}"; do

        index=$((index + 1))

        if (( index == count )); then
            interface_last=1
        else
            interface_last=0
        fi

        render_compact_interface \
            "$ns" \
            "$link_json" \
            "$addr_json" \
            "$ifname" \
            "$ns_indent" \
            "$interface_last"
    done
}


# -----------------------------------------------------------------------------
# Routes
# -----------------------------------------------------------------------------

get_routes() {
    local route_json="$1"

    if (( COMPACT )); then

        "$JQ" -r '
            .[]
            | [
                (.dst // "default"),
                (if .gateway then "via=" + .gateway else empty end),
                (if .dev then "dev=" + .dev else empty end),
                (if .metric then "metric=" + (.metric | tostring) else empty end)
              ]
            | map(select(length > 0))
            | join(" ")
        ' <<<"$route_json"

    else

        "$JQ" -r '
            .[]
            | [
                (.dst // "default"),
                (if .gateway then "via " + .gateway else empty end),
                (if .dev then "dev " + .dev else empty end),
                (if .protocol then "proto " + .protocol else empty end),
                (if .scope then "scope " + .scope else empty end),
                (if .metric then "metric " + (.metric | tostring) else empty end),
                (if .prefsrc then "src " + .prefsrc else empty end)
              ]
            | map(select(length > 0))
            | join(" ")
        ' <<<"$route_json"
    fi
}


render_routes_namespace() {

    local ns="$1"
    local route_json="$2"
    local indent="$3"
    local last="$4"

    print_branch \
        "$indent" \
        "$last" \
        "$ns"

    local ns_indent
    local routes_indent

    ns_indent="$(
        child_indent \
            "$indent" \
            "$last"
    )"

    print_branch \
        "$ns_indent" \
        1 \
        "routes"

    routes_indent="$(
        child_indent \
            "$ns_indent" \
            1
    )"

    mapfile -t routes < <(
        get_routes "$route_json"
    )

    local count=${#routes[@]}
    local index=0
    local route
    local route_last

    if (( count == 0 )); then

        print_branch \
            "$routes_indent" \
            1 \
            "none"

        return 0
    fi

    for route in "${routes[@]}"; do

        index=$((index + 1))

        if (( index == count )); then
            route_last=1
        else
            route_last=0
        fi

        print_branch \
            "$routes_indent" \
            "$route_last" \
            "$route"
    done
}


# -----------------------------------------------------------------------------
# All technical information view
# -----------------------------------------------------------------------------

render_all_namespace() {

    local ns="$1"
    local link_json="$2"
    local addr_json="$3"
    local route_json="$4"
    local indent="$5"
    local last="$6"

    print_branch \
        "$indent" \
        "$last" \
        "$ns"

    local ns_indent
    local interfaces_indent
    local routes_indent

    ns_indent="$(
        child_indent \
            "$indent" \
            "$last"
    )"

    print_branch \
        "$ns_indent" \
        0 \
        "interfaces"

    interfaces_indent="$(
        child_indent \
            "$ns_indent" \
            0
    )"

    local -a interfaces=()
    local count=0
    local index=0
    local ifname
    local interface_last

    if (( COMPACT )); then

        mapfile -t interfaces < <(
            get_root_interfaces "$link_json"
        )

    else

        mapfile -t interfaces < <(
            "$JQ" -r '
                .[]
                | select(.ifname != "lo")
                | .ifname
            ' <<<"$link_json" |
            sort -V
        )
    fi

    count=${#interfaces[@]}

    if (( count == 0 )); then

        print_branch \
            "$interfaces_indent" \
            1 \
            "none"

    else

        for ifname in "${interfaces[@]}"; do

            index=$((index + 1))

            if (( index == count )); then
                interface_last=1
            else
                interface_last=0
            fi

            if (( COMPACT )); then

                render_compact_interface \
                    "$ns" \
                    "$link_json" \
                    "$addr_json" \
                    "$ifname" \
                    "$interfaces_indent" \
                    "$interface_last"

            else

                render_detailed_interface \
                    "$ns" \
                    "$link_json" \
                    "$addr_json" \
                    "$ifname" \
                    "$interfaces_indent" \
                    "$interface_last"
            fi
        done
    fi

    print_branch \
        "$ns_indent" \
        1 \
        "routes"

    routes_indent="$(
        child_indent \
            "$ns_indent" \
            1
    )"

    mapfile -t routes < <(
        get_routes "$route_json"
    )

    count=${#routes[@]}
    index=0

    if (( count == 0 )); then

        print_branch \
            "$routes_indent" \
            1 \
            "none"

        return 0
    fi

    local route
    local route_last

    for route in "${routes[@]}"; do

        index=$((index + 1))

        if (( index == count )); then
            route_last=1
        else
            route_last=0
        fi

        print_branch \
            "$routes_indent" \
            "$route_last" \
            "$route"
    done
}


# -----------------------------------------------------------------------------
# Diagnostics
# -----------------------------------------------------------------------------

add_diag() {
    DIAG_LINES+=("$1")
    DIAG_COUNT=$((DIAG_COUNT + 1))
}


diagnose_veths() {

    local -A seen_pairs=()

    local ns
    local link_json
    local ifname
    local obj
    local kind
    local state
    local peer
    local peer_state
    local endpoint
    local key

    while IFS= read -r ns; do

        [[ -z "$ns" ]] && continue

        link_json="$(
            get_link_json "$ns"
        )"

        while IFS= read -r ifname; do

            [[ -z "$ifname" ]] && continue

            obj="$(
                get_interface \
                    "$link_json" \
                    "$ifname"
            )"

            [[ -n "$obj" ]] || continue

            kind="$(get_kind "$obj")"

            [[ "$kind" == "veth" ]] || continue

            endpoint="$ns:$ifname"
            state="$(get_state "$obj")"

            peer="$(
                get_peer_description \
                    "$ns" \
                    "$obj" ||
                    true
            )"

            if [[ -z "$peer" ]]; then

                add_diag \
                    "⚠ veth peer not found: $endpoint"

                continue
            fi

            key="$(
                canonical_pair_key \
                    "$endpoint" \
                    "$peer"
            )"

            if [[ -n "${seen_pairs[$key]+x}" ]]; then
                continue
            fi

            seen_pairs["$key"]=1

            peer_state="$(
                get_peer_state "$peer" ||
                true
            )"

            if [[ "$state" == "DOWN" ]]; then

                if [[ "$peer_state" == "UP" ]]; then

                    add_diag \
                        "⚠ veth link state mismatch: $endpoint [DOWN] ↔ $peer [UP]"

                elif [[ "$peer_state" == "DOWN" ]]; then

                    add_diag \
                        "⚠ veth link DOWN: $endpoint ↔ $peer"

                else

                    add_diag \
                        "⚠ veth link DOWN: $endpoint ↔ $peer"
                fi

            elif [[ "$peer_state" == "DOWN" ]]; then

                add_diag \
                    "⚠ veth link state mismatch: $endpoint [UP] ↔ $peer [DOWN]"

            elif [[ "$state" != "UP" ]]; then

                add_diag \
                    "⚠ veth link not UP: $endpoint [$state] ↔ $peer${peer_state:+ [$peer_state]}"

            elif [[ -n "$peer_state" && "$peer_state" != "UP" ]]; then

                add_diag \
                    "⚠ veth peer not UP: $endpoint [UP] ↔ $peer [$peer_state]"

            elif ! has_lower_up "$obj"; then

                add_diag \
                    "⚠ veth UP without LOWER_UP: $endpoint ↔ $peer"
            fi

        done < <(
            "$JQ" -r '
                .[]
                | select(.ifname != "lo")
                | select(.linkinfo.info_kind == "veth")
                | .ifname
            ' <<<"$link_json" |
            sort -V
        )

    done < <(
        get_namespaces
    )
}


diagnose_duplicate_ipv4() {

    local -A ip_records=()

    local ns
    local addr_json
    local ifname
    local ip
    local prefix
    local record
    local key

    while IFS= read -r ns; do

        [[ -z "$ns" ]] && continue

        addr_json="$(
            "$IP" -n "$ns" -j -4 addr show
        )"

        while IFS=$'\t' read -r \
            ifname \
            ip \
            prefix; do

            [[ -z "$ip" ]] && continue

            key="$ns|$ip"
            record="$ifname/$prefix"

            ip_records["$key"]+="$record"$'\n'

        done < <(
            get_ipv4_addresses "$addr_json"
        )

    done < <(
        get_namespaces
    )

    local ip_key
    local namespace
    local address
    local -a records=()

    for ip_key in "${!ip_records[@]}"; do

        mapfile -t records < <(
            printf '%s' "${ip_records[$ip_key]}" |
                sed '/^$/d' |
                sort -V
        )

        if (( ${#records[@]} > 1 )); then

            namespace="${ip_key%%|*}"
            address="${ip_key#*|}"

            local text

            text="$(
                printf '%s, ' "${records[@]}"
            )"

            text="${text%, }"

            add_diag \
                "⚠ duplicate IPv4 $address in namespace $namespace: $text"
        fi
    done
}


diagnose_bridge_ports() {

    local ns
    local link_json
    local bridge
    local bridge_obj
    local bridge_ifindex
    local port
    local port_obj
    local port_kind
    local peer

    while IFS= read -r ns; do

        [[ -z "$ns" ]] && continue

        link_json="$(
            get_link_json "$ns"
        )"

        while IFS= read -r bridge; do

            [[ -z "$bridge" ]] && continue

            bridge_obj="$(
                get_interface \
                    "$link_json" \
                    "$bridge"
            )"

            [[ -n "$bridge_obj" ]] || continue

            bridge_ifindex="$(
                "$JQ" -r '.ifindex' <<<"$bridge_obj"
            )"

            while IFS= read -r port; do

                [[ -z "$port" ]] && continue

                port_obj="$(
                    get_interface \
                        "$link_json" \
                        "$port"
                )"

                [[ -n "$port_obj" ]] || continue

                port_kind="$(
                    get_kind "$port_obj"
                )"

                [[ "$port_kind" == "veth" ]] || continue

                peer="$(
                    get_peer_description \
                        "$ns" \
                        "$port_obj" ||
                        true
                )"

                if [[ -z "$peer" ]]; then

                    add_diag \
                        "⚠ bridge port without visible peer: $ns:$bridge/$port"
                fi

            done < <(
                get_bridge_ports \
                    "$link_json" \
                    "$bridge" \
                    "$bridge_ifindex" |
                sort -V
            )

        done < <(
            "$JQ" -r '
                .[]
                | select(.ifname != "lo")
                | select(.linkinfo.info_kind == "bridge")
                | .ifname
            ' <<<"$link_json" |
            sort -V
        )

    done < <(
        get_namespaces
    )
}


diagnose_bridges() {

    local ns
    local link_json
    local bridge
    local bridge_obj
    local bridge_state

    while IFS= read -r ns; do

        [[ -z "$ns" ]] && continue

        link_json="$(
            get_link_json "$ns"
        )"

        while IFS= read -r bridge; do

            [[ -z "$bridge" ]] && continue

            bridge_obj="$(
                get_interface \
                    "$link_json" \
                    "$bridge"
            )"

            [[ -n "$bridge_obj" ]] || continue

            bridge_state="$(
                get_state "$bridge_obj"
            )"

            if [[ "$bridge_state" != "UP" ]]; then

                add_diag \
                    "⚠ bridge not UP: $ns:$bridge [$bridge_state]"
            fi

        done < <(
            "$JQ" -r '
                .[]
                | select(.ifname != "lo")
                | select(.linkinfo.info_kind == "bridge")
                | .ifname
            ' <<<"$link_json" |
            sort -V
        )

    done < <(
        get_namespaces
    )
}


gateway_on_connected_network() {

    local addr_json="$1"
    local gateway="$2"
    local dev="$3"

    local ifname
    local ip
    local prefix
    local local_network
    local gateway_network

    ipv4_to_int "$gateway" >/dev/null 2>&1 || return 1

    while IFS=$'\t' read -r \
        ifname \
        ip \
        prefix; do

        [[ -z "$ifname" ]] && continue
        [[ -z "$ip" ]] && continue
        [[ -z "$prefix" ]] && continue

        if [[ -n "$dev" && "$ifname" != "$dev" ]]; then
            continue
        fi

        local_network="$(
            ipv4_network \
                "$ip" \
                "$prefix"
        )" || continue

        gateway_network="$(
            ipv4_network \
                "$gateway" \
                "$prefix"
        )" || continue

        if [[ "$local_network" == "$gateway_network" ]]; then
            return 0
        fi

    done < <(
        get_ipv4_addresses "$addr_json"
    )

    return 1
}


diagnose_routes() {

    local ns
    local link_json
    local addr_json
    local route_json

    local dst
    local gateway
    local dev
    local route_type

    local route_label
    local route_dev_label
    local dev_obj
    local dev_state

    while IFS= read -r ns; do

        [[ -z "$ns" ]] && continue

        link_json="$(
            get_link_json "$ns"
        )"

        addr_json="$(
            get_addr_json "$ns"
        )"

        route_json="$(
            get_ipv4_route_json "$ns"
        )"

        while IFS=$'\t' read -r \
            dst \
            gateway \
            dev \
            route_type; do

            [[ -z "$dst" ]] && continue
            [[ -z "$gateway" ]] && continue

            [[ "$route_type" == "unicast" ]] || continue

            route_label="$dst"
            route_dev_label=""

            if [[ -n "$gateway" ]]; then
                route_label+=" via $gateway"
            fi

            if [[ -n "$dev" ]]; then
                route_dev_label=" dev $dev"
            fi

            route_label+="$route_dev_label"

            if [[ -z "$dev" ]]; then

                add_diag \
                    "⚠ route gateway without device: $ns: $route_label"

                continue
            fi

            dev_obj="$(
                get_interface \
                    "$link_json" \
                    "$dev" ||
                    true
            )"

            if [[ -z "$dev_obj" ]]; then

                add_diag \
                    "⚠ route uses missing device: $ns: $route_label"

                continue
            fi

            if ! gateway_on_connected_network \
                "$addr_json" \
                "$gateway" \
                "$dev"; then

                add_diag \
                    "⚠ gateway not on connected IPv4 network: $ns: $route_label"

                continue
            fi

            dev_state="$(
                get_state "$dev_obj"
            )"

            if [[ "$dev_state" != "UP" ]]; then

                add_diag \
                    "⚠ route uses DOWN interface: $ns: $route_label [$dev_state]"
            fi

        done < <(
            "$JQ" -r '
                .[]
                | select((.type // "unicast") == "unicast")
                | [
                    (.dst // "default"),
                    (.gateway // ""),
                    (.dev // ""),
                    (.type // "unicast")
                  ]
                | @tsv
            ' <<<"$route_json"
        )

    done < <(
        get_namespaces
    )
}


render_diagnostics() {

    DIAG_LINES=()
    DIAG_COUNT=0

    diagnose_veths
    diagnose_duplicate_ipv4
    diagnose_bridge_ports
    diagnose_bridges
    diagnose_routes

    printf 'DIAGNOSTICS\n\n'

    if (( DIAG_COUNT == 0 )); then
        printf '└── ✓ no problems detected\n'
        return 0
    fi

    local index=0
    local line

    for line in "${DIAG_LINES[@]}"; do

        index=$((index + 1))

        if (( index == DIAG_COUNT )); then
            print_branch "" 1 "$line"
        else
            print_branch "" 0 "$line"
        fi
    done

    printf '\n'

    printf 'SUMMARY\n'
    printf '├── warnings: %s\n' "$DIAG_COUNT"
    printf '└── status: warnings detected\n'
}


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

case "$VIEW" in

    network)
        render_network_view
        ;;

    summary)
        render_summary
        ;;

    diagnose)
        render_diagnostics
        ;;

    topology|interfaces|routes|all)

        printf 'NETWORK NAMESPACES\n\n'

        mapfile -t namespaces < <(
            get_namespaces
        )

        namespace_count=${#namespaces[@]}

        if (( namespace_count == 0 )); then
            printf '└── none\n'
            exit 0
        fi

        for idx in "${!namespaces[@]}"; do

            ns="${namespaces[$idx]}"

            if (( idx == namespace_count - 1 )); then
                ns_last=1
            else
                ns_last=0
            fi

            link_json="$(
                get_link_json "$ns"
            )"

            addr_json="$(
                get_addr_json "$ns"
            )"

            case "$VIEW" in

                topology)

                    if (( COMPACT )); then

                        render_compact_namespace \
                            "$ns" \
                            "$link_json" \
                            "$addr_json" \
                            "" \
                            "$ns_last"

                    else

                        render_topology_namespace \
                            "$ns" \
                            "$link_json" \
                            "$addr_json" \
                            "" \
                            "$ns_last"
                    fi

                    ;;

                interfaces)

                    if (( COMPACT )); then

                        render_compact_namespace \
                            "$ns" \
                            "$link_json" \
                            "$addr_json" \
                            "" \
                            "$ns_last"

                    else

                        render_interfaces_namespace \
                            "$ns" \
                            "$link_json" \
                            "$addr_json" \
                            "" \
                            "$ns_last"
                    fi

                    ;;

                routes)

                    route_json="$(
                        get_route_json "$ns"
                    )"

                    render_routes_namespace \
                        "$ns" \
                        "$route_json" \
                        "" \
                        "$ns_last"

                    ;;

                all)

                    route_json="$(
                        get_route_json "$ns"
                    )"

                    render_all_namespace \
                        "$ns" \
                        "$link_json" \
                        "$addr_json" \
                        "$route_json" \
                        "" \
                        "$ns_last"

                    ;;

            esac

            printf '\n'
        done

        ;;

    *)
        die "internal error: unknown view '$VIEW'"
        ;;

esac
