package org.keycloak.performance;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import static org.keycloak.common.util.ObjectUtil.capitalize;

/**
 * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
 */
public class RealmsConfigurationBuilder {

    private Random RANDOM = new Random();

    private JsonGenerator g;

    private String file;

    public static final String EXPORT_FILENAME = "benchmark-realms.json";

    public RealmsConfigurationBuilder(String filename) {
        this.file = filename;

        try {
            JsonFactory f = new JsonFactory();
            g = f.createGenerator(new File(file), JsonEncoding.UTF8);

            ObjectMapper mapper = new ObjectMapper();
            mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
            mapper.enable(SerializationFeature.INDENT_OUTPUT);
            g.setCodec(mapper);

        } catch (Exception e) {
            throw new RuntimeException("Failed to create realms export file", e);
        }
    }

    public void build() throws IOException {
        // There are several realms
        startRealms();
        for (int i = 0; i < TestConfig.numOfRealms; i++) {
            RealmConfig realm = new RealmConfig();

            String realmName = "realm_" + i;
            startRealm(realmName, realm);

            // first create clients,
            // then roles, and client roles
            // create users at the end
            // reason: each next depends on availability of the previous

            // each realm has some clients
            startClients();
            for (int j = 0; j < TestConfig.clientsPerRealm; j++) {
                ClientRepresentation client = new ClientRepresentation();

                String clientId = computeClientId(realmName, j);
                client.setClientId(clientId);
                client.setEnabled(true);

                String baseDir = computeAppUrl(clientId);
                client.setBaseUrl(baseDir);

                List<String> uris = new ArrayList<>();
                uris.add(baseDir + "/*");

                if (isClientConfidential(j)) {
                    client.setRedirectUris(uris);
                    client.setSecret(computeSecret(clientId));
                } else if (isClientBearerOnly(j)) {
                    client.setBearerOnly(true);
                } else {
                    client.setPublicClient(true);
                    client.setRedirectUris(uris);
                }

                addClient(client);
            }

            completeClients();

            // each realm has some realm roles
            startRoles();
            startRealmRoles();
            for (int j = 0; j < TestConfig.realmRoles; j++) {
                addRole("role_" + j + "_ofRealm_" + i);
            }
            completeRealmRoles();

            // each client has some client roles
            startClientRoles();
            for (int j = 0; j < TestConfig.clientsPerRealm; j++) {
                addClientRoles("client_" + j + "_ofRealm_" + i);
            }
            completeClientRoles();

            completeRoles();

            // each realm so many users
            startUsers();
            for (int j = 0; j < TestConfig.usersPerRealm; j++) {
                UserRepresentation user = new UserRepresentation();
                user.setUsername(computeUsername(realmName, j));
                user.setEnabled(true);
                user.setEmail(user.getUsername() + "@example.com");
                user.setFirstName("User" + j);
                user.setLastName("O'realm" + i);

                CredentialRepresentation creds = new CredentialRepresentation();
                creds.setType("password");
                creds.setValue(computePassword(user.getUsername()));
                user.setCredentials(Arrays.asList(creds));

                // add realm roles
                // each user some random realm roles
                Set<String> realmRoles = new HashSet<String>();
                while (realmRoles.size() < TestConfig.realmRolesPerUser) {
                    realmRoles.add("role_" + random(TestConfig.realmRoles) + "_ofRealm_" + i);
                }
                user.setRealmRoles(new ArrayList<>(realmRoles));

                // add client roles
                // each user some random client roles (random client + random role on that client)
                Set<String> clientRoles = new HashSet<String>();
                while (clientRoles.size() < TestConfig.clientRolesPerUser) {
                    int client = random(TestConfig.clientsPerRealm);
                    int clientRole = random(TestConfig.clientRolesPerClient);
                    clientRoles.add("clientrole_" + clientRole + "_ofClient_" + client + "_ofRealm_" + i);
                }
                Map<String, List<String>> clientRoleMappings = new HashMap<>();
                for (String item : clientRoles) {
                    int s = item.indexOf("_ofClient_");
                    int b = s + "_ofClient_".length();
                    int e = item.indexOf("_", b);
                    String key = "client_" + item.substring(b, e) + "_ofRealm_" + i;
                    List<String> cliRoles = clientRoleMappings.get(key);
                    if (cliRoles == null) {
                        cliRoles = new ArrayList<>();
                        clientRoleMappings.put(key, cliRoles);
                    }
                    cliRoles.add(item);
                }
                user.setClientRoles(clientRoleMappings);
                addUser(user);
            }
            completeUsers();

            completeRealm();
        }
        completeRealms();
        g.close();
    }

    private void addClientRoles(String client) throws IOException {
        g.writeArrayFieldStart(client);
        for (int i = 0; i < TestConfig.clientRolesPerClient; i++) {
            g.writeStartObject();
            String name = "clientrole_" + i + "_of" + capitalize(client);
            g.writeStringField("name", name);
            g.writeStringField("description", "Test realm role - " + name);
            g.writeEndObject();
        }
        g.writeEndArray();
    }

    private void addClient(ClientRepresentation client) throws IOException {
        g.writeObject(client);
    }

    private void startClients() throws IOException {
        g.writeArrayFieldStart("clients");
    }

    private void completeClients() throws IOException {
        g.writeEndArray();
    }

    private void startRealms() throws IOException {
        g.writeStartArray();
    }

    private void completeRealms() throws IOException {
        g.writeEndArray();
    }

    private int random(int max) {
        return RANDOM.nextInt(max);
    }

    private void startRealm(String realmName, RealmConfig conf) throws IOException {
        g.writeStartObject();
        g.writeStringField("realm", realmName);
        g.writeBooleanField("enabled", true);
        g.writeNumberField("accessTokenLifespan", conf.accessTokenLifeSpan);
        g.writeStringField("sslRequired", "none");
        g.writeBooleanField("registrationAllowed", conf.registrationAllowed);
        g.writeStringField("passwordPolicy", "hashIterations(" + TestConfig.hashIterations + ")");

        /*
        if (conf.requiredCredentials != null) {
            g.writeArrayFieldStart("requiredCredentials");
            //g.writeStartArray();
            for (String item: conf.requiredCredentials) {
                g.writeString(item);
            }
            g.writeEndArray();
        }
         */
    }

    private void completeRealm() throws IOException {
        g.writeEndObject();
    }

    private void startUsers() throws IOException {
        g.writeArrayFieldStart("users");
    }

    private void completeUsers() throws IOException {
        g.writeEndArray();
    }

    private void addUser(UserRepresentation user) throws IOException {
        g.writeObject(user);
    }

    private void startRoles() throws IOException {
        g.writeObjectFieldStart("roles");
    }

    private void startRealmRoles() throws IOException {
        g.writeArrayFieldStart("realm");
    }

    private void startClientRoles() throws IOException {
        g.writeObjectFieldStart("client");
    }

    private void completeClientRoles() throws IOException {
        g.writeEndObject();
    }

    private void addRole(String role) throws IOException {
        g.writeStartObject();
        g.writeStringField("name", role);
        g.writeStringField("description", "Test realm role - " + role);
        g.writeEndObject();
    }

    private void completeRealmRoles() throws IOException {
        g.writeEndArray();
    }

    private void completeRoles() throws IOException {
        g.writeEndObject();
    }


    static boolean isClientConfidential(int index) {
        // every third client starting with 0
        return index % 3 == 0;
    }

    static boolean isClientBearerOnly(int index) {
        // every third client starting with 1
        return index % 3 == 1;
    }

    static boolean isClientPublic(int index) {
        // every third client starting with 2
        return index % 3 == 2;
    }

    static String computeClientId(String realm, int idx) {
        return "client_" + idx + "_of" + capitalize(realm);
    }

    static String computeAppUrl(String clientId) {
        return "http://keycloak-test-" + clientId.toLowerCase();
    }

    static String computeSecret(String clientId) {
        return "secretFor_" + clientId;
    }

    static String computeUsername(String realm, int idx) {
        return "user_" + idx + "_of" + capitalize(realm);
    }

    static String computePassword(String username) {
        return "passOfUser_" + username;
    }

    


    public static void main(String[] args) throws IOException {

        File exportFile = new File(EXPORT_FILENAME);

        TestConfig.validateConfiguration();
        System.out.println("Generating test dataset with the following parameters: \n" + TestConfig.toStringDatasetProperties());

        new RealmsConfigurationBuilder(exportFile.getAbsolutePath()).build();

        System.out.println("Created " + exportFile.getAbsolutePath());
    }
}
