"""Direct-provider diagnostics, shared by FastAPI and the Bash interface.

Report structure and classification thresholds adapted from xykt/IPQuality
v2026-09-16. SPDX-License-Identifier: AGPL-3.0-only.
Remote values are parsed as data. No author proxies, counters, demo keys,
downloaded code, report uploads, account logins, or paid AI calls.
"""
from __future__ import annotations

import argparse
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from datetime import datetime, timezone
import ipaddress
import json
import math
import os
from pathlib import Path
import re
import subprocess
import sys
import tempfile
from urllib.parse import urlencode, urlsplit, urljoin, quote

ROOT = Path(__file__).resolve().parents[1]
VERSION = "0.1.1-draft"
BASIC_FIELDS = ("ip", "asn", "as_name", "as_domain", "country_code", "country", "continent_code", "continent")
FACTORS = ("proxy", "tor", "vpn", "server", "abuser", "robot")
PROVIDERS = {
    "ipinfo": ("IPinfo", "IPINFO_EXTENDED_TOKEN"),
    "ipregistry": ("ipregistry", "IPREGISTRY_KEY"),
    "ipapi": ("ipapi.is", "IPAPI_KEY"),
    "ip2location": ("IP2Location", "IP2LOCATION_KEY"),
    "abuseipdb": ("AbuseIPDB", "ABUSEIPDB_KEY"),
    "scamalytics": ("Scamalytics", "SCAMALYTICS_KEY"),
    "ipqs": ("IPQS", "IPQS_KEY"),
    "dbip": ("DB-IP", "DBIP_KEY"),
    "ipdata": ("ipdata", "IPDATA_KEY"),
}
SERVICES = json.loads((ROOT / "public/assets/services.json").read_text())


def utcnow():
    return datetime.now(timezone.utc).isoformat(timespec="seconds")


def public_ip(value):
    if not isinstance(value, str) or "%" in value:
        raise ValueError("Enter a public IPv4 or IPv6 address.")
    try:
        address = ipaddress.ip_address(value.strip())
    except ValueError:
        raise ValueError("Enter a public IPv4 or IPv6 address.") from None
    if not address.is_global or address.is_multicast or address.is_unspecified:
        raise ValueError("Only public unicast IP addresses can be checked.")
    return str(address)


def get(data, path):
    for key in path.split("."):
        if not isinstance(data, dict):
            return None
        data = data.get(key)
    return data


def clean(value, limit=180):
    if not isinstance(value, str) or not value.strip() or value in ("null", "-", "N/A"):
        return None
    return "".join(c for c in value if c >= " " and c != "\x7f")[:limit]


def boolean(value):
    return value if type(value) is bool else None


def any_flag(*values):
    """Unknown is not false, including when only some inputs are present."""
    values = [boolean(v) for v in values]
    if True in values:
        return True
    return False if values and all(v is False for v in values) else None


def number(value, maximum):
    if value is None or isinstance(value, bool):
        return None
    try:
        result = float(value)
    except (TypeError, ValueError):
        return None
    return result if math.isfinite(result) and 0 <= result <= maximum else None


def country(value):
    return value.upper() if isinstance(value, str) and re.fullmatch(r"[A-Za-z]{2}", value) else None


@dataclass
class Response:
    status: int = 0
    body: bytes = b""
    headers: dict | None = None
    error: str | None = None

    def json(self):
        try:
            value = json.loads(self.body)
            return value if isinstance(value, dict) else None
        except (ValueError, UnicodeDecodeError):
            return None


def config_quote(value):
    if any(c in value for c in "\r\n\x00"):
        raise ValueError("Invalid request configuration.")
    return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'


def request(url, headers=None, family="auto", proxy=None):
    """Bounded HTTPS request. Secrets travel through stdin, never argv.

    Ignore ~/.curlrc and environment proxies: the caller explicitly controls
    the tested route. No automatic retries, cookies, redirect following, or
    certificate-verification bypass. Provider errors never echo secrets.
    """
    if urlsplit(url).scheme != "https":
        return Response(error="invalid_endpoint")
    configuration = "url = " + config_quote(url) + "\n"
    for name, value in (headers or {}).items():
        configuration += "header = " + config_quote(f"{name}: {value}") + "\n"
    if proxy:
        parts = urlsplit(proxy)
        if parts.scheme not in ("http", "https", "socks5h", "socks5") or not parts.hostname:
            return Response(error="invalid_proxy")
        configuration += "proxy = " + config_quote(proxy) + "\n"
    else:
        configuration += 'noproxy = "*"\n'
    with tempfile.TemporaryDirectory(prefix="vishow-request-") as folder:
        body = Path(folder) / "body"
        head = Path(folder) / "headers"
        command = ["curl", "--disable", "--config", "-", "--silent", "--show-error",
                   "--proto", "=https", "--connect-timeout", "4", "--max-time", "10",
                   "--max-filesize", "1048576", "--user-agent", "Vishow-IP-Check/0.1",
                   "--output", str(body), "--dump-header", str(head), "--write-out", "%{http_code}"]
        if family in ("4", "6"):
            command.append("-" + family)
        environment = {k: v for k, v in os.environ.items() if k.lower() not in
                       ("http_proxy", "https_proxy", "all_proxy", "no_proxy")}
        try:
            process = subprocess.run(command, input=configuration.encode(), capture_output=True,
                                     timeout=12, env=environment, check=False)
        except (OSError, subprocess.TimeoutExpired):
            return Response(error="transport_error")
        errors = {5: "proxy_dns_error", 6: "dns_error", 7: "connection_error", 28: "timeout",
                  35: "tls_error", 60: "tls_error", 63: "response_too_large"}
        status = int(process.stdout) if process.stdout.strip().isdigit() else 0
        parsed = {}
        if head.exists():
            for line in head.read_text(errors="replace").splitlines():
                if line.startswith("HTTP/"):
                    parsed = {}
                elif ":" in line:
                    name, value = line.split(":", 1)
                    parsed[name.lower()] = value.strip()
        data = body.read_bytes()[:1048576] if body.exists() else b""
        return Response(status, data, parsed,
                        errors.get(process.returncode, "transport_error") if process.returncode else None)


def blank_provider(key, status="not_configured", detail="Data source is not enabled."):
    return {"id": key, "name": PROVIDERS[key][0], "status": status, "detail": detail,
            "usage": None, "company": None, "region": None,
            "flags": dict.fromkeys(FACTORS), "score": None}


def set_score(result, value, maximum, bands=(), unit="points", risk=None):
    value = number(value, maximum)
    if value is None:
        return
    if not risk:
        risk = next((label for threshold, label in bands if value < threshold), None)
    result["score"] = {"value": value, "maximum": maximum, "unit": unit,
                       "risk": clean(risk), "risk_source": "provider" if unit == "percent" else "script thresholds"}


def parse_provider(key, data):
    result = blank_provider(key, "ok", "Provider responded. Missing fields remain unknown.")
    if not isinstance(data, dict):
        return blank_provider(key, "invalid_response", "Provider returned an invalid response.")
    paths = {}
    if key == "ipinfo":
        result.update(usage=clean(get(data, "asn.type")), company=clean(get(data, "company.type")), region=country(data.get("country")))
        paths = {"proxy":"privacy.proxy", "tor":"privacy.tor", "vpn":"privacy.vpn", "server":"privacy.hosting"}
    elif key == "ipregistry":
        result.update(usage=clean(get(data, "connection.type")), company=clean(get(data, "company.type")), region=country(get(data, "location.country.code")))
        paths = {"proxy":"security.is_proxy", "vpn":"security.is_vpn", "server":"security.is_cloud_provider", "abuser":"security.is_abuser"}
        result["flags"]["tor"] = any_flag(get(data,"security.is_tor"), get(data,"security.is_tor_exit"))
    elif key == "ipapi":
        result.update(usage=clean(get(data, "asn.type")), company=clean(get(data, "company.type")), region=country(get(data, "location.country_code")))
        paths = {"proxy":"is_proxy", "tor":"is_tor", "vpn":"is_vpn", "server":"is_datacenter", "abuser":"is_abuser", "robot":"is_crawler"}
        score = get(data, "company.abuser_score")
        match = re.fullmatch(r"\s*(\d+(?:\.\d+)?)\s*\(([^()]{1,30})\)\s*", score) if isinstance(score, str) else None
        if match and number(match[1], 1) is not None:
            set_score(result, float(match[1]) * 100, 100, unit="percent", risk=match[2])
    elif key == "abuseipdb":
        data = data.get("data", {})
        result.update(usage=clean(get(data,"usageType")), region=country(get(data,"countryCode")))
        result["flags"]["tor"] = boolean(get(data,"isTor"))
        set_score(result, get(data,"abuseConfidenceScore"), 100,
                  ((25,"Low"),(75,"High"),(101,"Very high")))
    elif key == "ip2location":
        types = {"DCH":"Hosting", "ISP":"Line ISP", "MOB":"Mobile ISP", "COM":"Business", "EDU":"Education", "CDN":"CDN"}
        result.update(usage=types.get(get(data,"usage_type"),clean(get(data,"usage_type"))),
                      company=types.get(get(data,"as_info.as_usage_type"),clean(get(data,"as_info.as_usage_type"))), region=country(get(data,"country_code")))
        paths = {"tor":"proxy.is_tor", "vpn":"proxy.is_vpn", "server":"proxy.is_data_center", "abuser":"proxy.is_spammer"}
        result["flags"]["proxy"] = any_flag(get(data,"is_proxy"),get(data,"proxy.is_public_proxy"),get(data,"proxy.is_web_proxy"))
        result["flags"]["robot"] = any_flag(get(data,"proxy.is_web_crawler"),get(data,"proxy.is_scanner"),get(data,"proxy.is_botnet"))
        set_score(result, get(data,"fraud_score"), 99, ((33,"Low"),(66,"Medium"),(100,"High")))
    elif key == "ipqs":
        result["region"] = country(data.get("country_code"))
        paths = {"proxy":"proxy", "tor":"tor", "vpn":"vpn", "abuser":"recent_abuse", "robot":"bot_status"}
        set_score(result, data.get("fraud_score"), 100,
                  ((75,"Low"),(85,"Suspicious"),(90,"Risky"),(101,"High risk")))
    elif key == "dbip":
        result["region"] = country(data.get("countryCode"))
        paths = {"proxy":"isProxy", "robot":"isCrawler"}
        risk = clean(data.get("threatLevel"))
        if risk:
            result["score"] = {"value":None,"maximum":None,"unit":"category","risk":risk,"risk_source":"provider"}
    elif key == "ipdata":
        result["region"] = country(data.get("country_code"))
        paths = {"proxy":"threat.is_proxy", "tor":"threat.is_tor", "vpn":"threat.is_vpn", "server":"threat.is_datacenter"}
        result["flags"]["abuser"] = any_flag(get(data,"threat.is_threat"),get(data,"threat.is_known_abuser"),get(data,"threat.is_known_attacker"))
    elif key == "scamalytics":
        result["region"] = country(get(data,"external_datasources.maxmind_geolite2.ip_country_code"))
        paths = {"proxy":"external_datasources.firehol.is_proxy", "tor":"external_datasources.x4bnet.is_tor",
                 "vpn":"scamalytics.scamalytics_proxy.is_vpn", "server":"scamalytics.scamalytics_proxy.is_datacenter",
                 "abuser":"scamalytics.is_blacklisted_external"}
        result["flags"]["robot"] = any_flag(get(data,"external_datasources.x4bnet.is_blacklisted_spambot"),get(data,"external_datasources.x4bnet.is_bot_operamini"),get(data,"external_datasources.x4bnet.is_bot_semrush"))
        value = get(data,"scamalytics.scamalytics_score")
        if value is None:
            value = data.get("score")
        set_score(result, value, 100, ((20,"Low"),(60,"Medium"),(90,"High"),(101,"Very high")))
    for field, path in paths.items():
        result["flags"][field] = boolean(get(data, path))
    if not any(result[k] is not None for k in ("usage","company","region","score")) and all(v is None for v in result["flags"].values()):
        result.update(status="no_supported_fields", detail="Response has no supported fields; check the subscribed data product.")
    return result


def provider_endpoint(key, ip, env):
    token = env.get(PROVIDERS[key][1], "").strip()
    if not token:
        return None
    escaped = quote(token, safe="")
    if key == "ipinfo":
        return f"https://ipinfo.io/{ip}/json", {"Authorization":f"Bearer {token}"}
    if key == "ipregistry":
        return f"https://api.ipregistry.co/{ip}?" + urlencode({"key":token}), {}
    if key == "ipapi":
        return "https://api.ipapi.is/?" + urlencode({"q":ip,"key":token}), {}
    if key == "ip2location":
        return "https://api.ip2location.io/?" + urlencode({"key":token,"ip":ip,"format":"json"}), {}
    if key == "abuseipdb":
        return "https://api.abuseipdb.com/api/v2/check?" + urlencode({"ipAddress":ip,"maxAgeInDays":90}), {"Key":token,"Accept":"application/json"}
    if key == "ipqs":
        return f"https://www.ipqualityscore.com/api/json/ip/{escaped}/{ip}", {}
    if key == "dbip":
        return f"https://api.db-ip.com/v2/{escaped}/{ip}", {}
    if key == "ipdata":
        return f"https://api.ipdata.co/{ip}?" + urlencode({"api-key":token}), {}
    endpoint = env.get("SCAMALYTICS_ENDPOINT", "")
    parsed = urlsplit(endpoint)
    if (parsed.scheme != "https" or not parsed.hostname or
            not parsed.hostname.endswith(".scamalytics.com") or parsed.port not in (None,443)
            or parsed.username or parsed.password or parsed.query or parsed.fragment):
        return None
    return endpoint + "?" + urlencode({"key":token,"ip":ip}), {}


def response_problem(response):
    if response.error:
        return response.error, "Provider request did not complete."
    if response.status == 429:
        return "rate_limited", "Provider quota or rate limit reached."
    if response.status in (401,403):
        return "access_denied", "Provider refused access; check credentials and plan."
    if response.status != 200:
        return "http_error", f"Provider returned HTTP {response.status}."
    data = response.json()
    if not data or data.get("error") or data.get("errors") or data.get("success") is False:
        return "invalid_response", "Provider returned an error or invalid data."
    return None


def fetch_provider(key, ip, env, family="auto", proxy=None, transport=request):
    try:
        endpoint = provider_endpoint(key,ip,env)
        if endpoint is None:
            return blank_provider(key)
        response = transport(endpoint[0],headers=endpoint[1],family=family,proxy=proxy)
        problem = response_problem(response)
        if problem:
            return blank_provider(key,*problem)
        data = response.json()
        returned_ip = data.get("ip") or data.get("ipAddress") or get(data,"data.ipAddress")
        if returned_ip and public_ip(returned_ip) != ip:
            return blank_provider(key,"ip_mismatch","Provider returned a different IP address.")
        return parse_provider(key,data)
    except (ValueError,TypeError,KeyError):
        return blank_provider(key,"invalid_response","Provider configuration or response is invalid.")


def fetch_basic(ip, env, family="auto", proxy=None, transport=request):
    empty = {key: None for key in BASIC_FIELDS}
    if ip != "me":
        empty["ip"] = ip
    token = env.get("IPINFO_TOKEN", "").strip()
    if not token:
        return empty, "not_configured"
    try:
        response = transport(f"https://api.ipinfo.io/lite/{ip}",headers={"Authorization":f"Bearer {token}"},family=family,proxy=proxy)
    except ValueError:
        return empty,"invalid_response"
    problem = response_problem(response)
    if problem:
        return empty, problem[0]
    data = response.json()
    try:
        returned_ip = public_ip(data.get("ip"))
    except ValueError:
        return empty,"invalid_response"
    if ip != "me" and returned_ip != ip:
        return empty,"ip_mismatch"
    return {key: clean(data.get(key)) for key in BASIC_FIELDS}, "ok"


def ip_report(ip, env=None, family="auto", proxy=None, transport=request):
    ip = public_ip(ip)
    env = os.environ if env is None else env
    with ThreadPoolExecutor(max_workers=4) as pool:
        basic_future = pool.submit(fetch_basic,ip,env,family,proxy,transport)
        providers = list(pool.map(lambda key: fetch_provider(key,ip,env,family,proxy,transport),PROVIDERS))
        basic,status = basic_future.result()
    return {"schema_version":1,"version":VERSION,"generated_at":utcnow(),"kind":"ip",
            "subject":{"ip":ip,"family":ipaddress.ip_address(ip).version,"scope":"lookup"},
            "basic":basic,"basic_status":status,"providers":providers,"services":[]}


def classify_service(service, response):
    result = {"id":service["id"],"name":service["name"],"category":service["category"],
              "status":"inconclusive","http_status":response.status or None,"region":None,"type":None,
              "evidence":"Service availability is unverified.","checked_at":utcnow(),"scope":"local"}
    if response.error:
        result.update(status=response.error,evidence="The request did not complete; this does not prove an IP block.")
        return result
    data = response.json() or {}
    error = get(data,"error.code") or get(data,"error.type") or data.get("error_code")
    # Only explicit structured errors establish region rejection. A generic
    # 403, page text mentioning VPN, or a successful favicon never does.
    if error in ("unsupported_country","unsupported_country_region_territory","region_not_supported","country_not_supported"):
        result.update(status="regional_block",evidence="Endpoint returned an explicit unsupported-country error.")
    elif (response.headers or {}).get("cf-mitigated") == "challenge" or (
            response.status in (403,503) and b"/cdn-cgi/challenge-platform/" in response.body):
        result.update(status="challenge",evidence="An automated-request challenge prevented verification.")
    elif response.status == 429:
        result.update(status="rate_limited",evidence="HTTP 429: rate limited; no retry was made.")
    elif response.status == 401:
        result.update(status="login_required",evidence="Endpoint requires authentication.")
    elif response.status == 403:
        result.update(status="access_denied",evidence="HTTP 403: access refused; the cause is not established.")
    elif 200 <= response.status < 300:
        result.update(status="response_only",evidence="Web endpoint responded. Chat, playback, account access and region are unverified.")
    elif 300 <= response.status < 400:
        try:
            location = urlsplit(urljoin(service["url"],(response.headers or {}).get("location","")))
        except ValueError:
            result.update(status="redirect", evidence="Endpoint returned an invalid redirect destination.")
            return result
        login_path = re.search(r"/(?:login|signin|sign-in|auth)(?:/|$)",location.path,re.I)
        result.update(status="login_required" if login_path else "redirect",
                      evidence="Endpoint redirected to sign-in." if login_path else "Endpoint redirected; destination was not automatically followed.")
    else:
        result.update(status="http_error",evidence=f"Endpoint returned HTTP {response.status}; availability is unverified.")
    return result


# Supplementary endpoints are taken from the uploaded script, not inferred
# from another service's API. They are observations, not availability contracts.
EXTRA_PROBES = {
    "chatgpt": [("api_region", "OpenAI API region signal", "https://api.openai.com/compliance/cookie_requirements")],
    "netflix": [("title_81280792", "Netflix title 81280792", "https://www.netflix.com/title/81280792"),
                ("title_70143836", "Netflix title 70143836", "https://www.netflix.com/title/70143836")],
    "reddit": [("chat_page", "Reddit chat page", "https://www.reddit.com/svc/shreddit/reddit-chat")],
}
REDIRECT_CODES = (301, 302, 303, 307, 308)


def redirect_target(current, location, allowed_hosts):
    """Allow exact HTTPS hosts only; never turn a probe into an open fetcher."""
    if not isinstance(location, str) or not location or any(ord(c) < 33 for c in location):
        return None
    try:
        target = urlsplit(urljoin(current, location))
        if (target.scheme != "https" or target.hostname not in allowed_hosts
                or target.username is not None or target.password is not None
                or target.port not in (None, 443) or "\\" in location):
            return None
        return target._replace(fragment="").geturl()
    except ValueError:
        return None


def probe_service(service, probe_id, label, url, family, proxy, transport):
    # At most three requests per probe. Stop at sign-in rather than submitting
    # credentials, and do not follow third-party login/consent destinations.
    allowed_hosts = {urlsplit(url).hostname}
    current, visited, hops = url, set(), []
    for attempt in range(3):
        visited.add(current)
        response = transport(current, headers={"Accept-Language": "en"}, family=family, proxy=proxy)
        result = classify_service({**service, "url": current}, response)
        hops.append({"http_status": response.status or None, "host": urlsplit(current).hostname})
        if response.error or response.status not in REDIRECT_CODES:
            break
        target = redirect_target(current, (response.headers or {}).get("location"), allowed_hosts)
        if target is None:
            result.update(status="redirect", evidence="Redirect was not followed: destination is outside this probe's approved HTTPS host or is invalid.")
            break
        if result["status"] == "login_required":
            break
        if target in visited or attempt == 2:
            result.update(status="redirect", evidence="Redirect loop or the two-redirect limit prevented verification.")
            break
        current = target
    result.update(probe_id=probe_id, label=label, hops=hops)
    if result["status"] == "response_only":
        # These fields come from the original script's page heuristics. A page
        # hint is not an authoritative account, billing or playback territory.
        region_key = {"youtube": "contentRegion", "prime": "currentTerritory"}.get(service["id"])
        if region_key:
            matches = set(re.findall(rb'"' + region_key.encode() + rb'"\s*:\s*"([A-Z]{2})"', response.body))
            if len(matches) == 1:
                result["region"] = matches.pop().decode("ascii")
                result["region_source"] = "page_hint"
                result["evidence"] += f" Page field {region_key} suggests {result['region']}; this may reflect page configuration."
        if probe_id.startswith("title_"):
            result["evidence"] = "Title URL responded; this does not prove title availability, catalogue access or playback."
    if probe_id == "api_region":
        result["evidence"] = "OpenAI API endpoint only; not a ChatGPT web verdict. " + result["evidence"]
    return result


def check_service(service, family="auto", proxy=None, transport=request):
    primary = probe_service(service, "web", "Website", service["url"], family, proxy, transport)
    probes = [primary]
    # Avoid further requests after rate limiting or an automated challenge.
    # A blocked API endpoint must never overwrite a different web result.
    if primary["status"] not in ("rate_limited", "challenge", "regional_block", "access_denied",
                                 "dns_error", "proxy_dns_error", "tls_error", "timeout",
                                 "connection_error", "transport_error"):
        for probe_id, label, url in EXTRA_PROBES.get(service["id"], []):
            probe = probe_service(service, probe_id, label, url, family, proxy, transport)
            probes.append(probe)
            if probe["status"] in ("rate_limited", "challenge"):
                break
    result = {key: value for key, value in primary.items() if key not in ("probe_id", "label", "hops")}
    result["probes"] = probes
    result["coverage"] = "endpoint_diagnostics"
    return result


def service_report(category="all", family="auto", proxy=None, transport=request):
    selected = [s for s in SERVICES if category == "all" or s["category"] == category]
    def check(service):
        return check_service(service,family,proxy,transport)
    with ThreadPoolExecutor(max_workers=3) as pool:
        return list(pool.map(check,selected))


def main():
    parser = argparse.ArgumentParser(description="Vishow local IP and service diagnostics. No report upload.")
    parser.add_argument("--ip", help="Explicit public IP for database lookup; disables service probes.")
    parser.add_argument("--family", choices=("4","6"), default="4")
    parser.add_argument("--services", choices=("none","ai","media","all"), default="all")
    args = parser.parse_args()
    if args.ip and args.services != "none":
        parser.error("--ip is a database lookup. Add --services none; remote IPs cannot be service-tested.")
    proxy = os.environ.get("VISHOW_PROXY")
    ip = args.ip
    if not ip:
        basic,status = fetch_basic("me",os.environ,args.family,proxy)
        ip = basic.get("ip") if status == "ok" else None
        if not ip:
            # Same-origin public-IP endpoint; no unrelated echo provider.
            reply = request("https://vishow.top/ip",family=args.family,proxy=proxy)
            if not reply.error and reply.status == 200:
                try:
                    ip = public_ip(reply.body.decode().strip())
                except (ValueError,UnicodeDecodeError):
                    pass
    if ip:
        report = ip_report(ip,family=args.family,proxy=proxy)
    else:
        report = {"schema_version":1,"version":VERSION,"generated_at":utcnow(),"kind":"local",
                  "subject":{"ip":None,"family":int(args.family),"scope":"local"},
                  "basic":dict.fromkeys(BASIC_FIELDS),"basic_status":"ip_detection_failed",
                  "providers":[blank_provider(k,"not_checked","Public IP could not be detected.") for k in PROVIDERS],"services":[]}
    report["kind"] = "ip" if args.ip else "local"
    report["subject"]["scope"] = "lookup" if args.ip else "local"
    report["route"] = {"family":args.family,"proxy_configured":bool(proxy),
                       "note":"Services test this process route. VPN/split routing may use different exits per destination."}
    if args.services != "none":
        report["services"] = service_report(args.services,args.family,proxy)
    print(json.dumps(report,ensure_ascii=False,allow_nan=False))
    return 0 if ip or report["services"] else 2


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except (ValueError, KeyboardInterrupt):
        print("Check cancelled or invalid input. No report uploaded.",file=sys.stderr)
        raise SystemExit(2)
