/*
 * JORAM: Java(TM) Open Reliable Asynchronous Messaging
 * Copyright (C) 2001 - 2025 ScalAgent Distributed Technologies
 *
 * This library 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.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA.
 *
 * Initial developer(s): Sofiane Chibani
 * Contributor(s): David Feliot, Nicolas Tachker
 */
package fr.dyade.aaa.jndi2.impl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Enumeration;
import java.util.Hashtable;

import javax.naming.CompositeName;
import javax.naming.NamingException;

import org.objectweb.util.monolog.api.BasicLevel;

import fr.dyade.aaa.agent.AgentServer;
import fr.dyade.aaa.common.crypto.AESGCMTools;
import fr.dyade.aaa.common.crypto.Crypto;
import fr.dyade.aaa.util.Transaction;

public class StorageManager {
  private final static String ENCRYPT_CONFIGURATION_PROPERTY = "fr.dyade.aaa.jndi2.ENCRYPT_CONFIGURATION";
  private final static boolean ENCRYPT_CONFIGURATION = AgentServer.getBoolean(ENCRYPT_CONFIGURATION_PROPERTY);

  private final static String CRYPTO_ALGO_PROPERTY = "fr.dyade.aaa.jndi2.CRYPTO_ALGO";
  private final static String CRYPTO_DFLT_ALGO = "AES/GCM/NoPadding";
  private final static String CRYPTO_ALGO = AgentServer.getProperty(CRYPTO_ALGO_PROPERTY, CRYPTO_DFLT_ALGO);

  private final static String CRYPTO_PASSWORD_PROPERTY = "fr.dyade.aaa.jndi2.CRYPTO_PASWWORD";
  private final static String CRYPTO_DFLT_PASSWORD = "password";
  private final static String CRYPTO_PASSWORD = AgentServer.getProperty(CRYPTO_PASSWORD_PROPERTY, CRYPTO_DFLT_PASSWORD);
  private static final int CRYPTO_PASSWORD_MIN_LENGTH = 8;

  private final static String CRYPTO_SALT_PROPERTY = "fr.dyade.aaa.jndi2.CRYPTO_SALT";
  private final static String CRYPTO_DFLT_SALT = "saltsalt";
  private final static String CRYPTO_SALT = AgentServer.getProperty(CRYPTO_SALT_PROPERTY);
  private static final int CRYPTO_SALT_MIN_LENGTH = 8;

  // Note: Be careful, nonce/iv length needs to be 16 for somes algorithms.
  private final static String CRYPTO_NONCE_PROPERTY = "fr.dyade.aaa.jndi2.CRYPTO_NONCE";
  private final static String CRYPTO_DFLT_NONCE = "0123456789012345";
  // Note: Currently unused, the user name is used as nonce.
  private final static String CRYPTO_NONCE = AgentServer.getProperty(CRYPTO_NONCE_PROPERTY);

  private final static String CRYPTO_ITERATION_COUNT_PROPERTY = "fr.dyade.aaa.jndi2.CRYPTO_ITERATION_COUNT";
  private final static int CRYPTO_DFLT_ITERATION_COUNT = 10000;
  private final static int CRYPTO_ITERATION_COUNT = AgentServer.getInteger(CRYPTO_ITERATION_COUNT_PROPERTY, CRYPTO_DFLT_ITERATION_COUNT);

  private final static String CRYPTO_KEY_LENGTH_PROPERTY = "fr.dyade.aaa.jndi2.CRYPTO_KEY_LENGTH";
  private final static int CRYPTO_DFLT_KEY_LENGTH = 256;
  private final static int CRYPTO_KEY_LENGTH = AgentServer.getInteger(CRYPTO_KEY_LENGTH_PROPERTY, CRYPTO_DFLT_KEY_LENGTH);

  private transient Crypto crypto = null;

  public static final String ROOT = "jndiStorage";

  public static final String CTX_COUNTER = "jndiCtxCounter";

  public static final String CTX_INDEX = "jndiCtxIndex";

  private long contextCounter;

  private Transaction transaction;

  private Hashtable<CompositeName, NamingContextId> nameToIdIndex;

  private Object serverId;

  public StorageManager(Transaction transaction, Object serverId) {
    this.transaction = transaction;
    this.serverId = serverId;
  }

  public void initialize() throws Exception {
    // Load the local context counter
    Long counter = (Long) transaction.load(CTX_COUNTER);
    if (counter == null) {
      contextCounter = 0;
    } else {
      contextCounter = counter.longValue();
    }
    
    if (ENCRYPT_CONFIGURATION) {
      try {
        String salt = (CRYPTO_SALT==null)?CRYPTO_DFLT_SALT:CRYPTO_SALT;
        String nonce = (CRYPTO_NONCE==null)?CRYPTO_DFLT_NONCE:CRYPTO_NONCE;

        if (CRYPTO_DFLT_PASSWORD.equals(CRYPTO_PASSWORD))
          Trace.logger.log(BasicLevel.WARN, "Do not use the default master password.");
        if (CRYPTO_PASSWORD.length() < CRYPTO_PASSWORD_MIN_LENGTH)
          Trace.logger.log(BasicLevel.WARN, "Master password length must be at least " + CRYPTO_PASSWORD_MIN_LENGTH);
        if (salt.length() < CRYPTO_SALT_MIN_LENGTH)
          Trace.logger.log(BasicLevel.WARN, "Salt length must be at least " + CRYPTO_SALT_MIN_LENGTH);

        crypto = AESGCMTools.createCrypto(CRYPTO_ALGO, CRYPTO_PASSWORD, salt, nonce, CRYPTO_ITERATION_COUNT, CRYPTO_KEY_LENGTH);
      } catch (NoSuchAlgorithmException|InvalidKeySpecException exc) {
        String errorMsg = "Cannot initialize cryptographic module: ";
        if (Trace.DEBUG)
          Trace.logger.log(BasicLevel.WARN, errorMsg, exc);
        else
          Trace.logger.log(BasicLevel.WARN, errorMsg + exc);
        // TODO (AF): the module is not created, crypto is null!!
        // Should we continue without encryption, or should we exit abruptly?
      }
      if (Trace.DEBUG)
        Trace.logger.log(BasicLevel.DEBUG, "crypto=" + crypto);
    }
    
    // Load the context index
    nameToIdIndex = (Hashtable<CompositeName, NamingContextId>) transaction.load(CTX_INDEX);
    if (nameToIdIndex == null) {
      nameToIdIndex = new Hashtable<CompositeName, NamingContextId>();
    }
  }

  public NamingContext newNamingContext(Object ownerId, NamingContextId ncid,
                                        CompositeName name) throws NamingException {
    if (Trace.DEBUG)
      Trace.logger.log(BasicLevel.DEBUG, "StorageManager.newNamingContext(" + ownerId + ',' + name + ')');
    if (ncid == null)
      ncid = newNamingContextId();
    
    NamingContext nc = new NamingContext(ncid, ownerId, name);
    addNamingContext(nc, name);
    return nc;
  }

  public void addNamingContext(NamingContext nc,
                               CompositeName name) throws NamingException {
    if (Trace.DEBUG)
      Trace.logger.log(BasicLevel.DEBUG, "StorageManager.addNamingContext(" + nc
                                         + ',' + name + ')');
    nameToIdIndex.put(name, nc.getId());
    storeIndex();
    storeNamingContext(nc);
  }

  private NamingContextId newNamingContextId() throws NamingException {
    if (Trace.DEBUG)
      Trace.logger.log(BasicLevel.DEBUG, "StorageManager.newNamingContextId()");
    
    NamingContextId ncid = new NamingContextId(serverId, contextCounter);
    contextCounter++;
    try {
      transaction.save(Long.valueOf(contextCounter), CTX_COUNTER);
      return ncid;
    } catch (IOException ioexc) {
      NamingException nexc = new NamingException();
      nexc.setRootCause(ioexc);
      throw nexc;
    }
  }

  public void storeNamingContext(NamingContext nc) throws NamingException {
    if (Trace.DEBUG)
      Trace.logger.log(BasicLevel.DEBUG, "StorageManager.storeNamingContext(" + nc + ')');
    
    try {
      String name = nc.getId().toString();
      
      if ((ENCRYPT_CONFIGURATION) && (crypto != null)) {
        ByteArrayOutputStream bos = new ByteArrayOutputStream(256);
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(nc);
        oos.flush();
        byte[] content = bos.toByteArray();
        
        byte[] encrypted;
        try {
          encrypted = crypto.encrypt(content, name.getBytes("UTF-8"));
        } catch (GeneralSecurityException exc) {
          Trace.logger.log(BasicLevel.WARN, "Cannot encrypt " + name + ": " + exc);
          encrypted = content;
        }

        transaction.saveByteArray(encrypted, ROOT, name);
      } else {
        transaction.save(nc, ROOT, name);
      }
    } catch (IOException exc) {
      NamingException ne = new NamingException(exc.getMessage());
      ne.setRootCause(exc);
      throw ne;
    }
  }

  public NamingContext loadNamingContext(NamingContextId ncid) throws NamingException {
    if (Trace.DEBUG)
      Trace.logger.log(BasicLevel.DEBUG, "StorageManager.loadNamingContext(" + ncid + ')');
    
    return loadNamingContext(ncid.toString());
  }

  public NamingContext loadNamingContext(String name) throws NamingException {
    if (Trace.DEBUG)
      Trace.logger.log(BasicLevel.DEBUG, "StorageManager.loadNamingContext(" + name + ')');
    
    try {
      NamingContext nc = null;
      
      if ((ENCRYPT_CONFIGURATION) && (crypto != null)) {
        byte[] encrypted = transaction.loadByteArray(ROOT, name);
        if (encrypted == null) return null;
        
        byte[] content;
        try {
          content = crypto.decrypt(encrypted, name.getBytes("UTF-8"));
        } catch (GeneralSecurityException exc) {
          Trace.logger.log(BasicLevel.WARN, "Cannot decrypt " + name + ": " + exc);
          content = encrypted;
        }
        try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(content))) {
          nc = (NamingContext) ois.readObject();
        }
      } else {
        nc = (NamingContext) transaction.load(ROOT, name);
      }
      if (Trace.DEBUG)
        Trace.logger.log(BasicLevel.DEBUG, "Loads naming context: " + name + " -> " + nc);
      return nc;
    } catch (IOException exc) {
      if (Trace.DEBUG)
        Trace.logger.log(BasicLevel.DEBUG, "Cannot load naming context: " + name, exc);
      NamingException ne = new NamingException(exc.getMessage());
      ne.setRootCause(exc);
      throw ne;
    } catch (ClassNotFoundException exc2) {
      if (Trace.DEBUG)
        Trace.logger.log(BasicLevel.DEBUG, "Cannot load naming context: ", exc2);
      NamingException ne = new NamingException(exc2.getMessage());
      ne.setRootCause(exc2);
      throw ne;
    }
  }

  public void delete(NamingContextId ncid,
                     CompositeName name) throws NamingException {
    if (Trace.DEBUG)
      Trace.logger.log(BasicLevel.DEBUG,
                       "StorageManager.delete(" + ncid + ',' + name + ')');
    transaction.delete(ROOT, ncid.toString());
    nameToIdIndex.remove(name);
    storeIndex();
  }

  private void storeIndex() throws NamingException {
    try {
      transaction.save(nameToIdIndex, CTX_INDEX);
    } catch (IOException exc) {
      NamingException ne = new NamingException(exc.getMessage());
      ne.setRootCause(exc);
      throw ne;
    }
  }

  public Enumeration<NamingContextId> getContextIds() {
    return nameToIdIndex.elements();
  }

  public Enumeration<CompositeName> getContextNames() {
    return nameToIdIndex.keys();
  }

  public NamingContextId getIdFromName(CompositeName name) {
    return nameToIdIndex.get(name);
  }
}
