/*************************************************************************
 *                                                                       *
 *  EJBCA Community: The OpenSource Certificate Authority                *
 *                                                                       *
 *  This software is free software; you can redistribute it and/or       *
 *  modify it under the terms of the GNU Lesser General Public           *
 *  License as published by the Free Software Foundation; either         *
 *  version 2.1 of the License, or any later version.                    *
 *                                                                       *
 *  See terms of license at gnu.org.                                     *
 *                                                                       *
 *************************************************************************/
package org.ejbca.ui.cli.cryptotoken;

import java.io.File;
import java.util.List;
import java.util.Properties;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.cesecore.authorization.AuthorizationDeniedException;
import org.cesecore.keys.token.BaseCryptoToken;
import org.cesecore.keys.token.CryptoToken;
import org.cesecore.keys.token.CryptoTokenAuthenticationFailedException;
import org.cesecore.keys.token.CryptoTokenInfo;
import org.cesecore.keys.token.CryptoTokenManagementSessionRemote;
import org.cesecore.keys.token.CryptoTokenNameInUseException;
import org.cesecore.keys.token.CryptoTokenOfflineException;
import org.cesecore.keys.token.PKCS11CryptoToken;
import org.cesecore.keys.token.SoftCryptoToken;
import org.cesecore.keys.token.p11.Pkcs11SlotLabelType;
import org.cesecore.keys.token.p11.exception.NoSuchSlotException;
import org.cesecore.util.EjbRemoteHelper;
import org.cesecore.util.StringTools;
import org.ejbca.ui.cli.infrastructure.command.CommandResult;
import org.ejbca.ui.cli.infrastructure.command.EjbcaCliUserCommandBase;
import org.ejbca.ui.cli.infrastructure.parameter.Parameter;
import org.ejbca.ui.cli.infrastructure.parameter.ParameterContainer;
import org.ejbca.ui.cli.infrastructure.parameter.enums.MandatoryMode;
import org.ejbca.ui.cli.infrastructure.parameter.enums.ParameterMode;
import org.ejbca.ui.cli.infrastructure.parameter.enums.StandaloneMode;

/**
 * CryptoToken EJB CLI command. See {@link #getDescription()} implementation.
 * 
 * @version $Id: CryptoTokenCreateCommand.java 30510 2018-11-15 08:20:55Z anatom $
 */
public class CryptoTokenCreateCommand extends EjbcaCliUserCommandBase {

    private static final Logger log = Logger.getLogger(CryptoTokenCreateCommand.class);

    private static final String CRYPTOTOKEN_NAME_KEY = "--token";
    private static final String PIN_KEY = "--pin";
    private static final String AUTOACTIVATE_KEY = "--autoactivate";
    private static final String TYPE_KEY = "--type";
    private static final String PRIVATE_KEY_EXPORT_KEY = "--exportkey";
    private static final String USE_EXPLICIT_KEY_PARAMETERS = "--explicitkeyparams";
    private static final String PKCS11_LIB_KEY = "--lib";
    private static final String SLOT_REFERENCE_TYPE_KEY = "--slotlabeltype";
    private static final String SLOT_REFERENCE_KEY = "--slotlabel";
    private static final String PKCS11_ATTR_FILE_KEY = "--attr";
    private static final String PKCS11_SLOTCOLLIDE_IGNORE= "--forceusedslots";

    {
        registerParameter(new Parameter(CRYPTOTOKEN_NAME_KEY, "Token Name", MandatoryMode.MANDATORY, StandaloneMode.ALLOW, ParameterMode.ARGUMENT,
                "The name of the crypto token."));
        //Kept as a mandatory parameter for legacy reasons.
        registerParameter(new Parameter(PIN_KEY, "Pin", MandatoryMode.MANDATORY, StandaloneMode.ALLOW, ParameterMode.ARGUMENT,
                "Pin code for the crypto token. Set to 'null' to prompt."));
        // TODO: Make this a flag when legacy support isn't necessary
        registerParameter(new Parameter(AUTOACTIVATE_KEY, "true|false", MandatoryMode.MANDATORY, StandaloneMode.ALLOW, ParameterMode.ARGUMENT,
                "Set to true|false to allow|disallow whether crypto token should be autoactivated or not."));
        registerParameter(new Parameter(TYPE_KEY, "Type", MandatoryMode.MANDATORY, StandaloneMode.ALLOW, ParameterMode.ARGUMENT, "Available types: "
                + SoftCryptoToken.class.getSimpleName() + ", " + PKCS11CryptoToken.class.getSimpleName()));
        //Soft params
        registerParameter(new Parameter(PRIVATE_KEY_EXPORT_KEY, "true|false", MandatoryMode.OPTIONAL, StandaloneMode.ALLOW, ParameterMode.ARGUMENT,
                "(" + SoftCryptoToken.class.getSimpleName() + ") Set to true|false to allow|disallow private key export."));
        //PKCS#11
        registerParameter(new Parameter(PKCS11_LIB_KEY, "Library Name", MandatoryMode.OPTIONAL, StandaloneMode.ALLOW, ParameterMode.ARGUMENT, "("
                + PKCS11CryptoToken.class.getSimpleName() + ") PKCS#11 library file. Required if type is " + PKCS11CryptoToken.class.getSimpleName()));
        registerParameter(new Parameter(SLOT_REFERENCE_TYPE_KEY, "Slot Reference Type", MandatoryMode.OPTIONAL, StandaloneMode.ALLOW,
                ParameterMode.ARGUMENT, "(" + PKCS11CryptoToken.class.getSimpleName() + ") Slot Reference Type."));
        registerParameter(new Parameter(SLOT_REFERENCE_KEY, "Slot Reference", MandatoryMode.OPTIONAL, StandaloneMode.ALLOW, ParameterMode.ARGUMENT,
                "(" + PKCS11CryptoToken.class.getSimpleName() + ") Slot reference."));
        registerParameter(new Parameter(PKCS11_ATTR_FILE_KEY, "Attribute File", MandatoryMode.OPTIONAL, StandaloneMode.ALLOW, ParameterMode.ARGUMENT,
                "(" + PKCS11CryptoToken.class.getSimpleName() + ") PKCS#11 Attribute File"));
        registerParameter(new Parameter(PKCS11_SLOTCOLLIDE_IGNORE, "Ignore used P11 slots", MandatoryMode.OPTIONAL, StandaloneMode.FORBID, ParameterMode.FLAG, "Ignore warnings, and confirm yes, for P11 slots that are already used."));
        // ePassport CSCA only
        registerParameter(new Parameter(USE_EXPLICIT_KEY_PARAMETERS, "true|false", MandatoryMode.OPTIONAL, StandaloneMode.ALLOW, ParameterMode.ARGUMENT,
                "Set to true|false to allow|disallow usage of explicit ECC parameters( Only for ICAO CSCA and DS certificates)."));
    }
    
    @Override
    public String[] getCommandPath() {
        return new String[] { "cryptotoken" };
    }

    @Override
    public String getMainCommand() {
        return "create";
    }

    @Override
    public CommandResult execute(ParameterContainer parameters) {

        final String cryptoTokenName = parameters.get(CRYPTOTOKEN_NAME_KEY);
        final boolean autoActivate = Boolean.valueOf(parameters.get(AUTOACTIVATE_KEY));
        final boolean ignoreslotwarning = (parameters.get(PKCS11_SLOTCOLLIDE_IGNORE) != null);
        final String type = parameters.get(TYPE_KEY);
        final String className;
        final Properties cryptoTokenPropertes = new Properties();
        final CryptoTokenManagementSessionRemote cryptoTokenManagementSession = EjbRemoteHelper.INSTANCE
                .getRemoteSession(CryptoTokenManagementSessionRemote.class);
        if (SoftCryptoToken.class.getSimpleName().equals(type)) {
            className = SoftCryptoToken.class.getName();
            cryptoTokenPropertes.setProperty(CryptoToken.ALLOW_EXTRACTABLE_PRIVATE_KEY,
                    Boolean.toString(Boolean.valueOf(parameters.get(PRIVATE_KEY_EXPORT_KEY))));
            cryptoTokenPropertes.setProperty(SoftCryptoToken.NODEFAULTPWD, Boolean.TRUE.toString());
        } else if (PKCS11CryptoToken.class.getSimpleName().equals(type)) {
            className = PKCS11CryptoToken.class.getName();
            // Parse library file
            String pkcs11LibFilename = parameters.get(PKCS11_LIB_KEY);
            if(null == pkcs11LibFilename) {
                getLogger().info("You need to specify a PKCS#11 library file.");
                return CommandResult.CLI_FAILURE;
            }
            if (!new File(pkcs11LibFilename).exists()) {
                getLogger().info("PKCS#11 library file " + pkcs11LibFilename + " does not exist!");
                return CommandResult.CLI_FAILURE;
            }
            cryptoTokenPropertes.setProperty(PKCS11CryptoToken.SHLIB_LABEL_KEY, pkcs11LibFilename);         
            String slotPropertyValue = parameters.get(SLOT_REFERENCE_KEY);
            if(slotPropertyValue == null) {
                getLogger().info("Slot reference key (" + SLOT_REFERENCE_KEY + ") needs to be defined for PKCS#11 tokens.");
                return CommandResult.CLI_FAILURE;
            }
            
            if (!parameters.containsKey(SLOT_REFERENCE_TYPE_KEY)) {
                getLogger().info("Slot reference type (" + SLOT_REFERENCE_TYPE_KEY + ") needs to be defined for PKCS#11 tokens.");
                return CommandResult.CLI_FAILURE;
            }
            Pkcs11SlotLabelType labelType = Pkcs11SlotLabelType.getFromKey(parameters.get(SLOT_REFERENCE_TYPE_KEY));
            if(labelType == null) {
                getLogger().info(parameters.get(SLOT_REFERENCE_TYPE_KEY) + " was not a valid slot reference type.");
                return CommandResult.CLI_FAILURE;
            }
            cryptoTokenPropertes.setProperty(PKCS11CryptoToken.SLOT_LABEL_VALUE, slotPropertyValue);
            //If an index was given, accept just numbers as well
            if (labelType.isEqual(Pkcs11SlotLabelType.SLOT_INDEX)) {
                if (slotPropertyValue.charAt(0) != 'i') {
                    slotPropertyValue = "i" + slotPropertyValue;
                }
            }
            if (!labelType.validate(slotPropertyValue)) {
                getLogger().info("Invalid value " + slotPropertyValue + " given for slot type " + labelType.getDescription());
                return CommandResult.CLI_FAILURE;
            } else {
                cryptoTokenPropertes.setProperty(PKCS11CryptoToken.SLOT_LABEL_TYPE, labelType.getKey());
            }
            // Parse attribute file
            String attributeFileName = parameters.get(PKCS11_ATTR_FILE_KEY);
            if ( (attributeFileName != null) && (!"null".equalsIgnoreCase(attributeFileName)) ) {
                if (!new File(attributeFileName).exists()) {
                    getLogger().info("PKCS#11 attribute file " + attributeFileName + " does not exist!");
                    return CommandResult.CLI_FAILURE;
                }
                cryptoTokenPropertes.setProperty(PKCS11CryptoToken.ATTRIB_LABEL_KEY, attributeFileName);
            }

            // Check if this crypto token is already used
            try {
                List<String> usedBy = cryptoTokenManagementSession.isCryptoTokenSlotUsed(getAuthenticationToken(), cryptoTokenName, className, cryptoTokenPropertes);
                if (!usedBy.isEmpty() && !ignoreslotwarning) {
                    for (String usedByName : usedBy) {
                        String name = usedByName;
                        if (StringUtils.isNumeric(name)) {
                            // if the crypto token name is purely numeric, it is likely to be a database protection token
                            name = name + " (database protection?)";
                        }
                        getLogger().info("The P11 slot is already used by another crypto token: "+name);
                    }
                    getLogger().info("Do you want to continue anyhow? [yes/no]: ");
                    String yes = System.console().readLine();
                    if (!StringUtils.equalsIgnoreCase("yes", yes)) {
                        getLogger().info("Exiting...");
                        return CommandResult.CLI_FAILURE;                    
                    }
                }
            } catch (CryptoTokenNameInUseException | CryptoTokenOfflineException | CryptoTokenAuthenticationFailedException
                    | AuthorizationDeniedException | NoSuchSlotException e) {
                getLogger().info("There is an error creating the Crypto Token: "+e.getMessage());
                getLogger().info("Do you want to continue anyhow? [yes/no]: ");
                String yes = System.console().readLine();
                if (!StringUtils.equalsIgnoreCase("yes", yes)) {
                    getLogger().info("Exiting...");
                    return CommandResult.CLI_FAILURE;                    
                }
            }

        } else {
            getLogger().info("Invalid CryptoToken type: " + type);
            return CommandResult.CLI_FAILURE;
        }
        String useExplicitKeyParameter = parameters.get(USE_EXPLICIT_KEY_PARAMETERS);
        if (useExplicitKeyParameter != null) {
            cryptoTokenPropertes.setProperty(CryptoToken.EXPLICIT_ECC_PUBLICKEY_PARAMETERS,
                    Boolean.toString(Boolean.valueOf(useExplicitKeyParameter)));
        }
        final char[] authenticationCode = getAuthenticationCode(parameters.get(PIN_KEY));
        if (autoActivate) {
            BaseCryptoToken.setAutoActivatePin(cryptoTokenPropertes, new String(authenticationCode), true);
        }
        try {
            final Integer cryptoTokenIdNew = cryptoTokenManagementSession.createCryptoToken(getAuthenticationToken(), cryptoTokenName, className,
                    cryptoTokenPropertes, null, authenticationCode);
            getLogger().info("CryptoToken with id " + cryptoTokenIdNew + " created successfully.");
            return CommandResult.SUCCESS;
        } catch (AuthorizationDeniedException e) {
            getLogger().info(e.getMessage());
            return CommandResult.AUTHORIZATION_FAILURE;
        } catch (CryptoTokenOfflineException e) {
            getLogger().info("CryptoToken is not active. You need to activate the CryptoToken before you can interact with its content.");
        } catch (Exception e) {
            getLogger().info("Operation failed: " + e.getMessage());
        }
        return CommandResult.FUNCTIONAL_FAILURE;
    }

    @Override
    public String getCommandDescription() {
        return "Create a new CryptoToken";
    }

    @Override
    public String getFullHelpText() {
        StringBuilder sb = new StringBuilder();
        sb.append(getCommandDescription() + "\n\n");
        sb.append("Slot Reference Types:\n");
        for (Pkcs11SlotLabelType type : Pkcs11SlotLabelType.values()) {
            sb.append("    " + type.getKey() + " - " + type.getDescription() + "\n");
        }
        return sb.toString();
    }

    @Override
    protected Logger getLogger() {
        return log;
    }
    
    /** @return a decrypted version of the parameter or use input if the parameter equals "null" */
    private char[] getAuthenticationCode(final String commandLineArgument) {
        final char[] authenticationCode;
        if (commandLineArgument == null || "null".equalsIgnoreCase(commandLineArgument)) {
            getLogger().info("Enter CryptoToken password: ");
            getLogger().info("");
            authenticationCode = StringTools.passwordDecryption(String.valueOf(System.console().readPassword()), "CryptoToken pin").toCharArray();
        } else {
            authenticationCode = StringTools.passwordDecryption(commandLineArgument, "CryptoToken pin").toCharArray();

        }
        return authenticationCode;
    }


}
