package org.exoplatform.portal.gadget.core;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Iterator;
import java.util.Map;

import net.oauth.OAuth;
import net.oauth.OAuthConsumer;
import net.oauth.OAuthServiceProvider;
import net.oauth.signature.RSA_SHA1;

import org.apache.shindig.auth.SecurityToken;
import org.apache.shindig.gadgets.GadgetException;
import org.apache.shindig.gadgets.oauth.BasicOAuthStoreConsumerIndex;
import org.apache.shindig.gadgets.oauth.BasicOAuthStoreConsumerKeyAndSecret;
import org.apache.shindig.gadgets.oauth.BasicOAuthStoreConsumerKeyAndSecret.KeyType;
import org.apache.shindig.gadgets.oauth.BasicOAuthStoreTokenIndex;
import org.apache.shindig.gadgets.oauth.OAuthStore;
import org.exoplatform.container.ExoContainer;
import org.exoplatform.container.PortalContainer;
import org.json.JSONException;
import org.json.JSONObject;

import com.google.common.collect.Maps;

/*
 * Created by The eXo Platform SAS
 * Author : tung.dang
 *          tungcnw@gmail.com
 * Dec 10, 2009
 *
 */

/**
 * Simple implementation of the {@link OAuthStore} interface. We use a in-memory hash map. If initialized with a private key,
 * then the store will return an OAuthAccessor in {@code getOAuthAccessor} that uses that private key if no consumer key and
 * secret could be found.
 */

public class ExoOAuthStore implements OAuthStore {

    private static final String CONSUMER_SECRET_KEY = "consumer_secret";
    private static final String CONSUMER_KEY_KEY = "consumer_key";
    private static final String KEY_TYPE_KEY = "key_type";
    private static final String CALLBACK_URL = "callback_url";

    /**
     * HashMap of provider and consumer information. Maps BasicOAuthStoreConsumerIndexs (i.e. nickname of a service provider and
     * the gadget that uses that nickname) to {@link BasicOAuthStoreConsumerKeyAndSecret}s.
     */
    private final Map<BasicOAuthStoreConsumerIndex, BasicOAuthStoreConsumerKeyAndSecret> consumerInfos;

    /**
     * HashMap of token information. Maps BasicOAuthStoreTokenIndexs (i.e. gadget id, token nickname, module id, etc.) to
     * TokenInfos (i.e. access token and token secrets).
     */
    // TODO: tung.dang don't need it, we store token in our memory.
    // private Map<BasicOAuthStoreTokenIndex, TokenInfo> tokens = Maps.newHashMap();

    /**
     * Key to use when no other key is found.
     */
    private BasicOAuthStoreConsumerKeyAndSecret defaultKey;

    /**
     * Callback to use when no per-key callback URL is found.
     */
    private String defaultCallbackUrl;

    /** Number of times we looked up a consumer key */
    private int consumerKeyLookupCount = 0;

    /** Number of times we looked up an access token */
    private int accessTokenLookupCount = 0;

    /** Number of times we added an access token */
    private int accessTokenAddCount = 0;

    /** Number of times we removed an access token */
    private int accessTokenRemoveCount = 0;

    public ExoOAuthStore() {
        consumerInfos = Maps.newHashMap();
    }

    public void initFromConfigString(String oauthConfigStr) throws GadgetException {
        try {
            JSONObject oauthConfigs = new JSONObject(oauthConfigStr);
            for (Iterator<?> i = oauthConfigs.keys(); i.hasNext();) {
                String url = (String) i.next();
                URI gadgetUri = new URI(url);
                JSONObject oauthConfig = oauthConfigs.getJSONObject(url);
                storeConsumerInfos(gadgetUri, oauthConfig);
            }
        } catch (JSONException e) {
            throw new GadgetException(GadgetException.Code.OAUTH_STORAGE_ERROR, e);
        } catch (URISyntaxException e) {
            throw new GadgetException(GadgetException.Code.OAUTH_STORAGE_ERROR, e);
        }
    }

    private void storeConsumerInfos(URI gadgetUri, JSONObject oauthConfig) throws JSONException, GadgetException {
        for (String serviceName : JSONObject.getNames(oauthConfig)) {
            JSONObject consumerInfo = oauthConfig.getJSONObject(serviceName);
            storeConsumerInfo(gadgetUri, serviceName, consumerInfo);
        }
    }

    private void storeConsumerInfo(URI gadgetUri, String serviceName, JSONObject consumerInfo) throws JSONException {
        realStoreConsumerInfo(gadgetUri, serviceName, consumerInfo);
    }

    private void realStoreConsumerInfo(URI gadgetUri, String serviceName, JSONObject consumerInfo) throws JSONException {
        String callbackUrl = consumerInfo.optString(CALLBACK_URL, null);
        String consumerSecret = consumerInfo.getString(CONSUMER_SECRET_KEY);
        String consumerKey = consumerInfo.getString(CONSUMER_KEY_KEY);
        String keyTypeStr = consumerInfo.getString(KEY_TYPE_KEY);
        KeyType keyType = KeyType.HMAC_SYMMETRIC;

        if (keyTypeStr.equals("RSA_PRIVATE")) {
            keyType = KeyType.RSA_PRIVATE;
            consumerSecret = convertFromOpenSsl(consumerSecret);
        }

        BasicOAuthStoreConsumerKeyAndSecret kas = new BasicOAuthStoreConsumerKeyAndSecret(consumerKey, consumerSecret, keyType,
                null, callbackUrl);

        BasicOAuthStoreConsumerIndex index = new BasicOAuthStoreConsumerIndex();
        index.setGadgetUri(gadgetUri.toASCIIString());
        index.setServiceName(serviceName);
        setConsumerKeyAndSecret(index, kas);
    }

    // Support standard openssl keys by stripping out the headers and blank lines
    public static String convertFromOpenSsl(String privateKey) {
        return privateKey.replaceAll("-----[A-Z ]*-----", "").replace("\n", "");
    }

    public void setDefaultKey(BasicOAuthStoreConsumerKeyAndSecret defaultKey) {
        this.defaultKey = defaultKey;
    }

    public void setDefaultCallbackUrl(String defaultCallbackUrl) {
        this.defaultCallbackUrl = defaultCallbackUrl;
    }

    public void setConsumerKeyAndSecret(BasicOAuthStoreConsumerIndex providerKey,
            BasicOAuthStoreConsumerKeyAndSecret keyAndSecret) {
        consumerInfos.put(providerKey, keyAndSecret);
    }

    public ConsumerInfo getConsumerKeyAndSecret(SecurityToken securityToken, String serviceName, OAuthServiceProvider provider)
            throws GadgetException {
        ++consumerKeyLookupCount;
        BasicOAuthStoreConsumerIndex pk = new BasicOAuthStoreConsumerIndex();
        pk.setGadgetUri(securityToken.getAppUrl());
        pk.setServiceName(serviceName);
        BasicOAuthStoreConsumerKeyAndSecret cks = consumerInfos.get(pk);
        if (cks == null) {
            cks = defaultKey;
        }
        if (cks == null) {
            throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, "No key for gadget "
                    + securityToken.getAppUrl() + " and service " + serviceName);
        }
        OAuthConsumer consumer = null;
        if (cks.getKeyType() == KeyType.RSA_PRIVATE) {
            consumer = new OAuthConsumer(null, cks.getConsumerKey(), null, provider);
            // The oauth.net java code has lots of magic. By setting this property here, code thousands
            // of lines away knows that the consumerSecret value in the consumer should be treated as
            // an RSA private key and not an HMAC key.
            consumer.setProperty(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.RSA_SHA1);
            consumer.setProperty(RSA_SHA1.PRIVATE_KEY, cks.getConsumerSecret());
        } else {
            consumer = new OAuthConsumer(null, cks.getConsumerKey(), cks.getConsumerSecret(), provider);
            consumer.setProperty(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.HMAC_SHA1);
        }
        String callback = (cks.getCallbackUrl() != null ? cks.getCallbackUrl() : defaultCallbackUrl);
        return new ConsumerInfo(consumer, cks.getKeyName(), callback);
    }

    private BasicOAuthStoreTokenIndex makeBasicOAuthStoreTokenIndex(SecurityToken securityToken, String serviceName,
            String tokenName) {
        BasicOAuthStoreTokenIndex tokenKey = new BasicOAuthStoreTokenIndex();
        tokenKey.setGadgetUri(securityToken.getAppUrl());

        // TODO: tung.dang need to improve, why moduleId different each time?.
        // tokenKey.setModuleId(securityToken.getModuleId());

        tokenKey.setServiceName(serviceName);
        tokenKey.setTokenName(tokenName);
        tokenKey.setUserId(securityToken.getViewerId());
        return tokenKey;
    }

    public TokenInfo getTokenInfo(SecurityToken securityToken, ConsumerInfo consumerInfo, String serviceName, String tokenName) {
        ++accessTokenLookupCount;
        BasicOAuthStoreTokenIndex tokenKey = makeBasicOAuthStoreTokenIndex(securityToken, serviceName, tokenName);

        ExoContainer container = PortalContainer.getInstance();
        GadgetTokenInfoService tokenSer = (GadgetTokenInfoService) container
                .getComponentInstanceOfType(GadgetTokenInfoService.class);
        return tokenSer.getToken(tokenKey);
    }

    public void setTokenInfo(SecurityToken securityToken, ConsumerInfo consumerInfo, String serviceName, String tokenName,
            TokenInfo tokenInfo) {
        ++accessTokenAddCount;
        BasicOAuthStoreTokenIndex tokenKey = makeBasicOAuthStoreTokenIndex(securityToken, serviceName, tokenName);
        ExoContainer container = PortalContainer.getInstance();
        GadgetTokenInfoService tokenSer = (GadgetTokenInfoService) container
                .getComponentInstanceOfType(GadgetTokenInfoService.class);
        tokenSer.createToken(tokenKey, tokenInfo);
    }

    public void removeToken(SecurityToken securityToken, ConsumerInfo consumerInfo, String serviceName, String tokenName) {
        ++accessTokenRemoveCount;
        BasicOAuthStoreTokenIndex tokenKey = makeBasicOAuthStoreTokenIndex(securityToken, serviceName, tokenName);
        ExoContainer container = PortalContainer.getInstance();
        GadgetTokenInfoService tokenSer = (GadgetTokenInfoService) container
                .getComponentInstanceOfType(GadgetTokenInfoService.class);
        tokenSer.deleteToken(tokenKey);
    }

    public int getConsumerKeyLookupCount() {
        return consumerKeyLookupCount;
    }

    public int getAccessTokenLookupCount() {
        return accessTokenLookupCount;
    }

    public int getAccessTokenAddCount() {
        return accessTokenAddCount;
    }

    public int getAccessTokenRemoveCount() {
        return accessTokenRemoveCount;
    }
}
