import base64
import json
import logging
import os
from datetime import datetime, timedelta
from urllib.parse import quote

import requests
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.http import FileResponse, Http404, HttpResponse, JsonResponse
from django.urls import reverse
from rest_framework import mixins
from rest_framework.decorators import (
    action,
    api_view,
    detail_route,
    list_route
)
from rest_framework.exceptions import ParseError
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND
from rest_framework.views import APIView

from airavata.model.appcatalog.computeresource.ttypes import (
    CloudJobSubmission,
    GlobusJobSubmission,
    LOCALSubmission,
    SSHJobSubmission,
    UnicoreJobSubmission
)
from airavata.model.application.io.ttypes import DataType
from airavata.model.credential.store.ttypes import SummaryType
from airavata.model.data.movement.ttypes import (
    GridFTPDataMovement,
    LOCALDataMovement,
    SCPDataMovement,
    UnicoreDataMovement
)
from airavata.model.experiment.ttypes import ExperimentSearchFields
from airavata.model.group.ttypes import ResourcePermissionType
from airavata.model.user.ttypes import Status
from airavata_django_portal_sdk import user_storage
from django_airavata.apps.api.view_utils import (
    APIBackedViewSet,
    APIResultIterator,
    APIResultPagination,
    GenericAPIBackedViewSet,
    IsInAdminsGroupPermission
)
from django_airavata.apps.auth import iam_admin_client
from django_airavata.apps.auth.models import EmailVerification

from . import (
    exceptions,
    helpers,
    models,
    output_views,
    serializers,
    signals,
    thrift_utils,
    tus,
    view_utils
)

READ_PERMISSION_TYPE = '{}:READ'

log = logging.getLogger(__name__)


class GroupViewSet(APIBackedViewSet):
    serializer_class = serializers.GroupSerializer
    lookup_field = 'group_id'
    pagination_class = APIResultPagination
    pagination_viewname = 'django_airavata_api:group-list'

    def get_list(self):
        view = self

        class GroupResultsIterator(APIResultIterator):
            def get_results(self, limit=-1, offset=0):
                group_manager = view.request.profile_service['group_manager']
                groups = group_manager.getGroups(view.authz_token)
                end = offset + limit if limit > 0 else len(groups)
                return groups[offset:end] if groups else []

        return GroupResultsIterator()

    def get_instance(self, lookup_value):
        return self.request.profile_service['group_manager'].getGroup(
            self.authz_token, lookup_value)

    def perform_create(self, serializer):
        group = serializer.save()
        group_id = self.request.profile_service['group_manager'].createGroup(
            self.authz_token, group)
        group.id = group_id
        users_added_to_group = set(group.members) - {group.ownerId}
        self._send_users_added_to_group(users_added_to_group, group)

    def perform_update(self, serializer):
        group = serializer.save()
        group_manager_client = self.request.profile_service['group_manager']
        if len(group._added_members) > 0:
            group_manager_client.addUsersToGroup(
                self.authz_token, group._added_members, group.id)
            self._send_users_added_to_group(group._added_members, group)
        if len(group._removed_members) > 0:
            group_manager_client.removeUsersFromGroup(
                self.authz_token, group._removed_members, group.id)
        if len(group._added_admins) > 0:
            group_manager_client.addGroupAdmins(
                self.authz_token, group.id, group._added_admins)
        if len(group._removed_admins) > 0:
            group_manager_client.removeGroupAdmins(
                self.authz_token, group.id, group._removed_admins)
        group_manager_client.updateGroup(self.authz_token, group)

    def perform_destroy(self, group):
        group_manager_client = self.request.profile_service['group_manager']
        group_manager_client.deleteGroup(
            self.authz_token, group.id, group.ownerId)

    def _send_users_added_to_group(self, internal_user_ids, group):
        for internal_user_id in internal_user_ids:
            user_id, gateway_id = internal_user_id.rsplit("@", maxsplit=1)
            user_profile = self.request.profile_service['user_profile'].getUserProfileById(
                self.authz_token, user_id, gateway_id)
            signals.user_added_to_group.send(
                sender=self.__class__,
                user=user_profile,
                groups=[group],
                request=self.request)


class ProjectViewSet(APIBackedViewSet):
    serializer_class = serializers.ProjectSerializer
    lookup_field = 'project_id'
    pagination_class = APIResultPagination
    pagination_viewname = 'django_airavata_api:project-list'

    def get_list(self):
        view = self

        class ProjectResultIterator(APIResultIterator):
            def get_results(self, limit=-1, offset=0):
                return view.request.airavata_client.getUserProjects(
                    view.authz_token, view.gateway_id, view.username, limit, offset)

        return ProjectResultIterator()

    def get_instance(self, lookup_value):
        return self.request.airavata_client.getProject(
            self.authz_token, lookup_value)

    def perform_create(self, serializer):
        project = serializer.save(
            owner=self.username,
            gatewayId=self.gateway_id)
        project_id = self.request.airavata_client.createProject(
            self.authz_token, self.gateway_id, project)
        project.projectID = project_id
        self._update_most_recent_project(project_id)

    def perform_update(self, serializer):
        project = serializer.save()
        self.request.airavata_client.updateProject(
            self.authz_token, project.projectID, project)
        self._update_most_recent_project(project.projectID)

    @list_route()
    def list_all(self, request):
        projects = self.request.airavata_client.getUserProjects(
            self.authz_token, self.gateway_id, self.username, -1, 0)
        serializer = serializers.ProjectSerializer(
            projects, many=True, context={'request': request})
        return Response(serializer.data)

    @detail_route()
    def experiments(self, request, project_id=None):
        experiments = request.airavata_client.getExperimentsInProject(
            self.authz_token, project_id, -1, 0)
        serializer = serializers.ExperimentSerializer(
            experiments, many=True, context={'request': request})
        return Response(serializer.data)

    def _update_most_recent_project(self, project_id):
        prefs = helpers.WorkspacePreferencesHelper().get(self.request)
        prefs.most_recent_project_id = project_id
        prefs.save()


class ExperimentViewSet(APIBackedViewSet):
    serializer_class = serializers.ExperimentSerializer
    lookup_field = 'experiment_id'

    def get_list(self):
        return self.request.airavata_client.getUserExperiments(
            self.authz_token, self.gateway_id, self.username, -1, 0)

    def get_instance(self, lookup_value):
        return self.request.airavata_client.getExperiment(
            self.authz_token, lookup_value)

    def perform_create(self, serializer):
        experiment = serializer.save(
            gatewayId=self.gateway_id,
            userName=self.username)
        experiment_id = self.request.airavata_client.createExperiment(
            self.authz_token, self.gateway_id, experiment)
        self._update_workspace_preferences(
            project_id=experiment.projectId,
            group_resource_profile_id=experiment.userConfigurationData.groupResourceProfileId,
            compute_resource_id=experiment.userConfigurationData.computationalResourceScheduling.resourceHostId)
        experiment.experimentId = experiment_id

    def perform_update(self, serializer):
        experiment = serializer.save(
            gatewayId=self.gateway_id,
            userName=self.username)
        self.request.airavata_client.updateExperiment(
            self.authz_token, experiment.experimentId, experiment)
        self._update_workspace_preferences(
            project_id=experiment.projectId,
            group_resource_profile_id=experiment.userConfigurationData.groupResourceProfileId,
            compute_resource_id=experiment.userConfigurationData.computationalResourceScheduling.resourceHostId)

    def _set_storage_id_and_data_dir(self, experiment):
        # Storage ID
        experiment.userConfigurationData.storageId = \
            settings.GATEWAY_DATA_STORE_RESOURCE_ID
        # Create experiment dir and set it on model
        if not experiment.userConfigurationData.experimentDataDir:
            project = self.request.airavata_client.getProject(
                self.authz_token, experiment.projectId)
            exp_dir = user_storage.get_experiment_dir(
                self.request, project.name, experiment.experimentName)
            experiment.userConfigurationData.experimentDataDir = exp_dir
        else:
            # get_experiment_dir will also validate that absolute paths are
            # inside the user's storage directory
            exp_dir = user_storage.get_experiment_dir(
                self.request,
                path=experiment.userConfigurationData.experimentDataDir)
            experiment.userConfigurationData.experimentDataDir = exp_dir

    def _move_tmp_input_file_uploads_to_data_dir(self, experiment):
        exp_data_dir = experiment.userConfigurationData.experimentDataDir
        for experiment_input in experiment.experimentInputs:
            if experiment_input.type == DataType.URI:
                if experiment_input.value:
                    experiment_input.value = \
                        self._move_if_tmp_input_file_upload(
                            experiment_input.value, exp_data_dir)
            elif experiment_input.type == DataType.URI_COLLECTION:
                data_product_uris = experiment_input.value.split(
                    ",") if experiment_input.value else []
                moved_data_product_uris = []
                for data_product_uri in data_product_uris:
                    moved_data_product_uris.append(
                        self._move_if_tmp_input_file_upload(data_product_uri,
                                                            exp_data_dir))
                experiment_input.value = ",".join(moved_data_product_uris)

    def _move_if_tmp_input_file_upload(
            self, data_product_uri, experiment_data_dir):
        """
        Conditionally moves tmp input file to data dir and returns new dp URI.
        """
        data_product = self.request.airavata_client.getDataProduct(
            self.authz_token, data_product_uri)
        if user_storage.is_input_file(
                self.request, data_product):
            moved_data_product = \
                user_storage.move_input_file(
                    self.request,
                    data_product,
                    experiment_data_dir)
            return moved_data_product.productUri
        else:
            return data_product_uri

    @detail_route(methods=['post'])
    def launch(self, request, experiment_id=None):
        try:
            if getattr(
                settings,
                'GATEWAY_DATA_STORE_REMOTE_API',
                    None) is not None:
                # Proxy the launch/ request to the remote Django portal
                # instance since it must setup the experiment data directory
                # which is only on the remote Django portal instance
                headers = {
                    'Authorization': f'Bearer {request.authz_token.accessToken}'}
                r = requests.post(
                    f'{settings.GATEWAY_DATA_STORE_REMOTE_API}/experiments/{quote(experiment_id)}/launch/',
                    headers=headers,
                )
                r.raise_for_status()
                return Response(r.json())
            else:
                experiment = request.airavata_client.getExperiment(
                    self.authz_token, experiment_id)
                self._set_storage_id_and_data_dir(experiment)
                self._move_tmp_input_file_uploads_to_data_dir(experiment)
                request.airavata_client.updateExperiment(
                    self.authz_token, experiment_id, experiment)
                request.airavata_client.launchExperiment(
                    request.authz_token, experiment_id, self.gateway_id)
                return Response({'success': True})
        except Exception as e:
            return Response({'success': False, 'errorMessage': e.message})

    @detail_route(methods=['get'])
    def jobs(self, request, experiment_id=None):
        jobs = request.airavata_client.getJobDetails(
            self.authz_token, experiment_id)
        serializer = serializers.JobSerializer(
            jobs, many=True, context={'request': request})
        return Response(serializer.data)

    @detail_route(methods=['post'])
    def clone(self, request, experiment_id=None):
        if getattr(
            settings,
            'GATEWAY_DATA_STORE_REMOTE_API',
                None) is not None:
            # Proxy the clone/ request to the remote Django portal instance
            # since it must locally copy input files, which are only on the
            # remote Django portal instance
            headers = {
                'Authorization': f'Bearer {request.authz_token.accessToken}'}
            r = requests.post(
                f'{settings.GATEWAY_DATA_STORE_REMOTE_API}/experiments/{quote(experiment_id)}/clone/',
                headers=headers,
            )
            r.raise_for_status()
            return Response(r.json())
        else:
            # figure what project to clone into
            experiment = self.request.airavata_client.getExperiment(
                self.authz_token, experiment_id)
            project_id = self._get_writeable_project(experiment)

            # clone experiment
            cloned_experiment_id = request.airavata_client.cloneExperiment(
                self.authz_token, experiment_id,
                "Clone of {}".format(experiment.experimentName), project_id)
            cloned_experiment = request.airavata_client.getExperiment(
                self.authz_token, cloned_experiment_id)

            # Create a copy of the experiment input files
            self._copy_cloned_experiment_input_uris(cloned_experiment)

            # Null out experimentDataDir so a new one will get created at launch
            # time
            cloned_experiment.userConfigurationData.experimentDataDir = None
            request.airavata_client.updateExperiment(
                self.authz_token, cloned_experiment.experimentId, cloned_experiment
            )
            serializer = self.serializer_class(
                cloned_experiment, context={'request': request})
            return Response(serializer.data)

    @detail_route(methods=['post'])
    def cancel(self, request, experiment_id=None):
        try:
            request.airavata_client.terminateExperiment(
                request.authz_token, experiment_id, self.gateway_id)
            return Response({'success': True})
        except Exception as e:
            log.error("Cancel action has thrown the following error: ", e)
            raise e

    def _get_writeable_project(self, experiment):
        # figure what project to clone into:
        # 1) project of this experiment if writeable
        # 2) most recently used project if writeable
        # 3) else, first writeable project
        project_id = experiment.projectId
        if self._can_write(project_id):
            return project_id
        workspace_preferences = models.WorkspacePreferences.objects.filter(
            username=self.username).first()
        if (workspace_preferences and self._can_write(
                workspace_preferences.most_recent_project_id)):
            return workspace_preferences.most_recent_project_id
        user_projects = self.request.airavata_client.getUserProjects(
            self.authz_token, self.gateway_id, self.username, -1, 0)
        for user_project in user_projects:
            if self._can_write(user_project.projectID):
                return user_project.projectID
        raise Exception(
            "Could not find writeable project for user {} in "
            "gateway {}".format(self.username, self.gateway_id))

    def _can_write(self, entity_id):
        return self.request.airavata_client.userHasAccess(
            self.authz_token, entity_id, ResourcePermissionType.WRITE)

    def _copy_cloned_experiment_input_uris(self, cloned_experiment):
        # update the experimentInputs of type URI, copying input files into the
        # tmp input files directory of the data store
        for experiment_input in cloned_experiment.experimentInputs:
            # skip inputs without values
            if not experiment_input.value:
                continue
            if experiment_input.type == DataType.URI:
                cloned_data_product = self._copy_experiment_input_uri(
                    experiment_input.value)
                if cloned_data_product is None:
                    log.warning("Setting cloned input {} to null".format(
                        experiment_input.name))
                    experiment_input.value = None
                else:
                    experiment_input.value = cloned_data_product.productUri
            elif experiment_input.type == DataType.URI_COLLECTION:
                data_product_uris = experiment_input.value.split(
                    ",") if experiment_input.value else []
                cloned_data_product_uris = []
                for data_product_uri in data_product_uris:
                    cloned_data_product = self._copy_experiment_input_uri(
                        data_product_uri)
                    if cloned_data_product is None:
                        log.warning(
                            "Omitting a cloned input value for {}".format(
                                experiment_input.name))
                    else:
                        cloned_data_product_uris.append(
                            cloned_data_product.productUri)
                experiment_input.value = ",".join(cloned_data_product_uris)

    def _copy_experiment_input_uri(
            self,
            data_product_uri):
        source_data_product = self.request.airavata_client.getDataProduct(
            self.authz_token, data_product_uri)
        if user_storage.exists(self.request, source_data_product):
            return user_storage.copy_input_file(
                self.request, source_data_product)
        else:
            log.warning("Could not find file for source data "
                        "product {}".format(source_data_product))
            return None

    def _update_workspace_preferences(self, project_id,
                                      group_resource_profile_id,
                                      compute_resource_id):
        prefs = helpers.WorkspacePreferencesHelper().get(self.request)
        prefs.most_recent_project_id = project_id
        prefs.most_recent_group_resource_profile_id = group_resource_profile_id
        prefs.most_recent_compute_resource_id = compute_resource_id
        prefs.save()


class ExperimentSearchViewSet(mixins.ListModelMixin, GenericAPIBackedViewSet):
    serializer_class = serializers.ExperimentSummarySerializer
    pagination_class = APIResultPagination
    pagination_viewname = 'django_airavata_api:experiment-search-list'

    def get_list(self):
        view = self

        filters = {}
        for filter_item in self.request.query_params.items():
            if filter_item[0] in ExperimentSearchFields._NAMES_TO_VALUES:
                # Lookup enum value for this ExperimentSearchFields
                search_field = ExperimentSearchFields._NAMES_TO_VALUES[
                    filter_item[0]]
                filters[search_field] = filter_item[1]

        class ExperimentSearchResultIterator(APIResultIterator):
            def get_results(self, limit=-1, offset=0):
                return view.request.airavata_client.searchExperiments(
                    view.authz_token, view.gateway_id, view.username, filters,
                    limit, offset)

        return ExperimentSearchResultIterator()

    def get_instance(self, lookup_value):
        raise NotImplementedError()


class FullExperimentViewSet(mixins.RetrieveModelMixin,
                            GenericAPIBackedViewSet):
    serializer_class = serializers.FullExperimentSerializer
    lookup_field = 'experiment_id'

    def get_instance(self, lookup_value):
        """Get FullExperiment instance with resolved references."""
        # TODO: move loading experiment and references to airavata_sdk?
        experimentModel = self.request.airavata_client.getExperiment(
            self.authz_token, lookup_value)
        outputDataProducts = [
            self.request.airavata_client.getDataProduct(self.authz_token,
                                                        output.value)
            for output in experimentModel.experimentOutputs
            if (output.value and
                output.value.startswith('airavata-dp') and
                output.type in (DataType.URI,
                                DataType.STDOUT,
                                DataType.STDERR))]
        outputDataProducts += [
            self.request.airavata_client.getDataProduct(self.authz_token, dp)
            for output in experimentModel.experimentOutputs
            if (output.value and
                output.type == DataType.URI_COLLECTION)
            for dp in output.value.split(',')
            if output.value.startswith('airavata-dp')]
        appInterfaceId = experimentModel.executionId
        try:
            applicationInterface = self.request.airavata_client \
                .getApplicationInterface(self.authz_token, appInterfaceId)
        except Exception as e:
            log.exception("Failed to load app interface")
            applicationInterface = None
        exp_output_views = output_views.get_output_views(
            self.request, experimentModel, applicationInterface)
        inputDataProducts = [
            self.request.airavata_client.getDataProduct(self.authz_token,
                                                        inp.value)
            for inp in experimentModel.experimentInputs
            if (inp.value and
                inp.value.startswith('airavata-dp') and
                inp.type in (DataType.URI,
                             DataType.STDOUT,
                             DataType.STDERR))]
        inputDataProducts += [
            self.request.airavata_client.getDataProduct(self.authz_token, dp)
            for inp in experimentModel.experimentInputs
            if (inp.value and
                inp.type == DataType.URI_COLLECTION)
            for dp in inp.value.split(',')
            if inp.value.startswith('airavata-dp')]
        try:
            if applicationInterface is not None:
                appModuleId = applicationInterface.applicationModules[0]
                applicationModule = self.request.airavata_client \
                    .getApplicationModule(self.authz_token, appModuleId)
            else:
                log.warning(
                    "Cannot log application model since app interface failed to load")
        except Exception as e:
            log.exception("Failed to load app interface/module")
            applicationModule = None

        compute_resource_id = None
        user_conf = experimentModel.userConfigurationData
        if user_conf and user_conf.computationalResourceScheduling:
            comp_res_sched = user_conf.computationalResourceScheduling
            compute_resource_id = comp_res_sched.resourceHostId
        try:
            compute_resource = self.request.airavata_client.getComputeResource(
                self.authz_token, compute_resource_id) \
                if compute_resource_id else None
        except Exception as e:
            log.exception("Failed to load compute resource for {}".format(
                compute_resource_id))
            compute_resource = None
        if self.request.airavata_client.userHasAccess(
                self.authz_token,
                experimentModel.projectId,
                ResourcePermissionType.READ):
            project = self.request.airavata_client.getProject(
                self.authz_token, experimentModel.projectId)
        else:
            # User may not have access to project, only experiment
            project = None
        job_details = self.request.airavata_client.getJobDetails(
            self.authz_token, lookup_value)
        full_experiment = serializers.FullExperiment(
            experimentModel,
            project=project,
            outputDataProducts=outputDataProducts,
            inputDataProducts=inputDataProducts,
            applicationModule=applicationModule,
            computeResource=compute_resource,
            jobDetails=job_details,
            outputViews=exp_output_views)
        return full_experiment


class ApplicationModuleViewSet(APIBackedViewSet):
    serializer_class = serializers.ApplicationModuleSerializer
    lookup_field = 'app_module_id'

    def get_list(self):
        return self.request.airavata_client.getAccessibleAppModules(
            self.authz_token, self.gateway_id)

    def get_instance(self, lookup_value):
        return self.request.airavata_client.getApplicationModule(
            self.authz_token, lookup_value)

    def perform_create(self, serializer):
        app_module = serializer.save()
        app_module_id = self.request.airavata_client.registerApplicationModule(
            self.authz_token, self.gateway_id, app_module)
        app_module.appModuleId = app_module_id

    def perform_update(self, serializer):
        app_module = serializer.save()
        self.request.airavata_client.updateApplicationModule(
            self.authz_token, app_module.appModuleId, app_module)

    def perform_destroy(self, instance):
        self.request.airavata_client.deleteApplicationModule(
            self.authz_token, instance.appModuleId)

    @detail_route()
    def application_interface(self, request, app_module_id):
        all_app_interfaces = request.airavata_client.getAllApplicationInterfaces(
            self.authz_token, self.gateway_id)
        app_interfaces = []
        for app_interface in all_app_interfaces:
            if not app_interface.applicationModules:
                continue
            if app_module_id in app_interface.applicationModules:
                app_interfaces.append(app_interface)
        if len(app_interfaces) == 1:
            serializer = serializers.ApplicationInterfaceDescriptionSerializer(
                app_interfaces[0], context={'request': request})
            return Response(serializer.data)
        elif len(app_interfaces) > 1:
            log.error(
                "More than one application interface found for module {}: {}"
                .format(app_module_id, app_interfaces))
            raise Exception(
                'More than one application interface found for module {}'
                .format(app_module_id)
            )
        else:
            raise Http404("No application interface found for module id {}"
                          .format(app_module_id))

    @detail_route()
    def application_deployments(self, request, app_module_id):
        all_deployments = self.request.airavata_client.getAllApplicationDeployments(
            self.authz_token, self.gateway_id)
        app_deployments = [
            dep for dep in all_deployments if dep.appModuleId == app_module_id]
        serializer = serializers.ApplicationDeploymentDescriptionSerializer(
            app_deployments, many=True, context={'request': request})
        return Response(serializer.data)

    @detail_route(methods=['post'])
    def favorite(self, request, app_module_id):
        helper = helpers.WorkspacePreferencesHelper()
        workspace_preferences = helper.get(request)
        try:
            application_preferences = (
                workspace_preferences.applicationpreferences_set.get(
                    application_id=app_module_id))
            application_preferences.favorite = True
            application_preferences.save()
        except ObjectDoesNotExist:
            workspace_preferences.applicationpreferences_set.create(
                username=request.user.username,
                application_id=app_module_id,
                favorite=True)

        return HttpResponse(status=204)

    @detail_route(methods=['post'])
    def unfavorite(self, request, app_module_id):
        helper = helpers.WorkspacePreferencesHelper()
        workspace_preferences = helper.get(request)
        try:
            application_preferences = (
                workspace_preferences.applicationpreferences_set.get(
                    application_id=app_module_id))
            application_preferences.favorite = False
            application_preferences.save()
        except ObjectDoesNotExist:
            workspace_preferences.applicationpreferences_set.create(
                username=request.user.username,
                application_id=app_module_id,
                favorite=False)

        return HttpResponse(status=204)

    @list_route()
    def list_all(self, request, format=None):
        all_modules = self.request.airavata_client.getAllAppModules(
            self.authz_token, self.gateway_id)
        serializer = self.serializer_class(
            all_modules, many=True, context={'request': request})
        return Response(serializer.data)


class ApplicationInterfaceViewSet(APIBackedViewSet):
    serializer_class = serializers.ApplicationInterfaceDescriptionSerializer
    lookup_field = 'app_interface_id'

    def get_list(self):
        return self.request.airavata_client.getAllApplicationInterfaces(
            self.authz_token, self.gateway_id)

    def get_instance(self, lookup_value):
        return self.request.airavata_client.getApplicationInterface(
            self.authz_token, lookup_value)

    def perform_create(self, serializer):
        application_interface = serializer.save()
        self._update_input_metadata(application_interface)
        log.debug("application_interface: {}".format(application_interface))
        app_interface_id = self.request.airavata_client.registerApplicationInterface(
            self.authz_token, self.gateway_id, application_interface)
        application_interface.applicationInterfaceId = app_interface_id

    def perform_update(self, serializer):
        application_interface = serializer.save()
        self._update_input_metadata(application_interface)
        self.request.airavata_client.updateApplicationInterface(
            self.authz_token,
            application_interface.applicationInterfaceId,
            application_interface)

    def perform_destroy(self, instance):
        self.request.airavata_client.deleteApplicationInterface(
            self.authz_token, instance.applicationInterfaceId)

    def _update_input_metadata(self, app_interface):
        for app_input in app_interface.applicationInputs:
            if app_input.metaData:
                metadata = json.loads(app_input.metaData)
                # Automatically add {showOptions: {isRequired: true/false}} to
                # toggle isRequired on hidden/shown inputs
                if ("editor" in metadata and
                    "dependencies" in metadata["editor"] and
                        "show" in metadata["editor"]["dependencies"]):
                    if "showOptions" not in metadata["editor"]["dependencies"]:
                        metadata["editor"]["dependencies"]["showOptions"] = {}
                    o = metadata["editor"]["dependencies"]["showOptions"]
                    o["isRequired"] = app_input.isRequired
                    app_input.metaData = json.dumps(metadata)

    @detail_route()
    def compute_resources(self, request, app_interface_id):
        compute_resources = request.airavata_client.getAvailableAppInterfaceComputeResources(
            self.authz_token, app_interface_id)
        return Response(compute_resources)


class ApplicationDeploymentViewSet(APIBackedViewSet):
    serializer_class = serializers.ApplicationDeploymentDescriptionSerializer
    lookup_field = 'app_deployment_id'

    def get_list(self):
        app_module_id = self.request.query_params.get('appModuleId', None)
        group_resource_profile_id = self.request.query_params.get(
            'groupResourceProfileId', None)
        if (app_module_id and not group_resource_profile_id)\
                or (not app_module_id and group_resource_profile_id):
            raise ParseError("Query params appModuleId and "
                             "groupResourceProfileId are required together.")
        if app_module_id and group_resource_profile_id:
            return self.request.airavata_client.getApplicationDeploymentsForAppModuleAndGroupResourceProfile(
                self.authz_token, app_module_id, group_resource_profile_id)
        else:
            return self.request.airavata_client.getAccessibleApplicationDeployments(
                self.authz_token, self.gateway_id, ResourcePermissionType.READ)

    def get_instance(self, lookup_value):
        return self.request.airavata_client.getApplicationDeployment(
            self.authz_token, lookup_value)

    def perform_create(self, serializer):
        application_deployment = serializer.save()
        app_deployment_id = self.request.airavata_client.registerApplicationDeployment(
            self.authz_token, self.gateway_id, application_deployment)
        application_deployment.appDeploymentId = app_deployment_id

    def perform_update(self, serializer):
        application_deployment = serializer.save()
        self.request.airavata_client.updateApplicationDeployment(
            self.authz_token, application_deployment.appDeploymentId, application_deployment)

    def perform_destroy(self, instance):
        self.request.airavata_client.deleteApplicationDeployment(
            self.authz_token, instance.appDeploymentId)

    @detail_route()
    def queues(self, request, app_deployment_id):
        """Return queues for this deployment with defaults overridden by deployment defaults if they exist"""
        app_deployment = self.request.airavata_client.getApplicationDeployment(
            self.authz_token, app_deployment_id)
        compute_resource = request.airavata_client.getComputeResource(
            request.authz_token, app_deployment.computeHostId)
        # Override defaults with app deployment defaults
        batch_queues = []
        for batch_queue in compute_resource.batchQueues:
            if app_deployment.defaultQueueName:
                batch_queue.isDefaultQueue = (
                    app_deployment.defaultQueueName == batch_queue.queueName)
            if app_deployment.defaultNodeCount:
                batch_queue.defaultNodeCount = app_deployment.defaultNodeCount
            if app_deployment.defaultCPUCount:
                batch_queue.defaultCPUCount = app_deployment.defaultCPUCount
            if app_deployment.defaultWalltime:
                batch_queue.defaultWalltime = app_deployment.defaultWalltime
            batch_queues.append(batch_queue)
        serializer = serializers.BatchQueueSerializer(
            batch_queues, many=True, context={'request': request})
        return Response(serializer.data)


class ComputeResourceViewSet(mixins.RetrieveModelMixin,
                             GenericAPIBackedViewSet):
    serializer_class = serializers.ComputeResourceDescriptionSerializer
    lookup_field = 'compute_resource_id'

    def get_instance(self, lookup_value, format=None):
        return self.request.airavata_client.getComputeResource(
            self.authz_token, lookup_value)

    @list_route()
    def all_names(self, request, format=None):
        """Return a map of compute resource names keyed by resource id."""
        return Response(
            request.airavata_client.getAllComputeResourceNames(
                request.authz_token))

    @list_route()
    def all_names_list(self, request, format=None):
        """Return a list of compute resource names keyed by resource id."""
        all_names = request.airavata_client.getAllComputeResourceNames(
            request.authz_token)
        return Response([
            {
                'host_id': host_id,
                'host': host,
                'url': request.build_absolute_uri(
                    reverse('django_airavata_api:compute-resource-detail',
                            args=[host_id]))
            } for host_id, host in all_names.items()
        ])

    @detail_route()
    def queues(self, request, compute_resource_id, format=None):
        details = request.airavata_client.getComputeResource(
            request.authz_token, compute_resource_id)
        serializer = self.serializer_class(instance=details,
                                           context={'request': request})
        data = serializer.data
        return Response([queue["queueName"] for queue in data["batchQueues"]])


class LocalJobSubmissionView(APIView):
    renderer_classes = (JSONRenderer,)

    def get(self, request, format=None):
        job_submission_id = request.query_params["id"]
        local_job_submission = request.airavata_client.getLocalJobSubmission(
            request.authz_token, job_submission_id)
        return Response(
            thrift_utils.create_serializer(
                LOCALSubmission,
                instance=local_job_submission).data)


class CloudJobSubmissionView(APIView):
    renderer_classes = (JSONRenderer,)

    def get(self, request, format=None):
        job_submission_id = request.query_params["id"]
        job_submission = request.airavata_client.getCloudJobSubmission(
            request.authz_token, job_submission_id)
        return Response(
            thrift_utils.create_serializer(
                CloudJobSubmission,
                instance=job_submission).data)


class GlobusJobSubmissionView(APIView):
    renderer_classes = (JSONRenderer,)

    def get(self, request, format=None):
        job_submission_id = request.query_params["id"]
        job_submission = request.airavata_client.getClo(
            request.authz_token, job_submission_id)
        return Response(
            thrift_utils.create_serializer(
                GlobusJobSubmission,
                instance=job_submission).data)


class SshJobSubmissionView(APIView):
    renderer_classes = (JSONRenderer,)

    def get(self, request, format=None):
        job_submission_id = request.query_params["id"]
        job_submission = request.airavata_client.getSSHJobSubmission(
            request.authz_token, job_submission_id)
        return Response(
            thrift_utils.create_serializer(
                SSHJobSubmission,
                instance=job_submission).data)


class UnicoreJobSubmissionView(APIView):
    renderer_classes = (JSONRenderer,)

    def get(self, request, format=None):
        job_submission_id = request.query_params["id"]
        job_submission = request.airavata_client.getUnicoreJobSubmission(
            request.authz_token, job_submission_id)
        return Response(
            thrift_utils.create_serializer(
                UnicoreJobSubmission,
                instance=job_submission).data)


class GridFtpDataMovementView(APIView):
    renderer_classes = (JSONRenderer,)

    def get(self, request, format=None):
        data_movement_id = request.query_params["id"]
        data_movement = request.airavata_client.getGridFTPDataMovement(
            request.authz_token, data_movement_id)
        return Response(
            thrift_utils.create_serializer(
                GridFTPDataMovement,
                instance=data_movement).data)


class ScpDataMovementView(APIView):
    renderer_classes = (JSONRenderer,)

    def get(self, request, format=None):
        data_movement_id = request.query_params["id"]
        data_movement = request.airavata_client.getSCPDataMovement(
            request.authz_token, data_movement_id)
        return Response(
            thrift_utils.create_serializer(
                SCPDataMovement,
                instance=data_movement).data)


class UnicoreDataMovementView(APIView):
    renderer_classes = (JSONRenderer,)

    def get(self, request, format=None):
        data_movement_id = request.query_params["id"]
        data_movement = request.airavata_client.getUnicoreDataMovement(
            request.authz_token, data_movement_id)
        return Response(
            thrift_utils.create_serializer(
                UnicoreDataMovement,
                instance=data_movement).data)


class LocalDataMovementView(APIView):
    renderer_classes = (JSONRenderer,)

    def get(self, request, format=None):
        data_movement_id = request.query_params["id"]
        data_movement = request.airavata_client.getLocalDataMovement(
            request.authz_token, data_movement_id)
        return Response(
            thrift_utils.create_serializer(
                LOCALDataMovement,
                instance=data_movement).data)


class DataProductView(APIView):

    serializer_class = serializers.DataProductSerializer

    def get(self, request, format=None):
        data_product_uri = request.query_params['product-uri']
        data_product = request.airavata_client.getDataProduct(
            request.authz_token, data_product_uri)
        serializer = self.serializer_class(
            data_product, context={'request': request})
        return Response(serializer.data)


@api_view(http_method_names=['POST'])
def upload_input_file(request):
    try:
        input_file = request.FILES['file']
        data_product = user_storage.save_input_file(
            request, input_file, content_type=input_file.content_type)
        serializer = serializers.DataProductSerializer(
            data_product, context={'request': request})
        return JsonResponse({'uploaded': True,
                             'data-product': serializer.data})
    except Exception as e:
        log.error("Failed to upload file", exc_info=True)
        resp = JsonResponse({'uploaded': False, 'error': str(e)})
        resp.status_code = 500
        return resp


@api_view(http_method_names=['POST'])
def tus_upload_finish(request):
    uploadURL = request.POST['uploadURL']

    def move_input_file(file_path, file_name, file_type):
        return user_storage.move_input_file_from_filepath(
            request, file_path, name=file_name, content_type=file_type)
    try:
        data_product = tus.move_tus_upload(uploadURL, move_input_file)
        serializer = serializers.DataProductSerializer(
            data_product, context={'request': request})
        return JsonResponse({'uploaded': True,
                             'data-product': serializer.data})
    except Exception as e:
        return exceptions.generic_json_exception_response(e, status=400)


@api_view()
def download_file(request):
    # TODO check that user has access to this file using sharing API
    data_product_uri = request.GET.get('data-product-uri', '')
    force_download = 'download' in request.GET
    data_product = None
    try:
        data_product = request.airavata_client.getDataProduct(
            request.authz_token, data_product_uri)
        mime_type = "application/octet-stream"  # default mime-type
        if (data_product.productMetadata and
                'mime-type' in data_product.productMetadata):
            mime_type = data_product.productMetadata['mime-type']
        # 'mime-type' url parameter overrides
        mime_type = request.GET.get('mime-type', mime_type)
    except Exception as e:
        log.warning("Failed to load DataProduct for {}"
                    .format(data_product_uri), exc_info=True)
        raise Http404("data product does not exist") from e
    try:
        data_file = user_storage.open_file(request, data_product)
        response = FileResponse(data_file, content_type=mime_type)
        file_name = os.path.basename(data_file.name)
        if mime_type == 'application/octet-stream' or force_download:
            response['Content-Disposition'] = ('attachment; filename="{}"'
                                               .format(file_name))
        else:
            response['Content-Disposition'] = f'inline; filename="{file_name}"'
        return response
    except ObjectDoesNotExist as e:
        raise Http404(str(e)) from e


@api_view(http_method_names=['DELETE'])
def delete_file(request):
    # TODO check that user has write access to this file using sharing API
    data_product_uri = request.GET.get('data-product-uri', '')
    data_product = None
    try:
        data_product = request.airavata_client.getDataProduct(
            request.authz_token, data_product_uri)
    except Exception as e:
        log.warning("Failed to load DataProduct for {}"
                    .format(data_product_uri), exc_info=True)
        raise Http404("data product does not exist") from e
    try:
        if (data_product.gatewayId != settings.GATEWAY_ID or
                data_product.ownerName != request.user.username):
            raise PermissionDenied()
        user_storage.delete(request, data_product)
        return HttpResponse(status=204)
    except ObjectDoesNotExist as e:
        raise Http404(str(e)) from e


class UserProfileViewSet(mixins.RetrieveModelMixin,
                         mixins.ListModelMixin,
                         GenericAPIBackedViewSet):
    serializer_class = serializers.UserProfileSerializer

    def get_list(self):
        user_profile_client = self.request.profile_service['user_profile']
        return user_profile_client.getAllUserProfilesInGateway(
            self.authz_token, self.gateway_id, 0, -1)

    def get_instance(self, lookup_value):
        user_profile_client = self.request.profile_service['user_profile']
        return user_profile_client.getUserProfileById(
            self.authz_token, self.request.user.username, self.gateway_id)


class GroupResourceProfileViewSet(APIBackedViewSet):
    serializer_class = serializers.GroupResourceProfileSerializer
    lookup_field = 'group_resource_profile_id'

    def get_list(self):
        return self.request.airavata_client.getGroupResourceList(
            self.authz_token, self.gateway_id)

    def get_instance(self, lookup_value):
        return self.request.airavata_client.getGroupResourceProfile(
            self.authz_token, lookup_value)

    def perform_create(self, serializer):
        group_resource_profile = serializer.save()
        group_resource_profile.gatewayId = self.gateway_id
        group_resource_profile_id = self.request.airavata_client.createGroupResourceProfile(
            authzToken=self.authz_token, groupResourceProfile=group_resource_profile)
        group_resource_profile.groupResourceProfileId = group_resource_profile_id
        # Update the creationTime field on the group resource profile
        new_group_resource_profile = self.request.airavata_client.getGroupResourceProfile(
            self.authz_token, group_resource_profile_id)
        group_resource_profile.creationTime = new_group_resource_profile.creationTime

    def perform_update(self, serializer):
        grp = serializer.save()
        for removed_compute_resource_preference \
                in grp._removed_compute_resource_preferences:
            self.request.airavata_client.removeGroupComputePrefs(
                self.authz_token,
                removed_compute_resource_preference.computeResourceId,
                removed_compute_resource_preference.groupResourceProfileId)
        for removed_compute_resource_policy \
                in grp._removed_compute_resource_policies:
            self.request.airavata_client.removeGroupComputeResourcePolicy(
                self.authz_token,
                removed_compute_resource_policy.resourcePolicyId)
        for removed_batch_queue_resource_policy \
                in grp._removed_batch_queue_resource_policies:
            self.request.airavata_client.removeGroupBatchQueueResourcePolicy(
                self.authz_token,
                removed_batch_queue_resource_policy.resourcePolicyId)
        self.request.airavata_client.updateGroupResourceProfile(
            self.authz_token, grp)

    def perform_destroy(self, instance):
        self.request.airavata_client.removeGroupResourceProfile(
            self.authz_token, instance.groupResourceProfileId)


class SharedEntityViewSet(mixins.RetrieveModelMixin,
                          mixins.UpdateModelMixin,
                          GenericAPIBackedViewSet):
    serializer_class = serializers.SharedEntitySerializer
    lookup_field = 'entity_id'

    def get_instance(self, lookup_value):
        users = {}
        # Only load *directly* granted permissions since these are the only
        # ones that can be edited
        # Load accessible users in order of permission precedence: users that
        # have WRITE permission should also have READ
        users.update(self._load_directly_accessible_users(
            lookup_value, ResourcePermissionType.READ))
        users.update(self._load_directly_accessible_users(
            lookup_value, ResourcePermissionType.WRITE))
        users.update(self._load_directly_accessible_users(
            lookup_value, ResourcePermissionType.MANAGE_SHARING))
        owner_ids = self._load_directly_accessible_users(
            lookup_value, ResourcePermissionType.OWNER)
        # Assume that there is one and only one DIRECT owner (there may be one
        # or more INDIRECT cascading owners, which would the owners of the
        # ancestor entities, but getAllDirectlyAccessibleUsers does not return
        # indirectly cascading owners)
        owner_id = list(owner_ids.keys())[0]
        # Remove owner from the users list
        del users[owner_id]
        user_list = []
        for user_id in users:
            user_list.append({'user': self._load_user_profile(user_id),
                              'permissionType': users[user_id]})
        groups = {}
        groups.update(self._load_directly_accessible_groups(
            lookup_value, ResourcePermissionType.READ))
        groups.update(self._load_directly_accessible_groups(
            lookup_value, ResourcePermissionType.WRITE))
        groups.update(self._load_directly_accessible_groups(
            lookup_value, ResourcePermissionType.MANAGE_SHARING))
        group_list = []
        for group_id in groups:
            group_list.append({'group': self._load_group(group_id),
                               'permissionType': groups[group_id]})
        return {'entityId': lookup_value,
                'userPermissions': user_list,
                'groupPermissions': group_list,
                'owner': self._load_user_profile(owner_id)}

    def _load_accessible_users(self, entity_id, permission_type):
        users = self.request.airavata_client.getAllAccessibleUsers(
            self.authz_token, entity_id, permission_type)
        return {user_id: permission_type for user_id in users}

    def _load_directly_accessible_users(self, entity_id, permission_type):
        users = self.request.airavata_client.getAllDirectlyAccessibleUsers(
            self.authz_token, entity_id, permission_type)
        return {user_id: permission_type for user_id in users}

    def _load_user_profile(self, user_id):
        user_profile_client = self.request.profile_service['user_profile']
        username = user_id[0:user_id.rindex('@')]
        return user_profile_client.getUserProfileById(self.authz_token,
                                                      username,
                                                      settings.GATEWAY_ID)

    def _load_accessible_groups(self, entity_id, permission_type):
        groups = self.request.airavata_client.getAllAccessibleGroups(
            self.authz_token, entity_id, permission_type)
        return {group_id: permission_type for group_id in groups}

    def _load_directly_accessible_groups(self, entity_id, permission_type):
        groups = self.request.airavata_client.getAllDirectlyAccessibleGroups(
            self.authz_token, entity_id, permission_type)
        return {group_id: permission_type for group_id in groups}

    def _load_group(self, group_id):
        group_manager_client = self.request.profile_service['group_manager']
        return group_manager_client.getGroup(self.authz_token, group_id)

    def perform_update(self, serializer):
        shared_entity = serializer.save()
        entity_id = shared_entity['entityId']
        if len(shared_entity['_user_grant_read_permission']) > 0:
            self._share_with_users(
                entity_id, ResourcePermissionType.READ,
                shared_entity['_user_grant_read_permission'])
        if len(shared_entity['_user_grant_write_permission']) > 0:
            self._share_with_users(
                entity_id, ResourcePermissionType.WRITE,
                shared_entity['_user_grant_write_permission'])
        if len(shared_entity['_user_grant_manage_sharing_permission']) > 0:
            self._share_with_users(
                entity_id, ResourcePermissionType.MANAGE_SHARING,
                shared_entity['_user_grant_manage_sharing_permission'])
        if len(shared_entity['_user_revoke_read_permission']) > 0:
            self._revoke_from_users(
                entity_id, ResourcePermissionType.READ,
                shared_entity['_user_revoke_read_permission'])
        if len(shared_entity['_user_revoke_write_permission']) > 0:
            self._revoke_from_users(
                entity_id, ResourcePermissionType.WRITE,
                shared_entity['_user_revoke_write_permission'])
        if len(shared_entity['_user_revoke_manage_sharing_permission']) > 0:
            self._revoke_from_users(
                entity_id, ResourcePermissionType.MANAGE_SHARING,
                shared_entity['_user_revoke_manage_sharing_permission'])
        if len(shared_entity['_group_grant_read_permission']) > 0:
            self._share_with_groups(
                entity_id, ResourcePermissionType.READ,
                shared_entity['_group_grant_read_permission'])
        if len(shared_entity['_group_grant_write_permission']) > 0:
            self._share_with_groups(
                entity_id, ResourcePermissionType.WRITE,
                shared_entity['_group_grant_write_permission'])
        if len(shared_entity['_group_grant_manage_sharing_permission']) > 0:
            self._share_with_groups(
                entity_id, ResourcePermissionType.MANAGE_SHARING,
                shared_entity['_group_grant_manage_sharing_permission'])
        if len(shared_entity['_group_revoke_read_permission']) > 0:
            self._revoke_from_groups(
                entity_id, ResourcePermissionType.READ,
                shared_entity['_group_revoke_read_permission'])
        if len(shared_entity['_group_revoke_write_permission']) > 0:
            self._revoke_from_groups(
                entity_id, ResourcePermissionType.WRITE,
                shared_entity['_group_revoke_write_permission'])
        if len(shared_entity['_group_revoke_manage_sharing_permission']) > 0:
            self._revoke_from_groups(
                entity_id, ResourcePermissionType.MANAGE_SHARING,
                shared_entity['_group_revoke_manage_sharing_permission'])

    def _share_with_users(self, entity_id, permission_type, user_ids):
        self.request.airavata_client.shareResourceWithUsers(
            self.authz_token, entity_id,
            {user_id: permission_type for user_id in user_ids})

    def _revoke_from_users(self, entity_id, permission_type, user_ids):
        self.request.airavata_client.revokeSharingOfResourceFromUsers(
            self.authz_token, entity_id,
            {user_id: permission_type for user_id in user_ids})

    def _share_with_groups(self, entity_id, permission_type, group_ids):
        self.request.airavata_client.shareResourceWithGroups(
            self.authz_token, entity_id,
            {group_id: permission_type for group_id in group_ids})

    def _revoke_from_groups(self, entity_id, permission_type, group_ids):
        self.request.airavata_client.revokeSharingOfResourceFromGroups(
            self.authz_token, entity_id,
            {group_id: permission_type for group_id in group_ids})

    @detail_route(methods=['put'])
    def merge(self, request, entity_id=None):
        # Validate updated sharing settings
        updated = self.get_serializer(data=request.data)
        updated.is_valid(raise_exception=True)
        # Get the existing sharing settings and merge in the updated settings
        existing_instance = self.get_object()
        existing = self.get_serializer(instance=existing_instance)
        merged_data = existing.data
        merged_data['userPermissions'] = existing.data['userPermissions'] + \
            updated.initial_data['userPermissions']
        merged_data['groupPermissions'] = existing.data['groupPermissions'] + \
            updated.initial_data['groupPermissions']
        # Create a merged_serializer from the existing sharing settings and the
        # merged settings. This will calculate all permissions that need to be
        # granted and revoked to go from the exisitng settings to the merged
        # settings.
        merged_serializer = self.get_serializer(
            existing_instance, data=merged_data)
        merged_serializer.is_valid(raise_exception=True)
        self.perform_update(merged_serializer)
        return Response(merged_serializer.data)

    @detail_route(methods=['get'])
    def all(self, request, entity_id=None):
        """Load direct plus indirectly (inherited) shared permissions."""
        users = {}
        # Load accessible users in order of permission precedence: users that
        # have WRITE permission should also have READ
        users.update(self._load_accessible_users(
            entity_id, ResourcePermissionType.READ))
        users.update(self._load_accessible_users(
            entity_id, ResourcePermissionType.WRITE))
        users.update(self._load_accessible_users(
            entity_id, ResourcePermissionType.MANAGE_SHARING))
        owner_ids = self._load_accessible_users(
            entity_id, ResourcePermissionType.OWNER)
        # Assume that there is one and only one DIRECT owner (there may be one
        # or more INDIRECT cascading owners, which would the owners of the
        # ancestor entities, but getAllAccessibleUsers does not return
        # indirectly cascading owners)
        owner_id = list(owner_ids.keys())[0]
        # Remove owner from the users list
        del users[owner_id]
        user_list = []
        for user_id in users:
            user_list.append({'user': self._load_user_profile(user_id),
                              'permissionType': users[user_id]})
        groups = {}
        groups.update(self._load_accessible_groups(
            entity_id, ResourcePermissionType.READ))
        groups.update(self._load_accessible_groups(
            entity_id, ResourcePermissionType.WRITE))
        groups.update(self._load_accessible_groups(
            entity_id, ResourcePermissionType.MANAGE_SHARING))
        group_list = []
        for group_id in groups:
            group_list.append({'group': self._load_group(group_id),
                               'permissionType': groups[group_id]})
        shared_entity = {'entityId': entity_id,
                         'userPermissions': user_list,
                         'groupPermissions': group_list,
                         'owner': self._load_user_profile(owner_id)}
        serializer = self.serializer_class(
            shared_entity, context={'request': request})
        return Response(serializer.data)


class CredentialSummaryViewSet(APIBackedViewSet):
    serializer_class = serializers.CredentialSummarySerializer

    def get_list(self):
        ssh_creds = self.request.airavata_client.getAllCredentialSummaries(
            self.authz_token, SummaryType.SSH)
        pwd_creds = self.request.airavata_client.getAllCredentialSummaries(
            self.authz_token, SummaryType.PASSWD)
        return ssh_creds + pwd_creds

    def get_instance(self, lookup_value):
        return self.request.airavata_client.getCredentialSummary(
            self.authz_token, lookup_value)

    @action(detail=False)
    def ssh(self, request):
        summaries = self.request.airavata_client.getAllCredentialSummaries(
            self.authz_token, SummaryType.SSH
        )
        serializer = self.get_serializer(summaries, many=True)
        return Response(serializer.data)

    @action(detail=False)
    def password(self, request):
        summaries = self.request.airavata_client.getAllCredentialSummaries(
            self.authz_token, SummaryType.PASSWD
        )
        serializer = self.get_serializer(summaries, many=True)
        return Response(serializer.data)

    @action(methods=['post'], detail=False)
    def create_ssh(self, request):
        if 'description' not in request.data:
            raise ParseError("'description' is required in request")
        description = request.data.get('description')
        token_id = self.request.airavata_client.generateAndRegisterSSHKeys(
            request.authz_token, description)
        credential_summary = self.request.airavata_client.getCredentialSummary(
            request.authz_token, token_id)
        serializer = self.get_serializer(credential_summary)
        return Response(serializer.data)

    @action(methods=['post'], detail=False)
    def create_password(self, request):
        if ('username' not in request.data or
            'password' not in request.data or
                'description' not in request.data):
            raise ParseError("'username', 'password' and 'description' "
                             "are all required in request")
        username = request.data.get('username')
        password = request.data.get('password')
        description = request.data.get('description')
        token_id = self.request.airavata_client.registerPwdCredential(
            request.authz_token, username, password, description)
        credential_summary = self.request.airavata_client.getCredentialSummary(
            request.authz_token, token_id)
        serializer = self.get_serializer(credential_summary)
        return Response(serializer.data)

    def perform_destroy(self, instance):
        if instance.type == SummaryType.SSH:
            self.request.airavata_client.deleteSSHPubKey(
                self.authz_token, instance.token)
        elif instance.type == SummaryType.PASSWD:
            self.request.airavata_client.deletePWDCredential(
                self.authz_token, instance.token)


class GatewayResourceProfileViewSet(APIBackedViewSet):
    serializer_class = serializers.GatewayResourceProfileSerializer
    lookup_field = 'gateway_id'

    def get_list(self):
        return self.request.airavata_client.getAllGatewayResourceProfiles(
            self.authz_token)

    def get_instance(self, lookup_value):
        return self.request.airavata_client.getGatewayResourceProfile(
            self.authz_token, lookup_value)

    def perform_create(self, serializer):
        gateway_resource_profile = serializer.save()
        self.request.airavata_client.registerGatewayResourceProfile(
            self.authz_token, gateway_resource_profile)

    def perform_update(self, serializer):
        gateway_resource_profile = serializer.save()
        self.request.airavata_client.updateGatewayResourceProfile(
            self.authz_token,
            gateway_resource_profile.gatewayID,
            gateway_resource_profile)

    def perform_destroy(self, instance):
        self.request.airavata_client.deleteGatewayResourceProfile(
            self.authz_token, instance.gatewayID)


class GetCurrentGatewayResourceProfile(APIView):

    def get(self, request, format=None):
        gateway_resource_profile = \
            request.airavata_client.getGatewayResourceProfile(
                request.authz_token, settings.GATEWAY_ID)
        serializer = serializers.GatewayResourceProfileSerializer(
            gateway_resource_profile, context={'request': request})
        return Response(serializer.data)


class StorageResourceViewSet(mixins.RetrieveModelMixin,
                             GenericAPIBackedViewSet):
    serializer_class = serializers.StorageResourceSerializer
    lookup_field = 'storage_resource_id'

    def get_instance(self, lookup_value, format=None):
        return self.request.airavata_client.getStorageResource(
            self.authz_token, lookup_value)

    @list_route()
    def all_names(self, request, format=None):
        """Return a map of compute resource names keyed by resource id."""
        return Response(
            request.airavata_client.getAllStorageResourceNames(
                request.authz_token))


class StoragePreferenceViewSet(APIBackedViewSet):
    serializer_class = serializers.StoragePreferenceSerializer
    lookup_field = 'storage_resource_id'

    def get_list(self):
        return self.request.airavata_client.getAllGatewayStoragePreferences(
            self.authz_token, settings.GATEWAY_ID)

    def get_instance(self, lookup_value):
        return self.request.airavata_client.getGatewayStoragePreference(
            self.authz_token, settings.GATEWAY_ID, lookup_value)

    def perform_create(self, serializer):
        storage_preference = serializer.save()
        self.request.airavata_client.addGatewayStoragePreference(
            self.authz_token,
            settings.GATEWAY_ID,
            storage_preference.storageResourceId,
            storage_preference)

    def perform_update(self, serializer):
        storage_preference = serializer.save()
        self.request.airavata_client.updateGatewayStoragePreference(
            self.authz_token,
            settings.GATEWAY_ID,
            storage_preference.storageResourceId,
            storage_preference)

    def perform_destroy(self, instance):
        self.request.airavata_client.deleteGatewayStoragePreference(
            self.authz_token, settings.GATEWAY_ID, instance.storageResourceId)


class ParserViewSet(mixins.CreateModelMixin,
                    mixins.RetrieveModelMixin,
                    mixins.UpdateModelMixin,
                    mixins.ListModelMixin,
                    GenericAPIBackedViewSet):
    serializer_class = serializers.ParserSerializer
    lookup_field = 'parser_id'

    def get_list(self):
        return self.request.airavata_client.listAllParsers(
            self.authz_token, settings.GATEWAY_ID)

    def get_instance(self, lookup_value):
        return self.request.airavata_client.getParser(
            self.authz_token, lookup_value, settings.GATEWAY_ID)

    def perform_create(self, serializer):
        parser = serializer.save()
        self.request.airavata_client.saveParser(self.authz_token, parser)

    def perform_update(self, serializer):
        parser = serializer.save()
        self.request.airavata_client.saveParser(self.authz_token, parser)


class UserStoragePathView(APIView):
    serializer_class = serializers.UserStoragePathSerializer

    def get(self, request, path="/", format=None):
        return self._create_response(request, path)

    def post(self, request, path="/", format=None):
        if not user_storage.dir_exists(request, path):
            user_storage.create_user_dir(request, path)

        data_product = None
        # Handle direct upload
        if 'file' in request.FILES:
            user_file = request.FILES['file']
            data_product = user_storage.save(
                request, path, user_file, content_type=user_file.content_type)
        # Handle a tus upload
        elif 'uploadURL' in request.POST:
            uploadURL = request.POST['uploadURL']

            def move_file(file_path, file_name, file_type):
                return user_storage.move_from_filepath(
                    request, file_path, path, name=file_name,
                    content_type=file_type)
            data_product = tus.move_tus_upload(uploadURL, move_file)
        return self._create_response(request, path, uploaded=data_product)

    # Accept wither to replace file or to replace file content text.
    def put(self, request, path="/", format=None):
        # Replace the file if the request has a file upload.
        if 'file' in request.FILES:
            self.delete(request=request, path=path, format=format)
            dir_path, file_name = os.path.split(path)
            self.post(request=request, path=dir_path, format=format, file_name=file_name)
        # Replace only the file content if the request body has the `fileContentText`
        elif request.data and "fileContentText" in request.data:
            user_storage.update_file_content(
                request=request,
                path=path,
                fileContentText=request.data["fileContentText"])
        else:
            return Response(status=HTTP_400_BAD_REQUEST)

        return self._create_response(request=request, path=path)


    def delete(self, request, path="/", format=None):
        if user_storage.dir_exists(request, path):
            user_storage.delete_dir(request, path)
        else:
            user_storage.delete_user_file(request, path)

        return Response(status=204)

    def _create_response(self, request, path, uploaded=None):
        if user_storage.dir_exists(request, path):
            directories, files = user_storage.listdir(request, path)
            data = {
                'isDir': True,
                'directories': directories,
                'files': files
            }
            if uploaded is not None:
                data['uploaded'] = uploaded
            data['parts'] = self._split_path(path)
            serializer = self.serializer_class(data, context={'request': request})
            return Response(serializer.data)
        else:
            file = user_storage.get_file(request, path)
            data = {
                'isDir': False,
                'directories': [],
                'files': [file]
            }
            if uploaded is not None:
                data['uploaded'] = uploaded
            data['parts'] = self._split_path(path)
            serializer = self.serializer_class(data, context={'request': request})
            return Response(serializer.data)

    def _split_path(self, path):
        head, tail = os.path.split(path)
        if head != "":
            return self._split_path(head) + [tail]
        elif tail != "":
            return [tail]
        else:
            return []


class WorkspacePreferencesView(APIView):
    serializer_class = serializers.WorkspacePreferencesSerializer

    def get(self, request, format=None):
        helper = helpers.WorkspacePreferencesHelper()
        workspace_preferences = helper.get(request)
        serializer = self.serializer_class(
            workspace_preferences, context={'request': request})
        return Response(serializer.data)


class ManageNotificationViewSet(APIBackedViewSet):
    serializer_class = serializers.NotificationSerializer
    lookup_field = 'notification_id'

    def get_instance(self, lookup_value):
        return self.request.airavata_client.getNotification(
            self.authz_token, settings.GATEWAY_ID, lookup_value)

    def get_list(self):
        return self.request.airavata_client.getAllNotifications(
            self.authz_token, self.gateway_id)

    def perform_destroy(self, instance):
        self.request.airavata_client.deleteNotification(
            self.authz_token, settings.GATEWAY_ID, instance.notificationId)

    def perform_create(self, serializer):
        notification = serializer.save(gatewayId=self.gateway_id)
        notificationId = self.request.airavata_client.createNotification(
            self.authz_token, notification)
        notification.notificationId = notificationId

    def perform_update(self, serializer):
        notification = serializer.save()
        self.request.airavata_client.updateNotification(
            self.authz_token, notification)


class AckNotificationViewSet(APIView):

    def get(self, request, format=None):
        if 'id' in request.GET:
            notification_id = request.GET['id']
            try:
                notification = models.User_Notifications.objects.get(
                    notification_id=notification_id,
                    username=request.user.username)
                notification.is_read = True
                notification.save()
            except ObjectDoesNotExist:
                models.User_Notifications.objects.create(
                    username=request.user.username,
                    notification_id=notification.notificationId)
        return HttpResponse(status=204)


class IAMUserViewSet(mixins.RetrieveModelMixin,
                     mixins.UpdateModelMixin,
                     mixins.ListModelMixin,
                     mixins.DestroyModelMixin,
                     GenericAPIBackedViewSet):
    serializer_class = serializers.IAMUserProfile
    pagination_class = APIResultPagination
    permission_classes = (IsInAdminsGroupPermission,)
    lookup_field = 'user_id'

    def get_list(self):
        search = self.request.GET.get('search', None)

        convert_user_profile = self._convert_user_profile

        class IAMUsersResultIterator(APIResultIterator):
            def get_results(self, limit=-1, offset=0):
                return map(convert_user_profile,
                           iam_admin_client.get_users(offset, limit, search))
        return IAMUsersResultIterator()

    def get_instance(self, lookup_value):
        return self._convert_user_profile(
            iam_admin_client.get_user(lookup_value))

    def perform_update(self, serializer):
        managed_user_profile = serializer.save()
        group_manager_client = self.request.profile_service['group_manager']
        user_profile_client = self.request.profile_service['user_profile']
        user_id = managed_user_profile['airavataInternalUserId']
        added_groups = []
        for group_id in managed_user_profile['_added_group_ids']:
            group = group_manager_client.getGroup(self.authz_token, group_id)
            group_manager_client.addUsersToGroup(
                self.authz_token, [user_id], group_id)
            added_groups.append(group)
        if len(added_groups) > 0:
            user_profile = user_profile_client.getUserProfileById(
                self.authz_token,
                managed_user_profile['userId'],
                settings.GATEWAY_ID)
            signals.user_added_to_group.send(
                sender=self.__class__,
                user=user_profile,
                groups=added_groups,
                request=self.request)
        for group_id in managed_user_profile['_removed_group_ids']:
            group_manager_client.removeUsersFromGroup(
                self.authz_token, [user_id], group_id)

    def perform_destroy(self, instance):
        iam_admin_client.delete_user(instance['userId'])

    @detail_route(methods=['post'])
    def enable(self, request, user_id=None):
        iam_admin_client.enable_user(user_id)
        instance = self.get_instance(user_id)
        serializer = self.serializer_class(instance=instance,
                                           context={'request': request})
        return Response(serializer.data)

    def _convert_user_profile(self, user_profile):
        user_profile_client = self.request.profile_service['user_profile']
        group_manager_client = self.request.profile_service['group_manager']
        airavata_user_profile_exists = user_profile_client.doesUserExist(
            self.authz_token, user_profile.userId, self.gateway_id)
        groups = []
        if airavata_user_profile_exists:
            groups = group_manager_client.getAllGroupsUserBelongs(
                self.authz_token, user_profile.airavataInternalUserId)
        return {
            'airavataInternalUserId': user_profile.airavataInternalUserId,
            'userId': user_profile.userId,
            'gatewayId': user_profile.gatewayId,
            'email': user_profile.emails[0],
            'firstName': user_profile.firstName,
            'lastName': user_profile.lastName,
            'enabled': user_profile.State == Status.ACTIVE,
            'emailVerified': (user_profile.State == Status.CONFIRMED or
                              user_profile.State == Status.ACTIVE),
            'airavataUserProfileExists': airavata_user_profile_exists,
            'creationTime': user_profile.creationTime,
            'groups': groups
        }


class ExperimentStatisticsView(APIView):
    # TODO: restrict to only Admins or Read Only Admins group members
    serializer_class = serializers.ExperimentStatisticsSerializer

    def get(self, request, format=None):
        if 'fromTime' in request.GET:
            from_time = view_utils.convert_utc_iso8601_to_date(
                request.GET['fromTime']).timestamp() * 1000
        else:
            from_time = (datetime.utcnow() -
                         timedelta(days=7)).timestamp() * 1000
        from_time = int(from_time)
        if 'toTime' in request.GET:
            to_time = view_utils.convert_utc_iso8601_to_date(
                request.GET['toTime']).timestamp() * 1000
        else:
            to_time = datetime.utcnow().timestamp() * 1000
        to_time = int(to_time)
        username = request.GET.get('userName', None)
        application_name = request.GET.get('applicationName', None)
        resource_hostname = request.GET.get('resourceHostName', None)
        statistics = request.airavata_client.getExperimentStatistics(
            request.authz_token, settings.GATEWAY_ID, from_time, to_time,
            username, application_name, resource_hostname)
        serializer = self.serializer_class(
            statistics, context={'request': request})
        return Response(serializer.data)


class UnverifiedEmailUserViewSet(mixins.ListModelMixin,
                                 mixins.RetrieveModelMixin,
                                 GenericAPIBackedViewSet):
    serializer_class = serializers.UnverifiedEmailUserProfile
    pagination_class = APIResultPagination
    permission_classes = (IsInAdminsGroupPermission,)
    lookup_field = 'user_id'

    def get_list(self):
        get_users = self._get_unverified_email_user_profiles

        class UnverifiedEmailUsersResultIterator(APIResultIterator):
            def get_results(self, limit=-1, offset=0):
                return get_users(limit, offset)
        return UnverifiedEmailUsersResultIterator()

    def get_instance(self, lookup_value):
        users = self._get_unverified_email_user_profiles(
            limit=1, username=lookup_value)
        if len(users) == 0:
            raise Http404("No unverified email record found for user {}"
                          .format(lookup_value))
        else:
            return users[0]

    def _get_unverified_email_user_profiles(
            self, limit=-1, offset=0, username=None):
        unverified_emails = EmailVerification.objects.filter(
            verified=False).order_by('username').values('username').distinct()
        if username is not None:
            unverified_emails = unverified_emails.filter(username=username)
        if limit > 0:
            unverified_emails = unverified_emails[offset:offset + limit]
        results = []
        for unverified_email in unverified_emails:
            unverified_username = unverified_email['username']
            if iam_admin_client.is_user_exist(unverified_username):
                user_profile = iam_admin_client.get_user(unverified_username)
                if (user_profile.State == Status.CONFIRMED or
                        user_profile.State == Status.ACTIVE):
                    # TODO: test this
                    EmailVerification.objects.filter(
                        username=unverified_username).update(
                        verified=True)
                    continue
                results.append({
                    'userId': user_profile.userId,
                    'gatewayId': user_profile.gatewayId,
                    'email': user_profile.emails[0],
                    'firstName': user_profile.firstName,
                    'lastName': user_profile.lastName,
                    'enabled': user_profile.State == Status.ACTIVE,
                    'emailVerified': (user_profile.State == Status.CONFIRMED or
                                      user_profile.State == Status.ACTIVE),
                    'creationTime': user_profile.creationTime,
                })
            else:
                # Delete the EmailVerification records since that user no
                # longer exists in the IAM service
                EmailVerification.objects.filter(
                    username=unverified_username).delete()
        return results


class LogRecordConsumer(APIView):
    serializer_class = serializers.LogRecordSerializer

    def post(self, request, format=None):
        serializer = self.serializer_class(data=request.data)
        serializer.is_valid(raise_exception=True)
        log_record = serializer.validated_data
        log_level = getattr(logging, log_record['level'], None)
        if log_level is not None:
            stacktrace = "".join(
                map(lambda a: "\n    " + a, log_record['stacktrace']))
            log.log(log_level,
                    "Frontend error: {}: {}\nstacktrace: {}".format(
                        log_record['message'],
                        json.dumps(log_record['details'], indent=4),
                        stacktrace), extra={'request': request})
        return Response(serializer.data)


class SettingsAPIView(APIView):
    serializer_class = serializers.SettingsSerializer

    def get(self, request, format=None):
        data = {
            'fileUploadMaxFileSize': settings.FILE_UPLOAD_MAX_FILE_SIZE,
            'tusEndpoint': settings.TUS_ENDPOINT,
            'pgaUrl': settings.PGA_URL
        }
        serializer = self.serializer_class(
            data, context={'request': request})
        return Response(serializer.data)


class APIServerStatusCheckView(APIView):

    def get(self, request, format=None):
        try:
            request.airavata_client.getUserProjects(request.authz_token,
                                                    settings.GATEWAY_ID,
                                                    request.user.username,
                                                    1,  # limit
                                                    0)  # offset
            data = {
                "apiServerUp": True
            }
        except Exception as e:
            log.debug("API server status check failed: {}".format(str(e)))
            data = {
                "apiServerUp": False
            }
        return Response(data)


@api_view()
def notebook_output_view(request):
    provider_id = request.GET['provider-id']
    experiment_id = request.GET['experiment-id']
    experiment_output_name = request.GET['experiment-output-name']
    data = output_views.generate_data(request,
                                      provider_id,
                                      experiment_output_name,
                                      experiment_id)
    return HttpResponse(data['output'])


@api_view()
def html_output_view(request):
    data = _generate_output_view_data(request)
    return JsonResponse(data)


@api_view()
def image_output_view(request):
    data = _generate_output_view_data(request)
    # data should contain 'image' as a file-like object or raw bytes with the
    # file data and 'mime-type' with the images mimetype
    data['image'] = base64.b64encode(data['image']).decode('utf-8')
    return JsonResponse(data)


@api_view()
def link_output_view(request):
    data = _generate_output_view_data(request)
    return JsonResponse(data)


def _generate_output_view_data(request):
    params = request.GET.copy()
    provider_id = params.pop('provider-id')[0]
    experiment_id = params.pop('experiment-id')[0]
    experiment_output_name = params.pop('experiment-output-name')[0]
    return output_views.generate_data(request,
                                      provider_id,
                                      experiment_output_name,
                                      experiment_id,
                                      **params.dict())
