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

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

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

import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionOutcome;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.CorrectionSuggestion;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.DefaultPromptTemplate;
import de.gecheckt.pdf.umbenenner.application.validation.technicaltest.ResourceCreationPort;

/**
 * Dateisystem-basierte Implementierung von {@link ResourceCreationPort}.
 * <p>
 * Führt schreibende technische Korrekturmaßnahmen durch: Ordner anlegen,
 * SQLite-Elternordner vorbereiten und Prompt-Dateien mit übergebenem Inhalt erzeugen.
 * Alle Methoden sind idempotent, sofern die Ziel-Ressource bereits vorhanden ist.
 *
 * <h2>Windows- und Netzlaufwerk-Unterstützung</h2>
 * <p>
 * Gemappte Laufwerksbuchstaben wie {@code S:\} oder {@code H:\} werden ausdrücklich
 * akzeptiert. Die Implementierung nutzt ausschließlich {@link Paths#get(String)} und
 * {@link Files}-Methoden, die unter Windows gemappte Laufwerke korrekt respektieren.
 *
 * <h2>Thread-Safety</h2>
 * <p>
 * Diese Klasse ist zustandslos und thread-safe. Der Aufrufer ist verantwortlich dafür,
 * Methoden auf einem Hintergrund-Worker-Thread auszuführen, da Dateisystem-I/O
 * blockierend sein kann.
 *
 * <h2>Fehlerbehandlung</h2>
 * <p>
 * Jede Methode fängt alle technischen Ausnahmen und gibt ein entsprechendes
 * {@link CorrectionOutcome.Failed}-Ergebnis zurück. Es werden keine geprüften
 * Ausnahmen an den Aufrufer weitergegeben.
 */
public class FilesystemResourceCreationAdapter implements ResourceCreationPort {
    private static final String INVALID_PATH_PREFIX = "Ungültiger Pfad: ";


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

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

    /**
     * Legt den angegebenen Ordner an, einschließlich aller fehlenden übergeordneten Ordner.
     * <p>
     * Falls der Ordner bereits existiert, wird {@link CorrectionOutcome.Applied} zurückgegeben
     * (idempotente Ausführung). Die Aktion wird mit Zielpfad geloggt.
     *
     * @param suggestion der {@link CorrectionSuggestion.CreateDirectory}-Vorschlag; darf nicht {@code null} sein
     * @return Ergebnis der Ausführung; nie {@code null}
     */
    @Override
    public CorrectionOutcome createDirectory(CorrectionSuggestion.CreateDirectory suggestion) {
        Path path = toPath(suggestion.path());
        if (path == null) {
            String msg = INVALID_PATH_PREFIX + suggestion.path();
            LOG.warn("Ordner anlegen fehlgeschlagen: {}", msg);
            return new CorrectionOutcome.Failed(suggestion, msg);
        }

        try {
            if (Files.exists(path)) {
                if (Files.isDirectory(path)) {
                    LOG.info("Ordner bereits vorhanden (kein Anlegen nötig): {}", path);
                    return new CorrectionOutcome.Applied(suggestion,
                            "Ordner bereits vorhanden: " + path.toAbsolutePath());
                } else {
                    String msg = "Pfad existiert bereits als Datei (kein Ordner): " + path.toAbsolutePath();
                    LOG.warn("Ordner anlegen fehlgeschlagen: {}", msg);
                    return new CorrectionOutcome.Failed(suggestion, msg);
                }
            }
            Files.createDirectories(path);
            LOG.info("Ordner erfolgreich angelegt: {}", path.toAbsolutePath());
            return new CorrectionOutcome.Applied(suggestion,
                    "Ordner angelegt: " + path.toAbsolutePath());
        } catch (IOException e) {
            String msg = "Ordner konnte nicht angelegt werden: " + e.getMessage();
            LOG.warn("Ordner anlegen fehlgeschlagen: {} — {}", path, e.getMessage(), e);
            return new CorrectionOutcome.Failed(suggestion, msg);
        }
    }

    /**
     * Erzeugt eine neue Prompt-Datei mit dem übergebenen Inhalt.
     * <p>
     * Die Datei wird nur erzeugt, wenn sie noch nicht existiert. Falls die Datei bereits
     * vorhanden ist, wird {@link CorrectionOutcome.NotAttempted} zurückgegeben (kein
     * stilles Überschreiben). Der Inhalt wird als UTF-8-Text geschrieben.
     * Die Aktion wird mit Zielpfad geloggt.
     * <p>
     * Der Inhalt der erzeugten Datei wird von {@link DefaultPromptTemplate#defaultContent(int)}
     * geliefert. Als Parameter wird die im {@code CreatePromptFile}-Vorschlag enthaltene
     * konfigurierte maximale Titellänge verwendet. Es handelt sich um einen deutschen
     * Standardprompt, der ohne weitere Anpassung funktioniert.
     *
     * @param suggestion der {@link CorrectionSuggestion.CreatePromptFile}-Vorschlag; darf nicht {@code null} sein
     * @return Ergebnis der Ausführung; nie {@code null}
     */
    @Override
    public CorrectionOutcome createPromptFile(CorrectionSuggestion.CreatePromptFile suggestion) {
        Path path = toPath(suggestion.path());
        if (path == null) {
            String msg = INVALID_PATH_PREFIX + suggestion.path();
            LOG.warn("Prompt-Datei erzeugen fehlgeschlagen: {}", msg);
            return new CorrectionOutcome.Failed(suggestion, msg);
        }

        try {
            if (Files.exists(path)) {
                String msg = "Prompt-Datei bereits vorhanden – kein Überschreiben: " + path.toAbsolutePath();
                LOG.info("Prompt-Datei erzeugen: Datei bereits vorhanden, wird nicht überschrieben: {}", path);
                return new CorrectionOutcome.NotAttempted(suggestion, msg);
            }

            // Elternordner sicherstellen
            Path parent = path.getParent();
            if (parent != null && !Files.exists(parent)) {
                Files.createDirectories(parent);
                LOG.info("Prompt-Datei: Elternordner angelegt: {}", parent);
            }

            Files.writeString(path, DefaultPromptTemplate.defaultContent(suggestion.maxTitleLength()),
                    StandardCharsets.UTF_8,
                    StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
            LOG.info("Prompt-Datei erfolgreich erzeugt: {}", path.toAbsolutePath());
            return new CorrectionOutcome.Applied(suggestion,
                    "Prompt-Datei erzeugt: " + path.toAbsolutePath());
        } catch (FileAlreadyExistsException e) {
            String msg = "Prompt-Datei bereits vorhanden – kein Überschreiben: " + path.toAbsolutePath();
            LOG.info("Prompt-Datei erzeugen: race condition – Datei bereits vorhanden: {}", path);
            return new CorrectionOutcome.NotAttempted(suggestion, msg);
        } catch (IOException e) {
            String msg = "Prompt-Datei konnte nicht erzeugt werden: " + e.getMessage();
            LOG.warn("Prompt-Datei erzeugen fehlgeschlagen: {} — {}", path, e.getMessage(), e);
            return new CorrectionOutcome.Failed(suggestion, msg);
        }
    }

    /**
     * Bereitet den übergeordneten Ordner einer SQLite-Datei vor, sofern dieser fehlt.
     * <p>
     * Legt den Elternordner der SQLite-Datei mit allen fehlenden Zwischenordnern an,
     * falls er noch nicht vorhanden ist. Die SQLite-Datei selbst wird nicht erzeugt;
     * das übernimmt das JDBC-Layer beim ersten Datenbankzugriff. Die Aktion wird geloggt.
     *
     * @param suggestion der {@link CorrectionSuggestion.PrepareSqlitePath}-Vorschlag; darf nicht {@code null} sein
     * @return Ergebnis der Ausführung; nie {@code null}
     */
    @Override
    public CorrectionOutcome prepareSqlitePath(CorrectionSuggestion.PrepareSqlitePath suggestion) {
        Path path = toPath(suggestion.path());
        if (path == null) {
            String msg = INVALID_PATH_PREFIX + suggestion.path();
            LOG.warn("SQLite-Pfad vorbereiten fehlgeschlagen: {}", msg);
            return new CorrectionOutcome.Failed(suggestion, msg);
        }

        Path parent = path.getParent();
        if (parent == null) {
            // Datei liegt direkt im Wurzelverzeichnis — kein Elternordner anlegbar
            LOG.info("SQLite-Pfad: kein Elternordner vorhanden (Wurzelpfad): {}", path);
            return new CorrectionOutcome.Applied(suggestion,
                    "SQLite-Pfad liegt im Wurzelverzeichnis, kein Ordner anzulegen: " + path.toAbsolutePath());
        }

        try {
            if (Files.exists(parent)) {
                LOG.info("SQLite-Elternordner bereits vorhanden: {}", parent);
                return new CorrectionOutcome.Applied(suggestion,
                        "SQLite-Elternordner bereits vorhanden: " + parent.toAbsolutePath());
            }
            Files.createDirectories(parent);
            LOG.info("SQLite-Elternordner erfolgreich angelegt: {}", parent.toAbsolutePath());
            return new CorrectionOutcome.Applied(suggestion,
                    "SQLite-Elternordner angelegt: " + parent.toAbsolutePath());
        } catch (IOException e) {
            String msg = "SQLite-Elternordner konnte nicht angelegt werden: " + e.getMessage();
            LOG.warn("SQLite-Pfad vorbereiten fehlgeschlagen: {} — {}", parent, e.getMessage(), e);
            return new CorrectionOutcome.Failed(suggestion, msg);
        }
    }

    /**
     * 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.
     *
     * @param pathString der zu konvertierende Pfad-String
     * @return das {@link Path}-Objekt oder {@code null} bei ungültigem Eingabewert
     */
    private static Path toPath(String pathString) {
        if (pathString == null || pathString.isBlank()) {
            return null;
        }
        try {
            return Paths.get(pathString);
        } catch (InvalidPathException e) {
            LOG.warn("Pfad nicht parsebar: '{}' — {}", pathString, e.getMessage());
            return null;
        }
    }
}
