package com.workfusion.odf.mail.impl.javax;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.stream.Collectors;

import microsoft.exchange.webservices.data.core.ExchangeService;
import microsoft.exchange.webservices.data.core.PropertySet;
import microsoft.exchange.webservices.data.core.enumeration.misc.ExchangeVersion;
import microsoft.exchange.webservices.data.core.enumeration.property.BasePropertySet;
import microsoft.exchange.webservices.data.core.enumeration.property.BodyType;
import microsoft.exchange.webservices.data.core.enumeration.property.WellKnownFolderName;
import microsoft.exchange.webservices.data.core.enumeration.search.FolderTraversal;
import microsoft.exchange.webservices.data.core.enumeration.search.ItemTraversal;
import microsoft.exchange.webservices.data.core.enumeration.search.LogicalOperator;
import microsoft.exchange.webservices.data.core.enumeration.search.SortDirection;
import microsoft.exchange.webservices.data.core.enumeration.service.ConflictResolutionMode;
import microsoft.exchange.webservices.data.core.enumeration.service.DeleteMode;
import microsoft.exchange.webservices.data.core.exception.service.local.ServiceLocalException;
import microsoft.exchange.webservices.data.core.service.folder.Folder;
import microsoft.exchange.webservices.data.core.service.item.Item;
import microsoft.exchange.webservices.data.core.service.schema.EmailMessageSchema;
import microsoft.exchange.webservices.data.core.service.schema.FolderSchema;
import microsoft.exchange.webservices.data.core.service.schema.ItemSchema;
import microsoft.exchange.webservices.data.credential.WebCredentials;
import microsoft.exchange.webservices.data.property.complex.FolderId;
import microsoft.exchange.webservices.data.property.complex.ItemId;
import microsoft.exchange.webservices.data.property.definition.PropertyDefinitionBase;
import microsoft.exchange.webservices.data.search.FindFoldersResults;
import microsoft.exchange.webservices.data.search.FindItemsResults;
import microsoft.exchange.webservices.data.search.FolderView;
import microsoft.exchange.webservices.data.search.ItemView;
import microsoft.exchange.webservices.data.search.filter.SearchFilter;
import org.apache.commons.lang.NotImplementedException;

import com.workfusion.odf.mail.impl.javax.exception.EwsException;
import com.workfusion.odf.service.mail.EmailManager;
import com.workfusion.odf.service.mail.EmailSender;
import com.workfusion.odf.service.mail.MessageConverter;
import com.workfusion.odf.service.mail.entity.EmailAddress;
import com.workfusion.odf.service.mail.entity.EmailMessage;

public class EwsEmailsClient implements EmailManager, EmailSender {
    private ExchangeService service;
    private String host;
    private String domain;
    private WebCredentials credentials;
    private MessageConverter<microsoft.exchange.webservices.data.core.service.item.EmailMessage> messageConverter;
    private ExchangeVersion exchangeVersion;
    private PropertySet bindPropSet = new PropertySet();

    public EwsEmailsClient(ExchangeService service) {
        this.service = service;
        this.host = service.getUrl().getHost();
        this.credentials = (WebCredentials) service.getCredentials();
        this.exchangeVersion = service.getRequestedServerVersion();
        initConverter(service);
    }

    public EwsEmailsClient(String host, String username, String password, String domain, ExchangeVersion exchangeVersion) {
        this.host = host;
        this.exchangeVersion = exchangeVersion;
        this.credentials = new WebCredentials(username, password, domain);
        initEwsEmailClient(this.host, this.credentials, exchangeVersion);
    }

    public EwsEmailsClient(String host, String username, String password, ExchangeVersion exchangeVersion) {
        this.host = host;
        this.exchangeVersion = exchangeVersion;
        this.credentials = new WebCredentials(username, password);
        initEwsEmailClient(this.host, this.credentials, exchangeVersion);
    }

    public EwsEmailsClient(String host, String username, String password) {
        this.host = host;
        this.exchangeVersion = ExchangeVersion.Exchange2010_SP2;
        this.credentials = new WebCredentials(username, password);
        initEwsEmailClient(host, credentials, exchangeVersion);
    }


    private void initEwsEmailClient(String host, WebCredentials credentials, ExchangeVersion exchangeVersion) {
        this.service = new ExchangeService(exchangeVersion);
        initConverter(service);
        try {
            service.setUrl(new URI(host));
            service.setCredentials(credentials);
        } catch (URISyntaxException e) {
            throw new EwsException("Could not initialize Service", e);
        }
    }

    private void initConverter(ExchangeService service) {
        this.messageConverter = new EwsMessageConverter(service);
    }

    public boolean isConnected() {
        if (host == null || credentials == null) {
            return false;
        } else {
            try {
                Folder folder;
                folder = Folder.bind(service, WellKnownFolderName.Root);
                return (folder != null);
            } catch (Exception e) {
                throw new EwsException("Could not bind root folder", e);
            }
        }
    }

    @Override
    public boolean createFolder(String folderName) {
        Folder folder = createFolder(folderName, WellKnownFolderName.Inbox);
        return folder != null;
    }

    public Folder createFolder(String folderName, WellKnownFolderName wellKnownFolderName) {
        try {
            Folder newFolder = new Folder(service);
            newFolder.setDisplayName(folderName);
            newFolder.save(wellKnownFolderName);
            return newFolder;
        } catch (Exception e) {
            throw new EwsException("Could not create folder", e);
        }
    }

    public Folder createFolder(String folderName, FolderId parentFolderId) {
        try {
            Folder newFolder = new Folder(service);
            newFolder.setDisplayName(folderName);
            newFolder.save(parentFolderId);
            return newFolder;
        } catch (Exception e) {
            throw new EwsException("Could not create folder", e);
        }
    }

    @Override
    public boolean removeMessage(EmailMessage message) {
        String messageId = message.getId().orElseThrow(() -> new EwsException("Id is not set for EmailMessage"));
        try {
            microsoft.exchange.webservices.data.core.service.item.EmailMessage email =
                    microsoft.exchange.webservices.data.core.service.item.EmailMessage.bind(service, new ItemId(messageId));
            email.delete(DeleteMode.HardDelete);
            return true;
        } catch (Exception e) {
            throw new EwsException("Could not remove message", e);
        }

    }

    /**
     * Empty Folder select strict DeleteMode.HardDelete; DeleteMode.SoftDelete; DeleteMode.MoveToDeletedItems
     *
     * @param folderName
     * @param deleteMode
     * @param subFolder
     * @return
     * @throws EwsException
     */
    public boolean emptyFolder(String folderName, DeleteMode deleteMode, boolean subFolder) {
        try {
            service.emptyFolder(FolderId.getFolderIdFromWellKnownFolderName(WellKnownFolderName.valueOf(folderName)),
                    deleteMode, subFolder);
            return true;
        } catch (Exception e) {
            throw new EwsException("Could not empty the folder", e);
        }
    }


    @Override
    public EmailMessage putMessage(EmailMessage message, String targetFolder) {
        throw new NotImplementedException("Method putMessage is not implemented");
    }

    @Override
    public EmailMessage copyMessage(EmailMessage message, String targetFolder) {
        String messageId = message.getId().orElseThrow(() -> new EwsException("Id is not set for EmailMessage"));
        try {
            microsoft.exchange.webservices.data.core.service.item.EmailMessage email =
                    microsoft.exchange.webservices.data.core.service.item.EmailMessage.bind(service, new ItemId(messageId));
            Folder folder = getFolder(targetFolder);
            email.copy(folder.getId());
            return messageConverter.convertToEmailMessage(email);

        } catch (Exception e) {
            throw new EwsException("Could not copy message", e);
        }
    }

    public EmailMessage moveMessage(String emailId, String targetFolder) {
        try {
            microsoft.exchange.webservices.data.core.service.item.EmailMessage email =
                    microsoft.exchange.webservices.data.core.service.item.EmailMessage.bind(service, new ItemId(emailId));
            Folder folder = getFolder(targetFolder);
            email.move(folder.getId());
            return messageConverter.convertToEmailMessage(email);

        } catch (Exception e) {
            throw new EwsException("Could not move message", e);
        }
    }

    @Override
    public EmailMessage moveMessage(EmailMessage message, String targetFolder) {
        return moveMessage(message.getId().orElse(null), targetFolder);
    }

    @Override
    public List<EmailMessage> fetchAllMessages(String folderName) {
        return fetchAllMessages(folderName, Integer.MAX_VALUE);
    }

    public List<EmailMessage> fetchAllMessages(String folderName, int batchSize) {

        if (this.bindPropSet.getCount() == 0) {
            this.bindPropSet = new PropertySet(BasePropertySet.FirstClassProperties);
            this.bindPropSet.setRequestedBodyType(BodyType.HTML);
        }
        return fetchMessages(folderName, this.bindPropSet, null, false, batchSize);
    }

    public List<EmailMessage> fetchMessages(String folderName, PropertySet bindPropSet, SearchFilter searchFilter, boolean markAsRead) {
        return fetchMessages(folderName, bindPropSet, searchFilter, markAsRead, Integer.MAX_VALUE);
    }

    public List<EmailMessage> fetchMessages(String folderName, PropertySet bindPropSet, SearchFilter searchFilter, boolean markAsRead, int batchSize) {
        try {
            ItemView itemView = new ItemView(batchSize);
            itemView.getOrderBy().add(ItemSchema.DateTimeReceived, SortDirection.Ascending);

            Folder folder = getFolder(folderName);

            FindItemsResults<Item> itemsResults;
            if (searchFilter == null) {
                itemsResults = service.findItems(folder.getId(), itemView);
            } else {
                itemsResults = service.findItems(folder.getId(), searchFilter, itemView);
            }
            List<Item> items = itemsResults.getItems();
            return items.stream().map(i -> {
                try {
                    if (markAsRead) {
                        markEmailAsRead((microsoft.exchange.webservices.data.core.service.item.EmailMessage) i);
                    }
                    i.load(bindPropSet);
                } catch (Exception e) {
                    throw new EwsException("Could not mark email as read", e);
                }
                return this.messageConverter.convertToEmailMessage((microsoft.exchange.webservices.data.core.service.item.EmailMessage) i);
            }).collect(Collectors.toList());

        } catch (Exception e) {
            throw new EwsException("Could not fetch messages", e);
        }
    }

    @Override
    public List<EmailMessage> fetchUnreadMessages(String folderName, boolean markRead) {
        return fetchUnreadMessages(folderName, markRead, Integer.MAX_VALUE);
    }

    public List<EmailMessage> fetchUnreadMessages(String folderName, boolean markRead, int batchSize) {
        if (this.bindPropSet.getCount() == 0) {
            this.bindPropSet = new PropertySet(BasePropertySet.FirstClassProperties);
            this.bindPropSet.setRequestedBodyType(BodyType.HTML);
        }
        SearchFilter sf = new SearchFilter.SearchFilterCollection(LogicalOperator.And, new SearchFilter.IsEqualTo(EmailMessageSchema.IsRead, false));
        return fetchMessages(folderName, this.bindPropSet, sf, markRead, batchSize);
    }

    @Override
    public List<String> fetchFolderList() {
        FolderView view = new FolderView(Integer.MAX_VALUE);
        view.setTraversal(FolderTraversal.Deep);
        FindFoldersResults findFoldersResults;
        try {
            findFoldersResults = service.findFolders(WellKnownFolderName.Root, view);
            return findFoldersResults.getFolders().stream().map(f -> {
                try {
                    return f.getDisplayName();
                } catch (ServiceLocalException e) {
                    return null;
                }
            }).collect(Collectors.toList());

        } catch (Exception e) {
            throw new EwsException("Could not fetch folder list", e);
        }
    }

    @Override
    public String getDefaultInboxFolder() {
        try {
            return getInboxFolder().getDisplayName();
        } catch (ServiceLocalException e) {
            throw new EwsException("Could not get default Inbox folder", e);
        }
    }

    @Override
    public void setDefaultInboxFolder(String defaultInboxFolder) {
        throw new UnsupportedOperationException("Not supported for Exchange service");
    }

    public Folder getInboxFolder() {
        try {
            return Folder.bind(this.service, WellKnownFolderName.Inbox);
        } catch (Exception e) {
            throw new EwsException("Could not get Inbox folder", e);
        }
    }

    public Folder getFolder(String folderName) {
        return getFolder(folderName, false, null, Integer.MAX_VALUE);
    }

    public Folder getFolder(String folderName, boolean createIfNotExist, FolderId parentFolderId) {
        return getFolder(folderName, createIfNotExist, parentFolderId, Integer.MAX_VALUE);
    }

    public Folder getFolder(String folderName, boolean createIfNotExist, FolderId parentFolderId, int batchSize) {
        SearchFilter searchFilterByName = new SearchFilter.IsEqualTo(FolderSchema.DisplayName, folderName);
        FolderView view = new FolderView(batchSize);
        view.setTraversal(FolderTraversal.Deep);
        FindFoldersResults findFoldersResults;
        try {
            findFoldersResults = service.findFolders(WellKnownFolderName.Root, searchFilterByName, view);
        } catch (Exception e) {
            throw new EwsException("Couldn't find folder " + folderName, e);
        }
        List<Folder> folderList = findFoldersResults.getFolders();

        if (!folderList.isEmpty()) {
            return folderList.get(0);
        } else {
            if (createIfNotExist && parentFolderId != null) {
                return createFolder(folderName, parentFolderId);
            } else {
                throw new EwsException("Couldn't find folder " + folderName);
            }

        }
    }

    public ExchangeService getService() {
        return service;
    }

    public void setService(ExchangeService service) {
        this.service = service;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public String getDomain() {
        return domain;
    }

    public void setDomain(String domain) {
        this.domain = domain;
    }

    public ExchangeVersion getExchangeVersion() {
        return exchangeVersion;
    }

    public void setExchangeVersion(ExchangeVersion exchangeVersion) {
        this.exchangeVersion = exchangeVersion;
    }

    public void setCredentials(WebCredentials credentials) {
        this.credentials = credentials;
    }

    public PropertySet getEmailPropertySet() {
        return bindPropSet;
    }

    public void setEmailPropertySet(PropertySet bindPropSet) {
        this.bindPropSet = bindPropSet;
    }

    public void addEmailPropertySet(PropertyDefinitionBase property) throws Exception {
        this.bindPropSet.add(property);
    }

    @Override
    public void sendEmail(EmailMessage message) {
        Item item = messageConverter.createNativeMessage(message);

        if (item instanceof microsoft.exchange.webservices.data.core.service.item.EmailMessage) {
            microsoft.exchange.webservices.data.core.service.item.EmailMessage msg =
                    (microsoft.exchange.webservices.data.core.service.item.EmailMessage) item;

            try {
                msg.sendAndSaveCopy();
            } catch (Exception ex) {
                throw new EwsException("Could not sent email", ex);
            }
        } else {
            throw new EwsException("Could not create email");
        }
    }

    @Override
    public void sendEmail(final String from, final String to, final String subject, final String body) {
        EmailMessage message = EmailMessage.EmailMessageBuilder
                .newMessage(EmailAddress.of(from), EmailAddress.of(to))
                .text(body)
                .subject(subject)
                .build();

        sendEmail(message);
    }

    @Override
    public void sendEmail(String from, final List<String> to, final String subject, final String body) {
        List<EmailAddress> toEmailAddresses = to.stream().map(EmailAddress::of).collect(Collectors.toList());
        EmailMessage message = EmailMessage.EmailMessageBuilder
                .newMessage(EmailAddress.of(from), toEmailAddresses)
                .text(body)
                .subject(subject)
                .build();

        sendEmail(message);
    }

    public Folder getFolderById(FolderId id) {
        try {
            return Folder.bind(service, id);
        } catch (Exception e) {
            throw new EwsException("Could not get folder", e);
        }
    }

    public EmailMessage getByEmailId(ItemId itemId) {
        try {
            microsoft.exchange.webservices.data.core.service.item.EmailMessage emailMessage =
                    microsoft.exchange.webservices.data.core.service.item.EmailMessage.bind(service, itemId);
            return messageConverter.convertToEmailMessage(emailMessage);
        } catch (Exception e) {
            throw new EwsException("Could not get email by id", e);
        }
    }

    public List<EmailMessage> searchEmailWithFilter(String folderName, SearchFilter filter) {
        return searchEmailWithFilter(folderName, filter, Integer.MAX_VALUE);
    }

    public List<EmailMessage> searchEmailWithFilter(String folderName, SearchFilter filter, int batchSize) {
        ItemView itemView = new ItemView(batchSize);
        itemView.setTraversal(ItemTraversal.Shallow);
        itemView.setPropertySet(new PropertySet(BasePropertySet.FirstClassProperties));
        try {
            itemView.getOrderBy().add(ItemSchema.DateTimeReceived, SortDirection.Descending);
        } catch (ServiceLocalException e) {
            return null;
        }
        FindItemsResults<Item> findItemsResults = null;
        Folder folder = getFolder(folderName);
        if (folder == null) {
            throw new EwsException("Could not find folder");
        }

        try {
            findItemsResults = service.findItems(folder.getId(), filter, itemView);
        } catch (Exception e) {
            return null;
        }

        return findItemsResults.getItems()
                .stream()
                .map(e -> this.messageConverter.convertToEmailMessage((microsoft.exchange.webservices.data.core.service.item.EmailMessage) e))
                .collect(Collectors.toList());
    }

    private void markEmailAsRead(microsoft.exchange.webservices.data.core.service.item.EmailMessage emailMessage) throws Exception {
        emailMessage.setIsRead(true);
        emailMessage.update(ConflictResolutionMode.AlwaysOverwrite);
    }

    public boolean markEmailAsRead(EmailMessage emailMessage) {
        String messageId = emailMessage.getId().orElseThrow(() -> new EwsException("Id is not set for EmailMessage"));
        try {
            microsoft.exchange.webservices.data.core.service.item.EmailMessage message =
                    microsoft.exchange.webservices.data.core.service.item.EmailMessage.bind(service, ItemId.getItemIdFromString(messageId));
            markEmailAsRead(message);
            return true;
        } catch (Exception e) {
            throw new EwsException("Could not mark email as read", e);
        }
    }

    public String getImportance(EmailMessage emailMessage) {
        String messageId = emailMessage.getId().orElseThrow(() -> new EwsException("Id is not set for EmailMessage"));
        try {
            microsoft.exchange.webservices.data.core.service.item.EmailMessage message =
                    microsoft.exchange.webservices.data.core.service.item.EmailMessage.bind(service, ItemId.getItemIdFromString(messageId));
            return message.getImportance().toString();
        } catch (Exception e) {
            throw new EwsException("Could not get the importance of message: " + messageId, e);
        }
    }

    public String getInternetMessageId(EmailMessage emailMessage) {
        String messageId = emailMessage.getId().orElseThrow(() -> new EwsException("Id is not set for EmailMessage"));
        try {
            microsoft.exchange.webservices.data.core.service.item.EmailMessage message =
                    microsoft.exchange.webservices.data.core.service.item.EmailMessage.bind(service, ItemId.getItemIdFromString(messageId));
            return message.getInternetMessageId();
        } catch (Exception e) {
            throw new EwsException("Could not get the internet message id of message: " + messageId, e);
        }
    }
}
