package apoc.cypher;

import apoc.ApocConfig;
import apoc.util.Util;
import org.apache.commons.configuration2.Configuration;
import org.neo4j.common.DependencyResolver;
import org.neo4j.configuration.GraphDatabaseSettings;
import org.neo4j.internal.helpers.collection.Iterators;
import org.neo4j.kernel.api.procedure.GlobalProcedures;
import org.neo4j.kernel.availability.AvailabilityListener;
import org.neo4j.kernel.internal.GraphDatabaseAPI;
import org.neo4j.logging.Log;

import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.Map;
import java.util.TreeMap;

public class CypherInitializer implements AvailabilityListener {

    private final GraphDatabaseAPI db;
    private final Log userLog;
    private final GlobalProcedures procs;
    private final DependencyResolver dependencyResolver;

    /**
     * indicates the status of the initializer, to be used for tests to ensure initializer operations are already done
     */
    private boolean finished = false;

    public CypherInitializer(GraphDatabaseAPI db, Log userLog) {
        this.db = db;
        this.userLog = userLog;
        this.dependencyResolver = db.getDependencyResolver();
        this.procs = dependencyResolver.resolveDependency(GlobalProcedures.class);
    }

    public boolean isFinished() {
        return finished;
    }

    public GraphDatabaseAPI getDb() {
        return db;
    }

    @Override
    public void available() {

        // run initializers in a new thread
        // we need to wait until apoc procs are registered
        // unfortunately an AvailabilityListener is triggered before that
        Util.newDaemonThread(() -> {
            try {
                final boolean isSystemDatabase = db.databaseName().equals(GraphDatabaseSettings.SYSTEM_DATABASE_NAME);
                if (!isSystemDatabase) {
                    awaitApocProceduresRegistered();
                }
                Configuration config = dependencyResolver.resolveDependency(ApocConfig.class).getConfig();


                TreeMap<String, String> initializers = new TreeMap<>();

                config.getKeys(ApocConfig.APOC_CONFIG_INITIALIZER + "." + db.databaseName()).forEachRemaining(key -> initializers.put(key, config.getString(key)));

                if (!isSystemDatabase) {
                    config.getKeys(ApocConfig.APOC_CONFIG_INITIALIZER_CYPHER).forEachRemaining(key -> initializers.put(key, config.getString(key)));
                }

                for (Object initializer : initializers.values()) {
                    String query = initializer.toString();
                    if (!query.isEmpty()) {
                        try {
                            // we need to apply a retry strategy here since in systemdb we potentially conflict with
                            // creating contraints which could cause our query to fail with a transient error.
                            Util.retryInTx(userLog, db, tx -> Iterators.count(tx.execute(query)), 0, 5, retries -> {});
                            userLog.info("successfully initialized: " + query);
                        } catch (Exception e) {
                            userLog.error("error upon initialization, running: " + query, e);
                        }
                    }
                }
            } finally {
                finished = true;
            }
        }).start();
    }

    private Collection<String> collectInitializers(boolean isSystemDatabase, Configuration config) {
        Map<String, String> initializers = new TreeMap<>();

        config.getKeys(ApocConfig.APOC_CONFIG_INITIALIZER + "." + db.databaseName())
                .forEachRemaining(key -> putIfNotBlank(initializers, key, config.getString(key)));

        // add legacy style initializers
        if (!isSystemDatabase) {
            config.getKeys(ApocConfig.APOC_CONFIG_INITIALIZER_CYPHER)
                    .forEachRemaining(key -> initializers.put(key, config.getString(key)));
        }

        return initializers.values();
    }

    private void putIfNotBlank(Map<String,String> map, String key, String value) {
        if ((value!=null) && (!value.isBlank())) {
            map.put(key, value);
        }
    }

    private void awaitApocProceduresRegistered() {
        while (!areApocProceduresRegistered()) {
            Util.sleep(100);
        }
    }

    private boolean areApocProceduresRegistered() {
        try {
            return procs.getAllProcedures().stream().anyMatch(signature -> signature.name().toString().startsWith("apoc"));
        } catch (ConcurrentModificationException e) {
            // if a CME happens (possible during procedure scanning)
            // we return false and the caller will try again
            return false;
        }
    }

    @Override
    public void unavailable() {
        // intentionally empty
    }
}

@@ -528,16 +528,17 @@
 ctions;%0A
+%0A
 import j
@@ -2055,17 +2055,16 @@
 fig();%0A%0A
-%0A
         
@@ -2075,501 +2075,72 @@
     
-TreeMap%3CString, String%3E initializers = new TreeMap%3C%3E();%0A%0A                config.getKeys(ApocConfig.APOC_CONFIG_INITIALIZER + %22.%22 + db.databaseName()).forEachRemaining(key -%3E initializers.put(key, config.getString(key)));%0A%0A                if (!isSystemDatabase) %7B%0A                    config.getKeys(ApocConfig.APOC_CONFIG_INITIALIZER_CYPHER).forEachRemaining(key -%3E initializers.put(key, config.getString(key)));%0A                %7D%0A%0A                for (Object initializer : initializers.values(
+for (String query : collectInitializers(isSystemDatabase, config
 )) %7B
@@ -2164,125 +2164,14 @@
     
-S
 tr
-ing query = initializer.toString();%0A                    if (!query.isEmpty()) %7B%0A                        try %7B%0A    
+y %7B%0A
     
@@ -2303,20 +2303,16 @@
         
- 
-   
 // creat
@@ -2318,16 +2318,17 @@
 ting con
+s
 traints 
@@ -2387,20 +2387,16 @@
  error.%0A
-    
         
@@ -2499,16 +2499,17 @@
 ies -%3E %7B
+ 
 %7D);%0A    
@@ -2520,36 +2520,32 @@
                 
-    
 userLog.info(%22su
@@ -2596,28 +2596,24 @@
             
-    
 %7D catch (Exc
@@ -2640,36 +2640,32 @@
                 
-  
-  
 userLog.error(%22e
@@ -2718,34 +2718,8 @@
 e);%0A
-                        %7D%0A
     

refactor collecting initializers into a separate method