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

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Objects;

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

import de.gecheckt.pdf.umbenenner.application.port.out.DocumentKnownProcessable;
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.DocumentRecordLookupResult;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordRepository;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalFinalFailure;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalSuccess;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentUnknown;
import de.gecheckt.pdf.umbenenner.application.port.out.FailureCounters;
import de.gecheckt.pdf.umbenenner.application.port.out.PersistenceLookupTechnicalFailure;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentLocator;

/**
 * SQLite implementation of {@link DocumentRecordRepository}.
 * <p>
 * Provides CRUD operations for the document master record (Dokument-Stammsatz)
 * with explicit mapping between application types and the SQLite schema.
 * <p>
 * <strong>Architecture boundary:</strong> All JDBC and SQLite details are strictly
 * confined to this class. No JDBC types appear in the port interface or in any
 * application/domain type.
 */
public class SqliteDocumentRecordRepositoryAdapter implements DocumentRecordRepository {

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

    private final String jdbcUrl;

    /**
     * Constructs the adapter with the JDBC URL of the SQLite database file.
     *
     * @param jdbcUrl the JDBC URL of the SQLite database; must not be null or blank
     * @throws NullPointerException     if {@code jdbcUrl} is null
     * @throws IllegalArgumentException if {@code jdbcUrl} is blank
     */
    public SqliteDocumentRecordRepositoryAdapter(String jdbcUrl) {
        Objects.requireNonNull(jdbcUrl, "jdbcUrl must not be null");
        if (jdbcUrl.isBlank()) {
            throw new IllegalArgumentException("jdbcUrl must not be blank");
        }
        this.jdbcUrl = jdbcUrl;
    }

    /**
     * Looks up the master record for the given fingerprint.
     * <p>
     * Returns a {@link DocumentRecordLookupResult} that encodes all possible outcomes
     * including technical failures; this method never throws.
     *
     * @param fingerprint the content-based document identity to look up; must not be null
     * @return {@link DocumentUnknown} if no record exists,
     *         {@link DocumentKnownProcessable} if the document is known but not terminal,
     *         {@link DocumentTerminalSuccess} if the document succeeded,
     *         {@link DocumentTerminalFinalFailure} if the document finally failed, or
     *         {@link PersistenceLookupTechnicalFailure} if the lookup itself failed
     */
    @Override
    public DocumentRecordLookupResult findByFingerprint(DocumentFingerprint fingerprint) {
        if (fingerprint == null) {
            throw new NullPointerException("fingerprint must not be null");
        }

        String sql = """
                SELECT
                    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
                FROM document_record
                WHERE fingerprint = ?
                """;

        try (Connection connection = getConnection();
             PreparedStatement statement = connection.prepareStatement(sql)) {

            statement.setString(1, fingerprint.sha256Hex());

            try (ResultSet rs = statement.executeQuery()) {
                if (rs.next()) {
                    // Document exists - map to appropriate result type based on status
                    DocumentRecord record = mapResultSetToDocumentRecord(rs, fingerprint);
                    DocumentRecordLookupResult result = switch (record.overallStatus()) {
                        case SUCCESS -> new DocumentTerminalSuccess(record);
                        case FAILED_FINAL -> new DocumentTerminalFinalFailure(record);
                        case READY_FOR_AI, PROPOSAL_READY, PROCESSING, FAILED_RETRYABLE,
                             SKIPPED_ALREADY_PROCESSED, SKIPPED_FINAL_FAILURE ->
                                new DocumentKnownProcessable(record);
                    };
                    return result;
                } else {
                    return new DocumentUnknown();
                }
            }

        } catch (SQLException e) {
            String message = "Failed to lookup document record for fingerprint '" + 
                           fingerprint.sha256Hex() + "': " + e.getMessage();
            logger.error(message, e);
            return new PersistenceLookupTechnicalFailure(message, e);
        }
    }

    /**
     * Persists a new master record for a previously unknown document.
     * <p>
     * The fingerprint within {@code record} must not yet exist in the persistence store.
     *
     * @param record the new master record to persist; must not be null
     * @throws DocumentPersistenceException if the insert fails due to a technical error
     */
    @Override
    public void create(DocumentRecord record) {
        if (record == null) {
            throw new NullPointerException("record must not be null");
        }

        String sql = """
                INSERT INTO document_record (
                    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
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """;

        try (Connection connection = getConnection();
             PreparedStatement statement = connection.prepareStatement(sql)) {

            statement.setString(1, record.fingerprint().sha256Hex());
            statement.setString(2, record.lastKnownSourceLocator().value());
            statement.setString(3, record.lastKnownSourceFileName());
            statement.setString(4, record.overallStatus().name());
            statement.setInt(5, record.failureCounters().contentErrorCount());
            statement.setInt(6, record.failureCounters().transientErrorCount());
            statement.setString(7, instantToString(record.lastFailureInstant()));
            statement.setString(8, instantToString(record.lastSuccessInstant()));
            statement.setString(9, instantToString(record.createdAt()));
            statement.setString(10, instantToString(record.updatedAt()));
            statement.setString(11, record.lastTargetPath());
            statement.setString(12, record.lastTargetFileName());

            int rowsAffected = statement.executeUpdate();
            if (rowsAffected != 1) {
                throw new DocumentPersistenceException(
                        "Expected to insert 1 row but affected " + rowsAffected + " rows");
            }

            logger.debug("Created document record for fingerprint: {}", record.fingerprint().sha256Hex());

        } catch (SQLException e) {
            String message = "Failed to create document record for fingerprint '" +
                           record.fingerprint().sha256Hex() + "': " + e.getMessage();
            logger.error(message, e);
            throw new DocumentPersistenceException(message, e);
        }
    }

    /**
     * Updates the mutable fields of an existing master record.
     * <p>
     * The record is identified by its {@link DocumentFingerprint}; the fingerprint
     * itself is never changed. Mutable fields include the overall status, failure
     * counters, last known source location, and all timestamp fields.
     *
     * @param record the updated master record; must not be null; fingerprint must exist
     * @throws DocumentPersistenceException if the update fails due to a technical error
     */
    @Override
    public void update(DocumentRecord record) {
        if (record == null) {
            throw new NullPointerException("record must not be null");
        }

        String sql = """
                UPDATE document_record SET
                    last_known_source_locator = ?,
                    last_known_source_file_name = ?,
                    overall_status = ?,
                    content_error_count = ?,
                    transient_error_count = ?,
                    last_failure_instant = ?,
                    last_success_instant = ?,
                    updated_at = ?,
                    last_target_path = ?,
                    last_target_file_name = ?
                WHERE fingerprint = ?
                """;

        try (Connection connection = getConnection();
             PreparedStatement statement = connection.prepareStatement(sql)) {

            statement.setString(1, record.lastKnownSourceLocator().value());
            statement.setString(2, record.lastKnownSourceFileName());
            statement.setString(3, record.overallStatus().name());
            statement.setInt(4, record.failureCounters().contentErrorCount());
            statement.setInt(5, record.failureCounters().transientErrorCount());
            statement.setString(6, instantToString(record.lastFailureInstant()));
            statement.setString(7, instantToString(record.lastSuccessInstant()));
            statement.setString(8, instantToString(record.updatedAt()));
            statement.setString(9, record.lastTargetPath());
            statement.setString(10, record.lastTargetFileName());
            statement.setString(11, record.fingerprint().sha256Hex());

            int rowsAffected = statement.executeUpdate();
            if (rowsAffected != 1) {
                throw new DocumentPersistenceException(
                        "Expected to update 1 row but affected " + rowsAffected + " rows. " +
                        "Fingerprint may not exist: " + record.fingerprint().sha256Hex());
            }

            logger.debug("Updated document record for fingerprint: {}", record.fingerprint().sha256Hex());

        } catch (SQLException e) {
            String message = "Failed to update document record for fingerprint '" +
                           record.fingerprint().sha256Hex() + "': " + e.getMessage();
            logger.error(message, e);
            throw new DocumentPersistenceException(message, e);
        }
    }

    /**
     * Maps a ResultSet row to a DocumentRecord.
     *
     * @param rs the ResultSet positioned at the current row
     * @param fingerprint the fingerprint for this record
     * @return the mapped DocumentRecord
     * @throws SQLException if reading from the ResultSet fails
     */
    private DocumentRecord mapResultSetToDocumentRecord(ResultSet rs, DocumentFingerprint fingerprint) throws SQLException {
        return new DocumentRecord(
                fingerprint,
                new SourceDocumentLocator(rs.getString("last_known_source_locator")),
                rs.getString("last_known_source_file_name"),
                ProcessingStatus.valueOf(rs.getString("overall_status")),
                new FailureCounters(
                        rs.getInt("content_error_count"),
                        rs.getInt("transient_error_count")
                ),
                stringToInstant(rs.getString("last_failure_instant")),
                stringToInstant(rs.getString("last_success_instant")),
                stringToInstant(rs.getString("created_at")),
                stringToInstant(rs.getString("updated_at")),
                rs.getString("last_target_path"),
                rs.getString("last_target_file_name")
        );
    }

    /**
     * Converts an Instant to a string representation for storage.
     *
     * @param instant the instant to convert, may be null
     * @return the ISO-8601 string representation, or null if instant is null
     */
    private String instantToString(Instant instant) {
        return instant != null ? instant.toString() : null;
    }

    /**
     * Konvertiert eine String-Darstellung zurück zu einem Instant.
     * <p>
     * Unterstützt zwei Datumsformate für Rückwärtskompatibilität:
     * <ul>
     * <li>ISO-8601 (neu): "2026-04-27T11:32:25Z"</li>
     * <li>Legacy-Format (älter): "2026-04-27 11:32:25" (wird als UTC interpretiert)</li>
     * </ul>
     *
     * @param stringValue die String-Darstellung des Datums, kann null sein
     * @return das geparste Instant, oder null wenn stringValue null oder leer ist
     */
    private Instant stringToInstant(String stringValue) {
        if (stringValue == null || stringValue.isBlank()) {
            return null;
        }

        // Versuch mit ISO-8601 Format (moderner Standard)
        try {
            return Instant.parse(stringValue);
        } catch (Exception e) {
            // Fallback auf älteres Format "yyyy-MM-dd HH:mm:ss" (als UTC)
            try {
                LocalDateTime dateTime = LocalDateTime.parse(stringValue,
                        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
                return dateTime.atZone(ZoneId.of("UTC")).toInstant();
            } catch (Exception fallbackException) {
                logger.warn("Fehler beim Parsen der Instant-String '{}' in beiden Formaten (ISO-8601 und Legacy-Format)",
                        stringValue, fallbackException);
                return null;
            }
        }
    }

    /**
     * Deletes the master record for the given fingerprint.
     * <p>
     * Idempotent: if no record with the given fingerprint exists the method returns
     * without error. A {@link DocumentPersistenceException} is thrown only on technical
     * database failures.
     *
     * @param fingerprint the document identity whose master record should be removed;
     *                    must not be null
     * @throws DocumentPersistenceException if the delete fails due to a technical error
     */
    @Override
    public void deleteByFingerprint(DocumentFingerprint fingerprint) {
        Objects.requireNonNull(fingerprint, "fingerprint must not be null");

        String sql = "DELETE FROM document_record WHERE fingerprint = ?";

        try (Connection connection = getConnection();
             PreparedStatement statement = connection.prepareStatement(sql)) {

            statement.setString(1, fingerprint.sha256Hex());
            statement.executeUpdate();

        } catch (SQLException e) {
            String message = "Failed to delete document record for fingerprint '"
                    + fingerprint.sha256Hex() + "': " + e.getMessage();
            logger.error(message, e);
            throw new DocumentPersistenceException(message, e);
        }
    }

    /**
     * Returns the JDBC URL this adapter uses to connect to the SQLite database.
     * <p>
     * Intended for logging and diagnostics only.
     *
     * @return the JDBC URL; never null or blank
     */
    public String getJdbcUrl() {
        return jdbcUrl;
    }
    
    /**
     * Gets a connection to the database.
     * <p>
     * This method can be overridden by subclasses to provide a shared connection.
     *
     * @return a new database connection
     * @throws SQLException if the connection cannot be established
     */
    protected Connection getConnection() throws SQLException {
        return SqliteConnectionFactory.open(jdbcUrl);
    }
}