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

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

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.DocumentPersistenceException;
import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceSchemaInitializationPort;

/**
 * Flyway-basierte Implementierung von {@link PersistenceSchemaInitializationPort}.
 *
 * <p>Erstellt oder verifiziert das Zwei-Ebenen-Persistenzschema in der konfigurierten
 * SQLite-Datenbank und führt dabei eine differenzierte Startstrategie durch,
 * die drei Fälle unterscheidet:
 *
 * <h2>Fall 1 – Leere Datenbank</h2>
 * <p>Keine fachlichen Tabellen und keine Flyway-History-Tabelle vorhanden
 * (bzw. Datei existiert noch nicht). Flyway führt {@code V1__initial_schema.sql}
 * vollständig aus und legt das komplette Schema an.
 *
 * <h2>Fall 2 – Bestehende Datenbank ohne Flyway-History</h2>
 * <p>Fachliche Tabellen sind vorhanden, aber die Flyway-History-Tabelle fehlt.
 * Vor der Baseline-Eintralung wird eine vollständige Schema-Prüfung gegen das
 * V1-Zielschema durchgeführt. Bei konformem Schema wird ein datiertes Backup der
 * SQLite-Datei erstellt, und Flyway trägt nur eine Baseline ein (Skript wird
 * <em>nicht</em> ausgeführt). Bei fehlendem Schema-Element bricht der Start mit
 * einer klaren Fehlermeldung ab.
 *
 * <h2>Fall 3 – Folgestart mit Flyway-History</h2>
 * <p>Flyway-History-Tabelle ist vorhanden. Flyway läuft idempotent und
 * führt nur noch fehlende Migrationen aus.
 *
 * <h2>Fremdschlüssel</h2>
 * <p>Foreign-Key-Durchsetzung wird über {@code SQLiteConfig.enforceForeignKeys(true)}
 * auf DataSource-Ebene aktiviert, sodass jede neue Verbindung automatisch
 * {@code PRAGMA foreign_keys = ON} erhält.
 *
 * <h2>Architekturgrenze</h2>
 * <p>Alle JDBC-Verbindungen, SQL-DDL und SQLite-spezifisches Verhalten sind
 * ausschließlich in dieser Klasse gekapselt. Im Port-Interface und in den
 * Domain-/Application-Typen erscheinen keine JDBC- oder SQLite-Typen.
 */
public class SqliteSchemaInitializationAdapter implements PersistenceSchemaInitializationPort {
    private static final String TABLE_DOCUMENT_RECORD = "document_record";
    private static final String TABLE_PROCESSING_ATTEMPT = "processing_attempt";
    private static final String COL_FINGERPRINT = "fingerprint";



    private static final Logger logger = LogManager.getLogger(SqliteSchemaInitializationAdapter.class);

    // -------------------------------------------------------------------------
    // Erwartete Tabellen und Spalten gemäß V1-Zielschema
    // -------------------------------------------------------------------------

    /** Alle erwarteten Spalten der Tabelle {@code document_record}. */
    private static final Set<String> EXPECTED_COLUMNS_DOCUMENT_RECORD = Set.of(
            "id", COL_FINGERPRINT, "last_known_source_locator", "last_known_source_file_name",
            "overall_status", "content_error_count", "transient_error_count",
            "last_failure_instant", "last_success_instant", "created_at", "updated_at",
            "last_target_path", "last_target_file_name"
    );

    /**
     * Alle erwarteten Spalten der Tabelle {@code processing_attempt} im
     * V1-Zielschema.
     *
     * <p>Dies ist der minimale Zielzustand nach {@code V1__initial_schema}.
     * Spaetere Migrationen (z.B. {@code V2__token_tracking}) ergaenzen
     * additiv weitere Spalten; das Vorhandensein dieser zusaetzlichen Spalten
     * vor dem Baseline-Eintrag ist <strong>kein</strong> Konformitaetskriterium,
     * weil die Schema-Pruefung in Fall 2 ausschlie&szlig;lich gegen das
     * V1-Schema arbeitet. Die V2-Spalten werden nach der Baseline-Eintragung
     * durch Flyway ergaenzt.
     */
    private static final Set<String> EXPECTED_COLUMNS_PROCESSING_ATTEMPT = Set.of(
            "id", COL_FINGERPRINT, "run_id", "attempt_number", "started_at", "ended_at",
            "status", "failure_class", "failure_message", "retryable",
            "model_name", "prompt_identifier", "processed_page_count", "sent_character_count",
            "ai_raw_response", "ai_reasoning", "resolved_date", "date_source",
            "validated_title", "final_target_file_name", "ai_provider"
    );

    /**
     * Erwartete Indizes nach {@code V1__initial_schema}.
     *
     * <p>Spaetere Migrationen koennen additiv weitere Indizes anlegen; sie
     * sind kein Konformitaetskriterium fuer Fall 2.
     */
    private static final Set<String> EXPECTED_INDEXES = Set.of(
            "idx_processing_attempt_fingerprint",
            "idx_processing_attempt_run_id",
            "idx_document_record_overall_status"
    );

    /** Name der Flyway-History-Tabelle. */
    private static final String FLYWAY_HISTORY_TABLE = "flyway_schema_history";

    // -------------------------------------------------------------------------
    // Felder
    // -------------------------------------------------------------------------

    private final String jdbcUrl;

    /**
     * Erstellt den Adapter mit der JDBC-URL der SQLite-Datenbankdatei.
     *
     * @param jdbcUrl die JDBC-URL der SQLite-Datenbank; darf nicht {@code null} oder leer sein
     * @throws NullPointerException     wenn {@code jdbcUrl} {@code null} ist
     * @throws IllegalArgumentException wenn {@code jdbcUrl} leer ist
     */
    public SqliteSchemaInitializationAdapter(String jdbcUrl) {
        Objects.requireNonNull(jdbcUrl, "jdbcUrl darf nicht null sein");
        if (jdbcUrl.isBlank()) {
            throw new IllegalArgumentException("jdbcUrl darf nicht leer sein");
        }
        this.jdbcUrl = jdbcUrl;
    }

    /**
     * Erstellt oder verifiziert das Persistenzschema per Flyway.
     *
     * <p>Erkennt anhand des Datenbankzustands automatisch einen der drei Fälle
     * (leere DB, bestehende DB ohne Flyway-History, Folgestart mit Flyway-History)
     * und wählt die passende Flyway-Konfiguration.
     *
     * @throws DocumentPersistenceException wenn das Schema nicht erstellt oder verifiziert
     *                                      werden kann, oder wenn die Schema-Prüfung bei
     *                                      einer bestehenden Datenbank fehlschlägt
     */
    @Override
    public void initializeSchema() {
        logger.info("Schema-Initialisierung gestartet für: {}", jdbcUrl);
        try {
            DataSource dataSource = createDataSource();
            DbState state = determineDbState(dataSource);
            logger.info("Erkannter Datenbankzustand: {}", state);

            switch (state) {
                case EMPTY -> runFall1NewDb(dataSource);
                case EXISTING_WITHOUT_FLYWAY -> runFall2BaselineExistingDb(dataSource);
                case FLYWAY_MANAGED -> runFall3FollowUpStart(dataSource);
            }

            logger.info("Schema-Initialisierung erfolgreich abgeschlossen.");
        } catch (DocumentPersistenceException e) {
            throw e;
        } catch (Exception e) {
            String msg = "Schema-Initialisierung fehlgeschlagen für '" + jdbcUrl + "': " + e.getMessage();
            logger.error(msg, e);
            throw new DocumentPersistenceException(msg, e);
        }
    }

    /**
     * Gibt die JDBC-URL zurück, die dieser Adapter verwendet.
     *
     * @return die JDBC-URL; niemals {@code null} oder leer
     */
    public String getJdbcUrl() {
        return jdbcUrl;
    }

    // -------------------------------------------------------------------------
    // Fallbehandlung
    // -------------------------------------------------------------------------

    /**
     * Fall 1: Leere Datenbank – Flyway führt V1__initial_schema.sql vollständig aus.
     *
     * @param dataSource die konfigurierte DataSource
     */
    private void runFall1NewDb(DataSource dataSource) {
        logger.info("Fall 1: Leere Datenbank – Flyway legt vollständiges Schema an.");
        Flyway flyway = buildFlyway(dataSource, false);
        flyway.migrate();
        logger.info("Fall 1: Schema vollständig erstellt.");
    }

    /**
     * Fall 2: Bestehende Datenbank ohne Flyway-History.
     *
     * <p>Führt die vollständige Schema-Prüfcheckliste durch. Bei konformem Schema
     * wird ein datiertes Backup angelegt und Flyway trägt nur eine Baseline ein.
     * Bei fehlendem Schema-Element bricht der Start ab.
     *
     * @param dataSource die konfigurierte DataSource
     * @throws DocumentPersistenceException wenn das Schema nicht konform ist oder das Backup schlägt fehl
     */
    private void runFall2BaselineExistingDb(DataSource dataSource) {
        logger.info("Fall 2: Bestehende Datenbank ohne Flyway-History – Schema-Prüfung läuft.");

        // Vollständige Schema-Prüfung vor Baseline
        try (Connection conn = dataSource.getConnection()) {
            verifyExistingSchemaMatches(conn);
        } catch (SQLException e) {
            String msg = "Datenbankverbindung für Schema-Prüfung fehlgeschlagen: " + e.getMessage();
            logger.error(msg, e);
            throw new DocumentPersistenceException(msg, e);
        }
        logger.info("Fall 2: Schema-Prüfung bestanden.");

        // Backup der SQLite-Datei anlegen
        createDatedBackup();

        // Flyway-Baseline eintragen (V1 wird NICHT ausgeführt)
        Flyway flyway = buildFlyway(dataSource, true);
        flyway.migrate();
        logger.info("Fall 2: Flyway-Baseline erfolgreich eingetragen.");
    }

    /**
     * Fall 3: Folgestart – Flyway läuft idempotent und führt nur fehlende Migrationen aus.
     *
     * @param dataSource die konfigurierte DataSource
     */
    private void runFall3FollowUpStart(DataSource dataSource) {
        logger.info("Fall 3: Folgestart mit Flyway-History – idempotente Migration.");
        Flyway flyway = buildFlyway(dataSource, false);
        flyway.migrate();
        logger.info("Fall 3: Migration abgeschlossen (idempotent).");
    }

    /**
     * Erzeugt eine standardisiert konfigurierte {@link Flyway}-Instanz.
     *
     * <p>Alle drei Fälle nutzen dieselbe Grundkonfiguration:
     * <ul>
     *   <li>Explizite Migrations-Location {@code classpath:db/migration} – verhindert
     *       unerwünschtes Klasspfad-Scannen des gesamten JARs.</li>
     *   <li>Keine Umgebungsvariablen-Konfiguration – verhindert unbeabsichtigte
     *       Übersteuerung durch Build-System-Variablen.</li>
     *   <li>Kein Verbindungs-Retry ({@code connectRetries=0}) – Fehler schlagen
     *       sofort statt nach mehreren Sekunden Wartezeit fehl.</li>
     * </ul>
     *
     * @param dataSource       die zu verwendende DataSource
     * @param baselineOnMigrate ob beim Migrate eine Baseline einzutragen ist (nur Fall 2)
     * @return eine konfigurierte, betriebsbereite {@link Flyway}-Instanz
     */
    private Flyway buildFlyway(DataSource dataSource, boolean baselineOnMigrate) {
        var config = Flyway.configure()
                .dataSource(dataSource)
                .locations("classpath:db/migration")
                .connectRetries(0)
                .baselineOnMigrate(baselineOnMigrate);
        if (baselineOnMigrate) {
            config = config
                    .baselineVersion("1")
                    .baselineDescription("Bestehende Datenbank baselined");
        }
        return config.load();
    }

    // -------------------------------------------------------------------------
    // Datenbankzustand erkennen
    // -------------------------------------------------------------------------

    /**
     * Repräsentiert den erkannten Zustand der SQLite-Datenbank beim Start.
     */
    enum DbState {
        /** Keine fachlichen Tabellen und keine Flyway-History vorhanden. */
        EMPTY,
        /** Fachliche Tabellen vorhanden, aber keine Flyway-History-Tabelle. */
        EXISTING_WITHOUT_FLYWAY,
        /** Flyway-History-Tabelle vorhanden – Datenbank wird bereits von Flyway verwaltet. */
        FLYWAY_MANAGED
    }

    /**
     * Ermittelt den aktuellen Zustand der Datenbank.
     *
     * <p>"Leer" bedeutet: keine Tabellen vorhanden – nicht nur Dateigröße 0 Byte.
     *
     * @param dataSource die zu prüfende DataSource
     * @return der erkannte {@link DbState}
     * @throws DocumentPersistenceException bei Verbindungsfehlern
     */
    private DbState determineDbState(DataSource dataSource) {
        try (Connection conn = dataSource.getConnection()) {
            DatabaseMetaData meta = conn.getMetaData();
            Set<String> tables = readTableNames(meta);

            if (tables.contains(FLYWAY_HISTORY_TABLE)) {
                return DbState.FLYWAY_MANAGED;
            }
            // "Leer" = keine Tabellen vorhanden (unabhängig von Dateigröße)
            boolean hasFachlicheTabellen = tables.contains(TABLE_DOCUMENT_RECORD)
                    || tables.contains(TABLE_PROCESSING_ATTEMPT);
            if (hasFachlicheTabellen) {
                return DbState.EXISTING_WITHOUT_FLYWAY;
            }
            return DbState.EMPTY;
        } catch (SQLException e) {
            String msg = "Datenbankzustand konnte nicht ermittelt werden: " + e.getMessage();
            logger.error(msg, e);
            throw new DocumentPersistenceException(msg, e);
        }
    }

    // -------------------------------------------------------------------------
    // Schema-Prüfcheckliste (Fall 2)
    // -------------------------------------------------------------------------

    /**
     * Vollständige Schema-Prüfung gegen das V1-Zielschema.
     *
     * <p>Prüft alle erwarteten Tabellen, Spalten, Constraints und Indizes per
     * {@link DatabaseMetaData}. Bei fehlendem Element wird der Start sofort mit
     * einer aussagekräftigen Fehlermeldung abgebrochen – kein stilles Heilen.
     *
     * @param conn offene JDBC-Verbindung zur Datenbank
     * @throws DocumentPersistenceException wenn ein Schema-Element fehlt
     * @throws SQLException                 bei technischen Datenbankfehlern
     */
    private void verifyExistingSchemaMatches(Connection conn) throws SQLException {
        DatabaseMetaData meta = conn.getMetaData();
        List<String> fehler = new ArrayList<>();

        // Tabellen prüfen
        Set<String> tabellen = readTableNames(meta);
        if (!tabellen.contains(TABLE_DOCUMENT_RECORD)) {
            fehler.add("Tabelle 'document_record' fehlt");
        }
        if (!tabellen.contains(TABLE_PROCESSING_ATTEMPT)) {
            fehler.add("Tabelle 'processing_attempt' fehlt");
        }

        // Spalten prüfen – nur wenn Tabellen vorhanden
        if (tabellen.contains(TABLE_DOCUMENT_RECORD)) {
            pruefeSpaltenvollstaendigkeit(meta, TABLE_DOCUMENT_RECORD,
                    EXPECTED_COLUMNS_DOCUMENT_RECORD, fehler);
        }
        if (tabellen.contains(TABLE_PROCESSING_ATTEMPT)) {
            pruefeSpaltenvollstaendigkeit(meta, TABLE_PROCESSING_ATTEMPT,
                    EXPECTED_COLUMNS_PROCESSING_ATTEMPT, fehler);
        }

        // Indizes prüfen
        if (tabellen.contains(TABLE_DOCUMENT_RECORD) && tabellen.contains(TABLE_PROCESSING_ATTEMPT)) {
            Set<String> vorhandeneIndizes = readIndexNames(meta);
            for (String erwartetIndex : EXPECTED_INDEXES) {
                if (!vorhandeneIndizes.contains(erwartetIndex)) {
                    fehler.add("Index '" + erwartetIndex + "' fehlt");
                }
            }
        }

        // Constraints prüfen (soweit per Metadata prüfbar)
        if (tabellen.contains(TABLE_DOCUMENT_RECORD)) {
            pruefeUniqueConstraintAufFingerprint(conn, fehler);
        }
        if (tabellen.contains(TABLE_PROCESSING_ATTEMPT)) {
            pruefeForeignKeyAufDocumentRecord(conn, fehler);
        }

        if (!fehler.isEmpty()) {
            String fehlerliste = String.join("; ", fehler);
            String msg = "Schema-Prüfung fehlgeschlagen – folgende Elemente fehlen oder sind nicht konform: "
                    + fehlerliste;
            logger.error(msg);
            throw new DocumentPersistenceException(msg);
        }
    }

    /**
     * Prüft, ob alle erwarteten Spalten in der angegebenen Tabelle vorhanden sind.
     *
     * @param meta              Datenbankmetadaten
     * @param tabellenname      Name der zu prüfenden Tabelle
     * @param erwarteteSpalten  Menge der erwarteten Spaltennamen (Kleinschreibung)
     * @param fehler            Liste, in die fehlende Elemente eingetragen werden
     * @throws SQLException bei technischen Datenbankfehlern
     */
    private void pruefeSpaltenvollstaendigkeit(DatabaseMetaData meta, String tabellenname,
            Set<String> erwarteteSpalten, List<String> fehler) throws SQLException {
        Set<String> vorhandeneSpalten = new HashSet<>();
        try (ResultSet rs = meta.getColumns(null, null, tabellenname, null)) {
            while (rs.next()) {
                vorhandeneSpalten.add(rs.getString("COLUMN_NAME").toLowerCase());
            }
        }
        for (String erwartet : erwarteteSpalten) {
            if (!vorhandeneSpalten.contains(erwartet)) {
                fehler.add("Spalte '" + tabellenname + "." + erwartet + "' fehlt");
            }
        }
    }

    /**
     * Prüft das UNIQUE-Constraint auf {@code document_record.fingerprint} anhand der
     * Indexmetadaten.
     *
     * @param conn   offene JDBC-Verbindung
     * @param fehler Liste, in die fehlende Elemente eingetragen werden
     * @throws SQLException bei technischen Datenbankfehlern
     */
    private void pruefeUniqueConstraintAufFingerprint(Connection conn,
            List<String> fehler) throws SQLException {
        boolean uniqueGefunden = false;
        try (ResultSet rs = conn.getMetaData().getIndexInfo(null, null, TABLE_DOCUMENT_RECORD, true, false)) {
            while (rs.next()) {
                String spalte = rs.getString("COLUMN_NAME");
                if (COL_FINGERPRINT.equalsIgnoreCase(spalte)) {
                    uniqueGefunden = true;
                    break;
                }
            }
        }
        if (!uniqueGefunden) {
            fehler.add("UNIQUE-Constraint auf 'document_record.fingerprint' fehlt");
        }
    }

    /**
     * Prüft den Foreign Key von {@code processing_attempt.fingerprint} auf
     * {@code document_record.fingerprint} anhand der Importschlüssel-Metadaten.
     *
     * @param conn   offene JDBC-Verbindung
     * @param fehler Liste, in die fehlende Elemente eingetragen werden
     * @throws SQLException bei technischen Datenbankfehlern
     */
    private void pruefeForeignKeyAufDocumentRecord(Connection conn,
            List<String> fehler) throws SQLException {
        boolean fkGefunden = false;
        try (ResultSet rs = conn.getMetaData().getImportedKeys(null, null, TABLE_PROCESSING_ATTEMPT)) {
            while (rs.next()) {
                String pkTabelle = rs.getString("PKTABLE_NAME");
                String fkSpalte = rs.getString("FKCOLUMN_NAME");
                if (TABLE_DOCUMENT_RECORD.equalsIgnoreCase(pkTabelle)
                        && COL_FINGERPRINT.equalsIgnoreCase(fkSpalte)) {
                    fkGefunden = true;
                    break;
                }
            }
        }
        if (!fkGefunden) {
            fehler.add("Foreign Key von 'processing_attempt.fingerprint' auf 'document_record.fingerprint' fehlt");
        }
    }

    // -------------------------------------------------------------------------
    // Backup-Erstellung (Fall 2)
    // -------------------------------------------------------------------------

    /**
     * Erstellt eine datierte Kopie der SQLite-Datei als Backup.
     *
     * <p>Das Backup-Dateiname-Schema lautet: {@code <original>.<timestamp>.bak},
     * z. B. {@code data.db.20260430T120000Z.bak}.
     * Bei einer Kollision wird ein Zähler angehängt.
     *
     * @throws DocumentPersistenceException wenn das Backup nicht angelegt werden kann
     */
    private void createDatedBackup() {
        Path dbPath = extractDbPath();
        if (dbPath == null) {
            logger.warn("Kein lokaler Dateipfad aus JDBC-URL ableitbar – Backup übersprungen: {}", jdbcUrl);
            return;
        }
        if (!Files.exists(dbPath)) {
            logger.debug("Datenbankdatei existiert noch nicht – kein Backup nötig.");
            return;
        }

        String zeitstempel = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'")
                .format(java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC));
        Path backup = dbPath.resolveSibling(dbPath.getFileName() + "." + zeitstempel + ".bak");

        // Kollisionsauflösung
        int zaehler = 1;
        while (Files.exists(backup)) {
            backup = dbPath.resolveSibling(dbPath.getFileName() + "." + zeitstempel + "." + zaehler + ".bak");
            zaehler++;
        }

        try {
            Files.copy(dbPath, backup, StandardCopyOption.COPY_ATTRIBUTES);
            logger.info("Backup der Datenbankdatei erstellt: {}", backup);
        } catch (IOException e) {
            String msg = "Backup der Datenbankdatei konnte nicht erstellt werden: " + e.getMessage();
            logger.error(msg, e);
            throw new DocumentPersistenceException(msg, e);
        }
    }

    /**
     * Leitet den Dateisystempfad aus der JDBC-URL ab.
     *
     * <p>Erwartet URLs der Form {@code jdbc:sqlite:/pfad/zur/datei.db}.
     *
     * @return der abgeleitete {@link Path} oder {@code null}, wenn kein Pfad ableitbar ist
     */
    private Path extractDbPath() {
        // Erwartet: jdbc:sqlite:/pfad/zur/datei oder jdbc:sqlite:C:/pfad/datei
        String prefix = "jdbc:sqlite:";
        if (!jdbcUrl.startsWith(prefix)) {
            return null;
        }
        String pfad = jdbcUrl.substring(prefix.length());
        if (pfad.isBlank()) {
            return null;
        }
        try {
            return Paths.get(pfad);
        } catch (Exception e) {
            logger.warn("Pfad aus JDBC-URL konnte nicht geparst werden: {}", pfad);
            return null;
        }
    }

    // -------------------------------------------------------------------------
    // DataSource-Erstellung
    // -------------------------------------------------------------------------

    /**
     * Erstellt eine {@link SQLiteDataSource} mit aktivierten Fremdschlüsseln.
     *
     * <p>Die Aktivierung über {@link SQLiteConfig#enforceForeignKeys(boolean)} stellt
     * sicher, dass jede neue Verbindung automatisch {@code PRAGMA foreign_keys = ON}
     * erhält – ein einmaliges Statement nach dem Verbindungsaufbau wäre nicht ausreichend.
     *
     * @return eine konfigurierte {@link DataSource}; niemals {@code null}
     */
    private DataSource createDataSource() {
        SQLiteConfig config = new SQLiteConfig();
        config.enforceForeignKeys(true);
        SQLiteDataSource ds = new SQLiteDataSource(config);
        ds.setUrl(jdbcUrl);
        return ds;
    }

    // -------------------------------------------------------------------------
    // Hilfsmethoden
    // -------------------------------------------------------------------------

    /**
     * Liest alle Tabellennamen aus den Datenbankmetadaten (Kleinschreibung).
     *
     * @param meta Datenbankmetadaten
     * @return Menge aller Tabellennamen in Kleinschreibung
     * @throws SQLException bei technischen Datenbankfehlern
     */
    private static Set<String> readTableNames(DatabaseMetaData meta) throws SQLException {
        Set<String> names = new HashSet<>();
        try (ResultSet rs = meta.getTables(null, null, "%", new String[]{"TABLE"})) {
            while (rs.next()) {
                names.add(rs.getString("TABLE_NAME").toLowerCase());
            }
        }
        return names;
    }

    /**
     * Liest alle Indexnamen aus den Datenbankmetadaten für beide fachlichen Tabellen.
     *
     * @param meta Datenbankmetadaten
     * @return Menge aller Indexnamen in Kleinschreibung
     * @throws SQLException bei technischen Datenbankfehlern
     */
    private static Set<String> readIndexNames(DatabaseMetaData meta) throws SQLException {
        Set<String> names = new HashSet<>();
        for (String tabelle : new String[]{TABLE_DOCUMENT_RECORD, TABLE_PROCESSING_ATTEMPT}) {
            try (ResultSet rs = meta.getIndexInfo(null, null, tabelle, false, false)) {
                while (rs.next()) {
                    String indexName = rs.getString("INDEX_NAME");
                    if (indexName != null) {
                        names.add(indexName.toLowerCase());
                    }
                }
            }
        }
        return names;
    }
}
