"""Original LockedIn Labs HTTP retry workshop. Python 3.11+ standard library.

One caller and two owned loopback HTTP servers. Actual GETs and handler counts;
no vendor SDK, external service, model, business write or load benchmark.
Immediate bounded retries omit backoff, jitter and end-to-end deadlines.
"""
import argparse
from contextlib import contextmanager
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import sys
import threading
from urllib.error import HTTPError
from urllib.parse import parse_qs, urlsplit
from urllib.request import build_opener, ProxyHandler, Request

CASES = ("nested-unavailable", "single-owner-unavailable", "nested-invalid", "single-owner-invalid", "nested-transient", "single-owner-transient")


def get_json(url, headers, timeout=5):
    opener = build_opener(ProxyHandler({}))  # Local requests do not inherit a proxy.
    try:
        response = opener.open(Request(url, headers=headers), timeout=timeout)
    except HTTPError as error:
        response = error  # A received HTTP error is distinct from a transport error.
    with response:
        raw = response.read(4097)
        if len(raw) > 4096:
            raise RuntimeError("unexpected_fixture_body_size")
        return response.code, json.loads(raw)


class Handler(BaseHTTPRequestHandler):
    def log_message(self, *_args):
        pass

    def send_json(self, status, value):
        body = json.dumps(value).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


@contextmanager
def serving(role, handler, lifecycle):
    server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
    server.daemon_threads = False
    thread = threading.Thread(target=lambda: server.serve_forever(poll_interval=0.01), name="fde-http-retry-" + role)
    state = {"role": role, "host": server.server_address[0], "port": server.server_address[1], "stopped": False}
    lifecycle.append(state)
    thread.start()
    try:
        yield f"http://127.0.0.1:{server.server_address[1]}"
    finally:
        server.shutdown()
        server.server_close()
        thread.join(2)
        state["stopped"] = not thread.is_alive()
        if thread.is_alive():
            raise RuntimeError("owned_server_thread_not_stopped")


def run_case(case, attempts=3, failures=2):
    if case not in CASES:
        raise ValueError("unsupported case")
    if type(attempts) is not int or not 1 <= attempts <= 4:
        raise ValueError("attempts must be an integer from 1 through 4")
    if type(failures) is not int or not 0 <= failures <= 8:
        raise ValueError("failures must be an integer from 0 through 8")
    nested = case.startswith("nested-")
    transient = case.endswith("-transient")
    path = "/lookup" if case.endswith("-invalid") else "/lookup?item=synthetic"
    service_limit = attempts if nested else 1
    lock = threading.Lock()
    provider_events, service_events, caller_events, errors, lifecycle = [], [], [], [], []

    def should_retry(status):
        return status != 200 if nested else status == 503

    class Provider(Handler):
        def do_GET(self):
            parsed = urlsplit(self.path)
            with lock:
                number = len(provider_events) + 1
                if parsed.path != "/lookup":
                    status, reason = 404, "unknown_path"
                elif parse_qs(parsed.query).get("item") != ["synthetic"]:
                    status, reason = 400, "missing_or_invalid_item"
                elif transient and number > failures:
                    status, reason = 200, "found"
                else:
                    status, reason = 503, "fixture_unavailable"
                record = {"provider_request": number, "caller_attempt": int(self.headers["X-Caller-Attempt"]),
                          "service_attempt": int(self.headers["X-Service-Attempt"]), "method": "GET", "path": self.path, "status": status}
                provider_events.append(record)
            self.send_json(status, {"reason": reason, "provider_request": number})

    with serving("provider", Provider, lifecycle) as provider_url:
        class Service(Handler):
            def do_GET(self):
                caller_attempt = int(self.headers["X-Caller-Attempt"])
                record = {"caller_attempt": caller_attempt, "path": self.path, "provider_attempts": []}
                with lock:
                    service_events.append(record)
                try:
                    for inner in range(1, service_limit + 1):
                        status, body = get_json(provider_url + self.path, {"X-Caller-Attempt": str(caller_attempt), "X-Service-Attempt": str(inner)}, timeout=1)
                        record["provider_attempts"].append({"service_attempt": inner, "status": status, "provider_request": body["provider_request"]})
                        if not should_retry(status):
                            break
                    record["returned_status"] = status
                    self.send_json(status, {"provider_status": status, "provider_attempts_sent": len(record["provider_attempts"]), "reason": body["reason"]})
                except Exception as error:
                    with lock:
                        errors.append(type(error).__name__)
                    self.send_json(500, {"reason": "unexpected_fixture_failure"})

        with serving("service", Service, lifecycle) as service_url:
            for outer in range(1, attempts + 1):
                status, body = get_json(service_url + path, {"X-Caller-Attempt": str(outer)})
                caller_events.append({"caller_attempt": outer, "status": status, "body": body})
                if not should_retry(status):
                    break
    if errors:
        raise RuntimeError("unexpected_handler_error:" + ",".join(errors))
    return {"case": case, "configured_total_attempts": attempts, "service_total_attempts_per_request": service_limit,
            "retry_policy": "both layers retry every non-200" if nested else "caller alone retries received 503",
            "transient_failures_before_success": failures if transient else None,
            "caller_attempts": caller_events, "service_requests": service_events, "provider_requests": provider_events,
            "counts": {"caller_to_service": len(service_events), "service_to_provider": len(provider_events)},
            "final_http_status": caller_events[-1]["status"], "owned_servers": lifecycle,
            "handler_errors": errors}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--case", choices=CASES, help="One comparison; default runs all six")
    parser.add_argument("--attempts", type=int, default=3, help="Total attempts including first, from 1 through 4")
    parser.add_argument("--failures", type=int, default=2, help="Transient-case provider failures before success, from 0 through 8")
    args = parser.parse_args()
    if not 1 <= args.attempts <= 4 or not 0 <= args.failures <= 8:
        parser.error("--attempts must be 1..4 and --failures must be 0..8")
    print(json.dumps({"schema": "lockedin.http-retry-amplification/v1", "python_version": sys.version.split()[0],
                      "claim": "Actual sequential loopback GETs and handler observations. No SDK, real provider, business effect, end-to-end deadline, jitter or performance measurement.",
                      "cases": [run_case(case, args.attempts, args.failures) for case in ([args.case] if args.case else CASES)]}, indent=2))


if __name__ == "__main__":
    main()
