#!/usr/bin/env python3
#
# repology-get-maintainer-stats.py - print aggregate FreeBSD ports maintainer
# statistics as reported by repology.org.
#
# MAINTAINER= yuri@FreeBSD.org
#

import argparse
import getpass
import importlib.util
import re
import sys
from urllib.parse import quote

REPOLOGY_MAINTAINER_URL = "https://repology.org/maintainer/{maintainer}"
USER_AGENT = (
    "repology-get-maintainer-stats.py "
    "(FreeBSD ports tools; https://www.freebsd.org/ports/)"
)


def check_dependencies() -> None:
    """Verify that all non-stdlib dependencies are available."""
    missing = []
    for module, package in (("requests", "requests"), ("bs4", "beautifulsoup4")):
        if importlib.util.find_spec(module) is None:
            missing.append(package)
    if missing:
        pkg_prefix = f" py{sys.version_info.major}{sys.version_info.minor}-"
        print(
            "Error: missing required package(s): " + ", ".join(missing),
            file=sys.stderr,
        )
        print(
            f"Install with: sudo pkg install{pkg_prefix}" + pkg_prefix.join(missing),
            file=sys.stderr,
        )
        sys.exit(1)


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Print FreeBSD ports maintainer statistics from repology.org."
    )
    parser.add_argument(
        "maintainer",
        nargs="?",
        default=f"{getpass.getuser()}@FreeBSD.org",
        help="maintainer e-mail address (default: %(default)s)",
    )
    args = parser.parse_args()
    if len(sys.argv) > 2:
        parser.error("at most one maintainer argument is allowed")
    return args


def fetch_maintainer_page(maintainer: str) -> str:
    import requests

    url = REPOLOGY_MAINTAINER_URL.format(maintainer=quote(maintainer, safe="@"))
    try:
        response = requests.get(url, headers={"User-Agent": USER_AGENT}, timeout=30)
    except requests.RequestException as exc:
        print(f"Error fetching {url}: {exc}", file=sys.stderr)
        sys.exit(1)
    if response.status_code == 404:
        print(
            f"Error: maintainer '{maintainer}' was not found on repology.org.",
            file=sys.stderr,
        )
        sys.exit(1)
    response.raise_for_status()
    return response.text


def _cell_text(cell) -> str:
    """Return stripped text from a BeautifulSoup table cell."""
    return " ".join(cell.stripped_strings)


def extract_freebsd_stats(html: str) -> dict[str, str]:
    """Parse the repology maintainer page and return FreeBSD Ports aggregate stats."""
    from bs4 import BeautifulSoup

    soup = BeautifulSoup(html, "html.parser")

    # The page contains multiple tables; the one we need has a header with
    # "Repository" and "Packages".
    table = None
    for tbl in soup.find_all("table"):
        header_cells = tbl.find_all("th")
        texts = [_cell_text(c).lower() for c in header_cells]
        if "repository" in texts and "packages" in texts:
            table = tbl
            break

    if table is None:
        print(
            "Error: could not locate the repository activity table.",
            file=sys.stderr,
        )
        sys.exit(1)

    freebsd_row = None
    total_row = None
    tbody = table.find("tbody") or table
    for row in tbody.find_all("tr"):
        cells = row.find_all(["td", "th"])
        if not cells:
            continue
        first = _cell_text(cells[0]).strip()
        if re.search(r"FreeBSD\s+Ports", first, re.IGNORECASE):
            freebsd_row = cells
        elif first.lower() == "total":
            total_row = cells

    row = freebsd_row or total_row
    if row is None:
        print(
            "Error: no FreeBSD Ports or Total statistics row found.",
            file=sys.stderr,
        )
        sys.exit(1)

    # Repology collapses empty (count, percentage) pairs into a single cell
    # with colspan=2. Expand those cells so each logical column has its own
    # entry: Repository, Packages, Projects Total, Newest, Newest%, Outdated,
    # Outdated%, Problematic, Problematic%, Potentially vulnerable,
    # Potentially vulnerable%, Other lists, Feeds.
    def _expand_cells(cells):
        expanded = []
        for cell in cells:
            text = _cell_text(cell).strip()
            colspan = int(cell.get("colspan", 1) or 1)
            expanded.extend([text] * colspan)
        return expanded

    texts = _expand_cells(row)
    if len(texts) < 13:
        print(
            "Error: unexpected table row format on repology.org.",
            file=sys.stderr,
        )
        sys.exit(1)

    return {
        "repository": texts[0],
        "packages": texts[1],
        "projects_total": texts[2],
        "newest": texts[3],
        "newest_pct": texts[4],
        "outdated": texts[5],
        "outdated_pct": texts[6],
        "problematic": texts[7],
        "problematic_pct": texts[8],
        "vulnerable": texts[9],
        "vulnerable_pct": texts[10],
    }


def format_table(stats: dict[str, str]) -> str:
    headers = [
        "Packages",
        "Projects Total",
        "Newest",
        "Outdated",
        "Problematic",
        "Potentially Vulnerable",
    ]
    values = [
        stats["packages"],
        stats["projects_total"],
        f"{stats['newest']} ({stats['newest_pct']})",
        f"{stats['outdated']} ({stats['outdated_pct']})",
        f"{stats['problematic']} ({stats['problematic_pct']})",
        f"{stats['vulnerable']} ({stats['vulnerable_pct']})",
    ]

    widths = [
        max(len(h), len(v)) for h, v in zip(headers, values)
    ]

    def horizontal() -> str:
        return "+" + "+".join("-" * (w + 2) for w in widths) + "+"

    def line(cells) -> str:
        return (
            "|"
            + "|".join(
                f" {cell.ljust(width)} " for cell, width in zip(cells, widths)
            )
            + "|"
        )

    return "\n".join(
        [horizontal(), line(headers), horizontal(), line(values), horizontal()]
    )


def main() -> None:
    check_dependencies()
    args = parse_args()
    maintainer = args.maintainer

    print(f"Maintainer: {maintainer}")
    html = fetch_maintainer_page(maintainer)
    stats = extract_freebsd_stats(html)
    print(format_table(stats))


if __name__ == "__main__":
    main()
