package de.avatic.lcc.repositories.properties;

import de.avatic.lcc.dto.generic.PropertyDTO;
import de.avatic.lcc.model.db.properties.SystemPropertyMappingId;
import de.avatic.lcc.model.db.rates.ValidityPeriodState;
import de.avatic.lcc.util.exception.internalerror.DatabaseException;
import org.springframework.dao.DataAccessException;
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.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;


/**
 * Repository for managing operations related to properties and property sets.
 * Provides methods for saving, retrieving, and manipulating property data in the database.
 */
@Repository
public class PropertyRepository {

    private final JdbcTemplate jdbcTemplate;

    public PropertyRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    /**
     * Updates or inserts a property value for a specific property set and mapping ID.
     * This method performs an upsert operation to ensure the property value is updated if it exists.
     *
     * @param setId     the ID of the property set to which the property belongs
     * @param mappingId the external mapping ID of the property type
     * @param value     the value to set for the property
     */
    @Transactional
    public void setProperty(Integer setId, String mappingId, String value) {
        var typeId = getTypeIdByMappingId(mappingId);

        String validValueQuery = """
                SELECT sp.property_value
                FROM system_property sp
                JOIN property_set ps ON ps.id = sp.property_set_id
                WHERE ps.state = ? AND sp.system_property_type_id = ?""";

        String validValue = jdbcTemplate.queryForObject(validValueQuery, String.class,
                ValidityPeriodState.VALID.name(), typeId);

        if (value.equals(validValue)) {
            String deleteQuery = "DELETE FROM system_property WHERE property_set_id = ? AND system_property_type_id = ?";
            jdbcTemplate.update(deleteQuery, setId, typeId);
            return;
        }

        String query = """
                INSERT INTO system_property (property_set_id, system_property_type_id, property_value) VALUES (?, ?, ?)
                ON DUPLICATE KEY UPDATE property_value = ?""";

        var affectedRows = jdbcTemplate.update(query, setId, typeId, value, value);

        if (!(affectedRows > 0)) {
            throw new DatabaseException("Could not update property value for property set " + setId + " and property type " + mappingId);
        }

    }

    /**
     * Retrieves the database ID of a property type based on its external mapping ID.
     *
     * @param mappingId the external mapping ID of the property type
     * @return the ID of the property type in the database
     */
    private Integer getTypeIdByMappingId(String mappingId) {
        String query = "SELECT id FROM system_property_type WHERE external_mapping_id = ?";
        return jdbcTemplate.queryForObject(query, Integer.class, mappingId);
    }

    /**
     * Retrieves a list of all properties, including their draft and valid values,
     * along with metadata such as validation rules and data types.
     *
     * @return a list of {@link PropertyDTO} objects representing the properties
     */
    @Transactional
    public List<PropertyDTO> listProperties() {

        String query = """
                SELECT type.name as name, type.data_type as dataType, type.external_mapping_id as externalMappingId, type.validation_rule as validationRule,
                       type.description as description, type.property_group as propertyGroup, type.sequence_number as sequenceNumber,
                MAX(CASE WHEN ps.state = ? THEN sp.property_value END) as draftValue,
                MAX(CASE WHEN ps.state = ? THEN sp.property_value END) as validValue
                FROM system_property_type AS type
                LEFT JOIN system_property AS sp ON sp.system_property_type_id = type.id
                LEFT JOIN property_set AS ps ON ps.id = sp.property_set_id AND ps.state IN (?, ?)
                GROUP BY type.id, type.name, type.data_type, type.external_mapping_id, type.validation_rule, type.description, type.property_group, type.sequence_number
                HAVING draftValue IS NOT NULL OR validValue IS NOT NULL ORDER BY type.property_group , type.sequence_number;
                """;

        return jdbcTemplate.query(query, new PropertyMapper(), ValidityPeriodState.DRAFT.name(), ValidityPeriodState.VALID.name(), ValidityPeriodState.DRAFT.name(), ValidityPeriodState.VALID.name());

    }

    /**
     * Retrieves properties for a specific property set ID where the state of the set is "EXPIRED."
     *
     * @param propertySetId the ID of the property set
     * @return a list of {@link PropertyDTO} representing the properties of the specified set
     */
    public List<PropertyDTO> listPropertiesBySetId(Integer propertySetId) {

        String query = """
                SELECT type.name as name, type.data_type as dataType, type.external_mapping_id as externalMappingId, type.validation_rule as validationRule,
                       type.description as description, type.property_group as propertyGroup, type.sequence_number as sequenceNumber,
                NULL as draftValue, property.property_value as validValue
                FROM system_property_type AS type
                LEFT JOIN system_property AS property ON property.system_property_type_id = type.id
                LEFT JOIN property_set AS propertySet ON propertySet.id = property.property_set_id
                WHERE propertySet.id = ? AND (propertySet.state = ? OR propertySet.state = ?) ORDER BY type.property_group , type.sequence_number;
                """;

        return jdbcTemplate.query(query, new PropertyMapper(), propertySetId, ValidityPeriodState.EXPIRED.name(), ValidityPeriodState.INVALID.name());
    }

    @Transactional
    public Optional<PropertyDTO> getPropertyByMappingId(SystemPropertyMappingId mappingId) {
        String query = """
                SELECT type.name as name, type.data_type as dataType, type.external_mapping_id as externalMappingId, type.validation_rule as validationRule,
                       type.description as description, type.property_group as propertyGroup, type.sequence_number as sequenceNumber,
                property.property_value as draftValue, property.property_value as validValue
                FROM system_property_type AS type
                LEFT JOIN system_property AS property ON property.system_property_type_id = type.id
                LEFT JOIN property_set AS propertySet ON propertySet.id = property.property_set_id
                WHERE propertySet.state = ? AND type.external_mapping_id = ?
                """;

        var property = jdbcTemplate.query(query, new PropertyMapper(), ValidityPeriodState.VALID.name(), mappingId.name());

        if (property.isEmpty()) return Optional.empty();
        else return Optional.of(property.getFirst());

    }

    @Transactional
    public Optional<PropertyDTO> getPropertyByMappingId(SystemPropertyMappingId mappingId, Integer setId) {
        String query = """
                SELECT type.name as name, type.data_type as dataType, type.external_mapping_id as externalMappingId, type.validation_rule as validationRule,
                       type.description as description, type.property_group as propertyGroup, type.sequence_number as sequenceNumber,
                property.property_value as draftValue, property.property_value as validValue
                FROM system_property_type AS type
                LEFT JOIN system_property AS property ON property.system_property_type_id = type.id
                LEFT JOIN property_set AS propertySet ON propertySet.id = property.property_set_id
                WHERE propertySet.id = ? AND type.external_mapping_id = ?
                """;

        var property = jdbcTemplate.query(query, new PropertyMapper(), setId, mappingId.name());

        if (property.isEmpty()) return Optional.empty();
        else return Optional.of(property.getFirst());

    }

    /**
     * Fills the draft property set with values from a valid property set.
     * This method ensures that the draft set contains all properties with their latest valid values.
     *
     * @param setId the ID of the draft property set to fill
     */
    @Transactional
    public void fillDraft(Integer setId) {
        String query = """
            SELECT type.id AS typeId, property.property_value as value FROM system_property_type AS type
            LEFT JOIN system_property AS property ON property.system_property_type_id = type.id
            LEFT JOIN property_set AS propertySet ON propertySet.id = property.property_set_id 
            WHERE propertySet.state = ?""";

        try {
            List<Map<String, Object>> results = jdbcTemplate.queryForList(query, ValidityPeriodState.VALID.name());

            String insertQuery = """
                INSERT IGNORE INTO system_property (property_value, system_property_type_id, property_set_id)
                VALUES (?, ?, ?)""";

            List<Object[]> batchArgs = results.stream()
                    .map(row -> new Object[]{row.get("value"), row.get("typeId"), setId})
                    .collect(Collectors.toList());

            if (!batchArgs.isEmpty()) {
                jdbcTemplate.batchUpdate(insertQuery, batchArgs);
            }

        } catch (DataAccessException e) {
            throw new DatabaseException("Failed to fill draft property set with ID: " + setId);
        }
    }

    private static class PropertyMapper implements RowMapper<PropertyDTO> {
        @Override
        public PropertyDTO mapRow(ResultSet rs, int rowNum) throws SQLException {
            var dto = new PropertyDTO();

            dto.setName(rs.getString("name"));
            dto.setDraftValue(rs.getString("draftValue"));
            dto.setCurrentValue(rs.getString("validValue"));
            dto.setValidationRule(rs.getString("validationRule"));
            dto.setExternalMappingId(rs.getString("externalMappingId"));
            dto.setRequired(true);
            dto.setDataType(rs.getString("dataType"));

            dto.setDescription(rs.getString("description"));
            dto.setPropertyGroup(rs.getString("propertyGroup"));
            dto.setSequenceNumber(rs.getInt("sequenceNumber"));

            return dto;
        }
    }
}
