"""
We can't use starlette's StaticFiles when running in wasm mode, because it launches a
thread. Instead, use our own crappy version. Fortunately, this is all we need.

When running in native Python mode, use the starlette StaticFiles impl; it's battle
tested, whereas ours is not. Under wasm, it's OK if ours has bugs, even security holes:
everything is running in the browser sandbox including the filesystem, so there's
nothing we could disclose that an attacker wouldn't already have access to. The same is
not true when running in native Python, we want to be as safe as possible.
"""

from __future__ import annotations

import re
from typing import Any

from . import _utils

__all__ = (
    "StaticFiles",
    "FileResponse",
)

import sys

from starlette.background import BackgroundTask

if "pyodide" not in sys.modules:
    # Running in native mode; use starlette StaticFiles
    import os

    import starlette.responses
    import starlette.staticfiles

    FileResponse = starlette.responses.FileResponse  # type: ignore

    # Wrapper for StaticFiles to fix .js content-type issues on Windows 10 (see #1601)
    class StaticFiles(starlette.staticfiles.StaticFiles):  # type: ignore
        def file_response(
            self,
            full_path: str | os.PathLike[str],
            *args: Any,
            **kwargs: Any,
        ) -> starlette.responses.Response:
            resp = super().file_response(full_path, *args, **kwargs)
            if resp.headers.get("content-type", "").startswith("text/plain"):
                correct_type = _utils.guess_mime_type(full_path)
                resp.headers["content-type"] = (
                    f"{correct_type}; charset={resp.charset}"
                    if correct_type.startswith("text/")
                    else correct_type
                )
                resp.media_type = correct_type
            return resp

else:
    # Running in wasm mode; must use our own simple StaticFiles

    import os
    import os.path
    import pathlib
    import urllib.parse
    from typing import Iterable, MutableMapping, Optional

    from starlette.responses import PlainTextResponse
    from starlette.types import Receive, Scope, Send

    class StaticFiles:
        dir: pathlib.Path
        root_path: str

        def __init__(self, *, directory: str | os.PathLike[str]):
            self.dir = pathlib.Path(os.path.realpath(os.path.normpath(directory)))

        async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
            if scope["type"] != "http":
                raise AssertionError("StaticFiles can't handle non-http request")
            # following starlette >=0.33, tested to be compatible with 0.32-0.37.2
            root_path = scope.get("route_root_path", scope.get("root_path", ""))
            path = scope.get("route_path", re.sub(r"^" + root_path, "", scope["path"]))
            path_segments = path.split("/")
            final_path, trailing_slash = _traverse_url_path(self.dir, path_segments)
            if final_path is None:
                return await Error404()(scope, receive, send)

            if not final_path.exists():
                return await Error404()(scope, receive, send)

            # Sanity check that final path is under self.dir, and if not, 404
            # This construction is like .is_relative_to, but works with Python 3.8
            try:
                final_path.relative_to(self.dir)
            except ValueError:
                return await Error404()(scope, receive, send)

            # Serve up the path

            if final_path.is_dir():
                if trailing_slash:
                    # We could serve up index.html or directory listing if we wanted
                    return await Error404()(scope, receive, send)
                else:
                    # We could redirect with an added "/" if we wanted
                    return await Error404()(scope, receive, send)
            else:
                return await FileResponse(final_path)(scope, receive, send)

    def _traverse_url_path(
        dir: pathlib.Path, path_segments: list[str]
    ) -> tuple[Optional[pathlib.Path], bool]:
        assert len(path_segments) > 0

        new_dir = dir
        path_segment = urllib.parse.unquote(path_segments.pop(0))
        # Gratuitous whitespace is not allowed
        if path_segment != path_segment.strip():
            return None, False

        # Check for illegal paths
        if "/" in path_segment:
            return None, False
        elif path_segment == ".." or path_segment == ".":
            return None, False

        if path_segment != "":
            new_dir = dir / path_segment

        if len(path_segments) == 0:
            return new_dir, path_segment == ""
        else:
            return _traverse_url_path(new_dir, path_segments)

    class Error404(PlainTextResponse):
        def __init__(self):
            super().__init__("404", status_code=404)  # type: ignore

    class FileResponse:
        file: pathlib.Path
        headers: Optional[MutableMapping[str, str]]
        media_type: str

        def __init__(
            self,
            file: pathlib.Path,
            headers: Optional[MutableMapping[str, str]] = None,
            media_type: Optional[str] = None,
            background: Optional[BackgroundTask] = None,
        ) -> None:
            self.headers = headers
            self.file = file
            self.background = background

            if media_type is None:
                media_type = _utils.guess_mime_type(file, strict=False)
            self.media_type = media_type

        async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
            with open(self.file, "rb") as f:
                await send(
                    {
                        "type": "http.response.start",
                        "status": 200,
                        "headers": _convert_headers(self.headers, self.media_type),
                    }
                )

                while True:
                    # In pyodide mode (the only mode in which we use this codepath) the
                    # `send()` callback has quite a bit of per-call overhead, so use a
                    # very large chunk size to keep performance adequate.
                    data = f.read(262144)
                    if len(data) == 0:
                        break
                    await send(
                        {
                            "type": "http.response.body",
                            "body": data,
                            "more_body": True,
                        }
                    )

                await send(
                    {"type": "http.response.body", "body": b"", "more_body": False}
                )
            if self.background:
                await self.background()

    def _convert_headers(
        headers: Optional[MutableMapping[str, str]], media_type: Optional[str] = None
    ) -> Iterable[tuple[bytes, bytes]]:
        if headers is None:
            headers = {}

        header_list = [
            (k.encode("latin-1"), v.encode("latin-1")) for k, v in headers.items()
        ]
        if media_type is not None:
            header_list += [
                (
                    b"Content-Type",
                    media_type.encode("latin-1"),
                )
            ]
        return header_list
