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

import com.atlassian.bitbucket.Product;
import com.atlassian.bitbucket.ServerException;
import com.atlassian.bitbucket.ServiceException;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.jira.*;
import com.atlassian.bitbucket.license.LicenseException;
import com.atlassian.bitbucket.license.LicenseService;
import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.permission.Permission;
import com.atlassian.bitbucket.permission.PermissionAdminService;
import com.atlassian.bitbucket.permission.SetPermissionRequest;
import com.atlassian.bitbucket.server.ApplicationMode;
import com.atlassian.bitbucket.ui.PluginFormFragments;
import com.atlassian.bitbucket.ui.PluginFormFragmentsFactory;
import com.atlassian.bitbucket.user.*;
import com.atlassian.bitbucket.util.DevModeUtils;
import com.atlassian.bitbucket.util.UrlUtils;
import com.atlassian.bitbucket.validation.HttpURLValidator;
import com.atlassian.crowd.exception.CrowdException;
import com.atlassian.soy.springmvc.errors.ErrorUtils;
import com.atlassian.soy.springmvc.errors.WebErrorUtils;
import com.atlassian.stash.internal.ApplicationConstants;
import com.atlassian.stash.internal.db.DbType;
import com.atlassian.stash.internal.db.DbTypeBean;
import com.atlassian.stash.internal.i18n.LocaleHolder;
import com.atlassian.stash.internal.jdbc.DataSourceConfiguration;
import com.atlassian.stash.internal.jdbc.SimpleDataSourceConfiguration;
import com.atlassian.stash.internal.language.InternalLanguageService;
import com.atlassian.stash.internal.maintenance.MaintenanceCompletionCallback;
import com.atlassian.stash.internal.maintenance.MaintenanceTaskMonitor;
import com.atlassian.stash.internal.migration.MigrationException;
import com.atlassian.stash.internal.migration.MigrationService;
import com.atlassian.stash.internal.plugin.PluginValidationErrors;
import com.atlassian.stash.internal.server.InternalApplicationPropertiesService;
import com.atlassian.stash.internal.user.InternalPermissionService;
import com.atlassian.stash.internal.web.admin.DbConfigForm;
import com.atlassian.stash.internal.web.auth.IgnoresXsrf;
import com.atlassian.stash.internal.web.locale.UserLocaleWebUtils;
import com.atlassian.stash.internal.web.soy.StashSoyResponseBuilder;
import com.atlassian.stash.internal.web.util.ControllerSupport;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ListMultimap;
import org.apache.commons.lang3.LocaleUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Conventions;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;

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

@Controller
@RequestMapping("/setup")
public class SetupController extends ControllerSupport {

    // Used to determine whether the database has been setup. the global isSetup() flag is actually stored in the database
    // so we can't use that
    static final String HAS_SETUP_DB_NAME = Conventions.getQualifiedAttributeName(SetupController.class, "hasSetupDatabase");

    static final String DATABASE_VIEW = "bitbucket.internal.page.setup.database";
    static final String LICENSE_AND_SETTINGS_VIEW = "bitbucket.internal.page.setup.settings";
    static final String ADMIN_USER_VIEW = "bitbucket.internal.page.setup.adminUser";
    static final String JIRA_INTEGRATION_VIEW = "bitbucket.internal.page.setup.jiraIntegration";
    static final String MIRROR_VIEW = "bitbucket.internal.page.setup.mirror";
    static final String JIRA_UPM_INSTALL_URL_SUFFIX = "/plugins/servlet/upm#upgrade";
    static final String FIELD_BASE_URL = "baseUrl";

    private final EscalatedSecurityContext asSysAdmin;
    private final JiraSetupService jiraSetupService;
    private final InternalLanguageService languageService;
    private final LicenseService licenseService;
    private final LocaleHolder localeHolder;
    private final MigrationService migrationService;
    private final NavBuilder navBuilder;
    private final PermissionAdminService permissionAdminService;
    private final InternalPermissionService permissionService;
    private final InternalApplicationPropertiesService propertiesService;
    private final UserAdminService userAdminService;
    private final UserService userService;
    private final PluginFormFragmentsFactory formFragmentsFactory;

    @Autowired
    public SetupController(I18nService i18nService, JiraSetupService jiraSetupService,
                           InternalLanguageService languageService, LicenseService licenseService,
                           LocaleHolder localeHolder, MigrationService migrationService, NavBuilder navBuilder,
                           PermissionAdminService permissionAdminService, InternalPermissionService permissionService,
                           InternalApplicationPropertiesService propertiesService, SecurityService securityService,
                           UserAdminService userAdminService, UserService userService,
                           PluginFormFragmentsFactory formFragmentsFactory) {
        super(i18nService);

        this.jiraSetupService = jiraSetupService;
        this.languageService = languageService;
        this.licenseService = licenseService;
        this.localeHolder = localeHolder;
        this.migrationService = migrationService;
        this.navBuilder = navBuilder;
        this.permissionAdminService = permissionAdminService;
        this.permissionService = permissionService;
        this.propertiesService = propertiesService;
        this.userAdminService = userAdminService;
        this.userService = userService;
        this.formFragmentsFactory = formFragmentsFactory;

        asSysAdmin = securityService.withPermission(Permission.SYS_ADMIN, "SetupController");
    }

    @RequestMapping(method = GET)
    public ModelAndView setup(HttpSession session) {
        if (propertiesService.isSetup()) {
            return redirectToRoot();
        }

        ApplicationMode mode = propertiesService.getMode();
        switch (mode) {
            case DEFAULT:
                return setupDefault(session);
            case MIRROR:
                return setupMirrorView();
            default:
                log.error("Unexpected application mode '{}' reported.", mode);
                throw new IllegalStateException(String.format("Unexpected application mode '%s' reported.", mode));
        }
    }

    private ModelAndView setupDefault(HttpSession session) {
        if (!hasSetupDatabase(session)) {
            return getDatabaseForm(DbTypeBean.DEFAULT, null, true).build();
        }
        if (!hasLicenseAndBaseUrl()) {
            return getSettingsForm(new SettingsForm()).build();
        }

        return hasAdminUser() ? getJiraForm().build() : getAdminUserForm().build();
    }

    private ModelAndView setupMirrorView() {
        MirrorForm form = new MirrorForm();

        form.setApplicationTitle(propertiesService.getDisplayName());
        URI baseUrl = propertiesService.getBaseUrl();
        if (baseUrl == null) {
            form.setBaseUrl(navBuilder.buildAbsolute());
        } else {
            form.setBaseUrl(baseUrl);
        }
        return createMirrorView(form, null, getMirrorPluginFragments().getViewHtml());
    }

    private ModelAndView createMirrorView(MirrorForm form, Errors errors, String pluginHtml) {
        boolean sslError = isSslError(form.getBaseUrl());
        ListMultimap<String, Object> fieldErrors = ArrayListMultimap.create();
        Collection<String> formErrors = ImmutableList.of();
        if (errors != null) {
            WebErrorUtils.toFieldErrorMap(errors.getFieldErrors()).forEach(fieldErrors::putAll);
            formErrors = WebErrorUtils.toFormErrors(errors.getGlobalErrors());
        }
        if (sslError && !fieldErrors.containsKey(FIELD_BASE_URL)) {
            fieldErrors.put(FIELD_BASE_URL, i18nService.getMessage("bitbucket.web.setup.mirror.baseUrl.https.error"));
        }
        return setupSoyResponseBuilder(MIRROR_VIEW)
                .put("form", form)
                .put("pluginHtml", pluginHtml)
                .put("sslError", sslError)
                .put("fieldErrors", fieldErrors.asMap())
                .put("formErrors", formErrors)
                .build();
    }

    private PluginFormFragments getMirrorPluginFragments() {
        return formFragmentsFactory.forKey("bitbucket.mirror.setup", ImmutableMap.of());
    }

    private StashSoyResponseBuilder getAdminUserForm() {
        return setupSoyResponseBuilder(ADMIN_USER_VIEW);
    }

    private StashSoyResponseBuilder getDatabaseForm(DbTypeBean currentDbType, DbConfigForm form, boolean isInternal) {
        return setupSoyResponseBuilder(DATABASE_VIEW)
                .put("currentDbType", currentDbType)
                .put("dbTypes", DbTypeBean.ALL)
                .put("dcName", Product.DATA_CENTER_NAME)
                .put("form", form)
                .put("isInternal", isInternal)
                .put("languages", UserLocaleWebUtils.toSoy(languageService.getLanguages(), localeHolder.getLocales()));
    }

    private StashSoyResponseBuilder getSettingsForm(SettingsForm form) {
        String license = licenseService.getAsString();
        if (StringUtils.isNotBlank(license)) {
            form.setLicense(license);
        }

        form.setBaseUrl(propertiesService.getBaseUrl());
        form.setApplicationTitle(propertiesService.getDisplayName());

        return setupSoyResponseBuilder(LICENSE_AND_SETTINGS_VIEW)
                .put("createAccountAndGenerateEvalUrl", getCreateAccountAndGenerateEvalUrl())
                .put("form", form)
                .put("generateEvalUrl", getGenerateEvalUrl())
                .put("serverId", propertiesService.getServerId());
    }

    private StashSoyResponseBuilder getJiraForm() {
        URI baseUrl = propertiesService.getBaseUrl(); // note: the base URL should not be null as we set it in the initial screen (but check anyways)
        return setupSoyResponseBuilder(JIRA_INTEGRATION_VIEW)
                .put("form", new JiraIntegrationForm(baseUrl == null ? null : baseUrl.toASCIIString()));
    }

    @RequestMapping(params = "step=database", method = POST)
    public ModelAndView configureDatabase(
            HttpServletRequest request,
            HttpSession session,
            @RequestParam(value = "internal", defaultValue = "true") boolean isInternal,
            @RequestParam(value = "locale", required = false) String locale,
            @Valid DbConfigForm form, Errors errors) {
        checkInDefaultModeAndSetupNotComplete();

        // Set locale, even when data is not complete, to ensure it shows again
        setLocale(locale);

        if (hasSetupDatabase(session)) {
            return showNextStep();
        }

        DbTypeBean currentDbType = DbTypeBean.forKey(form.getType());
        if (currentDbType == null) {
            errors.rejectValue("type", "bitbucket.web.setup.database.type.required", "You must select database type");
        }
        if (!isInternal && errors.hasErrors()) {
            return getDatabaseForm(currentDbType, form, false)
                    .putValidationErrors(errors)
                    .build();
        }

        if (isInternal) {
            markDatabaseAsSetup(session);
        } else {
            //noinspection ConstantConditions
            String jdbcUrl = currentDbType.generateUrl(form.getHostname(), form.getDatabase(), form.getPortAsInteger());
            DataSourceConfiguration configuration = new SimpleDataSourceConfiguration(
                    currentDbType.getDriverClassName(), jdbcUrl, form.getUsername(), form.getPassword());

            try {
                // if the test button was clicked, only test the configuration, don't perform the setup
                if (request.getParameter("test") != null) {
                    performTestDatabaseConnection(configuration);
                    return getDatabaseForm(currentDbType, form, false)
                            .put("testPassed", true)
                            .build();
                } else {
                    performDatabaseSetup(session, configuration);
                }

            } catch (MigrationException e) {
                // we specially handle migration exceptions by displaying the stacktrace in the UI
                return getDatabaseForm(currentDbType, form, false)
                        .put("migrationException", e)
                        .build();
            } catch (ServiceException e) {
                return getDatabaseForm(currentDbType, form, false)
                        .putFormErrors(e)
                        .build();
            }
        }
        return showNextStep();
    }

    private void performTestDatabaseConnection(final DataSourceConfiguration configuration) {
        asSysAdmin.call(() -> {
            migrationService.validateConfiguration(configuration);
            return null;
        });
    }

    private void performDatabaseSetup(final HttpSession session, final DataSourceConfiguration configuration) {
        MaintenanceTaskMonitor task = asSysAdmin.call(() -> migrationService.setup(configuration));

        // We need to add the listener after we set the atomic boolean to prevent race conditions
        task.registerCallback(new MaintenanceCompletionCallback() {
            @Override
            public void onCancellation() {
                log.info("Database setup was canceled");
            }

            @Override
            public void onSuccess() {
                markDatabaseAsSetup(session);
            }

            @Override
            public void onFailure(@Nonnull Throwable t) {
                log.error("An exception occurred while setting up the database", t);
            }
        });

        try {
            task.awaitCompletion();
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            log.error("An unexpected exception occurred while setting up the database", e);
        }
    }


    @IgnoresXsrf // This allows my.atlassian.com to come back with an evaluation license
    @RequestMapping(method = POST)
    public ModelAndView callbackForMac(@Valid SettingsForm form, Errors errors) {
        checkInDefaultModeAndSetupNotComplete();

        return getSettingsForm(form)
                .build();
    }

    @RequestMapping(params = "step=settings", method = POST)
    public ModelAndView enterSettings(@Valid SettingsForm form, Errors errors) {
        checkInDefaultModeAndSetupNotComplete();

        if (errors.hasErrors()) {
            return getSettingsForm(form)
                    .putValidationErrors(errors)
                    .build();
        }
        try {
            // set the application title and base URL
            setNameAndBaseUrl(form.getApplicationTitle(), form.getBaseUrl());

            // set the license
            setLicense(form.getLicense());
        } catch (LicenseException e) {
            Map<String, ? extends Iterable<String>> fieldErrors =
                    Collections.singletonMap(SettingsForm.LICENSE_FIELD_NAME,
                            Collections.singletonList(e.getLocalizedMessage()));
            return getSettingsForm(form)
                    .putFieldErrors(fieldErrors)
                    .build();
        } catch (ServiceException e) {
            return getSettingsForm(form)
                    .putFormErrors(e.getLocalizedMessage())
                    .build();
        }

        return showNextStep();
    }

    @RequestMapping(params = "step=user", method = POST)
    public ModelAndView addFirstAdminUser(@Valid AdminUserForm form, Errors errors) {
        checkInDefaultModeAndSetupNotComplete();

        if (errors.hasErrors()) {
            return showForm(ADMIN_USER_VIEW, form, errors);
        } else {
            try {
                createAdminUser(form);
            } catch (Exception e) {
                return showForm(ADMIN_USER_VIEW, form, null, ErrorUtils.getLocalizedMessageOfRootCause(e));
            }
        }

        // if the user's elected not to integrate with JIRA, setup is complete
        if (!form.isIntegrateWithJira()) {
            markSetupAsComplete();
        }

        return showNextStep();
    }

    @RequestMapping(params = "step=jira", method = POST)
    public ModelAndView setupJiraIntegration(@Valid JiraIntegrationForm form, Errors errors) {
        checkInDefaultModeAndSetupNotComplete();

        if (!form.isSkip()) {
            if (errors.hasErrors()) {
                return showForm(JIRA_INTEGRATION_VIEW, form, errors);
            } else {
                try {
                    // attempt to integrate with JIRA
                    integrateWithJira(form);
                } catch (NotAJiraServerException e) {
                    // the specified URL doesn't appear to be a v4.3+ JIRA server
                    errors.rejectValue("jiraBaseUrl", "bitbucket.web.setup.stash.bad.jira.url", e.getLocalizedMessage());
                    return showForm(JIRA_INTEGRATION_VIEW, form, errors);
                } catch (JiraUserCredentialsException e) {
                    // it looks like the Stash ApplicationType has not been installed in the JIRA instance, prompt
                    // the user to install the Stash UAL types plugin in JIRA
                    return setupSoyResponseBuilder(JIRA_INTEGRATION_VIEW)
                            .put("form", form)
                            .put("credentialsProblem", true)
                            .build();
                } catch (IncorrectApplicationBaseUrlException e) {
                    // the base URL for Stash that the user supplied was not reachable
                    errors.rejectValue("stashBaseUrl", "bitbucket.web.setup.stash.bad.stash.url", e.getLocalizedMessage());
                    return showForm(JIRA_INTEGRATION_VIEW, form, errors);
                } catch (ApplicationTypeNotInstalledException e) {
                    // it looks like the Stash ApplicationType has not been installed in the JIRA instance, prompt
                    // the user to install the Stash UAL types plugin in JIRA
                    return setupSoyResponseBuilder(JIRA_INTEGRATION_VIEW)
                            .put("form", form)
                            .put("stashUalTypesMissing", true)
                            .put("jiraUpmUrl", UrlUtils.concatenate(form.getJiraBaseUrl(), JIRA_UPM_INSTALL_URL_SUFFIX))
                            .build();
                } catch (JiraSetupException e) {
                    // some other problem occurred, display the error message with a link to the troubleshooting guide
                    return setupSoyResponseBuilder(JIRA_INTEGRATION_VIEW)
                            .put("form", form)
                            .put("jiraIntegrationError", e.getLocalizedMessage())
                            .build();
                }
            }
        }

        markSetupAsComplete();
        return redirectToRoot();
    }

    @RequestMapping(params = "step=mirror", method = POST)
    public ModelAndView setupMirror(@Valid MirrorForm form, Errors errors, HttpServletRequest request) {
        checkInMirrorModeAndSetupNotComplete();

        PluginFormFragments mirrorPluginFragments = getMirrorPluginFragments();
        PluginValidationErrors pluginErrors = new PluginValidationErrors(errors);
        Map<String, String[]> parameterMap = request.getParameterMap();
        mirrorPluginFragments.validate(parameterMap, pluginErrors);

        if (pluginErrors.hasErrors() || isSslError(form.getBaseUrl())) {
            return createMirrorView(form, errors, mirrorPluginFragments.getErrorHtml(parameterMap, pluginErrors.getFieldErrors()));
        } else {
            setNameAndBaseUrl(form.getApplicationTitle(), form.getBaseUrl());
            mirrorPluginFragments.execute(parameterMap);
            markSetupAsComplete();

            return new ModelAndView("redirect:/mirror?setup=true");
        }
    }

    private boolean hasSetupDatabase(HttpSession session) {
        return Boolean.TRUE.equals(session.getAttribute(HAS_SETUP_DB_NAME)) ||
                !DbType.isInternal(propertiesService.getJdbcDriver());
    }

    /**
     * Since the existence of the unattended setup all the properties in a page are independently available and must
     * be checked
     */
    private boolean hasLicenseAndBaseUrl() {
        return licenseService.isPresent() && propertiesService.getBaseUrl() != null;
    }

    private void setNameAndBaseUrl(String name, String baseURL) {
        asSysAdmin.call(() -> {
            propertiesService.setDisplayName(name);
            propertiesService.setBaseURL(UrlUtils.uncheckedCreateURI(baseURL));
            return null;
        });
    }

    private void setLicense(String license) {
        asSysAdmin.call(() -> {
            licenseService.set(license);
            return null;
        });
    }

    private void setLocale(String locale) {
        asSysAdmin.call(() -> {
            propertiesService.setLocale(LocaleUtils.toLocale(locale));
            return null;
        });
    }

    private boolean hasAdminUser() {
        // check if there is at least one sysadmin
        return asSysAdmin.call(() -> !permissionService.getUsersWithPermission(Permission.SYS_ADMIN).isEmpty());
    }

    private void createAdminUser(AdminUserForm form) throws CrowdException {
        // create user & grant permission as sysadmin
        asSysAdmin.call(() -> {
            // create the user in Crowd
            userAdminService.createUser(form.getUsername(), form.getPassword(), form.getFullname(), form.getEmail());

            // check that the user was created in Stash
            ApplicationUser user = userService.getUserByName(form.getUsername());
            if (user == null) {
                throw new ServerException(i18nService.createKeyedMessage("bitbucket.web.setup.cannotcreateuser", form.getUsername(), ApplicationConstants.PRODUCT_NAME));
            }

            permissionAdminService.setPermission(new SetPermissionRequest.Builder().globalPermission(Permission.SYS_ADMIN).user(user).build());
            return null;
        });
    }

    private void integrateWithJira(JiraIntegrationForm form) throws JiraSetupException {
        asSysAdmin.call(() -> {
            try {
                jiraSetupService.createLink(
                        new URI(form.getJiraBaseUrl()), new URI(form.getStashBaseUrl()),
                        form.getJiraAdminUsername(), form.getJiraAdminPassword(),
                        form.getAddUserDirectory()
                );
            } catch (URISyntaxException e) {
                throw new IllegalStateException(HttpURLValidator.class.getName() + " should have successfully validated " +
                        "uri String, but failed to create new URI from String", e);
            }
            return null;
        });
    }

    private ModelAndView showForm(String formView, Object form, Errors errors, String... globalErrors) {
        StashSoyResponseBuilder builder = setupSoyResponseBuilder(formView)
                .put("form", form).putFormErrors(globalErrors);
        if (errors != null) {
            builder.putValidationErrors(errors);
        }
        return builder.build();
    }

    private void markDatabaseAsSetup(HttpSession session) {
        session.setAttribute(HAS_SETUP_DB_NAME, Boolean.TRUE);
    }

    private void markSetupAsComplete() {
        asSysAdmin.call(() -> {
            propertiesService.setSetup(true);
            return null;
        });
    }

    private void checkSetupNotComplete() {
        if (propertiesService.isSetup()) {
            throw new IllegalStateException(ApplicationConstants.PRODUCT_NAME + " is already setup!");
        }
    }

    private void checkInMirrorModeAndSetupNotComplete() {
        checkSetupNotComplete();

        if (propertiesService.getMode() != ApplicationMode.MIRROR) {
            throw new IllegalStateException("Bitbucket not in mirror mode.");
        }
    }

    private void checkInDefaultModeAndSetupNotComplete() {
        checkSetupNotComplete();

        if (propertiesService.getMode() != ApplicationMode.DEFAULT) {
            throw new IllegalStateException("Bitbucket not in default mode.");
        }
    }

    private ModelAndView showNextStep() {
        return new ModelAndView("redirect:/setup");
    }

    private ModelAndView redirectToRoot() {
        return new ModelAndView("redirect:/");
    }

    private String getCreateAccountAndGenerateEvalUrl() {
        return new StringBuilder(DevModeUtils.isEnabled() ? "https://id.stg.internal.atlassian.com/signup"
                : "https://id.atlassian.com/signup")
                .append("?application=").append("mac")
                .append("&tenant=")
                .append("&continue=").append(UrlUtils.encodeURL(getGenerateEvalUrl()))
                .toString();
    }

    private String getGenerateEvalUrl() {
        String callback = navBuilder.setup().buildAbsolute();
        return new StringBuilder(DevModeUtils.isEnabled() ? "https://my.stg.intsys.atlassian.com/license/evaluation"
                : "http://www.atlassian.com/ex/GenerateLicense.jspa")
                .append("?utm_nooverride=1")
                .append("&ref=prod")
                .append("&product=").append("Stash") // the MAC product key is still Stash
                .append("&version=").append(propertiesService.getBuildVersion())
                .append("&build=").append(propertiesService.getBuildNumber())
                .append("&sid=").append(propertiesService.getServerId())
                .append("&licensefieldname=").append("license")
                .append("&callback=").append(callback)
                .toString();
    }

    private StashSoyResponseBuilder setupSoyResponseBuilder(@Nonnull String viewName) {
        StashSoyResponseBuilder responseBuilder = new StashSoyResponseBuilder(viewName);
        Map<String, String> da = responseBuilder.getDataAttributes();
        da.put("server-id", propertiesService.getServerId());
        da.put("dev-mode-enabled", Boolean.toString(DevModeUtils.isEnabled()));
        return responseBuilder;
    }

    private static boolean isHttpsUrl(String url) {
        if (url == null) {
            return false;
        }
        try {
            return isHttpsUrl(new URL(url));
        } catch (MalformedURLException e) {
            return false;
        }
    }

    private static boolean isHttpsUrl(URL url) {
        return url != null && "https".equalsIgnoreCase(url.getProtocol());
    }

    private static boolean isSslError(String url) {
        return !DevModeUtils.isEnabled() && !isHttpsUrl(url);
    }
}
