/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.nifi.registry.db;

import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.registry.db.migration.BucketEntityV1;
import org.apache.nifi.registry.db.migration.FlowEntityV1;
import org.apache.nifi.registry.db.migration.FlowSnapshotEntityV1;
import org.apache.nifi.registry.db.migration.LegacyDataSourceFactory;
import org.apache.nifi.registry.db.migration.LegacyDatabaseService;
import org.apache.nifi.registry.db.migration.LegacyEntityMapper;
import org.apache.nifi.registry.properties.NiFiRegistryProperties;
import org.apache.nifi.registry.service.MetadataService;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.FlywayException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.io.File;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

/**
 * Custom Flyway migration strategy that lets us perform data migration from the original database used in the
 * 0.1.0 release, to the new database. The data migration will be triggered when it is determined that new database
 * is brand new AND the legacy DB properties are specified. If the primary database already contains the 'BUCKET' table,
 * or if the legacy database properties are not specified, then no data migration is performed.
 */
@Component
public class CustomFlywayMigrationStrategy implements FlywayMigrationStrategy {

    private static final Logger LOGGER = LoggerFactory.getLogger(CustomFlywayMigrationStrategy.class);

    private NiFiRegistryProperties properties;

    @Autowired
    public CustomFlywayMigrationStrategy(final NiFiRegistryProperties properties) {
        this.properties = properties;
    }

    @Override
    public void migrate(Flyway flyway) {
        final boolean newDatabase = isNewDatabase(flyway.getConfiguration().getDataSource());
        if (newDatabase) {
            LOGGER.info("First time initializing database...");
        } else {
            LOGGER.info("Found existing database...");
        }

        boolean existingLegacyDatabase = false;
        if (!StringUtils.isBlank(properties.getLegacyDatabaseDirectory())) {
            LOGGER.info("Found legacy database properties...");

            final File legacyDatabaseFile = new File(properties.getLegacyDatabaseDirectory(), "nifi-registry.mv.db");
            if (legacyDatabaseFile.exists()) {
                LOGGER.info("Found legacy database file...");
                existingLegacyDatabase = true;
            } else {
                LOGGER.info("Did not find legacy database file...");
                existingLegacyDatabase = false;
            }
        }

        // If newDatabase is true, then we need to run the Flyway migration first to create all the tables, then the data migration
        // If newDatabase is false, then we need to run the Flyway migration to run any schema updates, but no data migration

        flyway.migrate();

        if (newDatabase && existingLegacyDatabase) {
            final LegacyDataSourceFactory legacyDataSourceFactory = new LegacyDataSourceFactory(properties);
            final DataSource legacyDataSource = legacyDataSourceFactory.getDataSource();
            final DataSource primaryDataSource = flyway.getConfiguration().getDataSource();
            migrateData(legacyDataSource, primaryDataSource);
        }
    }

    /**
     * Determines if the database represented by this data source is being initialized for the first time based on
     * whether or not the table named 'BUCKET' or 'bucket' already exists.
     *
     * @param dataSource the data source
     * @return true if the database has never been initialized before, false otherwise
     */
    private boolean isNewDatabase(final DataSource dataSource) {
        try (final Connection connection = dataSource.getConnection();
             final ResultSet rsUpper = connection.getMetaData().getTables(null, null, "BUCKET", null);
             final ResultSet rsLower = connection.getMetaData().getTables(null, null, "bucket", null)) {
            return !rsUpper.next() && !rsLower.next();
        } catch (SQLException e) {
            LOGGER.error(e.getMessage(), e);
            throw new FlywayException("Unable to obtain connection from Flyway DataSource", e);
        }
    }

    /**
     * Transfers all data from the source to the destination.
     *
     * @param source the legacy H2 DataSource
     * @param dest the new destination DataSource
     */
    private void migrateData(final DataSource source, final DataSource dest) {
        final LegacyDatabaseService legacyDatabaseService = new LegacyDatabaseService(source);

        final JdbcTemplate destJdbcTemplate = new JdbcTemplate(dest);
        final MetadataService destMetadataService = new DatabaseMetadataService(destJdbcTemplate);

        LOGGER.info("Migrating data from legacy database to new new database...");

        // Migrate buckets
        final List<BucketEntityV1> sourceBuckets = legacyDatabaseService.getAllBuckets();
        LOGGER.info("Migrating {} buckets..", sourceBuckets.size());

        sourceBuckets.stream()
                .map(b -> LegacyEntityMapper.createBucketEntity(b))
                .forEach(b -> destMetadataService.createBucket(b));

        // Migrate flows
        final List<FlowEntityV1> sourceFlows = legacyDatabaseService.getAllFlows();
        LOGGER.info("Migrating {} flows..", sourceFlows.size());

        sourceFlows.stream()
                .map(f -> LegacyEntityMapper.createFlowEntity(f))
                .forEach(f -> destMetadataService.createFlow(f));

        // Migrate flow snapshots
        final List<FlowSnapshotEntityV1> sourceSnapshots = legacyDatabaseService.getAllFlowSnapshots();
        LOGGER.info("Migrating {} flow snapshots..", sourceSnapshots.size());

        sourceSnapshots.stream()
                .map(fs -> LegacyEntityMapper.createFlowSnapshotEntity(fs))
                .forEach(fs -> destMetadataService.createFlowSnapshot(fs));

        LOGGER.info("Data migration complete!");
    }

}
