/*
 * Copyright 2015 Google Inc.
 *
 * 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.google.android.apps.gutenberg.provider;

import android.accounts.Account;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.SyncResult;
import android.database.Cursor;
import android.os.Bundle;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;

import com.android.volley.ParseError;
import com.android.volley.RequestQueue;
import com.android.volley.ServerError;
import com.android.volley.toolbox.RequestFuture;
import com.google.android.apps.gutenberg.BuildConfig;
import com.google.android.apps.gutenberg.GutenbergApplication;
import com.google.android.apps.gutenberg.util.CheckInRequest;
import com.google.android.apps.gutenberg.util.GaeJsonArrayRequest;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.plus.People;
import com.google.android.gms.plus.Plus;
import com.google.android.gms.plus.model.people.Person;
import com.google.android.gms.plus.model.people.PersonBuffer;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

public class SyncAdapter extends AbstractThreadedSyncAdapter {

    public static final String EXTRA_AUTH_TOKEN = "auth_token";

    /**
     * Boolean extra for syncing checkinTime only
     */
    public static final String EXTRA_ONLY_CHECKINS = "only_checkins";

    private static final String TAG = "SyncAdapter";

    private GoogleApiClient mApiClient;

    public SyncAdapter(Context context, boolean autoInitialize) {
        this(context, autoInitialize, false);
    }

    public SyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) {
        super(context, autoInitialize, allowParallelSyncs);
        mApiClient = new GoogleApiClient.Builder(context)
                .addApi(Plus.API)
                .addScope(Plus.SCOPE_PLUS_LOGIN)
                .addScope(Plus.SCOPE_PLUS_PROFILE)
                .build();
    }

    @Override
    public void onPerformSync(Account account, Bundle extras, String authority,
                              ContentProviderClient provider, SyncResult syncResult) {
        String authToken = extras.getString(EXTRA_AUTH_TOKEN);
        if (TextUtils.isEmpty(authToken)) {
            Log.d(TAG, "Not authorized. Cannot sync.");
            return;
        }
        mApiClient.blockingConnect(5, TimeUnit.SECONDS);
        try {
            String cookie = getCookie(authToken);
            syncCheckins(provider, cookie);
            if (!extras.getBoolean(EXTRA_ONLY_CHECKINS, false)) {
                syncEvents(provider, cookie);
            }
        } catch (IOException e) {
            Log.e(TAG, "Error performing sync.", e);
        }
    }

    private void syncCheckins(ContentProviderClient provider, String cookie) {
        Cursor cursor = null;
        try {
            cursor = provider.query(Table.ATTENDEE.getBaseUri(), new String[]{
                    Table.Attendee.ID,
                    Table.Attendee.CHECKIN,
                    Table.Attendee.EVENT_ID,
            }, Table.Attendee.CHECKIN_MODIFIED, null, null);
            if (0 == cursor.getCount()) {
                Log.d(TAG, "No checkin to sync.");
                return;
            }
            int syncCount = 0;
            while (cursor.moveToNext()) {
                String attendeeId = cursor.getString(
                        cursor.getColumnIndexOrThrow(Table.Attendee.ID));
                String eventId = cursor.getString(
                        cursor.getColumnIndexOrThrow(Table.Attendee.EVENT_ID));
                long checkin = cursor.getLong(cursor.getColumnIndexOrThrow(Table.Attendee.CHECKIN));
                long serverCheckin = postCheckIn(attendeeId, eventId, checkin == 0, cookie);
                if (serverCheckin >= 0) {
                    ContentValues values = new ContentValues();
                    values.put(Table.Attendee.CHECKIN_MODIFIED, false);
                    if (0 == serverCheckin) {
                        values.putNull(Table.Attendee.CHECKIN);
                    } else {
                        values.put(Table.Attendee.CHECKIN, serverCheckin);
                    }
                    provider.update(Table.ATTENDEE.getItemUri(eventId, attendeeId),
                            values, null, null);
                    ++syncCount;
                }
            }
            Log.d(TAG, syncCount + " checkin(s) synced.");
        } catch (RemoteException e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

    private long postCheckIn(String attendeeId, String eventId, boolean revert, String cookie) {
        RequestQueue queue = GutenbergApplication.from(getContext()).getRequestQueue();
        RequestFuture<JSONObject> future = RequestFuture.newFuture();
        queue.add(new CheckInRequest(cookie, eventId, attendeeId, revert, future, future));
        try {
            JSONObject object = future.get();
            return object.getLong("checkinTime");
        } catch (InterruptedException | ExecutionException | JSONException e) {
            Throwable cause = e.getCause();
            if (cause instanceof ServerError) {
                ServerError error = (ServerError) cause;
                Log.e(TAG, "Server error: " + new String(error.networkResponse.data));
            }
            Log.e(TAG, "Cannot sync checkin.", e);
        }
        return -1;
    }

    private void syncEvents(ContentProviderClient provider, String cookie) {
        try {
            RequestQueue requestQueue = GutenbergApplication.from(getContext()).getRequestQueue();
            JSONArray events = getEvents(requestQueue, cookie);
            Pair<String[], ContentValues[]> pair = parseEvents(events);
            String[] eventIds = pair.first;
            provider.bulkInsert(Table.EVENT.getBaseUri(), pair.second);
            ArrayList<ContentProviderOperation> operations = new ArrayList<>();
            operations.add(ContentProviderOperation.newDelete(Table.EVENT.getBaseUri())
                    .withSelection(Table.Event.ID + " NOT IN ('" +
                            TextUtils.join("', '", eventIds) + "')", null)
                    .build());
            operations.add(ContentProviderOperation.newDelete(Table.ATTENDEE.getBaseUri())
                    .withSelection(Table.Attendee.EVENT_ID + " NOT IN ('" +
                            TextUtils.join("', '", eventIds) + "')", null)
                    .build());
            provider.applyBatch(operations);
            for (String eventId : eventIds) {
                JSONArray attendees = getAttendees(requestQueue, eventId, cookie);
                provider.bulkInsert(
                        Table.ATTENDEE.getBaseUri(), parseAttendees(eventId, attendees));
            }
            Log.d(TAG, eventIds.length + " event(s) synced.");
        } catch (ExecutionException | InterruptedException | JSONException | RemoteException |
                OperationApplicationException e) {
            Log.e(TAG, "Error performing sync.", e);
        }
    }

    private static String getCookie(String authToken) throws IOException {
        HttpURLConnection connection = null;
        try {
            connection = (HttpURLConnection) new URL(
                    BuildConfig.HOST + "/_ah/login?continue=http://localhost/&auth=" +
                            authToken).openConnection();
            connection.setInstanceFollowRedirects(false);
            connection.connect();
            if (connection.getResponseCode() != 302) {
                Log.e(TAG, "Cannot fetch the cookie: " + connection.getResponseCode());
                return null;
            }
            String cookie = connection.getHeaderField("Set-Cookie");
            if (!cookie.contains("SACSID")) {
                return null;
            }
            return cookie;
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }

    private static JSONArray getEvents(RequestQueue requestQueue, String cookie) throws
            ExecutionException, InterruptedException {
        RequestFuture<JSONArray> future = RequestFuture.newFuture();
        requestQueue.add(new GaeJsonArrayRequest(
                BuildConfig.HOST + "/v1/event/list", cookie, future, future));
        try {
            return future.get();
        } catch (ExecutionException e) {
            if (didServerReturnNull(e)) {
                return new JSONArray();
            }
            throw e;
        }
    }

    private static Pair<String[], ContentValues[]> parseEvents(JSONArray events) throws JSONException {
        int length = events.length();
        ContentValues[] array = new ContentValues[length];
        String[] ids = new String[length];
        for (int i = 0; i < length; ++i) {
            JSONObject event = events.getJSONObject(i);
            array[i] = new ContentValues();
            String id = event.getString("id");
            ids[i] = id;
            array[i].put(Table.Event.ID, id);
            array[i].put(Table.Event.NAME, event.getString("name"));
            array[i].put(Table.Event.PLACE, event.getString("place"));
            array[i].put(Table.Event.ORGANIZER_NAME, event.getString("organizerName"));
            array[i].put(Table.Event.START_TIME, event.getString("startTime"));
            array[i].put(Table.Event.END_TIME, event.getString("endTime"));
        }
        return new Pair<>(ids, array);
    }

    private static JSONArray getAttendees(RequestQueue requestQueue, String eventId, String cookie)
            throws ExecutionException, InterruptedException {
        RequestFuture<JSONArray> future = RequestFuture.newFuture();
        requestQueue.add(new GaeJsonArrayRequest(
                BuildConfig.HOST + "/v1/event/" + eventId + "/attendees", cookie, future, future));
        try {
            return future.get();
        } catch (ExecutionException e) {
            if (didServerReturnNull(e)) {
                return new JSONArray();
            }
            throw e;
        }
    }

    // A workaround for the server returning "null" for empty array
    private static boolean didServerReturnNull(ExecutionException e) {
        if (e.getCause() instanceof ParseError) {
            ParseError cause = (ParseError) e.getCause();
            if (cause.getCause() instanceof JSONException) {
                JSONException causeCause = (JSONException) cause.getCause();
                if (causeCause.getMessage().contains("Value null of")) {
                    return true;
                }
            }
        }
        return false;
    }

    private ContentValues[] parseAttendees(String eventId, JSONArray attendees)
            throws JSONException {
        int length = attendees.length();
        ContentValues[] array = new ContentValues[length];
        HashMap<String, String> imageUrls = new HashMap<>();
        for (int i = 0; i < length; ++i) {
            JSONObject attendee = attendees.getJSONObject(i);
            array[i] = new ContentValues();
            array[i].put(Table.Attendee.EVENT_ID, eventId);
            array[i].put(Table.Attendee.NAME, attendee.getString("name"));
            array[i].put(Table.Attendee.ID, attendee.getString("id"));
            array[i].put(Table.Attendee.EMAIL, attendee.getString("email"));
            String plusid = attendee.getString("plusid");
            if (!TextUtils.isEmpty(plusid)) {
                array[i].put(Table.Attendee.PLUSID, plusid);
                imageUrls.put(plusid, "null");
            }
            long checkinTime = attendee.getLong("checkinTime");
            if (0 == checkinTime) {
                array[i].putNull(Table.Attendee.CHECKIN);
            } else {
                array[i].put(Table.Attendee.CHECKIN, checkinTime);
            }
            array[i].putNull(Table.Attendee.IMAGE_URL);
        }
        // Fetch all the Google+ Image URLs at once if necessary
        if (mApiClient != null && mApiClient.isConnected() && !imageUrls.isEmpty()) {
            People.LoadPeopleResult result =
                    Plus.PeopleApi.load(mApiClient, imageUrls.keySet()).await();
            PersonBuffer personBuffer = result.getPersonBuffer();
            if (personBuffer != null) {
                // Copy URLs into the HashMap
                for (Person person : personBuffer) {
                    if (person.hasImage()) {
                        imageUrls.put(extractId(person.getUrl()), person.getImage().getUrl());
                    }
                }
                // Fill the missing URLs in the array of ContentValues
                for (ContentValues values : array) {
                    if (values.containsKey(Table.Attendee.PLUSID)) {
                        String plusId = values.getAsString(Table.Attendee.PLUSID);
                        String imageUrl = imageUrls.get(plusId);
                        if (!TextUtils.isEmpty(imageUrl)) {
                            values.put(Table.Attendee.IMAGE_URL, imageUrl);
                        }
                    }
                }
            }
        }
        return array;
    }

    private static String extractId(String profileUrl) {
        return profileUrl.substring(profileUrl.lastIndexOf('/') + 1);
    }

}