package drupalfit;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.squareup.okhttp.OkHttpClient;

import retrofit.Callback;
import retrofit.RestAdapter;
import retrofit.RetrofitError;
import retrofit.client.Client;
import retrofit.client.OkClient;
import retrofit.client.Response;
import retrofit.client.Header;
import retrofit.http.Field;
import retrofit.http.FormUrlEncoded;
import retrofit.http.GET;
import retrofit.http.Multipart;
import retrofit.http.POST;
import retrofit.http.Part;
import retrofit.http.Path;
import retrofit.http.Streaming;
import retrofit.mime.TypedFile;
import retrofit.RequestInterceptor;

import proguard.annotation.Keep;
import proguard.annotation.KeepClassMembers;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.GeneralSecurityException;
import java.security.cert.X509Certificate;
import java.io.IOException;
import java.security.SecureRandom;
import android.text.TextUtils;
import drupalfit.DrupalOAuth2.Credential;
import android.net.Uri;
import android.content.Context;

import rx.Observable;
import rx.functions.Func1;
import com.squareup.okhttp.Request;

public class DrupalManager implements DrupalService {
    private static DrupalManager sInstance = new DrupalManager();
    private DrupalService mService;
    private String endpoint;
    private String accessToken;
    private String cookie;
    private String xcsrfToken; //X-CSRF-Token
    private String username; // TODO
    private String email; // TODO
    private String password; // TODO

    /*
     * <pre>
     * hybridauth_ulogin/hybridauth_ulogin.admin.inc
     *
     * "vkontakte" => "Vkontakte",
     * "odnoklassniki" => "Odnoklassniki",
     * "mailru" => "Mailru",
     * "facebook" => "Facebook",
     * "twitter" => "Twitter",
     * "google" => "Google",
     * "yandex" => "Yandex",
     * "livejournal" => "",
     * "openid" => "OpenID",
     * "lastfm" => "LastFM",
     * "linkedin" => "LinkedIn",
     * "liveid" => "Live",
     * "soundcloud" => "",
     * "steam" => "Steam",
     * "flickr" => "",
     * "vimeo" => "",
     * "youtube" => "",
     * "webmoney" => "",
     *
     * additional-providers/hybridauth-/Providers/
     *
     * px500
     * Deezer
     * Disqus
     * Draugiem
     * DrupalOAuth2
     * Freeagent
     * GitHub
     * Goodreads
     * Google
     * Identica
     * Instagram
     * LastFM
     * Latch
     * Mailru
     * Murmur
     * Odnoklassniki
     * PaypalOpenID
     * Paypal
     * PixelPin
     * Pixnet
     * Plurk
     * QQ
     * Sina
     * Skyrock
     * Steam
     * Tumblr
     * TwitchTV
     * Viadeo
     * Vimeo
     * Vkontakte
     * XING
     * Yahoo
     * Yammer
     * Yandex
     * </pre>
     */

    public static final String DEEZER        = "Deezer";
    public static final String DISQUS        = "Disqus";
    public static final String DRAUGIEM      = "Draugiem";
    public static final String DRUPALOAUTH2  = "DrupalOAuth2";
    public static final String FACEBOOK      = "Facebook";
    public static final String FLICKR        = "flickr";
    public static final String FREEAGENT     = "Freeagent";
    public static final String GITHUB        = "GitHub";
    public static final String GOODREADS     = "Goodreads";
    public static final String GOOGLE        = "Google";
    public static final String IDENTICA      = "Identica";
    public static final String INSTAGRAM     = "Instagram";
    public static final String LASTFM        = "LastFM";
    public static final String LATCH         = "Latch";
    public static final String LINKEDIN      = "LinkedIn";
    public static final String LIVEJOURNAL   = "livejournal";
    public static final String LIVE          = "Live";
    public static final String MAILRU        = "Mailru";
    public static final String MURMUR        = "Murmur";
    public static final String ODNOKLASSNIKI = "Odnoklassniki";
    public static final String OPENID        = "OpenID";
    public static final String PAYPALOPENID  = "PaypalOpenID";
    public static final String PAYPAL        = "Paypal";
    public static final String PIXELPIN      = "PixelPin";
    public static final String PIXNET        = "Pixnet";
    public static final String PLURK         = "Plurk";
    public static final String PX500         = "px500";
    public static final String QQ            = "QQ";
    public static final String SINA          = "Sina";
    public static final String SKYROCK       = "Skyrock";
    public static final String SOUNDCLOUD    = "soundcloud";
    public static final String STEAM         = "Steam";
    public static final String TUMBLR        = "Tumblr";
    public static final String TWITCHTV      = "TwitchTV";
    public static final String TWITTER       = "Twitter";
    public static final String VIADEO        = "Viadeo";
    public static final String VIMEO         = "vimeo";
    public static final String VKONTAKTE     = "Vkontakte";
    public static final String WEBMONEY      = "webmoney";
    public static final String XING          = "XING";
    public static final String YAHOO         = "Yahoo";
    public static final String YAMMER        = "Yammer";
    public static final String YANDEX        = "Yandex";
    public static final String YOUTUBE       = "youtube";

    protected String provider = FACEBOOK;
    protected String token;

    protected SimpleRequestInterceptor mRequestInterceptor;

    private DrupalOAuth2Manager oauth;

    protected Context context;

    public DrupalManager setOAuth(DrupalOAuth2Manager oauth) {
        this.oauth = oauth;
        return this;
    }

    public DrupalOAuth2Manager getOAuth() {
        return oauth;
    }

    public DrupalManager setEndpoint(String endpoint) {
        this.endpoint = endpoint;
        return this;
    }

    public DrupalManager setContext(Context context) {
        this.context = context;
        return this;
    }

    public DrupalManager setProvider(String provider) {
        this.provider = provider;
        return this;
    }

    public DrupalManager setProvider(Context context, String provider, String token) {
        setContext(context);
        setProvider(provider);
        setToken(token);
        return this;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public DrupalManager build() {
        getService(endpoint);
        return this;
    }

    private DrupalManager() {
    }

    @Override
    public void register(
        String username,
        String email,
        String password,
        final Callback<User> callback) {
        setUsername(username);
        setEmail(email);
        setPassword(password);
        getService().register(username, email, password, new Callback<User>() {
            @Override
            public void success(final User user, final Response response) {
                for (Header header : response.getHeaders()) {
                    if ("Set-Cookie".equalsIgnoreCase(header.getName())) {
                        setCookie(header.getValue());
                        break;
                    }
                }
                getToken(new Callback<Login>() { // Fetch a new xcsrfToken
                    @Override
                    public void success(Login l, Response r) {
                        Log8.d(l.token);
                        setXcsrfToken(l.token);
                        callback.success(user, response);
                    }
                    @Override
                    public void failure(RetrofitError error) {
                        Log8.d("Auto-fetch new xcsrfToken failure");
                        //callback.failure(error);
                    }
                });
            }
            @Override
            public void failure(RetrofitError error) {
                Log8.d();
                callback.failure(error);
            }
        });
    }

    public void register(
        String email,
        String password,
        Callback<User> callback
    ) {
        register(email, email, password, callback);
    }

    @Override
    public void login(
        String username,
        String password,
        final Callback<Login> callback
    ) {
        setUsername(username);
        setPassword(password);
        getService().login(username, password, new Callback<Login>() {
            @Override
            public void success(final Login login, final Response response) {
                Log8.d(login.token);
                //setXcsrfToken(login.token); // Need fetch a new xcsrfToken or not?
                //callback.success(login, response);
                for (Header header : response.getHeaders()) {
                    if ("Set-Cookie".equalsIgnoreCase(header.getName())) {
                        setCookie(header.getValue());
                        break;
                    }
                }
                getToken(new Callback<Login>() { // Fetch a new xcsrfToken
                    @Override
                    public void success(Login l, Response r) {
                        Log8.d(l.token);
                        setXcsrfToken(l.token);
                        callback.success(login, response);
                    }
                    @Override
                    public void failure(RetrofitError error) {
                        Log8.d("Auto-fetch new xcsrfToken failure");
                        //callback.failure(error);
                    }
                });
            }
            @Override
            public void failure(RetrofitError error) {
                Log8.d();
                callback.failure(error);
            }
        });
    }

    @Override
    public void getProfile(
        String accessToken,
        final Callback<User> callback
    ) {
        Log8.d(accessToken);
        setAccessToken(accessToken);

        getProfile(callback);
    }

    private void syncOAuth() {
        if (oauth != null) {
            oauth.setUsername(username);
            //oauth.setEmail(email);
            oauth.setPassword(password);
            oauth.setCookie(cookie);
        }
    }

    @Override
    public Observable<User> getProfile() {
        if (accessToken != null) {
            return getService().getProfile(accessToken);
        }

        if (oauth != null) {
            syncOAuth();
            return oauth.getAccessToken()
                .doOnNext(token -> setAccessToken(token))
                .flatMap(token -> getProfile(token));
        }

        return getService().getProfile();
    }

    @Override
    public Observable<User> getProfile(String accessToken) {
        return getService().getProfile(accessToken);
    }

    @Override
    public void getProfile(
        final Callback<User> callback
    ) {
        if (cookie == null && accessToken == null) {
            if (oauth != null) {
                syncOAuth();
                oauth.getAccessToken(new Callback<Credential>() {
                    @Override
                    public void success(Credential credential, Response response) {
                        setAccessToken(credential.access_token);
                        Log8.d(accessToken);
                        getService().getProfile(callback);
                    }
                    @Override
                    public void failure(RetrofitError error) {
                        Log8.d();
                        callback.failure(error);
                    }
                });
            } else if (TextUtils.isEmpty(cookie) && context != null && !TextUtils.isEmpty(provider) && !TextUtils.isEmpty(token)) {
                getCookie(context, provider, token, new Callback<String>() {
                    @Override
                    public void success(String cookie, Response response) {
                        Log8.d(cookie);
                        setCookie(cookie);
                        if (TextUtils.isEmpty(xcsrfToken)) {
                            getToken(new Callback<Login>() {
                                @Override
                                public void success(Login login, Response response) {
                                    Log8.d(login.token);
                                    setXcsrfToken(login.token);
                                    getService().getProfile(callback);
                                }
                                @Override
                                public void failure(RetrofitError error) {
                                    Log8.d();
                                    callback.failure(error);
                                }
                            });
                        } else {
                            getService().getProfile(callback);
                        }
                    }
                    @Override
                    public void failure(RetrofitError error) {
                        Log8.d();
                        callback.failure(error);
                    }
                });
            }
        } else {
            Log8.d();
            getService().getProfile(callback);
        }
    }

    @Override
    public void logout(
        Callback<Logout> callback
    ) {
        getService().logout(callback);
    }

    public static DrupalManager get() {
        return sInstance;
    }

    public static DrupalService getService() {
        return get().getService(null);
    }

    public String getAccessToken() {
        if (!TextUtils.isEmpty(accessToken)) {
            return accessToken;
        }

        return accessToken;
    }

    /* DONT USE on main thread */
    public String getAccessToken(String username, String password) {
        return getAccessToken(username, password, (String) null);
    }

    /* DONT USE on main thread */
    public String getAccessToken(String username, String password, String authTokenType) {
        syncOAuth();
        Credential c = oauth.getAccessToken(username, password);
        if (c != null) {
            setAccessToken(c.access_token);
        }
        return accessToken;
    }

    public void getAccessToken(final Callback<Credential> callback) {
        if (oauth != null) {
            Log8.d();
            syncOAuth();
            oauth.getAccessToken(new Callback<Credential>() {
                @Override
                public void success(Credential credential, Response response) {
                    Log8.d();
                    setAccessToken(credential.access_token);
                    callback.success(credential, response);
                }
                @Override
                public void failure(RetrofitError error) {
                    Log8.d();
                    callback.failure(error);
                }
            });
        }
    }

    public void getAccessToken(String cookie, final Callback<Credential> callback) {
        if (oauth != null) {
            Log8.d();
            syncOAuth();
            oauth.getAccessToken(cookie, new Callback<Credential>() {
                @Override
                public void success(Credential credential, Response response) {
                    Log8.d();
                    setAccessToken(credential.access_token);
                    callback.success(credential, response);
                }
                @Override
                public void failure(RetrofitError error) {
                    Log8.d();
                    callback.failure(error);
                }
            });
        }
    }

    public void getAccessToken(String username, String password, final Callback<Credential> callback) {
        getAccessToken(username, password, (String) null, callback);
    }

    public void getAccessToken(String username, String password, String authTokenType, final Callback<Credential> callback) {
        if (oauth != null) {
            syncOAuth();
            oauth.getAccessToken(username, password, new Callback<Credential>() {
                @Override
                public void success(Credential credential, Response response) {
                    Log8.d();
                    setAccessToken(credential.access_token);
                    callback.success(credential, response);
                }
                @Override
                public void failure(RetrofitError error) {
                    Log8.d();
                    callback.failure(error);
                }
            });
        }
    }

    public DrupalManager setAccessToken(String accessToken) {
        this.accessToken = accessToken;
        Log8.d(accessToken);

        if (mRequestInterceptor != null) {
            mRequestInterceptor.accessToken = accessToken;
        }

        return this;
    }

    public DrupalService getService(String endpoint) {
        if (endpoint == null) return mService;

        if (mService == null) {
            this.endpoint = endpoint;
            Client client = new OkClient(getOkHttpClient());

            if (mRequestInterceptor == null) {
                mRequestInterceptor = new SimpleRequestInterceptor();
            }
            mRequestInterceptor.cookie = cookie;
            mRequestInterceptor.accessToken = accessToken;
            mRequestInterceptor.xcsrfToken = xcsrfToken;

            mService = new RestAdapter.Builder()
                .setEndpoint(endpoint)
                .setRequestInterceptor(mRequestInterceptor)
                .setErrorHandler(new ErrorHandler())
                .setClient(client)
                .setConverter(new retrofit.converter.JacksonConverter())
                .setLogLevel(RestAdapter.LogLevel.FULL)  // Do this for development too.
                .build().create(DrupalService.class);
        }

        return mService;
    }

    public String getCookie() {
        return cookie;
    }

    public DrupalManager setCookie(String cookie) {
        this.cookie = cookie;

        if (mRequestInterceptor != null) {
            mRequestInterceptor.cookie = cookie;
        }
        if (oauth != null) {
            oauth.setCookie(cookie);
        }

        return this;
    }

    /**
     * Allow sign-in with access_token.
     *
     * @see <a href="https://github.com/yongjhih/drupal-hybridauth/commit/268b72a598665b0738e3b06e7b59dcb3bda5b999">Allow sign-in with access_token</a>
     */
    private void getCookie(Context context, String provider, String token, final Callback<String> callback) {
        if (context == null) return;
        if (TextUtils.isEmpty(token)) return;

        Uri uri = Uri.parse(endpoint);
        final String url = uri.getScheme() + "://" + uri.getAuthority() + "/hybridauth/window/" + provider + "?destination=node&destination_error=node&access_token=" + token;

        //new WebDialog(context, url, callback).show();
        Request request = new Request.Builder()
            .url(url)
            .build();
        //com.squareup.okhttp.Response response = getOkHttpClient().newCall(request).execute();
        com.squareup.okhttp.Call call = getOkHttpClient().newCall(request);
        call.enqueue(new com.squareup.okhttp.Callback() {
            @Override
            public void onFailure(Request request, IOException e){
                Log8.d(e);
                callback.failure(RetrofitError.unexpectedError(url, e));
            }

            @Override
            public void onResponse(com.squareup.okhttp.Response response) throws IOException {
                final String cookie = response.header("Set-Cookie");
                if (!TextUtils.isEmpty(cookie)) {
                    setCookie(cookie);
                    getToken(new Callback<Login>() { // Fetch a new xcsrfToken
                        @Override
                        public void success(Login l, Response r) {
                            Log8.d(l.token);
                            setXcsrfToken(l.token);
                            callback.success(cookie, (Response) null);
                        }
                        @Override
                        public void failure(RetrofitError error) {
                            Log8.d("Auto-fetch new xcsrfToken failure");
                            //callback.failure(error);
                        }
                    });
                } else {
                    callback.failure(RetrofitError.unexpectedError(url, new RuntimeException()));
                }
            }
        });
        //call.execute();
    }

    public void getAccessToken(Context context, String provider, String token, final Callback<Credential> callback) {
        Log8.d();
        if (oauth != null) {
            Log8.d();
            getCookie(context, provider, token, new Callback<String>() {
                @Override
                public void success(final String cookie, final Response response) {
                    getAccessToken(callback);
                }
                @Override
                public void failure(RetrofitError error) {
                    Log8.d();
                    callback.failure(error);
                }
            });
        } else {
        Log8.d();
            callback.failure(RetrofitError.unexpectedError("failure", new RuntimeException()));
        }
    }

    protected OkHttpClient okHttpClient;

    public OkHttpClient getOkHttpClient() {
        if (okHttpClient == null) {
            okHttpClient = new OkHttpClient();
            okHttpClient.setSslSocketFactory(getTrustedFactory());
            okHttpClient.setHostnameVerifier(getTrustedVerifier());
            okHttpClient.setFollowSslRedirects(true);
        }

        return okHttpClient;
    }

    public class SimpleRequestInterceptor implements RequestInterceptor {
        public String cookie;
        public String accessToken;
        public String xcsrfToken;

        @Override
        public void intercept(RequestFacade request) {
            /*
            if (!TextUtils.isEmpty(cookie)) {
                Log8.d(cookie);
                request.addHeader("Cookie", cookie);
            }
            if (!TextUtils.isEmpty(xcsrfToken)) {
                Log8.d(xcsrfToken);
                request.addHeader("X-CSRF-Token", xcsrfToken);
            }
            if (!TextUtils.isEmpty(accessToken)) {
                Log8.d(accessToken);
                //request.addQueryParam("access_token", accessToken);
                request.addEncodedQueryParam("access_token", accessToken);
            }
            */
        }
    }

    public DrupalManager setXcsrfToken(String xcsrfToken) {
        this.xcsrfToken = xcsrfToken;

        if (mRequestInterceptor != null) {
            mRequestInterceptor.xcsrfToken = xcsrfToken;
        }

        return this;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class ErrorHandler implements retrofit.ErrorHandler {
        public ErrorHandler() {}

        @Override
        public Throwable handleError(RetrofitError cause) {
            //Response r = cause.getResponse();
            Log8.d(cause.getUrl());
            cause.printStackTrace();
            return cause;
        }
    }

    private static SSLSocketFactory sTrustedFactory;

    public static SSLSocketFactory getTrustedFactory() {
        if (sTrustedFactory == null) {
            final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {

                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }

                public void checkClientTrusted(X509Certificate[] chain, String authType) {
                    // Intentionally left blank
                }

                public void checkServerTrusted(X509Certificate[] chain, String authType) {
                    // Intentionally left blank
                }
            } };
            try {
                SSLContext context = SSLContext.getInstance("TLS");
                context.init(null, trustAllCerts, new SecureRandom());
                sTrustedFactory = context.getSocketFactory();
            } catch (GeneralSecurityException e) {
                IOException ioException = new IOException(
                        "Security exception configuring SSL context");
                ioException.initCause(e);
                e.printStackTrace();
            }
        }
        return sTrustedFactory;
    }

    private static HostnameVerifier sTrustedVerifier;

    public static HostnameVerifier getTrustedVerifier() {
        if (sTrustedVerifier == null) {
            sTrustedVerifier = new HostnameVerifier() {

                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }
            };
        }

        return sTrustedVerifier;
    }

    /** {@inheritDoc} */
    @Override
    public void systemConnect(
        Callback<User> callback
    ) {
        getService().systemConnect(callback);
    }

    /** {@inheritDoc} */
    @Override
    public void getNode(
        int nid,
        Callback<Node> callback
    ) {
        getService().getNode(nid, callback);
    }

    public void getNode(
        String nid,
        Callback<Node> callback
    ) {
        int i;
        try {
            i = Integer.parseInt(nid);
        } catch (NumberFormatException e) {
            callback.failure(RetrofitError.unexpectedError("failure", e));
            return;
        }
        getNode(i, callback);
    }

    /** {@inheritDoc} */
    @Override
    public void getTaxonomyVocabulary(
        int vid,
        int parent,
        Callback<Vocabulary> callback
    ) {
        getService().getTaxonomyVocabulary(vid, parent, callback);
    }

    /** {@inheritDoc} */
    @Override
    public void getTaxonomyVocabulary(
        int vid,
        int parent,
        int maxdepth,
        Callback<Vocabulary> callback
    ) {
        getService().getTaxonomyVocabulary(vid, parent, maxdepth, callback);
    }

    /** {@inheritDoc} */
    @Override
    public void getViews(
        String name,
        int limit,
        String args,
        int displayId,
        Callback<Views> callback
    ) {
        getService().getViews(name, limit, args, displayId, callback);
    }

    /** {@inheritDoc} */
    @Override
    public void getToken(
        String username,
        String password,
        Callback<Login> callback
    ) {
        setUsername(username);
        setPassword(password);
        getService().getToken(username, password, callback);
    }

    /** {@inheritDoc} */
    @Override
    public void getToken(
        Callback<Login> callback
    ) {
        getService().getToken(callback);
    }

    @Override
    public void getComment(
        int cid,
        Callback<Comment> callback
    ) {
        getService().getComment(cid, callback);
    }

    @Override
    public void addComment(
        String comment,
        int nid,
        Callback<Comment> callback
    ) {
        getService().addComment(comment, nid, callback);
    }

    public void addComment(
        String comment,
        String nid,
        Callback<Comment> callback
    ) {
        int i;
        try {
            i = Integer.parseInt(nid);
        } catch (NumberFormatException e) {
            callback.failure(RetrofitError.unexpectedError("failure", e));
            return;
        }
        addComment(comment, i, callback);
    }

    @Override
    public void addComment(
        String subject,
        String comment,
        int nid,
        Callback<Comment> callback
    ) {
        getService().addComment(subject, comment, nid, callback);
    }

    public void addComment(
        String subject,
        String comment,
        String nid,
        Callback<Comment> callback
    ) {
        int i;
        try {
            i = Integer.parseInt(nid);
        } catch (NumberFormatException e) {
            callback.failure(RetrofitError.unexpectedError("failure", e));
            return;
        }
        getService().addComment(subject, comment, i, callback);
    }

    @Override
    public void setComment(
        int cid,
        String comment,
        int nid,
        Callback<Comment> callback
    ) {
        getService().setComment(cid, comment, nid, callback);
    }

    @Override
    public void deleteComment(
        int cid,
        Callback<Comment> callback
    ) {
        getService().deleteComment(cid, callback);
    }

    @Override
    public void getUser(
        int uid,
        String accessToken,
        Callback<User> callback
    ) {
        getService().getUser(uid, accessToken, callback);
    }

    @Override
    public void getUser(
        int uid,
        Callback<User> callback
    ) {
        getService().getUser(uid, callback);
    }

    @Override
    public Observable<Login> observeLogin(
        String username,
        String password
    ) {
        setUsername(username);
        setPassword(password);
        return getService().observeLogin(username, password);
    }

    @Override
    public Observable<Login> observeToken() {
        return getService().observeToken();
    }

    public DrupalManager setUsername(String username) {
        this.username = username;
        if (oauth != null) {
            oauth.setUsername(username);
        }
        return this;
    }

    public DrupalManager setEmail(String email) {
        this.email = email;
        //if (oauth != null) {
            //oauth.setEmail(email);
        //}
        return this;
    }

    public DrupalManager setPassword(String password) {
        this.password = password;
        if (oauth != null) {
            oauth.setPassword(password);
        }
        return this;
    }

    @Override
    public void getUser(String user, Callback<User> callback) {
        getService().getUser(user, callback);
    }

    @Override
    public void getUser(String user, String accessToken, Callback<User> callback) {
        getService().getUser(user, accessToken, callback);
    }

    /*
    @Override
    public <P> void getGenericUser(String user, Callback<P> callback) {
        getService().getGenericUser(user, callback);
    }
    */

    @Override
    public User getUser(String user) {
        return getService().getUser(user);
    }

    @Override
    public User getUser(String user, String accessToken) {
        return getService().getUser(user);
    }

}