package de.gecheckt.pdf.umbenenner.adapter.out.prompt;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Objects;
import java.util.UUID;

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

import de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingFailure;
import de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingResult;
import de.gecheckt.pdf.umbenenner.application.port.out.PromptLoadingSuccess;
import de.gecheckt.pdf.umbenenner.application.port.out.PromptPort;
import de.gecheckt.pdf.umbenenner.application.port.out.PromptSaveResult;
import de.gecheckt.pdf.umbenenner.domain.model.PromptIdentifier;

/**
 * Dateisystembasierte Implementierung von {@link PromptPort}.
 * <p>
 * Lädt Prompt-Templates aus einer externen Datei auf dem Datenträger und leitet einen
 * stabilen Identifikator aus dem Dateinamen ab. Stellt sicher, dass leere oder technisch
 * unbrauchbare Prompts abgelehnt werden.
 * <p>
 * <strong>Identifikatorableitung:</strong>
 * Der stabile Identifikator wird aus dem Dateinamen der Prompt-Datei abgeleitet.
 * Eine Prompt-Datei namens {@code "prompt_de_v2.txt"} erhält den Identifikator
 * {@code "prompt_de_v2.txt"}.
 * <p>
 * <strong>Inhaltsprüfung:</strong>
 * Nach dem Laden wird der Inhalt getrimmt und auf Leerheit geprüft. Ein leerer Prompt
 * (oder einer, der nur Leerzeichen enthält) gilt als technisch unbrauchbar und führt zu
 * {@link PromptLoadingFailure}.
 * <p>
 * <strong>Atomares Speichern:</strong>
 * {@link #savePrompt(String)} schreibt zunächst in eine temporäre Datei <em>im selben
 * Verzeichnis</em> wie die Zieldatei und verschiebt diese danach atomar via
 * {@code ATOMIC_MOVE}. Bei einem Fehler beim atomaren Verschieben wird kein stiller
 * Fallback auf nicht-atomares Schreiben durchgeführt.
 * <p>
 * <strong>Fehlerbehandlung:</strong>
 * Alle technischen Fehler (Datei nicht gefunden, I/O-Fehler, fehlende Berechtigungen)
 * werden abgefangen und als strukturierte Ergebnistypen zurückgegeben – keine Exceptions
 * werden propagiert.
 */
public class FilesystemPromptPortAdapter implements PromptPort {
    private static final String SAVE_FAILED_LOG_MSG = "Prompt speichern fehlgeschlagen: {}";


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

    private final Path promptFilePath;

    /**
     * Erstellt den Adapter mit dem konfigurierten Pfad zur Prompt-Datei.
     *
     * @param promptFilePath Pfad zur Prompt-Template-Datei; darf nicht {@code null} sein
     * @throws NullPointerException wenn {@code promptFilePath} null ist
     */
    public FilesystemPromptPortAdapter(Path promptFilePath) {
        this.promptFilePath = Objects.requireNonNull(promptFilePath, "promptFilePath must not be null");
    }

    /**
     * Lädt das konfigurierte Prompt-Template aus der Datei.
     *
     * @return {@link PromptLoadingResult} mit dem geladenen Inhalt oder einem klassifizierten Fehler;
     *         nie {@code null}
     */
    @Override
    public PromptLoadingResult loadPrompt() {
        try {
            if (!Files.exists(promptFilePath)) {
                return new PromptLoadingFailure(
                        "FILE_NOT_FOUND",
                        "Prompt file not found at: " + promptFilePath);
            }

            String content = Files.readString(promptFilePath, StandardCharsets.UTF_8);
            String trimmedContent = content.trim();

            if (trimmedContent.isEmpty()) {
                return new PromptLoadingFailure(
                        "EMPTY_CONTENT",
                        "Prompt file is empty or contains only whitespace: " + promptFilePath);
            }

            PromptIdentifier identifier = deriveIdentifier();
            LOG.debug("Prompt erfolgreich geladen von {}", promptFilePath);
            return new PromptLoadingSuccess(identifier, trimmedContent);

        } catch (IOException e) {
            LOG.error("Fehler beim Laden der Prompt-Datei: {}", promptFilePath, e);
            return new PromptLoadingFailure(
                    "IO_ERROR",
                    "Failed to read prompt file: " + e.getMessage());
        }
    }

    /**
     * Speichert den übergebenen Inhalt atomar in die konfigurierte Prompt-Datei.
     * <p>
     * Der Ablauf:
     * <ol>
     *   <li>Prüfen, ob der Zielordner existiert.</li>
     *   <li>Temporäre Datei im selben Verzeichnis wie die Zieldatei anlegen.</li>
     *   <li>Inhalt in UTF-8 in die temporäre Datei schreiben.</li>
     *   <li>Temporäre Datei via {@code ATOMIC_MOVE} zur Zieldatei verschieben.</li>
     *   <li>Bei Fehler: temporäre Datei aufräumen, Fehler als Ergebnis zurückgeben.</li>
     * </ol>
     * <p>
     * Zeilenenden werden unverändert übernommen. Es findet keine Normalisierung statt.
     *
     * @param content der zu speichernde Inhalt; darf nicht {@code null} sein
     * @return {@link PromptSaveResult} mit Erfolg oder klassifiziertem Fehler; nie {@code null}
     * @throws NullPointerException wenn {@code content} null ist
     */
    @Override
    public PromptSaveResult savePrompt(String content) {
        Objects.requireNonNull(content, "content must not be null");

        Path targetDir = promptFilePath.getParent();
        if (targetDir == null || !Files.isDirectory(targetDir)) {
            String message = "Zielordner der Prompt-Datei existiert nicht: "
                    + (targetDir != null ? targetDir.toAbsolutePath() : "unbekannt");
            LOG.warn(SAVE_FAILED_LOG_MSG, message);
            return new PromptSaveResult.TargetDirectoryMissing(message);
        }

        // Temporäre Datei im selben Verzeichnis wie die Zieldatei anlegen
        // (nicht im System-Temp – ATOMIC_MOVE funktioniert nicht zuverlässig über Dateisystem-Grenzen)
        Path tempFile = targetDir.resolve(".prompt-tmp-" + UUID.randomUUID() + ".tmp");

        try {
            Files.write(tempFile, content.getBytes(StandardCharsets.UTF_8));
        } catch (IOException e) {
            beräumeTempDatei(tempFile);
            String message = "Fehler beim Schreiben der temporären Prompt-Datei: " + e.getMessage();
            LOG.warn(SAVE_FAILED_LOG_MSG, message, e);
            return new PromptSaveResult.WriteFailed(message, e);
        }

        // Atomares Verschieben – kein stiller Fallback auf nicht-atomares Move
        try {
            Files.move(tempFile, promptFilePath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
            LOG.info("Prompt-Datei erfolgreich gespeichert: {}", promptFilePath.toAbsolutePath());
            return new PromptSaveResult.Saved(promptFilePath.toAbsolutePath().toString());
        } catch (AtomicMoveNotSupportedException e) {
            beräumeTempDatei(tempFile);
            String message = "Atomares Verschieben der Prompt-Datei wird vom Dateisystem nicht unterstützt: " + e.getMessage();
            LOG.warn("Prompt speichern fehlgeschlagen (kein Fallback): {}", message, e);
            return new PromptSaveResult.AtomicMoveFailed(message);
        } catch (IOException e) {
            beräumeTempDatei(tempFile);
            String message = "Fehler beim atomaren Verschieben der Prompt-Datei: " + e.getMessage();
            LOG.warn(SAVE_FAILED_LOG_MSG, message, e);
            return new PromptSaveResult.AtomicMoveFailed(message);
        }
    }

    /**
     * Leitet den stabilen Prompt-Identifikator aus dem Dateinamen ab.
     * <p>
     * Der Identifikator entspricht dem Dateinamen ohne Verzeichnispfad.
     *
     * @return stabiler {@link PromptIdentifier} basierend auf dem Dateinamen
     */
    private PromptIdentifier deriveIdentifier() {
        String filename = promptFilePath.getFileName().toString();
        return new PromptIdentifier(filename);
    }

    /**
     * Versucht, die temporäre Datei zu löschen. Fehler werden nur geloggt.
     *
     * @param tempFile die zu löschende temporäre Datei
     */
    private void beräumeTempDatei(Path tempFile) {
        try {
            Files.deleteIfExists(tempFile);
        } catch (IOException ex) {
            LOG.warn("Temporäre Prompt-Datei konnte nicht gelöscht werden: {}", tempFile, ex);
        }
    }
}
