package io.openems.backend.alerting;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;
import org.osgi.service.event.propertytypes.EventTopics;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;

import io.openems.backend.alerting.handler.OfflineEdgeHandler;
import io.openems.backend.alerting.handler.SumStateHandler;
import io.openems.backend.alerting.scheduler.Scheduler;
import io.openems.backend.common.component.AbstractOpenemsBackendComponent;
import io.openems.backend.common.debugcycle.DebugLoggable;
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.utils.JsonUtils;
import io.openems.common.utils.ThreadPoolUtils;

@Designate(ocd = Config.class, factory = false)
@Component(//
		name = "Alerting", //
		configurationPolicy = ConfigurationPolicy.REQUIRE, //
		immediate = true //
)
@EventTopics({ //
		Edge.Events.ON_SET_ONLINE, //
		Edge.Events.ON_SET_SUM_STATE, //
		Metadata.Events.AFTER_IS_INITIALIZED //
})
public class Alerting extends AbstractOpenemsBackendComponent implements EventHandler, DebugLoggable {

	public static final String COMPONENT_ID = "alerting0";
	public static final String METRIC_MESSAGES_SENT = "AlertingMessagesSent";
	public static final String METRIC_MESSAGES_QUEUE = "AlertingMessagesQueue";

	// Maximum number of messages constructed at the same time
	private static final byte THREAD_POOL_SIZE = 4;
	// Queue size from which warnings are issued
	private static final byte THREAD_QUEUE_WARNING_THRESHOLD = 50;

	private static ThreadPoolExecutor createDefaultExecutorService() {
		final var threadFactory = new ThreadFactoryBuilder()
				.setNameFormat(Alerting.class.getSimpleName() + ".EventHandler-%d").build();
		return (ThreadPoolExecutor) Executors.newFixedThreadPool(THREAD_POOL_SIZE, threadFactory);
	}

	private final Logger log = AbstractOpenemsBackendComponent.getComponentLogger(this);
	private final ThreadPoolExecutor executor;

	@Reference
	protected Metadata metadata;

	@Reference
	protected Mailer mailer;

	private final Scheduler scheduler;

	protected final List<Handler<?>> handler = new ArrayList<>(2);

	protected Alerting(Scheduler scheduler, ThreadPoolExecutor executor) {
		super("Alerting");
		this.executor = executor;
		this.scheduler = scheduler;
	}

	public Alerting() {
		this(new Scheduler(), Alerting.createDefaultExecutorService());
	}

	@Activate
	protected void activate(Config config) {
		this.log.info("Activate");
		this.scheduler.start();

		if (config.notifyOnOffline()) {
			this.handler.add(new OfflineEdgeHandler(this.scheduler, this.scheduler, this.mailer, this.metadata, //
					config.initialDelay()));
		}

		if (config.notifyOnSumStateChange()) {
			this.handler.add(new SumStateHandler(this.scheduler, this.scheduler, this.mailer, this.metadata));
		}
	}

	@Deactivate
	protected void deactivate() {
		this.log.info("Deactivate");
		this.handler.forEach(Handler::stop);
		this.handler.clear();
		this.scheduler.stop();
	}

	@Override
	public void handleEvent(Event event) {
		final var reader = new EventReader(event);
		for (var h : this.handler) {
			final var task = h.getEventHandler(reader.getTopic());
			if (task != null) {
				this.execute(task, reader);
			}
		}
	}

	private void execute(Consumer<EventReader> consumer, EventReader reader) {
		this.executor.execute(() -> consumer.accept(reader));
	}

	@Override
	public String debugLog() {
		final var sb = new StringBuilder("[Alerting] ");

		final int queueSize = this.executor.getQueue().size();
		if (queueSize >= THREAD_QUEUE_WARNING_THRESHOLD) {
			sb.append("%d tasks in the EventHandlerQueue! ".formatted(queueSize));
		}
		final String handlerStr = this.handler.stream().map(Handler::debugLog).collect(Collectors.joining(", "));
		sb.append(handlerStr);

		if (sb.isEmpty()) {
			return null;
		}
		return sb.toString();
	}

	@Override
	public Map<String, JsonElement> debugMetrics() {
		final var map = new HashMap<String, JsonElement>();

		final var poolMetrics = ThreadPoolUtils.debugMetrics(this.executor);
		for (var entry : poolMetrics.entrySet()) {
			map.put("%s/%s".formatted(COMPONENT_ID, entry.getKey()), new JsonPrimitive(entry.getValue()));
		}

		for (var h : this.handler) {
			final var id = h.id();
			final var metrics = h.getMetrics();
			map.put("%s/%s".formatted(id, METRIC_MESSAGES_SENT), JsonUtils.toJson(metrics.messagesSent()));
			map.put("%s/%s".formatted(id, METRIC_MESSAGES_QUEUE), JsonUtils.toJson(metrics.messagesQueue()));
		}

		return map;
	}

}
