import asyncio
import json
import os
import pathlib
import re
import urllib.parse
from pprint import pformat
from typing import Optional

from aext_panels_server.exceptions import CondaEnvironmentNotFound, FileNotFound
from tornado.escape import json_encode
from tornado.httpclient import AsyncHTTPClient, HTTPRequest

from aext_shared.errors import BackendError, BadRequest
from aext_shared.handler import BackendHandler

from .api import PythonAnywhereAPI
from .logger import custom_logger
from .schemas import FileInfo
from .utils import clean_notebook


class PABaseHandler(BackendHandler):
    def _get_server_root(self):
        # Get the base URL of the Jupyter server
        referer = self.request.headers.get("Referer")
        try:
            if referer:
                parsed = urllib.parse.urlparse(referer)
                path = [el for el in parsed.path.split("/") if el]
                if path:
                    # structure = protocol; domain; jupyterlab; user; user_id
                    return f"{parsed.scheme}://{parsed.netloc}/{path[0]}/{path[1]}/{path[2]}"
        except Exception as ex:
            custom_logger.error(f"Could not get server root from Referer {ex}")

        server_root = f"{self.request.protocol}://{self.request.host}"
        return server_root

    async def _evaluate_feature_flag(self, namespace: str, flag_name: str):
        if namespace not in ["nucleus", "notebooks"]:
            raise ValueError(f"Feature flag namespace {namespace} not known")
        try:
            response = await self.anaconda_cloud_proxy(
                f"feature-flags/evaluate/{namespace}/{flag_name}?feature_flag_default=true",
                method="POST",
            )
            if response.get("remote_status_code") == 200:
                return response.get("remote_data")
        except (BackendError, BadRequest) as ex:
            custom_logger.error(f"Error evaluating feature flag {vars(ex)}")

        return False

    async def _log_notebook_data(self, notebook_path: str, log_event_name: str):
        json_data = {"log": True, "notebook_path": notebook_path, "event_name": log_event_name}
        if await self._evaluate_feature_flag("notebooks", "panels-send-logs-to-snake-eyes"):
            headers = {}
            url = f"{self._get_server_root()}/aext_notebook_info_server/info"
            incoming_cookies = {key: morsel.value for key, morsel in self.request.cookies.items()}
            outbound_cookies = "; ".join([f"{name}={value}" for name, value in incoming_cookies.items()])
            if authorization := self.request.headers.get("Authorization"):
                headers["Authorization"] = authorization
            if xsrf := self.cookies.get("_xsrf"):
                headers["X-Xsrftoken"] = xsrf.value
            if outbound_cookies:
                headers["Cookie"] = outbound_cookies

            http_client = AsyncHTTPClient()
            request = HTTPRequest(
                url,
                method="POST",
                body=json_encode(json_data),
                headers=headers,
            )
            response = await http_client.fetch(request)
            custom_logger.info(f"Log: {response}")

    def initialize(self, **kwargs):
        super().initialize(**kwargs)
        self.notebook_path = kwargs.pop("notebook_path", [])
        self.api = PythonAnywhereAPI(
            csrftoken=self.get_cookie("csrftoken"),
            sessionid=self.get_cookie("sessionid"),
        )
        api_vars_dump = vars(self.api)
        with open("/tmp/app.log", "w") as f:
            f.write(pformat(api_vars_dump))

    def _on_finish(self, future):
        if self.api.session is not None:
            asyncio.ensure_future(self.api.session.close())

    def finish(self, *args, **kwargs):
        status_code = kwargs.pop("status_code", None)
        if status_code:
            self.set_status(status_code)

        if not self._finished:
            future = super().finish(*args, **kwargs)
            future.add_done_callback(self._on_finish)
            return future


class NotebookBaseHandler(PABaseHandler):
    def _get_path(self, path: str) -> str:
        root_dir = get_server_root_dir(self.application.settings)
        rel_path = pathlib.Path(self.notebook_path or path)
        if rel_path.is_absolute():
            notebook_path = str(rel_path)
        else:
            notebook_path = str((root_dir / rel_path).absolute())
        return notebook_path

    async def _get_file_info(self, file_path: str) -> Optional[FileInfo]:
        notebook_path = self._get_path(file_path)
        size_str = "0 Bytes"

        if os.path.isfile(notebook_path):
            # TODO: Return error if file does not exist instead of returning dummy data
            # return 400 with a payload saying that the notebook was not found
            with open(notebook_path, "rb") as f:
                data = f.read()
            data = clean_notebook(data.decode("utf-8")).encode("utf-8")

            file_size = os.path.getsize(notebook_path)
            if file_size < 1024:
                size_str = f"{file_size} Bytes"
            elif file_size < 1024 * 1024:
                size_str = f"{file_size / 1024:.2f} KB"
            elif file_size < 1024 * 1024 * 1024:
                size_str = f"{file_size / (1024 * 1024):.2f} MB"
            else:
                size_str = f"{file_size / (1024 * 1024 * 1024):.2f} GB"

            return FileInfo(file_name=file_path, file_size=size_str)

    async def extract_notebook_environment(self, notebook_file_path: str) -> str:
        """Extract conda environment name from notebook file

        Parameters
        ----------
        notebook_file_path: Notebook file path

        Returns
        -------
        conda environment name
        """
        try:
            with open(notebook_file_path, "r", encoding="utf-8") as f:
                nb = json.load(f)
                kernel = nb.get("metadata", {}).get("kernelspec", {}).get("name")

        except FileNotFound:
            kernel = None

        if not kernel:
            raise CondaEnvironmentNotFound()

        kernelspec = self.kernel_manager.kernel_spec_manager.get_kernel_spec(kernel)
        environment = kernelspec.metadata.get("conda_env_name") if kernelspec else None

        # checks for custom conda environment and strips ".conda-" prefix
        match = re.search(r"^.conda-(.*)", environment)
        if match:
            return match.group(1)

        return environment


def get_server_root_dir(settings):
    if "server_root_dir" in settings:
        # notebook >= 5.0.0 has this in the settings
        root_dir = settings["server_root_dir"]
    else:
        # This copies the logic added in the notebook in
        #  https://github.com/jupyter/notebook/pull/2234
        contents_manager = settings["contents_manager"]
        root_dir = contents_manager.root_dir
    return os.path.expanduser(root_dir)
