package io.openems.backend.alerting.handler;

import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.LinkedList;
import java.util.List;
import java.util.StringJoiner;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.openems.backend.alerting.Handler;
import io.openems.backend.alerting.HandlerMetrics;
import io.openems.backend.alerting.message.OfflineEdgeMessage;
import io.openems.backend.alerting.scheduler.MessageScheduler;
import io.openems.backend.alerting.scheduler.MessageSchedulerService;
import io.openems.backend.alerting.scheduler.TimedExecutor;
import io.openems.backend.alerting.scheduler.TimedExecutor.TimedTask;
import io.openems.backend.common.alerting.OfflineEdgeAlertingSetting;
import io.openems.backend.common.mail.Mailer;
import io.openems.backend.common.metadata.Edge;
import io.openems.backend.common.metadata.Metadata;
import io.openems.common.event.EventReader;
import io.openems.common.exceptions.OpenemsException;

public class OfflineEdgeHandler implements Handler<OfflineEdgeMessage> {

	// Definition of unrealistically high values for messages sent simultaneously
	public static final int MAX_SIMULTANEOUS_MSGS = 500;
	public static final int MAX_SIMULTANEOUS_EDGES = 1000;
	public static final int EDGE_REBOOT_MINUTES = 5;

	private final Logger log = LoggerFactory.getLogger(OfflineEdgeHandler.class);

	private final int initialDelay; // in Minutes
	private final Metadata metadata;
	private final Mailer mailer;

	private MessageSchedulerService mss;
	private MessageScheduler<OfflineEdgeMessage> msgScheduler;

	private TimedTask initMetadata;
	private final TimedExecutor timeService;

	private final AtomicLong messagesSent = new AtomicLong();

	public OfflineEdgeHandler(MessageSchedulerService mss, TimedExecutor timeService, Mailer mailer, Metadata metadata,
			int initialDelay) {
		this.mailer = mailer;
		this.metadata = metadata;
		this.initialDelay = initialDelay;
		this.timeService = timeService;

		this.mss = mss;
		this.msgScheduler = mss.register(this);
		if (this.metadata.isInitialized()) {
			this.handleMetadataAfterInitialize(null);
		}
	}

	@Override
	public String id() {
		return "alerting_offline";
	}

	@Override
	public void stop() {
		this.timeService.cancel(this.initMetadata);
		this.initMetadata = null;
		this.mss.unregister(this);
		this.msgScheduler = null;
		this.mss = null;
	}

	private void reschedule(List<OfflineEdgeMessage> pack) {
		if (this.log.isDebugEnabled()) {
			final var logStr = new StringJoiner(", ", "Sent OfflineEdgeMsg: ", "");
			pack.forEach(msg -> {
				logStr.add(msg.toString());
				this.tryReschedule(msg);
			});
			this.log.debug(logStr.toString());
		} else {
			pack.forEach(this::tryReschedule);
		}
	}

	@Override
	public void send(ZonedDateTime sentAt, List<OfflineEdgeMessage> pack) {
		// Ensure Edge is still offline before sending mail.
		pack.removeIf(msg -> !this.isEdgeOffline(msg.getEdgeId()));
		if (pack.isEmpty()) {
			this.log.debug("No OfflineEdgeMessages to send after filtering for offline edges. Skip sending mail.");
			return;
		}

		final var params = pack.stream().map(OfflineEdgeMessage::getContext).toList();
		
		this.mailer.sendMail(sentAt, OfflineEdgeMessage.TEMPLATE, params) //
				.whenComplete((sentMessages, error) -> {
					if (error == null) {
						this.messagesSent.getAndAdd(sentMessages);
					} else {
						this.log.error("Failed to send OfflineEdgeMessage: {}", error.getMessage());
						this.log.debug("Failed to send OfflineEdgeMessage for: {}", params, error);
					}
				});
		this.reschedule(pack);
	}

	private void tryReschedule(OfflineEdgeMessage msg) {
		if (msg.update()) {
			this.msgScheduler.schedule(msg);
		}
	}

	private boolean isEdgeOffline(String edgeId) {
		final var edge = this.metadata.getEdge(edgeId);
		return edge.map(Edge::isOffline).orElse(false);
	}

	private void checkMetadata() {
		this.log.info("[OfflineEdgeHandler] check Metadata for Offline Edges");

		final var msgs = new LinkedList<OfflineEdgeMessage>();
		final var count = new AtomicInteger();
		final var validOfflineEges = this.metadata.getAllOfflineEdges().stream() //
				.filter(this::isValidEdge) //
				.toList();

		if (validOfflineEges.size() > OfflineEdgeHandler.MAX_SIMULTANEOUS_EDGES) {
			this.log.error(
					"[OfflineEdgeHandler] Canceled checkMetadata(); tried to schedule msgs for {} Offline-Edges at once!!",
					OfflineEdgeHandler.MAX_SIMULTANEOUS_EDGES);
			return;
		}

		for (var edge : validOfflineEges) {
			final var msg = this.getEdgeMessage(edge);
			if (msg == null) {
				continue;
			}

			final var completeCnt = count.addAndGet(msg.getMessageCount());
			if (completeCnt > OfflineEdgeHandler.MAX_SIMULTANEOUS_MSGS) {
				this.log.error(
						"[OfflineEdgeHandler] Canceled checkMetadata(); tried to schedule over {} EdgeOffline Messages at once!!",
						OfflineEdgeHandler.MAX_SIMULTANEOUS_MSGS);
				return;
			}

			msgs.add(msg);
		}

		msgs.forEach(this.msgScheduler::schedule);
	}

	/**
	 * Check if Edge is valid for Scheduling.
	 *
	 * @param edge to test
	 * @return true if valid
	 */
	private boolean isValidEdge(Edge edge) {
		final var invalid = edge.getLastmessage() == null // was never online
				|| edge.getLastmessage() //
						.isBefore(this.timeService.now().minusWeeks(1)); // already offline for a week
		return !invalid;
	}

	/**
	 * Add Edge to list, with calculated TimeStamp (at which to notify).
	 *
	 * @param edge to add
	 * @return {@link OfflineEdgeMessage} generated from edge
	 */
	protected OfflineEdgeMessage getEdgeMessage(Edge edge) {
		if (edge == null || edge.getId() == null) {
			this.log.warn("Called method getEdgeMessage with {}", (edge == null ? "Edge{null}" : "Edge{id=null}"));
			return null;
		}
		try {
			final var alertingSettings = this.metadata.getEdgeOfflineAlertingSettings(edge.getId());
			if (alertingSettings == null || alertingSettings.isEmpty()) {
				return null;
			}
			final var message = new OfflineEdgeMessage(edge.getId(), edge.getLastmessage());
			for (var setting : alertingSettings) {
				if (setting.delay() > 0 && this.shouldReceiveMail(edge, setting)) {
					message.addRecipient(setting);
				}
			}
			if (!message.isEmpty()) {
				return message;
			}
		} catch (OpenemsException e) {
			this.log.warn("Could not get alerting settings for {}", edge.getId(), e);
		}
		return null;
	}

	private boolean shouldReceiveMail(Edge edge, OfflineEdgeAlertingSetting setting) {
		final var lastMailRecievedAt = setting.lastNotification();
		final var edgeOfflineSince = edge.getLastmessage();

		var hasNotRecievedMailYet = true;
		final var neverRecievedAnyMail = lastMailRecievedAt == null;

		if (!neverRecievedAnyMail) {
			final var nextMailRecieveAt = edgeOfflineSince.plus(setting.delay(), ChronoUnit.MINUTES);
			hasNotRecievedMailYet = nextMailRecieveAt.isAfter(lastMailRecievedAt);
		}

		return neverRecievedAnyMail || hasNotRecievedMailYet;
	}

	protected void tryRemoveEdge(Edge edge) {
		this.msgScheduler.remove(edge.getId());
	}

	protected void tryAddEdge(Edge edge) {
		if (!this.isValidEdge(edge)) {
			return;
		}
		final var msg = this.getEdgeMessage(edge);
		if (msg != null && this.msgScheduler != null) {
			this.msgScheduler.schedule(msg);
		}
	}

	/**
	 * Check Metadata for all OfflineEdges. Waits given in initialDelay, before
	 * executing.
	 *
	 * @param event Event data
	 */
	private void handleMetadataAfterInitialize(EventReader event) {
		if (this.initialDelay <= 0) {
			this.checkMetadata();
		} else {
			final var executeAt = this.timeService.now().plusMinutes(OfflineEdgeHandler.this.initialDelay);
			this.initMetadata = this.timeService.schedule(executeAt, now -> this.checkMetadata());
		}
	}

	private void handleOnSetOnline(EventReader event) {
		final var edgeId = event.getString(Edge.Events.OnSetOnline.EDGE_ID);
		final var isOnline = event.getBoolean(Edge.Events.OnSetOnline.IS_ONLINE);

		final var edgeOpt = this.metadata.getEdge(edgeId);
		if (edgeOpt.isPresent()) {
			final var edge = edgeOpt.get();
			/* Ensure that the online-state has not changed */
			if (edge.isOnline() == isOnline) {
				if (isOnline) {
					this.tryRemoveEdge(edge);
				} else {
					this.timeService.schedule(this.timeService.now().plusMinutes(EDGE_REBOOT_MINUTES), t -> {
						if (edge.isOffline()) {
							this.tryAddEdge(edge);
						}
					});
				}
			}
		} else {
			this.log.warn("Edge with id: {} not found", edgeId);
		}
	}

	@Override
	public Consumer<EventReader> getEventHandler(String eventTopic) {
		return switch (eventTopic) {
		case Edge.Events.ON_SET_ONLINE:
			yield this::handleOnSetOnline;

		case Metadata.Events.AFTER_IS_INITIALIZED:
			yield this::handleMetadataAfterInitialize;

		default:
			yield null;
		};
	}

	@Override
	public Class<OfflineEdgeMessage> getGeneric() {
		return OfflineEdgeMessage.class;
	}

	@Override
	public HandlerMetrics getMetrics() {
		return new HandlerMetrics(this.messagesSent.get(), this.msgScheduler.size());
	}
	
	private boolean waitforInit() {
		return this.initMetadata != null && !this.initMetadata.isDone();
	}
	
	@Override
	public String debugLog() {
		if (this.waitforInit()) {
			return "OfflineEdgeHandler{MessagesSent: %d, MessagesQueue: %d, !initial wait!}" //
					.formatted(this.messagesSent.get(), this.msgScheduler.size());
		}
		return "OfflineEdgeHandler{MessagesSent: %d, MessagesQueue: %d}" //
				.formatted(this.messagesSent.get(), this.msgScheduler.size());
	}
}
