#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""cidr-aggregate.py -- collapse a list of CIDR blocks into the smallest
equivalent set, and count the addresses they cover.

This recipe does NOT call the ip.xakki.pro API -- collapsing/aggregating a
CIDR list you already have is pure local set arithmetic
(ipaddress.collapse_addresses from the standard library), the same honest
"no API needed here" case as subnet-membership.js in this section.

Reads a text file with one CIDR block per line (IPv4 and IPv6 may be mixed
freely -- each family is collapsed separately, since a v4 prefix and a v6
prefix can never overlap or merge). Prints the minimal equivalent CIDR set
per family, plus how many individual addresses the input covers before and
after collapsing (collapsing removes redundant overlap, it never changes
the covered address count).

Zero dependencies -- stdlib only (ipaddress, argparse). Python 3.8+.

Usage:
    python3 cidr-aggregate.py prefixes.txt
"""

import argparse
import ipaddress
import sys


def main():
    parser = argparse.ArgumentParser(description="Collapse a list of CIDR blocks into the minimal equivalent set.")
    parser.add_argument("input_file", help="Path to a text file, one CIDR block per line.")
    args = parser.parse_args()

    v4_networks, v6_networks = [], []
    errors = []
    with open(args.input_file, encoding="utf-8") as f:
        for line_no, raw_line in enumerate(f, start=1):
            line = raw_line.strip()
            if not line or line.startswith("#"):
                continue
            try:
                # strict=False: "10.1.2.3/24" is accepted and normalized to
                # "10.1.2.0/24" rather than rejected for having host bits set
                # -- this recipe aggregates real-world lists, which routinely
                # contain that shape.
                network = ipaddress.ip_network(line, strict=False)
            except ValueError as e:
                errors.append("line {}: {!r} -- {}".format(line_no, line, e))
                continue
            (v6_networks if network.version == 6 else v4_networks).append(network)

    if errors:
        print("skipped {} malformed line(s):".format(len(errors)), file=sys.stderr)
        for err in errors:
            print("  " + err, file=sys.stderr)

    for family, networks in (("IPv4", v4_networks), ("IPv6", v6_networks)):
        if not networks:
            continue
        collapsed = list(ipaddress.collapse_addresses(networks))
        addresses_before = sum(n.num_addresses for n in networks)
        addresses_after = sum(n.num_addresses for n in collapsed)
        print("\n{}: {} input block(s) -> {} collapsed block(s)".format(family, len(networks), len(collapsed)))
        print("  addresses covered: {} (collapsing never changes this total, "
              "it only removes redundant overlap)".format(addresses_after))
        if addresses_before != addresses_after:
            # Only possible if the input itself double-counted overlapping
            # ranges before collapsing folded them together.
            print("  (raw sum of individual input blocks was {}, {} of which was overlap)".format(
                addresses_before, addresses_before - addresses_after))
        for network in collapsed:
            print("  " + str(network))

    if not v4_networks and not v6_networks:
        print("no valid CIDR blocks found in {}".format(args.input_file), file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()
