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

import com.atlassian.bitbucket.NoSuchEntityException;
import com.atlassian.bitbucket.Product;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.license.InvalidLicenseException;
import com.atlassian.bitbucket.license.LicenseService;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionValidationService;
import com.atlassian.bitbucket.rest.BadRequestException;
import com.atlassian.bitbucket.rest.RestErrors;
import com.atlassian.bitbucket.rest.license.RestBitbucketLicense;
import com.atlassian.bitbucket.rest.license.RestLicenseStatus;
import com.atlassian.bitbucket.rest.util.ResponseFactory;
import com.atlassian.bitbucket.rest.util.RestUtils;
import com.atlassian.bitbucket.server.ApplicationPropertiesService;
import com.atlassian.extras.api.bitbucket.BitbucketServerLicense;
import com.atlassian.plugins.rest.common.security.AnonymousAllowed;
import com.sun.jersey.spi.resource.Singleton;
import org.apache.commons.lang3.StringUtils;

import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

/**
 * A REST endpoint for retrieving or updating the license.
 *
 * @since 2.6
 */
@AnonymousAllowed
@Consumes({MediaType.APPLICATION_JSON})
@Path("admin/license")
@Produces({RestUtils.APPLICATION_JSON_UTF8})
@Singleton
public class LicenseResource {

    private final I18nService i18nService;
    private final LicenseService licenseService;
    private final ApplicationPropertiesService propertiesService;
    private final PermissionValidationService validationService;

    public LicenseResource(I18nService i18nService, LicenseService licenseService,
                           ApplicationPropertiesService propertiesService,
                           PermissionValidationService validationService) {
        this.i18nService = i18nService;
        this.licenseService = licenseService;
        this.propertiesService = propertiesService;
        this.validationService = validationService;
    }

    /**
     * Retrieves details about the current license, as well as the current status of the system with regards to the
     * installed license. The status includes the current number of users applied toward the license limit, as well
     * as any status messages about the license (warnings about expiry or user counts exceeding license limits).
     * <p>
     * The authenticated user must have <b>ADMIN</b> permission. Unauthenticated users, and non-administrators, are
     * not permitted to access license details.
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname license
     * @response.representation.200.example {@link RestBitbucketLicense#RESPONSE_EXAMPLE}
     * @response.representation.200.doc The currently-installed license.
     *
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions to view the
     * license, or the request is anonymous.
     *
     * @response.representation.404.mediaType application/json
     * @response.representation.404.qname errors
     * @response.representation.404.example {@link RestErrors#EXAMPLE}
     * @response.representation.404.doc No license has been installed.
     */
    @GET
    public Response get() {
        //The LicenseService does not apply any permission checks on getting the license (either as an object or as
        //a String), or any of the other methods called by getAndTransform(). Various plugins (UPM mainly) require
        //access to that data in unauthenticated and low-permission scenarios. However, just because such components
        //require unsecured access to the data _within the server_, that doesn't mean this REST endpoint should.
        //
        //For the LicenseController, since it is under /mvc/admin, Spring Security wil apply an ADMIN check at the
        //URL level. No similar check is performed for /rest/*/admin, though, so we need to do it manually
        validationService.validateForGlobal(Permission.ADMIN);

        return getAndTransform();
    }

    /**
     * Decodes the provided encoded license and sets it as the active license. If no license was provided, a 400 is
     * returned. If the license cannot be decoded, or cannot be applied, a 409 is returned. Some possible reasons a
     * license may not be applied include:
     * <ul>
     *     <li>It is for a different product</li>
     *     <li>It is already expired</li>
     * </ul>
     * Otherwise, if the license is updated successfully, details for the new license are returned with a 200 response.
     * <p>
     * <b>Warning</b>: It is possible to downgrade the license during update, applying a license with a lower number
     * of permitted users. If the number of currently-licensed users exceeds the limits of the new license, pushing
     * will be disabled until the licensed user count is brought into compliance with the new license.
     * <p>
     * The authenticated user must have <b>SYS_ADMIN</b> permission. <b>ADMIN</b> users may <i>view</i> the current
     * license details, but they may not <i>update</i> the license.
     *
     * @param restLicense a JSON payload containing the encoded license to apply
     *
     * @request.representation.mediaType application/json
     * @request.representation.example {@link RestBitbucketLicense#UPDATE_EXAMPLE}
     *
     * @response.representation.200.mediaType application/json
     * @response.representation.200.qname license
     * @response.representation.200.example {@link RestBitbucketLicense#RESPONSE_EXAMPLE}
     * @response.representation.200.doc The newly-installed license.
     *
     * @response.representation.400.mediaType application/json
     * @response.representation.400.qname errors
     * @response.representation.400.example {@link RestErrors#VALIDATION_EXAMPLE}
     * @response.representation.400.doc No encoded license was provided in the JSON body for the POST.
     *
     * @response.representation.401.mediaType application/json
     * @response.representation.401.qname errors
     * @response.representation.401.example {@link RestErrors#EXAMPLE}
     * @response.representation.401.doc The currently authenticated user has insufficient permissions to update the
     *                                  license.
     *
     * @response.representation.409.mediaType application/json
     * @response.representation.409.qname errors
     * @response.representation.409.example {@link RestErrors#EXAMPLE}
     * @response.representation.409.doc The encoded license could not be decoded, or it is not valid for use on this
     *                                  server. For example, it may be for a different product, or it may have already
     *                                  expired.
     */
    @POST
    public Response update(RestBitbucketLicense restLicense) {
        String license = restLicense.getLicense();
        if (StringUtils.isBlank(license)) {
            throw new BadRequestException(i18nService.getMessage("bitbucket.rest.license.nolicenseprovided"));
        }

        try {
            licenseService.set(license);
        } catch (InvalidLicenseException e) {
            //Something was unacceptable about the provided license. It may not be encoded correctly, it may already
            //be expired, or something else. The syntax of the request was correct, so a 400 isn't right. 409 seems
            //like the only code we can use here (though it still feels wrong-ish), and the exception message will be
            //returned as the body to allow the caller to (hopefully) provide a valid license.
            return ResponseFactory.status(Response.Status.CONFLICT)
                    .entity(new RestErrors(e))
                    .build();
        }

        return getAndTransform(); //Skip the permission check; calling LicenseService.update has already done it
    }

    /**
     * Retrieves the current {@link BitbucketServerLicense}, transforms it to a {@link RestBitbucketLicense} and appends status
     * information.
     * <p>
     * <i>No permissions are checked by this method.</i> All of the methods on the {@link LicenseService} that are
     * called here are <i>unsecured</i>. Calling methods are expected to validate the correct permissions.
     *
     * @return 200 OK with a {@link RestBitbucketLicense} body
     */
    private Response getAndTransform() {
        BitbucketServerLicense license = licenseService.get();
        if (license == null) {
            throw new NoSuchEntityException(i18nService.createKeyedMessage("bitbucket.rest.license.nolicenseset", Product.NAME));
        }

        //In addition to the license details, provide the status of the current license
        RestLicenseStatus status = new RestLicenseStatus(propertiesService.getServerId(),
                licenseService.getLicensedUsersCount(), licenseService.getOverLimitMessage(),
                licenseService.getValidityMessage());

        //Transform the license details, and then append the encoded license string and the current license status
        return ResponseFactory.ok(new RestBitbucketLicense(license, licenseService.getAsString(), status)).build();
    }
}
