# maubot - A plugin-based Matrix bot system.
# Copyright (C) 2022 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations

from typing import Callable
import asyncio
import importlib.resources as resources
import json
import logging
import pathlib

from aiohttp import hdrs, web
from aiohttp.abc import AbstractAccessLogger
from yarl import URL

from mautrix.api import Method, PathBuilder

from .__meta__ import __version__
from .config import Config
from .plugin_server import PluginWebApp, PrefixResource


class AccessLogger(AbstractAccessLogger):
    def log(self, request: web.Request, response: web.Response, time: int):
        self.logger.info(
            f'{request.remote} "{request.method} {request.path} '
            f"{response.status} {response.body_length} "
            f'in {round(time, 4)}s"'
        )


class MaubotServer:
    log: logging.Logger = logging.getLogger("maubot.server")
    plugin_routes: dict[str, PluginWebApp]

    def __init__(
        self, management_api: web.Application, config: Config, loop: asyncio.AbstractEventLoop
    ) -> None:
        self.loop = loop or asyncio.get_event_loop()
        self.app = web.Application(loop=self.loop, client_max_size=100 * 1024 * 1024)
        self.config = config

        self.setup_appservice()
        self.app.add_subapp("/_matrix/maubot/v1", management_api)
        self.setup_instance_subapps()
        self.setup_management_ui()

        self.runner = web.AppRunner(self.app, access_log_class=AccessLogger)

    async def handle_plugin_path(self, request: web.Request) -> web.StreamResponse:
        for path, app in self.plugin_routes.items():
            if request.path.startswith(path):
                request = request.clone(
                    rel_url=request.rel_url.with_path(
                        request.rel_url.path[len(path) - 1 :]
                    ).with_query(request.query_string)
                )
                return await app.handle(request)
        return web.Response(status=404)

    def get_instance_subapp(self, instance_id: str) -> tuple[PluginWebApp, str]:
        subpath = self.config["server.plugin_base_path"] + instance_id + "/"
        url = self.config["server.public_url"] + subpath
        try:
            return self.plugin_routes[subpath], url
        except KeyError:
            app = PluginWebApp()
            self.plugin_routes[subpath] = app
            return app, url

    def remove_instance_webapp(self, instance_id: str) -> None:
        try:
            subpath = self.config["server.plugin_base_path"] + instance_id + "/"
            self.plugin_routes.pop(subpath).clear()
        except KeyError:
            return

    def setup_instance_subapps(self) -> None:
        self.plugin_routes = {}
        resource = PrefixResource(self.config["server.plugin_base_path"].rstrip("/"))
        resource.add_route(hdrs.METH_ANY, self.handle_plugin_path)
        self.app.router.register_resource(resource)

    def setup_appservice(self) -> None:
        as_path = PathBuilder("/_matrix/appservice/v1")
        self.add_route(Method.PUT, as_path.transactions, self.handle_transaction)

    def setup_management_ui(self) -> None:
        ui_base = self.config["server.ui_base_path"]
        if ui_base == "/":
            ui_base = ""
        directory = self.config["server.override_resource_path"]
        if not directory:
            resource_dir = resources.files("maubot").joinpath("management/frontend/build")
            assert isinstance(
                resource_dir, pathlib.Path
            ), "Resources must be available on disk (use override_resource_path to specify custom path)"
            directory = str(resource_dir)
        self.app.router.add_static(f"{ui_base}/static", f"{directory}/static")
        self.setup_static_root_files(directory, ui_base)

        with open(f"{directory}/index.html", "r") as file:
            index_html = file.read()

        @web.middleware
        async def frontend_404_middleware(request: web.Request, handler) -> web.Response:
            if hasattr(handler, "__self__") and isinstance(handler.__self__, web.StaticResource):
                try:
                    return await handler(request)
                except web.HTTPNotFound:
                    return web.Response(body=index_html, content_type="text/html")
            return await handler(request)

        async def ui_base_redirect(_: web.Request) -> web.Response:
            raise web.HTTPFound(f"{ui_base}/")

        self.app.middlewares.append(frontend_404_middleware)
        self.app.router.add_get(
            f"{ui_base}/", lambda _: web.Response(body=index_html, content_type="text/html")
        )
        self.app.router.add_get(ui_base, ui_base_redirect)

    @staticmethod
    def _static_data(data: bytes, mime: str) -> Callable[[web.Request], web.Response]:
        def fn(_: web.Request) -> web.Response:
            return web.Response(body=data, content_type=mime)

        return fn

    def setup_static_root_files(self, directory: str, ui_base: str) -> None:
        files = {
            "asset-manifest.json": "application/json",
            "manifest.json": "application/json",
            "favicon.png": "image/png",
        }
        for file, mime in files.items():
            with open(f"{directory}/{file}", "rb") as stream:
                data = stream.read()
            self.app.router.add_get(f"{ui_base}/{file}", self._static_data(data, mime))

        public_url = self.config["server.public_url"]
        public_url_path = ""
        if public_url:
            public_url_path = URL(public_url).path.rstrip("/")

        api_path = f"{public_url_path}/_matrix/maubot/v1"

        path_prefix_response_body = json.dumps({"api_path": api_path.rstrip("/")})
        self.app.router.add_get(
            f"{ui_base}/paths.json",
            lambda _: web.Response(
                body=path_prefix_response_body, content_type="application/json"
            ),
        )

    def add_route(self, method: Method, path: PathBuilder, handler) -> None:
        self.app.router.add_route(method.value, str(path), handler)

    async def start(self) -> None:
        await self.runner.setup()
        site = web.TCPSite(self.runner, self.config["server.hostname"], self.config["server.port"])
        await site.start()
        self.log.info(f"Listening on {site.name}")

    async def stop(self) -> None:
        await self.runner.shutdown()
        await self.runner.cleanup()

    @staticmethod
    async def version(_: web.Request) -> web.Response:
        return web.json_response({"version": __version__})

    async def handle_transaction(self, request: web.Request) -> web.Response:
        return web.Response(status=501)
