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

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Objects;

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.PersistenceSchemaInitializationPort;

/**
 * SQLite implementation of {@link PersistenceSchemaInitializationPort}.
 * <p>
 * Creates or verifies the two-level persistence schema in the configured SQLite
 * database file, and performs a controlled schema evolution from an earlier schema
 * version to the current one.
 *
 * <h2>Two-level schema</h2>
 * <p>The schema consists of exactly two tables:
 * <ol>
 *   <li><strong>{@code document_record}</strong> — the document master record
 *       (Dokument-Stammsatz). One row per unique SHA-256 fingerprint.</li>
 *   <li><strong>{@code processing_attempt}</strong> — the processing attempt history
 *       (Versuchshistorie). One row per historised processing attempt, referencing
 *       the master record via fingerprint.</li>
 * </ol>
 *
 * <h2>Schema evolution</h2>
 * <p>
 * When upgrading from an earlier schema, this adapter uses idempotent
 * {@code ALTER TABLE ... ADD COLUMN} statements for both tables. Columns that already
 * exist are silently skipped, making the evolution safe to run on both fresh and existing
 * databases. The current evolution adds:
 * <ul>
 *   <li>AI-traceability columns to {@code processing_attempt}</li>
 *   <li>Target-copy columns ({@code last_target_path}, {@code last_target_file_name}) to
 *       {@code document_record}</li>
 *   <li>Target-copy column ({@code final_target_file_name}) to {@code processing_attempt}</li>
 *   <li>Provider-identifier column ({@code ai_provider}) to {@code processing_attempt};
 *       existing rows receive {@code NULL} as the default, which is the correct value for
 *       attempts recorded before provider tracking was introduced.</li>
 * </ul>
 *
 * <h2>Legacy-state migration</h2>
 * <p>
 * Documents in an earlier positive intermediate state ({@code SUCCESS} recorded without
 * a validated naming proposal) are idempotently migrated to {@code READY_FOR_AI} so that
 * the AI naming pipeline processes them in the next run. Terminal negative states
 * ({@code FAILED_RETRYABLE}, {@code FAILED_FINAL}, skip states) are left unchanged.
 *
 * <h2>Initialisation timing</h2>
 * <p>This adapter must be invoked <em>once</em> at program startup, before the batch
 * document processing loop begins.
 *
 * <h2>Architecture boundary</h2>
 * <p>All JDBC connections, SQL DDL, and SQLite-specific behaviour are strictly confined
 * to this class. No JDBC or SQLite types appear in the port interface or in any
 * application/domain type.
 */
public class SqliteSchemaInitializationAdapter implements PersistenceSchemaInitializationPort {

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

    // -------------------------------------------------------------------------
    // DDL — document_record table
    // -------------------------------------------------------------------------

    /**
     * DDL for the document master record table.
     * <p>
     * Columns: id (PK), fingerprint (unique), 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.
     */
    private static final String DDL_CREATE_DOCUMENT_RECORD = """
            CREATE TABLE IF NOT EXISTS document_record (
                id                          INTEGER PRIMARY KEY AUTOINCREMENT,
                fingerprint                 TEXT    NOT NULL,
                last_known_source_locator   TEXT    NOT NULL,
                last_known_source_file_name TEXT    NOT NULL,
                overall_status              TEXT    NOT NULL,
                content_error_count         INTEGER NOT NULL DEFAULT 0,
                transient_error_count       INTEGER NOT NULL DEFAULT 0,
                last_failure_instant        TEXT,
                last_success_instant        TEXT,
                created_at                  TEXT    NOT NULL,
                updated_at                  TEXT    NOT NULL,
                CONSTRAINT uq_document_record_fingerprint UNIQUE (fingerprint)
            )
            """;

    // -------------------------------------------------------------------------
    // DDL — processing_attempt table (base schema, without AI traceability cols)
    // -------------------------------------------------------------------------

    /**
     * DDL for the base processing attempt history table.
     * <p>
     * Base columns (present in all schema versions): id, fingerprint, run_id,
     * attempt_number, started_at, ended_at, status, failure_class, failure_message, retryable.
     * <p>
     * AI traceability columns are added separately via {@code ALTER TABLE} to support
     * idempotent evolution from earlier schemas.
     */
    private static final String DDL_CREATE_PROCESSING_ATTEMPT = """
            CREATE TABLE IF NOT EXISTS processing_attempt (
                id              INTEGER PRIMARY KEY AUTOINCREMENT,
                fingerprint     TEXT    NOT NULL,
                run_id          TEXT    NOT NULL,
                attempt_number  INTEGER NOT NULL,
                started_at      TEXT    NOT NULL,
                ended_at        TEXT    NOT NULL,
                status          TEXT    NOT NULL,
                failure_class   TEXT,
                failure_message TEXT,
                retryable       INTEGER NOT NULL DEFAULT 0,
                CONSTRAINT fk_processing_attempt_fingerprint
                    FOREIGN KEY (fingerprint) REFERENCES document_record (fingerprint),
                CONSTRAINT uq_processing_attempt_fingerprint_number
                    UNIQUE (fingerprint, attempt_number)
            )
            """;

    // -------------------------------------------------------------------------
    // DDL — indexes
    // -------------------------------------------------------------------------

    /** Index on {@code processing_attempt.fingerprint} for fast per-document lookups. */
    private static final String DDL_IDX_ATTEMPT_FINGERPRINT =
            "CREATE INDEX IF NOT EXISTS idx_processing_attempt_fingerprint "
            + "ON processing_attempt (fingerprint)";

    /** Index on {@code processing_attempt.run_id} for fast per-run lookups. */
    private static final String DDL_IDX_ATTEMPT_RUN_ID =
            "CREATE INDEX IF NOT EXISTS idx_processing_attempt_run_id "
            + "ON processing_attempt (run_id)";

    /** Index on {@code document_record.overall_status} for fast status-based filtering. */
    private static final String DDL_IDX_RECORD_STATUS =
            "CREATE INDEX IF NOT EXISTS idx_document_record_overall_status "
            + "ON document_record (overall_status)";

    // -------------------------------------------------------------------------
    // DDL — columns added to processing_attempt via schema evolution
    // -------------------------------------------------------------------------

    /**
     * Columns to add idempotently to {@code processing_attempt}.
     * Each entry is {@code [column_name, column_type]}.
     * <p>
     * {@code ai_provider} is nullable; existing rows receive {@code NULL}, which is the
     * correct sentinel for attempts recorded before provider tracking was introduced.
     */
    private static final String[][] EVOLUTION_ATTEMPT_COLUMNS = {
            {"model_name",             "TEXT"},
            {"prompt_identifier",      "TEXT"},
            {"processed_page_count",   "INTEGER"},
            {"sent_character_count",   "INTEGER"},
            {"ai_raw_response",        "TEXT"},
            {"ai_reasoning",           "TEXT"},
            {"resolved_date",          "TEXT"},
            {"date_source",            "TEXT"},
            {"validated_title",        "TEXT"},
            {"final_target_file_name", "TEXT"},
            {"ai_provider",            "TEXT"},
    };

    // -------------------------------------------------------------------------
    // DDL — columns added to document_record via schema evolution
    // -------------------------------------------------------------------------

    /**
     * Columns to add idempotently to {@code document_record}.
     * Each entry is {@code [column_name, column_type]}.
     */
    private static final String[][] EVOLUTION_RECORD_COLUMNS = {
            {"last_target_path",      "TEXT"},
            {"last_target_file_name", "TEXT"},
    };

    // -------------------------------------------------------------------------
    // Legacy-state status migration
    // -------------------------------------------------------------------------

    /**
     * Migrates earlier positive intermediate states in {@code document_record} that were
     * recorded as {@code SUCCESS} without a validated naming proposal to {@code READY_FOR_AI},
     * so the AI naming pipeline processes them in the next run.
     * <p>
     * Only rows with {@code overall_status = 'SUCCESS'} that have no corresponding
     * {@code processing_attempt} with {@code status = 'PROPOSAL_READY'} are updated.
     * This migration is idempotent.
     */
    private static final String SQL_MIGRATE_LEGACY_SUCCESS_TO_READY_FOR_AI = """
            UPDATE document_record
            SET overall_status = 'READY_FOR_AI',
                updated_at = datetime('now')
            WHERE overall_status = 'SUCCESS'
              AND NOT EXISTS (
                SELECT 1 FROM processing_attempt pa
                WHERE pa.fingerprint = document_record.fingerprint
                  AND pa.status = 'PROPOSAL_READY'
              )
            """;

    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 SqliteSchemaInitializationAdapter(String jdbcUrl) {
        Objects.requireNonNull(jdbcUrl, "jdbcUrl must not be null");
        if (jdbcUrl.isBlank()) {
            throw new IllegalArgumentException("jdbcUrl must not be blank");
        }
        this.jdbcUrl = jdbcUrl;
    }

    /**
     * Creates or verifies the persistence schema and performs schema evolution and
     * status migration.
     * <p>
     * Execution order:
     * <ol>
     *   <li>Enable foreign key enforcement.</li>
     *   <li>Create {@code document_record} table (if not exists).</li>
     *   <li>Create {@code processing_attempt} table (if not exists).</li>
     *   <li>Create all indexes (if not exist).</li>
     *   <li>Add AI-traceability and provider-identifier columns to {@code processing_attempt}
     *       (idempotent evolution).</li>
     *   <li>Migrate earlier positive intermediate state to {@code READY_FOR_AI} (idempotent).</li>
     * </ol>
     * <p>
     * All steps are safe to run on both fresh and existing databases.
     *
     * @throws DocumentPersistenceException if any DDL or migration step fails
     */
    @Override
    public void initializeSchema() {
        logger.info("Initialising SQLite persistence schema at: {}", jdbcUrl);
        try (Connection connection = DriverManager.getConnection(jdbcUrl);
             Statement statement = connection.createStatement()) {

            // Enable foreign key enforcement (SQLite disables it by default)
            statement.execute("PRAGMA foreign_keys = ON");

            // Level 1: document master record
            statement.execute(DDL_CREATE_DOCUMENT_RECORD);
            logger.debug("Table 'document_record' created or already present.");

            // Level 2: processing attempt history (base columns only)
            statement.execute(DDL_CREATE_PROCESSING_ATTEMPT);
            logger.debug("Table 'processing_attempt' created or already present.");

            // Indexes for efficient per-document, per-run, and per-status access
            statement.execute(DDL_IDX_ATTEMPT_FINGERPRINT);
            statement.execute(DDL_IDX_ATTEMPT_RUN_ID);
            statement.execute(DDL_IDX_RECORD_STATUS);
            logger.debug("Indexes created or already present.");

            // Schema evolution: add AI-traceability + target-copy columns (idempotent)
            evolveTableColumns(connection, "processing_attempt", EVOLUTION_ATTEMPT_COLUMNS);
            evolveTableColumns(connection, "document_record", EVOLUTION_RECORD_COLUMNS);

            // Status migration: earlier positive intermediate state → READY_FOR_AI
            int migrated = statement.executeUpdate(SQL_MIGRATE_LEGACY_SUCCESS_TO_READY_FOR_AI);
            if (migrated > 0) {
                logger.info("Status migration: {} document(s) migrated from legacy SUCCESS state to READY_FOR_AI.",
                        migrated);
            } else {
                logger.debug("Status migration: no documents required migration.");
            }

            logger.info("SQLite schema initialisation and migration completed successfully.");

        } catch (SQLException e) {
            String message = "Failed to initialise SQLite persistence schema at '" + jdbcUrl + "': " + e.getMessage();
            logger.error(message, e);
            throw new DocumentPersistenceException(message, e);
        }
    }

    /**
     * Idempotently adds the given columns to the specified table.
     * <p>
     * For each column that does not yet exist, an {@code ALTER TABLE ... ADD COLUMN}
     * statement is executed. Columns that already exist are silently skipped.
     *
     * @param connection an open JDBC connection to the database
     * @param tableName  the name of the table to evolve
     * @param columns    array of {@code [column_name, column_type]} pairs to add
     * @throws SQLException if a column addition fails for a reason other than duplicate column
     */
    private void evolveTableColumns(Connection connection, String tableName, String[][] columns)
            throws SQLException {
        java.util.Set<String> existingColumns = new java.util.HashSet<>();
        try (ResultSet rs = connection.getMetaData().getColumns(null, null, tableName, null)) {
            while (rs.next()) {
                existingColumns.add(rs.getString("COLUMN_NAME").toLowerCase());
            }
        }

        for (String[] col : columns) {
            String columnName = col[0];
            String columnType = col[1];
            if (!existingColumns.contains(columnName.toLowerCase())) {
                String alterSql = "ALTER TABLE " + tableName + " ADD COLUMN " + columnName + " " + columnType;
                try (Statement stmt = connection.createStatement()) {
                    stmt.execute(alterSql);
                }
                logger.debug("Schema evolution: added column '{}' to '{}'.", columnName, tableName);
            } else {
                logger.debug("Schema evolution: column '{}' in '{}' already present, skipped.",
                        columnName, tableName);
            }
        }
    }

    /**
     * Returns the JDBC URL this adapter uses to connect to the SQLite database.
     *
     * @return the JDBC URL; never null or blank
     */
    public String getJdbcUrl() {
        return jdbcUrl;
    }
}
