"""
Utilities for serving a kdlj2 application directly in a HTTPServer.

Useful for debugging and dynamic serving.
"""

import json
import mimetypes
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
from io import TextIOWrapper
from typing import Any

from jinja2 import Environment

from kdlj2_web.errors import TemplateNotFoundException

from .fs import get_gitignore_spec, handle_files_route
from .render import render_template


class BaseKdlJ2Handler(BaseHTTPRequestHandler):
    """Subclass of BaseHTTPRequestHandler with kdlj2 project speciifc logic."""

    DEBUG_MODE: bool = True
    CUSTOM_OPTS: dict[str, Any] | None = None
    SRC_PATH: str = ""
    ENV: Environment = Environment()

    def open(self, path: str, *args: Any, **kwargs: Any) -> TextIOWrapper:
        """
        Apply SRC_PATH transformation to the passed path before opening it.

        Returns a file object - the return value of open.

        WARNING: 'path' must be a relative path!
        """
        fp: TextIOWrapper = open(os.path.join(self.SRC_PATH, path), *args, **kwargs)
        return fp

    def path_join(self, *path_components: str) -> str:
        """
        Apply SRC_PATH transformation to passed path components and join them.

        WARNING: All path components _must_ be relative paths!
        """
        return os.path.join(".", self.SRC_PATH, *path_components)

    def do_GET(self) -> None:
        """
        Handle a HTTP GET request.

        In kdlj2, this means applying custom options, rendering templates as pages,
        and serving static files inside the 'static' subdir of the source path within
        the '/_files' HTTP path namespace.
        """
        # TODO: "HTTP path namespace" probably has a better name
        with self.open("options.json") as opts_file:
            self.ENV.globals = {**self.ENV.globals, **json.load(opts_file)}

        if self.path.startswith("/_files"):
            self.serve_filesystem()
            return

        if self.path.startswith("/static"):
            path = os.path.relpath("/" + self.path, "/")
            self.serve_file(path)
            return

        path = self.path.strip("/") or "index"
        template_name = f"{path}"

        self.serve_template(
            template_name, {"debug": self.DEBUG_MODE, **(self.CUSTOM_OPTS or {})}
        )  # overides Jinja globals

    def serve_template(
        self, template_name: str, options: dict[str, Any] | None = None
    ) -> None:
        """Render a kdlj2 template and return it to the HTTP request."""
        options = options or {}

        try:
            html = render_template(self.ENV, template_name, options)

            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            self.wfile.write(html.encode())

        except TemplateNotFoundException as e:
            status = 404
            self.serve_error(
                status, f"Error while trying to serve {template_name}: {e}"
            )
            return

        except Exception as e:
            status = 500
            self.serve_error(
                status, f"Error while trying to serve {template_name}: {e}"
            )

    def serve_error(self, status: int, error_description: str) -> None:
        """Render an error page if possible, and return it to the HTTP request."""
        try:
            html = render_template(
                self.ENV,
                "_error",
                {
                    "error": error_description,
                    "status": status,
                    "debug": self.DEBUG_MODE,
                    **(self.CUSTOM_OPTS or {}),
                },
            )

        except TemplateNotFoundException as e:
            self.send_error(
                404, error_description + " | Error rendering normal 404 page: " + str(e)
            )
            return

        self.send_response(status)
        self.send_header("Content-type", "text/html")
        self.end_headers()
        self.wfile.write(html.encode())

    def serve_filesystem(self) -> None:
        """Serve a filesystem path as a file browser if possible, and return in HTTP."""
        _, _, subpath = self.path.partition("/_files/")
        subpath = subpath or "/"

        request = handle_files_route(subpath, source_path=self.SRC_PATH)

        if request is None:
            self.serve_error(
                404, f"Path '{subpath}' doesn't exist in the project source code tree!"
            )
            return

        elif request.template is not None:
            self.serve_template(
                request.template,
                {
                    "debug": self.DEBUG_MODE,
                    "path": request.path,
                    "items": request.items,
                    **(self.CUSTOM_OPTS or {}),
                },
            )

        else:
            self.serve_file(request.path)

    def serve_file(self, rel_path_of_file: str) -> None:
        """Serve a raw file with the right MIME-type."""
        safe_path = os.path.relpath("/" + rel_path_of_file, "/")
        abs_path = self.path_join(os.getcwd(), self.SRC_PATH, safe_path)

        spec = get_gitignore_spec(self.SRC_PATH)
        check_path = rel_path_of_file + ("/" if os.path.isdir(abs_path) else "")

        if spec and spec.match_file(check_path):
            self.serve_error(403, "Access Denied: Path is gitignored.")
            return

        if not os.path.abspath(abs_path).startswith(os.getcwd()):
            self.serve_error(403, "Access Denied: Path is outside project root.")
            return

        try:
            stat = os.stat(abs_path)
            file_mtime = stat.st_mtime

            with open(abs_path, "rb") as f:
                content = f.read()

            ctype, _ = mimetypes.guess_type(abs_path)
            if not ctype:
                ctype = "text/plain"  # Fallback for unknown extensions

            self.send_response(200)
            self.send_header("Content-type", ctype)
            self.send_header("Content-Length", str(len(content)))
            self.send_header("Last-Modified", self.date_time_string(file_mtime))

            if rel_path_of_file.startswith("/static/"):
                self.send_header("Cache-Control", "public, max-age=31536000, immutable")
            else:
                self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
                self.send_header("Pragma", "no-cache")
                self.send_header("Expires", "0")

            self.end_headers()
            self.wfile.write(content)

        except Exception as e:
            self.serve_error(500, f"Error reading file: {e}")


def make_kdlj2_http_handler(
    real_env: Environment,
    source_path: str,
    custom_opts: dict[str, Any] | None = None,
    debug_mode: bool = True,
) -> type[BaseHTTPRequestHandler]:
    """
    Create a KDLJ2 HTTP request handler for HTTPServer.

    A server with this handler automatically serves rendered templates and 'static'
    files.
    """

    class _SpecifiedHandler(BaseKdlJ2Handler, BaseHTTPRequestHandler):
        CUSTOM_OPTS: dict[str, Any] = custom_opts or {}
        DEBUG_MODE: bool = debug_mode
        SRC_PATH: str = source_path
        ENV: Environment = real_env

    return _SpecifiedHandler


def setup_debug_server(
    env: Environment,
    source_path: str,
    port: int = 8080,
    custom_opts: dict[str, Any] | None = None,
) -> HTTPServer:
    """Make a HTTPServer configued to be a dev server using the kdlj2 handler."""
    print(f"Serving at http://localhost:{port}")
    handler = make_kdlj2_http_handler(env, source_path, custom_opts)

    return HTTPServer(("", port), handler)
