/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.activemq.artemis.utils;

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.ServiceLoader;

import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.ActiveMQExceptionType;
import org.apache.activemq.artemis.logs.ActiveMQUtilBundle;

public final class PasswordMaskingUtil {

   public static final String BEGIN_ENC = "ENC(";
   public static final String END_ENC = ")";

   private PasswordMaskingUtil() {

   }

   /**
    * This method deals with password masking and returns the password in its plain text form.
    *
    * @param password   : the original value of password string; interpreted as masked if wrapped in ENC() or as plain
    *                   text otherwise.
    * @param codecClass : the codec used to decode the password. Only when the password is interpreted as masked will
    *                   this codec be used. Ignored otherwise.
    */
   public static String resolveMask(String password, String codecClass) throws Exception {
      return resolveMask(null, password, codecClass);
   }

   /**
    * This method deals with password masking and returns the password in its plain text form.
    *
    * @param maskPassword : explicit mask flag. If it's true, the password is interpreted as masked. If it is false, the
    *                     password is interpreted as plain text. if it is null, the password will be interpreted as
    *                     masked if the password is wrapped in ENC(), or as plain text otherwise.
    * @param password     : the original value of password string
    * @param codecClass   : the codec used to decode the password. Only when the password is interpreted as masked will
    *                     this codec be used. Ignored otherwise.
    */
   public static String resolveMask(Boolean maskPassword, String password, String codecClass) throws Exception {
      String plainText = password;
      if (maskPassword == null) {
         if (isEncMasked(password)) {
            //masked
            String bareMaskedPassword = unwrap(password);
            plainText = getCodec(codecClass).decode(bareMaskedPassword);
         }
      } else if (maskPassword) {
         plainText = getCodec(codecClass).decode(password);
      }
      return plainText;
   }

   public static boolean isEncMasked(String password) {
      return password == null ? false : (password.startsWith(BEGIN_ENC) && password.endsWith(END_ENC));
   }

   //remove ENC() from the password body
   public static String unwrap(String password) {
      return password.substring(4, password.length() - 1);
   }

   public static String wrap(String password) {
      return BEGIN_ENC + password + END_ENC;
   }

   private static final class LazyPlainTextProcessorHolder {

      private LazyPlainTextProcessorHolder() {
      }

      private static final HashProcessor INSTANCE = new NoHashProcessor();

   }

   private static final class LazySecureProcessorHolder {

      private LazySecureProcessorHolder() {

      }

      private static final HashProcessor INSTANCE;
      private static final Exception EXCEPTION;

      static {
         HashProcessor processor = null;
         Exception exception = null;
         final String codecDesc = new StringBuilder().append(DefaultSensitiveStringCodec.class.getName()).append(";").append(DefaultSensitiveStringCodec.ALGORITHM).append("=").append(DefaultSensitiveStringCodec.ONE_WAY).toString();
         try {
            final DefaultSensitiveStringCodec codec = (DefaultSensitiveStringCodec) PasswordMaskingUtil.getCodec(codecDesc);
            processor = new SecureHashProcessor(codec);
         } catch (Exception e) {
            //THE STACK TRACE IS THE ORIGINAL ONE!
            exception = e;
         } finally {
            EXCEPTION = exception;
            INSTANCE = processor;
         }
      }
   }

   //stored password takes 2 forms, ENC() or plain text
   public static HashProcessor getHashProcessor(String storedPassword) {
      return getHashProcessor(storedPassword, null);
   }

   public static HashProcessor getHashProcessor(String storedPassword, HashProcessor secureHashProcessor) {

      if (!isEncoded(storedPassword)) {
         return LazyPlainTextProcessorHolder.INSTANCE;
      }
      if (secureHashProcessor != null) {
         return secureHashProcessor;
      }
      final Exception secureProcessorException = LazySecureProcessorHolder.EXCEPTION;
      if (secureProcessorException != null) {
         //reuse old descriptions/messages of the exception but refill the stack trace
         throw new RuntimeException(secureProcessorException);
      }
      return LazySecureProcessorHolder.INSTANCE;
   }

   private static boolean isEncoded(String storedPassword) {
      return storedPassword == null || isEncMasked(storedPassword);
   }

   public static HashProcessor getHashProcessor() {
      HashProcessor processor = LazySecureProcessorHolder.INSTANCE;
      //it can be null due to a previous failed attempts to instantiate it!
      if (processor == null) {
         processor = LazyPlainTextProcessorHolder.INSTANCE;
      }
      return processor;
   }

   /**
    * Loading the codec class.
    *
    * @param codecDesc This parameter must have the following format:
    *                  {@code <full qualified class name>;key=value;key1=value1;...} where only
    *                  {@code <full qualified class name>} is required. key/value pairs are optional
    */
   public static SensitiveDataCodec<String> getCodec(String codecDesc) throws ActiveMQException {
      SensitiveDataCodec<String> codecInstance;

      if (codecDesc == null) {
         return getDefaultCodec();
      }

      // semi colons
      String[] parts = codecDesc.split(";");
      if (parts.length < 1)
         throw new ActiveMQException(ActiveMQExceptionType.ILLEGAL_STATE, "Invalid PasswordCodec value: " + codecDesc);

      final String codecClassName = parts[0];

      // load class
      codecInstance = AccessController.doPrivileged((PrivilegedAction<SensitiveDataCodec<String>>) () -> {
         ServiceLoader<SensitiveDataCodec> serviceLoader = ServiceLoader.load(SensitiveDataCodec.class, PasswordMaskingUtil.class.getClassLoader());
         try {
            // Service load the codec, if a service is available
            for (SensitiveDataCodec<String> codec : serviceLoader) {
               if (codec.getClass().getCanonicalName().equals(codecClassName)) {
                  return codec.getClass().getDeclaredConstructor().newInstance();
               }
            }
         } catch (Exception e) {
            // Will ignore the exception and attempt to load the class directly
         }
         try {
            // If a service is not available, load the codec class using this class's class loader
            return (SensitiveDataCodec<String>) PasswordMaskingUtil.class.getClassLoader().loadClass(codecClassName).getDeclaredConstructor().newInstance();
         } catch (Exception e) {
            try {
               // As a last resort, load the codec class using the current thread's context class loader
               return (SensitiveDataCodec<String>) Thread.currentThread().getContextClassLoader().loadClass(codecClassName).getDeclaredConstructor().newInstance();
            } catch (Exception e2) {
               throw ActiveMQUtilBundle.BUNDLE.errorCreatingCodec(codecClassName, e2);
            }
         }
      });

      Map<String, String> props = new HashMap<>();
      if (parts.length > 1) {
         for (int i = 1; i < parts.length; i++) {
            String[] keyVal = parts[i].split("=");
            if (keyVal.length != 2)
               throw ActiveMQUtilBundle.BUNDLE.invalidProperty(parts[i]);
            props.put(keyVal[0], keyVal[1]);
         }
      }
      try {
         codecInstance.init(props);
      } catch (Exception e) {
         throw new ActiveMQException("Fail to init codec", e, ActiveMQExceptionType.SECURITY_EXCEPTION);
      }

      return codecInstance;
   }

   public static DefaultSensitiveStringCodec getDefaultCodec() {
      return getDefaultCodec(Collections.emptyMap());
   }

   public static DefaultSensitiveStringCodec getDefaultCodec(Map<String, String> params) {
      DefaultSensitiveStringCodec defaultCodec = new DefaultSensitiveStringCodec();
      try {
         defaultCodec.init(params);
      } catch (Exception e) {
         throw ActiveMQUtilBundle.BUNDLE.errorCreatingCodec(DefaultSensitiveStringCodec.class.getName(), e);
      }
      return defaultCodec;
   }

}
