/*
 * Copyright 2017-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package org.opendevstack.provision.services;

import static java.util.stream.Collectors.toMap;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import org.apache.commons.lang3.NotImplementedException;
import org.opendevstack.provision.adapter.IJobExecutionAdapter;
import org.opendevstack.provision.adapter.exception.CreateProjectPreconditionException;
import org.opendevstack.provision.config.JenkinsPipelineProperties;
import org.opendevstack.provision.config.Quickstarter;
import org.opendevstack.provision.controller.CheckPreconditionFailure;
import org.opendevstack.provision.model.ExecutionJob;
import org.opendevstack.provision.model.ExecutionsData;
import org.opendevstack.provision.model.OpenProjectData;
import org.opendevstack.provision.model.OpenProjectDataValidator;
import org.opendevstack.provision.model.jenkins.Execution;
import org.opendevstack.provision.model.jenkins.Job;
import org.opendevstack.provision.model.webhookproxy.CreateProjectResponse;
import org.opendevstack.provision.util.HttpVerb;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

/**
 * Service to interact with Jenkins in order to provision projects and components.
 *
 * @author Torsten Jaeschke
 */
@Service
public class JenkinsPipelineAdapter extends BaseServiceAdapter implements IJobExecutionAdapter {

  private static final Logger logger = LoggerFactory.getLogger(JenkinsPipelineAdapter.class);

  public static final String EXECUTION_URL_COMP_PREFIX = "ods-qs";
  public static final String EXECUTION_URL_ADMIN_JOB_COMP_PREFIX = "ods-corejob";

  public static final List<Consumer<Map<String, String>>> COMPONENT_ID_VALIDATOR_LIST =
      Arrays.asList(
          OpenProjectDataValidator.createComponentIdValidator(
              OpenProjectDataValidator.COMPONENT_ID_MIN_LENGTH,
              OpenProjectDataValidator.COMPONENT_ID_MAX_LENGTH));

  public static final String PROJECT_ID_KEY = "PROJECT_ID";

  public static final String OPTION_KEY_GIT_SERVER_URL = "GIT_SERVER_URL";

  @Value("${openshift.jenkins.admin.webhookproxy.host}")
  protected String adminWebhookProxyHost;

  @Value("${openshift.jenkins.project.webhookproxy.host.pattern}")
  protected String projectWebhookProxyHostPattern;

  @Value("${openshift.jenkins.trigger.secret}")
  private String projectOpenshiftJenkinsTriggerSecret;

  @Value("${artifact.group.pattern}")
  protected String groupPattern;

  @Value("${openshift.apps.basedomain}")
  protected String projectOpenshiftBaseDomain;

  @Value("${openshift.console.uri}")
  protected String projectOpenshiftConsoleUri;

  @Value("${openshift.test.project.name.pattern}")
  protected String projectOpenshiftTestProjectPattern;

  @Value("${openshift.dev.project.name.pattern}")
  protected String projectOpenshiftDevProjectPattern;

  @Value("${openshift.cd.project.name.pattern}")
  protected String projectOpenshiftCdProjectPattern;

  @Value("${openshift.jenkins.project.name.pattern}")
  protected String projectOpenshiftJenkinsProjectPattern;

  @Autowired protected JenkinsPipelineProperties jenkinsPipelineProperties;

  @Value("${bitbucket.uri}")
  protected String bitbucketUri;

  @Value("${bitbucket.opendevstack.project}")
  protected String bitbucketOdsProject;

  @Value("${ods.image-tag}")
  private String odsImageTag;

  @Value("${ods.git-ref}")
  private String odsGitRef;

  private List<Job> quickstarterJobs;

  @Value("${bitbucket.technical.user}")
  protected String generalCdUser;

  public JenkinsPipelineAdapter() {
    super("jenkinspipeline");
  }

  private Map<String, Job> nameToJobMappings;

  private Map<String, String> legacyComponentTypeToNameMappings;

  @PostConstruct
  public void init() {
    quickstarterJobs = convertQuickstarterToJobs(jenkinsPipelineProperties.getQuickstarter());
    logger.info("All Quickstarters:" + jenkinsPipelineProperties.getQuickstarter());
    logger.info("All Adminjobs:" + jenkinsPipelineProperties.getAdminjobs());

    nameToJobMappings = quickstarterJobs.stream().collect(toMap(Job::getName, job -> job));
    legacyComponentTypeToNameMappings =
        ImmutableMap.<String, String>builder()
            .put("e5b77f0f-262a-42f9-9d06-5d9052c1f394", "be-java-springboot")
            .put("e59e71f5-76e0-4b8c-b040-a526197ee84d", "docker-plain")
            .put("f3a7717d-f51a-426c-82fe-4574d4e595ad", "be-golang-plain")
            .put("9992a587-959c-4ceb-8e3f-c1390e40c582", "be-python-flask")
            .put("14ce143c-7d2a-11e7-bb31-be2e44b06b34", "be-scala-akka")
            .put("7f98bafb-c81d-4eb0-aad1-700b6c05fc12", "be-typescript-express")
            .put("a7b930b2-d125-48ce-9997-9643faa9cdd0", "ds-jupyter-notebook")
            .put("69405fd4-b0c2-45a8-a6dc-0870ea56166e", "ds-rshiny")
            .put("1deb3f34-5cd4-439b-b987-440dc6591fdf", "ds-ml-service")
            .put("560954ef-d245-456c-9460-6c592c9d7784", "fe-angular")
            .put("a86d6f06-cedc-4c16-a92c-5ca48e400c3a", "fe-react")
            .put("15b927c0-f46b-46a6-984b-2bf5c4c2c756", "fe-vue")
            .put("6b205842-6321-4ade-b094-219b78d5acc0", "fe-ionic")
            .put("7d10fbfe-e129-4bab-87f5-4cc2de89f071", "airflow-cluster")
            .put("48c077f7-8bda-4f05-af5a-6fe085c9d405", "release-manager")
            .build();

    logger.info("legacyComponentTypeToNameMappings: {}", legacyComponentTypeToNameMappings);
  }

  private List<Job> convertQuickstarterToJobs(Map<String, Quickstarter> quickstarterMap) {
    return quickstarterMap.values().stream()
        .map(qs -> new Job(qs, odsGitRef))
        .sorted(Comparator.comparing(Job::getDescription))
        .collect(Collectors.toList());
  }

  public List<Job> getQuickstarterJobs() {
    return quickstarterJobs;
  }

  public List<ExecutionsData> provisionComponentsBasedOnQuickstarters(OpenProjectData project)
      throws IOException {
    if (project == null || project.quickstarters == null) {
      return new ArrayList<>();
    }

    List<ExecutionsData> executionList = new ArrayList<>();
    if (project.quickstarters != null) {
      for (Map<String, String> options : project.quickstarters) {
        String jobId = options.get(OpenProjectData.COMPONENT_TYPE_KEY);
        String groupId =
            String.format(groupPattern, project.projectKey.toLowerCase()).replace('_', '-');
        String packageName =
            String.format(
                "%s.%s",
                String.format(groupPattern, project.projectKey.toLowerCase()),
                options.get(OpenProjectData.COMPONENT_ID_KEY).replace('-', '_'));

        options.put("GROUP_ID", groupId);
        options.put(PROJECT_ID_KEY, project.projectKey.toLowerCase());
        options.put("PACKAGE_NAME", packageName);
        options.put("ODS_IMAGE_TAG", odsImageTag);
        options.put("ODS_GIT_REF", odsGitRef);

        String triggerSecret =
            project.webhookProxySecret != null
                ? project.webhookProxySecret
                : projectOpenshiftJenkinsTriggerSecret;

        final Job job =
            getQuickstarterJobs().stream()
                .filter(x -> x.id.equals(jobId))
                .findFirst()
                .orElseThrow(
                    () ->
                        new RuntimeException(
                            String.format("Cannot find quickstarter with id=%s!", jobId)));
        executionList.add(prepareAndExecuteJob(job, options, triggerSecret));
      }
    }
    return executionList;
  }

  private Optional<Job> getComponentByName(String name) {
    return Optional.ofNullable(nameToJobMappings.get(name));
  }

  @Override
  public Optional<Job> getComponentByType(String componentType) {
    Optional<String> maybeName =
        Optional.ofNullable(legacyComponentTypeToNameMappings.get(componentType));

    if (maybeName.isPresent()) {
      return maybeName.flatMap(this::getComponentByName);
    } else {
      return getComponentByName(componentType);
    }
  }

  @Override
  public OpenProjectData createPlatformProjects(OpenProjectData project) throws IOException {

    Map<String, String> options = new HashMap<>();
    Preconditions.checkNotNull(project, "Cannot create null project");
    validateQuickstarters(
        OpenProjectDataValidator.API_COMPONENT_ID_VALIDATOR_LIST, project.getQuickstarters());

    //    init webhook secret
    project.webhookProxySecret = UUID.randomUUID().toString();
    options.put(
        "PIPELINE_TRIGGER_SECRET",
        Base64.getEncoder().encodeToString(project.webhookProxySecret.getBytes()));

    String projectCdUser = generalCdUser;
    String cdUserType = "general";
    if (project.cdUser != null && !project.cdUser.trim().isEmpty()) {
      projectCdUser = project.cdUser;
      cdUserType = "project";
    }

    options.put("CD_USER_TYPE", cdUserType);
    options.put("CD_USER_ID_B64", Base64.getEncoder().encodeToString(projectCdUser.getBytes()));

    try {
      options.put(PROJECT_ID_KEY, project.projectKey.toLowerCase());
      if (project.specialPermissionSet) {
        String entitlementGroups =
            "ADMINGROUP="
                + project.projectAdminGroup
                + ","
                + "USERGROUP="
                + project.projectUserGroup
                + ","
                + "READONLYGROUP="
                + project.projectReadonlyGroup;

        logger.debug(
            "project id: {} passed project owner: {} passed groups: {}",
            project.projectKey,
            project.projectAdminUser,
            entitlementGroups);

        options.put("PROJECT_ADMIN", project.projectAdminUser);
        options.put("PROJECT_GROUPS", entitlementGroups);
      } else {
        // someone is always logged in :)
        logger.debug("project id: {} admin: {}", project.projectKey, getUserName());
        options.put("PROJECT_ADMIN", getUserName());
      }

      options.put("ODS_IMAGE_TAG", odsImageTag);
      options.put("ODS_GIT_REF", odsGitRef);
      options.put(OPTION_KEY_GIT_SERVER_URL, bitbucketUri);

      ExecutionsData data =
          prepareAndExecuteJob(
              new Job(jenkinsPipelineProperties.getCreateProjectQuickstarter(), odsGitRef),
              options,
              projectOpenshiftJenkinsTriggerSecret);

      // add openshift based links - for jenkins we know the link - hence create the
      // direct
      // access link to openshift app domain
      project.platformBuildEngineUrl =
          "https://"
              + String.format(
                  projectOpenshiftJenkinsProjectPattern,
                  project.projectKey.toLowerCase(),
                  projectOpenshiftBaseDomain);

      // we can only add the console based links - as no routes are created per
      // default
      project.platformCdEnvironmentUrl =
          String.format(
              projectOpenshiftCdProjectPattern,
              projectOpenshiftConsoleUri.trim(),
              project.projectKey.toLowerCase());

      project.platformDevEnvironmentUrl =
          String.format(
              projectOpenshiftDevProjectPattern,
              projectOpenshiftConsoleUri.trim(),
              project.projectKey.toLowerCase());

      project.platformTestEnvironmentUrl =
          String.format(
              projectOpenshiftTestProjectPattern,
              projectOpenshiftConsoleUri.trim(),
              project.projectKey.toLowerCase());

      project.lastExecutionJobs = new ArrayList<>();
      ExecutionJob executionJob = new ExecutionJob(data.getJobName(), data.getPermalink());
      project.lastExecutionJobs.add(executionJob);
      logger.debug("Project creation job: {} ", executionJob);
      return project;
    } catch (IOException ex) {
      String error =
          String.format(
              "Cannot execute job for project %s, error: %s", project.projectKey, ex.getMessage());
      logger.error(error, ex);
      throw ex;
    }
  }

  @Override
  public Map<String, String> getProjects(String filter) {
    throw new NotImplementedException("JenkinsPipelineAdapter#getProjects");
  }

  @Override
  public String getAdapterApiUri() {
    throw new NotImplementedException("JenkinsPipelineAdapter#getAdapterApiUri");
  }

  private ExecutionsData prepareAndExecuteJob(
      final Job job, Map<String, String> options, String webhookProxySecret) throws IOException {

    String jobNameOrId = job.getName();
    Preconditions.checkNotNull(jobNameOrId, "Cannot execute Null Job!");
    Execution execution = buildExecutionObject(job, options, webhookProxySecret);

    try {
      CreateProjectResponse data =
          getRestClient()
              .execute(
                  notAuthenticatedCall(HttpVerb.POST)
                      .url(execution.url)
                      .body(execution)
                      .returnType(CreateProjectResponse.class));
      logger.info("Webhook proxy returned " + data.toString());
      ExecutionsData executionsData = new ExecutionsData();
      executionsData.setMessage(data.toString());

      String namespace = data.extractNamespace();

      String jobName = String.format("%s-%s", namespace, data.extractBuildConfigName());
      String buildNumber = data.extractBuildNumber();
      String jenkinsHost = String.format("jenkins-%s%s", namespace, projectOpenshiftBaseDomain);
      String href =
          String.format(
              "https://%s/job/%s/job/%s/%s", jenkinsHost, namespace, jobName, buildNumber);
      executionsData.setJobName(jobName);
      executionsData.setPermalink(href);
      return executionsData;
    } catch (IOException ex) {
      String url = null != execution.url ? execution.url : "null'";
      logger.error("Error starting job {} for url '{}' - details:", jobNameOrId, url, ex);
      throw ex;
    }
  }

  private Execution buildExecutionObject(
      Job job, Map<String, String> options, String webhookProxySecret) {

    String projID = Objects.toString(options.get(PROJECT_ID_KEY));
    Execution execution = new Execution();

    String componentId = Objects.toString(options.get(OpenProjectData.COMPONENT_ID_KEY));

    if (jenkinsPipelineProperties.isAdminjob(job.getId())) {

      boolean deleteComponentJob = jenkinsPipelineProperties.isDeleteComponentJob(job.getId());
      String webhookProxyHost = computeWebhookProxyHost(job.getId(), projID);
      String url =
          buildExecutionUrlAdminJob(
              job, componentId, projID, webhookProxySecret, webhookProxyHost, deleteComponentJob);
      execution.url = url;
      execution.branch = job.branch;
      execution.repository = job.gitRepoName;
      execution.project = bitbucketOdsProject;

    } else {
      String webhookProxyHost = computeWebhookProxyHost(job.getId(), projID);

      execution.url =
          JenkinsPipelineAdapter.buildExecutionUrlQuickstarterJob(
              job, componentId, webhookProxySecret, webhookProxyHost);
      execution.branch = job.branch;
      execution.repository = job.gitRepoName;
      execution.project = bitbucketOdsProject;
    }
    if (options != null) {
      execution.setOptions(options);
    }

    logger.info("Execution url={}", execution.url);

    return execution;
  }

  private String computeWebhookProxyHost(String jobId, String projID) {
    if (jenkinsPipelineProperties.isDeleteComponentJob(jobId)) {
      return String.format(projectWebhookProxyHostPattern, projID, projectOpenshiftBaseDomain);
    } else if (jenkinsPipelineProperties.isAdminjob(jobId)) {
      return adminWebhookProxyHost + projectOpenshiftBaseDomain;
    } else {
      return String.format(projectWebhookProxyHostPattern, projID, projectOpenshiftBaseDomain);
    }
  }

  private static String buildExecutionBaseUrl(
      Job job, String webhookProxySecret, String webhookProxyHost) {
    return "https://"
        + webhookProxyHost
        + "/build?trigger_secret="
        + webhookProxySecret
        + "&jenkinsfile_path="
        + job.jenkinsfilePath;
  }

  public static String buildExecutionUrlAdminJob(
      Job job,
      String componentId,
      String projID,
      String webhookProxySecret,
      String webhookProxyHost,
      boolean deleteComponentJob) {
    String baseUrl = buildExecutionBaseUrl(job, webhookProxySecret, webhookProxyHost);
    String componentName =
        EXECUTION_URL_ADMIN_JOB_COMP_PREFIX + "-" + (deleteComponentJob ? componentId : projID);
    // yes, validating component name before calling jenkins
    validateComponentName(COMPONENT_ID_VALIDATOR_LIST, componentName);
    return baseUrl + "&component=" + componentName;
  }

  public static String buildExecutionUrlQuickstarterJob(
      Job job, String componentId, String webhookProxySecret, String webhookProxyHost) {
    String baseUrl = buildExecutionBaseUrl(job, webhookProxySecret, webhookProxyHost);
    String componentName = EXECUTION_URL_COMP_PREFIX + "-" + componentId;
    // yes, validating component name before calling jenkins
    validateComponentName(COMPONENT_ID_VALIDATOR_LIST, componentName);
    return baseUrl + "&component=" + componentName;
  }

  private static void validateQuickstarters(
      List<Consumer<Map<String, String>>> validators, List<Map<String, String>> quickstarters) {

    validators.forEach(
        validator -> {
          quickstarters.stream().forEach(validator);
        });
  }

  private static void validateComponentName(
      List<Consumer<Map<String, String>>> validators, String componentName) {

    HashMap<String, String> quickstarters = new HashMap();
    quickstarters.put(OpenProjectData.COMPONENT_ID_KEY, componentName);

    validateQuickstarters(validators, Arrays.asList(quickstarters));
  }

  @Override
  public Map<CLEANUP_LEFTOVER_COMPONENTS, Integer> cleanup(
      LIFECYCLE_STAGE stage, OpenProjectData project) {
    Preconditions.checkNotNull(stage);
    Preconditions.checkNotNull(project);

    Map<CLEANUP_LEFTOVER_COMPONENTS, Integer> leftovers =
        stage.equals(LIFECYCLE_STAGE.INITIAL_CREATION)
            ? cleanupWholeProjects(project)
            : cleanupQuickstartersOnly(project);
    logger.debug("Cleanup done - status: {} components are left ..", leftovers.size());

    return leftovers;
  }

  private Map<CLEANUP_LEFTOVER_COMPONENTS, Integer> cleanupWholeProjects(OpenProjectData project) {
    if (project.lastExecutionJobs != null && !project.lastExecutionJobs.isEmpty()) {
      String componentId = project.projectKey.toLowerCase();
      Quickstarter adminQuickstarter = jenkinsPipelineProperties.getDeleteProjectsQuickstarter();

      CLEANUP_LEFTOVER_COMPONENTS objectType = CLEANUP_LEFTOVER_COMPONENTS.PLTF_PROJECT;
      return runAdminJob(adminQuickstarter, project, componentId, objectType);
    }

    logger.debug("Project {} not affected from cleanup", project.projectKey);
    return Collections.emptyMap();
  }

  private Map<CLEANUP_LEFTOVER_COMPONENTS, Integer> cleanupQuickstartersOnly(
      OpenProjectData project) {

    int leftoverCount =
        project.getQuickstarters().stream()
            .map(q1 -> q1.get(OpenProjectData.COMPONENT_ID_KEY))
            .map(
                component ->
                    runAdminJob(
                        jenkinsPipelineProperties.getDeleteComponentsQuickstarter(),
                        project,
                        component,
                        CLEANUP_LEFTOVER_COMPONENTS.QUICKSTARTER))
            .filter(m -> !m.isEmpty())
            .mapToInt(e -> 1)
            .sum();

    if (leftoverCount > 0) {
      Map<CLEANUP_LEFTOVER_COMPONENTS, Integer> leftovers = new HashMap<>();
      leftovers.put(CLEANUP_LEFTOVER_COMPONENTS.QUICKSTARTER, leftoverCount);
      return leftovers;
    } else {
      return Collections.emptyMap();
    }
  }

  private Map<CLEANUP_LEFTOVER_COMPONENTS, Integer> runAdminJob(
      Quickstarter adminQuickstarter,
      OpenProjectData project,
      String componentId,
      CLEANUP_LEFTOVER_COMPONENTS objectType) {
    String projectId = project.projectKey.toLowerCase();
    Map<String, String> options = buildAdminJobOptions(projectId, componentId);
    Job job = new Job(adminQuickstarter, odsGitRef);
    try {

      logger.debug("Calling job {} for project {}", job.getId(), project.projectKey);
      ExecutionsData data =
          prepareAndExecuteJob(job, options, projectOpenshiftJenkinsTriggerSecret);
      logger.info("Result of cleanup: {}", data.toString());
      return Collections.emptyMap();
    } catch (RuntimeException | IOException e) {
      logger.debug(
          "Could not start job {} for project {}/component {} : {}",
          job.getId(),
          project.projectKey,
          componentId,
          e.getMessage());
      Map<CLEANUP_LEFTOVER_COMPONENTS, Integer> leftovers = new HashMap<>();

      leftovers.put(objectType, 1);
      return leftovers;
    }
  }

  private Map<String, String> buildAdminJobOptions(String projectId, String componentId) {
    Map<String, String> options = new HashMap<>();

    options.put(PROJECT_ID_KEY, projectId);
    options.put(OpenProjectData.COMPONENT_ID_KEY, componentId);
    options.put("ODS_IMAGE_TAG", odsImageTag);
    options.put("ODS_GIT_REF", odsGitRef);
    return options;
  }

  @Override
  public List<CheckPreconditionFailure> checkCreateProjectPreconditions(OpenProjectData newProject)
      throws CreateProjectPreconditionException {
    throw new UnsupportedOperationException("not implemented yet!");
  }
}