/**
 * Copyright Red Hat, Inc, and individual contributors.
 *
 * 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 com.feedhenry.sdk;

import android.content.Context;
import com.feedhenry.sdk.api.FHActRequest;
import com.feedhenry.sdk.api.FHAuthRequest;
import com.feedhenry.sdk.api.FHAuthSession;
import com.feedhenry.sdk.api.FHCloudRequest;
import com.feedhenry.sdk.api.FHCloudRequest.Methods;
import com.feedhenry.sdk.api.FHInitializeRequest;
import com.feedhenry.sdk.exceptions.FHNotReadyException;
import com.feedhenry.sdk.utils.DataManager;
import com.feedhenry.sdk.utils.FHLog;
import cz.msebera.android.httpclient.Header;
import cz.msebera.android.httpclient.message.BasicHeader;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import org.jboss.aerogear.android.core.Callback;
import org.jboss.aerogear.android.unifiedpush.RegistrarManager;
import org.jboss.aerogear.android.unifiedpush.fcm.AeroGearFCMPushConfiguration;
import org.jboss.aerogear.android.unifiedpush.fcm.AeroGearFCMPushRegistrar;
import org.jboss.aerogear.android.unifiedpush.metrics.UnifiedPushMetricsMessage;
import org.json.fh.JSONObject;

/**
 * The FH class provides static methods to initialize the library, create new instances of all the
 * API request objects, and configure global settings.
 */
public class FH {

    private static boolean mReady = false;

    private static final String LOG_TAG = "com.feedhenry.sdk.FH";

    private static final String FH_API_ACT = "act";
    private static final String FH_API_AUTH = "auth";
    private static final String FH_API_CLOUD = "cloud";

    private static final String FH_PUSH_NAME = "FH";

    public static final int LOG_LEVEL_VERBOSE = 1;
    public static final int LOG_LEVEL_DEBUG = 2;
    public static final int LOG_LEVEL_INFO = 3;
    public static final int LOG_LEVEL_WARNING = 4;
    public static final int LOG_LEVEL_ERROR = 5;
    public static final int LOG_LEVEL_NONE = Integer.MAX_VALUE;

    private static int mLogLevel = LOG_LEVEL_ERROR;

    public static final String VERSION = "2.1.0"; // DO NOT CHANGE, the ant build task will automatically update this value. Update it in VERSION.txt

    private static boolean mInitCalled = false;

    private static Context mContext;

    private FH()  {
        throw new UnsupportedOperationException("Not Supported");
    }

    /**
     * Initializes the application.
     * This must be called before the application can use the FH library.
     * The initialization process happens in a background thread so that the UI thread won't be blocked.
     * If you need to call other FH API methods, you need to make sure they are called after the init
     * finishes. The best way to do it is to provide a FHActCallback instance and implement the success method.
     * The callback functions are invoked on the main UI thread.
     * For example, in your main activity class's onCreate method, you can do this:
     *
     * <pre>
     * {@code
     *  FH.init(this, new FHActCallback() {
     *    public void success(FHResponse pRes) {
     *      //pRes will be null for init call if it succeeds, don't use it to access response data
     *      FHActRequest request = FH.buildActRequest("readData", new JSONObject());
     *      request.executeAsync(new FHActCallback(){
     *        public void success(FHResponse pResp){
     *          //process response data
     *        }
     *
     *        public void fail(FHResponse pResp){
     *          //process error data
     *        }
     *      })
     *    }
     *
     *    public void fail(FHResponse pRes) {
     *      Log.e("FHInit", pRes.getErrorMessage(), pRes.getError());
     *    }
     *  });
     * }
     * </pre>
     *
     * @param pContext  your application's context
     * @param pCallback the callback function to be executed after initialization is finished
     */
    public static void init(Context pContext, FHActCallback pCallback) {
        mContext = pContext;
        if (!mInitCalled) {
            DataManager.init(mContext).migrateLegacyData();
            checkNetworkStatus();
            try {
                AppProps.load(mContext);
            } catch (IOException e) {
                mReady = false;
                FHLog.e(LOG_TAG, "Can not load property file.", e);
            }
            mInitCalled = true;
        }
        if (mReady) {
            if (pCallback != null) {
                pCallback.success(null);
            }
            return;
        }

        final FHActCallback cb = pCallback;
        if (AppProps.getInstance().isLocalDevelopment()) {
            FHLog.i(
                LOG_TAG,
                "Local development mode enabled, loading properties from assets/fhconfig.local.properties file");
            CloudProps.initDev();
            mReady = true;
            if (cb != null) {
                cb.success(null);
            }
            return;
        }
        FHInitializeRequest initRequest = new FHInitializeRequest(mContext);
        try {
            initRequest.executeAsync(
                new FHActCallback() {
                    @Override
                    public void success(FHResponse pResponse) {
                        mReady = true;
                        FHLog.v(LOG_TAG, "FH init response = " + pResponse.getJson().toString());
                        JSONObject cloudProps = pResponse.getJson();
                        CloudProps.init(cloudProps);
                        CloudProps.getInstance().save();
                        if (cb != null) {
                            cb.success(null);
                        }
                    }

                    @Override
                    public void fail(FHResponse pResponse) {
                        FHLog.e(
                            LOG_TAG,
                            "FH init failed with error = " + pResponse.getErrorMessage(),
                            pResponse.getError());
                        CloudProps props = CloudProps.load();
                        if (props != null) {
                            mReady = true;
                            FHLog.i(LOG_TAG, "Cached CloudProps data found");
                            if (cb != null) {
                                cb.success(null);
                            }
                        } else {
                            mReady = false;
                            FHLog.i(LOG_TAG, "No cache data found for CloudProps");
                            if (cb != null) {
                                cb.fail(pResponse);
                            }
                        }
                    }
                });
        } catch (Exception e) {
            FHLog.e(LOG_TAG, "FH init exception = " + e.getMessage(), e);
        }
    }

    private static void checkNetworkStatus() {
        NetworkManager networkManager = NetworkManager.init(mContext);
        networkManager.registerNetworkListener();
        networkManager.checkNetworkStatus();
        if (networkManager.isOnline() && !mReady && mInitCalled) {
            init(mContext, null);
        }
    }

    private static FHAct buildAction(String pAction) throws FHNotReadyException {
        if (!mInitCalled) {
            throw new FHNotReadyException();
        }
        FHAct action = null;
        if (FH_API_ACT.equalsIgnoreCase(pAction)) {
            action = new FHActRequest(mContext);
        } else if (FH_API_AUTH.equalsIgnoreCase(pAction)) {
            action = new FHAuthRequest(mContext);
        } else if (FH_API_CLOUD.equalsIgnoreCase(pAction)) {
            action = new FHCloudRequest(mContext);
        } else {
            FHLog.w(LOG_TAG, "Invalid action : " + pAction);
        }
        return action;
    }

    public static boolean isOnline() {
        return NetworkManager.getInstance().isOnline();
    }

    public static void stop() {
        NetworkManager.getInstance().unregisterNetworkListener();
    }

    /**
     * Checks if FH is ready.
     *
     * @return whether FH has finished initialization
     */
    public static boolean isReady() {
        return mReady;
    }

    @Deprecated
    /**
     * Builds an instance of {@link FHActRequest} to perform an act request.
     * @param pRemoteAction the name of the cloud side function
     * @param pParams the parameters for the cloud side function
     * @return an instance of FHActRequest
     * @throws FHNotReadyException
     */
    public static FHActRequest buildActRequest(String pRemoteAction, JSONObject pParams) throws FHNotReadyException {
        FHActRequest request = (FHActRequest) buildAction(FH_API_ACT);
        request.setRemoteAction(pRemoteAction);
        request.setArgs(pParams);
        return request;
    }

    /**
     * Builds an instance of FHAuthRequest object to perform an authentication request.
     *
     * @return an instance of FHAuthRequest
     * @throws FHNotReadyException if init has not been called
     */
    public static FHAuthRequest buildAuthRequest() throws FHNotReadyException {
        return (FHAuthRequest) buildAction(FH_API_AUTH);
    }

    /**
     * Builds an instance of FHAuthRequest object to perform an authentication request with an auth policy id set.
     *
     * @param pPolicyId the auth policy id used by this auth request
     * @return an instance of FHAuthRequest
     * @throws FHNotReadyException if init has not been called
     */
    public static FHAuthRequest buildAuthRequest(String pPolicyId) throws FHNotReadyException {
        FHAuthRequest request = (FHAuthRequest) buildAction(FH_API_AUTH);
        request.setAuthPolicyId(pPolicyId);
        return request;
    }

    /**
     * Builds an instance of FHAuthRequest to perform an authentication request with an auth policy id,
     * user name, and password set.
     *
     * @param pPolicyId the auth policy id used by this auth request
     * @param pUserName the required user name for the auth request
     * @param pPassword the required password for the auth request
     * @return an instance of FHAuthRequest
     * @throws FHNotReadyException if init has not been called
     */
    public static FHAuthRequest buildAuthRequest(String pPolicyId, String pUserName, String pPassword)
        throws FHNotReadyException {
        FHAuthRequest request = (FHAuthRequest) buildAction(FH_API_AUTH);
        request.setAuthUser(pPolicyId, pUserName, pPassword);
        return request;
    }

    /**
     * Builds an instance of FHCloudRequest to call cloud APIs.
     *
     * @param pPath    the path of the cloud API
     * @param pMethod  currently supports GET, POST, PUT and DELETE
     * @param pHeaders headers need to be set, can be null
     * @param pParams  the request params, can be null
     * @return an instance of FHCloudRequest
     * @throws FHNotReadyException if init has not been called
     * @throws IllegalArgumentException if pMethod is not one of GET, POST, PUT or DELETE
     */
    public static FHCloudRequest buildCloudRequest(String pPath, String pMethod, Header[] pHeaders, JSONObject pParams) 
            throws FHNotReadyException
         {
        FHCloudRequest request = (FHCloudRequest) buildAction(FH_API_CLOUD);
        request.setPath(pPath);
        request.setHeaders(pHeaders);
        request.setMethod(Methods.parse(pMethod));
        request.setRequestArgs(pParams);
        return request;
    }

    /**
     * Gets the cloud host.
     *
     * @return the cloud host of the app
     * @throws FHNotReadyException if init has not been called
     */
    public static String getCloudHost() throws FHNotReadyException {
        if (CloudProps.getInstance() == null) {
            throw new FHNotReadyException();
        }
        return CloudProps.getInstance().getCloudHost();
    }

    /**
     * Gets the default params for customised HTTP Requests.
     * These params will be required to enable app analytics on the FH platform.
     * You can either add the params to your request body as a JSONObject with the key "__fh", or use the
     * {@link #getDefaultParamsAsHeaders(Header[]) getDefaultParamsAsHeaders} method to add them as HTTP request
     * headers.
     *
     * @return a JSONObject contains the default params
     * @throws IllegalStateException if the app property file is not loaded
     */
    public static JSONObject getDefaultParams()  {
        AppProps appProps = AppProps.getInstance();
        JSONObject defaultParams = new JSONObject();
        defaultParams.put("appid", appProps.getAppId());
        defaultParams.put("appkey", appProps.getAppApiKey());
        defaultParams.put("cuid", Device.getDeviceId(mContext));
        defaultParams.put("destination", "android");
        defaultParams.put("sdk_version", "FH_ANDROID_SDK/" + FH.VERSION);
        String projectId = appProps.getProjectId();
        if (projectId != null && !projectId.isEmpty()) {
            defaultParams.put("projectid", projectId);
        }
        String connectionTag = appProps.getConnectionTag();
        if (connectionTag != null && !connectionTag.isEmpty()) {
            defaultParams.put("connectiontag", connectionTag);
        }
        // Load init
        String init = CloudProps.getInitValue();
        if (init != null) {
            JSONObject initObj = new JSONObject(init);
            defaultParams.put("init", initObj);
        }

        if (FHAuthSession.exists()) {
            defaultParams.put(FHAuthSession.SESSION_TOKEN_KEY, FHAuthSession.getToken());
        }

        return defaultParams;
    }

    /**
     * Similar to {@link #getDefaultParams() getDefaultParams}, but returns HTTP headers instead.
     *
     * @param pHeaders existing headers
     * @return new headers by combining existing headers and default headers
     * @throws IllegalStateException if the app property file is not loaded
     */
    public static Header[] getDefaultParamsAsHeaders(Header[] pHeaders)  {
        JSONObject defaultParams = FH.getDefaultParams();
        ArrayList<Header> headers = new ArrayList<Header>(defaultParams.length());

        for (Iterator<String> it = defaultParams.keys(); it.hasNext(); ) {
            String key = it.next();
            headers.add(new BasicHeader("X-FH-" + key, defaultParams.getString(key)));
        }
        if (pHeaders != null) {
            headers.ensureCapacity(pHeaders.length + 1);
            Collections.addAll(headers, pHeaders);
        }
        return headers.toArray(new Header[headers.size()]);
    }

    /**
     * Calls cloud APIs asynchronously.
     *
     * @param pPath     the path to the cloud API
     * @param pMethod   currently supports GET, POST, PUT and DELETE
     * @param pHeaders  headers need to be set, can be null
     * @param pParams   the request params, can be null. Will be converted to query strings depending
     *                  on the HTTP method
     * @param pCallback the callback to be executed when the cloud call is finished
     * @throws FHNotReadyException if init has not been called
     * @throws IllegalArgumentException if pMethod is not one of GET, POST, PUT and DELETE 
     */
    public static void cloud(
        String pPath,
        String pMethod,
        Header[] pHeaders,
        JSONObject pParams,
        FHActCallback pCallback) throws FHNotReadyException {
        FHCloudRequest cloudRequest = buildCloudRequest(pPath, pMethod, pHeaders, pParams);
        cloudRequest.executeAsync(pCallback);
    }

    /**
     * Sets the log level for the library.
     * The default level is {@link #LOG_LEVEL_ERROR}. Please make sure this is set to {@link #LOG_LEVEL_ERROR}
     * or {@link #LOG_LEVEL_NONE} before releasing the application.
     * The log level can be one of
     * <ul>
     * <li>{@link #LOG_LEVEL_VERBOSE}</li>
     * <li>{@link #LOG_LEVEL_DEBUG}</li>
     * <li>{@link #LOG_LEVEL_INFO}</li>
     * <li>{@link #LOG_LEVEL_WARNING}</li>
     * <li>{@link #LOG_LEVEL_ERROR}</li>
     * <li>{@link #LOG_LEVEL_NONE}</li>
     * </ul>
     *
     * @param pLogLevel The level of logging for the FH library
     */
    public static void setLogLevel(int pLogLevel) {
        mLogLevel = pLogLevel;
    }

    /**
     * Gets the current log level for the FH library.
     *
     * @return The current log level
     */
    public static int getLogLevel() {
        return mLogLevel;
    }

    /**
     * Gets the customized user-agent string for the SDK.
     *
     * @return customized user-agent string
     */
    public static String getUserAgent() {
        return Device.getUserAgent();
    }

    /**
     * Registers a device on a push network.
     * The push information will be loaded from fhconfig.properties.
     *
     * This method need to be called <b>after</b> an {@link #init(android.content.Context, FHActCallback)} success
     * callback.
     *
     * @param pCallback the pCallback function to be executed after the device registration is finished
     */
    public static void pushRegister(FHActCallback pCallback) {
        pushRegister(new PushConfig(), pCallback);
    }

    /**
     * Registers a device on a push network.
     * The push information will be loaded from fhconfig.properties.
     *
     * This method need to be called <b>after</b> an {@link #init(android.content.Context, FHActCallback)} success
     * callback.
     *
     * @param pPushConfig extra configuration for push
     * @param pCallback   the pCallback function to be executed after the device registration is finished
     */
    public static void pushRegister(final PushConfig pPushConfig, final FHActCallback pCallback) {
        RegistrarManager.config(FH_PUSH_NAME, AeroGearFCMPushConfiguration.class)
            .setPushServerURI(URI.create(AppProps.getInstance().getPushServerUrl()))
            .setSenderId(AppProps.getInstance().getPushSenderId())
            .setVariantID(AppProps.getInstance().getPushVariant())
            .setSecret(AppProps.getInstance().getPushSecret())
            .setAlias(pPushConfig.getAlias())
            .setCategories(pPushConfig.getCategories())
            .asRegistrar()
            .register(
                mContext, new Callback<Void>() {
                    @Override
                    public void onSuccess(Void aVoid) {
                        pCallback.success(new FHResponse(null, null, null, null));
                    }

                    @Override
                    public void onFailure(Exception e) {
                        pCallback.fail(new FHResponse(null, null, e, e.getMessage()));
                    }
                });
    }

    /**
     * Sends confirmation a message was opened.
     *
     * @param pMessageId Id of the message received
     * @param pCallback  the pCallback function to be executed after the metrics sent
     */
    public static void sendPushMetrics(String pMessageId, final FHActCallback pCallback) {
        AeroGearFCMPushRegistrar registrar = (AeroGearFCMPushRegistrar) RegistrarManager.getRegistrar(FH_PUSH_NAME);
        registrar.sendMetrics(
            new UnifiedPushMetricsMessage(pMessageId), new Callback<UnifiedPushMetricsMessage>() {
                @Override
                public void onSuccess(UnifiedPushMetricsMessage data) {
                    pCallback.success(new FHResponse(null, null, null, null));
                }

                @Override
                public void onFailure(Exception e) {
                    pCallback.fail(new FHResponse(null, null, e, e.getMessage()));
                }
            });
    }
}
