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

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import javax.sql.DataSource;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.flywaydb.core.Flyway;
import org.sqlite.SQLiteConfig;
import org.sqlite.SQLiteDataSource;

import de.gecheckt.pdf.umbenenner.application.port.out.DatabaseCreationPort;

/**
 * SQLite-Implementierung des {@link DatabaseCreationPort}.
 * <p>
 * Erzeugt eine neue, leere SQLite-Datenbank gegen einen vom Aufrufer übergebenen
 * temporären Zielpfad und führt eine vollständige Flyway-Migration auf den neuesten
 * Schema-Stand aus. Anschließend wird ein Verbindungstest durchgeführt, der drei
 * Aspekte verifiziert:
 * <ol>
 *   <li>Eine SQLite-Verbindung kann erfolgreich geöffnet werden.</li>
 *   <li>Die Flyway-History-Tabelle (Standardname {@code flyway_schema_history}) ist
 *       vorhanden und enthält mindestens einen erfolgreichen Migrationseintrag.</li>
 *   <li>Eine einfache Leseabfrage gegen Schema-Metadaten
 *       ({@code sqlite_master}) liefert ohne Fehler.</li>
 * </ol>
 * <p>
 * Im Fehlerfall wird die temporäre Datei zuverlässig wieder entfernt; aufrufende
 * Komponenten erhalten ein klassifiziertes
 * {@link DatabaseCreationPort.DatabaseCreationResult.Failure}-Ergebnis.
 *
 * <h2>Architekturgrenze</h2>
 * <p>JDBC, SQLite-Konfiguration und Flyway-spezifische Typen verbleiben vollständig in
 * dieser Klasse. Nach außen wird ausschließlich der versiegelte Port-Ergebnistyp
 * herausgereicht.
 */
public class SqliteDatabaseCreationAdapter implements DatabaseCreationPort {

    private static final Logger LOG = LogManager.getLogger(SqliteDatabaseCreationAdapter.class);
    private static final String FLYWAY_HISTORY_TABLE = "flyway_schema_history";

    /**
     * Standardkonstruktor.
     */
    public SqliteDatabaseCreationAdapter() {
        // keine Felder, kein Zustand
    }

    /**
     * Legt eine neue, leere SQLite-Datenbank an, migriert sie auf den neuesten Stand
     * und führt einen Verbindungstest durch. Bei Fehlern wird die Temp-Datei entfernt.
     *
     * @param tempFile Pfad der zu erzeugenden temporären SQLite-Datei; darf nicht
     *                 {@code null} sein und sollte vor dem Aufruf nicht existieren
     * @return {@link DatabaseCreationResult.Success} bei Erfolg oder
     *         {@link DatabaseCreationResult.Failure} mit klassifizierter Phase
     */
    @Override
    public DatabaseCreationResult createAndInitialize(Path tempFile) {
        if (tempFile == null) {
            throw new NullPointerException("tempFile darf nicht null sein");
        }
        Path absoluteTemp = tempFile.toAbsolutePath().normalize();
        LOG.info("Lege neue temporäre SQLite-Datenbank an: {}", absoluteTemp);

        // Verhindern, dass eine versehentlich vorhandene Temp-Datei mitmigiert wird
        try {
            if (Files.exists(absoluteTemp)) {
                Files.delete(absoluteTemp);
            }
        } catch (IOException e) {
            LOG.error("Vorhandene temporäre Datei konnte nicht entfernt werden: {}",
                    absoluteTemp, e);
            return new DatabaseCreationResult.Failure(
                    DatabaseCreationResult.Phase.FILE_CREATION,
                    "Vorhandene temporäre Datei konnte nicht entfernt werden: " + e.getMessage(),
                    e);
        }

        String jdbcUrl = buildJdbcUrl(absoluteTemp);
        DataSource dataSource = createDataSource(jdbcUrl);

        // Schema-Migration auf neuesten Stand
        try {
            Flyway flyway = Flyway.configure()
                    .dataSource(dataSource)
                    .locations("classpath:db/migration")
                    .connectRetries(0)
                    .load();
            flyway.migrate();
            LOG.info("Flyway-Migration auf neuesten Stand abgeschlossen für: {}", absoluteTemp);
        } catch (RuntimeException e) {
            LOG.error("Flyway-Migration fehlgeschlagen für {}: {}", absoluteTemp, e.getMessage(), e);
            cleanup(absoluteTemp);
            return new DatabaseCreationResult.Failure(
                    DatabaseCreationResult.Phase.SCHEMA_MIGRATION,
                    "Schema-Migration fehlgeschlagen: " + e.getMessage(),
                    e);
        }

        // Verbindungstest gegen die migrierte Temp-Datei
        try {
            verifyConnection(dataSource);
            LOG.info("Verbindungstest gegen neue SQLite-Datenbank erfolgreich: {}", absoluteTemp);
        } catch (SQLException | IllegalStateException e) {
            LOG.error("Verbindungstest fehlgeschlagen für {}: {}", absoluteTemp, e.getMessage(), e);
            cleanup(absoluteTemp);
            return new DatabaseCreationResult.Failure(
                    DatabaseCreationResult.Phase.CONNECTION_TEST,
                    "Verbindungstest fehlgeschlagen: " + e.getMessage(),
                    e);
        }

        return new DatabaseCreationResult.Success(absoluteTemp);
    }

    /**
     * Verifiziert die migrierte Datenbank durch drei aufeinander aufbauende Prüfungen.
     *
     * @param dataSource die DataSource gegen die Temp-Datei
     * @throws SQLException             bei JDBC-Fehlern
     * @throws IllegalStateException    wenn eine fachliche Erwartung (z. B. Flyway-History
     *                                  vorhanden, mind. ein erfolgreicher Eintrag) verletzt ist
     */
    private void verifyConnection(DataSource dataSource) throws SQLException {
        try (Connection conn = dataSource.getConnection()) {
            try (Statement stmt = conn.createStatement()) {
                try (ResultSet rs = stmt.executeQuery(
                        "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='"
                                + FLYWAY_HISTORY_TABLE + "'")) {
                    if (!rs.next() || rs.getInt(1) != 1) {
                        throw new IllegalStateException(
                                "Flyway-History-Tabelle fehlt nach der Migration.");
                    }
                }
                try (ResultSet rs = stmt.executeQuery(
                        "SELECT count(*) FROM " + FLYWAY_HISTORY_TABLE + " WHERE success = 1")) {
                    if (!rs.next() || rs.getInt(1) < 1) {
                        throw new IllegalStateException(
                                "Flyway-History enthält keinen erfolgreichen Migrationseintrag.");
                    }
                }
                // einfache Leseabfrage gegen Schema-Metadaten
                try (ResultSet rs = stmt.executeQuery(
                        "SELECT name FROM sqlite_master WHERE type='table'")) {
                    int tableCount = 0;
                    while (rs.next()) {
                        tableCount++;
                    }
                    if (tableCount < 1) {
                        throw new IllegalStateException(
                                "Schema-Metadatenabfrage lieferte keine Tabellen.");
                    }
                }
            }
        }
    }

    private void cleanup(Path tempFile) {
        try {
            Files.deleteIfExists(tempFile);
        } catch (IOException e) {
            LOG.warn("Temporäre SQLite-Datei konnte nach Fehler nicht entfernt werden: {} – {}",
                    tempFile, e.getMessage());
        }
    }

    /**
     * Baut die JDBC-URL für eine SQLite-Datei nach dem im Projekt etablierten Schema.
     *
     * @param dbFile absoluter Pfad der SQLite-Datei; darf nicht {@code null} sein
     * @return die JDBC-URL in der Form {@code jdbc:sqlite:/pfad/zur/datei.db}
     */
    private static String buildJdbcUrl(Path dbFile) {
        return "jdbc:sqlite:" + dbFile.toAbsolutePath().toString().replace('\\', '/');
    }

    /**
     * Erstellt eine SQLite-DataSource mit aktivierten Fremdschlüsseln.
     *
     * @param jdbcUrl die JDBC-URL der SQLite-Datei
     * @return eine konfigurierte {@link DataSource}; nie {@code null}
     */
    private static DataSource createDataSource(String jdbcUrl) {
        SQLiteConfig config = new SQLiteConfig();
        config.enforceForeignKeys(true);
        SQLiteDataSource ds = new SQLiteDataSource(config);
        ds.setUrl(jdbcUrl);
        return ds;
    }
}
