/**
 * Copyright (c) 2013, Sana
 * All rights reserved.
 * <p>
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * <p>
 * * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 * * Neither the name of the Sana nor the
 * names of its contributors may be used to endorse or promote products
 * derived from this software without specific prior written permission.
 * <p>
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL Sana BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.sana.android.service.impl;

import java.util.HashMap;
import java.util.UUID;

import org.apache.http.client.methods.HttpPost;
import org.sana.R;
import org.sana.android.Constants;
import org.sana.android.content.core.ObserverWrapper;
import org.sana.android.net.HttpTask;
import org.sana.android.net.MDSInterface2;
import org.sana.net.Response;
import org.sana.android.net.NetworkTaskListener;
import org.sana.android.provider.Observers;
import org.sana.android.service.ISessionCallback;
import org.sana.android.service.ISessionService;
import org.sana.api.IObserver;
import org.sana.util.UUIDUtil;

import android.app.Service;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;

/**
 * Service which provides session based authentication into the system. 
 *
 * @author Sana Development
 *
 */
public class SessionService extends Service {
    public static final String TAG = SessionService.class.getSimpleName();

    public static final String ACTION_START = "org.sana.service.SessionService.START";

    public static final UUID INVALID = UUIDUtil.uuid3(UUIDUtil.EMPTY, "invalid");
    public static final UUID INVALID_PASSWORD = UUIDUtil.uuid3(UUIDUtil.EMPTY, "password");
    public static final UUID VALID = UUIDUtil.uuid3(UUIDUtil.EMPTY, "valid");

    public static final int INDETERMINATE = -1;
    public static final int STATE_REMOTE = 1;
    public static final int STATE_LOCAL = 2;

    public static final int FAILURE = 0;
    public static final int SUCCESS = 1;

    //TODO replace the two HashMaps with thread safe versions.
    // map of user valid session key to credentials
    private static final HashMap<String, String[]> openSessions = new HashMap<String, String[]>();

    // map of temp key to credentials
    private static final HashMap<String, String[]> tempSessions = new HashMap<String, String[]>();

    private final RemoteCallbackList<ISessionCallback> mCallbacks
            = new RemoteCallbackList<ISessionCallback>();

    private final ISessionService.Stub mBinder = new ISessionService.Stub() {
        @Override
        public boolean read(String arg0) throws RemoteException {
            boolean result = false;
            if (TextUtils.isEmpty(arg0))
                return false;
            else {
                result = isOpen(arg0);
            }
            return result;
        }

        @Override
        public String create(String tempKey, String username, String password) throws RemoteException {
            //String tempKey = SessionUtil.generateSessionKey(username);
            addTempSession(tempKey, new String[]{username, password});
            openSession(STATE_REMOTE, tempKey);
            return tempKey;
        }

        @Override
        public boolean delete(String arg0) throws RemoteException {
            return removeAuthenticatedSession(arg0);
        }

        @Override
        public void registerCallback(ISessionCallback arg0, String arg1)
                throws RemoteException {
            Log.i(TAG + ".mBinder", "registerCallback(): " + arg1);
            if (arg0 != null) mCallbacks.register(arg0, arg1);
        }

        @Override
        public void unregisterCallback(ISessionCallback arg0)
                throws RemoteException {
            Log.i(TAG + ".mBinder", "unregisterCallback(): " + arg0);
            if (arg0 != null) mCallbacks.unregister(arg0);
        }

    };

    //TODO refactor this out

    /**
     * Simple callback interface for HttpSessions
     *
     * @author Sana Development
     *
     *
    private class HttpSessionAuthListener implements NetworkTaskListener<MDSResult>{

    String tempKey = null;

    HttpSessionAuthListener(String key){
    this.tempKey = key;
    }

     @Override public void onTaskComplete(MDSResult t) {
     // TODO Auto-generated method stub
     if(t.succeeded()){
     // MDSResult should return the actual session key
     handleSessionAuthResult(SUCCESS, tempKey, t.getData());
     } else if(TextUtils.isEmpty(t.getCode())){
     handleSessionAuthResult(INDETERMINATE, tempKey, INVALID.toString());
     } else {
     // data should be some informative message
     handleSessionAuthResult(FAILURE, tempKey, INVALID.toString());
     }
     }
     }
     */
    private class AuthListener implements NetworkTaskListener<Response<String>> {

        String tempKey = null;

        AuthListener(String key) {
            this.tempKey = key;
        }

        @Override
        public void onTaskComplete(Response<String> t) {
            // TODO Auto-generated method stub
            if (t.succeeded()) {
                // MDSResult should return the actual session key
                handleSessionAuthResult(SUCCESS, tempKey, t.getMessage());
            } else if (t.getCode() == -1) {
                handleSessionAuthResult(INDETERMINATE, tempKey, INVALID.toString());
            } else {
                // data should be some informative message
                handleSessionAuthResult(FAILURE, tempKey, INVALID.toString());
            }
        }
    }

    //HttpSessionAuthListener mNetListener = null;

    HttpTask mNetTask = null;

    /* (non-Javadoc)
     * @see android.app.Service#onBind(android.content.Intent)
     */
    @Override
    public IBinder onBind(Intent arg0) {
        Log.i(TAG, "onBind()");
        // Try the call back binder defined in IRemoteService.onBind()
        if (arg0.getAction().equals(SessionService.class.getName()) ||
                arg0.getAction().equals(ACTION_START))
            return mBinder;
        else
            return null;
    }

    @Override
    public void onDestroy() {
        Log.i(TAG, "onDestroy()");
        if (mCallbacks != null)
            mCallbacks.kill();
        super.onDestroy();
    }

    @Override
    public boolean onUnbind(Intent arg0) {
        Log.i(TAG, "onUnbind()");
        int connections = 0;
        try {
            connections = mCallbacks.beginBroadcast();
        } finally {
            mCallbacks.finishBroadcast();
        }
        if (!(connections > 0))
            stopSelf();
        return super.onUnbind(arg0);

    }

    /**
     * Handles the logic for opening a session by first trying the remote
     * dispatcher if connected and then falling back locally to either (1) the
     * credential cache in the Observer ContentProvider or (2) the default
     * admin credentials.
     *
     * @param state
     * @param username
     * @param password
     */
    protected void openSession(int state, String tempKey) {
        switch (state) {
            case STATE_LOCAL:
                openLocalSession(tempKey);
                break;
            case STATE_REMOTE:
                openNetworkSession(tempKey);
                break;
            default:
                throw new IllegalArgumentException("Use STATE_LOCAL or STATE_REMOTE only");
        }
    }

    // Tries to open a session from the local ContentProvider and then the
    // admin backdoor by default - admin backdoor only works once from the
    // resource value
    private void openLocalSession(String tempKey) {
        String[] credentials = tempSessions.get(tempKey);
        String username = credentials[0];
        String password = credentials[1];
        String session = INVALID.toString();
        if (isLocalUsername(username)) {
            try {
                Log.d(TAG, "auth: " + username + " pass: " + password);
                IObserver o = ObserverWrapper.getOneByAuth(
                        getContentResolver(), username, password);

                // user exists and was validated locally
                if (o != null) {
                    session = o.getUuid();
                    handleSessionAuthResult(SUCCESS, tempKey, session);
                    // the user was good but password didn't match, return a fail
                } else {
                    handleSessionAuthResult(FAILURE, tempKey, username);
                }
            } catch (Exception e) {
                e.printStackTrace();
                Log.e(TAG, e.getMessage());
                handleSessionAuthResult(FAILURE, tempKey, INVALID.toString());
            }

            // Try the admin backdoor for local connection only
        } else if (username.equals(getString(R.string.admin_username))
                && password.equals(getString(R.string.admin_password))) {
            session = UUIDUtil.generateObserverUUID(username).toString();
            registerNew(username, password, session);
            handleSessionAuthResult(SUCCESS, tempKey, username);
        } else {
            handleSessionAuthResult(FAILURE, tempKey, INVALID.toString());
        }

    }

    //TODO replace with an https connection.
    // Starts an async http task to POST credentials to dispatch server
    private void openNetworkSession(String tempKey) {
        Log.d(TAG, "Opening network session: " + tempKey);
        if (!connected()) {
            Log.d(TAG, "openNetworkSession()..connected = false");
            openLocalSession(tempKey);
        } else {
            try {
                Log.d(TAG, "openNetworkSession()..connected = true");
                String[] credentials = tempSessions.get(tempKey);
                HttpPost post = MDSInterface2.createSessionRequest(this,
                        credentials[0], credentials[1]);
                Log.i(TAG, "openNetworkSession(...) " + post.getURI());
                new HttpTask<String>(new AuthListener(tempKey)).execute(post);
            } catch (Exception e) {
                Log.e(TAG, e.getMessage());
                e.printStackTrace();
            }
        }
    }

    /**
     * Handles authentication attempts for temporary tempSessions. If successful,
     * the temporary session will be moved to a list of authorized tempSessions.
     *
     * @param status {@link #SUCCESS}, {@link #FAILURE}, {@link #INDETERMINATE}
     * @param tempKey the temporary key initially passed from the client.
     * @param sessionKey the authorized key.
     */
    protected void handleSessionAuthResult(int status, String tempKey,
                                           String sessionKey) {
        Log.i(TAG, "Handling result: '" + status
                + "' for temp session: " + tempKey);
        String[] credentials = tempSessions.get(tempKey);
        final String username = credentials[0];
        final String password = credentials[1];

        switch (status) {
            case SUCCESS:
                // Got successful authentication from network
                // update cache with the validated password, may exist already but
                // this should handle any situations where the network value changed
                if (isLocalUsername(username)) {
                    Log.i(TAG, "Updating credentials for user: " + username);
                    ContentValues values = new ContentValues();
                    values.put(Observers.Contract.PASSWORD, encrypt(password));
                    int updated = getContentResolver().update(
                            Observers.CONTENT_URI,
                            values,
                            Observers.Contract.USERNAME + " = ?",
                            new String[]{username});
                    if (updated == 1)
                        Log.i(TAG, "Succesfully updated: " + username);
                    else
                        Log.w(TAG, "OOPS! Something went horribly wrong updating" +
                                "the password for user: " + username
                                + " or the password was unchanged!");

                    // i name didn't exist already we need to insert instead of update.
                } else {
                    ContentValues values = new ContentValues();
                    values.put(Observers.Contract.USERNAME, username);
                    values.put(Observers.Contract.UUID, sessionKey);
                    values.put(Observers.Contract.PASSWORD, encrypt(password));
                    getContentResolver().insert(Observers.CONTENT_URI,
                            values);
                }
                SharedPreferences preferences = PreferenceManager
                        .getDefaultSharedPreferences(this.getBaseContext());
                preferences.edit().putString(
                        Constants.PREFERENCE_EMR_USERNAME, username);
                preferences.edit().putString(
                        Constants.PREFERENCE_EMR_PASSWORD, password);
                preferences.edit().commit();

                // send result to the call back (INVALID, user uuid)
                removeTempSession(tempKey);
                addAuthenticatedSession(sessionKey, credentials);
                sendResult(SUCCESS, tempKey, sessionKey);
                break;
            // connected and failed
            case FAILURE:
                // send result to the call back (INVALID, user )
                removeTempSession(tempKey);
                sendResult(FAILURE, tempKey, sessionKey);
                break;
            case INDETERMINATE:
                openSession(STATE_LOCAL, tempKey);
                break;
            default:
                throw new IllegalArgumentException();
        }
    }

    // sends the callback message
    protected void sendResult(int status, String cookie, String msg) {
        int i = mCallbacks.beginBroadcast();
        Log.i(TAG, "sendResult( " + cookie + ", " + status + ") --> " + i + " callbacks");
        while (i > 0) {
            i--;
            String instanceKey = mCallbacks.getBroadcastCookie(i).toString();
            if (cookie.equals(instanceKey)) {
                Log.i(TAG, "....Sending result to " + cookie);
                try {
                    mCallbacks.getBroadcastItem(i).onValueChanged(status, cookie, msg);
                } catch (RemoteException e) {
                    Log.e(TAG, "....disconnected from " + cookie);
                }

            } else {
                Log.i(TAG, "....Trying next " + i);

            }
        }
        mCallbacks.finishBroadcast();
    }

    // queries the content provider for the username
    private boolean isLocalUsername(String username) {
        Cursor c = null;
        boolean isValid = false;
        try {
            c = getContentResolver().query(Observers.CONTENT_URI,
                    new String[]{Observers.Contract._ID},
                    Observers.Contract.USERNAME + " = ?",
                    new String[]{username}, null);
            //(Observers.CONTENT_URI, getContentResolver(),Observers.Contract.USERNAME,username);
            if (c != null && c.moveToFirst() && c.getCount() == 1)
                isValid = true;
        } finally {
            if (c != null)
                c.close();
        }
        return isValid;
    }

    private synchronized void addAuthenticatedSession(String sessionKey, String[] credentials) {
        Log.d(TAG, "Adding authorized session: " + sessionKey + ":" + credentials.length);
        openSessions.put(sessionKey, credentials);
    }

    private synchronized boolean removeAuthenticatedSession(String sessionKey) {
        Log.d(TAG, "Removing authorized session: " + sessionKey);
        boolean result = false;
        result = (openSessions.remove(sessionKey) != null);
        return result;
    }

    private synchronized void addTempSession(String sessionKey, String[] credentials) {
        Log.d(TAG, "Adding temp session: " + sessionKey + ":" + credentials.length);
        tempSessions.put(sessionKey, credentials);
    }

    private synchronized boolean removeTempSession(String sessionKey) {
        Log.d(TAG, "Removing temp session: " + sessionKey);
        boolean result = false;
        result = (tempSessions.remove(sessionKey) != null);
        return result;
    }

    private synchronized boolean isOpen(String sessionKey) {
        boolean result = false;
        synchronized (openSessions) {
            String[] session = openSessions.get(sessionKey);
            result = (session != null);
        }
        openSessions.notify();
        return result;
    }

    /**
     * Retrieves the memory stored password for network authentication
     * @param session
     * @return
     */
    protected String getPassword(String sessionKey) {
        return decrypt(openSessions.get(sessionKey)[1]);
    }

    //TODO do placeholder for encrypting the in memory String
    private String encrypt(String str) {
        return str;
    }

    //TODO do placeholder for decrypting the in memory String
    private String decrypt(String str) {
        return str;
    }

    private Uri registerNew(String username, String password, String uuid) {
        uuid = (uuid == null) ? UUIDUtil.generateObserverUUID(username).toString() : uuid;
        ContentValues values = new ContentValues();
        values.put(Observers.Contract.USERNAME, username);
        values.put(Observers.Contract.PASSWORD, encrypt(password));
        values.put(Observers.Contract.UUID, uuid);
        Uri uri = getContentResolver().insert(Observers.CONTENT_URI,
                values);
        return uri;
    }


    protected boolean connected() {
        ConnectivityManager cm = (ConnectivityManager)
                getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
        return (activeNetwork != null &&
                activeNetwork.isConnectedOrConnecting());

    }
}