"""Nouvelles CVE de la stack — lecture seule.

Interroge l'API publique en GET uniquement :

- ``/api/v1/organizations``
- ``/api/v1/organizations/{org_id}/projects``
- ``/api/v1/organizations/{org_id}/projects/{project_id}/alerts?since=``

La clé est lue dans ``TWA_API_KEY``. ``TWA_SINCE`` (ISO 8601) borne la fenêtre ;
vide, ce sont les dernières 24 heures. ``TWA_ORG_ID`` et ``TWA_PROJECT_ID``
restreignent le parcours aux identifiants renvoyés par l'API.

Code 0 s'il n'y a aucune alerte, 1 s'il y en a, 2 si l'appel échoue.
"""
from __future__ import annotations

import json
import os
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timedelta, timezone

LIMITE = 200
PAGES_MAX = 50
_UUID = re.compile(
    r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
)


class ErreurApi(Exception):
    pass


def depuis(valeur: str | None) -> str:
    if valeur and valeur.strip():
        return valeur.strip()
    instant = datetime.now(timezone.utc) - timedelta(hours=24)
    return instant.strftime("%Y-%m-%dT%H:%M:%SZ")


def _joindre(base: str, chemin: str, params: dict | None) -> str:
    url = base.rstrip("/") + chemin
    if params:
        return url + "?" + urllib.parse.urlencode(params)
    return url


def lire_http(cle: str):
    """Rend une fonction ``url -> JSON`` qui ne fait que des GET."""

    def lire(url: str):
        req = urllib.request.Request(
            url,
            method="GET",
            headers={
                "Authorization": f"Bearer {cle}",
                "Accept": "application/json",
                "User-Agent": "TechWatchAlert-Action/1",
            },
        )
        try:
            with urllib.request.urlopen(req, timeout=30) as rep:
                return json.load(rep)
        except urllib.error.HTTPError as exc:
            corps = exc.read().decode("utf-8", "replace")[:300]
            raise ErreurApi(f"HTTP {exc.code} sur {url.split('?', 1)[0]} : {corps}") from None
        except urllib.error.URLError as exc:
            raise ErreurApi(f"réseau : {exc.reason}") from None

    return lire


def collecter(lire, *, base: str, since: str, org_id: str | None, project_id: str | None) -> list[dict]:
    """Alertes déclenchées depuis ``since``, pour les projets visibles par la clé."""
    orgs = lire(_joindre(base, "/api/v1/organizations", None))
    if not isinstance(orgs, list):
        raise ErreurApi("la liste des organisations n'est pas une liste")
    if org_id:
        orgs = [o for o in orgs if isinstance(o, dict) and o.get("id") == org_id]
        if not orgs:
            raise ErreurApi("organisation introuvable pour cette clé")

    trouvailles = []
    projet_vu = False
    for org in orgs:
        if not isinstance(org, dict) or not _UUID.fullmatch(str(org.get("id") or "")):
            continue
        oid = org["id"]
        projets = lire(_joindre(base, f"/api/v1/organizations/{oid}/projects", None))
        if not isinstance(projets, list):
            raise ErreurApi("la liste des projets n'est pas une liste")
        if project_id:
            projets = [p for p in projets if isinstance(p, dict) and p.get("id") == project_id]
        if not projets:
            continue
        if project_id:
            projet_vu = True
        for projet in projets:
            if not isinstance(projet, dict) or not _UUID.fullmatch(str(projet.get("id") or "")):
                continue
            pid = projet["id"]
            page = 1
            while page <= PAGES_MAX:
                charge = lire(_joindre(
                    base,
                    f"/api/v1/organizations/{oid}/projects/{pid}/alerts",
                    {"since": since, "page": page, "limit": LIMITE},
                ))
                if not isinstance(charge, dict):
                    raise ErreurApi("la liste des alertes n'est pas un objet")
                items = charge.get("items") or []
                for item in items:
                    if not isinstance(item, dict):
                        continue
                    trouvailles.append({
                        "organisation": org.get("name") or oid,
                        "projet": projet.get("name") or pid,
                        "cve_id": item.get("cve_id"),
                        "severity": item.get("severity"),
                        "vendor": item.get("vendor"),
                        "product": item.get("product"),
                        "triggered_at": item.get("triggered_at"),
                    })
                total = int(charge.get("total") or 0)
                if not items or page * LIMITE >= total:
                    break
                page += 1
    if project_id and not projet_vu:
        raise ErreurApi("projet introuvable pour cette clé")
    return trouvailles


def formater(alertes: list[dict], since: str) -> str:
    if not alertes:
        return f"Aucune nouvelle alerte depuis {since}."
    lignes = [f"{len(alertes)} alerte(s) depuis {since}.", ""]
    for alerte in alertes:
        lignes.append(
            f"- {alerte['cve_id']} {alerte['severity']} — "
            f"{alerte['vendor'] or '?'}/{alerte['product'] or '?'} — "
            f"{alerte['organisation']} / {alerte['projet']} — {alerte['triggered_at']}"
        )
    return "\n".join(lignes)


def main() -> int:
    cle = os.environ.get("TWA_API_KEY", "").strip()
    if not cle:
        print("TWA_API_KEY absent", file=sys.stderr)
        return 2
    base = os.environ.get("TWA_BASE_URL", "https://app.techwatchalert.com").strip()
    since = depuis(os.environ.get("TWA_SINCE"))
    try:
        alertes = collecter(
            lire_http(cle),
            base=base,
            since=since,
            org_id=(os.environ.get("TWA_ORG_ID") or "").strip() or None,
            project_id=(os.environ.get("TWA_PROJECT_ID") or "").strip() or None,
        )
    except ErreurApi as exc:
        print(str(exc), file=sys.stderr)
        return 2
    texte = formater(alertes, since)
    print(texte)
    sommaire = os.environ.get("GITHUB_STEP_SUMMARY")
    if sommaire:
        with open(sommaire, "a", encoding="utf-8") as fichier:
            fichier.write(texte + "\n")
    return 1 if alertes else 0


if __name__ == "__main__":
    raise SystemExit(main())
