#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""geofeed-parse.py -- parse and validate an RFC 8805 self-published geofeed
CSV, aggregating prefix counts by country and reporting any syntax errors
with line numbers.

This recipe does NOT call the ip.xakki.pro API -- validating a geofeed file
you already downloaded is pure local parsing (stdlib csv + ipaddress), the
same honest "no API needed here" case as subnet-membership.js and
cidr-aggregate.py in this section. (Our own service ingests geofeeds as ONE
input among several for its own geo data -- see /docs/ -- but reading and
checking a geofeed file's syntax is not itself an IP lookup.)

RFC 8805 defines an unquoted CSV, one prefix per line, in this exact column
order: prefix,country[,region[,city[,postal code]]] -- only "prefix" and
"country" are required; region/city/postal may be blank or omitted
entirely. This script validates:
  - prefix parses as a valid IPv4 or IPv6 CIDR block.
  - country is exactly 2 letters (ISO 3166-1 alpha-2 shape; NOT checked
    against the real country-code list, since RFC 8805 itself doesn't
    require that and new codes do get assigned).
  - the line has between 2 and 5 comma-separated fields.

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

Usage:
    python3 geofeed-parse.py geofeed.csv
"""

import argparse
import csv
import ipaddress
import re
from collections import Counter

COUNTRY_RE = re.compile(r"^[A-Za-z]{2}$")


def parse_geofeed(path):
    """Returns (rows, errors). rows is a list of dicts
    {prefix, country, region, city, postal, line}; errors is a list of
    "line N: message" strings for every row that failed validation."""
    rows, errors = [], []
    with open(path, newline="", encoding="utf-8") as f:
        for line_no, fields in enumerate(csv.reader(f), start=1):
            if not fields or (len(fields) == 1 and fields[0].strip() == ""):
                continue  # blank line, not a syntax error
            if not (2 <= len(fields) <= 5):
                errors.append("line {}: expected 2-5 fields (prefix,country[,region[,city[,postal]]]), got {}".format(
                    line_no, len(fields)))
                continue

            prefix_raw, country_raw = fields[0].strip(), fields[1].strip()
            try:
                prefix = ipaddress.ip_network(prefix_raw, strict=False)
            except ValueError as e:
                errors.append("line {}: invalid prefix {!r} -- {}".format(line_no, prefix_raw, e))
                continue
            if not COUNTRY_RE.match(country_raw):
                errors.append("line {}: invalid country code {!r} (expected 2 letters)".format(line_no, country_raw))
                continue

            rows.append({
                "prefix": str(prefix),
                "country": country_raw.upper(),
                "region": fields[2].strip() if len(fields) > 2 else "",
                "city": fields[3].strip() if len(fields) > 3 else "",
                "postal": fields[4].strip() if len(fields) > 4 else "",
                "line": line_no,
            })
    return rows, errors


def main():
    parser = argparse.ArgumentParser(description="Parse and validate an RFC 8805 geofeed CSV.")
    parser.add_argument("input_file", help="Path to the geofeed CSV file.")
    parser.add_argument("--top", type=int, default=20, help="How many countries to print (default: 20).")
    args = parser.parse_args()

    rows, errors = parse_geofeed(args.input_file)

    print("{} valid row(s), {} error(s)".format(len(rows), len(errors)))
    if errors:
        print("\nSyntax errors:")
        for err in errors:
            print("  " + err)

    per_country = Counter(row["country"] for row in rows)
    print("\nTop {} countries by prefix count:".format(args.top))
    for country, count in per_country.most_common(args.top):
        print("  {:6d}  {}".format(count, country))

    addresses_total = sum(ipaddress.ip_network(row["prefix"]).num_addresses for row in rows)
    print("\n{} prefixes covering {} addresses total (no overlap collapsing -- see "
          "cidr-aggregate.py in this section for that)".format(len(rows), addresses_total))


if __name__ == "__main__":
    main()
