#!/usr/bin/env python3
"""Original HTTP-cache teaching fixture: owned loopback only, no external data.
Not a general HTTP cache or production server. Python 3.11+ standard library.
"""
import argparse
import copy
import email.utils
import http.client
import json
import platform
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

CASES = ("url-only-variant", "vary-aware", "no-cache-revalidation", "no-store", "vary-star", "orphan-304")


class Origin(ThreadingHTTPServer):
    daemon_threads = True
    def __init__(self):
        super().__init__(("127.0.0.1", 0), Handler)
        self.revision = 1
        self.exchanges = []


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

    def do_GET(self):
        language = self.headers.get("Accept-Language", "en")
        if self.path not in ("/localized", "/validate", "/no-store", "/vary-star", "/orphan-304") or language not in ("en", "fr"):
            self.send_error(400)
            return
        revision = self.server.revision
        body = json.dumps({"language": language, "revision": revision, "instruction": ("Keep the original evidence." if language == "en" else "Conservez les preuves originales.")}, ensure_ascii=True, sort_keys=True).encode()
        etag = '"' + language + "-r" + str(revision) + '"'
        control = "public, max-age=600"
        if self.path == "/validate": control = "no-cache, max-age=600"
        if self.path == "/no-store": control = "no-store, max-age=600"
        vary = "*" if self.path == "/vary-star" else "Accept-Language"
        # Deliberately faulty upstream response in one separately reset case.
        status = 304 if self.path == "/orphan-304" or self.headers.get("If-None-Match") == etag else 200
        sent = b"" if status == 304 else body
        self.server.exchanges.append({"path": self.path, "accept_language": language, "if_none_match": self.headers.get("If-None-Match"), "status": status, "etag": etag, "body_bytes": len(sent)})
        self.send_response(status)
        self.send_header("Cache-Control", control)
        self.send_header("Vary", vary)
        self.send_header("ETag", etag)
        if status == 200:
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(sent)))
        self.end_headers()
        if sent: self.wfile.write(sent)


class LocalCache:
    """Bounded GET-only fixture. One exact language value, no cookies or auth.
    Only these authored directives/headers are supported. No stale fallback,
    partial bodies, redirects, arbitrary URLs or general header grammar.
    """
    def __init__(self, origin, ignore_vary=False):
        self.origin = origin
        self.ignore_vary = ignore_vary
        self.entries = []

    def request(self, path, language):
        request_headers = {"accept-language": language}
        entry = next((e for e in self.entries if e["path"] == path and "*" not in e["vary"] and (self.ignore_vary or all(e["request_headers"].get(h) == request_headers.get(h) for h in e["vary"]))), None)
        age = None if entry is None else entry["initial_age"] + time.monotonic() - entry["received_at"]
        if entry and "no-cache" not in entry["directives"] and age < entry["max_age"]:
            return {"requested_language": language, "source": "stored-fresh", "network_status": None, "network_body_bytes": 0, "representation": copy.deepcopy(entry["body"]), "validated_etag": None}
        headers = {"Accept-Language": language}
        if entry: headers["If-None-Match"] = entry["etag"]
        conn = http.client.HTTPConnection("127.0.0.1", self.origin.server_port, timeout=3)
        start = time.monotonic()
        try:
            conn.request("GET", path, headers=headers)
            response = conn.getresponse()
            raw = response.read()
            received = time.monotonic()
            date = email.utils.parsedate_to_datetime(response.getheader("Date")).timestamp()
            control = response.getheader("Cache-Control", "")
            vary = [v.strip().lower() for v in response.getheader("Vary", "").split(",") if v.strip()]
            etag = response.getheader("ETag")
            status = response.status
        finally:
            conn.close()
        base = {"requested_language": language, "network_status": status, "network_body_bytes": len(raw), "response_headers": {"cache-control": control, "vary": vary, "etag": etag}}
        if status == 304:
            # Narrow authored validators: exactly one strong tag and unchanged
            # Vary/directives. A 304 is not a replacement representation.
            if entry is None or etag != entry["etag"] or vary != entry["vary"] or control != entry["control"] or raw:
                return {**base, "source": "withheld", "reason": "no_matching_stored_representation", "representation": None, "validated_etag": None}
            entry["received_at"] = received
            entry["initial_age"] = max(0, time.time() - date) + received - start
            return {**base, "source": "validated-stored", "representation": copy.deepcopy(entry["body"]), "validated_etag": etag}
        if status != 200:
            return {**base, "source": "withheld", "reason": "unsupported_response", "representation": None, "validated_etag": None}
        body = json.loads(raw)
        directives = [d.strip() for d in control.split(",")]
        max_age = next((int(d.split("=", 1)[1]) for d in directives if d.startswith("max-age=")), 0)
        if entry is not None: self.entries.remove(entry)
        if "no-store" not in directives and "*" not in vary:
            self.entries.append({"path": path, "request_headers": request_headers, "vary": vary, "body": body, "etag": etag, "control": control, "directives": directives, "max_age": max_age, "initial_age": max(0, time.time() - date) + received - start, "received_at": received})
        return {**base, "source": "origin-body", "representation": copy.deepcopy(body), "validated_etag": None}


def run_case(name, first_language):
    origin = Origin()
    worker = threading.Thread(target=origin.serve_forever, kwargs={"poll_interval": 0.01})
    worker.start()
    cache = LocalCache(origin, ignore_vary=name == "url-only-variant")
    other = "fr" if first_language == "en" else "en"
    try:
        if name in ("url-only-variant", "vary-aware"):
            steps = [cache.request("/localized", first_language), cache.request("/localized", other), cache.request("/localized", first_language)]
        elif name == "no-cache-revalidation":
            steps = [cache.request("/validate", first_language), cache.request("/validate", first_language)]
            origin.revision = 2  # Fixture driver changes source between requests.
            steps.append(cache.request("/validate", first_language))
        elif name in ("no-store", "vary-star"):
            path = "/" + name
            steps = [cache.request(path, first_language), cache.request(path, first_language)]
        else:
            steps = [cache.request("/orphan-304", first_language)]
        result = {"case": name, "listener": {"host": "127.0.0.1", "port": origin.server_port}, "steps": steps, "origin_exchanges": copy.deepcopy(origin.exchanges), "stored_entries": [{"path": e["path"], "vary": e["vary"], "request_headers": e["request_headers"], "etag": e["etag"], "cache_control": e["control"], "body": copy.deepcopy(e["body"])} for e in cache.entries]}
    finally:
        origin.shutdown()
        worker.join(timeout=3)
        origin.server_close()
    if worker.is_alive() or origin.socket.fileno() != -1:
        raise RuntimeError("Owned loopback listener did not close")
    result["listener_closed"] = True
    return result


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--case", choices=CASES)
    parser.add_argument("--first-language", choices=("en", "fr"), default="en")
    args = parser.parse_args()
    cases = [args.case] if args.case else CASES
    report = {"schema": "lockedin.http-cache-lab/v1", "python": platform.python_version(), "first_language": args.first_language, "scope": "Owned loopback GETs; bounded teaching cache, no authentication or external requests", "cases": [run_case(name, args.first_language) for name in cases]}
    print(json.dumps(report, indent=2, ensure_ascii=True))


if __name__ == "__main__":
    main()
