package com.atlassian.stash.internal.web.maintenance;

import com.atlassian.soy.renderer.SoyTemplateRenderer;
import com.atlassian.soy.springmvc.SoyView;
import com.atlassian.soy.springmvc.InjectedDataFactory;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.stash.internal.maintenance.*;
import com.atlassian.stash.internal.rest.RestMaintenanceLock;
import com.atlassian.stash.internal.web.auth.IgnoresXsrf;
import com.atlassian.stash.internal.web.soy.MaintenanceInjectedDataFactory;
import com.atlassian.stash.internal.web.soy.StashSoyViewResolver;
import com.atlassian.stash.internal.web.util.ControllerSupport;
import com.atlassian.bitbucket.request.RequestContext;
import com.atlassian.bitbucket.request.RequestManager;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * Controller for managing maintenance operations. NOTE: this controller also contains REST methods. This exist here
 * rather than on a separate {@code MaintenanceResource} class because we do not wish these to be part of our published
 * REST interface yet.
 *
 * @since 1.2
 */
@Controller
@RequestMapping(MaintenanceController.PATH_MAINTENANCE)
public class MaintenanceController extends ControllerSupport {

    public static final String PATH_MAINTENANCE = "/maintenance";
    public static final String PATH_LOCK = "lock";
    public static final String VIEW_DB_REDIRECT = "redirect:/admin/db";
    public static final String VIEW_HOME_REDIRECT = "redirect:/";
    public static final String VIEW_MAINTENANCE_BACKUP = "bitbucket.internal.maintenance.backup";
    public static final String VIEW_MAINTENANCE_MIGRATION = "bitbucket.internal.maintenance.migration";

    private final InjectedDataFactory injectedDataFactory;
    private final MaintenanceService maintenanceService;
    private final RequestManager requestManager;
    private final SoyTemplateRenderer soyTemplateRenderer;

    private long cancelTimeout;

    @Autowired
    public MaintenanceController(I18nService i18nService, MaintenanceService maintenanceService,
                                 RequestManager requestManager, SoyTemplateRenderer soyTemplateRenderer) {
        super(i18nService);

        this.maintenanceService = maintenanceService;
        this.requestManager = requestManager;
        this.soyTemplateRenderer = soyTemplateRenderer;

        injectedDataFactory = new MaintenanceInjectedDataFactory();
    }

    @IgnoresXsrf //Uses the token provided in the URL to verify the authenticity of the request
    @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public ModelAndView cancelMaintenance(@RequestParam(value = "token", required = false) String token,
                                          RedirectAttributes redirectAttributes) {
        MaintenanceTaskMonitor runningTask = maintenanceService.getRunningTask();
        if (runningTask == null) {
            //If no maintenance is in progress, redirect the user to the front page. There's nothing to cancel, so it
            //is impossible to know where they should have gone, or if the provided token is correct.
            return new ModelAndView(VIEW_HOME_REDIRECT);
        }

        try {
            redirectAttributes.addFlashAttribute("cancelMigrationSuccessful",
                    runningTask.cancel(token, cancelTimeout, TimeUnit.MINUTES));

            return new ModelAndView(chooseRedirect(runningTask.getType()));
        } catch (IncorrectTokenMaintenanceException e) {
            //The cancel token provided was not correct, and so the in-progress maintenance was not canceled. Continue
            //to display the maintenance progress screen
            return viewMaintenance(runningTask, ImmutableList.of(e.getLocalizedMessage()));
        }
    }

    @RequestMapping(method = RequestMethod.DELETE, params = "token", value = PATH_LOCK)
    public ResponseEntity<RestErrorTexts> unlock(@RequestParam(value = "token") String token) {
        MaintenanceLock lock = maintenanceService.getLock();
        if (lock == null) {
            return newResponse(HttpStatus.NOT_FOUND);
        }

        try {
            lock.unlock(token);

            return newResponse(HttpStatus.OK);
        } catch (IncorrectTokenMaintenanceException e) {
            return newResponse(HttpStatus.CONFLICT, new RestErrorTexts(e.getLocalizedMessage()));
        }
    }

    @RequestMapping(method = RequestMethod.GET, value = PATH_LOCK)
    @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
    public ModelAndView viewLock() {
        MaintenanceLock lock = maintenanceService.getLock();
        if (lock == null) {
            return new ModelAndView(VIEW_HOME_REDIRECT);
        }

        MaintenanceTaskStatus taskStatus = maintenanceService.getRunningTask();
        ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();

        if (taskStatus != null) {
            builder.put("status", taskStatus);
        }

        return modelAndView("bitbucket.internal.maintenance.lock", builder.build());
    }

    @RequestMapping(method = RequestMethod.GET)
    @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
    public ModelAndView viewMaintenance() {
        MaintenanceTaskMonitor task = maintenanceService.getRunningTask();
        if (task == null) {
            //If no maintenance is in progress... The most likely scenario is that maintenance was completed after
            //the request was processed by the JohnsonServlet and before the call arrived here. the only sensible
            //thing to do is redirect to the same place Johnson would have.
            return new ModelAndView(VIEW_HOME_REDIRECT);
        }

        //Otherwise maintenance is still in progress, so render the view for it
        return viewMaintenance(task, null);
    }

    // REST operations

    /*
     * Uses the token provided in the URL to verify the authenticity of the request so needs no XSRF protection
     *
     * consumes="!application/x-www-form-urlencoded" is required because Spring MVC was funnelling requests to this
     * REST method that should have been fulfilled by the form POST method of the same name above (no matter the
     * produces/consumes attributes on either).
     */
    @IgnoresXsrf //Is not called from a normal form, so it will not have an XSRF token. The 'token' param protects against XSRF
    @RequestMapping(method = RequestMethod.POST, params = "token", produces = MediaType.APPLICATION_JSON_VALUE,
                    consumes = "!" + MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public ResponseEntity<RestMaintenanceStatus> cancelMaintenance(@RequestParam(value = "token", required = true) String token) {
        MaintenanceTaskMonitor runningTask = maintenanceService.getRunningTask();
        if (runningTask == null) {
            return newResponse(HttpStatus.NOT_FOUND);
        }

        try {
            runningTask.cancel(token, cancelTimeout, TimeUnit.MINUTES);
            return newResponse(HttpStatus.ACCEPTED, new RestMaintenanceStatus(maintenanceService.getStatus()));
        } catch (IncorrectTokenMaintenanceException e) {
            return newResponse(HttpStatus.CONFLICT, new RestMaintenanceStatus(maintenanceService.getStatus()));
        }
    }

    @IgnoresXsrf //Is not called from a normal form, so it will not have an XSRF token. Consuming JSON mime type protects against XSRF
    @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE,
                    produces = MediaType.APPLICATION_JSON_VALUE, value = PATH_LOCK)
    public ResponseEntity<RestMaintenanceLock> lock() {
        MaintenanceLock context = maintenanceService.lock();

        //Include the unlockToken in the response immediately after locking. The caller has already been verified to
        //be a sysadmin or the lock() call would have thrown an exception, so showing the token is safe. They'll need
        //it later to be able to unlock the system, which will _also_ verify that the caller is a sysadmin
        return newResponse(HttpStatus.ACCEPTED, new RestMaintenanceLock(context, true));
    }

    @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE, value = PATH_LOCK)
    public ResponseEntity<RestMaintenanceLock> viewLockStatus() {
        MaintenanceLock lock = maintenanceService.getLock();
        if (lock == null) {
            return newResponse(HttpStatus.NOT_FOUND);
        }

        //When marshaling the lock status after the initial lock attempt, do not include the token. This endpoint
        return newResponse(HttpStatus.OK, new RestMaintenanceLock(lock, false));
    }

    @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<RestMaintenanceStatus> viewMaintenanceStatus() {
        return newResponse(HttpStatus.OK, new RestMaintenanceStatus(maintenanceService.getStatus()));
    }

    @Value("${maintenance.cancel.timeout}")
    void setCancelTimeout(long cancelTimeout) {
        if (cancelTimeout < 1) {
            //Don't support waiting forever. Note that this does _not_ prevent configuring a multi-hour wait
            log.warn("The configured cancel timeout, [{}] minutes, is too short and will be ignored. " +
                    "It will be defaulted to 1 minute", cancelTimeout);
            cancelTimeout = 1;
        }
        this.cancelTimeout = cancelTimeout;
    }

    private ModelAndView modelAndView(String viewName, Map<String, Object> data) {
        return new ModelAndView(new SoyView(viewName, soyTemplateRenderer, injectedDataFactory, StashSoyViewResolver.SERVER_SOY_MODULE_KEY), data);
    }

    private ModelAndView viewMaintenance(MaintenanceTaskMonitor runningTask, List<String> errors) {
        RequestContext requestContext = requestManager.getRequestContext();

        Map<String, Object> data = Maps.newHashMap();
        if (requestContext != null && runningTask.isOwner(requestContext)) {
            data.put("cancelToken", runningTask.getCancelToken());
        }
        if (CollectionUtils.isNotEmpty(errors)) {
            data.put("errorTexts", errors);
        }
        data.put("status", runningTask.getProgress());

        return modelAndView(chooseView(runningTask.getType()), data);
    }

    private static <T> ResponseEntity<T> newResponse(HttpStatus status) {
        return newResponse(status, null);
    }

    private static <T> ResponseEntity<T> newResponse(HttpStatus status, T result) {
        return new ResponseEntity<>(result, noCache(), status);
    }

    private static HttpHeaders noCache() {
        HttpHeaders headers = new HttpHeaders();
        headers.setCacheControl("no-cache");

        return headers;
    }

    private static String chooseRedirect(MaintenanceType type) {
        switch (type) {
            case MIGRATION:
                return VIEW_DB_REDIRECT;
        }

        return VIEW_HOME_REDIRECT;
    }

    private static String chooseView(MaintenanceType type) {
        switch (type) {
            case BACKUP:
                return VIEW_MAINTENANCE_BACKUP;
            case MIGRATION:
                return VIEW_MAINTENANCE_MIGRATION;
        }

        throw new IllegalArgumentException(type + " maintenance is not supported");
    }
}
