#!/usr/bin/env python3
"""
Bulk apply TAG-SED and TAG-SEI to SANEF DMZ assets via Qualys QPS REST API.

Usage:
    # Dry run (default) - just lists what would happen
    python apply_sed_sei_tags.py

    # Apply for real
    python apply_sed_sei_tags.py --apply

Auth:
    Set env vars QUALYS_URL, QUALYS_USER, QUALYS_PASS
    OR script will prompt interactively.

Files expected:
    C:\\Claude\\sanef\\QL\\inputs\\bulk_tag_sed.txt (9 hostnames)
    C:\\Claude\\sanef\\QL\\inputs\\bulk_tag_sei.txt (21 hostnames)
"""
import os
import sys
import time
import getpass
import requests
import urllib3

urllib3.disable_warnings()

URL = os.environ.get("QUALYS_URL", "https://qualysapi.qualys.eu")
USER = os.environ.get("QUALYS_USER") or input("Qualys user: ")
PASS = os.environ.get("QUALYS_PASS") or getpass.getpass("Qualys password: ")

APPLY = "--apply" in sys.argv

session = requests.Session()
session.auth = (USER, PASS)
session.headers.update({
    "X-Requested-With": "SANEF-bulk-sed-sei",
    "Content-Type": "application/json",
})
session.verify = False
# Force no proxy (ignore HTTP_PROXY/HTTPS_PROXY env vars)
session.trust_env = False
session.proxies = {"http": "", "https": ""}


def call(endpoint, body=None, retries=3):
    for attempt in range(retries):
        r = session.post(URL + endpoint, json=body, timeout=60)
        if r.status_code == 200:
            return r.text
        if r.status_code == 409 or r.status_code == 429:  # rate limit
            print(f"    [RATE LIMIT] wait 30s...")
            time.sleep(30)
            continue
        print(f"    [HTTP {r.status_code}] {endpoint}")
        print(f"    Response body (first 500 chars): {r.text[:500]}")
        r.raise_for_status()
    raise RuntimeError(f"Max retries exceeded for {endpoint}")


def parse_xml(text, tag):
    s, e = f"<{tag}>", f"</{tag}>"
    pos = 0
    out = []
    while True:
        i = text.find(s, pos)
        if i == -1:
            break
        i += len(s)
        j = text.find(e, i)
        if j == -1:
            break
        out.append(text[i:j].strip())
        pos = j + len(e)
    return out


def find_tag_id(name):
    body = {"ServiceRequest": {"filters": {"Criteria": [
        {"field": "name", "operator": "EQUALS", "value": name}
    ]}}}
    resp = call("/qps/rest/2.0/search/am/tag", body)
    ids = parse_xml(resp, "id")
    return ids[0] if ids else None


def _search_hostasset(field, operator, value):
    body = {"ServiceRequest": {"filters": {"Criteria": [
        {"field": field, "operator": operator, "value": value}
    ]}, "preferences": {"limitResults": 20}}}
    return call("/qps/rest/2.0/search/am/hostasset", body)


def find_asset_id(name):
    # Strategy 1: exact match on name (short hostname)
    resp = _search_hostasset("name", "EQUALS", name)
    if "<HostAsset>" in resp:
        block = resp.split("<HostAsset>", 1)[1].split("</HostAsset>", 1)[0]
        ids = parse_xml(block, "id")
        if ids:
            return ids[0]
    # Strategy 2: CONTAINS name â€” couvre les cas FQDN stockÃ©s (vrosapsrv1 -> vrosapsrv1.sanef.groupe)
    resp = _search_hostasset("name", "CONTAINS", name)
    blocks = [b.split("</HostAsset>", 1)[0] for b in resp.split("<HostAsset>")[1:]]
    for b in blocks:
        nm = (parse_xml(b, "name") or [""])[0].lower()
        if nm == name.lower() or nm.startswith(name.lower() + "."):
            ids = parse_xml(b, "id")
            if ids:
                return ids[0]
    # Strategy 3: netbiosName EQUALS uppercase (cas Windows hosts)
    try:
        resp = _search_hostasset("netbiosName", "EQUALS", name.upper())
        if "<HostAsset>" in resp:
            block = resp.split("<HostAsset>", 1)[1].split("</HostAsset>", 1)[0]
            ids = parse_xml(block, "id")
            if ids:
                return ids[0]
    except Exception:
        pass
    return None


def add_tag_to_asset(asset_id, tag_id):
    body = {"ServiceRequest": {"data": {"HostAsset": {"tags": {
        "add": {"TagSimple": {"id": tag_id}}
    }}}}}
    resp = call(f"/qps/rest/2.0/update/am/hostasset/{asset_id}", body)
    return "SUCCESS" in resp


def process(tag_name, hostnames_file):
    print(f"\n=== {tag_name} ===")
    tag_id = find_tag_id(tag_name)
    if not tag_id:
        print(f"  !! Tag '{tag_name}' introuvable sur Qualys. SKIP.")
        return 0, 0
    print(f"  Tag ID: {tag_id}")
    try:
        with open(hostnames_file, encoding="utf-8") as f:
            hosts = [l.strip() for l in f if l.strip()]
    except FileNotFoundError:
        print(f"  !! Fichier introuvable: {hostnames_file}")
        return 0, 0
    print(f"  {len(hosts)} hostnames a traiter\n")

    ok, ko = 0, 0
    for h in hosts:
        aid = find_asset_id(h)
        if not aid:
            print(f"  [NOT_FOUND] {h}")
            ko += 1
            continue
        if not APPLY:
            print(f"  [DRY]   {h:<25} aid={aid} -> would add tag {tag_id}")
            ok += 1
            continue
        try:
            if add_tag_to_asset(aid, tag_id):
                print(f"  [OK]    {h:<25} aid={aid} tagged")
                ok += 1
            else:
                print(f"  [FAIL]  {h:<25} aid={aid}")
                ko += 1
            time.sleep(0.4)  # rate limit margin
        except Exception as e:
            print(f"  [ERR]   {h}: {e}")
            ko += 1
    return ok, ko


def main():
    mode = "APPLY (real)" if APPLY else "DRY RUN"
    print(f"Mode: {mode}")
    print(f"URL:  {URL}")
    print(f"User: {USER}")

    t_ok, t_ko = 0, 0
    for tag, path in [
        ("TAG-SED", r"C:\Claude\sanef\QL\inputs\bulk_tag_sed.txt"),
        ("TAG-SEI", r"C:\Claude\sanef\QL\inputs\bulk_tag_sei.txt"),
    ]:
        ok, ko = process(tag, path)
        t_ok += ok
        t_ko += ko

    print(f"\n=== TOTAL: {t_ok} OK / {t_ko} KO ===")
    if not APPLY:
        print("\n>>> DRY RUN termine. Pour appliquer: python apply_sed_sei_tags.py --apply")


if __name__ == "__main__":
    main()
