@Override
    public void upgrade() {
        if (configService.get(MigrationCompleted.class) != null) {
            LOG.debug("Migration already completed.");
            return;
        }

        var previousMigration = Optional.ofNullable(configService.get(V20191219090834_AddSourcesPage.MigrationCompleted.class));

        var previousInstallation = previousMigration.flatMap(this::previousInstallation);
        var notPreviouslyInstalled = previousInstallation.isEmpty();

        final ContentPack contentPack = readContentPack();

        var contentPackShouldBeUninstalled = previousInstallation
                .filter(this::userHasNotModifiedSourcesPage);
        var notLocallyModified = contentPackShouldBeUninstalled.isPresent();
        var previousDashboard = contentPackShouldBeUninstalled.flatMap(this::dashboardFromInstallation);

        var pack = insertContentPack(contentPack)
                .orElseThrow(() -> {
                    configService.write(MigrationCompleted.create(contentPack.id().toString(), false, false));
                    return new ContentPackException("Content pack " + contentPack.id() + " with this revision " + contentPack.revision() + " already found!");
                });

        contentPackShouldBeUninstalled.ifPresent(this::uninstallContentPack);

        if (notPreviouslyInstalled || notLocallyModified) {
            var newInstallation = installContentPack(pack);
            assert (newInstallation != null);
            previousDashboard.ifPresent(dashboard -> fixupNewDashboardId(dashboard, newInstallation));
        } else {
            notificationService.publishIfFirst(notificationService.buildNow()
                    .addType(Notification.Type.GENERIC)
                    .addSeverity(Notification.Severity.NORMAL)
                    .addDetail("title", "Updating Sources Dashboard")
                    .addDetail("description", """
                            While updating the Sources Dashboard, it was detected that the previous version was modified locally. To save these modifications from getting lost,
                            a new version of the content pack containing the Sources Dashboard was uploaded, but not installed.

                            If you want to use the new version of the dashboard, you can go to "System" -> "Content Packs" -> "Sources Page Dashboard" and install version 2.
                            In addition, you can either keep your current "Sources" dashboard (having two "Sources" dashboards) or uninstall version 1 of the content pack to remove it.
                            """));
        }

        configService.write(MigrationCompleted.create(pack.id().toString(), notPreviouslyInstalled || notLocallyModified, contentPackShouldBeUninstalled.isPresent()));
    }

    private void fixupNewDashboardId(Document previousDashboard, ContentPackInstallation newInstallation) {
        var newDashboard = dashboardFromInstallation(newInstallation);
        var previousDashboardId = previousDashboard.getObjectId("_id");

        newDashboard.ifPresent(dashboard -> {
            var newDashboardId = dashboard.getObjectId("_id");
            dashboard.append("_id", previousDashboardId);
            views.deleteOne(Filters.eq("_id", newDashboardId));
            views.insertOne(dashboard);
            contentPackInstallations.updateOne(Filters.eq("_id", newInstallation.id()), Updates.set("entities.0.id", previousDashboardId.toHexString()));
        });
    }


    private Optional<ContentPackInstallation> previousInstallation(V20191219090834_AddSourcesPage.MigrationCompleted previousMigration) {
        return Optional.ofNullable(previousMigration.contentPackId())
                .map(id -> contentPackInstallationPersistenceService.findByContentPackId(ModelId.of(id)))
                .flatMap(installations -> installations.stream()
                        .filter(installation -> installation.contentPackRevision() == 1
                                && installation.createdBy().equals("admin")
                                && installation.comment().equals("Add Sources Page"))
                        .findFirst());
    }

    private Optional<Document> dashboardFromInstallation(ContentPackInstallation installation) {
        return Optional.ofNullable(installation.entities())
                .flatMap(entities -> entities.stream().findFirst())
                .map(Identified::id)
                .map(ModelId::id)
                .flatMap(dashboardId -> Optional.ofNullable(views.find(Filters.eq("_id", new ObjectId(dashboardId))).first()));
    }

    private boolean userHasNotModifiedSourcesPage(ContentPackInstallation previousInstallation) {
        var previousDashboard = dashboardFromInstallation(previousInstallation)
                .flatMap(dashboard -> Optional.ofNullable(dashboard.getString("search_id")))
                .flatMap(searchId -> Optional.ofNullable(searches.find(Filters.eq("_id", new ObjectId(searchId))).first()));

        var userHasModifiedSourcesPage = previousDashboard
                .map(dashboard -> dashboard.getDate("created_at"))
                .map(createdAt -> !createdAt.equals(UNMODIFIED_SOURCES_SEARCH_DATE))
                .orElse(false);
        return !userHasModifiedSourcesPage;
    }

    private ContentPackInstallation installContentPack(ContentPack contentPack) {
        return contentPackService.installContentPack(contentPack, Collections.emptyMap(), "Add Sources Page V2", "admin");
    }

    private Optional<ContentPack> insertContentPack(ContentPack contentPack) {
        return this.contentPackPersistenceService.insert(contentPack);
    }

    private void uninstallContentPack(ContentPackInstallation contentPackInstallation) {
        contentPackPersistenceService.findByIdAndRevision(contentPackInstallation.contentPackId(), contentPackInstallation.contentPackRevision())
                .ifPresent(contentPack -> contentPackService.uninstallContentPack(contentPack, contentPackInstallation));
    }

    private ContentPack readContentPack() {
        try {
            final URL contentPackURL = V20230601104500_AddSourcesPageV2.class.getResource("V20230601104500_AddSourcesPage_V2_Content_Pack.json");
            return this.objectMapper.readValue(contentPackURL, ContentPack.class);
        } catch (IOException e) {
            throw new RuntimeException("Unable to read content pack source in migration: ", e);
        }
    }

    @JsonAutoDetect
    @AutoValue
    @WithBeanGetter
    public static abstract class MigrationCompleted {
        @JsonProperty("content_pack_id")
        public abstract String contentPackId();

        @JsonProperty("installed_content_pack")
        public abstract boolean installedContentPack();

        @JsonProperty("uninstalled_previous_revision")
        public abstract boolean uninstalledPreviousRevision();

        @JsonCreator
        public static MigrationCompleted create(@JsonProperty("content_pack_id") final String contentPackId,
                                                @JsonProperty("installed_content_pack") boolean installedContentPack,
                                                @JsonProperty("uninstalled_previous_revision") boolean uninstalledPreviousRevision) {
            return new AutoValue_V20230601104500_AddSourcesPageV2_MigrationCompleted(contentPackId, installedContentPack, uninstalledPreviousRevision);
        }
    }
}
@Test
    @MongoDBFixtures({"V20230601104500_AddSourcesPageV2/previousMigration.json", "V20230601104500_AddSourcesPageV2/previousInstallationWithoutLocalModifications.json"})
    void previousInstallationWithoutLocalModificationsIsUninstalled() {
        previousMigrationHasRun();
        thisMigrationHasNotRun();

        this.migration.upgrade();

        var migrationCompleted = expectMigrationCompleted();

        assertThat(migrationCompleted.contentPackId()).isNotBlank();
        assertThat(migrationCompleted.installedContentPack()).isTrue();
        assertThat(migrationCompleted.uninstalledPreviousRevision()).isTrue();
    }