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

Stdlib only. Reads the shared secret as raw bytes, so secrets containing NUL
or trailing newline bytes sign correctly (the previous bash version did not).
"""

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

    # Must match verify_hmac in wlanpi_core/core/auth.py.
    # verify_hmac re-encodes the parsed params (last value wins per key), so
    # sign and send exactly that form rather than the raw --query 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())
