package de.gecheckt.pdf.umbenenner.application.usecase;

import java.time.Instant;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

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

import de.gecheckt.pdf.umbenenner.application.dto.ModelPriceChangeSet;
import de.gecheckt.pdf.umbenenner.application.dto.ModelPriceEntry;
import de.gecheckt.pdf.umbenenner.application.dto.ModelPriceKey;
import de.gecheckt.pdf.umbenenner.application.dto.ModelPriceView;
import de.gecheckt.pdf.umbenenner.application.port.out.ClockPort;
import de.gecheckt.pdf.umbenenner.application.port.out.ModelPriceRepository;

/**
 * Use Case zur Verwaltung persistierter Modell-Preise.
 *
 * <p>Bietet eine schmale CRUD-Fassade ueber das {@link ModelPriceRepository}
 * inklusive der ChangeSet-Konfliktvalidierung und einer Whitelist-Pruefung
 * auf bekannte Provider beim Upsert. Loeschungen sind auch fuer unbekannte
 * Provider erlaubt, damit verwaiste Eintraege entfernt werden koennen.
 *
 * <p>Der {@link ClockPort} liefert den {@code updatedAt}-Wert fuer alle
 * Upserts, so da&szlig; die Schreiblogik testbar bleibt.
 *
 * <p>Konfliktvalidierungsregeln (alle vier sind in dieser Reihenfolge wirksam):
 * <ol>
 *   <li>Doppelte (provider, modelName)-Schluessel innerhalb der Upsert-Liste werden abgewiesen.</li>
 *   <li>Doppelte (provider, modelName)-Schluessel innerhalb der Deletions-Liste werden abgewiesen.</li>
 *   <li>Ein Schluessel darf nicht zugleich in {@code upserts} und {@code deletions} stehen.</li>
 *   <li>Beim Upsert ist nur die V3.3-Provider-Whitelist erlaubt
 *       ({@code openai-compatible}, {@code claude}).</li>
 * </ol>
 *
 * <p>Ein leeres ChangeSet erzeugt keinen Repository-Aufruf, sondern wird
 * als No-op mit INFO-Log behandelt.
 */
public class DefaultManageModelPricesUseCase {

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

    /**
     * Whitelist der in V3.3 fachlich unterstuetzten Provider.
     *
     * <p>Wird beim Upsert geprueft. Loeschungen umgehen die Whitelist
     * absichtlich, damit verwaiste Eintraege entfernbar bleiben.
     */
    public static final Set<String> SUPPORTED_PROVIDERS = Set.of("openai-compatible", "claude");

    private final ModelPriceRepository repository;
    private final ClockPort clockPort;

    /**
     * Erzeugt den Use Case mit allen erforderlichen Ports.
     *
     * @param repository Repository-Port; nicht {@code null}
     * @param clockPort  Clock-Port; nicht {@code null}
     */
    public DefaultManageModelPricesUseCase(ModelPriceRepository repository, ClockPort clockPort) {
        this.repository = Objects.requireNonNull(repository, "repository");
        this.clockPort = Objects.requireNonNull(clockPort, "clockPort");
    }

    /**
     * Liefert alle persistierten Modell-Preise.
     *
     * @return Liste aller Eintraege; nie {@code null}
     */
    public List<ModelPriceView> findAll() {
        return repository.findAll();
    }

    /**
     * Liefert den Preis-Eintrag zu einem Composite-Key.
     *
     * @param provider  Provider-Identifikator
     * @param modelName Modellname
     * @return {@link Optional} mit Eintrag, leer wenn nicht vorhanden
     */
    public Optional<ModelPriceView> findByProviderAndModelName(String provider, String modelName) {
        return repository.findByProviderAndModelName(provider, modelName);
    }

    /**
     * Validiert das ChangeSet und persistiert es transaktional.
     *
     * <p>Bei einem leeren ChangeSet wird kein Repository-Aufruf ausgefuehrt;
     * die Methode kehrt direkt zurueck. Bei Konfliktverletzungen oder
     * Whitelist-Verstoessen wird eine {@link ModelPriceValidationException}
     * mit deutscher Meldung geworfen, ohne dass eine Transaktion gestartet
     * wird.
     *
     * <p>Vor der Repository-Weitergabe werden die Upsert-Eintraege auf den
     * Clock-Zeitpunkt der aktuellen Aktion umgeschrieben (gleicher
     * Zeitstempel fuer alle Eintraege eines ChangeSets).
     *
     * @param changeSet zu speicherndes ChangeSet; nicht {@code null}
     * @throws ModelPriceValidationException bei Konfliktverstoessen oder unbekanntem Provider beim Upsert
     */
    public void saveAllChanges(ModelPriceChangeSet changeSet) {
        Objects.requireNonNull(changeSet, "changeSet");

        if (changeSet.isEmpty()) {
            LOG.info("Modell-Preis-ChangeSet ist leer – kein Schreibvorgang durchgefuehrt");
            return;
        }

        validateNoDuplicateUpsertKeys(changeSet.upserts());
        validateNoDuplicateDeletionKeys(changeSet.deletions());
        validateNoCrossOverlap(changeSet.upserts(), changeSet.deletions());
        validateUpsertProviders(changeSet.upserts());

        Instant now = clockPort.now();
        ModelPriceChangeSet stamped = stampUpdatedAt(changeSet, now);

        try {
            repository.saveAllChanges(stamped);
            LOG.info("Modell-Preis-Batch persistiert: {} Upserts, {} Deletions",
                    stamped.upserts().size(), stamped.deletions().size());
        } catch (RuntimeException ex) {
            LOG.error("Modell-Preis-Batch fehlgeschlagen, Rollback ausgefuehrt: {}", ex.getMessage());
            throw ex;
        }
    }

    /**
     * Erstellt eine Kopie des ChangeSets, in der jeder Upsert-Eintrag den
     * uebergebenen Zeitpunkt als {@code updatedAt} traegt.
     *
     * @param changeSet Original-ChangeSet
     * @param now       neuer Zeitstempel
     * @return neues ChangeSet mit identischen Daten und einheitlichem {@code updatedAt}
     */
    private ModelPriceChangeSet stampUpdatedAt(ModelPriceChangeSet changeSet, Instant now) {
        List<ModelPriceEntry> stampedUpserts = changeSet.upserts().stream()
                .map(entry -> new ModelPriceEntry(
                        entry.provider(),
                        entry.modelName(),
                        entry.priceInputPerTokenNanoUsd(),
                        entry.priceOutputPerTokenNanoUsd(),
                        entry.currency(),
                        now))
                .toList();
        return new ModelPriceChangeSet(stampedUpserts, changeSet.deletions());
    }

    private void validateNoDuplicateUpsertKeys(List<ModelPriceEntry> upserts) {
        Set<String> seen = new HashSet<>();
        for (ModelPriceEntry entry : upserts) {
            String key = entry.provider() + "|" + entry.modelName();
            if (!seen.add(key)) {
                throw new ModelPriceValidationException(
                        "ChangeSet enthaelt doppelten Upsert fuer Provider \""
                                + entry.provider() + "\" und Modell \"" + entry.modelName() + "\"");
            }
        }
    }

    private void validateNoDuplicateDeletionKeys(List<ModelPriceKey> deletions) {
        Set<String> seen = new HashSet<>();
        for (ModelPriceKey key : deletions) {
            String composite = key.provider() + "|" + key.modelName();
            if (!seen.add(composite)) {
                throw new ModelPriceValidationException(
                        "ChangeSet enthaelt doppelte Loeschung fuer Provider \""
                                + key.provider() + "\" und Modell \"" + key.modelName() + "\"");
            }
        }
    }

    private void validateNoCrossOverlap(List<ModelPriceEntry> upserts, List<ModelPriceKey> deletions) {
        Set<String> upsertKeys = new HashSet<>();
        for (ModelPriceEntry entry : upserts) {
            upsertKeys.add(entry.provider() + "|" + entry.modelName());
        }
        for (ModelPriceKey key : deletions) {
            String composite = key.provider() + "|" + key.modelName();
            if (upsertKeys.contains(composite)) {
                throw new ModelPriceValidationException(
                        "ChangeSet enthaelt Schluessel sowohl in Upserts als auch in Deletions: Provider \""
                                + key.provider() + "\", Modell \"" + key.modelName() + "\"");
            }
        }
    }

    private void validateUpsertProviders(List<ModelPriceEntry> upserts) {
        for (ModelPriceEntry entry : upserts) {
            if (!SUPPORTED_PROVIDERS.contains(entry.provider())) {
                throw new ModelPriceValidationException(
                        "Unbekannter Provider beim Upsert: \"" + entry.provider()
                                + "\". Zulaessig sind: " + SUPPORTED_PROVIDERS);
            }
        }
    }
}
