package org.mercury_im.messenger.core.stores;

import org.jivesoftware.smack.roster.packet.RosterPacket;
import org.jxmpp.jid.Jid;
import org.mercury_im.messenger.xmpp.model.AccountModel;
import org.mercury_im.messenger.xmpp.model.ContactModel;
import org.mercury_im.messenger.xmpp.model.EntityModel;
import org.mercury_im.messenger.xmpp.repository.RequeryAccountRepository;
import org.mercury_im.messenger.xmpp.repository.RosterRepository;
import org.mercury_im.messenger.xmpp.enums.SubscriptionDirection;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.inject.Inject;

import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;

public class RosterStore implements org.jivesoftware.smack.roster.rosterstore.RosterStore {

    private static final Logger LOGGER = Logger.getLogger(RosterStore.class.getName());

    private final RosterRepository rosterRepository;
    private final RequeryAccountRepository accountRepository;
    private AccountModel account;
    private CompositeDisposable disposable = null;

    private final Map<Jid, RosterPacket.Item> itemMap = new HashMap<>();
    private String rosterVersion;

    @Inject
    public RosterStore(RosterRepository rosterRepository, RequeryAccountRepository accountRepository) {
        this.rosterRepository = rosterRepository;
        this.accountRepository = accountRepository;
    }

    public void subscribe() {
        LOGGER.log(Level.INFO, "Subscribing...");
        if (disposable != null) {
            return;
        }
        disposable = new CompositeDisposable();

        disposable.add(rosterRepository.getAllContactsOfAccount(account)
                .observeOn(Schedulers.computation())
                .subscribe(contactsList -> {
                            itemMap.clear();
                            for (ContactModel contactModel : contactsList) {
                                itemMap.put(contactModel.getEntity().getJid(), fromModel(contactModel));
                                LOGGER.log(Level.INFO, "Populate itemMap with " + contactsList.toList().size() + " items");

                            }
                        },
                        error -> LOGGER.log(Level.WARNING, "An error occurred while updating roster cache", error)));

        disposable.add(rosterRepository.getRosterVersion(account)
                .observeOn(Schedulers.computation())
                .subscribe(
                        result -> setRosterVersion(result),
                        error -> LOGGER.log(Level.WARNING, "An error occurred updating cached roster version", error)));
    }

    public void unsubscribe() {
        if (disposable == null) {
            return;
        }
        disposable.dispose();
        disposable = null;
    }

    public void setAccountId(long accountId) {
        this.account = accountRepository.getAccount(accountId)
                .doOnSubscribe(subscribe -> LOGGER.log(Level.FINE, "Fetching account " + accountId))
                .blockingFirst().first();
    }

    private void setRosterVersion(String rosterVersion) {
        this.rosterVersion = rosterVersion;
    }

    @Override
    public List<RosterPacket.Item> getEntries() {
        return new ArrayList<>(itemMap.values());
    }

    @Override
    public RosterPacket.Item getEntry(Jid bareJid) {
        return itemMap.get(bareJid);
    }

    @Override
    public String getRosterVersion() {
        return rosterVersion != null ? rosterVersion : "";
    }

    @Override
    public boolean addEntry(RosterPacket.Item item, String version) {
        LOGGER.log(Level.INFO, "Add entry " + item.toXML().toString());
        // Update database
        ContactModel contact = toModel(item);
        disposable.add(rosterRepository.upsertContact(contact)
                .subscribe(
                        success -> LOGGER.log(Level.FINE, "Upserted contact model " + success + " successfully"),
                        error -> LOGGER.log(Level.WARNING, "An error occurred upserting contact " + contact, error)
                ));
        disposable.add(rosterRepository.updateRosterVersion(account, version)
                .subscribe(
                        success -> LOGGER.log(Level.FINE, "Upserted roster version to " + rosterVersion + " successfully"),
                        error -> LOGGER.log(Level.WARNING, "An error occurred upserting roster version", error)
                ));

        return true;
    }

    @Override
    public boolean resetEntries(Collection<RosterPacket.Item> items, String version) {
        LOGGER.log(Level.INFO, "Reset Entries: " + Arrays.toString(items.toArray()));
        // Update database
        // TODO: Delete other contacts
        for (RosterPacket.Item item : items) {
            ContactModel model = toModel(item);
            disposable.add(rosterRepository.upsertContact(model)
                    .subscribe(
                            success -> LOGGER.log(Level.FINE, "Upserted contact model " + success + " successfully"),
                            error -> LOGGER.log(Level.WARNING, "An error occurred upserting contact " + model, error)
                    ));
        }

        disposable.add(rosterRepository.updateRosterVersion(account, version)
                .subscribe(
                        success -> LOGGER.log(Level.FINE, "Upserted roster version to " + rosterVersion + " successfully"),
                        error -> LOGGER.log(Level.WARNING, "An error occurred upserting roster version", error)
                ));

        return true;
    }

    @Override
    public boolean removeEntry(Jid bareJid, String version) {
        LOGGER.log(Level.INFO, "Remove entry " + bareJid.toString());

        disposable.add(rosterRepository.deleteContact(account.getId(), bareJid.asEntityBareJidOrThrow())
                .subscribe(
                        () -> LOGGER.log(Level.FINE, "Deletion of contact " + bareJid.toString() + " successful"),
                        error -> LOGGER.log(Level.WARNING, "An error occurred deleting contact " + bareJid.toString(), error)
                ));
        disposable.add(rosterRepository.updateRosterVersion(account, version)
                .subscribe(
                        success -> LOGGER.log(Level.FINE, "Upserted roster version to " + rosterVersion + " successfully"),
                        error -> LOGGER.log(Level.WARNING, "An error occurred upserting roster version", error)
                ));
        return true;
    }

    @Override
    public void resetStore() {
        LOGGER.log(Level.INFO, "Reset Store");

        disposable.add(rosterRepository.deleteAllContactsOfAccount(account)
                .subscribe(
                        success -> LOGGER.log(Level.FINE, "Successfully reset store."),
                        error -> LOGGER.log(Level.WARNING, "An error occurred resetting store", error)
                ));
        disposable.add(rosterRepository.updateRosterVersion(account, "")
                .subscribe(
                        success -> LOGGER.log(Level.FINE, "Successfully reset roster version"),
                        error -> LOGGER.log(Level.WARNING, "An error occurred resetting roster version", error)
                ));
    }

    public RosterPacket.Item fromModel(ContactModel contactModel) {
        RosterPacket.Item item = new RosterPacket.Item(
                contactModel.getEntity().getJid(),
                contactModel.getRostername());
        if (contactModel.getSub_direction() != null) {
            item.setItemType(convert(contactModel.getSub_direction()));
        }
        item.setApproved(contactModel.isSub_approved());
        item.setSubscriptionPending(contactModel.isSub_pending());
        return item;
    }

    public ContactModel toModel(RosterPacket.Item item) {
        ContactModel contact = new ContactModel();

        contact.setRostername(item.getName());
        if (item.getItemType() != null) {
            contact.setSub_direction(convert(item.getItemType()));
        }
        contact.setSub_approved(item.isApproved());
        contact.setSub_pending(item.isSubscriptionPending());

        EntityModel entity = new EntityModel();
        entity.setAccount(account);
        entity.setJid(item.getJid().asEntityBareJidOrThrow());

        contact.setEntity(entity);

        return contact;
    }

    public SubscriptionDirection convert(RosterPacket.ItemType type) {
        return SubscriptionDirection.valueOf(type.toString());

    }

    public RosterPacket.ItemType convert(SubscriptionDirection direction) {
        return RosterPacket.ItemType.fromString(direction.toString());
    }
}
