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

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Objects;
import java.util.function.Consumer;

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

import de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt;
import de.gecheckt.pdf.umbenenner.application.port.out.UnitOfWorkPort;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;

/**
 * SQLite implementation of {@link UnitOfWorkPort}.
 * <p>
 * Provides transactional semantics for coordinated writes to both the document record
 * and processing attempt repositories.
 */
public class SqliteUnitOfWorkAdapter implements UnitOfWorkPort {
    private static final String ROLLBACK_FAILED_MSG = "Rollback fehlgeschlagen: {}";


    private static final Logger logger = LogManager.getLogger(SqliteUnitOfWorkAdapter.class);
    
    private final String jdbcUrl;

    public SqliteUnitOfWorkAdapter(String jdbcUrl) {
        Objects.requireNonNull(jdbcUrl, "jdbcUrl must not be null");
        if (jdbcUrl.isBlank()) {
            throw new IllegalArgumentException("jdbcUrl must not be blank");
        }
        this.jdbcUrl = jdbcUrl;
    }

    @Override
    public void executeInTransaction(Consumer<TransactionOperations> operations) {
        Objects.requireNonNull(operations, "operations must not be null");

        try (Connection connection = SqliteConnectionFactory.open(jdbcUrl)) {
            connection.setAutoCommit(false);

            try {
                TransactionOperationsImpl txOps = new TransactionOperationsImpl(connection);
                operations.accept(txOps);

                connection.commit();
                logger.debug("Transaktion erfolgreich abgeschlossen.");

            } catch (DocumentPersistenceException e) {
                // Datenbankfehler auf Dokumentebene: Rollback, dann weiterpropagieren
                try {
                    connection.rollback();
                    logger.debug("Transaktion zurückgerollt (Dokumentfehler): {}", e.getMessage());
                } catch (SQLException rollbackEx) {
                    logger.error(ROLLBACK_FAILED_MSG, rollbackEx.getMessage(), rollbackEx);
                }
                throw e;
            } catch (RuntimeException e) {
                // Unerwarteter Laufzeitfehler: Rollback, dann als Persistenzfehler weitergeben
                try {
                    connection.rollback();
                    logger.debug("Transaktion zurückgerollt (Laufzeitfehler): {}", e.getMessage());
                } catch (SQLException rollbackEx) {
                    logger.error(ROLLBACK_FAILED_MSG, rollbackEx.getMessage(), rollbackEx);
                }
                throw new DocumentPersistenceException("Transaktion fehlgeschlagen: " + e.getMessage(), e);
            } catch (SQLException e) {
                // SQL-Fehler innerhalb der Transaktion: Rollback, dann als Persistenzfehler weitergeben
                try {
                    connection.rollback();
                    logger.debug("Transaktion zurückgerollt (SQL-Fehler): {}", e.getMessage());
                } catch (SQLException rollbackEx) {
                    logger.error(ROLLBACK_FAILED_MSG, rollbackEx.getMessage(), rollbackEx);
                }
                throw new DocumentPersistenceException("Transaktion fehlgeschlagen: " + e.getMessage(), e);
            }

        } catch (DocumentPersistenceException e) {
            throw e;
        } catch (SQLException e) {
            // Verbindungsaufbau oder setAutoCommit(false) fehlgeschlagen
            throw new DocumentPersistenceException(
                    "Datenbankverbindung konnte nicht hergestellt werden: " + e.getMessage(), e);
        }
    }
    
    /**
     * Wraps a shared transaction connection so that {@code close()} becomes a no-op.
     * <p>
     * Repository adapters manage their own connection lifecycle via try-with-resources,
     * which would close the shared transaction connection prematurely if not wrapped.
     * All other {@link Connection} methods are delegated unchanged to the underlying connection.
     *
     * @param underlying the real shared connection; must not be null
     * @return a proxy connection that ignores {@code close()} calls
     */
    private static Connection nonClosingWrapper(Connection underlying) {
        return (Connection) Proxy.newProxyInstance(
                Connection.class.getClassLoader(),
                new Class<?>[] { Connection.class },
                (proxy, method, args) -> {
                    if ("close".equals(method.getName())) {
                        return null;
                    }
                    try {
                        return method.invoke(underlying, args);
                    } catch (InvocationTargetException e) {
                        throw e.getCause();
                    }
                });
    }

    private class TransactionOperationsImpl implements TransactionOperations {
        private final Connection connection;

        TransactionOperationsImpl(Connection connection) {
            this.connection = connection;
        }

        @Override
        public void saveProcessingAttempt(ProcessingAttempt attempt) {
            SqliteProcessingAttemptRepositoryAdapter repo =
                new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl) {
                    @Override
                    protected Connection getConnection() throws SQLException {
                        return nonClosingWrapper(connection);
                    }
                };
            repo.save(attempt);
        }

        @Override
        public void createDocumentRecord(DocumentRecord record) {
            SqliteDocumentRecordRepositoryAdapter repo =
                new SqliteDocumentRecordRepositoryAdapter(jdbcUrl) {
                    @Override
                    protected Connection getConnection() throws SQLException {
                        return nonClosingWrapper(connection);
                    }
                };
            repo.create(record);
        }

        @Override
        public void updateDocumentRecord(DocumentRecord record) {
            SqliteDocumentRecordRepositoryAdapter repo =
                new SqliteDocumentRecordRepositoryAdapter(jdbcUrl) {
                    @Override
                    protected Connection getConnection() throws SQLException {
                        return nonClosingWrapper(connection);
                    }
                };
            repo.update(record);
        }

        /**
         * Deletes all attempt history entries and the document master record for the
         * given fingerprint within the current transaction.
         * <p>
         * Attempts are deleted first to satisfy the foreign-key constraint between
         * {@code processing_attempt} and {@code document_record}. Both deletes are
         * idempotent: missing rows are silently ignored.
         *
         * @param fingerprint the document identity to fully reset; must not be null
         * @throws DocumentPersistenceException if either delete fails
         */
        @Override
        public void resetDocumentByFingerprint(DocumentFingerprint fingerprint) {
            // Zuerst Versuche löschen (FK-Constraint: processing_attempt → document_record)
            SqliteProcessingAttemptRepositoryAdapter attemptRepo =
                new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl) {
                    @Override
                    protected Connection getConnection() throws SQLException {
                        return nonClosingWrapper(connection);
                    }
                };
            attemptRepo.deleteAllByFingerprint(fingerprint);

            // Dann den Stammsatz löschen
            SqliteDocumentRecordRepositoryAdapter recordRepo =
                new SqliteDocumentRecordRepositoryAdapter(jdbcUrl) {
                    @Override
                    protected Connection getConnection() throws SQLException {
                        return nonClosingWrapper(connection);
                    }
                };
            recordRepo.deleteByFingerprint(fingerprint);
        }

        /**
         * Setzt ausschließlich die vier fachlich relevanten Status-Felder zurück,
         * ohne die Versuchshistorie zu löschen.
         * <p>
         * Die Felder {@code overall_status}, {@code content_error_count},
         * {@code transient_error_count} und {@code last_failure_instant} werden innerhalb
         * der laufenden Transaktion per direktem SQL-UPDATE aktualisiert.
         * Alle anderen Felder sowie alle {@code processing_attempt}-Einträge bleiben unverändert.
         * <p>
         * Ist kein Stammsatz für den Fingerprint vorhanden, kehrt die Methode stillschweigend zurück.
         *
         * @param fingerprint der Dokumentbezeichner, dessen Status zurückgesetzt werden soll;
         *                    darf nicht {@code null} sein
         * @throws DocumentPersistenceException bei technischen Datenbankfehlern
         */
        @Override
        public void resetDocumentStatusForRetry(DocumentFingerprint fingerprint) {
            Objects.requireNonNull(fingerprint, "fingerprint darf nicht null sein");

            String sql = """
                    UPDATE document_record SET
                        overall_status        = 'READY_FOR_AI',
                        content_error_count   = 0,
                        transient_error_count = 0,
                        last_failure_instant  = NULL
                    WHERE fingerprint = ?
                    """;

            try (java.sql.PreparedStatement stmt = connection.prepareStatement(sql)) {
                stmt.setString(1, fingerprint.sha256Hex());
                stmt.executeUpdate();
                logger.debug("Status-Reset (feldgenau) für Fingerprint: {}", fingerprint.sha256Hex());
            } catch (java.sql.SQLException e) {
                String message = "Status-Reset fehlgeschlagen für Fingerprint '"
                        + fingerprint.sha256Hex() + "': " + e.getMessage();
                logger.error(message, e);
                throw new DocumentPersistenceException(message, e);
            }
        }
    }
}