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

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;

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

import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.PathCheckPort;

/**
 * Dateisystem-basierte Implementierung von {@link PathCheckPort}.
 * <p>
 * Prüft die Zugänglichkeit von Pfaden für Quellordner, Zielordner, SQLite-Datei
 * und Prompt-Datei. Schreibbarkeitstests erfolgen über kurzlebige Probe-Dateien,
 * die unmittelbar nach dem Schreibversuch wieder gelöscht werden.
 *
 * <h2>Windows- und Netzlaufwerk-Unterstützung</h2>
 * <p>
 * Gemappte Laufwerksbuchstaben wie {@code S:\} oder {@code H:\} werden ausdrücklich
 * akzeptiert. Solche Pfade werden nicht allein deshalb abgelehnt, weil dahinter technisch
 * ein UNC-Pfad stehen könnte. Maßgeblich ist, dass Windows den Pfad als gültig bereitstellt.
 * UNC-Pfade ({@code \\server\share\...}) werden ebenfalls akzeptiert, sofern das
 * Betriebssystem sie direkt auflösen kann. Es findet keine Umdeutung zwischen gemappten
 * Laufwerksbuchstaben und UNC-Pfaden statt.
 * <p>
 * Die Implementierung nutzt {@link Paths#get(String)}, {@link Files#exists(Path, java.nio.file.LinkOption...)},
 * {@link Files#isReadable(Path)} sowie einen Probe-Datei-Ansatz ({@link Files#createTempFile})
 * für Schreibbarkeitstests. Dieser Ansatz ist auf gemappten Windows-Netzlaufwerken
 * zuverlässiger als {@link Files#isWritable(Path)}, das dort bekannte False Negatives liefert.
 *
 * <h2>Thread-Safety</h2>
 * <p>
 * Diese Klasse ist zustandslos und damit thread-safe. Jede Methode kann gleichzeitig
 * von mehreren Threads aufgerufen werden. Der Aufrufer ist dafür verantwortlich, die
 * Methoden auf einem Hintergrund-Worker-Thread auszuführen, da Dateisystem-I/O
 * blockierend sein kann.
 *
 * <h2>Fehlerbehandlung</h2>
 * <p>
 * Erwartete Fehlerbedingungen (Pfad nicht vorhanden, keine Leseberechtigung) werden
 * als {@code boolean}-Rückgabewert kommuniziert. Unerwartete technische Fehler werden
 * geloggt und als {@code false} zurückgegeben.
 */
public class FilesystemPathCheckAdapter implements PathCheckPort {

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

    /**
     * Erstellt einen neuen {@code FilesystemPathCheckAdapter}.
     */
    public FilesystemPathCheckAdapter() {
        // stateless — kein Zustand zu initialisieren
    }

    /**
     * Prüft, ob der angegebene Pfad auf einen vorhandenen, lesbaren Ordner zeigt.
     * <p>
     * Gibt {@code false} zurück, wenn der Pfad leer, nicht parsebar, nicht vorhanden,
     * kein Verzeichnis oder nicht lesbar ist.
     *
     * @param path zu prüfender Pfad als String; darf nicht {@code null} oder leer sein
     * @return {@code true} wenn der Ordner existiert und gelesen werden kann
     */
    @Override
    public boolean isDirectoryReadable(String path) {
        LOG.debug("Prüfe Ordner auf Lesbarkeit: {}", path);
        Path resolved = toPath(path);
        if (resolved == null) {
            LOG.warn("Ordner-Lesbarkeit: ungültiger Pfad: {}", path);
            return false;
        }
        boolean result = Files.exists(resolved)
                && Files.isDirectory(resolved)
                && Files.isReadable(resolved);
        if (result) {
            LOG.debug("Ordner lesbar: {}", resolved);
        } else {
            LOG.warn("Ordner nicht lesbar oder nicht vorhanden: {}", resolved);
        }
        return result;
    }

    /**
     * Prüft, ob der angegebene Pfad auf einen vorhandenen, schreibbaren Ordner zeigt
     * oder ob dieser Ordner technisch anlegbar wäre.
     * <p>
     * Gibt {@code true} zurück, wenn:
     * <ul>
     *   <li>der Ordner existiert und schreibbar ist, oder</li>
     *   <li>der Ordner noch nicht existiert, aber sein Elternpfad erreichbar und
     *       schreibbar ist (anlegbar).</li>
     * </ul>
     * Gibt {@code false} zurück, wenn der Pfad leer, nicht parsebar, der Ordner
     * existiert aber nicht schreibbar ist, oder weder der Ordner noch ein schreibbarer
     * Elternpfad vorhanden ist.
     *
     * @param path zu prüfender Pfad als String; darf nicht {@code null} oder leer sein
     * @return {@code true} wenn der Ordner vorhanden und schreibbar oder anlegbar ist
     */
    @Override
    public boolean isDirectoryWritableOrCreatable(String path) {
        LOG.debug("Prüfe Ordner auf Schreibbarkeit oder Anlegbarkeit: {}", path);
        Path resolved = toPath(path);
        if (resolved == null) {
            LOG.warn("Ordner-Schreibbarkeit: ungültiger Pfad: {}", path);
            return false;
        }
        if (Files.exists(resolved)) {
            boolean writable = Files.isDirectory(resolved) && probeDirectoryWritable(resolved);
            if (writable) {
                LOG.debug("Ordner vorhanden und schreibbar: {}", resolved);
            } else {
                LOG.warn("Ordner vorhanden, aber nicht schreibbar: {}", resolved);
            }
            return writable;
        }
        // Ordner existiert nicht — prüfen ob Elternpfad schreibbar ist
        Path parent = resolved.getParent();
        if (parent != null && Files.exists(parent) && Files.isDirectory(parent) && probeDirectoryWritable(parent)) {
            LOG.debug("Ordner nicht vorhanden, aber anlegbar (Elternpfad schreibbar): {}", resolved);
            return true;
        }
        LOG.warn("Ordner nicht vorhanden und nicht anlegbar: {}", resolved);
        return false;
    }

    /**
     * Prüft, ob der angegebene Pfad auf eine vorhandene, lesbare Datei zeigt.
     * <p>
     * Gibt {@code false} zurück, wenn der Pfad leer, nicht parsebar, nicht vorhanden,
     * kein reguläres File oder nicht lesbar ist.
     *
     * @param path zu prüfender Pfad als String; darf nicht {@code null} oder leer sein
     * @return {@code true} wenn die Datei existiert und gelesen werden kann
     */
    @Override
    public boolean isFileReadable(String path) {
        LOG.debug("Prüfe Datei auf Lesbarkeit: {}", path);
        Path resolved = toPath(path);
        if (resolved == null) {
            LOG.warn("Datei-Lesbarkeit: ungültiger Pfad: {}", path);
            return false;
        }
        boolean result = Files.exists(resolved)
                && Files.isRegularFile(resolved)
                && Files.isReadable(resolved);
        if (result) {
            LOG.debug("Datei lesbar: {}", resolved);
        } else {
            LOG.warn("Datei nicht lesbar oder nicht vorhanden: {}", resolved);
        }
        return result;
    }

    /**
     * Prüft, ob der angegebene Pfad als SQLite-Datenbankpfad technisch nutzbar ist.
     * <p>
     * Gibt {@code true} zurück, wenn:
     * <ul>
     *   <li>die Datei existiert, les- und schreibbar ist, oder</li>
     *   <li>die Datei noch nicht existiert, aber ihr übergeordneter Ordner vorhanden
     *       und schreibbar ist (Datei wäre anlegbar).</li>
     * </ul>
     * Gibt {@code false} zurück, wenn der Pfad leer, nicht parsebar, die Datei
     * existiert aber nicht nutzbar ist, oder weder die Datei noch ein beschreibbarer
     * Elternordner vorhanden ist.
     *
     * @param path zu prüfender Pfad als String; darf nicht {@code null} oder leer sein
     * @return {@code true} wenn der SQLite-Pfad nutzbar oder anlegbar ist
     */
    @Override
    public boolean isSqlitePathUsable(String path) {
        LOG.debug("Prüfe SQLite-Pfad auf Nutzbarkeit: {}", path);
        Path resolved = toPath(path);
        if (resolved == null) {
            LOG.warn("SQLite-Pfad: ungültiger Pfad: {}", path);
            return false;
        }
        if (Files.exists(resolved)) {
            Path parentDir = resolved.getParent();
            boolean usable = Files.isRegularFile(resolved)
                    && Files.isReadable(resolved)
                    && parentDir != null
                    && probeDirectoryWritable(parentDir);
            if (usable) {
                LOG.debug("SQLite-Datei vorhanden und nutzbar: {}", resolved);
            } else {
                LOG.warn("SQLite-Datei vorhanden, aber nicht les- und schreibbar: {}", resolved);
            }
            return usable;
        }
        // Datei existiert nicht — prüfen ob Elternordner schreibbar ist
        Path parent = resolved.getParent();
        if (parent != null && Files.exists(parent) && Files.isDirectory(parent) && probeDirectoryWritable(parent)) {
            LOG.debug("SQLite-Datei nicht vorhanden, aber anlegbar (Elternordner schreibbar): {}", resolved);
            return true;
        }
        LOG.warn("SQLite-Pfad nicht nutzbar und nicht anlegbar: {}", resolved);
        return false;
    }

    /**
     * Prüft die Schreibbarkeit eines Verzeichnisses durch einen echten Schreibversuch.
     * <p>
     * Eine temporäre Probe-Datei wird im übergebenen Verzeichnis angelegt und sofort
     * wieder gelöscht. Dieses Vorgehen ist auf gemappten Windows-Netzlaufwerken
     * zuverlässiger als {@link Files#isWritable(Path)}, das dort False Negatives liefern kann.
     *
     * @param directory das zu prüfende Verzeichnis; muss ein existierendes Verzeichnis sein
     * @return {@code true} wenn eine Datei im Verzeichnis angelegt werden konnte
     */
    private static boolean probeDirectoryWritable(Path directory) {
        try {
            Path probe = Files.createTempFile(directory, ".writetest-", ".tmp");
            try {
                Files.deleteIfExists(probe);
            } catch (IOException cleanupFailure) {
                LOG.debug("Probe-Datei konnte nicht gelöscht werden: {} — {}", probe, cleanupFailure.getMessage());
            }
            return true;
        } catch (IOException e) {
            return false;
        }
    }

    /**
     * Konvertiert den übergebenen Pfad-String in ein {@link Path}-Objekt.
     * <p>
     * Gibt {@code null} zurück, wenn der String {@code null}, leer oder nicht parsebar ist
     * (z. B. wegen ungültiger Zeichen auf Windows). Keine Ausnahme wird geworfen.
     *
     * @param path der zu konvertierende Pfad-String
     * @return das {@link Path}-Objekt oder {@code null} bei ungültigem Eingabewert
     */
    private static Path toPath(String path) {
        if (path == null || path.isBlank()) {
            return null;
        }
        try {
            return Paths.get(path);
        } catch (InvalidPathException e) {
            LOG.warn("Pfad nicht parsebar: '{}' — {}", path, e.getMessage());
            return null;
        }
    }
}
