#!/usr/bin/env python3
"""Command line client for the archive's read-only JSON API.

Only needs Python 3.8+ (standard library). Examples (replace <file> with this file's name):
  python3 <file> latest -n 20
  python3 <file> domain example.com
  python3 <file> attacker SomeNotifier --json
  python3 <file> stats
  python3 <file> search exam
  python3 <file> config --key sk_xxx
"""
import argparse, json, os, sys, urllib.request, urllib.parse, urllib.error

__version__ = "1.0.0"
CLI_NAME = "c99php-com"  # set automatically
DEFAULT_URL = "https://www.c99php.com"  # set automatically by C99PHP.COM
CONF = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), CLI_NAME, "config.json")


def load_conf():
    try:
        with open(CONF, encoding="utf-8") as f:
            return json.load(f)
    except (OSError, ValueError):
        return {}


def save_conf(c):
    os.makedirs(os.path.dirname(CONF), exist_ok=True)
    with open(CONF, "w", encoding="utf-8") as f:
        json.dump(c, f, indent=2)
    try:
        os.chmod(CONF, 0o600)
    except OSError:
        pass


class Client:
    def __init__(self, base, key):
        self.base, self.key = base.rstrip("/"), key

    def get(self, path, **params):
        params = {k: v for k, v in params.items() if v is not None}
        url = self.base + path + ("?" + urllib.parse.urlencode(params) if params else "")
        headers = {"User-Agent": CLI_NAME + "-cli/" + __version__, "Accept": "application/json"}
        if self.key:
            headers["X-API-Key"] = self.key
        try:
            with urllib.request.urlopen(urllib.request.Request(url, headers=headers), timeout=20) as r:
                return json.loads(r.read().decode("utf-8"))
        except urllib.error.HTTPError as e:
            try:
                msg = json.loads(e.read().decode("utf-8")).get("error", "")
            except Exception:
                msg = ""
            hint = " (get an API key at /my/api/ and run: %s config --key ...)" % os.path.basename(sys.argv[0]) + "" if e.code == 429 else ""
            die("HTTP %d: %s%s" % (e.code, msg or e.reason, hint))
        except urllib.error.URLError as e:
            die("Connection failed: %s" % e.reason)
        except ValueError:
            die("Server did not return valid JSON (wrong --url?)")


def die(msg):
    print("error: " + msg, file=sys.stderr)
    sys.exit(1)


def trunc(s, n):
    s = "" if s is None else str(s)
    return s if len(s) <= n else s[: n - 1] + "…"


def print_entries(entries):
    if not entries:
        print("No entries.")
        return
    cols = [("ID", 7, "id"), ("Date", 19, "date"), ("Notifier", 16, "notifier"),
            ("Domain", 32, "domain"), ("CC", 3, "country_code"), ("Flags", 5, None), ("Mirror", 0, "mirror_url")]
    print("  ".join(h.ljust(w) for h, w, _ in cols[:-1]) + "  Mirror")
    for e in entries:
        flags = ("S" if e.get("is_special") else "-") + ("M" if e.get("is_mass") else "-") + ("H" if e.get("is_homepage") else "-")
        row = [e.get("id"), e.get("date"), e.get("notifier"), e.get("domain"), e.get("country_code"), flags]
        print("  ".join(trunc(v, w).ljust(w) for v, (_, w, _k) in zip(row, cols[:-1])) + "  " + str(e.get("mirror_url", "")))


def show(data, args, header=None):
    if args.json:
        print(json.dumps(data, indent=2, ensure_ascii=False))
        return
    if header:
        print(header)
    print_entries(data.get("entries", []))
    if "total" in data:
        print("\nPage %s - %d shown - %s total" % (data.get("page", args.page), len(data.get("entries", [])), data["total"]))


def cmd_latest(c, a):
    show(c.get("/api/latest/", limit=a.n, page=a.page), a)


def cmd_domain(c, a):
    d = c.get("/api/domain/" + urllib.parse.quote(a.domain, safe="") + "/", limit=a.n, page=a.page)
    show(d, a, "Domain: %s" % d.get("domain", a.domain))


def cmd_attacker(c, a):
    d = c.get("/api/attacker/" + urllib.parse.quote(a.name, safe="") + "/", limit=a.n, page=a.page)
    show(d, a, "Attacker: %s" % d.get("attacker", a.name))


def cmd_stats(c, a):
    d = c.get("/api/stats/")
    if a.json:
        print(json.dumps(d, indent=2, ensure_ascii=False))
        return
    for k, label in [("total_entries", "Entries"), ("total_special", "Special"), ("total_notifiers", "Notifiers"),
                     ("total_domains", "Domains"), ("today", "Today"), ("online", "Online now")]:
        print("%-12s %s" % (label, d.get(k)))
    if d.get("top_notifiers"):
        print("\nTop notifiers:")
        for t in d["top_notifiers"]:
            print("  %-24s %s" % (t["notifier"], t["count"]))


def cmd_search(c, a):
    if len(a.query) < 2:
        die("query needs at least 2 characters")
    res = c.get("/api/autocomplete/", q=a.query).get("results", [])
    if a.json:
        print(json.dumps(res, indent=2, ensure_ascii=False))
    else:
        print("\n".join(res) if res else "No matches.")


def cmd_config(c, a):
    conf = load_conf()
    if a.key is not None:
        conf["key"] = a.key
    if a.url is not None:
        conf["url"] = a.url
    if a.key is None and a.url is None and not a.clear:
        print("config file: %s\nurl: %s\nkey: %s" % (CONF, conf.get("url", DEFAULT_URL),
              (conf["key"][:7] + "…") if conf.get("key") else "(none)"))
        return
    if a.clear:
        conf = {}
    save_conf(conf)
    print("Saved to " + CONF)


def main(argv=None):
    p = argparse.ArgumentParser(prog=CLI_NAME, description="Archive CLI (read-only) for " + DEFAULT_URL,
                                epilog="Server: " + DEFAULT_URL + "  |  API key: " + DEFAULT_URL + "/my/api/")
    p.add_argument("--url", help="base URL (default %s)" % DEFAULT_URL)
    p.add_argument("--key", help="API key (or env DEFACER_API_KEY)")
    p.add_argument("--version", action="version", version="%s %s (%s)" % (CLI_NAME, __version__, DEFAULT_URL))
    sub = p.add_subparsers(dest="cmd", required=True)

    def add(name, fn, help_, *pos, paged=True):
        sp = sub.add_parser(name, help=help_)
        for x in pos:
            sp.add_argument(x)
        sp.add_argument("--json", action="store_true", help="raw JSON output")
        if paged:
            sp.add_argument("-n", type=int, default=20, help="entries per page (max 200)")
            sp.add_argument("--page", type=int, default=1)
        sp.set_defaults(fn=fn)
        return sp

    add("latest", cmd_latest, "latest entries")
    add("domain", cmd_domain, "entries for a domain", "domain")
    add("attacker", cmd_attacker, "entries for an attacker/notifier", "name")
    add("stats", cmd_stats, "global statistics", paged=False)
    add("search", cmd_search, "domain autocomplete", "query", paged=False)
    sc = sub.add_parser("config", help="save API key / base URL")
    sc.add_argument("--key"); sc.add_argument("--url"); sc.add_argument("--clear", action="store_true")
    sc.set_defaults(fn=cmd_config)

    a = p.parse_args(argv)
    conf = load_conf()
    base = a.url or os.environ.get("DEFACER_URL") or conf.get("url") or DEFAULT_URL
    key = a.key or os.environ.get("DEFACER_API_KEY") or conf.get("key")
    if hasattr(a, "n"):
        a.n = max(1, min(200, a.n))
    try:
        a.fn(Client(base, key), a)
    except BrokenPipeError:
        pass


if __name__ == "__main__":
    main()
