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.DriverManager;
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 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");

        Connection connection = null;
        try {
            connection = DriverManager.getConnection(jdbcUrl);
            connection.setAutoCommit(false);

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

            connection.commit();
            logger.debug("Transaction committed successfully");

        } catch (DocumentPersistenceException e) {
            // Re-throw document-level persistence errors as-is, but still rollback
            if (connection != null) {
                try {
                    connection.rollback();
                    logger.debug("Transaction rolled back due to document error: {}", e.getMessage());
                } catch (SQLException rollbackEx) {
                    logger.error("Failed to rollback transaction: {}", rollbackEx.getMessage(), rollbackEx);
                }
            }
            throw e;
        } catch (RuntimeException e) {
            // Rollback on any RuntimeException and wrap in DocumentPersistenceException
            if (connection != null) {
                try {
                    connection.rollback();
                    logger.debug("Transaction rolled back due to error: {}", e.getMessage());
                } catch (SQLException rollbackEx) {
                    logger.error("Failed to rollback transaction: {}", rollbackEx.getMessage(), rollbackEx);
                }
            }
            throw new DocumentPersistenceException("Transaction failed: " + e.getMessage(), e);
        } catch (SQLException e) {
            // Rollback for any SQL error
            if (connection != null) {
                try {
                    connection.rollback();
                    logger.debug("Transaction rolled back due to error: {}", e.getMessage());
                } catch (SQLException rollbackEx) {
                    logger.error("Failed to rollback transaction: {}", rollbackEx.getMessage(), rollbackEx);
                }
            }
            throw new DocumentPersistenceException("Transaction failed: " + e.getMessage(), e);
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    logger.warn("Failed to close connection: {}", 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) {
            // Delete attempts first (FK constraint: processing_attempt → document_record)
            SqliteProcessingAttemptRepositoryAdapter attemptRepo =
                new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl) {
                    @Override
                    protected Connection getConnection() throws SQLException {
                        return nonClosingWrapper(connection);
                    }
                };
            attemptRepo.deleteAllByFingerprint(fingerprint);

            // Then delete the master record
            SqliteDocumentRecordRepositoryAdapter recordRepo =
                new SqliteDocumentRecordRepositoryAdapter(jdbcUrl) {
                    @Override
                    protected Connection getConnection() throws SQLException {
                        return nonClosingWrapper(connection);
                    }
                };
            recordRepo.deleteByFingerprint(fingerprint);
        }
    }
}