package de.avatic.lcc.repositories.calculation;

import de.avatic.lcc.model.db.calculations.CalculationJob;
import de.avatic.lcc.model.db.calculations.CalculationJobState;
import de.avatic.lcc.util.exception.internalerror.DatabaseException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.sql.*;
import java.util.Optional;

@Repository
public class CalculationJobRepository {
    private final JdbcTemplate jdbcTemplate;

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

     @Transactional
     public Integer insert(CalculationJob job) {
         String sql = """
        INSERT INTO calculation_job (premise_id, calculation_date, validity_period_id, property_set_id, job_state, user_id)
        VALUES (?, ?, ?, ?, ?, ?)
        """;

         GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();

         jdbcTemplate.update(connection -> {
             PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
             ps.setInt(1, job.getPremiseId());

             ps.setTimestamp(2, job.getCalculationDate() == null ? null : Timestamp.valueOf(job.getCalculationDate()));

             ps.setInt(3, job.getValidityPeriodId());
             ps.setInt(4, job.getPropertySetId());
             ps.setString(5, job.getJobState().name()); // Convert enum to string
             ps.setInt(6, job.getUserId());
             return ps;
         }, keyHolder);

         // Return the generated ID
         return keyHolder.getKey() != null ? keyHolder.getKey().intValue() : null;
     }

    @Transactional
    public Optional<CalculationJob> getCalculationJob(Integer id) {
        String query = "SELECT * FROM calculation_job WHERE id = ?";

        var job = jdbcTemplate.query(query, new CalculationJobMapper(), id);

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

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

    @Transactional
    public void reschedule(Integer id) {
        String sql = "UPDATE calculation_job SET job_state = ?, calculation_date = ? WHERE id = ?";

        var affectedRows = jdbcTemplate.update(sql, CalculationJobState.CREATED.name(), java.time.LocalDateTime.now(), id);

        if(1 != affectedRows) {
            throw new DatabaseException("Unable to update calculation job with id " + id);
        }
    }

    @Transactional
    public Optional<CalculationJob> getCalculationJobWithJobStateValid(Integer periodId, Integer setId, Integer nodeId, Integer materialId) {

        /* there should only be one job per period id, node id and material id combination */
        String query = "SELECT * FROM calculation_job AS cj INNER JOIN premise AS p ON cj.premise_id = p.id WHERE job_state = 'VALID' AND validity_period_id = ? AND property_set_id = ? AND p.supplier_node_id = ? AND material_id = ? ORDER BY cj.calculation_date DESC LIMIT 1";

        var job = jdbcTemplate.query(query, new CalculationJobMapper(), periodId, setId, nodeId, materialId);

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

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

    @Transactional
    public Optional<CalculationJob> getCalculationJobWithJobStateValidUserNodeId(Integer periodId, Integer setId, Integer userNodeId, Integer materialId) {

        /* there should only be one job per period id, node id and material id combination */
        String query = "SELECT * FROM calculation_job AS cj INNER JOIN premise AS p ON cj.premise_id = p.id WHERE job_state = 'VALID' AND validity_period_id = ? AND property_set_id = ? AND p.user_supplier_node_id = ? AND material_id = ? ORDER BY cj.calculation_date DESC LIMIT 1";

        var job = jdbcTemplate.query(query, new CalculationJobMapper(), periodId, setId, userNodeId, materialId);

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

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



    @Transactional
    public void setStateTo(Integer id, CalculationJobState calculationJobState) {
        String sql = "UPDATE calculation_job SET job_state = ? WHERE id = ?";

        var affectedRows = jdbcTemplate.update(sql, calculationJobState.name(), id);

        if(1 != affectedRows) {
            throw new DatabaseException("Unable to update calculation job with id " + id);
        }
    }

    @Transactional
    public Optional<CalculationJob> findJob(Integer premiseId, Integer validSetId, Integer validPeriodId) {
        String sql = "SELECT * FROM calculation_job WHERE premise_id = ? AND validity_period_id = ? AND property_set_id = ?";

        var jobs = jdbcTemplate.query(sql, new CalculationJobMapper(), premiseId, validPeriodId, validSetId);

        return jobs.isEmpty() ? Optional.empty() : Optional.ofNullable(jobs.getFirst());
    }

    @Transactional
    public void invalidateByPropertySetId(Integer id) {
        String sql = "UPDATE calculation_job SET job_state = ? WHERE property_set_id = ?";
        jdbcTemplate.update(sql, CalculationJobState.INVALIDATED.name(), id);
    }

    @Transactional
    public void invalidateByPeriodId(Integer id) {
        String sql = "UPDATE calculation_job SET job_state = ? WHERE validity_period_id = ?";
        jdbcTemplate.update(sql, CalculationJobState.INVALIDATED.name(), id);
    }


    private static class CalculationJobMapper implements RowMapper<CalculationJob> {
        @Override
        public CalculationJob mapRow(ResultSet rs, int rowNum) throws SQLException {

            CalculationJob entity = new CalculationJob();

            entity.setId(rs.getInt("id"));
            entity.setPremiseId(rs.getInt("premise_id"));
            entity.setValidityPeriodId(rs.getInt("validity_period_id"));
            entity.setJobState(CalculationJobState.valueOf(rs.getString("job_state")));
            entity.setPropertySetId(rs.getInt("property_set_id"));
            entity.setUserId(rs.getInt("user_id"));
            entity.setCalculationDate(rs.getTimestamp("calculation_date").toLocalDateTime());

            return entity;

        }
    }
}
