package io.cloudtrust.keycloak.export;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.cloudtrust.keycloak.export.dto.BetterRealmRepresentation;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.exportimport.ImportProvider;
import org.keycloak.exportimport.Strategy;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.services.managers.RealmManager;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

public class SingleFileImportProvider implements ImportProvider {
    private static final Logger logger = Logger.getLogger(SingleFileImportProvider.class);
    private static final ThreadLocal<ObjectMapper> objMapperProvider = ThreadLocal.withInitial(ObjectMapper::new);

    private final File file;

    // Allows to cache representation per provider to avoid parsing them twice
    private List<BetterRealmRepresentation> realmReps;

    public SingleFileImportProvider(File file) {
        this.file = file;
    }

    @Override
    public void importModel(KeycloakSessionFactory factory, Strategy strategy) throws IOException {
        logger.infof("Full importing from file %s", this.file.getAbsolutePath());

        BetterRealmRepresentation masterRealm = getMasterRealm();
        KeycloakModelUtils.runJobInTransaction(factory, session -> {
            // Import master realm first, if exists
            if (masterRealm != null) {
                importRealm(session, masterRealm, strategy);
            }
            realmReps.stream().filter(r -> r != masterRealm).forEach(r -> importRealm(session, r, strategy));

            if (masterRealm != null) {
                // If master was imported, we may need to re-create realm management clients
                for (RealmModel realm : session.realms().getRealms()) {
                    if (realm.getMasterAdminClient() == null) {
                        logger.infof("Re-created management client in master realm for realm '%s'", realm.getName());
                        new RealmManager(session).setupMasterAdminManagement(realm);
                    }
                }
            }
        });
    }

    @Override
    public void importRealm(KeycloakSessionFactory factory, String realmName, Strategy strategy) throws IOException {
        // import just that single realm in case that file contains many realms?
        importModel(factory, strategy);
    }

    @Override
    public boolean isMasterRealmExported() throws IOException {
        return getMasterRealm() != null;
    }

    @Override
    public void close() {
        // Nothing to close
    }

    private void importRealm(KeycloakSession session, BetterRealmRepresentation realm, Strategy strategy) {
        ImportExportUtils.importRealm(session, null, realm, strategy, false);
    }

    private BetterRealmRepresentation getMasterRealm() throws IOException {
        checkRealmReps();
        return realmReps.stream().filter(r -> Config.getAdminRealm().equals(r.getRealm())).findFirst().orElse(null);
    }

    private void checkRealmReps() throws IOException {
        if (realmReps == null) {
            try (InputStream is = new FileInputStream(file)) {
                realmReps = getObjectsFromStream(objMapperProvider.get(), is, BetterRealmRepresentation.class);
            }
        }
    }

    private static <T> List<T> getObjectsFromStream(ObjectMapper mapper, InputStream is, Class<T> clazz) throws IOException {
        List<T> result = new ArrayList<>();
        JsonFactory factory = mapper.getFactory();
        try (JsonParser parser = factory.createParser(is)) {
            parser.nextToken();

            if (parser.getCurrentToken() == JsonToken.START_ARRAY) {
                // Case with more realms in stream
                parser.nextToken();

                while (parser.getCurrentToken() == JsonToken.START_OBJECT) {
                    T realmRep = parser.readValueAs(clazz);
                    parser.nextToken();
                    result.add(realmRep);
                }
            } else if (parser.getCurrentToken() == JsonToken.START_OBJECT) {
                // Case with single realm in stream
                T realmRep = parser.readValueAs(clazz);
                result.add(realmRep);
            }
        }

        return result;
    }
}