package org.keycloak.client.registration.cli.commands;

import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.registration.cli.config.ConfigData;
import org.keycloak.client.registration.cli.config.ConfigHandler;
import org.keycloak.client.registration.cli.config.FileConfigHandler;
import org.keycloak.client.registration.cli.config.InMemoryConfigHandler;
import org.keycloak.client.registration.cli.config.RealmConfigData;
import org.keycloak.client.registration.cli.util.ConfigUtil;
import org.keycloak.client.registration.cli.util.HttpUtil;
import org.keycloak.client.registration.cli.util.IoUtil;

import java.io.File;

import static org.keycloak.client.registration.cli.config.FileConfigHandler.setConfigFile;
import static org.keycloak.client.registration.cli.util.ConfigUtil.checkAuthInfo;
import static org.keycloak.client.registration.cli.util.ConfigUtil.checkServerInfo;
import static org.keycloak.client.registration.cli.util.ConfigUtil.loadConfig;

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

    static final String DEFAULT_CLIENT = "admin-cli";


    @Option(name = "config", description = "Path to the config file (~/.keycloak/kcreg.config by default)", hasValue = true)
    protected String config;

    @Option(name = "no-config", description = "No configuration file should be used, no authentication info should be saved", hasValue = false)
    protected boolean noconfig;

    @Option(name = "server", description = "Server endpoint url (e.g. 'http://localhost:8080/auth')", hasValue = true)
    protected String server;

    @Option(name = "realm", description = "Realm name to authenticate against", hasValue = true)
    protected String realm;

    @Option(name = "client", description = "Realm name to authenticate against", hasValue = true)
    protected String clientId;

    @Option(name = "user", description = "Username to login with", hasValue = true)
    protected String user;

    @Option(name = "password", description = "Password to login with (prompted for if not specified and --user is used)", hasValue = true)
    protected String password;

    @Option(name = "secret", description = "Secret to authenticate the client (prompted for if no --user or --keystore is specified)", hasValue = true)
    protected String secret;

    @Option(name = "keystore", description = "Path to a keystore containing private key", hasValue = true)
    protected String keystore;

    @Option(name = "storepass", description = "Keystore password (prompted for if not specified and --keystore is used)", hasValue = true)
    protected String storePass;

    @Option(name = "keypass", description = "Key password (prompted for if not specified and --keystore is used without --storepass, \n                             otherwise defaults to keystore password)", hasValue = true)
    protected String keyPass;

    @Option(name = "alias", description = "Alias of the key inside a keystore (defaults to the value of ClientId)", hasValue = true)
    protected String alias;

    @Option(name = "truststore", description = "Path to a truststore", hasValue = true)
    protected String trustStore;

    @Option(name = "trustpass", description = "Truststore password (prompted for if not specified and --truststore is used)", hasValue = true)
    protected String trustPass;

    @Option(shortName = 't', name = "token", description = "Initial / Registration access token to use)", hasValue = true)
    protected String token;

    protected void initFromParent(AbstractAuthOptionsCmd parent) {

        super.initFromParent(parent);

        noconfig = parent.noconfig;
        config = parent.config;
        server = parent.server;
        realm = parent.realm;
        clientId = parent.clientId;
        user = parent.user;
        password = parent.password;
        secret = parent.secret;
        keystore = parent.keystore;
        storePass = parent.storePass;
        keyPass = parent.keyPass;
        alias = parent.alias;
        trustStore = parent.trustStore;
        trustPass = parent.trustPass;
        token = parent.token;
    }

    protected void applyDefaultOptionValues() {
        if (clientId == null) {
            clientId = DEFAULT_CLIENT;
        }
    }

    protected boolean noOptions() {
        return server == null && realm == null && clientId == null && secret == null &&
                user == null && password == null &&
                keystore == null && storePass == null && keyPass == null && alias == null &&
                trustStore == null && trustPass == null &&
                token == null && config == null;
    }

    protected void processGlobalOptions() {

        super.processGlobalOptions();

        if (config != null && noconfig) {
            throw new RuntimeException("Options --config and --no-config are mutually exclusive");
        }

        if (!noconfig) {
            setConfigFile(config != null ? config : ConfigUtil.DEFAULT_CONFIG_FILE_PATH);
            ConfigUtil.setHandler(new FileConfigHandler());
        } else {
            InMemoryConfigHandler handler = new InMemoryConfigHandler();
            ConfigData data = new ConfigData();
            initConfigData(data);
            handler.setConfigData(data);
            ConfigUtil.setHandler(handler);
        }
    }

    protected void setupTruststore(ConfigData configData, CommandInvocation invocation ) {

        if (!configData.getServerUrl().startsWith("https:")) {
            return;
        }

        String truststore = trustStore;
        if (truststore == null) {
            truststore = configData.getTruststore();
        }

        if (truststore != null) {
            String pass = trustPass;
            if (pass == null) {
                pass = configData.getTrustpass();
            }
            if (pass == null) {
                pass = IoUtil.readSecret("Enter truststore password: ", invocation);
            }

            try {
                HttpUtil.setTruststore(new File(truststore), pass);
            } catch (Exception e) {
                throw new RuntimeException("Failed to load truststore: " + truststore, e);
            }
        }
    }

    protected ConfigData ensureAuthInfo(ConfigData config, CommandInvocation commandInvocation) {

        if (requiresLogin()) {
            // make sure current handler is in-memory handler
            // restore it at the end
            ConfigHandler old = ConfigUtil.getHandler();
            try {
                // make sure all defaults are initialized after this point
                applyDefaultOptionValues();

                initConfigData(config);
                ConfigUtil.setupInMemoryHandler(config);

                ConfigCredentialsCmd login = new ConfigCredentialsCmd();
                login.initFromParent(this);
                login.init(config);
                login.process(commandInvocation);

                // this must be executed before finally block which restores config handler
                return loadConfig();

            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                ConfigUtil.setHandler(old);
            }

        } else {
            checkAuthInfo(config);

            // make sure all defaults are initialized after this point
            applyDefaultOptionValues();
            return loadConfig();
        }
    }

    protected boolean requiresLogin() {
        return user != null || password != null || secret != null || keystore != null
                || keyPass != null || storePass != null || alias != null;
    }

    protected ConfigData copyWithServerInfo(ConfigData config) {

        ConfigData result = config.deepcopy();

        if (server != null) {
            result.setServerUrl(server);
        }
        if (realm != null) {
            result.setRealm(realm);
        }

        checkServerInfo(result);
        return result;
    }

    private void initConfigData(ConfigData data) {
        if (server != null)
            data.setServerUrl(server);
        if (realm != null)
            data.setRealm(realm);
        if (trustStore != null)
            data.setTruststore(trustStore);

        RealmConfigData rdata = data.sessionRealmConfigData();
        if (clientId != null)
            rdata.setClientId(clientId);
        if (secret != null)
            rdata.setSecret(secret);
    }

    protected void checkUnsupportedOptions(String ... options) {
        if (options.length % 2 != 0) {
            throw new IllegalArgumentException("Even number of argument required");
        }

        for (int i = 0; i < options.length; i++) {
            String name = options[i];
            String value = options[++i];

            if (value != null) {
                throw new RuntimeException("Unsupported option: " + name);
            }
        }
    }
}
