package de.avatic.lcc.repositories.rates;

import de.avatic.lcc.model.db.ValidityTuple;
import de.avatic.lcc.model.db.rates.ValidityPeriod;
import de.avatic.lcc.model.db.rates.ValidityPeriodState;
import de.avatic.lcc.util.exception.internalerror.DatabaseException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

/**
 * Repository for managing {@link ValidityPeriod} entities.
 * Provides methods to retrieve and update validity periods in the database.
 */
@Repository
public class ValidityPeriodRepository {


    /**
     * The {@link JdbcTemplate} used for interacting with the database.
     */
    private final JdbcTemplate jdbcTemplate;

    /**
     * Constructs a new repository with a given {@link JdbcTemplate}.
     *
     * @param jdbcTemplate the {@link JdbcTemplate} used for executing SQL queries.
     */
    public ValidityPeriodRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    /**
     * Retrieves all validity periods from the database.
     *
     * @return a list of {@link ValidityPeriod} objects representing all validity periods.
     */
    @Transactional
    public List<ValidityPeriod> listPeriods() {
        String query = "SELECT * FROM validity_period";

        return jdbcTemplate.query(query, new ValidityPeriodMapper());
    }

    /**
     * Finds the ID of the validity period that includes the specified timestamp.
     *
     * @param validAt the timestamp to check against validity periods.
     * @return an {@link Optional} containing the ID if a validity period is found, otherwise empty.
     */
    @Transactional
    public Optional<Integer> getPeriodId(LocalDateTime validAt) {
        String query = "SELECT id FROM validity_period WHERE ? BETWEEN start_date AND end_date";
        return Optional.ofNullable(jdbcTemplate.query(query, (rs) -> rs.next() ? rs.getInt("id") : null, validAt));
    }

    /**
     * Sets the state of a validity period to {@code INVALID} for the specified ID
     * if its current state is {@code EXPIRED}.
     *
     * @param id the ID of the validity period to invalidate.
     */
    @Transactional
    public boolean invalidateById(Integer id) {
        var affectedRows = jdbcTemplate.update("UPDATE validity_period SET state = ? WHERE id = ? AND state = ? ", ValidityPeriodState.INVALID.name(), id, ValidityPeriodState.EXPIRED.name());
        return affectedRows > 0;
    }

    /**
     * Retrieves a {@link ValidityPeriod} by its ID.
     *
     * @param id the unique identifier of the validity period.
     * @return the {@link ValidityPeriod} corresponding to the ID.
     */
    @Transactional
    public ValidityPeriod getById(Integer id) {
        String query = "SELECT * FROM validity_period WHERE id = ?";
        return jdbcTemplate.queryForObject(query, new ValidityPeriodMapper(), id);
    }

    /**
     * Creates a draft validity period if none exists in the database.
     */
    private void createSet() {
        final Timestamp currentTimestamp = new Timestamp(System.currentTimeMillis());
        jdbcTemplate.update("INSERT INTO validity_period (state, start_date) SELECT ?, ? WHERE NOT EXISTS (SELECT 1 FROM validity_period WHERE state = ?)", ValidityPeriodState.DRAFT.name(), currentTimestamp, ValidityPeriodState.DRAFT.name());
    }

    /**
     * Retrieves the ID of the validity period currently in the {@code VALID} state.
     *
     * @return the ID of the valid {@link ValidityPeriod}.
     */
    @Transactional
    public Optional<Integer> getValidPeriodId() {
        return getValidPeriod().map(ValidityPeriod::getId);
    }

    /**
     * Retrieves the validity period currently in the {@code VALID} state.
     *
     * @return the {@link ValidityPeriod} in the {@code VALID} state.
     */
    @Transactional
    public Optional<ValidityPeriod> getValidPeriod() {
        String query = "SELECT * FROM validity_period WHERE state = ?";
        var period = jdbcTemplate.query(query, new ValidityPeriodMapper(), ValidityPeriodState.VALID.name());

        if (period.isEmpty())
            return Optional.empty();

        return Optional.of(period.getFirst());
    }

    /**
     * Retrieves the validity period currently in the {@code DRAFT} state.
     *
     * @return the {@link ValidityPeriod} in the {@code DRAFT} state.
     */
    @Transactional
    public ValidityPeriod getDraftPeriod() {
        createSet();

        String query = "SELECT * FROM validity_period WHERE state = ?";
        var period = jdbcTemplate.query(query, new ValidityPeriodMapper(), ValidityPeriodState.DRAFT.name());

        if(period.isEmpty())
            throw new DatabaseException("No draft validity period exists");

        return period.getFirst();
    }

    /**
     * Retrieves the ID of the validity period currently in the {@code DRAFT} state.
     *
     * @return the ID of the draft {@link ValidityPeriod}.
     */
    @Transactional
    public Integer getDraftPeriodId() {
        return getDraftPeriod().getId();
    }


    /**
     * Checks if there are any draft matrix rates associated with the current draft validity period.
     *
     * @return {@code true} if draft matrix rates exist for the current draft validity period;
     * {@code false} otherwise.
     */
    @Transactional
    public boolean hasRateDrafts() {
        Integer id = getDraftPeriodId();
        if (id == null) return false;

        String query = "SELECT COUNT(*) FROM country_matrix_rate WHERE validity_period_id = ?";
        var matrixCount = jdbcTemplate.queryForObject(query, Integer.class, id);

        query = "SELECT COUNT(*) FROM container_rate WHERE validity_period_id = ?";
        var containerCount = jdbcTemplate.queryForObject(query, Integer.class, id);

        int totalCount = (matrixCount != null ? matrixCount : 0) + (containerCount != null ? containerCount : 0);

        return totalCount > 0;
    }

    /**
     * Checks if there are any draft matrix rates associated with the current draft validity period.
     *
     * @return {@code true} if draft matrix rates exist for the current draft validity period;
     * {@code false} otherwise.
     */
    @Transactional
    public boolean hasMatrixRateDrafts() {
        Integer id = getDraftPeriodId();
        if (id == null) return false;

        String query = "SELECT COUNT(*) FROM country_matrix_rate WHERE validity_period_id = ?";
        var matrixCount = jdbcTemplate.queryForObject(query, Integer.class, id);

        int totalCount = (matrixCount != null ? matrixCount : 0);

        return totalCount > 0;
    }

    /**
     * Checks if there are any draft matrix rates associated with the current draft validity period.
     *
     * @return {@code true} if draft matrix rates exist for the current draft validity period;
     * {@code false} otherwise.
     */
    @Transactional
    public boolean hasContainerRateDrafts() {
        Integer id = getDraftPeriodId();
        if (id == null) return false;


        String query = "SELECT COUNT(*) FROM container_rate WHERE validity_period_id = ?";
        var containerCount = jdbcTemplate.queryForObject(query, Integer.class, id);

        int totalCount = (containerCount != null ? containerCount : 0);

        return totalCount > 0;
    }

    /**
     * Applies the draft validity period, making it the new valid period.
     * The current valid period is marked as expired, and timestamps are adjusted accordingly.
     */
    @Transactional
    public void applyDraft() {
        if (!hasRateDrafts()) return;

        final Timestamp currentTimestamp = new Timestamp(System.currentTimeMillis());

        /* set current to expired */
        jdbcTemplate.update("UPDATE validity_period SET state = ?, end_date = ? WHERE state = ? ", ValidityPeriodState.EXPIRED.name(), currentTimestamp, ValidityPeriodState.VALID.name());
        jdbcTemplate.update("UPDATE validity_period SET state = ?, start_date = ?  WHERE state = ? ", ValidityPeriodState.VALID.name(), currentTimestamp, ValidityPeriodState.DRAFT.name());
    }

    @Transactional
    public Optional<ValidityTuple> getValidPeriodForReportingByMaterialId(Integer materialId, List<Integer> nodeIds, List<Integer> userNodeIds) {

        // Check if both lists are empty
        if ((nodeIds == null || nodeIds.isEmpty()) && (userNodeIds == null || userNodeIds.isEmpty())) {
            return Optional.empty();
        }

        // Handle null lists
        List<Integer> safeNodeIds = nodeIds != null ? nodeIds : Collections.emptyList();
        List<Integer> safeUserNodeIds = userNodeIds != null ? userNodeIds : Collections.emptyList();

        int totalNodeCount = safeNodeIds.size() + safeUserNodeIds.size();

        // Build placeholders for both node types
        String nodePlaceholders = safeNodeIds.isEmpty() ? "" : String.join(",", Collections.nCopies(safeNodeIds.size(), "?"));
        String userNodePlaceholders = safeUserNodeIds.isEmpty() ? "" : String.join(",", Collections.nCopies(safeUserNodeIds.size(), "?"));

        // Build the WHERE clause dynamically
        StringBuilder whereClause = new StringBuilder();
        if (!safeNodeIds.isEmpty() && !safeUserNodeIds.isEmpty()) {
            whereClause.append("AND (p.supplier_node_id IN (").append(nodePlaceholders)
                    .append(") OR p.user_supplier_node_id IN (").append(userNodePlaceholders).append("))");
        } else if (!safeNodeIds.isEmpty()) {
            whereClause.append("AND p.supplier_node_id IN (").append(nodePlaceholders).append(")");
        } else {
            whereClause.append("AND p.user_supplier_node_id IN (").append(userNodePlaceholders).append(")");
        }

        String validityPeriodSql = """
                SELECT
                    cj.validity_period_id,
                    cj.property_set_id
                FROM
                    premise p
                INNER JOIN
                    calculation_job cj ON p.id = cj.premise_id
                INNER JOIN
                    validity_period vp ON cj.validity_period_id = vp.id
                INNER JOIN
                    property_set ps ON cj.property_set_id = ps.id
                WHERE
                    p.material_id = ?
                """
                + whereClause + """
                GROUP BY
                    cj.validity_period_id,
                    cj.property_set_id
                HAVING
                    COUNT(DISTINCT COALESCE(p.supplier_node_id, p.user_supplier_node_id)) = ?
                ORDER BY
                    ps.start_date DESC, vp.start_date DESC
                """;

        // Build parameters array
        Object[] params = new Object[1 + totalNodeCount + 1];
        int paramIndex = 0;
        params[paramIndex++] = materialId;

        // Add nodeIds
        for (Integer nodeId : safeNodeIds) {
            params[paramIndex++] = nodeId;
        }

        // Add userNodeIds
        for (Integer userNodeId : safeUserNodeIds) {
            params[paramIndex++] = userNodeId;
        }

        params[paramIndex] = totalNodeCount;

        var tuples = jdbcTemplate.query(validityPeriodSql,
                (rs, cnt) -> new ValidityTuple(rs.getInt("validity_period_id"), rs.getInt("property_set_id")),
                params);

        if (tuples.isEmpty()) {
            return Optional.empty();
        }

        return Optional.of(tuples.getFirst());
    }

    @Transactional
    public List<ValidityTuple> findValidityPeriodsWithReportByMaterialId(Integer materialId) {

        String validityPeriodSql = """
                SELECT DISTINCT cj.validity_period_id, cj.property_set_id
                FROM premise p
                INNER JOIN calculation_job cj ON p.id = cj.premise_id
                WHERE p.material_id = ?
                """;

        return jdbcTemplate.query(validityPeriodSql, (rs, cnt) -> new ValidityTuple(rs.getInt("validity_period_id"), rs.getInt("property_set_id")), materialId);
    }

    @Transactional
    public void increaseRenewal(Integer increase) {
        String sql = "UPDATE validity_period SET renewals = renewals + ? WHERE state = 'VALID'";
        jdbcTemplate.update(sql, increase);
    }


    /**
     * Maps rows of a {@link ResultSet} to {@link ValidityPeriod} objects.
     */
    private static class ValidityPeriodMapper implements RowMapper<ValidityPeriod> {

        @Override
        public ValidityPeriod mapRow(ResultSet rs, int rowNum) throws SQLException {

            var period = new ValidityPeriod();

            period.setId(rs.getInt("id"));

            Timestamp startTs = rs.getTimestamp("start_date");
            period.setStartDate(startTs != null ? startTs.toLocalDateTime() : null);

            Timestamp endTs = rs.getTimestamp("end_date");
            period.setEndDate(endTs != null ? endTs.toLocalDateTime() : null);

            String stateStr = rs.getString("state");
            period.setState(stateStr != null ? ValidityPeriodState.valueOf(stateStr) : null);

            period.setRenewals(rs.getInt("renewals"));


            return period;

        }
    }
}
