package com.atlassian.stash.internal.build.rest;

import com.atlassian.bitbucket.build.*;
import com.atlassian.bitbucket.rest.BadRequestException;
import com.atlassian.bitbucket.rest.util.ResponseFactory;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.stash.internal.build.dao.AoBuildStatus;
import com.atlassian.stash.internal.build.util.OAuthHelper;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.rest.util.RestPage;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.UncheckedOperation;
import com.google.common.collect.Iterables;
import com.sun.jersey.spi.resource.Singleton;
import net.java.ao.schema.StringLength;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import static com.atlassian.stash.internal.build.rest.RestBuildStatus.REST_TRANSFORM;

@AnonymousAllowed
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON})
@Path("commits")
@Singleton
public class BuildStatusResource {

    private static final Pattern COMMIT_ID_PATTERN = Pattern.compile("[a-fA-F0-9]{40}");

    private final BuildStatusService buildStatusService;
    private final I18nService i18nService;
    private final OAuthHelper oauthHelper;

    public BuildStatusResource(BuildStatusService buildStatusService, I18nService i18nService, OAuthHelper oauthHelper) {
        this.buildStatusService = buildStatusService;
        this.i18nService = i18nService;
        this.oauthHelper = oauthHelper;
    }

    /**
     * Gets the build statuses associated with a commit.
     *
     * @param commitId full SHA1 of the commit (ex: <code>e00cf62997a027bbf785614a93e2e55bb331d268</code>)
     *
     * @response.representation.200.doc a page of build statuses associated with the commit
     *                                  (limited to the most recent 100 build statuses associated with the commit)
     * @response.representation.200.mediaType application/json
     * @response.representation.200.example {@link RestBuildStatus#PAGE_EXAMPLE}
     * @response.representation.401.doc The user is not authenticated or does not have the <strong>LICENSED</strong> permission.
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link #PERMISSION_ERROR_EXAMPLE}
     */
    @GET
    @Path("{commitId}")
    public Response getBuildStatus(@PathParam("commitId") String commitId) {
        Page<BuildStatus> page = buildStatusService.findAll(commitId);

        return ResponseFactory.ok(new RestPage<>(page, REST_TRANSFORM)).build();
    }

    /**
     * Gets statistics regarding the builds associated with a commit.
     *
     * @param commitId full SHA1 of the commit (ex: <code>e00cf62997a027bbf785614a93e2e55bb331d268</code>)
     * @param includeUnique include a unique build result if there is either only one failed build,
     *                      only one in-progress build or only one successful build
     *
     * @response.representation.200.doc The number of successful/failed/in-progress builds for the commit
     * @response.representation.200.mediaType application/json
     * @response.representation.200.example {@link RestBuildStats#EXAMPLE}
     * @response.representation.401.doc The user is not authenticated or does not have the <strong>LICENSED</strong> permission.
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link #PERMISSION_ERROR_EXAMPLE}
     */
    @GET
    @Path("stats/{commitId}")
    public Response getBuildStatusStats(@PathParam("commitId") String commitId,
                                        @QueryParam("includeUnique") @DefaultValue("false") boolean includeUnique) {
        BuildSummary stats = buildStatusService.getSummary(commitId);
        RestBuildStats restStats = new RestBuildStats(stats);

        // optionally include a unique build result if there is only one failed, in-progress or failed build
        if (includeUnique) {
            includeUniqueResult(restStats, commitId, stats);
        }

        return ResponseFactory.ok(restStats).build();
    }

    protected void includeUniqueResult(RestBuildStats restStats, String commitId, BuildSummary stats) {
        BuildState state = null;
        int failedCount = stats.getFailedCount();
        if (failedCount > 0) {
            if (failedCount == 1) {
                state = BuildState.FAILED;
            }
        } else {
            int inProgressCount = stats.getInProgressCount();
            if (inProgressCount > 0) {
                if (inProgressCount == 1) {
                    state = BuildState.INPROGRESS;
                }
            } else  {
                int successfulCount = stats.getSuccessfulCount();
                if (successfulCount == 1) {
                    state = BuildState.SUCCESSFUL;
                }
            }
        }

        if (state != null) {
            restStats.include(firstMatching(commitId, state));
        }
    }

    private BuildStatus firstMatching(String commitId, final BuildState state) {
        return Iterables.find(buildStatusService.findAll(commitId).getValues(), build -> build.getState() == state, null);
    }

    /**
     * Produces a list of the build statistics for multiple commits.
     * <p>
     * Commits <em>without any builds associated with them</em> will not be returned. For example if the
     * commit <code>e00cf62997a027bbf785614a93e2e55bb331d268</code> does not have any build statuses associated
     * with it, it will not be present in the response.
     *
     * @param commitIds full SHA1 of each commit
     *
     * @request.representation.mediaType application/json
     * @request.representation.example {@link #REQUEST_SUMMARY_EXAMPLE}
     *
     * @response.representation.200.doc The number of successful/failed/in-progress builds for each commit
     *                                  (with the caveat that the commits <em>without any builds associated with
     *                                  them</em> will not be present in the response)
     * @response.representation.200.mediaType application/json
     * @response.representation.200.example {@link RestMultipleBuildStats#EXAMPLE}
     * @response.representation.401.doc The user is not authenticated or does not have the <strong>LICENSED</strong> permission.
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link #PERMISSION_ERROR_EXAMPLE}
     */
    @POST
    @Path("stats")
    public Response getMultipleBuildStatusStats(List<String> commitIds) {
        Map<String, BuildSummary> stats = buildStatusService.getSummaries(commitIds);
        return ResponseFactory.ok(new RestMultipleBuildStats(stats)).build();
    }

    /**
     * Associates a build status with a commit.
     * <p>
     * The <code>state</code>, the <code>key</code> and the <code>url</code> are mandatory. The <code>name</code> and
     * <code>description</code> fields are optional.
     * <p>
     * All fields (mandatory or optional) are limited to 255 characters, except for the <code>url</code>,
     * which is limited to 450 characters.
     * <p>
     * Supported values for the <code>state</code> are <code>SUCCESSFUL</code>, <code>FAILED</code>
     * and <code>INPROGRESS</code>.
     * <p>
     * The authenticated user must have <strong>LICENSED</strong> permission or higher to call this resource.
     *
     *
     * @param commitId full SHA1 of the commit (ex: <code>e00cf62997a027bbf785614a93e2e55bb331d268</code>)
     * @param status build status to associate with the commit
     *
     * @request.representation.mediaType application/json
     * @request.representation.example {@link #REQUEST_EXAMPLE}
     *
     * @response.representation.204.doc An empty response if the build status was successfully stored
     * @response.representation.400.doc An error message if the <code>commitId</code> if not a full 40-characters SHA1,
     * if the build status has a missing mandatory field or if the fields are too large
     * @response.representation.400.example {@link #VALIDATION_ERROR_EXAMPLE}
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.401.doc The user is not authenticated or does not have the <strong>LICENSED</strong> permission.
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link #PERMISSION_ERROR_EXAMPLE}
     *
     * @see BuildState
     */
    @POST
    @Path("{commitId}")
    public Response addBuildStatus(@PathParam("commitId") final String commitId, final RestBuildStatus status) {
        validateCommitId(commitId);
        validate(status);

        oauthHelper.withPermissionIfAnonymous2LO(Permission.LICENSED_USER, new UncheckedOperation<Void>() {
            @Override
            public Void perform() {
                buildStatusService.set(new BuildStatusSetRequest.Builder(commitId)
                        .dateAdded(status.getDateAdded())
                        .description(status.getDescription())
                        .key(status.getKey())
                        .name(status.getName())
                        .state(status.getState())
                        .url(status.getUrl())
                        .build());
                return null;
            }
        });
        return ResponseFactory.noContent().build();
    }

    private void validateCommitId(String commitId) {
        if (!COMMIT_ID_PATTERN.matcher(commitId).matches()) {
            String message = i18nService.getMessage("bitbucket.rest.build.status.commit.idinvalid");
            throw new BadRequestException(message);
        }
    }

    private void validate(RestBuildStatus status) {
        List<String> errors = new ArrayList<>();
        if (!hasValidState(status)) {
            errors.add(i18nService.getMessage("bitbucket.rest.build.status.state.required", ArrayUtils.toString(BuildState.values())));
        }
        if (StringUtils.isBlank(status.getKey())) {
            errors.add(i18nService.getMessage("bitbucket.rest.build.status.key.required"));
        }
        if (StringUtils.length(status.getKey()) > AoBuildStatus.MAX_FIELDS) {
            errors.add(i18nService.getMessage("bitbucket.rest.build.status.key.oversized", AoBuildStatus.MAX_FIELDS));
        }
        if (StringUtils.length(status.getName()) > AoBuildStatus.MAX_FIELDS) {
            errors.add(i18nService.getMessage("bitbucket.rest.build.status.name.oversized", AoBuildStatus.MAX_FIELDS));
        }
        if (!isValidUrl(status.getUrl())) {
            errors.add(i18nService.getMessage("bitbucket.rest.build.status.url.required"));
        }
        if (StringUtils.length(status.getUrl()) > StringLength.MAX_LENGTH) {
            errors.add(i18nService.getMessage("bitbucket.rest.build.status.url.oversized", StringLength.MAX_LENGTH));
        }
        if (!errors.isEmpty()) {
            throw new BadRequestException(errors);
        }
    }

    private boolean hasValidState(RestBuildStatus status) {
        try {
            BuildState state = status.getState();

            return state != null; //If no state was specified, the returned state will be null
        } catch (IllegalArgumentException e) {
            //If an invalid state was specified, Enum.valueOf will throw an exception
            return false;
        }
    }

    private boolean isValidUrl(String url) {
        if (url == null) {
            return false;
        }

        try {
            new URL(url);
            return true;
        } catch (MalformedURLException e) {
            return false;
        }
    }

    // REST Documentation Examples

    @SuppressWarnings("UnusedDeclaration")
    private static final RestBuildStatus REQUEST_EXAMPLE = new RestBuildStatus(BuildState.SUCCESSFUL, "REPO-MASTER", "REPO-MASTER-42",
            "https://bamboo.example.com/browse/REPO-MASTER-42", "Changes by John Doe", null);

    @SuppressWarnings({"UnusedDeclaration", "MismatchedReadAndWriteOfArray"})
    private static final String[] REQUEST_SUMMARY_EXAMPLE = {
            "db0dd118fa6ccdf1d593fef00df839dd27702df7",
            "99e9634a6d1dbb2e0c12d7c22aad6c0f66f1c489",
            "6f6cf2f1afa443b03de1e586ee6ece98f4b46633",
            "29357b5a57a0a1149dc5e7761fd89b02d22353c4",
            "056b647404c611003fe260e9766cd5facfdc7b3a",
            "c2d9e5b9e07f90bf584b2f5aeceed5a78c68a7ce",
            "bde37280e62fcc2b1151b50fe91f387d1e84abee",
            "dc56b0f2ce65245fa8ddd67711765a915602541f",
            "290998570c81dfc4c2d9152391ecfe2b752ac8c8",
            "a464d845c05da15fec9d5580715ada7af56b8772",
            "4d0cfc66f56e05cc86127a4e5e1a88c32c689af8"
    };

    @SuppressWarnings("UnusedDeclaration")
    private static final RestErrors PERMISSION_ERROR_EXAMPLE = new RestErrors("You are not permitted to access this resource");

    @SuppressWarnings("UnusedDeclaration")
    private static final RestErrors VALIDATION_ERROR_EXAMPLE = new RestErrors("Please specify a non-blank key");
}
