package com.thinkbiganalytics.jobrepo.rest.controller;

/*-
 * #%L
 * thinkbig-job-repository-controller
 * %%
 * Copyright (C) 2017 ThinkBig Analytics
 * %%
 * 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.
 * #L%
 */

import com.thinkbiganalytics.DateTimeUtil;
import com.thinkbiganalytics.jobrepo.query.model.ExecutedJob;
import com.thinkbiganalytics.jobrepo.query.model.ExecutedStep;
import com.thinkbiganalytics.jobrepo.query.model.FeedHealth;
import com.thinkbiganalytics.jobrepo.query.model.JobStatusCount;
import com.thinkbiganalytics.jobrepo.query.model.SearchResult;
import com.thinkbiganalytics.jobrepo.query.model.transform.JobModelTransform;
import com.thinkbiganalytics.jobrepo.query.model.transform.JobStatusTransform;
import com.thinkbiganalytics.jobrepo.query.model.transform.ModelUtils;
import com.thinkbiganalytics.jobrepo.repository.rest.model.JobAction;
import com.thinkbiganalytics.jobrepo.security.OperationsAccessControl;
import com.thinkbiganalytics.jobrepo.service.JobExecutionException;
import com.thinkbiganalytics.jobrepo.service.JobService;
import com.thinkbiganalytics.metadata.api.MetadataAccess;
import com.thinkbiganalytics.metadata.api.feed.OpsManagerFeedProvider;
import com.thinkbiganalytics.metadata.api.jobrepo.job.BatchJobExecution;
import com.thinkbiganalytics.metadata.api.jobrepo.job.BatchJobExecutionProvider;
import com.thinkbiganalytics.metadata.api.jobrepo.step.BatchStepExecution;
import com.thinkbiganalytics.metadata.api.jobrepo.step.BatchStepExecutionProvider;
import com.thinkbiganalytics.rest.model.RestResponseStatus;
import com.thinkbiganalytics.security.AccessController;

import org.apache.commons.lang3.StringUtils;
import org.joda.time.Period;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;

/**
 * Provides rest endpoints for control and monitoring of the pipeline
 */
@Api(tags = "Operations Manager - Jobs", produces = "application/json")
@Path(JobsRestController.BASE)
public class JobsRestController {

    public static final String BASE = "/v1/jobs";

    @Inject
    OpsManagerFeedProvider opsFeedManagerFeedProvider;

    @Inject
    BatchJobExecutionProvider jobExecutionProvider;

    @Inject
    BatchStepExecutionProvider stepExecutionProvider;

    @Inject
    private MetadataAccess metadataAccess;

    @Inject
    private JobService jobService;

    @Inject
    private AccessController accessController;

    @GET
    @Path("/{executionId}")
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation("Gets the specified job.")
    @ApiResponses({
                      @ApiResponse(code = 200, message = "Returns the job.", response = ExecutedJob.class),
                      @ApiResponse(code = 400, message = "The executionId is not a valid integer.", response = RestResponseStatus.class)
                  })
    public ExecutedJob getJob(@PathParam("executionId") String executionId,
                              @QueryParam(value = "includeSteps") @DefaultValue("false") boolean includeSteps) {
        this.accessController.checkPermission(AccessController.SERVICES, OperationsAccessControl.ACCESS_OPS);

        return metadataAccess.read(() -> {
            ExecutedJob executedJob = null;
            BatchJobExecution jobExecution = jobExecutionProvider.findByJobExecutionId(Long.parseLong(executionId));
            if (jobExecution != null) {
                if (includeSteps) {
                    executedJob = JobModelTransform.executedJob(jobExecution);
                } else {
                    executedJob = JobModelTransform.executedJobSimple(jobExecution);
                }
            }
            return executedJob;
        });

    }

    /**
     * Get the progress of each of the steps of the job execution for the given job instance id
     *
     * @return A list of each step and its progress, or an HTTP error code on failure
     */
    @GET
    @Path("/{executionId}/steps")
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation("Gets the steps of the specified job.")
    @ApiResponses({
                      @ApiResponse(code = 200, message = "Returns the steps.", response = ExecutedStep.class, responseContainer = "List"),
                      @ApiResponse(code = 400, message = "The executionId is not a valid integer.", response = RestResponseStatus.class)
                  })
    public List<ExecutedStep> getJobSteps(@PathParam("executionId") String executionId) {
        this.accessController.checkPermission(AccessController.SERVICES, OperationsAccessControl.ACCESS_OPS);
        return metadataAccess.read(() -> {
            List<? extends BatchStepExecution> steps = stepExecutionProvider.getSteps(Long.parseLong(executionId));
            return JobModelTransform.executedSteps(steps);
        });
    }

    /**
     * Restart the job associated with the given instance id
     *
     * @return A status message and the appropriate http status code
     */
    @POST
    @Path("/{executionId}/restart")
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation(value = "Restarts the specified job.", hidden = true)
    @ApiResponses({
                      @ApiResponse(code = 200, message = "Returns the job.", response = ExecutedJob.class),
                      @ApiResponse(code = 404, message = "The executionId is not a valid integer.", response = RestResponseStatus.class)
                  })
    public ExecutedJob restartJob(@PathParam("executionId") Long executionId) throws JobExecutionException {

        this.accessController.checkPermission(AccessController.SERVICES, OperationsAccessControl.ADMIN_OPS);
        ExecutedJob job = metadataAccess.commit(() -> {
            Long newJobExecutionId = this.jobService.restartJobExecution(executionId);
            if (newJobExecutionId != null) {
                BatchJobExecution jobExecution = jobExecutionProvider.findByJobExecutionId(newJobExecutionId);
                if (jobExecution != null) {
                    return JobModelTransform.executedJob(jobExecution);
                } else {
                    return null;
                }
            } else {
                return null;
            }
        });
        if (job == null) {
            throw new JobExecutionException("Could not restart the job with execution Id of " + executionId);
        }
        return job;
    }

    /**
     * Stop the job associated with the given instance id
     *
     * @param executionId The job instance id
     * @return A status message and the appropriate http status code
     */
    @POST
    @Path("/{executionId}/stop")
    @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED})
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation(value = "Stops the specified job.", hidden = true)
    @ApiResponses({
                      @ApiResponse(code = 200, message = "Returns the job.", response = ExecutedJob.class),
                      @ApiResponse(code = 404, message = "The executionId is not a valid integer.", response = RestResponseStatus.class)
                  })
    public ExecutedJob stopJob(@PathParam("executionId") Long executionId, JobAction jobAction) {

        this.accessController.checkPermission(AccessController.SERVICES, OperationsAccessControl.ADMIN_OPS);
        metadataAccess.commit(() -> {
            boolean stopped = this.jobService.stopJobExecution(executionId);
            return stopped;
        });
        return getJob(executionId.toString(), jobAction.isIncludeSteps());
    }

    /**
     * Abandon the job associated with the given instance id
     *
     * @param executionId The job instance id
     * @return A status message and the appropriate http status code
     */
    @POST
    @Path("/{executionId}/abandon")
    @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED})
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation("Abandons the specified job.")
    @ApiResponses({
                      @ApiResponse(code = 200, message = "Returns the abandoned job.", response = ExecutedJob.class),
                      @ApiResponse(code = 204, message = "The job could not be found."),
                      @ApiResponse(code = 404, message = "The executionId is not a valid integer.", response = RestResponseStatus.class)
                  })
    public ExecutedJob abandonJob(@PathParam("executionId") Long executionId, JobAction jobAction) {

        this.accessController.checkPermission(AccessController.SERVICES, OperationsAccessControl.ADMIN_OPS);
        metadataAccess.commit(() -> {
            this.jobService.abandonJobExecution(executionId);
            return null;
        });
        return getJob(executionId.toString(), jobAction.isIncludeSteps());
    }

    /**
     * Abandon the job associated with the given instance id
     *
     * @param feedName Full feed name (including category) for which all jobs are to be abandoned
     * @return Feed Health status
     */
    @POST
    @Path("/abandon-all/{feedName}")
    @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED})
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation("Abandons all jobs for the specified feed.")
    @ApiResponses(
        @ApiResponse(code = 200, message = "Returns the feed health.", response = FeedHealth.class)
    )
    public FeedHealth abandonAllJobs(@Context HttpServletRequest request, @PathParam("feedName") String feedName) {

        this.accessController.checkPermission(AccessController.SERVICES, OperationsAccessControl.ADMIN_OPS);

        return metadataAccess.commit(() -> {
            opsFeedManagerFeedProvider.abandonFeedJobs(feedName);
            return null;
        });
    }

    /**
     * Fail the job associated with the given instance id
     *
     * @param executionId The job instance id
     * @return A status message and the appropriate http status code
     */
    @POST
    @Path("/{executionId}/fail")
    @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_FORM_URLENCODED})
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation("Fails the specified job.")
    @ApiResponses({
                      @ApiResponse(code = 200, message = "Returns the job.", response = ExecutedJob.class),
                      @ApiResponse(code = 404, message = "The executionId is not a valid integer.", response = RestResponseStatus.class)
                  })
    public ExecutedJob failJob(@PathParam("executionId") Long executionId, JobAction jobAction) {

        this.accessController.checkPermission(AccessController.SERVICES, OperationsAccessControl.ADMIN_OPS);
        metadataAccess.commit(() -> {
            this.jobService.failJobExecution(executionId);
            return null;
        });
        return getJob(executionId.toString(), jobAction.isIncludeSteps());
    }


    @GET
    @Path("/")
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation("Lists all jobs.")
    @ApiResponses({
                      @ApiResponse(code = 200, message = "Returns the jobs.", response = SearchResult.class),
                      @ApiResponse(code = 400, message = "The sort cannot be empty.", response = RestResponseStatus.class),
                      @ApiResponse(code = 404, message = "The start or limit is not a valid integer.", response = RestResponseStatus.class),
                      @ApiResponse(code = 500, message = "The sort contains an invalid value.", response = RestResponseStatus.class)
                  })
    public SearchResult findJobs(@QueryParam("sort") @DefaultValue("") String sort,
                                 @QueryParam("limit") @DefaultValue("10") Integer limit,
                                 @QueryParam("start") @DefaultValue("1") Integer start,
                                 @QueryParam("filter") String filter,
                                 @Context HttpServletRequest request) {
        return metadataAccess.read(() -> {
            Page<ExecutedJob> page = jobExecutionProvider.findAll(filter, pageRequest(start, limit, sort)).map(jobExecution -> JobModelTransform.executedJobSimple(jobExecution));
            return ModelUtils.toSearchResult(page);
        });


    }

    @GET
    @Path("/list")
    @ApiOperation("Lists all jobs.")
    @ApiResponses({
                      @ApiResponse(code = 200, message = "Returns the jobs.", response = SearchResult.class),
                      @ApiResponse(code = 400, message = "The sort cannot be empty.", response = RestResponseStatus.class),
                      @ApiResponse(code = 404, message = "The start or limit is not a valid integer.", response = RestResponseStatus.class),
                      @ApiResponse(code = 500, message = "The sort contains an invalid value.", response = RestResponseStatus.class)
                  })
    public List<ExecutedJob> findJobsList(@QueryParam("sort") @DefaultValue("") String sort,
                                          @QueryParam("limit") @DefaultValue("10") Integer limit,
                                          @QueryParam("start") @DefaultValue("1") Integer start,
                                          @QueryParam("filter") String filter,
                                          @Context HttpServletRequest request) {
        this.accessController.checkPermission(AccessController.SERVICES, OperationsAccessControl.ACCESS_OPS);

        return metadataAccess.read(() -> {
            Page<ExecutedJob> page = jobExecutionProvider.findAll(filter, pageRequest(start, limit, sort)).map(jobExecution -> JobModelTransform.executedJobSimple(jobExecution));
            return page != null ? page.getContent() : Collections.emptyList();
        });
    }


    @GET
    @Path("/running")
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation("Gets the list of running jobs.")
    @ApiResponses({
                      @ApiResponse(code = 200, message = "Returns the jobs.", response = SearchResult.class),
                      @ApiResponse(code = 400, message = "The sort cannot be empty.", response = RestResponseStatus.class),
                      @ApiResponse(code = 404, message = "The start or limit is not a valid integer.", response = RestResponseStatus.class),
                      @ApiResponse(code = 500, message = "The sort contains an invalid value.", response = RestResponseStatus.class)
                  })
    public SearchResult findRunningJobs(@QueryParam("sort") @DefaultValue("") String sort,
                                        @QueryParam("limit") @DefaultValue("10") Integer limit,
                                        @QueryParam("start") @DefaultValue("1") Integer start,
                                        @QueryParam("filter") String filter,
                                        @Context HttpServletRequest request) {

        this.accessController.checkPermission(AccessController.SERVICES, OperationsAccessControl.ACCESS_OPS);

        return metadataAccess.read(() -> {
            String defaultFilter = ensureDefaultFilter(filter, jobExecutionProvider.RUNNING_FILTER);
            Page<ExecutedJob> page = jobExecutionProvider.findAll(defaultFilter, pageRequest(start, limit, sort)).map(jobExecution -> JobModelTransform.executedJobSimple(jobExecution));
            return ModelUtils.toSearchResult(page);
        });

    }


    @GET
    @Path("/failed")
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation("Gets the list of failed jobs.")
    @ApiResponses({
                      @ApiResponse(code = 200, message = "Returns the jobs.", response = SearchResult.class),
                      @ApiResponse(code = 400, message = "The sort cannot be empty.", response = RestResponseStatus.class),
                      @ApiResponse(code = 404, message = "The start or limit is not a valid integer.", response = RestResponseStatus.class),
                      @ApiResponse(code = 500, message = "The sort contains an invalid value.", response = RestResponseStatus.class)
                  })
    public SearchResult findFailedJobs(@QueryParam("sort") @DefaultValue("") String sort,
                                       @QueryParam("limit") @DefaultValue("10") Integer limit,
                                       @QueryParam("start") @DefaultValue("1") Integer start,
                                       @QueryParam("filter") String filter,
                                       @Context HttpServletRequest request) {

        return metadataAccess.read(() -> {
            String defaultFilter = ensureDefaultFilter(filter, jobExecutionProvider.FAILED_FILTER);
            Page<ExecutedJob> page = jobExecutionProvider.findAll(defaultFilter, pageRequest(start, limit, sort)).map(jobExecution -> JobModelTransform.executedJobSimple(jobExecution));
            return ModelUtils.toSearchResult(page);
        });
    }


    @GET
    @Path("/stopped")
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation("Gets the list of failed jobs.")
    @ApiResponses({
                      @ApiResponse(code = 200, message = "Returns the jobs.", response = SearchResult.class),
                      @ApiResponse(code = 400, message = "The sort cannot be empty.", response = RestResponseStatus.class),
                      @ApiResponse(code = 404, message = "The start or limit is not a valid integer.", response = RestResponseStatus.class),
                      @ApiResponse(code = 500, message = "The sort contains an invalid value.", response = RestResponseStatus.class)
                  })
    public SearchResult findStoppedJobs(@QueryParam("sort") @DefaultValue("") String sort,
                                        @QueryParam("limit") @DefaultValue("10") Integer limit,
                                        @QueryParam("start") @DefaultValue("1") Integer start,
                                        @QueryParam("filter") String filter,
                                        @Context HttpServletRequest request) {

        this.accessController.checkPermission(AccessController.SERVICES, OperationsAccessControl.ACCESS_OPS);

        return metadataAccess.read(() -> {
            String defaultFilter = ensureDefaultFilter(filter, jobExecutionProvider.STOPPED_FILTER);
            Page<ExecutedJob> page = jobExecutionProvider.findAll(defaultFilter, pageRequest(start, limit, sort)).map(jobExecution -> JobModelTransform.executedJobSimple(jobExecution));
            return ModelUtils.toSearchResult(page);
        });

    }

    @GET
    @Path("/completed")
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation("Gets the list of completed jobs.")
    @ApiResponses({
                      @ApiResponse(code = 200, message = "Returns the jobs.", response = SearchResult.class),
                      @ApiResponse(code = 400, message = "The sort cannot be empty.", response = RestResponseStatus.class),
                      @ApiResponse(code = 404, message = "The start or limit is not a valid integer.", response = RestResponseStatus.class),
                      @ApiResponse(code = 500, message = "The sort contains an invalid value.", response = RestResponseStatus.class)
                  })
    public SearchResult findCompletedJobs(@QueryParam("sort") @DefaultValue("") String sort,
                                          @QueryParam("limit") @DefaultValue("10") Integer limit,
                                          @QueryParam("start") @DefaultValue("1") Integer start,
                                          @QueryParam("filter") String filter,
                                          @Context HttpServletRequest request) {

        this.accessController.checkPermission(AccessController.SERVICES, OperationsAccessControl.ACCESS_OPS);

        return metadataAccess.read(() -> {
            String defaultFilter = ensureDefaultFilter(filter, jobExecutionProvider.COMPLETED_FILTER);
            Page<ExecutedJob> page = jobExecutionProvider.findAll(defaultFilter, pageRequest(start, limit, sort)).map(jobExecution -> JobModelTransform.executedJobSimple(jobExecution));
            return ModelUtils.toSearchResult(page);
        });

    }


    @GET
    @Path("/abandoned")
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation("Gets the list of abandoned jobs.")
    @ApiResponses({
                      @ApiResponse(code = 200, message = "Returns the jobs.", response = SearchResult.class),
                      @ApiResponse(code = 400, message = "The sort cannot be empty.", response = RestResponseStatus.class),
                      @ApiResponse(code = 404, message = "The start or limit is not a valid integer.", response = RestResponseStatus.class),
                      @ApiResponse(code = 500, message = "The sort contains an invalid value.", response = RestResponseStatus.class)
                  })
    public SearchResult findAbandonedJobs(@QueryParam("sort") @DefaultValue("") String sort,
                                          @QueryParam("limit") @DefaultValue("10") Integer limit,
                                          @QueryParam("start") @DefaultValue("1") Integer start,
                                          @QueryParam("filter") String filter,
                                          @Context HttpServletRequest request) {

        this.accessController.checkPermission(AccessController.SERVICES, OperationsAccessControl.ACCESS_OPS);

        return metadataAccess.read(() -> {
            String defaultFilter = ensureDefaultFilter(filter, jobExecutionProvider.ABANDONED_FILTER);
            Page<ExecutedJob> page = jobExecutionProvider.findAll(defaultFilter, pageRequest(start, limit, sort)).map(jobExecution -> JobModelTransform.executedJobSimple(jobExecution));
            return ModelUtils.toSearchResult(page);
        });
    }


    @GET
    @Path("/daily-status-count")
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation("Gets the daily statistics.")
    @ApiResponses(
        @ApiResponse(code = 200, message = "Returns the daily stats.", response = JobStatusCount.class, responseContainer = "List")
    )
    public List<JobStatusCount> findDailyStatusCount(@QueryParam("period") String periodString) {

        this.accessController.checkPermission(AccessController.SERVICES, OperationsAccessControl.ACCESS_OPS);

        Period period = DateTimeUtil.period(periodString);
        return metadataAccess.read(() -> {
            List<com.thinkbiganalytics.metadata.api.jobrepo.job.JobStatusCount> counts = jobExecutionProvider.getJobStatusCountByDateFromNow(period, null);
            if (counts != null) {
                List<JobStatusCount> jobStatusCounts = counts.stream().map(c -> JobStatusTransform.jobStatusCount(c)).collect(Collectors.toList());
                JobStatusTransform.ensureDateFromPeriodExists(jobStatusCounts, period);
                return jobStatusCounts;
            }
            return Collections.emptyList();
        });
    }


    @GET
    @Path("/running-failed-counts")
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation("Gets the daily statistics.")
    @ApiResponses(
        @ApiResponse(code = 200, message = "Returns the daily stats.", response = JobStatusCount.class, responseContainer = "List")
    )
    public List<JobStatusCount> getRunningOrFailedJobCounts() {
        this.accessController.checkPermission(AccessController.SERVICES, OperationsAccessControl.ACCESS_OPS);

        return metadataAccess.read(() -> {
            List<com.thinkbiganalytics.metadata.api.jobrepo.job.JobStatusCount> counts = jobExecutionProvider.getJobStatusCount(jobExecutionProvider.RUNNING_OR_FAILED_FILTER);
            if (counts != null) {
                return counts.stream().map(c -> JobStatusTransform.jobStatusCount(c)).collect(Collectors.toList());
            }
            return Collections.emptyList();
        });

    }

    /**
     * This will evaluate the {@code incomingFilter} and append/set the value including the {@code defaultFilter} and return a new String with the updated filter
     */
    private String ensureDefaultFilter(String incomingFilter, String defaultFilter) {
        String filter = incomingFilter;
        if (StringUtils.isBlank(filter) || !StringUtils.containsIgnoreCase(filter, defaultFilter)) {
            if (StringUtils.isNotBlank(filter)) {
                if (StringUtils.endsWith(filter, ",")) {
                    filter += defaultFilter;
                } else {
                    filter += "," + defaultFilter;
                }
            } else {
                filter = defaultFilter;
            }
        }
        return filter;
    }

    private PageRequest pageRequest(Integer start, Integer limit, String sort) {
        if (StringUtils.isNotBlank(sort)) {
            Sort.Direction dir = Sort.Direction.ASC;
            if (sort.startsWith("-")) {
                dir = Sort.Direction.DESC;
                sort = sort.substring(1);
            }
            return new PageRequest((start / limit), limit, dir, sort);
        } else {
            return new PageRequest((start / limit), limit);
        }
    }

}
