# Hi.
# I was tempted to implement my own custom HTTP server to show how an HTTP server works.
# But I'm pretty sure that's out of the scope of this demo.
# After all, HTTPS is just HTTP with TLS on top of it, and I'm not about to program Diffe-Hellman for this.
# Anyways, holy yap sesh.

# Import the HTTPStatus "enumeration" to have more readable code.
# An enumeration is just a thing with multiple diffent possibilities, and there are only so many HTTP
# status codes outlined by HTTP specifications.
# In reality, HTTP statuses are intentgers, with 1XX-5XX, with the first number dictating the category
# of error.
# For example, the HTTPStatus.NOT_FOUND would be a 404 error status code,
# And a HTTPStatus.OK would be the 200 success status code
from http import HTTPStatus

# This (flask) is the HTTP server library we are using.
# Response is the Python object representing what we return to the client
# Request is the Python object representing the request from the client
# send_file is a function that turns the contents of the specified file into a Response
# Flask is the server itself.
from flask import Flask, Response, request, send_file

# Cyrptographically secure, random bits.
# On Linux this is reading the bits from the special file /dev/urandom
# Less specifically, this uses a lot of the system's entropy (non-deterministic things based off practically
# random events, like the users recent mouse movements and secure positions) to generate random bits.
from os import urandom

# The threading library allows you to spawn multiple processes from within this one program.
# This is called concurrency, with each Thread being a concurrent process spun from a main thread.
# The Thread object represents one of those other Threads within the process.
# The Event object just allows for cross-thread communications.
# In this program, we will spin two threads up, one for the HTTP server and one for the HTTP server.
# We will use the Event object as a way to tell the servers to shut down.
from threading import Thread, Event

# The make_server function is just a sort of wrapper for the HTTP server to allow things like shutting down
# the server through a function call. Normally the call to run a Flask server is blocking I believe,
# meaning it stops program execution until it returns, which doesn't happen under normal circumstances.
from werkzeug.serving import make_server

# This is for type hinting. This is mostly for me, so that I know the type that each variable should be.
# Optional means that it can either be the None object, or whatever is within the brackets following the
# "Optional" type hint.
# For example, Optional[str] means that the value will either be None or a string.
from typing import Optional

# More type hinting...
from types import FrameType

# This is what allows us to handle signals like Ctrl+C (SIGINT), which we will use to trigger the Event
# for the Threads to know to shut the server down
import signal

# We only use this as a way to tell the system the program excuted sucessfully, and as intented.
import sys

# Used as a way to encode the password, or way of "hashing" it.
from base64 import b64encode as base64_encode

# The IP address the server will listen on.
SERVER_IP: str = "127.0.0.1"
# The exepected HTTP host header.
# This can use used to only response if the URL in the web browser contains "sniphbank.com"
# rather than accessing the IP (127.0.0.1) directly. Right now, this is not enabled.
HTTP_HOST: str = "sniphbank.com"
# The port for the HTTP socket.
# 80 is the default port, and what the browser automatically connects to when given "http://"
HTTP_PORT: int = 80
# The port for the HTTPS socket.
# 443 is the default port, and what the browser automatically connects to when given "https://"
HTTPS_PORT: int = 443
# This will be used to tell the Flask server whether or not it should be running in Debug mode.
DEBUG: bool = False

# Required to be in global scope
# The Flask server itself.
app = Flask(__name__)
# A secret key required for secure operations.
app.secret_key = urandom(24)


# The logic for what happens when someone visits the root of the HTTP server, so
# "http(s)://sniphbank.com", as the browser automatically requests "/"
@app.route("/")
def index():
    # We simply return the contents of the index.html file stored in the "website" directory in this
    # projects structure.
    return send_file("./website/index.html")


# The logic for when a user clicks on the "submit" button.
# The HTML login form's submit button sends an HTTP "POST" to "/login", and the response is then
# awaited for as if you clicked on a link to a new page.
@app.route("/login", methods=["POST"])
def login():
    # There isn't really a reason to validate the user for the demo,
    # just decide on a username that might pertain to whomever's
    # personal information

    # The "user" input field in the HTML form.
    user = request.form["username"]
    # Likewise, the "password" input field in the HTML form.
    # We turn this into bytes so we can encode it with base64
    password = request.form["password"].encode()

    # In a real web application, the values are pulled from a database and not hardcoded.
    # If the username is not "ronniej"
    if user != "ronniej":
        # Then we return the respponse "User not found" with the HTTP 401 status code (UNAUTHORIZED)
        return Response("User not found", HTTPStatus.UNAUTHORIZED)
    # Also in a real application, the password will be hashed rather than merely base64 encoded.
    # The difference with it being hashed is that it's easly reversable, someone with access to this
    # code (or in a real case, a database) would be able to easily reverse this to get the genuine
    # password.
    # The point of this here is to showcase that password is in plaintext until it gets to the server.
    if base64_encode(password).decode() != "Z29GQUxDT05TMTIzCg==":
        # Likewise if the password doesn't match, we return a similar response.
        return Response("Incorrect password", HTTPStatus.UNAUTHORIZED)

    # In order to get this far, the username and password are correct, so we can send the webpage.
    return send_file("./website/account.html")


# CSS, or Cascading Style Sheets, is the very thing that makes the web useable.
# Otherwise, every element in HTML (Hypertext Markup Language, NOT a programming language)
# would be completely black text on white background, and blocks placed top to bottom
# If you wanna try it yourself, feel free to run this code and rename/move/change the file path
# below. The server will return a "404: Not Found" error and the browser won't be able to render
# the webpage as expected.
@app.route("/css/style.css", methods=["GET"])
def stylesheet():
    return send_file("./website/css/style.css")


# Very similar, the favicon.ico is automatically requested from the server by the Web Browser
# This is the lil icon that is displayed on the tab next to the HTML page's title.
@app.route("/favicon.ico", methods=["GET"])
def favicon():
    return send_file("./website/favicon.ico")


# This is a lil fancy, but I thought it was a fair obstraction to handle the threading in a...more...
# "readable" way...?
# Basically, we are creating a class that is based off the "Thread" class, so it shares the same properties
# and methods.
# Here, we are going to add some things to it, for example, the Flask server, the IP and port we want
# to listen on, and the SSL Context if we are using HTTPS
class ServerThread(Thread):
    def __init__(
        self,
        app: Flask,
        host: str,
        port: int,
        ssl_context: Optional[tuple[str, str]] = None,
        name: str = "ServerThread",
    ) -> None:
        super().__init__(name=name, daemon=True)
        # threaded=True is telling the werkzeug server that it can use threads to handle multiple
        # requests at the same time. Not really important for the demo I don't think, and I'll
        # likely get rid of it.
        self.server = make_server(
            host,
            port,
            app,
            threaded=True,
            # This is either None or the public SSL cert and private key (or is it the other way around...?)
            ssl_context=ssl_context,  # tuple (cert, key) or SSLContext
        )

    def run(self):
        # serve_forever blocks until shutdown() is called
        self.server.serve_forever()

    def shutdown(self):
        self.server.shutdown()


def _install_signal_handlers(stop_event: Event):
    def handler(signum: signal.Signals, frame: Optional[FrameType]):
        stop_event.set()

    # Handle Ctrl+C and SIGTERM
    signal.signal(signal.SIGINT, handler)
    try:
        signal.signal(signal.SIGTERM, handler)
    except AttributeError:
        # SIGTERM may not be available on some platforms (e.g., Windows Python in some contexts)
        # I'm always on Linux, so if you wanna run this, then you should either use WSL or Podman.
        # (Podman is a generally more performant and secure alternative to Docker, if you are familiar)
        pass


# This block literally just says "If this is the program being run, do the following"
if __name__ == "__main__":
    # Create the stop event
    stop_event = Event()
    # Install the signal handler for SIGINT (Ctrl+C signal)
    _install_signal_handlers(stop_event)

    # Our HTTP server thread.
    http_server = ServerThread(
        app, SERVER_IP, HTTP_PORT, ssl_context=None, name="HTTP-Thread"
    )
    # Similarly, the HTTPS server thread.
    https_server = ServerThread(
        app,
        SERVER_IP,
        HTTPS_PORT,
        ssl_context=("./certs/cert.pem", "./certs/key.pem"),
        name="HTTPS-Thread",
    )

    try:
        # Start the HTTP and HTTPS server threads
        http_server.start()
        https_server.start()
        # Make sure there's some output to show that the servers running.
        print(f"HTTP  server running at http://{SERVER_IP}:{HTTP_PORT}")
        print(f"HTTPS server running at https://{SERVER_IP}:{HTTPS_PORT}")
        print("Press Ctrl+C to stop.")
        # Wait until a signal requests shutdown
        stop_event.wait()
    except KeyboardInterrupt:
        # Typically how you handle Ctrl+C, but since we have a singal hanlder it's pointless.
        # Doesn't hurt anyone though.
        pass
    finally:
        # Stop both servers.
        for srv in (http_server, https_server):
            try:
                srv.shutdown()
            except Exception:
                pass

        # Join both Threads (meaning take their execution and join it with the main thread's).
        for srv in (http_server, https_server):
            srv.join(timeout=5)

        # Output that the servers shut down gracefully (enough)
        print("Servers stopped!")
        # This just tells the shell that "hey, we exited as expected."
        sys.exit(0)
