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

import java.io.IOException;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;

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

import de.gecheckt.pdf.umbenenner.application.port.in.CreateNewDatabaseUseCase;
import de.gecheckt.pdf.umbenenner.application.port.out.ActiveDatabaseContextPort;
import de.gecheckt.pdf.umbenenner.application.port.out.DatabaseCreationPort;

/**
 * Standardimplementierung des {@link CreateNewDatabaseUseCase}.
 * <p>
 * Orchestriert den vollständigen Anlage- und Wechselvorgang einer neuen, leeren
 * SQLite-Datenbankdatei und delegiert die technischen Teilschritte an die Ports
 * {@link DatabaseCreationPort} und {@link ActiveDatabaseContextPort}. Der Adapter
 * darunter (z. B. SQLite/Flyway) bleibt für den Use-Case unsichtbar.
 *
 * <h2>Atomarität</h2>
 * Aus Anwendungssicht ist der Wechsel atomar:
 * <ul>
 *   <li>Bei einem Fehler in einem der Schritte wird die temporäre Datei zuverlässig
 *       entfernt; die aktive Datenbank bleibt unverändert in Betrieb.</li>
 *   <li>Erst nach erfolgreichem Verbindungstest wird die temporäre Datei via
 *       {@link StandardCopyOption#ATOMIC_MOVE} mit
 *       {@link StandardCopyOption#REPLACE_EXISTING} an den endgültigen Zielpfad
 *       verschoben. Bei nicht unterstützter Kombination wird der Vorgang mit
 *       klarer Fehlermeldung abgebrochen – kein stiller Fallback.</li>
 * </ul>
 *
 * <h2>Pfad-Sicherheitsprüfung</h2>
 * Aktive DB und Zielpfad werden über {@link Path#toRealPath(java.nio.file.LinkOption...)}
 * normalisiert verglichen. Für noch nicht existierende Dateien wird das Elternverzeichnis
 * real aufgelöst und der Dateiname normalisiert verglichen. Auf Windows erfolgt der
 * Vergleich case-insensitive. Ist aktuell keine aktive Datenbank in Betrieb (der
 * {@link ActiveDatabasePathSupplier} liefert {@code null}), entfällt die Prüfung und die
 * Anlage fährt fort – es gibt dann keine in Benutzung befindliche Datei zu schützen.
 */
public class DefaultCreateNewDatabaseUseCase implements CreateNewDatabaseUseCase {

    private static final Logger LOG = LogManager.getLogger(DefaultCreateNewDatabaseUseCase.class);
    private static final String OS_NAME = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);

    private final DatabaseCreationPort databaseCreationPort;
    private final ActiveDatabaseContextPort activeDatabaseContextPort;
    private final ActiveDatabasePathSupplier activeDatabasePathSupplier;

    /**
     * Liefert den Pfad der aktuell aktiven SQLite-Datei.
     * <p>
     * Diese Indirektion erlaubt es dem Bootstrap, sowohl den
     * {@link ActiveDatabaseContextPort}-Override als auch den Wert aus der geladenen
     * Konfigurationsdatei zu berücksichtigen, ohne dass der Use-Case Konfigurationstypen
     * kennen muss.
     */
    @FunctionalInterface
    public interface ActiveDatabasePathSupplier {

        /**
         * Liefert den Pfad der aktuell aktiven SQLite-Datei.
         *
         * @return den absoluten Pfad der aktiven Datei oder {@code null}, wenn aktuell keine
         *         aktive Datenbank in Betrieb ist. Bei {@code null} entfällt die
         *         Selbst-Überschreib-Prüfung und die Anlage darf fortfahren.
         */
        Path get();
    }

    /**
     * Erzeugt den Use-Case mit den drei erforderlichen Ports/Lieferanten.
     *
     * @param databaseCreationPort       Port zum Anlegen und Initialisieren der Temp-Datei;
     *                                   darf nicht {@code null} sein
     * @param activeDatabaseContextPort  Port zum Umstellen der aktiven DB-Referenz;
     *                                   darf nicht {@code null} sein
     * @param activeDatabasePathSupplier Lieferant für den Pfad der aktuell aktiven
     *                                   SQLite-Datei; darf nicht {@code null} sein
     * @throws NullPointerException wenn ein Parameter {@code null} ist
     */
    public DefaultCreateNewDatabaseUseCase(DatabaseCreationPort databaseCreationPort,
                                           ActiveDatabaseContextPort activeDatabaseContextPort,
                                           ActiveDatabasePathSupplier activeDatabasePathSupplier) {
        this.databaseCreationPort = Objects.requireNonNull(databaseCreationPort,
                "databaseCreationPort darf nicht null sein");
        this.activeDatabaseContextPort = Objects.requireNonNull(activeDatabaseContextPort,
                "activeDatabaseContextPort darf nicht null sein");
        this.activeDatabasePathSupplier = Objects.requireNonNull(activeDatabasePathSupplier,
                "activeDatabasePathSupplier darf nicht null sein");
    }

    /**
     * Orchestriert den vollständigen Anlage- und Wechselvorgang.
     * <p>
     * Liefert der {@link ActiveDatabasePathSupplier} {@code null} (keine aktive Datenbank in
     * Betrieb), entfällt die Selbst-Überschreib-Prüfung und die Anlage fährt fort.
     *
     * @param targetFile der vom Benutzer ausgewählte Zielpfad; darf nicht {@code null} sein
     * @return strukturiertes Ergebnis mit Erfolg oder klassifiziertem Fehler; nie {@code null}
     */
    @Override
    public CreateNewDatabaseResult createNewDatabase(Path targetFile) {
        Objects.requireNonNull(targetFile, "targetFile darf nicht null sein");
        Path absoluteTarget = targetFile.toAbsolutePath().normalize();
        LOG.info("Neue Datenbank anlegen: angeforderter Zielpfad = {}", absoluteTarget);

        // Schritt 1: Pfad-Sicherheitsprüfung
        // Ohne aktive Datenbank (Supplier liefert null) gibt es nichts zu schützen; die
        // Selbst-Überschreib-Prüfung entfällt und die Anlage darf fortfahren.
        Path activeDb = activeDatabasePathSupplier.get();
        if (activeDb == null) {
            LOG.info("Keine aktive Datenbank in Betrieb – Selbst-Überschreib-Prüfung entfällt, "
                    + "Anlage fährt fort.");
        } else {
            Path absoluteActive = activeDb.toAbsolutePath().normalize();
            boolean sameFile;
            try {
                sameFile = isSameFile(absoluteActive, absoluteTarget);
            } catch (IOException e) {
                LOG.error("Pfad-Sicherheitsprüfung fehlgeschlagen: {}", e.getMessage(), e);
                return new CreateNewDatabaseResult.CreationFailed(
                        CreateNewDatabaseResult.Phase.PATH_RESOLUTION,
                        "Pfad-Sicherheitsprüfung fehlgeschlagen: " + e.getMessage(),
                        e);
            }
            if (sameFile) {
                LOG.warn("Anlage abgelehnt: Zielpfad entspricht der aktuell aktiven Datenbank: {}",
                        absoluteTarget);
                return new CreateNewDatabaseResult.SameAsActiveDatabase(absoluteTarget);
            }
        }

        // Schritt 2: Temp-Datei im Zielverzeichnis vorbereiten
        Path parent = absoluteTarget.getParent();
        if (parent == null) {
            LOG.error("Zielpfad besitzt kein Elternverzeichnis: {}", absoluteTarget);
            return new CreateNewDatabaseResult.CreationFailed(
                    CreateNewDatabaseResult.Phase.PATH_RESOLUTION,
                    "Zielpfad besitzt kein Elternverzeichnis.",
                    null);
        }
        try {
            if (!Files.isDirectory(parent)) {
                Files.createDirectories(parent);
            }
        } catch (IOException e) {
            LOG.error("Zielverzeichnis konnte nicht angelegt werden: {}", parent, e);
            return new CreateNewDatabaseResult.CreationFailed(
                    CreateNewDatabaseResult.Phase.FILE_CREATION,
                    "Zielverzeichnis konnte nicht angelegt werden: " + e.getMessage(),
                    e);
        }
        Path tempFile = parent.resolve(absoluteTarget.getFileName().toString()
                + ".new-" + UUID.randomUUID() + ".tmp");

        // Schritt 3: Adapter führt Anlage + Schema-Migration + Verbindungstest aus
        DatabaseCreationPort.DatabaseCreationResult creationResult;
        try {
            creationResult = databaseCreationPort.createAndInitialize(tempFile);
        } catch (RuntimeException e) {
            LOG.error("Unerwarteter Fehler beim Anlegen der temporären Datenbank: {}",
                    e.getMessage(), e);
            deleteTempQuietly(tempFile);
            return new CreateNewDatabaseResult.CreationFailed(
                    CreateNewDatabaseResult.Phase.FILE_CREATION,
                    "Unerwarteter Fehler beim Anlegen der temporären Datenbank: " + e.getMessage(),
                    e);
        }
        if (creationResult instanceof DatabaseCreationPort.DatabaseCreationResult.Failure failure) {
            CreateNewDatabaseResult.Phase phase = mapPhase(failure.phase());
            LOG.error("Anlage der neuen Datenbank fehlgeschlagen ({}): {}",
                    failure.phase(), failure.message());
            deleteTempQuietly(tempFile);
            return new CreateNewDatabaseResult.CreationFailed(phase, failure.message(),
                    failure.cause());
        }

        // Schritt 4: atomarer Move auf Zielpfad
        try {
            Files.move(tempFile, absoluteTarget,
                    StandardCopyOption.ATOMIC_MOVE,
                    StandardCopyOption.REPLACE_EXISTING);
        } catch (AtomicMoveNotSupportedException e) {
            LOG.error("Atomarer Move nicht unterstützt für Zielpfad {}: {}",
                    absoluteTarget, e.getMessage(), e);
            deleteTempQuietly(tempFile);
            return new CreateNewDatabaseResult.CreationFailed(
                    CreateNewDatabaseResult.Phase.ATOMIC_MOVE,
                    "Atomarer Move (ATOMIC_MOVE + REPLACE_EXISTING) wird vom Dateisystem nicht "
                            + "unterstützt. Kein nicht-atomarer Fallback. Ziel: "
                            + absoluteTarget,
                    e);
        } catch (IOException e) {
            LOG.error("Atomarer Move fehlgeschlagen: {}", e.getMessage(), e);
            deleteTempQuietly(tempFile);
            return new CreateNewDatabaseResult.CreationFailed(
                    CreateNewDatabaseResult.Phase.ATOMIC_MOVE,
                    "Verschieben der temporären Datenbank zum Zielpfad fehlgeschlagen: "
                            + e.getMessage(),
                    e);
        }

        // Schritt 5: Aktive DB-Referenz umstellen
        try {
            activeDatabaseContextPort.switchActiveDatabase(absoluteTarget);
        } catch (RuntimeException e) {
            LOG.error("Umstellen der aktiven DB-Referenz fehlgeschlagen: {}", e.getMessage(), e);
            return new CreateNewDatabaseResult.CreationFailed(
                    CreateNewDatabaseResult.Phase.CONTEXT_SWITCH,
                    "Aktive DB-Referenz konnte nicht umgestellt werden: " + e.getMessage(),
                    e);
        }

        LOG.info("Neue Datenbank erfolgreich angelegt und aktiviert: {}", absoluteTarget);
        return new CreateNewDatabaseResult.Success(absoluteTarget);
    }

    /**
     * Vergleicht zwei Datenbankpfade mit Berücksichtigung von Symlinks und
     * (auf Windows) Case-Insensitivität.
     * <p>
     * Existieren beide Dateien, wird {@code Files.isSameFile(...)} verwendet.
     * Existiert eine der beiden Dateien (typischerweise das Ziel) noch nicht, werden
     * Elternverzeichnisse via {@link Path#toRealPath(java.nio.file.LinkOption...)}
     * aufgelöst und mit den Dateinamen kombiniert verglichen. Auf Windows erfolgt der
     * abschließende String-Vergleich case-insensitive.
     *
     * @param a Pfad A; darf nicht {@code null} sein
     * @param b Pfad B; darf nicht {@code null} sein
     * @return {@code true}, wenn beide Pfade auf dieselbe Datei zeigen
     * @throws IOException bei Auflösungsfehlern existierender Pfadbestandteile
     */
    static boolean isSameFile(Path a, Path b) throws IOException {
        Objects.requireNonNull(a, "a darf nicht null sein");
        Objects.requireNonNull(b, "b darf nicht null sein");
        if (Files.exists(a) && Files.exists(b)) {
            return Files.isSameFile(a, b);
        }
        Path realA = resolveBest(a);
        Path realB = resolveBest(b);
        if (isWindows()) {
            return realA.toString().equalsIgnoreCase(realB.toString());
        }
        return realA.equals(realB);
    }

    /**
     * Löst die existierenden Bestandteile eines Pfades soweit möglich real auf und
     * normalisiert den Rest. Wird verwendet, wenn die Datei selbst noch nicht existiert.
     *
     * @param path der zu normalisierende Pfad
     * @return ein bestmöglich aufgelöster, normalisierter Pfad
     * @throws IOException bei {@link Path#toRealPath(java.nio.file.LinkOption...)}-Fehlern
     */
    private static Path resolveBest(Path path) throws IOException {
        if (Files.exists(path)) {
            return path.toRealPath();
        }
        Path parent = path.toAbsolutePath().normalize().getParent();
        Path fileName = path.getFileName();
        if (parent != null && Files.exists(parent)) {
            return parent.toRealPath().resolve(fileName == null ? "" : fileName.toString());
        }
        return path.toAbsolutePath().normalize();
    }

    /**
     * Liefert {@code true}, wenn die laufende JVM auf Windows läuft.
     *
     * @return {@code true}, wenn Windows; sonst {@code false}
     */
    private static boolean isWindows() {
        return OS_NAME.contains("win");
    }

    private void deleteTempQuietly(Path tempFile) {
        if (tempFile == null) {
            return;
        }
        try {
            Files.deleteIfExists(tempFile);
        } catch (IOException e) {
            LOG.warn("Temporäre Datenbankdatei konnte nicht gelöscht werden: {} – {}",
                    tempFile, e.getMessage());
        }
    }

    private static CreateNewDatabaseResult.Phase mapPhase(
            DatabaseCreationPort.DatabaseCreationResult.Phase phase) {
        return switch (phase) {
            case FILE_CREATION -> CreateNewDatabaseResult.Phase.FILE_CREATION;
            case SCHEMA_MIGRATION -> CreateNewDatabaseResult.Phase.SCHEMA_MIGRATION;
            case CONNECTION_TEST -> CreateNewDatabaseResult.Phase.CONNECTION_TEST;
        };
    }

    /**
     * Liefert das gesetzte Override (sofern vorhanden), für Diagnose- und Logging-Zwecke.
     * Nicht Teil der öffentlichen Use-Case-API.
     *
     * @return das Override aus dem {@link ActiveDatabaseContextPort}; nie {@code null}
     */
    Optional<Path> currentOverride() {
        return activeDatabaseContextPort.activeDatabaseOverride();
    }
}
