package com.kryptnostic.v2.crypto;

import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import retrofit.RetrofitError;

import com.google.common.base.Optional;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Maps;
import com.kryptnostic.api.v1.KryptnosticConnection;
import com.kryptnostic.kodex.v1.crypto.ciphers.AesCryptoService;
import com.kryptnostic.kodex.v1.crypto.ciphers.CryptoService;
import com.kryptnostic.kodex.v1.crypto.ciphers.Cypher;
import com.kryptnostic.kodex.v1.exceptions.types.SecurityConfigurationException;
import com.kryptnostic.v2.storage.api.KeyStorageApi;
import com.kryptnostic.v2.storage.api.ObjectStorageApi;
import com.kryptnostic.v2.storage.models.VersionedObjectKey;
import com.kryptnostic.v2.storage.models.VersionedObjectKeySet;

/**
 * Provider class for {@link CryptoService} objects used for encrypting and decrypting objects.
 * 
 * @author Matthew Tamayo-Rios &lt;matthew@kryptnostic.com&gt;
 *
 */
public class KryptnosticCryptoServiceLoader implements CryptoServiceLoader {
    private static final Logger                                   logger = LoggerFactory
                                                                                 .getLogger( KryptnosticCryptoServiceLoader.class );

    private final LoadingCache<VersionedObjectKey, CryptoService> keyCache;
    private KeyStorageApi                                         directoryApi;
    private ObjectStorageApi                                      objectStorageApi;
    private KryptnosticConnection                                 connection;
    private Cypher                                                cypher;

    public KryptnosticCryptoServiceLoader(
            final KryptnosticConnection connection,
            final KeyStorageApi directoryApi,
            ObjectStorageApi objectStorageApi,
            Cypher cypher ) {
        this.directoryApi = directoryApi;
        this.objectStorageApi = objectStorageApi;
        this.connection = connection;
        this.cypher = cypher;
        keyCache = CacheBuilder.newBuilder().maximumSize( 1000 ).expireAfterWrite( 10, TimeUnit.MINUTES )
                .build( new CacheLoader<VersionedObjectKey, CryptoService>() {
                    @Override
                    public Map<VersionedObjectKey, CryptoService> loadAll( Iterable<? extends VersionedObjectKey> keys )
                            throws IOException,
                            SecurityConfigurationException {
                        VersionedObjectKeySet ids = new VersionedObjectKeySet();

                        for ( VersionedObjectKey key : keys ) {
                            ids.add( key );
                        }

                        Map<VersionedObjectKey, byte[]> data = directoryApi.getObjectCryptoServices( ids );
                        if ( data.size() != ids.size() ) {
                            throw new InvalidCacheLoadException( "Unable to retrieve all keys." );
                        }
                        Map<VersionedObjectKey, CryptoService> processedData = Maps.newHashMap();

                        for ( Map.Entry<VersionedObjectKey, byte[]> entry : data.entrySet() ) {
                            byte[] crypto = entry.getValue();
                            if ( crypto != null ) {
                                CryptoService service = connection.getCryptoManager().getRsaCryptoService().decrypt(
                                        crypto,
                                        AesCryptoService.class );
                                processedData.put( entry.getKey(), service );
                            }
                        }
                        return processedData;
                    }

                    @Override
                    public CryptoService load( VersionedObjectKey key ) throws IOException,
                            SecurityConfigurationException {
                        byte[] crypto = null;
                        try {
                            crypto = directoryApi.getObjectCryptoService( key.getObjectId(), key.getVersion() );
                        } catch ( RetrofitError e ) {
                            logger.error( "Failed to load crypto service from backend for id {} ", key, e );
                            throw new IOException( e );
                        }
                        if ( ( crypto == null ) ) {
                            try {
                                CryptoService cs = new AesCryptoService( KryptnosticCryptoServiceLoader.this.cypher );
                                put( key, cs );
                                return cs;
                            } catch (
                                    NoSuchAlgorithmException
                                    | InvalidAlgorithmParameterException
                                    | ExecutionException e ) {
                                logger.error( "Failed while trying to create new crypto service for object id: {} ",
                                        key );
                            }
                        }
                        return connection
                                .getCryptoManager()
                                .getRsaCryptoService()
                                .decrypt( crypto, AesCryptoService.class );
                    }
                } );
    }

    @Override
    public Optional<CryptoService> get( VersionedObjectKey id ) throws ExecutionException {
        return Optional.fromNullable( keyCache.get( id ) );
    }

    @Override
    public void put( VersionedObjectKey id, CryptoService service ) throws ExecutionException {
        keyCache.put( id, service );
        try {
            byte[] cs = connection.getCryptoManager().getRsaCryptoService().encrypt( service );
            directoryApi.setObjectCryptoService( id.getObjectId(), id.getVersion(), cs );
        } catch ( SecurityConfigurationException | IOException e ) {
            throw new ExecutionException( e );
        }
    }

    @Override
    public Map<VersionedObjectKey, CryptoService> getAll( Set<VersionedObjectKey> ids ) throws ExecutionException {
        return keyCache.getAllPresent( ids );
    }

    @Override
    public void clear() {
        keyCache.invalidateAll();
        keyCache.cleanUp();
    }

    @Override
    public Optional<CryptoService> getLatest( UUID id ) throws ExecutionException {
        return get( VersionedObjectKey.fromObjectMetadata( objectStorageApi.getObjectMetadata( id ) ) );
    }
}
@@ -1275,10 +1275,9 @@
 .%0A *
- 
 %0A
+
  * @
@@ -1784,25 +1784,26 @@
         
-directory
+keyStorage
 Api;%0A   
@@ -2163,24 +2163,18 @@
 rageApi 
-director
+ke
 yApi,%0A  
@@ -2265,31 +2265,26 @@
 his.
-directoryApi = director
+keyStorageApi = ke
 yApi
@@ -2509,16 +2509,68 @@
 NUTES )%0A
+                // TODO: make this a separate class%0A
         
@@ -3180,24 +3180,18 @@
  data = 
-director
+ke
 yApi.get
@@ -4442,24 +4442,18 @@
 rypto = 
-director
+ke
 yApi.get
@@ -6229,17 +6229,18 @@
     
-directory
+keyStorage
 Api.

rename a variable for clarity