package com.malmstein.yahnac.data;

import android.content.ContentValues;
import android.text.TextUtils;
import android.util.Pair;

import com.firebase.client.DataSnapshot;
import com.firebase.client.Firebase;
import com.firebase.client.FirebaseError;
import com.firebase.client.ValueEventListener;
import com.malmstein.yahnac.comments.parser.CommentsParser;
import com.malmstein.yahnac.comments.parser.VoteUrlParser;
import com.malmstein.yahnac.injection.Inject;
import com.malmstein.yahnac.model.Login;
import com.malmstein.yahnac.model.OperationResponse;
import com.malmstein.yahnac.model.Story;
import com.novoda.notils.logger.simple.Log;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import org.jsoup.Connection;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

import rx.Observable;
import rx.Subscriber;
import rx.functions.Func1;
import rx.schedulers.Schedulers;

public class HNewsApi {

    private static final String BAD_UPVOTE_RESPONSE = "Can't make that vote.";

    private static Element extractHmac(Document replyDocument) {
        return replyDocument
                .select("input[name=hmac]")
                .first();
    }

    public Observable<List<ContentValues>> getStories(final Story.FILTER FILTER) {

        return Observable.create(new Observable.OnSubscribe<DataSnapshot>() {
            @Override
            public void call(final Subscriber<? super DataSnapshot> subscriber) {
                Firebase topStories = getStoryFirebase(FILTER);
                topStories.addValueEventListener(new ValueEventListener() {
                    @Override
                    public void onDataChange(DataSnapshot dataSnapshot) {
                        if (dataSnapshot != null) {
                            subscriber.onNext(dataSnapshot);
                        } else {
                            Inject.crashAnalytics().logSomethingWentWrong("HNewsApi: getStories is empty for " + FILTER.name());
                        }
                        subscriber.onCompleted();
                    }

                    @Override
                    public void onCancelled(FirebaseError firebaseError) {
                        Log.d(firebaseError.getCode());
                    }
                });
            }
        }).flatMap(new Func1<DataSnapshot, Observable<Pair<Integer, Long>>>() {
            @Override
            public Observable<Pair<Integer, Long>> call(final DataSnapshot dataSnapshot) {
                return Observable.create(new Observable.OnSubscribe<Pair<Integer, Long>>() {
                    @Override
                    public void call(Subscriber<? super Pair<Integer, Long>> subscriber) {
                        for (int i = 0; i < dataSnapshot.getChildrenCount(); i++) {
                            Long id = (Long) dataSnapshot.child(String.valueOf(i)).getValue();
                            Integer rank = Integer.valueOf(dataSnapshot.child(String.valueOf(i)).getKey());
                            Pair<Integer, Long> storyRoot = new Pair<>(rank, id);
                            subscriber.onNext(storyRoot);
                        }
                        subscriber.onCompleted();
                    }
                });
            }
        }).flatMap(new Func1<Pair<Integer, Long>, Observable<ContentValues>>() {
            @Override
            public Observable<ContentValues> call(final Pair<Integer, Long> storyRoot) {
                return Observable.create(new Observable.OnSubscribe<ContentValues>() {
                    @Override
                    public void call(final Subscriber<? super ContentValues> subscriber) {
                        final Firebase story = new Firebase("https://hacker-news.firebaseio.com/v0/item/" + storyRoot.second);
                        story.addValueEventListener(new ValueEventListener() {
                            @Override
                            public void onDataChange(DataSnapshot dataSnapshot) {
                                Map<String, Object> newItem = (Map<String, Object>) dataSnapshot.getValue();
                                if (newItem != null) {
                                    ContentValues story = mapStory(newItem, FILTER, storyRoot.first);
                                    if (story != null) {
                                        subscriber.onNext(story);
                                    } else {
                                        subscriber.onNext(new ContentValues());
                                        Inject.crashAnalytics().logSomethingWentWrong("HNewsApi: onDataChange is empty in " + storyRoot.second);
                                    }
                                }
                                subscriber.onCompleted();
                            }

                            @Override
                            public void onCancelled(FirebaseError firebaseError) {
                                Log.d(firebaseError.getCode());
                                Inject.crashAnalytics().logSomethingWentWrong("HNewsApi: onCancelled " + firebaseError.getMessage());
                                subscriber.onCompleted();
                            }
                        });
                    }
                });
            }
        })
                .toList();
    }

    private ContentValues mapStory(Map<String, Object> map, Story.FILTER filter, Integer rank) {

        ContentValues storyValues = new ContentValues();

        try {
            String by = (String) map.get("by");
            Long id = (Long) map.get("id");
            String type = (String) map.get("type");
            Long time = (Long) map.get("time");
            Long score = (Long) map.get("score");
            String title = (String) map.get("title");
            String url = (String) map.get("url");
            Long descendants = Long.valueOf(0);
            if (map.get("descendants") != null) {
                descendants = (Long) map.get("descendants");
            }

            storyValues.put(HNewsContract.StoryEntry.ITEM_ID, id);
            storyValues.put(HNewsContract.StoryEntry.BY, by);
            storyValues.put(HNewsContract.StoryEntry.TYPE, type);
            storyValues.put(HNewsContract.StoryEntry.TIME_AGO, time * 1000);
            storyValues.put(HNewsContract.StoryEntry.SCORE, score);
            storyValues.put(HNewsContract.StoryEntry.TITLE, title);
            storyValues.put(HNewsContract.StoryEntry.COMMENTS, descendants);
            storyValues.put(HNewsContract.StoryEntry.URL, url);
            storyValues.put(HNewsContract.StoryEntry.RANK, rank);
            storyValues.put(HNewsContract.StoryEntry.TIMESTAMP, System.currentTimeMillis());
            storyValues.put(HNewsContract.StoryEntry.FILTER, filter.name());
        } catch (Exception ex) {
            Log.d(ex.getMessage());
        }

        return storyValues;
    }

    private Firebase getStoryFirebase(Story.FILTER FILTER) {
        switch (FILTER) {
            case show:
                return new Firebase("https://hacker-news.firebaseio.com/v0/showstories");
            case ask:
                return new Firebase("https://hacker-news.firebaseio.com/v0/askstories");
            case jobs:
                return new Firebase("https://hacker-news.firebaseio.com/v0/jobstories");
            default:
                return new Firebase("https://hacker-news.firebaseio.com/v0/topstories");
        }
    }

    Observable<Vector<ContentValues>> getCommentsFromStory(Long storyId) {
        return Observable.create(
                new CommentsUpdateOnSubscribe(storyId))
                .subscribeOn(Schedulers.io());
    }

    Observable<Login> login(String username, String password) {
        return Observable.create(
                new LoginOnSubscribe(username, password))
                .subscribeOn(Schedulers.io());
    }

    Observable<OperationResponse> vote(Story storyId) {
        return Observable.create(
                new ParseVoteUrlOnSubscribe(storyId.getId()))
                .flatMap(new Func1<String, Observable<OperationResponse>>() {
                    @Override
                    public Observable<OperationResponse> call(final String voteUrl) {
                        return Observable.create(new Observable.OnSubscribe<OperationResponse>() {
                            @Override
                            public void call(Subscriber<? super OperationResponse> subscriber) {

                                if (voteUrl.equals(VoteUrlParser.EMPTY)) {
                                    subscriber.onNext(OperationResponse.FAILURE);
                                }

                                try {
                                    ConnectionProvider connectionProvider = Inject.connectionProvider();
                                    Connection.Response response = connectionProvider
                                            .voteConnection(voteUrl)
                                            .execute();

                                    if (response.statusCode() == 200) {
                                        if (response.body() == null) {
                                            subscriber.onError(new Throwable(""));
                                        }

                                        Document doc = response.parse();
                                        String text = doc.text();

                                        if (text.equals(BAD_UPVOTE_RESPONSE)) {
                                            subscriber.onNext(OperationResponse.FAILURE);
                                        } else {
                                            subscriber.onNext(OperationResponse.SUCCESS);
                                        }
                                    } else {
                                        subscriber.onNext(OperationResponse.FAILURE);
                                    }

                                } catch (IOException e) {
                                    subscriber.onError(e);
                                }

                            }
                        });
                    }
                })
                .subscribeOn(Schedulers.io());
    }

    Observable<OperationResponse> commentOnStory(final Long itemId, final String comment) {
        return Observable.create(
                new ParseHmacOnSubscribe(itemId))
                .flatMap(new Func1<String, Observable<OperationResponse>>() {
                    @Override
                    public Observable<OperationResponse> call(final String hmac) {
                        return Observable.create(new Observable.OnSubscribe<OperationResponse>() {
                            @Override
                            public void call(Subscriber<? super OperationResponse> subscriber) {

                                try {
                                    ConnectionProvider connectionProvider = Inject.connectionProvider();
                                    Request request = connectionProvider
                                            .commentOnStoryRequest(String.valueOf(itemId), comment, hmac);

                                    OkHttpClient client = new OkHttpClient();
                                    Response response = client.newCall(request).execute();

                                    if (response.code() == 200) {
                                        subscriber.onNext(OperationResponse.SUCCESS);
                                    } else {
                                        subscriber.onNext(OperationResponse.FAILURE);
                                    }

                                } catch (IOException e) {
                                    subscriber.onError(e);
                                }

                            }
                        });
                    }
                })
                .subscribeOn(Schedulers.io());
    }

    Observable<OperationResponse> replyToComment(final Long storyId, final long commentId, final String comment) {
        return Observable.create(
                new ParseReplyHmacOnSubscribe(storyId, commentId))
                .flatMap(new Func1<String, Observable<OperationResponse>>() {
                    @Override
                    public Observable<OperationResponse> call(final String hmac) {
                        return Observable.create(new Observable.OnSubscribe<OperationResponse>() {
                            @Override
                            public void call(Subscriber<? super OperationResponse> subscriber) {

                                try {
                                    ConnectionProvider connectionProvider = Inject.connectionProvider();
                                    Request request = connectionProvider
                                            .replyToCommentRequest(String.valueOf(storyId),
                                                    String.valueOf(commentId), comment, hmac);

                                    OkHttpClient client = new OkHttpClient();
                                    Response response = client.newCall(request).execute();

                                    if (response.code() == 200) {
                                        subscriber.onNext(OperationResponse.SUCCESS);
                                    } else {
                                        subscriber.onNext(OperationResponse.FAILURE);
                                    }

                                } catch (IOException e) {
                                    subscriber.onError(e);
                                }

                            }
                        });
                    }
                })
                .subscribeOn(Schedulers.io());
    }

    private static class CommentsUpdateOnSubscribe implements Observable.OnSubscribe<Vector<ContentValues>> {

        private final Long storyId;
        private Subscriber<? super Vector<ContentValues>> subscriber;

        private CommentsUpdateOnSubscribe(Long storyId) {
            this.storyId = storyId;
        }

        @Override
        public void call(Subscriber<? super Vector<ContentValues>> subscriber) {
            this.subscriber = subscriber;
            startFetchingComments();
            subscriber.onCompleted();
        }

        private void startFetchingComments() {
            Vector<ContentValues> commentsList = new Vector<>();
            try {
                ConnectionProvider connectionProvider = Inject.connectionProvider();
                Document commentsDocument = connectionProvider
                        .commentsConnection(storyId)
                        .get();

                commentsList = new CommentsParser(storyId, commentsDocument).parse();

            } catch (IOException e) {
                subscriber.onError(e);
            }
            subscriber.onNext(commentsList);
        }
    }

    private static class LoginOnSubscribe implements Observable.OnSubscribe<Login> {

        private final String username;
        private final String password;
        private Subscriber<? super Login> subscriber;

        private LoginOnSubscribe(String username, String password) {
            this.username = username;
            this.password = password;
        }

        @Override
        public void call(Subscriber<? super Login> subscriber) {
            this.subscriber = subscriber;
            attemptLogin();
            subscriber.onCompleted();
        }

        private void attemptLogin() {
            try {
                ConnectionProvider connectionProvider = Inject.connectionProvider();
                Connection.Response response = connectionProvider
                        .loginConnection(username, password)
                        .execute();

                String cookie = response.cookie("user");
                String cfduid = response.cookie("_cfduid");

                if (!TextUtils.isEmpty(cookie)) {
                    subscriber.onNext(new Login(username, cookie, Login.Status.SUCCESSFUL));
                } else {
                    subscriber.onNext(new Login(username, null, Login.Status.WRONG_CREDENTIALS));
                }

            } catch (IOException e) {
                subscriber.onError(e);
            }
        }
    }

    private static class ParseVoteUrlOnSubscribe implements Observable.OnSubscribe<String> {

        private final Long storyId;
        private Subscriber<? super String> subscriber;

        private ParseVoteUrlOnSubscribe(Long storyId) {
            this.storyId = storyId;
        }

        @Override
        public void call(Subscriber<? super String> subscriber) {
            this.subscriber = subscriber;
            startFetchingVoteUrl();
            subscriber.onCompleted();
        }

        private void startFetchingVoteUrl() {
            try {
                ConnectionProvider connectionProvider = Inject.connectionProvider();
                Document commentsDocument = connectionProvider
                        .commentsConnection(storyId)
                        .get();

                String voteUrl = new VoteUrlParser(commentsDocument, storyId).parse();
                if (voteUrl.equals("/null")) {
                    subscriber.onError(new LoggedOutException());
                } else {
                    subscriber.onNext(voteUrl);
                }
            } catch (IOException e) {
                subscriber.onError(e);
            }
        }
    }

    private static class ParseHmacOnSubscribe implements Observable.OnSubscribe<String> {

        private final Long storyId;
        private Subscriber<? super String> subscriber;

        private ParseHmacOnSubscribe(Long storyId) {
            this.storyId = storyId;
        }

        @Override
        public void call(Subscriber<? super String> subscriber) {
            this.subscriber = subscriber;
            startFetchingHmac();
            subscriber.onCompleted();
        }

        private void startFetchingHmac() {
            try {
                ConnectionProvider connectionProvider = Inject.connectionProvider();

                Document replyDocument = connectionProvider
                        .commentsConnection(storyId)
                        .get();

                Element replyInput = extractHmac(replyDocument);

                if (replyInput != null) {
                    String replyFnid = replyInput.attr("value");
                    subscriber.onNext(replyFnid);
                } else {
                    subscriber.onError(new LoggedOutException());
                }

            } catch (IOException e) {
                subscriber.onError(e);
            }
        }
    }

    private static class ParseReplyHmacOnSubscribe implements Observable.OnSubscribe<String> {

        private final Long storyId;
        private final Long commentId;
        private Subscriber<? super String> subscriber;

        private ParseReplyHmacOnSubscribe(Long storyId, Long commentId) {
            this.storyId = storyId;
            this.commentId = commentId;
        }

        @Override
        public void call(Subscriber<? super String> subscriber) {
            this.subscriber = subscriber;
            startFetchingHmac();
            subscriber.onCompleted();
        }

        private void startFetchingHmac() {
            try {
                ConnectionProvider connectionProvider = Inject.connectionProvider();

                Document replyDocument = connectionProvider
                        .replyCommentConnection(storyId, commentId)
                        .get();

                Element replyInput = extractHmac(replyDocument);

                if (replyInput != null) {
                    String hmac = replyInput.attr("value");
                    subscriber.onNext(hmac);
                } else {
                    subscriber.onError(new LoggedOutException());
                }

            } catch (IOException e) {
                subscriber.onError(e);
            }
        }
    }
}