/*
 * This file is part of Hopsworks
 * Copyright (C) 2018, Logical Clocks AB. All rights reserved
 *
 * Hopsworks is free software: you can redistribute it and/or modify it under the terms of
 * the GNU Affero General Public License as published by the Free Software Foundation,
 * either version 3 of the License, or (at your option) any later version.
 *
 * Hopsworks is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE.  See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License along with this program.
 * If not, see <https://www.gnu.org/licenses/>.
 */
package io.hops.hopsworks.api.python.library;

import io.hops.hopsworks.api.filter.AllowedProjectRoles;
import io.hops.hopsworks.api.filter.Audience;
import io.hops.hopsworks.api.jwt.JWTHelper;
import io.hops.hopsworks.api.python.library.command.LibraryCommandsResource;
import io.hops.hopsworks.api.python.library.search.LibrarySearchBuilder;
import io.hops.hopsworks.api.python.library.search.LibrarySearchDTO;
import io.hops.hopsworks.api.util.Pagination;
import io.hops.hopsworks.common.api.ResourceRequest;
import io.hops.hopsworks.common.python.commands.CommandsController;
import io.hops.hopsworks.common.python.environment.EnvironmentController;
import io.hops.hopsworks.common.python.library.LibraryController;
import io.hops.hopsworks.common.util.Settings;
import io.hops.hopsworks.exceptions.GenericException;
import io.hops.hopsworks.exceptions.PythonException;
import io.hops.hopsworks.exceptions.ServiceException;
import io.hops.hopsworks.jwt.annotation.JWTRequired;
import io.hops.hopsworks.persistence.entity.project.Project;
import io.hops.hopsworks.persistence.entity.python.CondaInstallType;
import io.hops.hopsworks.persistence.entity.python.PythonDep;
import io.hops.hopsworks.persistence.entity.user.Users;
import io.hops.hopsworks.restutils.RESTCodes;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;

import javax.ejb.EJB;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.BeanParam;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Api(value = "Python Environment Library Resource")
@RequestScoped
@TransactionAttribute(TransactionAttributeType.NEVER)
public class LibraryResource {

  @EJB
  private LibraryController libraryController;
  @EJB
  private LibraryBuilder librariesBuilder;
  @EJB
  private LibrarySearchBuilder librariesSearchBuilder;
  @EJB
  private CommandsController commandsController;
  @EJB
  private Settings settings;
  @Inject
  private LibraryCommandsResource libraryCommandsResource;
  @EJB
  private EnvironmentController environmentController;
  @EJB
  private JWTHelper jwtHelper;

  private static final Pattern VALIDATION_PATTERN = Pattern.compile("[a-zA-Z0-9_\\-\\.]+");
  private static final Pattern CHANNEL_PATTERN = Pattern.compile("[a-zA-Z0-9_\\-:/~?&\\.]+");

  private Project project;
  private String pythonVersion;
  
  public LibraryResource setProjectAndVersion(Project project, String pythonVersion) {
    this.project = project;
    this.pythonVersion = pythonVersion;
    return this;
  }
  
  public Project getProject() {
    return project;
  }

  @ApiOperation(value = "Get the python libraries installed in this environment", response = LibraryDTO.class)
  @GET
  @Produces(MediaType.APPLICATION_JSON)
  @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST})
  @JWTRequired(acceptedTokens = {Audience.API}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER"})
  public Response get(@BeanParam Pagination pagination,
      @BeanParam LibrariesBeanParam librariesBeanParam,
      @Context UriInfo uriInfo, @Context SecurityContext sc) throws PythonException {
    environmentController.checkCondaEnabled(project, pythonVersion);
    ResourceRequest resourceRequest = new ResourceRequest(ResourceRequest.Name.LIBRARIES);
    resourceRequest.setOffset(pagination.getOffset());
    resourceRequest.setLimit(pagination.getLimit());
    resourceRequest.setSort(librariesBeanParam.getSortBySet());
    resourceRequest.setFilter(librariesBeanParam.getFilter());
    if (librariesBeanParam.getExpansions() != null) {
      resourceRequest.setExpansions(librariesBeanParam.getExpansions().getResources());
    }
    LibraryDTO libraryDTO = librariesBuilder.buildItems(uriInfo, resourceRequest, project);
    return Response.ok().entity(libraryDTO).build();
  }
  
  @ApiOperation(value = "Get the a python library installed in this environment", response = LibraryDTO.class)
  @GET
  @Path("{library}")
  @Produces(MediaType.APPLICATION_JSON)
  @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST})
  @JWTRequired(acceptedTokens = {Audience.API}, allowedUserRoles = {"HOPS_ADMIN", "HOPS_USER"})
  public Response getByName(@PathParam("library") String library, @BeanParam LibraryExpansionBeanParam expansions,
    @Context UriInfo uriInfo, @Context SecurityContext sc) throws PythonException {
    validatePattern(library);
    environmentController.checkCondaEnabled(project, pythonVersion);
    PythonDep dep = libraryController.getPythonDep(library, project);
    ResourceRequest resourceRequest = new ResourceRequest(ResourceRequest.Name.LIBRARIES);
    if (expansions != null) {
      resourceRequest.setExpansions(expansions.getResources());
    }
    LibraryDTO libraryDTO = librariesBuilder.buildItem(uriInfo, resourceRequest, dep, project);
    return Response.ok().entity(libraryDTO).build();
  }


  @ApiOperation(value = "Uninstall a python library from the environment")
  @DELETE
  @Produces(MediaType.APPLICATION_JSON)
  @Path("{library}")
  @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST})
  @JWTRequired(acceptedTokens={Audience.API}, allowedUserRoles={"HOPS_ADMIN", "HOPS_USER"})
  public Response uninstall(@Context SecurityContext sc, @PathParam("library") String library)
    throws ServiceException, GenericException, PythonException {
    validatePattern(library);
    Users user = jwtHelper.getUserPrincipal(sc);
    environmentController.checkCondaEnabled(project, pythonVersion);
    if (settings.getUnmutablePythonLibraryNames().contains(library)) {
      throw new ServiceException(RESTCodes.ServiceErrorCode.ANACONDA_DEP_REMOVE_FORBIDDEN, Level.INFO,
          "library: " + library);
    }

    environmentController.checkCondaEnvExists(project, user);

    commandsController.deleteCommands(project, library);
    libraryController.uninstallLibrary(project, user, library);
    return Response.noContent().build();
  }

  @ApiOperation(value = "Install a python library in the environment")
  @POST
  @Path("{library}")
  @Produces(MediaType.APPLICATION_JSON)
  @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST})
  @JWTRequired(acceptedTokens={Audience.API}, allowedUserRoles={"HOPS_ADMIN", "HOPS_USER"})
  public Response install(@PathParam("library") String library,
                          @QueryParam("package_manager") LibraryDTO.PackageManager packageManager,
                          @QueryParam("version") String version,
                          @QueryParam("channel") String channel,
                          @Context UriInfo uriInfo,
                          @Context HttpServletRequest req)
      throws ServiceException, GenericException, PythonException {
    Users user = jwtHelper.getUserPrincipal(req);
    if (version == null || version.isEmpty()) {
      throw new PythonException(RESTCodes.PythonErrorCode.VERSION_NOT_SPECIFIED, Level.FINE);
    }
    validatePattern(library);
    validatePattern(version);

    environmentController.checkCondaEnabled(project, pythonVersion);
    if (packageManager == null) {
      throw new PythonException(RESTCodes.PythonErrorCode.INSTALL_TYPE_NOT_SUPPORTED, Level.FINE);
    }
    switch (packageManager) {
      case PIP:
        //indicate that the library comes from the distribution published in PyPi
        channel = "pypi";
        break;
      case CONDA:
        if(channel == null) {
          throw new PythonException(RESTCodes.PythonErrorCode.CONDA_INSTALL_REQUIRES_CHANNEL, Level.FINE);
        } else {
          validateChannel(channel);
        }
        break;
      default:
        throw new PythonException(RESTCodes.PythonErrorCode.INSTALL_TYPE_NOT_SUPPORTED, Level.FINE);
    }

    //TODO account for ongoing operations
    for(PythonDep dep: project.getPythonDepCollection()) {
      if(dep.getDependency().equalsIgnoreCase(library)) {
        throw new ServiceException(RESTCodes.ServiceErrorCode.ANACONDA_DEP_INSTALL_FORBIDDEN, Level.FINE);
      }
    }

    environmentController.checkCondaEnvExists(project, user);
  
    PythonDep dep = libraryController.addLibrary(project, user,
      CondaInstallType.valueOf(packageManager.name().toUpperCase()), channel, library, version);
    ResourceRequest resourceRequest = new ResourceRequest(ResourceRequest.Name.LIBRARIES);
    LibraryDTO libraryDTO = librariesBuilder.build(uriInfo, resourceRequest, dep, project);
    return Response.created(libraryDTO.getHref()).entity(libraryDTO).build();
  }


  @ApiOperation(value = "Search for libraries using conda or pip package managers", response = LibrarySearchDTO.class)
  @GET
  @Path("{search: conda|pip}")
  @Produces(MediaType.APPLICATION_JSON)
  @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST})
  @JWTRequired(acceptedTokens={Audience.API}, allowedUserRoles={"HOPS_ADMIN", "HOPS_USER"})
  public Response search(@PathParam("search") String search,
                         @QueryParam("query") String query,
                         @QueryParam("channel") String channel, @Context UriInfo uriInfo, @Context SecurityContext sc)
    throws ServiceException, PythonException {
    validatePattern(query);
    environmentController.checkCondaEnabled(project, pythonVersion);
    LibrarySearchDTO librarySearchDTO;
    LibraryDTO.PackageManager packageManager = LibraryDTO.PackageManager.fromString(search);
    switch (packageManager) {
      case CONDA:
        validateChannel(channel);
        librarySearchDTO = librariesSearchBuilder.buildCondaItems(uriInfo, query, project, channel);
        break;
      case PIP:
        librarySearchDTO = librariesSearchBuilder.buildPipItems(uriInfo, query, project);
        break;
      default:
        throw new PythonException(RESTCodes.PythonErrorCode.PYTHON_SEARCH_TYPE_NOT_SUPPORTED, Level.FINE);
    }
    return Response.ok().entity(librarySearchDTO).build();
  }
  
  @ApiOperation(value = "Python Library Commands sub-resource", tags = {"LibraryCommandsResource"})
  @Path("{library}/commands")
  @AllowedProjectRoles({AllowedProjectRoles.DATA_OWNER, AllowedProjectRoles.DATA_SCIENTIST})
  public LibraryCommandsResource libraryCommandsResource() {
    return this.libraryCommandsResource.setProject(project, pythonVersion);
  }

  private void validatePattern(String element) throws IllegalArgumentException {
    Matcher matcher = VALIDATION_PATTERN.matcher(element);
    if (!matcher.matches()) {
      throw new IllegalArgumentException("Library names should follow the pattern: [a-zA-Z0-9_\\-]+");
    }
  }

  private void validateChannel(String channel) throws IllegalArgumentException {
    Matcher matcher = CHANNEL_PATTERN.matcher(channel);
    if (!matcher.matches()) {
      throw new IllegalArgumentException("Library names should follow the pattern: [a-zA-Z0-9_\\-:/~?&]+");
    }
  }
}
