name: Auto-label issues

on:
  issues:
    types: [opened, edited]

concurrency:
  group: auto-label-${{ github.event.issue.number || github.event.issue.index || 'manual' }}
  cancel-in-progress: true

jobs:
  label:
    runs-on: mac-ci
    container:
      image: mcr.microsoft.com/devcontainers/javascript-node:22-bookworm

    steps:
      - name: Apply labels from Type/State/Category
        env:
          # IMPORTANT: préfère FORGE_BASE (LAN) si défini, sinon FORGE_API
          FORGE_BASE: ${{ vars.FORGE_BASE || vars.FORGE_API || vars.FORGE_API_BASE }}
          FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
          REPO_FULL: ${{ gitea.repository }}
          EVENT_PATH: ${{ github.event_path }}
          NODE_OPTIONS: --dns-result-order=ipv4first
        run: |
          python3 - <<'PY'
          import json, os, re, time, urllib.request, urllib.error, socket

          forge = (os.environ.get("FORGE_BASE") or "").rstrip("/")
          if not forge:
              raise SystemExit("Missing FORGE_BASE/FORGE_API repo variable (e.g. http://192.168.1.20:3000)")

          token = os.environ.get("FORGE_TOKEN") or ""
          if not token:
              raise SystemExit("Missing secret FORGE_TOKEN")

          owner, repo = os.environ["REPO_FULL"].split("/", 1)
          event_path = os.environ["EVENT_PATH"]

          with open(event_path, "r", encoding="utf-8") as f:
              ev = json.load(f)

          issue = ev.get("issue") or {}
          title = issue.get("title") or ""
          body = issue.get("body") or ""
          number = issue.get("number") or issue.get("index")
          if not number:
              raise SystemExit("No issue number/index in event payload")

          def pick_line(key: str) -> str:
              m = re.search(rf"^\s*{re.escape(key)}\s*:\s*([^\n\r]+)", body, flags=re.M)
              return m.group(1).strip() if m else ""

          desired = set()

          t = pick_line("Type")
          s = pick_line("State")
          c = pick_line("Category")

          print("PARSED:", {"Type": t, "State": s, "Category": c})

          # 1) explicite depuis le body
          if t: desired.add(t)
          if s: desired.add(s)
          if c: desired.add(c)

          # 2) fallback depuis le titre si Type absent
          if not t:
              if title.startswith("[Correction]"):
                  desired.add("type/correction")
              elif title.startswith("[Fact-check]") or title.startswith("[Vérification]"):
                  desired.add("type/fact-check")

          # 3) fallback State si absent
          if not s:
              if "type/fact-check" in desired:
                  desired.add("state/a-sourcer")
              elif "type/correction" in desired:
                  desired.add("state/recevable")

          if not desired:
              print("No labels to apply.")
              raise SystemExit(0)

          api = forge + "/api/v1"
          headers = {
              "Authorization": f"token {token}",
              "Accept": "application/json",
              "Content-Type": "application/json",
              "User-Agent": "archicratie-auto-label/1.1",
          }

          def jreq(method, url, payload=None, timeout=60, retries=4, backoff=2.0):
              data = None if payload is None else json.dumps(payload).encode("utf-8")
              last_err = None
              for i in range(retries):
                  req = urllib.request.Request(url, data=data, headers=headers, method=method)
                  try:
                      with urllib.request.urlopen(req, timeout=timeout) as r:
                          b = r.read()
                          return json.loads(b.decode("utf-8")) if b else None
                  except urllib.error.HTTPError as e:
                      b = e.read().decode("utf-8", errors="replace")
                      raise RuntimeError(f"HTTP {e.code} {method} {url}\n{b}") from e
                  except (TimeoutError, socket.timeout, urllib.error.URLError) as e:
                      last_err = e
                      # retry only on network/timeout
                      time.sleep(backoff * (i + 1))
              raise RuntimeError(f"Network/timeout after retries: {method} {url}\n{last_err}")

          # labels repo
          labels = jreq("GET", f"{api}/repos/{owner}/{repo}/labels?limit=1000", timeout=60) or []
          name_to_id = {x.get("name"): x.get("id") for x in labels}

          missing = [x for x in desired if x not in name_to_id]
          if missing:
              raise SystemExit("Missing labels in repo: " + ", ".join(sorted(missing)))

          wanted_ids = sorted({int(name_to_id[x]) for x in desired})

          # labels actuels de l'issue
          current = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels", timeout=60) or []
          current_ids = {int(x.get("id")) for x in current if x.get("id") is not None}

          final_ids = sorted(current_ids.union(wanted_ids))

          # Replace labels = union (n'enlève rien)
          url = f"{api}/repos/{owner}/{repo}/issues/{number}/labels"

          # IMPORTANT: on n'envoie JAMAIS une liste brute ici (ça a causé le 422)
          jreq("PUT", url, {"labels": final_ids}, timeout=90, retries=4)

          # vérif post-apply (anti "timeout mais appliqué")
          post = jreq("GET", f"{api}/repos/{owner}/{repo}/issues/{number}/labels", timeout=60) or []
          post_ids = {int(x.get("id")) for x in post if x.get("id") is not None}

          missing_ids = [i for i in wanted_ids if i not in post_ids]
          if missing_ids:
              raise RuntimeError(f"Labels not applied after PUT (missing ids): {missing_ids}")

          print(f"OK labels #{number}: {sorted(desired)}")
          PY