#!/usr/bin/env python3
"""Validera geofeed-filerna mot RFC 8805.

Körs av CI på varje pull request. Exit 1 vid fel.

Bakgrund: en rad låg med sex fält i stället för fem i tre månader —

    46.252.2.0/24,SE,DE,DE-BE,Berlin,

Prefixet hade flyttats till Tyskland men DE-uppgifterna skrevs efter det
befintliga "SE" i stället för att ersätta det. En parser som läser
positionellt fick country=SE, och tyska kunder fick svenska Google-annonser
och priser i SEK. Sju supportärenden innan någon tittade i filen.

Kontroller
    FEL   fel antal fält, ogiltigt prefix, ogiltig landskod,
          region som inte hör till landet, dubblett av ett prefix,
          samma prefix med olika land i två filer
    VARN  tom feedfil, misstänkt brett prefix, privat/reserverad rymd

Ingen tredjepartsberoende — bara stdlib.
"""
from __future__ import annotations

import ipaddress
import pathlib
import re
import sys
from collections import defaultdict

FIELDS = ("prefix", "country", "region", "city", "postal")
CC_RE = re.compile(r"^[A-Za-z]{2}$")

# ISO 3166-1 alpha-2. RFC 8805 kräver den listan och inget annat — "UK" är en
# vanlig felskrivning som ser giltig ut men inte är det; Storbritannien är "GB".
ISO3166 = set(
    "AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR "
    "BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ "
    "EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW "
    "GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY "
    "KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV "
    "MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY "
    "QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG "
    "TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW"
    .split())
COMMON_MISTAKES = {"UK": "GB", "EL": "GR", "EN": "GB", "SU": "RU"}
ROOT = pathlib.Path(__file__).resolve().parent


def feeds():
    """Filerna att validera, en gång var.

    geofeed.csv i roten är källan. Varumärkessökvägarna är symlänkar hit så att
    länkar vi redan delat ut fortsätter fungera — de ska inte valideras en gång
    till, annars rapporteras varje prefix som sjufaldig dubblett.
    """
    out, seen = [], set()
    for p in [ROOT / "geofeed.csv"] + sorted(ROOT.glob("*/geofeed.csv")):
        if not p.exists():
            continue
        real = p.resolve()
        if real in seen:
            continue
        seen.add(real)
        out.append(p)
    return out


def main() -> int:
    errors: list[str] = []
    warnings: list[str] = []
    seen: dict[str, list[tuple[str, int, str]]] = defaultdict(list)
    total = 0

    files = feeds()
    if not files:
        print("Hittade ingen geofeed.csv — fel katalog?", file=sys.stderr)
        return 1
    # Varje varumärkessökväg måste peka på rotfilen, annars kan en gammal länk
    # tyst servera inaktuellt innehåll.
    for d in sorted(x for x in ROOT.iterdir() if x.is_dir() and not x.name.startswith(".")):
        link = d / "geofeed.csv"
        if not link.exists():
            errors.append(f"{d.name}/geofeed.csv saknas — gammal länk skulle ge 404")
        elif not link.is_symlink():
            errors.append(f"{d.name}/geofeed.csv är en egen fil, inte en symlänk till "
                          f"../geofeed.csv — två källor som kan glida isär")
        elif link.resolve() != (ROOT / "geofeed.csv").resolve():
            errors.append(f"{d.name}/geofeed.csv pekar på {link.resolve()}, "
                          f"inte på rotens geofeed.csv")

    for path in files:
        brand = path.name if path.parent == ROOT else path.parent.name
        rows = 0
        for lineno, raw in enumerate(path.read_text(errors="replace").splitlines(), 1):
            line = raw.rstrip("\r")
            if not line.strip() or line.lstrip().startswith("#"):
                continue
            rows += 1
            total += 1
            where = f"{brand}/geofeed.csv:{lineno}"
            parts = line.split(",")

            if len(parts) != 5:
                errors.append(
                    f"{where}: {len(parts)} fält, RFC 8805 kräver exakt 5 "
                    f"({','.join(FIELDS)})\n      {line}")
                continue

            prefix, country, region, city, postal = parts

            try:
                net = ipaddress.ip_network(prefix, strict=True)
            except ValueError as e:
                errors.append(f"{where}: ogiltigt prefix {prefix!r} — {e}\n      {line}")
                continue

            cc = country.upper()
            if not CC_RE.match(country) or cc not in ISO3166:
                hint = (f" — menade du {COMMON_MISTAKES[cc]!r}?"
                        if cc in COMMON_MISTAKES else "")
                errors.append(f"{where}: ogiltig landskod {country!r}, "
                              f"ska vara ISO 3166-1 alpha-2{hint}\n      {line}")
            elif region and not region.upper().startswith(country.upper() + "-"):
                errors.append(f"{where}: region {region!r} hör inte till land {country!r}, "
                              f"ska vara ISO 3166-2 t.ex. {country.upper()}-BE\n      {line}")

            if net.is_private or net.is_reserved or net.is_loopback:
                warnings.append(f"{where}: {prefix} är privat/reserverad rymd")
            if net.version == 4 and net.prefixlen < 12:
                warnings.append(f"{where}: {prefix} är ovanligt brett (/{net.prefixlen})")

            seen[str(net)].append((brand, lineno, country.upper()))

        if rows == 0:
            warnings.append(f"{brand}: tom — inga prefix publiceras")

    for prefix, hits in sorted(seen.items()):
        if len(hits) < 2:
            continue
        where = ", ".join(f"{b}:{ln}" for b, ln, _ in hits)
        countries = {cc for _, _, cc in hits}
        if len(countries) > 1:
            errors.append(f"{prefix}: deklareras med olika land "
                          f"({', '.join(sorted(countries))}) på {where} — vilken som vinner "
                          f"är odefinierat")
        else:
            errors.append(f"{prefix}: deklareras {len(hits)} gånger ({where}) — ta bort dubbletten")

    for w in warnings:
        print(f"VARN  {w}")
    for e in errors:
        print(f"FEL   {e}")

    print(f"\n{total} rader i {len(files)} feeds — {len(errors)} fel, {len(warnings)} varningar")
    if errors:
        print("\nRFC 8805: prefix,country,region,city,postal — exakt fem fält.")
        print("Tomma fält behåller sina kommatecken: 94.142.224.0/24,SE,,,")
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())
