package ru.yandex.wmconsole.notifier.handler;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ru.yandex.wmconsole.data.NotificationTypeEnum;
import ru.yandex.wmconsole.service.HostInfoService;
import ru.yandex.wmconsole.service.NotificationService;
import ru.yandex.wmconsole.service.UsersHostsService;
import ru.yandex.wmconsole.service.VirusNotificationService;
import ru.yandex.wmconsole.util.WwwUtil;
import ru.yandex.wmtools.common.SupportedProtocols;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.error.InternalProblem;

/**
 * @author Andrey Mima (amima@yandex-team.ru)
 */
public class VirusNotificationHandler implements Handler {
    private static final Logger log = LoggerFactory.getLogger(VirusNotificationHandler.class);

    private static final String TAG_HOST = "host";
    private static final String TAG_EMAIL = "email";
    private static final String ATTRIBUTE_NAME = "name";

    private class VirusHost {
        private final String name;
        private final List<String> emails = new LinkedList<String>();

        public VirusHost(String name) {
            this.name = name;
        }

        public void addEmail(String email) {
            emails.add(email);
        }

        public String getName() {
            return name;
        }

        public List<String> getEmails() {
            return emails;
        }
    }

    private NotificationService notificationService;
    private VirusNotificationService virusNotificationService;
    private UsersHostsService usersHostsService;
    private HostInfoService hostInfoService;
    private Collection<String> publicHosts;

    public VirusNotificationHandler() throws InternalException {
        InputStream ldListStream = getClass().getResourceAsStream("/data/2ld.list");
        BufferedReader reader = new BufferedReader(new InputStreamReader(ldListStream));
        publicHosts = new HashSet<String>();
        try {
            try {
                int count = 0;
                while (reader.ready()) {
                    String line = reader.readLine().trim();
                    if (line.length() != 0) {
                        if (publicHosts.add(line)) {
                            count++;
                        }
                    }
                }
                log.debug("VirusNotificationHandler: " + count + " publicHosts loaded");
            } finally {
                reader.close();
            }
        } catch (FileNotFoundException e) {
            throw new InternalException(InternalProblem.PROCESSING_ERROR, "FileNotFoundException", e);
        } catch (IOException e) {
            throw new InternalException(InternalProblem.PROCESSING_ERROR, "IOException", e);
        }
    }

    public void setNotificationService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void setVirusNotificationService(VirusNotificationService virusNotificationService) {
        this.virusNotificationService = virusNotificationService;
    }

    public void setUsersHostsService(UsersHostsService usersHostsService) {
        this.usersHostsService = usersHostsService;
    }

    public void setHostInfoService(HostInfoService hostInfoService) {
        this.hostInfoService = hostInfoService;
    }

    @Override
    @SuppressWarnings("unchecked")
    public void handleNotification(String xmlData) {
        log.debug("Virus notifications received externally");
        List<VirusHost> virusList = handleXML(xmlData);
        try {
            if ((virusList == null) || (virusList.size() == 0)) {
                log.error(TAG_HOST + " is null or empty (possible corrupt xml)");
                return;
            }

            for (VirusHost virusHost : virusList) {
                try {
                    URL url = SupportedProtocols.getURL(virusHost.getName());
                    String hostName = SupportedProtocols.getCanonicalHostname(url);

                    //We have www.test.ru and test.ru at input, so we want to notify only one of them.
                    //In our DB there can bo only one version of host: eighter www.test.ru or test.ru.
                    //So we ignore main mirror, so that one of www.test.ru or test.ru will be null.
                    Long hostId = getHostIdHavingUsers(hostName, true);

                    boolean mainMirrorInsteadOfHost = false;

                    //There is a situation when neighter www.test.ru or test.ru is in our DB,
                    //but there is a main mirror for test.ru, like home.test.ru.
                    //So if test.ru and www.test.ru cannot be found in DB, we try to find mirror
                    //of test.ru (it is acceptable only if mirror is not www.test.ru)
                    String[] levels = hostName.split("\\.");
                    if (hostId == null && !WwwUtil.isWWW(hostName)) {
                        hostId = getHostIdHavingUsers(WwwUtil.switchWWW(hostName), true);
                        if (hostId != null) {
                            log.warn("Host with name " + hostName + " has main mirror www." + hostName +
                                    " that is expected to be processed in another virus request");
                            continue;
                        } else {
                            // Получаем host id главного зеркала
                            hostId = getHostIdHavingUsers(hostName, false);
                            mainMirrorInsteadOfHost = (hostId != null);
                        }
                    }

                    if (hostId == null) {
                        log.warn("Host with name " + hostName + " was not found in database");
                        notifyParentDomain(virusHost, new Date(), hostName);
                    } else {
                        // In order to distinct notification for a main mirror from
                        // notifications for the host, VIRUS_FOR_MIRROR message is sent (see WMCON-3448)
                        final NotificationTypeEnum notificationType = mainMirrorInsteadOfHost ?
                                NotificationTypeEnum.VIRUS_FOR_MIRROR : NotificationTypeEnum.VIRUS;
                        final List<Long> userIds = getHostUsers(hostId);

                        addNotificationsForHost(virusHost, hostName, hostId, userIds, new Date(), notificationType);
                    }
                } catch (InternalException e) {
                    log.error("InternalException in " + getClass().getName() + " " +
                            "while inserting notification(s)", e);
                } catch (MalformedURLException e) {
                    log.error("MalformedURLException in " + getClass().getName() + " " +
                            "while inserting notification(s)", e);
                } catch (URISyntaxException e) {
                    log.error("URISyntaxException in " + getClass().getName() + " " +
                            "while inserting notification(s)", e);
                } catch (SupportedProtocols.UnsupportedProtocolException e) {
                    log.error("SupportedProtocols.UnsupportedProtocolException in " + getClass().getName() + " " +
                            "while inserting notification(s)", e);
                }
            }
        } catch (ClassCastException e) {
            log.error("ClassCastException in " + getClass().getName() + " " +
                    "while extracting notification data (possible corrupt xml)", e);
        }
    }

    private void addNotificationsForHost(VirusHost virusHost, String hostName, Long hostId, List<Long> userIds, Date date, NotificationTypeEnum type) throws InternalException {
        if (!userIds.isEmpty()) {
            Long issueId = fillTables(hostId, hostName, null, date);
            notificationService.insertNotificationForUsers(type, issueId, userIds, date);
        } else {
            for (String email : virusHost.getEmails()) {
                fillTables(null, hostName, email, date);
            }
        }
    }

    @Override
    public void internalHandle(String... params) {
        throw new UnsupportedOperationException("Internal handle not supported for Virus notification");
    }

    @Override
    public void handleInternalNotification(Map<String, String> params) {
        throw new UnsupportedOperationException("Internal handle not supported for Virus notification");
    }

    private Long fillTables(Long hostId, String name, String email, Date date) throws InternalException {
        return virusNotificationService.addVirusNotification(hostId, name, email, date);
    }

    private Long getHostId(String hostName, boolean ignoreMainMirror) throws InternalException {
        return hostInfoService.getHostIdByHostName(hostName, ignoreMainMirror);
    }

    private List<Long> getHostUsers(Long hostId) throws InternalException {
        if (hostId == null) {
            return Collections.emptyList();
        }
        return usersHostsService.getHostVerifiedUserIds(hostId);
    }

    private Long getHostIdHavingUsers(final String hostName, final boolean ignoreMainMirror) throws InternalException {
        Long hostId = getHostId(hostName, ignoreMainMirror);
        if (hostId == null) {
            return null;
        }
        return (getHostUsers(hostId).size() > 0) ? hostId : null;
    }

    @SuppressWarnings("unchecked")
    private List<VirusHost> handleXML(String xmlData) {
        List<VirusHost> virusHostList = new ArrayList<VirusHost>();
        StringReader stringReader = new StringReader(xmlData);
        SAXBuilder builder = new SAXBuilder();
        try {
            Document eventDocument = builder.build(stringReader);
            Element rootElement = eventDocument.getRootElement();
            List<Element> hostElementList = rootElement.getChildren(TAG_HOST);
            for (Element hostElement : hostElementList) {
                VirusHost virusHost = new VirusHost(hostElement.getAttributeValue(ATTRIBUTE_NAME));
                List<Element> emailElementList = hostElement.getChildren(TAG_EMAIL);
                for (Element emailElement : emailElementList) {
                    virusHost.addEmail(emailElement.getText());
                }
                virusHostList.add(virusHost);
            }
            log.debug("Virus host list size is " + virusHostList.size());
        } catch (JDOMException e) {
            log.error("JDOMException in " + getClass().getName() + " " + "while extracting xml data", e);
            throw new IllegalArgumentException("Invalid xml notification data format");
        } catch (IOException e) {
            log.error("IOException in " + getClass().getName() + " " + "while reading xml data", e);
            throw new IllegalArgumentException("Invalid xml notification data format");
        } catch (NullPointerException e) {
            log.warn("Invalid xml notification data format", e);
            throw new IllegalArgumentException("Invalid xml notification data format");
        }

        return virusHostList;
    }

    /**
     * Returns nearest parent domain that we can find in our database
     * @param virusHost host info
     * @param date receive date
     * @param hostName input host name
     * @throws ru.yandex.wmtools.common.error.InternalException DB errors
     */
    private void notifyParentDomain(VirusHost virusHost, Date date, String hostName) throws InternalException {
        if (WwwUtil.isWWW(hostName)) {
            return;
        }

        final String[] levs = hostName.split("\\.");
        final List<String> levels = new LinkedList<String>(Arrays.asList(levs));
        levels.remove(0);
        String upLevelHostName = getUpLevelHostName(levels);

        while (levels.size() >= 2 && upLevelHostName != null && !publicHosts.contains(upLevelHostName)) {
            Long upLevelHostId = getHostId(upLevelHostName, true);
            if (getHostUsers(upLevelHostId).size() == 0) {
                upLevelHostId = null;
            }
            if (upLevelHostId != null) {
                addNotificationsForHost(virusHost, hostName, upLevelHostId, getHostUsers(upLevelHostId), date, NotificationTypeEnum.VIRUS_DOMAIN);
                return;
            }

            upLevelHostName = "www." + upLevelHostName;
            upLevelHostId = getHostId(upLevelHostName, true);
            if (getHostUsers(upLevelHostId).size() == 0) {
                upLevelHostId = null;
            }
            if (upLevelHostId != null) {
                addNotificationsForHost(virusHost, hostName, upLevelHostId, getHostUsers(upLevelHostId), date, NotificationTypeEnum.VIRUS_DOMAIN);
                return;
            }

            levels.remove(0);
            upLevelHostName = getUpLevelHostName(levels);
        }
    }

    private String getUpLevelHostName(List<String> levels) {
        StringBuilder builder = new StringBuilder();
        String separator = "";
        for (String level: levels) {
            builder.append(separator);
            builder.append(level);
            separator = ".";
        }
        return builder.toString();
    }
}
