from __future__ import annotations

import json
from typing import Dict, Type, TypedDict
from uuid import UUID

import httpx
import tornado
from tornado.routing import _RuleList

from aext_shared.backend_proxy import backend_proxy
from aext_shared.config import get_config
from aext_shared.handler import BackendHandler, create_rules

from .base_handlers import NotebookBaseHandler, PABaseHandler
from .const import MS_API_Version
from .exceptions import (
    CantCreateProject,
    CantCreateProjectVersion,
    CantUpdateProject,
    CondaEnvironmentNotFound,
)
from .handlers_helpers import create_response
from .logger import custom_logger
from .request_schemas.notebook_projects_microservice import (
    CreateProject,
    CreateProjectVersion,
    UpdateProject,
)
from .schemas import (
    MetaResponse,
    ProjectInfoData,
    RequestError,
    ServiceResponse,
    VersioningData,
)
from .services.deployment_microservice import project_microservice_service
from .services.panel import panel_service
from .services.project import project_service
from .utils import to_json

config = get_config()


class ProjectInfo(TypedDict):
    name: str
    path: str
    description: str
    versions: list[str]


class HealthzRouteHandler(BackendHandler):
    @tornado.web.authenticated
    async def get(self):
        self.finish({"live": True})


class ProjectInfoHandler(NotebookBaseHandler):
    """
    Return unique CalVar version for a project by querying existing
    versions and incrementing until a new version is found.
    """

    @tornado.web.authenticated
    async def get(self):
        self.set_header("Content-Type", "application/json")
        file_path = self.get_argument("notebook_file_path", None)
        project_id = self.get_argument("project_id", None)

        if not file_path.endswith(".ipynb"):
            file_path += ".ipynb"
        file_info = await self._get_file_info(f"{file_path}")
        if not file_info:
            response = create_response(status=True, message="Project file_path not found")
            return self.finish(response, status_code=400)

        pa_project_info = None
        try:
            # Check if project already exists
            if project_id:
                pa_project_info = await project_service.get_project_info(self.api, project_id)
        except Exception:
            custom_logger.error("Not possible to get the project from PythonAnywhere")

        # if not exists a pa_project_info, create a new one project
        if pa_project_info is None:
            # Get Conda environment
            try:
                environment = await self.extract_notebook_environment(file_info.file_name)
            except CondaEnvironmentNotFound:
                err_msg = "Notebook doesn't have a Conda environment"
                response = create_response(status=True, message=err_msg, content="")
                return self.finish(response, status_code=400)

            new_project = await project_service.create_project(file_info, environment)
            response = create_response(message="", content=new_project)
        else:
            next_version = project_service._create_or_get_version(pa_project_info.versions[-1])
            versioning = VersioningData(
                next_version=next_version,
                current_version=pa_project_info.versions[-1],
                all_versions=pa_project_info.versions,
            )
            existing_project = ProjectInfoData(
                title=pa_project_info.title,
                project_id=pa_project_info.project_id,
                versioning=versioning,
                notebook_file_name=pa_project_info.notebook_file_name,
                notebook_file_size=pa_project_info.notebook_file_size,
                permission="base",
                environment=pa_project_info.environment,
                status="",
                description=pa_project_info.description,
                domains=[],
            )
            response = create_response(message="", content=existing_project)

        return self.finish(response)


class ProjectHandler(NotebookBaseHandler):
    @tornado.web.authenticated
    async def post(self):
        self.set_header("Content-Type", "application/json")
        path = self.get_argument("path")
        title = self.get_argument("title")
        version = self.get_argument("version")
        description = self.get_argument("description", default="")
        environment = self.get_argument("environment", default="base")
        project_id = self.get_argument("project_id", default=None)

        notebook_path = self._get_path(path)
        notebook_file_info = await self._get_file_info(notebook_path)
        user_access = await self.get_user_access_credentials()

        # Validate Project version
        if not project_service.is_valid_version(version):
            response = create_response(status=True, message="Project Version is not valid", content="")
            return self.finish(response, status_code=400)

        # Validate notebook file path
        if not notebook_file_info:
            response = create_response(status=True, message="Project file_path not found", content="")
            return self.finish(response, status_code=400)

        try:
            await self._log_notebook_data(notebook_path, "aext-cloud-panels-notebook-data")
        except Exception as ex:
            custom_logger.error(f"Error while trying to log: {vars(ex)}")

        # Get Conda environment
        try:
            environment = await self.extract_notebook_environment(notebook_path)
        except CondaEnvironmentNotFound:
            response = create_response(status=True, message="Conda environment not found", content="")
            return self.finish(response, status_code=400)
        err = RequestError(status=False)
        custom_logger.info("Creating project info")
        project_info = await project_service.create_project_info(
            title,
            version,
            description,
            notebook_file_info,
            environment,
        )
        custom_logger.info("Generating project files")
        project_content = await project_service.generate_files(
            user_access.username,
            title,
            version,
            project_info,
            environment,
            notebook_path,
        )
        try:
            api_version = self.request.headers.get("X-MS-API-Version", MS_API_Version.V1)
            user_access = await self.get_user_access_credentials()
            async with project_microservice_service.access_credentials(user_access):
                async with project_microservice_service.incoming_request(self.request):
                    if not project_id:
                        custom_logger.info("Creating a new project")
                        request_data = CreateProject(
                            user_id=user_access.username,
                            title=title,
                            version=version,
                            environment=environment,
                            notebook_file_name=notebook_file_info.file_name,
                            notebook_file_size=notebook_file_info.file_size,
                            permission="public",
                            description=description,
                        )
                        project_response = await project_microservice_service.create_project(
                            request_data, project_content.archive_content, api_version
                        )
                    else:
                        custom_logger.info("Updating the project")
                        project_response = await project_microservice_service.update_project(
                            project_id,
                            UpdateProject(title=title, description=description),
                            project_content.archive_content,
                        )
        except CantCreateProject:
            project_response = "Can't create new project"
            err.status = True
            err.message = "Failure while creating new project"
        except CantUpdateProject:
            project_response = "Can't create new project"
            err.status = True
            err.message = "Failure while updating project"

        response = to_json(ServiceResponse(meta=MetaResponse(error=err), content=project_response))
        self.finish(response)


class ProjectDownloadHandler(PABaseHandler):
    @tornado.web.authenticated
    async def get(self):
        project_id = self.get_argument("project_id")
        project_version = self.get_argument("project_version")

        if not project_id or not project_version:
            return self.finish(
                to_json(
                    ServiceResponse(
                        content=None,
                        meta=MetaResponse(RequestError(True, "Missing project_id and/or project_version")),
                    )
                ),
                status_code=httpx.codes.BAD_REQUEST,
            )

        self.set_header("Content-Type", "application/json, charset=utf-8")
        api_version = self.request.headers.get("X-MS-API-Version", MS_API_Version.V1)
        user_access = await self.get_user_access_credentials()
        async with project_microservice_service.access_credentials(user_access):
            async with project_microservice_service.incoming_request(self.request):
                response = await project_microservice_service.download_project_files(
                    project_id, project_version, api_version
                )
        return self.finish(to_json(response))


class DeploymentHandler(PABaseHandler):
    @tornado.web.authenticated
    async def get(self):
        self.set_header("Content-Type", "application/json")
        user_access = await self.get_user_access_credentials()
        async with project_microservice_service.access_credentials(user_access):
            async with project_microservice_service.incoming_request(self.request):
                custom_logger.debug("Calling microservice to list projects")
                response = await project_microservice_service.list_projects(user_access.username)
        return self.finish(to_json(response))

    @tornado.web.authenticated
    async def patch(self):
        self.set_header("Content-Type", "application/json")

        is_enabled = self.get_argument("enabled").lower()
        domain_name = self.get_argument("domain")
        user_access = await self.get_user_access_credentials()
        async with project_microservice_service.access_credentials(user_access):
            async with project_microservice_service.incoming_request(self.request):
                if is_enabled == "true":
                    service_response = await project_microservice_service.start_app(domain_name, user_access.username)
                elif is_enabled == "false":
                    service_response = await project_microservice_service.stop_app(domain_name, user_access.username)
                else:
                    service_response = ServiceResponse(
                        content=None,
                        meta=MetaResponse(RequestError(True, "The enable parameter must either true or false string")),
                    )

        return self.finish(to_json(service_response))

    @tornado.web.authenticated
    async def put(self):
        self.set_header("Content-Type", "application/json")
        domain_name = self.get_argument("domain")
        user_access = await self.get_user_access_credentials()
        async with project_microservice_service.access_credentials(user_access):
            async with project_microservice_service.incoming_request(self.request):
                response = await project_microservice_service.restart_app(domain_name, user_access.username)
        if response.has_failed():
            self.set_status(500)
        return self.finish(to_json(response))


class ProjectActionHandler(PABaseHandler):
    @tornado.web.authenticated
    async def post(self, project: str, action: str):
        if not project:
            return self.finish(
                to_json(
                    ServiceResponse(
                        content=None,
                        meta=MetaResponse(RequestError(True, "Missing project_id and/or version_id")),
                    )
                ),
                status_code=httpx.codes.BAD_REQUEST,
            )

        self.set_header("Content-Type", "application/json")
        self.set_status(200)
        api_version = self.request.headers.get("X-MS-API-Version", MS_API_Version.V1)

        user_access = await self.get_user_access_credentials()
        async with project_microservice_service.access_credentials(user_access):
            async with project_microservice_service.incoming_request(self.request):
                if action == "publish":
                    response = await project_microservice_service.publish_latest_version(project)
                    return self.finish(to_json(response))
                elif action == "delete":
                    self.set_status(200)
                    response = await project_microservice_service.delete_project(project, api_version)
                    if response.has_failed():
                        self.set_status(400)
                    return self.finish(to_json(response))
                else:
                    response = {"success": False, "message": f"Unknown action {action}"}
                    self.set_status(400)

        self.finish(json.dumps(response))

    @tornado.web.authenticated
    async def get(self, domain_name: str, log_type: str):
        """
        Fetch Panel app custom_logger.
        Parameters
        ----------
        domain_name: app domain
        log_type: [access_log | error_log]

        Returns
        -------
        JSON response
        """
        self.set_header("Content-Type", "application/json")

        response = await panel_service.get_app_log(self.api, domain_name, log_type)

        return self.finish(to_json(response))


class ProjectVersionHandler(NotebookBaseHandler):
    @tornado.web.authenticated
    async def get(self, project_id: UUID, version_id: UUID):
        if not project_id or not version_id:
            return self.finish(
                to_json(
                    ServiceResponse(
                        content=None,
                        meta=MetaResponse(RequestError(True, "Missing project_id and/or version_id")),
                    )
                ),
                status_code=httpx.codes.BAD_REQUEST,
            )

        self.set_header("Content-Type", "application/json")
        api_version = self.request.headers.get("X-MS-API-Version", MS_API_Version.V1)

        user_access = await self.get_user_access_credentials()
        async with project_microservice_service.access_credentials(user_access):
            async with project_microservice_service.incoming_request(self.request):
                response = await project_microservice_service.get_project_notebook(project_id, version_id, api_version)

        self.finish(to_json(response))

    @tornado.web.authenticated
    async def post(self, project_id: UUID, version_id: UUID):
        if version_id:
            self.finish(
                to_json(
                    ServiceResponse(
                        content=None,
                        meta=MetaResponse(RequestError(True, "POST method not supported")),
                    )
                ),
                status_code=httpx.codes.METHOD_NOT_ALLOWED,
            )

        if not project_id:
            self.finish(
                to_json(
                    ServiceResponse(
                        content=None,
                        meta=MetaResponse(RequestError(True, "Missing project_id")),
                    )
                ),
                status_code=httpx.codes.BAD_REQUEST,
            )

        self.set_header("Content-Type", "application/json")

        title = self.get_argument("title")
        description = self.get_argument("description")
        path = self.get_argument("path")
        version = self.get_argument("version")

        notebook_path = self._get_path(path)
        notebook_file_info = await self._get_file_info(notebook_path)
        user_access = await self.get_user_access_credentials()

        # validate project version
        if not project_service.is_valid_version(version):
            return self.finish(
                to_json(
                    ServiceResponse(
                        content=None,
                        meta=MetaResponse(RequestError(True, "Project version is not valid")),
                    )
                ),
                status_code=httpx.codes.BAD_REQUEST,
            )

        # validate notebook file path
        if not notebook_file_info:
            return self.finish(
                to_json(
                    ServiceResponse(
                        content=None,
                        meta=MetaResponse(RequestError(True, "Project file path not found")),
                    )
                ),
                status_code=httpx.codes.BAD_REQUEST,
            )

        # validate onda environment
        try:
            environment = await self.extract_notebook_environment(notebook_path)
        except CondaEnvironmentNotFound:
            return self.finish(
                to_json(
                    ServiceResponse(
                        content=None,
                        meta=MetaResponse(RequestError(True, "Conda environment not found")),
                    )
                ),
                status_code=httpx.codes.BAD_REQUEST,
            )

        err = RequestError(status=False)

        custom_logger.info("Creating project version info")
        project_info = await project_service.create_project_info(
            title,
            version,
            description,
            notebook_file_info,
            environment,
        )

        custom_logger.info("Generating project files")
        project_content = await project_service.generate_files(
            user_access.username,
            title,
            version,
            project_info,
            environment,
            notebook_path,
        )
        try:
            api_version = self.request.headers.get("X-MS-API-Version", MS_API_Version.V1)
            async with project_microservice_service.access_credentials(user_access):
                async with project_microservice_service.incoming_request(self.request):
                    custom_logger.info("Creating a new project version")
                    request_data = CreateProjectVersion(
                        project_id=project_id,
                        version=version,
                        environment=environment,
                        notebook_file_name=notebook_file_info.file_name,
                        notebook_file_size=notebook_file_info.file_size,
                    )

                    project_version_response = await project_microservice_service.create_project_version(
                        request_data, project_content.archive_content, api_version
                    )
        except CantCreateProjectVersion:
            project_version_response = "Can't create new project version"
            err.status = True
            err.message = "Failure while creating new project version"

        response = to_json(ServiceResponse(meta=MetaResponse(error=err), content=project_version_response))
        self.finish(response)

    @tornado.web.authenticated
    async def delete(self, project_id: UUID, version_id: UUID):
        if not project_id or not version_id:
            return self.finish(
                to_json(
                    ServiceResponse(
                        content=None,
                        meta=MetaResponse(RequestError(True, "Missing project_id and/or version_id")),
                    )
                ),
                status_code=httpx.codes.BAD_REQUEST,
            )

        self.set_header("Content-Type", "application/json")
        api_version = self.request.headers.get("X-MS-API-Version", MS_API_Version.V1)

        user_access = await self.get_user_access_credentials()
        async with project_microservice_service.access_credentials(user_access):
            async with project_microservice_service.incoming_request(self.request):
                response = await project_microservice_service.delete_project_version(
                    project_id, version_id, api_version
                )

        self.finish(to_json(response))


class FeaturedAppsHandler(BackendHandler):
    @tornado.web.authenticated
    async def get(self):
        try:
            response = await backend_proxy(
                self.request,
                f"{config['cloud']['staticContent']}/anaconda-notebooks/panels/featured-apps/featured-apps.json",
            )
            self.finish(response)
        except Exception:
            custom_logger.error("Not possible to get featured apps")


def get_routes(base_url: str) -> _RuleList:
    handlers: Dict[str, Type[PABaseHandler]] = {
        "healthz": HealthzRouteHandler,
        "deployments": DeploymentHandler,
        "project-info": ProjectInfoHandler,
        "projects": ProjectHandler,
        "projects/download": ProjectDownloadHandler,
        r"projects/(.*?)/version/(.*?)": ProjectVersionHandler,
        r"projects/(.*?)/(.*)/": ProjectActionHandler,
        "featured-apps": FeaturedAppsHandler,
    }
    rules_new_prefix = create_rules(base_url, "aext_panels_server", handlers)
    rules_old_prefix = create_rules(base_url, "pythonanywhere", handlers)

    return rules_new_prefix + rules_old_prefix
