/**
 * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2006-2014.
 *
 * Licensed 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.italiangrid.voms.store.impl;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.security.auth.x500.X500Principal;

import org.italiangrid.voms.VOMSError;
import org.italiangrid.voms.store.LSCInfo;
import org.italiangrid.voms.store.VOMSTrustStore;
import org.italiangrid.voms.store.VOMSTrustStoreStatusListener;
import org.italiangrid.voms.util.NullListener;

import eu.emi.security.authn.x509.helpers.trust.OpensslTruststoreHelper;
import eu.emi.security.authn.x509.impl.CertificateUtils;
import eu.emi.security.authn.x509.impl.CertificateUtils.Encoding;

/**
 * 
 * The default implementation for the VOMS trust store. This implementation
 * <b>does not</b> refresh the trust information on a periodic basis. For an
 * updating trust store see {@link DefaultUpdatingVOMSTrustStore}.
 * 
 * @author Andrea Ceccanti
 * 
 */
public class DefaultVOMSTrustStore implements VOMSTrustStore {

  /**
   * The default directory where local VOMS trust information is rooted:
   * {@value #DEFAULT_VOMS_DIR}
   **/
  public static final String DEFAULT_VOMS_DIR = "/etc/grid-security/vomsdir";

  /**
   * The filename suffix used to match certificates in the VOMS local trust
   * directories
   **/
  public static final String CERTIFICATE_FILENAME_SUFFIX = ".pem";

  /**
   * The filename suffix used to match LSC files in the VOMS local trust
   * directories
   **/
  public static final String LSC_FILENAME_SUFFIX = ".lsc";

  /**
   * The list of local trusted directories that is searched for trust
   * information (certs or LSC files)
   **/
  private final List<String> localTrustedDirs;

  /** Map of local parsed AA certificates keyed by certificate subject hash **/
  private Map<String, X509Certificate> localAACertificatesByHash = new HashMap<String, X509Certificate>();

  /** The set of local parsed LSC information keyed by VO **/
  private Map<String, Set<LSCInfo>> localLSCInfo = new HashMap<String, Set<LSCInfo>>();

  /**
   * The trust store status listener that will be notified of changes in this
   * trust store
   **/
  private VOMSTrustStoreStatusListener listener;

  /** The read/write lock that implements thread safety for this store **/
  protected final ReadWriteLock rwLock = new ReentrantReadWriteLock();

  /** A reference to the read lock **/
  protected final Lock read = rwLock.readLock();

  /** A reference to the write lock **/
  protected final Lock write = rwLock.writeLock();

  /** A lock to guard the setting of the status listener **/
  protected final Object listenerLock = new Object();

  /**
   * Builds a list of trusted directories containing only
   * {@link #DEFAULT_VOMS_DIR}.
   * 
   * @return a list of default trusted directory containing the
   *         {@link #DEFAULT_VOMS_DIR}
   **/
  protected static List<String> buildDefaultTrustedDirs() {

    List<String> tDirs = new ArrayList<String>();
    tDirs.add(DEFAULT_VOMS_DIR);
    return tDirs;
  }

  /**
   * 
   * @param localTrustDirs
   *          a non-null list of local trust directories
   * @param listener
   *          the {@link VOMSTrustStoreStatusListener} to use for this trust
   *          store
   * @throws IllegalArgumentException
   *           when the list passed as argument is null
   * 
   */
  public DefaultVOMSTrustStore(List<String> localTrustDirs,
    VOMSTrustStoreStatusListener listener) {

    if (localTrustDirs == null)
      throw new IllegalArgumentException(
        "Please provide a non-null list of local trust directories!");

    this.localTrustedDirs = localTrustDirs;
    this.listener = listener;
    loadTrustInformation();
  }

  public DefaultVOMSTrustStore(VOMSTrustStoreStatusListener listener) {

    this(buildDefaultTrustedDirs(), listener);
  }

  public DefaultVOMSTrustStore(List<String> localTrustDirs) {

    this(localTrustDirs, NullListener.INSTANCE);
  }

  /**
   * Default constructor.
   * 
   * Sets the local trusted directories to the default of
   * {@value #DEFAULT_VOMS_DIR}.
   * 
   * 
   */
  public DefaultVOMSTrustStore() {

    this(buildDefaultTrustedDirs());
  }

  public List<String> getLocalTrustedDirectories() {

    read.lock();
    try {
      return localTrustedDirs;
    } finally {
      read.unlock();
    }
  }

  public List<X509Certificate> getLocalAACertificates() {

    read.lock();

    try {
      return Collections.unmodifiableList(new ArrayList<X509Certificate>(
        localAACertificatesByHash.values()));
    } finally {
      read.unlock();
    }
  }

  public LSCInfo getLSC(String voName, String hostname) {

    read.lock();

    try {

      Set<LSCInfo> candidates = localLSCInfo.get(voName);

      if (candidates == null)
        return null;

      for (LSCInfo lsc : candidates) {

        if (lsc.getHostname().equals(hostname))
          return lsc;

      }

      return null;

    } finally {
      read.unlock();
    }
  }

  /**
   * Loads all the certificates in the local directory. Only files with the
   * extension matching the {@link #CERTIFICATE_FILENAME_PATTERN} are
   * considered.
   * 
   * @param directory
   */
  private void loadCertificatesFromDirectory(File directory) {

    directorySanityChecks(directory);

    synchronized (listenerLock) {
      listener.notifyCertficateLookupEvent(directory.getAbsolutePath());
    }

    File[] certFiles = directory.listFiles(new FilenameFilter() {

      public boolean accept(File dir, String name) {

        return name.endsWith(CERTIFICATE_FILENAME_SUFFIX);
      }
    });

    for (File f : certFiles)
      loadCertificateFromFile(f);

  }

  /**
   * Loads a VOMS AA certificate from a given file and stores this certificate
   * in the local map of trusted VOMS AA certificate.
   * 
   * @param file
   */
  private void loadCertificateFromFile(File file) {

    certificateFileSanityChecks(file);

    try {

      X509Certificate aaCert = CertificateUtils.loadCertificate(
        new FileInputStream(file), Encoding.PEM);

      // Get certificate subject hash, using the CANL implementation for CA
      // files
      String aaCertHash = getOpensslCAHash(aaCert.getSubjectX500Principal());

      // Store certificate in the local map
      localAACertificatesByHash.put(aaCertHash, aaCert);

      synchronized (listenerLock) {
        listener.notifyCertificateLoadEvent(aaCert, file);
      }

    } catch (IOException e) {
      String errorMessage = String.format(
        "Error parsing VOMS trusted certificate from %s. Reason: %s",
        file.getAbsolutePath(), e.getMessage());
      throw new VOMSError(errorMessage, e);
    }

  }

  /**
   * 
   * @param directory
   */
  private void loadLSCFromDirectory(File directory) {

    directorySanityChecks(directory);

    synchronized (listenerLock) {
      listener.notifyLSCLookupEvent(directory.getAbsolutePath());
    }

    File[] lscFiles = directory.listFiles(new FilenameFilter() {

      public boolean accept(File dir, String name) {

        return name.endsWith(LSC_FILENAME_SUFFIX);
      }
    });

    if (lscFiles.length == 0)
      return;

    DefaultLSCFileParser lscParser = new DefaultLSCFileParser();

    // In the VOMS trust anchor structure, LSC files are contained in a
    // directory named
    // as the VO the LSC belongs to
    String voName = directory.getName();

    for (File lsc : lscFiles) {

      String lscFileName = lsc.getName();

      // In the VOMS trust anchor structure, LSC files are named as
      // <hostname>.lsc where hostname
      // is the name of host where the VOMS AA is running
      String hostname = lscFileName.substring(0,
        lscFileName.indexOf(LSC_FILENAME_SUFFIX));

      LSCInfo info = null;

      info = lscParser.parse(voName, hostname, lsc);

      Set<LSCInfo> localLscForVo = localLSCInfo.get(voName);

      if (localLscForVo == null) {
        localLscForVo = new HashSet<LSCInfo>();
        localLSCInfo.put(voName, localLscForVo);
      }

      localLscForVo.add(info);

      listener.notifyLSCLoadEvent(info, lsc);

    }

  }

  /**
   * Performs basic sanity checks performed on a file supposed to hold a VOMS AA
   * certificate.
   * 
   * @param certFile
   */
  private void certificateFileSanityChecks(File certFile) {

    if (!certFile.exists())
      throw new VOMSError("Local VOMS trusted certificate does not exist:"
        + certFile.getAbsolutePath());

    if (!certFile.canRead())
      throw new VOMSError("Local VOMS trusted certificate is not readable:"
        + certFile.getAbsolutePath());

  }

  /**
   * Performs basic sanity checks on a directory that is supposed to contain
   * VOMS AA certificates and LSC files.
   * 
   * @param directory
   */
  private void directorySanityChecks(File directory) {

    if (!directory.exists())
      throw new VOMSError("Local trust directory does not exists:"
        + directory.getAbsolutePath());

    if (!directory.isDirectory())
      throw new VOMSError("Local trust directory is not a directory:"
        + directory.getAbsolutePath());

    if (!directory.canRead())
      throw new VOMSError("Local trust directory is not readable:"
        + directory.getAbsolutePath());

    if (!directory.canExecute())
      throw new VOMSError("Local trust directory is not traversable:"
        + directory.getAbsolutePath());

  }

  private void cleanupStores() {

    localAACertificatesByHash.clear();
    localLSCInfo.clear();

  }

  public void loadTrustInformation() {

    write.lock();

    try {

      if (localTrustedDirs.isEmpty()) {
        throw new VOMSError(
          "No local trust directory was specified for this trust store. Please provide at least one path where LSC and VOMS service certificates will be searched for.");
      }

      cleanupStores();

      for (String localDir : localTrustedDirs) {

        File baseTrustDir = new File(localDir);

        // Legacy VOMS dir structure put all the certificates in the base trust
        // directory
        loadCertificatesFromDirectory(baseTrustDir);

        // Load LSC and certificates files starting from each of the
        // sub-directory of the starting trust info directory
        File[] voDirs = baseTrustDir.listFiles(new FileFilter() {

          public boolean accept(File pathname) {

            return pathname.isDirectory();
          }
        });

        for (File voDir : voDirs) {
          loadLSCFromDirectory(voDir);
          loadCertificatesFromDirectory(voDir);
        }
      }

    } finally {
      write.unlock();
    }
  }

  private String getOpensslCAHash(X500Principal principal) {

    return OpensslTruststoreHelper.getOpenSSLCAHash(principal, false);

  }

  public X509Certificate getAACertificateBySubject(X500Principal aaCertSubject) {

    read.lock();
    try {
      String theCertHash = getOpensslCAHash(aaCertSubject);
      return localAACertificatesByHash.get(theCertHash);
    } finally {
      read.unlock();
    }
  }

  public Map<String, Set<LSCInfo>> getAllLSCInfo() {

    read.lock();
    try {
      return Collections.unmodifiableMap(localLSCInfo);
    } finally {
      read.unlock();
    }
  }

  public void setStatusListener(VOMSTrustStoreStatusListener statusListener) {

    synchronized (listenerLock) {
      this.listener = statusListener;
    }
  }
}
