#!/usr/bin/env python3
"""Exercise one disposable local Auth request path with a verified CLI archive.

Usage: python3 run.py --cli /path/to/trustplane [--walkthrough]
Only safe command templates and status/reason summaries reach stdout. The CLI's
sample headers are captured in a private temporary log; fresh request headers
remain in process memory. Temporary files are removed on exit.
"""

import argparse
import json
import os
import pathlib
import re
import subprocess
import tempfile
import time
import urllib.error
import urllib.request

VERSION = "0.1.3"


def step(number, title, operation, purpose, walkthrough):
    if not walkthrough:
        return
    print(f"\nStep {number}: {title}", flush=True)
    print(f"  {operation}", flush=True)
    print(f"  Purpose: {purpose}", flush=True)
    try:
        input("  Press Enter to execute... ")
        print(flush=True)
    except EOFError:
        # A piped recording or CI run can still use --walkthrough without hanging.
        print("  Running without an interactive pause.", flush=True)


def request(url, headers=None):
    req = urllib.request.Request(url, headers=headers or {}, method="GET")
    try:
        with urllib.request.urlopen(req, timeout=5) as response:
            return response.status, json.loads(response.read())
    except urllib.error.HTTPError as error:
        return error.code, json.loads(error.read())


def reason(body):
    return body.get("reason") or body.get("error") or "none"


def issue(cli, socket, authority, route="orders.list"):
    output = subprocess.run(
        [
            cli, "broker", "issue", "--socket", socket,
            "--method", "GET", "--scheme", "http", "--authority", authority,
            "--path", "/orders", "--audience", "example-api",
            "--route-id", route, "--allow-software-fallback",
        ],
        check=True, capture_output=True, text=True, timeout=10,
    )
    payload = json.loads(output.stdout)
    return payload["headers"]


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--version", action="version", version=f"local-first-request {VERSION}")
    parser.add_argument("--cli", required=True, help="path to a checksum-verified trustplane CLI")
    parser.add_argument("--walkthrough", action="store_true", help="show and pause at safe real request steps")
    args = parser.parse_args()
    cli = str(pathlib.Path(args.cli).resolve(strict=True))

    with tempfile.TemporaryDirectory(prefix="trustplane-local-first-") as work_dir:
        os.chmod(work_dir, 0o700)
        log_path = pathlib.Path(work_dir) / "up.log"
        with log_path.open("w", encoding="utf-8") as log:
            os.chmod(log_path, 0o600)
            step(
                "1/5", "Start disposable local Auth",
                "$ trustplane up --listen 127.0.0.1:0 --upstream-listen 127.0.0.1:0 --work-dir <private temporary directory>",
                "Start the real broker, adapter, and synthetic upstream. Startup headers stay in the private log.",
                args.walkthrough,
            )
            proc = subprocess.Popen(
                [cli, "up", "--listen", "127.0.0.1:0",
                 "--upstream-listen", "127.0.0.1:0", "--work-dir", work_dir],
                stdout=log, stderr=subprocess.STDOUT,
            )
            try:
                deadline = time.monotonic() + 15
                while time.monotonic() < deadline:
                    output = log_path.read_text(encoding="utf-8")
                    if proc.poll() is not None:
                        raise RuntimeError("local Auth exited before readiness; check localhost socket permission and port availability")
                    match = re.search(r"  adapter: http://(127\.0\.0\.1:\d+) ", output)
                    if match and "Press Ctrl-C to stop" in output:
                        break
                    time.sleep(0.1)
                else:
                    raise RuntimeError("local Auth readiness timed out; check localhost socket permission and retry")

                authority = match.group(1)
                url = f"http://{authority}/orders"
                socket = str(pathlib.Path(work_dir) / "broker.sock")
                step(
                    "2/5", "Try the protected route unsigned",
                    f"HTTP GET {url}  (no Authorization header)",
                    "The adapter should deny before the synthetic upstream is called.",
                    args.walkthrough,
                )
                unsigned_status, unsigned_body = request(url)
                if args.walkthrough:
                    print(f"  Result: HTTP {unsigned_status}; reason={reason(unsigned_body)}", flush=True)

                step(
                    "3/5", "Issue proof and call the exact route",
                    "$ trustplane broker issue --socket <private socket> --method GET --scheme http "
                    f"--authority {authority} --path /orders --audience example-api "
                    "--route-id orders.list --allow-software-fallback\n"
                    f"  HTTP GET {url}  (fresh broker headers held in memory)",
                    "The broker signs the exact method, authority, path, audience, and route; Auth forwards only a valid call.",
                    args.walkthrough,
                )
                headers = issue(cli, socket, authority)
                allowed_status, allowed_body = request(url, headers)
                if args.walkthrough:
                    print(f"  Result: HTTP {allowed_status}; upstream={allowed_body.get('service', 'local-demo')}", flush=True)

                step(
                    "4/5", "Replay the same proof",
                    f"HTTP GET {url}  (same in-memory broker headers)",
                    "The one-time passport should be rejected by replay consume-on-accept.",
                    args.walkthrough,
                )
                replay_status, replay_body = request(url, headers)
                if args.walkthrough:
                    print(f"  Result: HTTP {replay_status}; reason={reason(replay_body)}", flush=True)

                step(
                    "5/5", "Change the path after signing",
                    "$ trustplane broker issue --socket <private socket> --method GET --scheme http "
                    f"--authority {authority} --path /orders --audience example-api "
                    "--route-id orders.list --allow-software-fallback  (new nonce)\n"
                    f"  HTTP GET http://{authority}/other  (fresh proof bound to /orders)",
                    "Auth should reject the path mismatch before the request reaches upstream.",
                    args.walkthrough,
                )
                fresh_headers = issue(cli, socket, authority)
                tamper_status, tamper_body = request(f"http://{authority}/other", fresh_headers)

                if args.walkthrough:
                    print(f"  Result: HTTP {tamper_status}; reason={reason(tamper_body)}", flush=True)
                else:
                    print(f"unsigned: HTTP {unsigned_status}; reason={reason(unsigned_body)}")
                    print(f"signed: HTTP {allowed_status}; upstream={allowed_body.get('service', 'local-demo')}")
                    print(f"replay: HTTP {replay_status}; reason={reason(replay_body)}")
                    print(f"changed path: HTTP {tamper_status}; reason={reason(tamper_body)}")
                expected = (unsigned_status, allowed_status, replay_status, tamper_status)
                if expected != (401, 200, 403, 403):
                    raise RuntimeError(f"unexpected HTTP status sequence: {expected}")
                if reason(replay_body) != "jti_replay":
                    raise RuntimeError("replay denial reason changed")
                if reason(tamper_body) != "request_binding_mismatch":
                    raise RuntimeError("path-binding denial reason changed")
                print("PASS: local protected request, replay, and path binding")
            finally:
                proc.terminate()
                try:
                    proc.wait(timeout=5)
                except subprocess.TimeoutExpired:
                    proc.kill()
                    proc.wait(timeout=5)


if __name__ == "__main__":
    main()
