package de.gecheckt.pdf.umbenenner.adapter.in.scheduler;

import de.gecheckt.pdf.umbenenner.application.port.out.BatchRunTrigger;
import de.gecheckt.pdf.umbenenner.application.port.out.BatchRunTriggerResult;
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerConfig;
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerPort;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

/**
 * Implementiert {@link SchedulerPort} auf Basis eines
 * {@link ScheduledExecutorService} mit
 * {@link ScheduledExecutorService#scheduleWithFixedDelay}.
 * <p>
 * Der erste Tick startet sofort (Initial Delay 0). Nachfolgende Ticks starten
 * {@link SchedulerConfig#intervalSeconds()} Sekunden nach dem Ende des
 * vorherigen Ticks. Der Verarbeitungsaufruf erfolgt synchron im
 * Scheduler-Thread; der aufrufende Tick-Zyklus wartet also auf den Abschluss
 * des Laufs, bevor der nächste Tick geplant wird.
 * <p>
 * Der Adapter delegiert ausschließlich an den injizierten {@link BatchRunTrigger}
 * und trifft keine eigenen fachlichen Entscheidungen. Ergebnisse werden über
 * den injizierten {@code Consumer<BatchRunTriggerResult>} zurückgemeldet.
 * <p>
 * Alle Ausnahmen innerhalb eines Ticks werden abgefangen und geloggt, damit
 * der {@link ScheduledExecutorService} den Tick-Zyklus nicht still abbricht.
 * <p>
 * Instanzen dieser Klasse sind für den Einsatz in einem einzigen Steuerungs-Thread
 * ausgelegt. {@link #startScheduler} und {@link #stopScheduler} müssen serialisiert
 * aufgerufen werden.
 */
public class ScheduledExecutorServiceSchedulerAdapter implements SchedulerPort {

    private static final Logger logger =
            LogManager.getLogger(ScheduledExecutorServiceSchedulerAdapter.class);

    private static final String SCHEDULER_THREAD_NAME = "pdf-umbenenner-scheduler";

    private final Consumer<BatchRunTriggerResult> resultConsumer;

    /**
     * Hält den aktuell aktiven {@link BatchRunTrigger}. Package-private,
     * damit Tests {@code onTick()} isoliert prüfen können, ohne den
     * gesamten Lifecycle zu durchlaufen.
     */
    final AtomicReference<BatchRunTrigger> currentTrigger = new AtomicReference<>();

    private final AtomicReference<ScheduledExecutorService> executor = new AtomicReference<>();

    /**
     * Erstellt einen neuen Adapter.
     *
     * @param resultConsumer Empfänger für Tick-Ergebnisse; darf nicht {@code null} sein
     */
    public ScheduledExecutorServiceSchedulerAdapter(Consumer<BatchRunTriggerResult> resultConsumer) {
        this.resultConsumer = Objects.requireNonNull(resultConsumer,
                "resultConsumer darf nicht null sein");
    }

    // -------------------------------------------------------------------------
    // SchedulerPort
    // -------------------------------------------------------------------------

    /**
     * Startet den periodischen Scheduler-Mechanismus.
     * <p>
     * Ist der Scheduler bereits aktiv, hat dieser Aufruf keine Wirkung (idempotent).
     * Andernfalls wird ein Single-Thread-{@link ScheduledExecutorService} angelegt
     * und mit {@code scheduleWithFixedDelay} und Initial-Delay 0 gestartet.
     * Der erzeugte Thread heißt {@value SCHEDULER_THREAD_NAME} und ist kein Daemon-Thread.
     *
     * @param config  Betriebskonfiguration; insbesondere das Intervall zwischen den Ticks
     * @param trigger Auslöser, der bei jedem Tick synchron aufgerufen wird
     */
    @Override
    public void startScheduler(SchedulerConfig config, BatchRunTrigger trigger) {
        Objects.requireNonNull(config, "config darf nicht null sein");
        Objects.requireNonNull(trigger, "trigger darf nicht null sein");
        if (executor.get() != null) {
            logger.debug("Scheduler ist bereits aktiv – Start-Aufruf wird ignoriert.");
            return;
        }
        currentTrigger.set(trigger);
        ThreadFactory threadFactory = runnable -> {
            Thread t = new Thread(runnable, SCHEDULER_THREAD_NAME);
            t.setDaemon(false);
            t.setUncaughtExceptionHandler((thread, ex) ->
                    logger.error("Unbehandelte Ausnahme im Scheduler-Thread '{}'.",
                            thread.getName(), ex));
            return t;
        };
        ScheduledExecutorService newExecutor =
                Executors.newSingleThreadScheduledExecutor(threadFactory);
        newExecutor.scheduleWithFixedDelay(
                this::onTick,
                0L,
                config.intervalSeconds(),
                TimeUnit.SECONDS);
        executor.set(newExecutor);
        logger.info("Scheduler gestartet. Intervall: {} Sekunden.", config.intervalSeconds());
    }

    /**
     * Stoppt den periodischen Scheduler-Mechanismus.
     * <p>
     * Laufende Ticks werden nicht abgebrochen; es werden lediglich keine weiteren
     * Ticks geplant. Ist der Scheduler bereits gestoppt, hat dieser Aufruf keine
     * Wirkung (idempotent).
     */
    @Override
    public void stopScheduler() {
        ScheduledExecutorService localExecutor = executor.getAndSet(null);
        if (localExecutor == null) {
            logger.debug("Scheduler ist bereits gestoppt – Stop-Aufruf wird ignoriert.");
            return;
        }
        currentTrigger.set(null);
        localExecutor.shutdown();
        logger.info("Scheduler angehalten.");
    }

    // -------------------------------------------------------------------------
    // Tick-Logik (package-private für Testbarkeit)
    // -------------------------------------------------------------------------

    /**
     * Führt einen Verarbeitungstick aus.
     * <p>
     * Holt den aktuellen {@link BatchRunTrigger}, ruft ihn synchron auf und
     * leitet das Ergebnis an den {@link Consumer} weiter. Ist kein Trigger
     * gesetzt, wird der Tick übersprungen. Alle {@link Exception}en werden
     * abgefangen und auf ERROR geloggt, damit der
     * {@link ScheduledExecutorService} den Tick-Zyklus nicht still abbricht.
     * <p>
     * Package-private, damit Unit-Tests diese Methode direkt aufrufen können.
     */
    void onTick() {
        BatchRunTrigger trigger = currentTrigger.get();
        if (trigger == null) {
            logger.warn("Scheduler-Tick ausgelöst, aber kein aktiver Trigger vorhanden. "
                    + "Tick wird übersprungen.");
            return;
        }
        try {
            BatchRunTriggerResult result = trigger.triggerRun();
            resultConsumer.accept(result);
        } catch (Exception e) {
            logger.error("Unbehandelte Ausnahme während des Scheduler-Ticks. "
                    + "Der nächste Tick wird planmäßig ausgelöst.", e);
        }
    }
}
