#!/usr/bin/env python3
"""Un sitio web de juguete, en memoria, para mirar HTTP/1.1 por dentro.

    python3 mini_web.py <dirección> <puerto>

No lee archivos del disco. Sirve dos sitios en la misma dirección, para ver para qué existe Host,
y cuenta las peticiones y las conexiones. A propósito no valida las peticiones como un servidor de
verdad: por ejemplo, acepta una petición HTTP/1.1 sin Host.
"""
import hashlib
import sys
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

SITES = {
    "www.wayra.test": {
        "/": b"<html><img src=/a.png><img src=/b.png><img src=/c.png>wayra</html>\n",
        "/a.png": b"A" * 900,
        "/b.png": b"B" * 900,
        "/c.png": b"C" * 900,
    },
    "blog.wayra.test": {"/": b"<html>el blog, en la misma IP</html>\n"},
}
DEFAULT_SITE = "www.wayra.test"
# Una página que cambia, para ver el fin de un ETag.
versions = {"/noticia": 1}
counters = {"requests": 0, "connections": 0}
lock = threading.Lock()


def etag_of(body):
    return '"' + hashlib.sha256(body).hexdigest()[:12] + '"'


class Handler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"  # persistentes por defecto
    server_version = "mini_web"
    # Encabezado y cuerpo salen en dos write. Sin esto, Nagle más el ACK retardado del cliente
    # agregarían unos 40 ms que no tienen nada que ver con lo que queremos medir.
    disable_nagle_algorithm = True

    def setup(self):
        super().setup()
        with lock:
            counters["connections"] += 1

    def version_string(self):
        return self.server_version

    def log_message(self, format, *args):
        print(f"{self.client_address[0]}:{self.client_address[1]} {format % args}", flush=True)

    def reply(self, status, body=b"", headers=()):
        self.send_response(status)
        for name, value in headers:
            self.send_header(name, value)
        # Un 304 no lleva cuerpo, y por eso tampoco necesita decir su largo.
        if status != 304:
            self.send_header("Content-Length", str(len(body)))
        # Si el cliente pidió cerrar, se lo confirmamos. send_header además marca el cierre.
        if self.headers.get("Connection", "").lower() == "close":
            self.send_header("Connection", "close")
        self.end_headers()
        if self.command != "HEAD":
            self.wfile.write(body)

    def do_HEAD(self):
        self.do_GET()

    def do_GET(self):
        with lock:
            counters["requests"] += 1
        if self.path == "/contadores":
            body = f"{counters['requests']} {counters['connections']}\n".encode()
            return self.reply(200, body, [("Cache-Control", "no-store")])
        if self.path == "/cambiar":
            with lock:
                versions["/noticia"] += 1
            return self.reply(200, b"cambiada\n", [("Cache-Control", "no-store")])
        if self.path == "/hora":
            # Sin ninguna indicación de cache.
            return self.reply(200, f"{time.time():.3f}\n".encode())
        if self.path == "/noticia":
            body = f"<html>noticia, versión {versions['/noticia']}</html>\n".encode()
            return self.conditional(body, "max-age=3")
        if self.path == "/visitas":
            # El servidor no recuerda a nadie: el estado viaja en la cookie, ida y vuelta.
            visits = int(self.cookie("visitas") or 0) + 1
            return self.reply(200, f"visita {visits}\n".encode(), [
                ("Set-Cookie", f"visitas={visits}; Path=/"), ("Cache-Control", "no-store")])
        if self.path == "/privado":
            return self.reply(200, b"su saldo\n", [("Cache-Control", "no-store")])
        host = (self.headers.get("Host") or DEFAULT_SITE).split(":")[0]
        body = SITES.get(host, SITES[DEFAULT_SITE]).get(self.path)
        if body is None:
            return self.reply(404, b"no existe\n")
        kind = "image/png" if self.path.endswith(".png") else "text/html"
        return self.conditional(body, "max-age=60", [("Content-Type", kind)])

    def cookie(self, name):
        for item in (self.headers.get("Cookie") or "").split(";"):
            key, _, value = item.strip().partition("=")
            if key == name and value.isdigit():
                return value
        return None

    def conditional(self, body, cache_control, extra=()):
        etag = etag_of(body)
        headers = [("ETag", etag), ("Cache-Control", cache_control), *extra]
        if self.headers.get("If-None-Match") == etag:
            return self.reply(304, b"", headers)
        return self.reply(200, body, headers)


def main():
    if len(sys.argv) != 3:
        sys.exit("uso: mini_web.py <dirección> <puerto>")
    server = ThreadingHTTPServer((sys.argv[1], int(sys.argv[2])), Handler)
    print(f"mini_web en {sys.argv[1]}:{sys.argv[2]}", flush=True)
    server.serve_forever()


if __name__ == "__main__":
    main()
