package drupalfit;

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

import retrofit.Callback;
import retrofit.ResponseCallback;
import retrofit.RestAdapter;
import retrofit.RequestInterceptor;
import retrofit.RetrofitError;
import retrofit.client.Client;
import retrofit.client.OkClient;
import retrofit.client.Response;
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 com.squareup.okhttp.Request;

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 drupalfit.DrupalOAuth2.Credential;
import android.net.Uri;
import android.text.TextUtils;
import android.content.Context;
import rx.Observable;

/**
 * OAuth of authentication.
 *
 */
public class DrupalOAuth2Manager {
    protected String endpoint;
    protected String clientId;
    protected String clientSecret;
    //protected String responseType = "code";
    protected String state = "state"; // modify here
    //protected String grantType = "authorization_code";
    protected String cookie;
    protected String username;
    protected String password;

    /*
     * <p>
     * TODO merge into DrupalManager about hybridauth
     * </p>
     *
     * @see hybridauth_ulogin/hybridauth_ulogin.admin.inc.
     *
     * <pre>
     * "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" => "",
     * </pre>
     *
     * @see additional-providers/hybridauth-/Providers/
     *
     * <pre>
     * 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;

    public static class Builder {
        String endpoint;
        String clientId;
        String clientSecret;
        String cookie;
        Context context;
        String provider = FACEBOOK;
        String token;

        public Builder() {
        }

        public Builder(Context context) {
            setContext(context);
        }

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

        public Builder setClientId(String clientId) {
            this.clientId = clientId;
            return this;
        }

        public Builder setClientSecret(String clientSecret) {
            this.clientSecret = clientSecret;
            return this;
        }

        public Builder setCookie(String cookie) {
            this.cookie = cookie;
            return this;
        }

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

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

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

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

        public DrupalOAuth2Manager build() {
            DrupalOAuth2Manager manager = new DrupalOAuth2Manager(context, endpoint, clientId, clientSecret);
            manager.setCookie(cookie);
            manager.setProvider(provider);
            manager.setToken(token);
            return manager;
        }
    }

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

    public void setClientId(String clientId) {
        this.clientId = clientId;
    }

    public void setClientSecret(String clientSecret) {
        this.clientSecret = clientSecret;
    }

    private DrupalOAuth2 mService;

    public DrupalOAuth2Manager(String endpoint) {
        this(endpoint, null, null);
    }

    public DrupalOAuth2Manager(String endpoint, String clientId) {
        this(endpoint, clientId, null);
    }

    private boolean allTrust = true;

    public void disableAllTrust() {
        allTrust = false;
    }

    public void allTrust() {
        allTrust = true;
    }

    /**
     * getAccessToken
     */
    public void getAccessToken(String username, String password, Callback<Credential> callback) {
        mService.token(
            clientId,
            clientSecret,
            "password",
            state,
            username,
            password,
            callback
        );
    }

    // DONT USE on main thread
    public Credential getAccessToken(String username, String password) {
        return mService.token(
            clientId,
            clientSecret,
            "password",
            state,
            username,
            password
        );
    }

    // DONT USE on main thread
    public Credential getAccessToken(String cookie) {
        setCookie(cookie);

        Response response = mService.authorize(
            clientId,
            clientSecret,
            "code",
            state
        );

        Uri uri = Uri.parse(response.getUrl());
        String code = uri.getQueryParameter("code");

        if (!TextUtils.isEmpty(code)) {
            return mService.token(code, clientId, clientSecret, "authorization_code", state);
        }

        return null;
    }

    public void getAccessToken(String cookie, final Callback<Credential> callback) {
        setCookie(cookie);

        final Callback<Response> authorizeCallback = new Callback<Response>() {
            @Override
            public void success(Response response, Response response2) {
                Log8.d();
                Uri uri = Uri.parse(response.getUrl());
                String code = uri.getQueryParameter("code");
                if (TextUtils.isEmpty(code)) {
                    callback.failure(RetrofitError.unexpectedError(response.getUrl(), new RuntimeException()));
                } else {
                    mService.token(code, clientId, clientSecret, "authorization_code", state, callback);
                }
            }
            @Override
            public void failure(RetrofitError error) {
                callback.failure(error);
                Log8.d(error);
            }
        };

        mService.authorize(
            clientId,
            clientSecret,
            "code",
            state,
            authorizeCallback
        );
    }

    public Observable<Credential> getCredential() {
        return Observable.create(sub -> {
            getAccessToken(new Callback<Credential>() {
                @Override
                public void success(Credential credential, Response response) {
                    sub.onNext(credential);
                }
                @Override
                public void failure(RetrofitError error) {
                    sub.onError(error);
                }
            });
        });
    }

    public Observable<String> getAccessToken() {
        return getCredential().map(credential -> credential.access_token);
    }

    public void getAccessToken(final Callback<Credential> callback) {
        if (!TextUtils.isEmpty(cookie)) {
            Log8.d();
            getAccessToken(cookie, callback);
        } else if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) {
            Log8.d();
            getAccessToken(username, password, callback);
        } else {
            Log8.d();
            getAccessToken(context, provider, token, callback);
        }
    }

    public void getAccessToken(Context context, final Callback<Credential> callback) {
        getAccessToken(context, provider, token, callback);
    }

    public Credential getAccessToken(Context context, String provider, String token) {
        if (TextUtils.isEmpty(token)) {
            return null;
        }
        if (TextUtils.isEmpty(provider)) {
            return null;
        }
        if (context == null) {
            return null;
        }

        // TODO String cookie = getHybridauthCookie(context, provider, token)
        // return getAccessToken(cookie);
        return null;
    }

    public void getAccessToken(Context context, String provider, String token, final Callback<Credential> callback) {
        if (TextUtils.isEmpty(token)) {
            Log8.d();
            callback.failure(RetrofitError.unexpectedError("oauth://failure", new RuntimeException()));
            return;
        }
        if (TextUtils.isEmpty(provider)) {
            Log8.d();
            callback.failure(RetrofitError.unexpectedError("oauth://failure", new RuntimeException()));
            return;
        }
        if (context == null) {
            Log8.d();
            callback.failure(RetrofitError.unexpectedError("oauth://failure", new RuntimeException()));
            return;
        }

        requestHybridauthCookie(context, provider, token, new Callback<String>() {
            @Override
            public void success(String cookie, Response response) {
                Log8.d(cookie);
                getAccessToken(cookie, callback);
            }
            @Override
            public void failure(RetrofitError error) {
                Log8.d();
                callback.failure(error);
            }
        });
    }

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

    /**
     * Allow sign-up with access_token.
     *
     * @see <a href="https://github.com/yongjhih/drupal-hybridauth/commit/268b72a598665b0738e3b06e7b59dcb3bda5b999">Allow sign-up with access_token</a>
     */
    private void requestHybridauthCookie(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 {
                String cookie = response.header("Set-Cookie");
                if (!TextUtils.isEmpty(cookie)) {
                    setCookie(cookie);
                    callback.success(cookie, (Response) null);
                } else {
                    callback.failure(RetrofitError.unexpectedError(url, new RuntimeException()));
                }
            }
        });
        //call.execute();
    }

    protected Context context;

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

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

    public void setProvider(Context context, String provider, String token) {
        setContext(context);
        setProvider(provider);
        setToken(token);
    }

    public DrupalOAuth2Manager(String endpoint, String clientId, String clientSecret) {
        this((Context) null, endpoint, clientId, clientSecret);
    }

    protected OkHttpClient okHttpClient;

    public OkHttpClient getOkHttpClient() {
        if (okHttpClient == null) {
            okHttpClient = new OkHttpClient();

            if (allTrust) {
                okHttpClient.setSslSocketFactory(getTrustedFactory());
                okHttpClient.setHostnameVerifier(getTrustedVerifier());
            }

            okHttpClient.setFollowSslRedirects(true);
        }

        return okHttpClient;
    }

    public DrupalOAuth2Manager(Context context, String endpoint, String clientId, String clientSecret) {
        setContext(context);
        setEndpoint(endpoint);
        setClientId(clientId);
        setClientSecret(clientSecret);

        Client client = new OkClient(getOkHttpClient());

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

        RestAdapter restAdapter = new RestAdapter.Builder()
            .setEndpoint(endpoint)
            .setRequestInterceptor(mRequestInterceptor)
            .setErrorHandler(new ErrorHandler())
            .setClient(client)
            .setConverter(new retrofit.converter.JacksonConverter())
            .build();

        mService = restAdapter.create(DrupalOAuth2.class);
    }

    public String getCookie() {
        return cookie;
    }

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

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

    /**
     * A simple {@link RequestInterceptor} for each request of interception
     */
    class SimpleRequestInterceptor implements RequestInterceptor {
        public String cookie;

        @Override
        public void intercept(RequestFacade request) {
            if (!android.text.TextUtils.isEmpty(cookie)) {
                request.addHeader("Cookie", cookie);
            }
        }
    }

    protected SimpleRequestInterceptor mRequestInterceptor;

    /**
     * A simple {@link retrofit.ErrorHandler}
     */
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class ErrorHandler implements retrofit.ErrorHandler {
        public ErrorHandler() {}

        @Override
        public Throwable handleError(RetrofitError cause) {
            cause.printStackTrace();
            return cause;
        }
    }

    /**
     * A all trust SSLSocketFactory
     */
    private static SSLSocketFactory sTrustedFactory;

    /**
     * A getter of all trust SSLSocketFactory
     */
    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;
    }

    /**
     * A all trust host name verifier.
     */
    private static HostnameVerifier sTrustedVerifier;

    /**
     * A getter of all trust host name verifier
     */
    public static HostnameVerifier getTrustedVerifier() {
        if (sTrustedVerifier == null) {
            sTrustedVerifier = new HostnameVerifier() {
                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }
            };
        }

        return sTrustedVerifier;
    }

    public DrupalOAuth2Manager setUsername(String username) {
        this.username = username;
        return this;
    }

    public DrupalOAuth2Manager setPassword(String password) {
        this.password = password;
        return this;
    }
}