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

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;

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

import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageEntry;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiMessageSeverity;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiModelFieldContainer;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiModelSource;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiProviderConfigurationState;
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.AiModelCatalogPort;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogRequest;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.ModelCatalogResult;
import javafx.application.Platform;

/**
 * Coordinates asynchronous model catalogue retrieval for the GUI provider section.
 * <p>
 * This coordinator is responsible for:
 * <ul>
 *   <li>Triggering a background HTTP call via {@link AiModelCatalogPort} on a dedicated
 *       daemon thread named {@code gui-model-catalog}.</li>
 *   <li>Returning the result to the JavaFX Application Thread via {@code Platform.runLater}.</li>
 *   <li>Updating the per-provider {@link GuiModelFieldContainer} to show either a
 *       non-editable {@code ComboBox} (success) or a manual text field (all other cases).</li>
 *   <li>Appending a {@link GuiMessageEntry} to the supplied pending-messages list for each
 *       completed retrieval attempt, so later GUI layers can display the result.</li>
 * </ul>
 * <p>
 * Parallele Abrufanfragen (z.&nbsp;B. durch schnellen Provider-Wechsel oder mehrfaches Klicken
 * auf „Modelle neu laden") werden durch einen Generationszähler entschärft: Jede neue Anfrage
 * erhöht den Zähler. Wenn das Ergebnis eines Hintergrund-Threads auf dem JavaFX-Thread
 * verarbeitet wird, prüft der Coordinator, ob die Generationsnummer noch aktuell ist. Veraltete
 * Ergebnisse (aus einer früheren Anfrage) werden verworfen, sodass stets nur das Ergebnis der
 * jüngsten Anfrage in die Meldungsliste und die Feldcontainer geschrieben wird.
 * <p>
 * The worker thread factory is injectable so tests can supply a synchronous or latch-guarded
 * executor without spinning a real OS thread.
 * <p>
 * This class is not thread-safe by itself. All methods intended to mutate GUI state must be
 * called on the JavaFX Application Thread. Background threads only interact through
 * {@code Platform.runLater}.
 */
public final class GuiModelCatalogCoordinator {
    private static final String LOG_MODEL_FETCH_FMT = "GUI-Modellabruf: {} (Provider: {})";
    private static final String OPERATION_MODELLABRUF = "Modellabruf";



    private static final Logger LOG = LogManager.getLogger(GuiModelCatalogCoordinator.class);

    /** Default timeout used when no timeout is configured in the provider state. */
    static final int DEFAULT_TIMEOUT_SECONDS = 10;

    private final AiModelCatalogPort modelCatalogPort;
    private final List<GuiMessageEntry> pendingMessages;

    /**
     * Factory for the background worker thread. Package-private to allow test substitution.
     * The default creates a daemon thread named {@code gui-model-catalog}.
     */
    Function<Runnable, Thread> modelCatalogThreadFactory;

    /** Per-provider field containers; populated by the workspace when it builds provider blocks. */
    private final Map<AiProviderFamily, GuiModelFieldContainer> fieldContainers =
            new ConcurrentHashMap<>();

    /**
     * Generationszähler zur Erkennung veralteter Abruf-Ergebnisse.
     * Wird bei jeder neuen Anfrage in {@link #triggerModelRetrieval} atomar erhöht.
     * Hintergrund-Threads erfassen die Generation beim Start; auf dem JavaFX-Thread wird
     * das Ergebnis verworfen, wenn die gespeicherte Generation nicht mehr aktuell ist.
     */
    private final AtomicLong retrievalGeneration = new AtomicLong(0);

    /**
     * Consumer that delivers the retrieval result. In production this wraps the call in
     * {@code Platform.runLater}. In tests it can be replaced with a direct call so the result
     * is applied immediately on the worker thread without needing an FX queue drain.
     * Package-private to allow test substitution.
     */
    java.util.function.Consumer<Runnable> resultDelivery = Platform::runLater;

    /**
     * Optional callback invoked on the JavaFX Application Thread after each retrieval result has
     * been applied. The workspace uses this hook to refresh the central message area and field-error
     * labels without coupling the coordinator to the workspace implementation.
     * Package-private to allow substitution in tests.
     */
    Runnable postResultCallback = () -> { };

    /**
     * Creates a coordinator backed by the given catalogue port and shared message list.
     *
     * @param modelCatalogPort port used for background HTTP calls; must not be {@code null}
     * @param pendingMessages  mutable list to append result messages to; must not be {@code null}
     */
    public GuiModelCatalogCoordinator(AiModelCatalogPort modelCatalogPort,
                                      List<GuiMessageEntry> pendingMessages) {
        this.modelCatalogPort = Objects.requireNonNull(modelCatalogPort,
                "modelCatalogPort must not be null");
        this.pendingMessages = Objects.requireNonNull(pendingMessages,
                "pendingMessages must not be null");
        this.modelCatalogThreadFactory = task -> {
            Thread t = new Thread(task, "gui-model-catalog");
            t.setDaemon(true);
            return t;
        };
    }

    /**
     * Registers a {@link GuiModelFieldContainer} for the given provider family.
     * <p>
     * Must be called on the JavaFX Application Thread before the first retrieval is triggered.
     *
     * @param family    the provider family this container belongs to; must not be {@code null}
     * @param container the container to register; must not be {@code null}
     */
    public void registerFieldContainer(AiProviderFamily family, GuiModelFieldContainer container) {
        Objects.requireNonNull(family, "family must not be null");
        Objects.requireNonNull(container, "container must not be null");
        fieldContainers.put(family, container);
    }

    /**
     * Triggers an asynchronous model catalogue retrieval for the given provider family.
     * <p>
     * The retrieval is performed on a background worker thread. The result is delivered back
     * to the JavaFX Application Thread via {@code Platform.runLater}. The registered
     * {@link GuiModelFieldContainer} for the provider is updated accordingly, and a
     * {@link GuiMessageEntry} is appended to the pending-messages list.
     * <p>
     * If no field container is registered for the provider, the call is a no-op.
     * <p>
     * Must be called on the JavaFX Application Thread.
     *
     * @param family        the provider family to retrieve models for; must not be {@code null}
     * @param providerState the current editor state for the provider; must not be {@code null}
     */
    public void triggerModelRetrieval(AiProviderFamily family,
                                      GuiProviderConfigurationState providerState) {
        Objects.requireNonNull(family, "family must not be null");
        Objects.requireNonNull(providerState, "providerState must not be null");

        GuiModelFieldContainer container = fieldContainers.get(family);
        if (container == null) {
            LOG.debug("GUI-Modellabruf: Kein Feld-Container für Provider '{}' registriert – übersprungen.",
                    family.getIdentifier());
            return;
        }

        // Capture the current manual value before starting the background call.
        String previousManualValue = container.currentModelValue();

        // Build the request from the current editor state.
        ModelCatalogRequest request = buildRequest(family, providerState);

        // Generationsnummer erhöhen – laufende Hintergrund-Threads mit einer älteren
        // Generationsnummer verwerfen ihr Ergebnis, sobald sie auf dem FX-Thread ankommen.
        long currentGeneration = retrievalGeneration.incrementAndGet();

        LOG.info("GUI-Modellabruf: Modelllistenabruf für Provider '{}' gestartet (Generation {}).",
                family.getIdentifier(), currentGeneration);

        Runnable task = () -> {
            ModelCatalogResult result = modelCatalogPort.fetchAvailableModels(request);
            resultDelivery.accept(() -> {
                // Veraltetes Ergebnis verwerfen, wenn inzwischen eine neuere Anfrage gestartet wurde.
                if (retrievalGeneration.get() != currentGeneration) {
                    LOG.debug("GUI-Modellabruf: Ergebnis für Provider '{}' verworfen"
                            + " (Generation {} ist nicht mehr aktuell).",
                            family.getIdentifier(), currentGeneration);
                    return;
                }
                applyResult(family, container, result, previousManualValue);
                postResultCallback.run();
            });
        };

        Thread worker = modelCatalogThreadFactory.apply(task);
        worker.start();
    }

    /**
     * Applies the result of a completed model catalogue retrieval to the field container and
     * appends a message entry to the pending-messages list.
     * <p>
     * Must only be called on the JavaFX Application Thread (via {@code Platform.runLater}).
     *
     * @param family              the provider family that was queried; must not be {@code null}
     * @param container           the field container to update; must not be {@code null}
     * @param result              the retrieval result; must not be {@code null}
     * @param previousManualValue the model value that was in the text field before the call
     */
    private void applyResult(AiProviderFamily family,
                             GuiModelFieldContainer container,
                             ModelCatalogResult result,
                             String previousManualValue) {
        // Remove any previous message entries from an earlier retrieval so messages do not
        // accumulate across repeated triggers of the same retrieval action.
        pendingMessages.removeIf(msg -> OPERATION_MODELLABRUF.equals(msg.source().orElse("")));

        String displayName = displayNameFor(family);

        switch (result) {
            case ModelCatalogResult.Success success -> {
                List<String> models = success.models();
                container.applyModelList(models, previousManualValue);
                String message = "Modellliste für " + displayName + " geladen ("
                        + models.size() + " " + (models.size() == 1 ? "Eintrag" : "Einträge") + ").";
                pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.INFO, message, OPERATION_MODELLABRUF));
                LOG.info(LOG_MODEL_FETCH_FMT, message, family.getIdentifier());
            }
            case ModelCatalogResult.EmptyList emptyList -> {
                container.applyManualFallback(GuiModelSource.LIST_UNAVAILABLE_MANUAL_INPUT);
                String message = "Provider " + displayName
                        + " liefert aktuell keine Modelle. Manuelle Eingabe aktiv.";
                pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.HINT, message, OPERATION_MODELLABRUF));
                LOG.warn(LOG_MODEL_FETCH_FMT, message, family.getIdentifier());
            }
            case ModelCatalogResult.IncompleteConfiguration incomplete -> {
                container.applyManualFallback(GuiModelSource.LIST_UNAVAILABLE_MANUAL_INPUT);
                String message = "Modellliste nicht abrufbar: " + incomplete.missingReason()
                        + ". Manuelle Eingabe aktiv.";
                pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.WARNING, message, OPERATION_MODELLABRUF));
                LOG.warn(LOG_MODEL_FETCH_FMT, message, family.getIdentifier());
            }
            case ModelCatalogResult.TechnicalFailure failure -> {
                container.applyManualFallback(GuiModelSource.LIST_FAILED_MANUAL_INPUT);
                String message = "Modellliste nicht abrufbar (" + failure.errorCategory()
                        + "). Manuelle Eingabe aktiv.";
                pendingMessages.add(GuiMessageEntry.of(GuiMessageSeverity.ERROR, message, OPERATION_MODELLABRUF));
                LOG.warn("GUI-Modellabruf: {} Detail: {} (Provider: {})",
                        message, failure.errorDetail(), family.getIdentifier());
            }
        }
    }

    /**
     * Builds a {@link ModelCatalogRequest} from the current provider editor state.
     * <p>
     * Missing or blank values are passed as {@code Optional.empty()} so the adapter can apply
     * its own defaults or return {@link ModelCatalogResult.IncompleteConfiguration} if required
     * values are absent.
     *
     * @param family        the target provider family; must not be {@code null}
     * @param providerState the current provider editor state; must not be {@code null}
     * @return a new request; never {@code null}
     */
    private static ModelCatalogRequest buildRequest(AiProviderFamily family,
                                                    GuiProviderConfigurationState providerState) {
        Optional<String> baseUrl = Optional.ofNullable(providerState.baseUrl())
                .filter(s -> !s.isBlank());
        Optional<String> apiKey = Optional.ofNullable(providerState.apiKey())
                .map(keyState -> keyState.propertyValue())
                .filter(s -> !s.isBlank());

        int timeout = DEFAULT_TIMEOUT_SECONDS;
        String timeoutStr = providerState.timeoutSeconds();
        if (timeoutStr != null && !timeoutStr.isBlank()) {
            try {
                int parsed = Integer.parseInt(timeoutStr.trim());
                if (parsed > 0) {
                    timeout = parsed;
                }
            } catch (NumberFormatException ignored) {
                // Use default.
            }
        }

        return new ModelCatalogRequest(family.getIdentifier(), baseUrl, apiKey, timeout);
    }

    /**
     * Returns a human-readable display name for the given provider family.
     *
     * @param family the provider family; must not be {@code null}
     * @return the display name; never {@code null}
     */
    private static String displayNameFor(AiProviderFamily family) {
        return switch (family) {
            case CLAUDE -> "Claude";
            case OPENAI_COMPATIBLE -> "OpenAI-kompatibel";
        };
    }

    /**
     * Returns an unmodifiable snapshot of the pending messages collected so far.
     * <p>
     * This method is intended for tests that need to inspect the message list after
     * a retrieval completes.
     *
     * @return unmodifiable list of pending messages; never {@code null}
     */
    public List<GuiMessageEntry> pendingMessagesSnapshot() {
        return List.copyOf(pendingMessages);
    }
}
