#!/usr/bin/python3
"""lhapitest: localhost API test client for wlanpi-core (HMAC-signed requests).

Stdlib only (system python3). The secret is read as raw bytes so NUL and trailing newline bytes sign correctly.
"""

import argparse
import hashlib
import hmac
import json
import ssl
import sys
import urllib.error
import urllib.parse
import urllib.request

SECRET = "/home/wlanpi/.local/share/wlanpi-core/secrets/shared_secret.bin"
CACERT = "/etc/nginx/ssl/self-signed-wlanpi.cert"


def main() -> int:
    """Parse args, sign, send, print the JSON response."""
    p = argparse.ArgumentParser(
        epilog="examples:\n"
        "  %(prog)s -e /system/device/model\n"
        '  %(prog)s -X POST -e /auth/token -P \'{"device_id": "1"}\'\n'
        "  %(prog)s -e /system/device/model -p 8000 --http   # dev server, no TLS",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    p.add_argument("-p", "--port", type=int, default=31415)
    p.add_argument("-b", "--base", default="/api/v1")
    p.add_argument("-s", "--secrets", default=SECRET)
    p.add_argument("-X", "--method", default="GET")
    p.add_argument("-e", "--endpoint", required=True)
    p.add_argument("-q", "--query", default="", help="raw query string, no '?'")
    p.add_argument("-P", "--payload", default="", help="JSON body for POST/PUT/PATCH")
    p.add_argument("-H", "--headers", default="", help="comma-separated 'K: V' headers")
    p.add_argument(
        "--http", action="store_true", help="plain HTTP (dev server on :8000)"
    )
    p.add_argument("-v", "--verbose", action="store_true", help="show canonical string")
    a = p.parse_args()

    method = a.method.upper()
    path = a.base + a.endpoint
    body = a.payload.encode() if method in ("POST", "PUT", "PATCH") else b""

    try:
        with open(a.secrets, "rb") as fh:
            secret = fh.read()
    except PermissionError:
        print(f"cannot read {a.secrets}; run with sudo", file=sys.stderr)
        return 1
    except FileNotFoundError:
        print(f"secret not found at {a.secrets}", file=sys.stderr)
        return 1

    # verify_hmac in wlanpi_core/core/auth.py signs the re-encoded parsed query, not the raw text.
    query = urllib.parse.urlencode(
        dict(urllib.parse.parse_qsl(a.query, keep_blank_values=True))
    )
    canonical = f"{method}\n{path}\n{query}\n".encode() + body
    sig = hmac.new(secret, canonical, hashlib.sha256).hexdigest()
    if a.verbose:
        print(f"canonical: {canonical!r}\nsignature: {sig}", file=sys.stderr)

    headers = {"accept": "application/json", "X-Request-Signature": sig}
    if body:
        headers["Content-Type"] = "application/json"
    for h in filter(None, (x.strip() for x in a.headers.split(","))):
        k, _, v = h.partition(":")
        headers[k.strip()] = v.strip()

    scheme = "http" if a.http else "https"
    url = f"{scheme}://127.0.0.1:{a.port}{path}" + (f"?{query}" if query else "")
    req = urllib.request.Request(url, data=body or None, method=method, headers=headers)
    ctx = None
    if scheme == "https":
        ctx = ssl.create_default_context(cafile=CACERT)
    try:
        with urllib.request.urlopen(req, context=ctx) as resp:
            raw, status = resp.read(), resp.status
    except urllib.error.HTTPError as exc:
        raw, status = exc.read(), exc.code
    except (urllib.error.URLError, ConnectionError) as exc:
        print(
            f"request failed: {exc.reason if hasattr(exc, 'reason') else exc}",
            file=sys.stderr,
        )
        return 1

    try:
        print(json.dumps(json.loads(raw), indent=2))
    except ValueError:
        sys.stdout.write(raw.decode(errors="replace") + "\n")
    print(f"http={status}", file=sys.stderr)
    return 0 if status < 400 else 2


if __name__ == "__main__":
    sys.exit(main())
