#!/usr/bin/env python3

# This is a simple HTTP server based on the HTTPServer and
# SimpleHTTPRequestHandler. It has been extended with PUT
# and DELETE functionality to store or delete results.
#
# See: https://github.com/python/cpython/blob/main/Lib/http/server.py

import argparse
import base64
import os
import signal
import socket
import sys
from functools import partial
from http import HTTPStatus
from http.server import HTTPServer, SimpleHTTPRequestHandler
from pathlib import Path


class AuthenticationError(Exception):
    pass


class PUTEnabledHTTPRequestHandler(SimpleHTTPRequestHandler):
    def __init__(self, *args, basic_auth=None, **kwargs):
        self.basic_auth = None
        if basic_auth:
            self.basic_auth = base64.b64encode(basic_auth.encode("ascii")).decode(
                "ascii"
            )
        super().__init__(*args, **kwargs)

    def do_GET(self):  # noqa: N802
        try:
            self._handle_auth()
            super().do_GET()
        except AuthenticationError:
            self.send_error(HTTPStatus.UNAUTHORIZED, "Need Authentication")

    def do_HEAD(self):  # noqa: N802
        try:
            self._handle_auth()
            super().do_HEAD()
        except AuthenticationError:
            self.send_error(HTTPStatus.UNAUTHORIZED, "Need Authentication")

    def do_PUT(self):  # noqa: N802
        path = Path(self.translate_path(self.path))
        path.parent.mkdir(parents=True, exist_ok=True)
        try:
            self._handle_auth()
            file_length = int(self.headers["Content-Length"])
            path.write_bytes(self.rfile.read(file_length))
            self.send_response(HTTPStatus.CREATED)
            self.send_header("Content-Length", "0")
            self.end_headers()
        except AuthenticationError:
            self.send_error(HTTPStatus.UNAUTHORIZED, "Need Authentication")
        except OSError:
            self.send_error(
                HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot open file for writing"
            )

    def do_DELETE(self):  # noqa: N802
        path = Path(self.translate_path(self.path))
        try:
            self._handle_auth()
            path.unlink()
            self.send_response(HTTPStatus.OK)
            self.send_header("Content-Length", "0")
            self.end_headers()
        except AuthenticationError:
            self.send_error(HTTPStatus.UNAUTHORIZED, "Need Authentication")
        except OSError:
            self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot delete file")

    def _handle_auth(self):
        if not self.basic_auth:
            return
        authorization = self.headers.get("authorization")
        if authorization:
            authorization = authorization.split()
            if authorization == ["Basic", self.basic_auth]:
                return
        raise AuthenticationError("Authentication required")


def _get_best_family(*address):
    infos = socket.getaddrinfo(
        *address,
        type=socket.SOCK_STREAM,
        flags=socket.AI_PASSIVE,
    )
    family, _type, _proto, _canonname, sockaddr = next(iter(infos))
    return family, sockaddr


def run(handler_class, server_class, port, bind):
    handler_class.protocol_version = "HTTP/1.1"
    server_class.address_family, addr = _get_best_family(bind, port)

    with server_class(addr, handler_class) as httpd:
        host, port = httpd.socket.getsockname()[:2]
        url_host = f"[{host}]" if ":" in host else host
        print(f"Serving HTTP on {host} port {port} (http://{url_host}:{port}/) ...")
        try:
            httpd.serve_forever()
        except KeyboardInterrupt:
            print("\nKeyboard interrupt received, exiting.")
            sys.exit(0)


def on_terminate(signum, _frame):
    sys.stdout.flush()
    sys.stderr.flush()
    sys.exit(128 + signum)


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--basic-auth", "-B", help="Basic auth tuple like user:pass")
    parser.add_argument(
        "--bind",
        "-b",
        metavar="ADDRESS",
        help="Specify alternate bind address [default: all interfaces]",
    )
    parser.add_argument(
        "--directory",
        "-d",
        default=Path.cwd(),
        help="Specify alternative directory [default:current directory]",
    )
    parser.add_argument(
        "port",
        action="store",
        default=8080,
        type=int,
        nargs="?",
        help="Specify alternate port [default: 8080]",
    )
    args = parser.parse_args()

    handler_class = partial(PUTEnabledHTTPRequestHandler, basic_auth=args.basic_auth)

    os.chdir(args.directory)

    signal.signal(signal.SIGINT, on_terminate)
    signal.signal(signal.SIGTERM, on_terminate)

    run(
        handler_class=handler_class,
        server_class=HTTPServer,
        port=args.port,
        bind=args.bind,
    )
