package ru.yandex.webmaster.periodic.host;

import java.util.List;

import com.codahale.metrics.Counter;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import org.joda.time.DateTime;
import org.joda.time.Hours;
import org.joda.time.Minutes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;

import ru.yandex.common.scheduler.ExecutionContext;
import ru.yandex.misc.db.q.SqlCondition;
import ru.yandex.misc.db.q.SqlLimits;
import ru.yandex.misc.db.q.SqlOrder;
import ru.yandex.webmaster.common.host.HostEvent;
import ru.yandex.webmaster.common.host.HostEventType;
import ru.yandex.webmaster.common.host.dao.TblHostEventConditions;
import ru.yandex.webmaster.common.host.dao.TblHostEventDao;
import ru.yandex.webmaster.periodic.host.dao.TblHostEventTaskStateCondition;
import ru.yandex.webmaster.periodic.host.dao.TblHostEventTaskStateDao;
import ru.yandex.wmconsole.data.partition.WMCPartition;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.error.InternalProblem;
import ru.yandex.wmtools.common.error.UserException;
import ru.yandex.wmtools.common.util.scheduler.timetable.AbstractLockableTaskExecutor;

/**
 * @author aherman
 */
public abstract class AbstractHostEventTask extends AbstractLockableTaskExecutor {
    private static final Logger log = LoggerFactory.getLogger(AbstractHostEventTask.class);
    private static final String LOCK_NAME_PREFIX = "host_event_task.";

    private TblHostEventTaskStateDao tblHostEventTaskStateDao;
    private TblHostEventDao tblHostEventDao;
    private MetricRegistry metricRegistry;

    private Hours retryAfterHours = Hours.ONE;
    private int retryBatchSize = 128;
    private Minutes autoreleasAfter = Minutes.minutes(60);

    @Override
    public String runWithRELogging(ExecutionContext context) throws InternalException {
        log.info("Start host event task: " + getTaskType());

        final String lockName = LOCK_NAME_PREFIX + getTaskType().name();
        if (!getLock(WMCPartition.nullPartition(), lockName, autoreleasAfter.getMinutes())) {
            String message = "Failed to get lock for task: " + getTaskType();
            log.warn(message);
            return message;
        }

        int retryEventsCount = 0;
        int newEventsCount = 0;

        try {
            log.info("Retry old events...");
            try {
                retryEventsCount = retryEvents();
                log.info("Events retried: " + retryEventsCount);
            } catch (InternalException e) {
                log.error("Error while retrying events", e);
            }

            log.info("Process new events...");
            newEventsCount = processNewEvents();
            log.info("New events processed: " + newEventsCount);

            Meter processedEventsMeter = metricRegistry.meter(getMetricNamePrefix() + ".events.processed");
            processedEventsMeter.mark(newEventsCount);
        } finally {
            releaseLock(WMCPartition.nullPartition(), lockName);
        }
        return String.format("Events processed: retry=%d new=%d", retryEventsCount, newEventsCount);
    }

    private int retryEvents() throws InternalException {
        int retryEventsCount = 0;
        List<LastEventId> eventsToRetry = tblHostEventTaskStateDao.getEvents(
                getTaskType(),
                SqlCondition.all(
                        TblHostEventTaskStateCondition.state(LastEventState.RETRY),
                        TblHostEventTaskStateCondition.dateOlderThan(DateTime.now().minus(retryAfterHours)),
                        TblHostEventTaskStateCondition.retryCountGreaterThan(0)
                ),
                SqlLimits.first(retryBatchSize),
                SqlOrder.unordered()
        );

        for (LastEventId previousEventId : eventsToRetry) {
            LastEventId eventId = decreaseRetryCount(previousEventId);
            try {
                tblHostEventTaskStateDao.replaceEvent(previousEventId, eventId);
                if (eventId.getRetryCount() <= 0) {
                    log.error("Unable to retry event: " + eventId);
                    continue;
                }

                HostEvent event = tblHostEventDao.getFirst(TblHostEventConditions.eventId(eventId.getEventId()));
                process(event);
                retryEventsCount++;
                tblHostEventTaskStateDao.remove(eventId);
            } catch (Exception e) {
                log.error("Unable to retry event", e);
            }
        }

        return retryEventsCount;
    }

    private int processNewEvents() throws InternalException {
        int newEventsCount = 0;

        List<LastEventId> latestProcessedEvents = tblHostEventTaskStateDao.getEvents(
                getTaskType(),
                TblHostEventTaskStateCondition.state(LastEventState.LAST_PROCESSED_EVENT),
                SqlLimits.first(1),
                TblHostEventTaskStateDao.orderByEventIdDesc()
        );

        SqlCondition eventsCondition = TblHostEventConditions.type(getEventType());
        LastEventId previousEventId = null;
        if (!latestProcessedEvents.isEmpty()) {
            previousEventId = latestProcessedEvents.get(0);
            eventsCondition = eventsCondition.and(
                    TblHostEventConditions.eventIdGreaterThan(previousEventId.getEventId())
            );
        }

        int newValue = tblHostEventDao.countEvents(eventsCondition);
        Counter unprocessedEventsMeter = metricRegistry.counter(getMetricNamePrefix() + ".queue.size");
        long oldValue = unprocessedEventsMeter.getCount();
        unprocessedEventsMeter.inc(newValue - oldValue);

        List<HostEvent> events = tblHostEventDao
                .getEvents(eventsCondition, SqlLimits.first(getBatchSize()), TblHostEventDao.ORDER_BY_EVENT_ID);
        for (HostEvent event : events) {
            LastEventId currentEventId = toEventId(event, LastEventState.LAST_PROCESSED_EVENT);
            LastEventId retryEventId = decreaseRetryCount(currentEventId);
            try {
                tblHostEventTaskStateDao.replaceEvent(previousEventId, currentEventId);
            } catch (Exception e) {
                throw new InternalException(InternalProblem.PROCESSING_ERROR, "Unable to advance last event id", e);
            }
            try {
                tblHostEventTaskStateDao.replaceEvent(retryEventId);
                process(event);
                newEventsCount++;
                tblHostEventTaskStateDao.remove(retryEventId);
            } catch (Exception e) {
                log.error("Unable to process event", e);
            }
            previousEventId = currentEventId;
        }

        return newEventsCount;
    }

    private String getMetricNamePrefix() {
        return "task." + this.getClass().getSimpleName();
    }

    private LastEventId toEventId(HostEvent event, LastEventState state) {
        return new LastEventId(getTaskType(), event.getEventId(), 0, state, DateTime.now());
    }

    protected abstract void process(HostEvent hostEvent) throws InternalException, UserException;

    protected LastEventId decreaseRetryCount(LastEventId lastEventId) {
        int retryCount;
        if (lastEventId.getState() != LastEventState.RETRY) {
            retryCount = getRetryCount();
        } else {
            retryCount = lastEventId.getRetryCount() - 1;
        }
        return new LastEventId(lastEventId.getTaskType(), lastEventId.getEventId(), retryCount, LastEventState.RETRY,
                DateTime.now());
    }

    protected abstract int getRetryCount();
    protected abstract HostEventTaskType getTaskType();
    protected abstract HostEventType getEventType();
    protected abstract int getBatchSize();

    @Required
    public void setTblHostEventTaskStateDao(TblHostEventTaskStateDao tblHostEventTaskStateDao) {
        this.tblHostEventTaskStateDao = tblHostEventTaskStateDao;
    }

    @Required
    public void setTblHostEventDao(TblHostEventDao tblHostEventDao) {
        this.tblHostEventDao = tblHostEventDao;
    }

    @Required
    public void setMetricRegistry(MetricRegistry metricRegistry) {
        this.metricRegistry = metricRegistry;
    }
}
