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

import com.atlassian.bitbucket.*;
import com.atlassian.bitbucket.project.*;
import com.atlassian.soy.springmvc.errors.DetailedError;
import com.atlassian.bitbucket.avatar.CacheableAvatarSupplier;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.stash.internal.avatar.InternalAvatarService;
import com.atlassian.stash.internal.project.InternalProjectService;
import com.atlassian.stash.internal.web.soy.StashSoyResponseBuilder;
import com.atlassian.stash.internal.web.util.AvatarSupport;
import com.atlassian.stash.internal.web.util.StashWebErrorUtils;
import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.repository.RepositoryService;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionService;
import com.atlassian.bitbucket.permission.PermissionValidationService;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequestImpl;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.view.RedirectView;

import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import javax.validation.Valid;
import java.io.IOException;
import java.util.List;

import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST;

@Controller
@RequestMapping("/projects")
public class ProjectAdminController extends AvatarSupport {

    public static final String PROJECT_CREATE = "bitbucket.internal.page.createProject";
    public static final String PROJECT_EDIT_SETTINGS = "bitbucket.internal.project.projectSettings";
    public static final String PROJECT_LIST = "bitbucket.internal.page.projectList";

    private final InternalAvatarService avatarService;
    private final NavBuilder navBuilder;
    private final PermissionService permissionService;
    private final PermissionValidationService permissionValidationService;
    private final InternalProjectService projectService;
    private final RepositoryService repositoryService;

    @Autowired
    public ProjectAdminController(InternalAvatarService avatarService, I18nService i18nService,
                                  NavBuilder navBuilder, PermissionService permissionService,
                                  PermissionValidationService permissionValidationService,
                                  InternalProjectService projectService, RepositoryService repositoryService) {
        super(i18nService);

        this.avatarService = avatarService;
        this.navBuilder = navBuilder;
        this.permissionService = permissionService;
        this.permissionValidationService = permissionValidationService;
        this.projectService = projectService;
        this.repositoryService = repositoryService;
    }

    @RequestMapping(params = "create", method = POST)
    public ModelAndView createProject(@Valid ProjectForm form, Errors errors) {
        StashSoyResponseBuilder responseBuilder = new StashSoyResponseBuilder(PROJECT_CREATE);
        List<DetailedError> detailedErrors = Lists.newArrayList();
        if (!errors.hasErrors()) {
            try {
                //Note: If an avatar is provided, this version of ProjectService.create will _fail_, not creating the
                //      project, if the avatar data cannot be correctly parsed from the data URI
                Project project = projectService.create(new ProjectCreateRequest.Builder()
                        .key(form.getKey())
                        .name(form.getName())
                        .description(form.getDescription())
                        .avatarUri(form.getAvatar())
                        .build());

                return new ModelAndView(new RedirectView(navBuilder.project(project.getKey()).buildRelNoContext(), true));
            } catch (RequestCanceledException e) {
                detailedErrors.add(StashWebErrorUtils.toDetailedError(e));
            } catch (ServiceException e) {
                rejectException(errors, e);
            } catch (ConstraintViolationException e) {
                rejectException(errors, e);
            }
        }

        return responseBuilder
                .putValidationErrors(errors)
                .putDetailedErrors(detailedErrors)
                .put("projectForm", form)
                .build();
    }

    @RequestMapping(params = "create", method = GET)
    public ModelAndView createProjectView() {
        if (permissionService.hasGlobalPermission(Permission.PROJECT_CREATE)) {
            return new StashSoyResponseBuilder(PROJECT_CREATE).build();
        } else {
            throw new AccessDeniedException(i18nService.getMessage("bitbucket.web.project.create.accessdenied"));
        }
    }

    @RequestMapping(value = "{projectKey}", params = "delete", method = POST)
    public RedirectView deleteProject(@PathVariable("projectKey") String projectKey) {
        Project project = projectService.getByKey(projectKey);
        if (project == null || !projectService.delete(project)) {
            throw newNoSuchProjectException(projectKey);
        }
        return new RedirectView(navBuilder.allProjects().buildRelNoContext(), true);
    }

    @RequestMapping(value = "{projectKey}/settings", method = GET)
    public ModelAndView editProject(@PathVariable("projectKey") String projectKey) {
        Project project = projectService.getByKey(projectKey);
        if (project == null) {
            throw newNoSuchProjectException(projectKey);
        }
        if (project.getType() == ProjectType.PERSONAL) {
            throw new IntegrityException(i18nService.createKeyedMessage("bitbucket.web.project.edit.personalunmodifiable"));
        }
        if (permissionService.hasProjectPermission(project, Permission.PROJECT_ADMIN)) {
            return new StashSoyResponseBuilder(PROJECT_EDIT_SETTINGS)
                    .putProject(project)
                    .build();
        }

        throw new AccessDeniedException(i18nService.getMessage("bitbucket.web.project.edit.accessdenied", project.getName()));
    }

    @RequestMapping(method = GET)
    public ModelAndView getProjects(@RequestParam(value = "start", required = false, defaultValue = "0") int start,
                                    @RequestParam(value = "limit", required = false, defaultValue = "100") int limit) {

        Page<? extends Project> projects = projectService.findAll(new PageRequestImpl(start, limit));
        validatePage(projects);

        return new StashSoyResponseBuilder(PROJECT_LIST)
                .put("projectPage", projects)
                .build();
    }

    @RequestMapping(value = "{projectKey}", method = GET)
    public ModelAndView getProject(@PathVariable("projectKey") String projectKey,
                                   @RequestParam(value = "start", required = false, defaultValue = "0") int start,
                                   @RequestParam(value = "limit", required = false, defaultValue = "100") int limit) {

        Project project = projectService.getByKey(projectKey);
        if (project == null) {
            if (projectService.isPersonalProject(projectKey)) {
                //If the user is trying to access their personal project and it doesn't already exist, silently
                //create it for them. This way, only users who try to use their personal projects will actually
                //have one in the system (reducing load on the project table)
                project = projectService.getPersonalProject();
            } else {
                throw newNoSuchProjectException(projectKey);
            }
        }

        ApplicationUser owner = project.accept(new AbstractProjectVisitor<ApplicationUser>() {
            @Override
            public ApplicationUser visit(@Nonnull PersonalProject project) {
                return project.getOwner();
            }
        });
        if (owner != null) {
            //If the user is trying to access their personal project redirect to their profile page
            return new ModelAndView("redirect:" + navBuilder.user(owner).buildRelNoContext());
        } else {
            Page<? extends Repository> repositoryPage = repositoryService.findByProjectKey(project.getKey(),
                    new PageRequestImpl(start, limit));

            return new StashSoyResponseBuilder("bitbucket.internal.project.projectOverview")
                    .putProject(project)
                    .put("repositoryPage", repositoryPage)
                    .build();
        }
    }

    @ExceptionHandler(NoSuchEntityException.class)
    public ModelAndView handleNoSuchProject(NoSuchEntityException exception, HttpServletResponse response) {
        response.setStatus(HttpServletResponse.SC_NOT_FOUND);
        return new ModelAndView("bitbucket.internal.errors.project404", "error", exception);
    }

    @RequestMapping(value = "{projectKey}/avatar.png", method = GET, produces = MediaType.IMAGE_PNG_VALUE)
    public void retrieveAvatar(@PathVariable("projectKey") String projectKey,
                               @RequestParam(defaultValue = "256", value = "s") int size,
                               HttpServletRequest request,
                               HttpServletResponse response) throws IOException {
        Project project;
        try {
            project = projectService.getByKey(projectKey);
        } catch (AuthorisationException e) {
            // We always want to render an avatar regardless of permissions - just show a hard-coded one
            project = null;
        }

        CacheableAvatarSupplier supplier = project == null ?
                avatarService.getProjectDefault(size) :
                projectService.getAvatar(project.getId(), size);

        streamAvatar(supplier, request, response);
    }

    @RequestMapping(value = "{projectKey}/settings", method = POST)
    public ModelAndView updateProject(@PathVariable("projectKey") String projectKey,
                                      @Valid ProjectForm form, Errors errors,
                                      RedirectAttributes redirectAttributes) {
        Project project = projectService.getByKey(projectKey);
        if (project == null) {
            throw newNoSuchProjectException(projectKey);
        }

        StashSoyResponseBuilder responseBuilder = new StashSoyResponseBuilder(PROJECT_EDIT_SETTINGS);
        List<DetailedError> detailedErrors = Lists.newArrayList();

        if (!errors.hasErrors()) {
            try {
                //Note: This separates updating the project's details in the database from updating its avatar on disk.
                //      If the avatar cannot be updated, the project's details still may be.
                project = projectService.update(new ProjectUpdateRequest.Builder(project)
                        .key(form.getKey())
                        .name(form.getName())
                        .description(form.getDescription())
                        .build());
                if (form.hasAvatar()) {
                    projectService.updateAvatar(project.getId(), form.getAvatar());
                }

                redirectAttributes.addFlashAttribute("isEditSuccess", true);
                return new ModelAndView("redirect:" + navBuilder.project(project.getKey()).settings().buildRelNoContext());
            } catch (RequestCanceledException e) {
                detailedErrors.add(StashWebErrorUtils.toDetailedError(e));
            } catch (ServiceException e) {
                rejectException(errors, e);
            } catch (ConstraintViolationException e) {
                rejectException(errors, e);
            }
        }
        return responseBuilder
                .putValidationErrors(errors)
                .putDetailedErrors(detailedErrors)
                .putProject(project)
                .put("projectForm", form)
                .build();
    }

    private void validatePage(Page<?> page) {
        if (page.getIsLastPage() && page.getSize() == 0 && page.getStart() == 0) {
            // Ensure there is an authenticated user. This means that if the page
            // is empty for anon users they get redirected to the login screen
            permissionValidationService.validateAuthenticated();
        }
    }
}
