"""Local stale-check experiment. Python 3.9+, standard library only.

Runs on loopback, uses no cloud accounts, and shuts down its server on exit.
The X-Caller header is a test identity, not production authentication.
"""

import argparse
import hashlib
import json
import platform
import threading
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import ProxyHandler, Request, build_opener


def utc():
    return datetime.now(timezone.utc).isoformat()


def digest(value):
    return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest()


def main(output):
    # This fixed request specification represents the unchanged release input.
    change = {"caller": "checkout", "method": "GET", "path": "/ledger"}
    change_sha = digest(change)
    state = {"policy": {"allowed_callers": ["checkout"]}, "collector_up": True}
    lock = threading.Lock()
    events = []
    client = build_opener(ProxyHandler({}))  # Local traffic never uses a proxy.

    class Handler(BaseHTTPRequestHandler):
        def do_GET(self):
            with lock:
                policy = json.loads(json.dumps(state["policy"]))
                collector_up = state["collector_up"]
            if self.path == "/policy":
                status = 200 if collector_up else 503
                body = policy if collector_up else {"error": "collector unavailable"}
            elif self.path == "/ledger":
                allowed = self.headers.get("X-Caller") in policy["allowed_callers"]
                status = 200 if allowed else 403
                body = {"result": "ok" if allowed else "forbidden"}
            else:
                status, body = 404, {"error": "not found"}
            data = json.dumps(body).encode()
            self.send_response(status)
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(data)))
            self.end_headers()
            self.wfile.write(data)

        def log_message(self, *_):
            pass

    server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
    worker = threading.Thread(target=server.serve_forever, daemon=True)
    worker.start()
    base = f"http://127.0.0.1:{server.server_port}"

    def request(path, headers=None):
        try:
            response = client.open(Request(base + path, headers=headers or {}), timeout=3)
        except HTTPError as error:
            response = error
        with response:
            return response.code, json.loads(response.read())

    def collect():
        started = utc()
        status, policy = request("/policy")
        if status != 200:
            return {
                "complete": False,
                "http_status": status,
                "started_at": started,
                "finished_at": utc(),
                "target": base,
                "change_sha256": change_sha,
            }
        return {
            "complete": True,
            "started_at": started,
            "finished_at": utc(),
            "target": base,
            "change_sha256": change_sha,
            "policy": policy,
            "policy_sha256": digest(policy),
        }

    def assess(receipt):
        if not receipt["complete"]:
            return "HOLD"
        return "PASS" if change["caller"] in receipt["policy"]["allowed_callers"] else "BLOCK"

    def record(name, **fields):
        events.append({"event": name, "at": utc(), **fields})

    try:
        original = collect()
        baseline, _ = request(change["path"], {"X-Caller": change["caller"]})
        record("baseline", check=assess(original), request_http=baseline, receipt=original)
        assert assess(original) == "PASS" and baseline == 200

        with lock:
            state["policy"] = {"allowed_callers": []}
        cached_result = assess(original)
        blocked, _ = request(change["path"], {"X-Caller": change["caller"]})
        record("policy_changed", cached_check=cached_result, request_http=blocked)
        assert cached_result == "PASS" and blocked == 403

        fresh = collect()
        record("fresh_check", check=assess(fresh), receipt=fresh)
        assert assess(fresh) == "BLOCK"
        assert original["policy_sha256"] != fresh["policy_sha256"]
        assert original["change_sha256"] == fresh["change_sha256"]

        # Reset access, but fail collection. Required-gate behavior is HOLD,
        # even though the service is reachable and an earlier PASS exists.
        with lock:
            state["policy"] = {"allowed_callers": ["checkout"]}
            state["collector_up"] = False
        failed = collect()
        reachable, _ = request(change["path"], {"X-Caller": change["caller"]})
        record("collector_failed", check=assess(failed), request_http=reachable, receipt=failed)
        assert assess(failed) == "HOLD" and reachable == 200
        report = {
            "python": platform.python_version(),
            "system": platform.system(),
            "change": change,
            "change_sha256": change_sha,
            "events": events,
        }
        output.mkdir(parents=True, exist_ok=True)
        (output / "evidence.json").write_text(json.dumps(report, indent=2) + "\n")
        transcript = (
            f"change_sha256={change_sha}\n"
            f"baseline: check={assess(original)} request_http={baseline}\n"
            f"policy_changed: cached_check={cached_result} request_http={blocked}\n"
            f"fresh_check: check={assess(fresh)} change_unchanged=true policy_changed=true\n"
            f"collector_failed: check={assess(failed)} collector_http={failed['http_status']} request_http={reachable}\n"
            "assertions=passed\n"
        )
        (output / "transcript.txt").write_text(transcript)
        print(transcript, end="")
    finally:
        server.shutdown()
        server.server_close()
        worker.join(timeout=3)


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--output", type=Path, default=Path("demo-output"))
    main(parser.parse_args().output)
