import base64
import contextlib
from dataclasses import fields
from typing import IO, Iterator

import httpx
from aext_panels_server.api import (
    ProjectDeploymentMicroserviceAPI,
    project_deployment_ms_api,
)
from aext_panels_server.api_v2 import project_deployment_ms_api_v2
from aext_panels_server.const import MS_API_Version
from aext_panels_server.exceptions import (
    CantCreateProject,
    CantCreateProjectVersion,
    CantUpdateProject,
)
from aext_panels_server.handlers_helpers import create_response
from aext_panels_server.logger import custom_logger
from aext_panels_server.request_schemas.notebook_projects_microservice import (
    CreateProject,
    CreateProjectVersion,
    UpdateProject,
)
from aext_panels_server.schemas import (
    AppVersion,
    CreatePanelAppResponse,
    Deployment,
    Domain,
    ListAppsResponse,
    ProjectInfoData,
    ServiceResponse,
    WebappResponse,
)
from tornado.httputil import HTTPServerRequest

from aext_shared.handler import UserAccessCredentials


class ProjectDeploymentMicroserviceService:
    @contextlib.asynccontextmanager
    async def access_credentials(self, user_credentials: UserAccessCredentials) -> None:
        custom_logger.info(f"Set user creds: {user_credentials}")
        ProjectDeploymentMicroserviceStrategy.set_access_credentials(user_credentials)
        yield
        for field in fields(user_credentials):
            setattr(user_credentials, field.name, None)
        ProjectDeploymentMicroserviceStrategy.set_access_credentials(user_credentials)

    @contextlib.asynccontextmanager
    async def incoming_request(self, incoming_request: HTTPServerRequest) -> None:
        ProjectDeploymentMicroserviceStrategy.set_incoming_request(incoming_request)
        yield
        ProjectDeploymentMicroserviceStrategy.set_incoming_request(None)

    async def create_project(
        self, request_data: CreateProject, archive_content: IO, api_version: MS_API_Version
    ) -> ProjectInfoData:
        """
        Creates a new project
        Parameters
        ----------
        request_data: data to be sent to the microservice
        archive_content: archive content

        Returns ProjectInfoData
        -------
        """
        project_deployment_ms_api = ProjectDeploymentMicroserviceStrategy.get_api(api_version)
        response = await project_deployment_ms_api.create_project(request_data, archive_content)
        if response.status_code == httpx.codes.CREATED:
            return ProjectInfoData.create_from_microservice_response(response.json())

        raise CantCreateProject

    async def upload_project_archive(self, project_id: str, archive_content: IO) -> bool:
        """
        Uploads an archive to the latest version of the project
        Parameters
        ----------
        project_id: id of the project
        archive_content: file content

        Returns boolean
        -------

        """
        response = await project_deployment_ms_api.upload_project_archive(project_id, archive_content)
        if response.status_code == httpx.codes.OK:
            return True
        return False

    async def update_project(
        self, project_id: str, request_data: UpdateProject, archive_content: IO
    ) -> ProjectInfoData:
        """
        Update project information
        Parameters
        ----------
        project_id: project id that will be updated
        request_data: data  used to update the project that should be sent to the microservice
        archive_content: zip file containing the new version

        Returns ProjectInfoData
        -------

        """
        if not await self.upload_project_archive(project_id, archive_content):
            custom_logger.error("Could not update project: archive upload failed")
            raise CantUpdateProject

        response = await project_deployment_ms_api.update_project(project_id, request_data)
        if response.status_code == httpx.codes.OK:
            return ProjectInfoData.create_from_microservice_response(response.json())

        custom_logger.error(f"Could not update project: {response}")
        raise CantUpdateProject

    async def download_project_files(
        self, project_id: str, project_version: str, api_version: MS_API_Version
    ) -> ServiceResponse:
        """
        Downloads project archive files
        Parameters
        ----------
        project_id: id of the project
        project_version: version of the project

        Returns service response containing the archive content
        -------

        """
        project_deployment_ms_api = ProjectDeploymentMicroserviceStrategy.get_api(api_version)
        response = await project_deployment_ms_api.download_files(project_id, project_version)
        if response.status_code == httpx.codes.OK:
            if response.headers["content-type"] != "application/json":
                json_response = base64.b64encode(response.content).decode("utf-8")
            else:
                json_response = response.json()

            return create_response(
                status=False,
                content={"archive": json_response["archive"]},
                json_response=False,
            )

        return create_response(status=True, content={}, json_response=False)

    async def list_projects(self, user_id: str) -> ServiceResponse:
        """
        List all user apps

        Parameters
        ----------
        user_id: id of the user

        Returns
        -------

        """
        custom_logger.debug("Listing user projects")
        response = await project_deployment_ms_api.list_projects(user_id)
        custom_logger.debug(f"MICROSERVICE Response: {response}")
        if not response:
            return create_response(status=True, content={}, json_response=False)

        if response.status_code == httpx.codes.OK:
            custom_logger.debug("Listing user projects")
            json_response = response.json()
            all_apps = [
                ProjectInfoData.create_from_microservice_response(project_data)
                for project_data in json_response["projects"]
            ]
            return create_response(
                status=False,
                content=ListAppsResponse(
                    num_active_apps=json_response["num_active_apps"],
                    num_max_apps=json_response["num_max_apps"],
                    apps=all_apps,
                ),
                json_response=False,
            )

        return create_response(status=True, content={}, json_response=False)

    def _handle_app_action_response(self, json_response: dict, user_id: str) -> ServiceResponse:
        """
        This function handle the response returned by the microservice for
        start_app, stop_app and restart_app
        """
        domains = [
            Domain(
                webapp_id=json_response["webapp_id"],
                domain=domain["name"],
                enabled=domain["enabled"],
                user=user_id,
            )
            for domain in json_response["domains"]
        ]
        deployment = Deployment(
            id=json_response["id"],
            domains=domains,
            webapp=json_response["webapp_id"],
            enabled=True,
        )
        return create_response(
            status=False,
            content=deployment,
            json_response=False,
        )

    async def start_app(self, app_domain_name: str, user_id: str) -> ServiceResponse:
        """
        Start a deployed app that was previously stopped
        Parameters
        ----------
        app_domain_name: domain associated with the app
        user_id: id of the user

        Returns ServiceResponse
        -------

        """
        response = await project_deployment_ms_api.start_app(app_domain_name)
        if response.status_code == httpx.codes.OK:
            return self._handle_app_action_response(response.json(), user_id)

        return create_response(
            status=True,
            content={},
            json_response=False,
        )

    async def stop_app(self, app_domain_name: str, user_id: str) -> ServiceResponse:
        """
        Stops a deployed app

        Parameters
        ----------
        app_domain_name: domain associated with the app
        user_id: the app owner id

        Returns
        -------

        """
        response = await project_deployment_ms_api.stop_app(app_domain_name)
        if response.status_code == httpx.codes.OK:
            return self._handle_app_action_response(response.json(), user_id)

        return create_response(
            status=True,
            content={},
            json_response=False,
        )

    async def restart_app(self, app_domain_name: str, user_id: str) -> ServiceResponse:
        """
        Restart a deployed app
        Parameters
        ----------
        app_domain_name: domain associated with the app
        user_id: the app owner id

        Returns
        -------

        """
        response = await project_deployment_ms_api.restart_app(app_domain_name)
        if response.status_code == httpx.codes.OK:
            return self._handle_app_action_response(response.json(), user_id)

        return create_response(
            status=True,
            content={},
            json_response=False,
        )

    async def publish_latest_version(self, project_id: str) -> ServiceResponse:
        """
        Publishes/deploy the latest project version.

        Parameters
        ----------
        project_id: id of the project

        Returns ServiceResponse
        -------

        """
        api_response = await project_deployment_ms_api.publish_latest_version(project_id)
        if api_response.status_code in (httpx.codes.OK, httpx.codes.CREATED):
            json_response = api_response.json()
            if json_response["domains"]:
                response_model = CreatePanelAppResponse(
                    app=WebappResponse(
                        id=json_response["domains"][0]["webapp"],
                        domain_name=json_response["domains"][0]["domain"],
                        enabled=json_response["domains"][0]["enabled"],
                    )
                )
                return create_response(
                    status=False,
                    content=response_model,
                    json_response=False,
                )

        return create_response(
            status=True,
            content={},
            json_response=False,
        )

    async def delete_project(self, project_id: str, api_version: MS_API_Version) -> ServiceResponse:
        """
        Delete project files and possible deployments if there are any

        Parameters
        ----------
        project_id: id of the project

        Returns ServiceResponse
        -------

        """
        project_deployment_ms_api = ProjectDeploymentMicroserviceStrategy.get_api(api_version)
        response = await project_deployment_ms_api.delete_project(project_id)
        if response.status_code == httpx.codes.NO_CONTENT:
            return create_response(status=False, content={}, json_response=False)

        return create_response(status=True, content={}, json_response=False)

    async def create_project_version(
        self, request_data: CreateProjectVersion, archive_content: IO, api_version: MS_API_Version
    ) -> ServiceResponse:
        """
        Creates a new project version

        Parameters
        ----------
        request_data: data to be sent to the microservice
        archive_content: archive content

        Returns AppVersion
        -------
        """
        project_deployment_ms_api = ProjectDeploymentMicroserviceStrategy.get_api(api_version)
        response = await project_deployment_ms_api.create_project_version(request_data, archive_content)
        if response.status_code == httpx.codes.CREATED:
            return AppVersion.create_from_microservice_response(response.json())

        raise CantCreateProjectVersion

    async def get_project_notebook(self, project_id: str, version_id: str, api_version: MS_API_Version):
        """
        Retrieve a project notebook

        Parameters
        ----------
        project_id: id of the project
        version_id: version of the notebook to be retrieved

        Returns ServiceResponse
        ----------
        """
        project_deployment_ms_api = ProjectDeploymentMicroserviceStrategy.get_api(api_version)
        response = await project_deployment_ms_api.get_project_notebook(project_id, version_id)
        if response.status_code == httpx.codes.OK:
            return create_response(status=False, content=response.json(), json_response=False)

        return create_response(status=True, content={}, json_response=False)

    async def delete_project_version(self, project_id: str, version_id: str, api_version: MS_API_Version):
        """
        Delete a project version

        Parameters
        ----------
        project_id: id of the project
        version_id: version to be deleted

        Returns ServiceResponse
        ----------
        """
        project_deployment_ms_api = ProjectDeploymentMicroserviceStrategy.get_api(api_version)
        response = await project_deployment_ms_api.delete_project_version(project_id, version_id)
        if response.status_code == httpx.codes.NO_CONTENT:
            return create_response(status=False, content=response.json(), json_response=False)

        return create_response(status=True, content={}, json_response=False)


class ProjectDeploymentMicroserviceStrategy:
    API = {
        "v1": project_deployment_ms_api,
        "v2": project_deployment_ms_api_v2,
    }

    @classmethod
    def get_api(cls, version: MS_API_Version) -> ProjectDeploymentMicroserviceAPI:
        try:
            return cls.API.get(version, MS_API_Version.V1)
        except IndexError as e:
            msg = "Unknown API version"
            raise ValueError(msg) from e

    @classmethod
    def apis(cls) -> Iterator[ProjectDeploymentMicroserviceAPI]:
        for api in cls.API.values():
            yield api

    @classmethod
    def set_access_credentials(cls, user_access_credentials: UserAccessCredentials):
        for api in cls.apis():
            api.set_anaconda_cloud_access_token(user_access_credentials.access_token)
            api.set_anaconda_cloud_username(user_access_credentials.username)

    @classmethod
    def set_incoming_request(cls, incoming_request: HTTPServerRequest):
        for api in cls.apis():
            api.set_incoming_request(incoming_request)


project_microservice_service = ProjectDeploymentMicroserviceService()
