package de.avatic.lcc.repositories.premise;

import de.avatic.lcc.model.db.premises.route.Destination;
import de.avatic.lcc.util.exception.badrequest.InvalidArgumentException;
import de.avatic.lcc.util.exception.base.ForbiddenException;
import de.avatic.lcc.util.exception.internalerror.DatabaseException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class DestinationRepository {

    private final JdbcTemplate jdbcTemplate;
    private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;


    public DestinationRepository(JdbcTemplate jdbcTemplate, NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
        this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
    }

    @Transactional
    public Optional<Destination> getById(Integer id) {
        String query = "SELECT * FROM premise_destination WHERE id = ?";

        var destinations = jdbcTemplate.query(query, new DestinationMapper(), id);

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

    }

    @Transactional
    public List<Destination> getByPremiseId(Integer id) {
        String query = "SELECT * FROM premise_destination WHERE premise_id = ?";

        return jdbcTemplate.query(query, new DestinationMapper(), id);
    }

    @Transactional
    public List<Destination> getByPremiseIdAndUserId(Integer premiseId, Integer userId) {

        String premiseCheckQuery = "SELECT COUNT(*) FROM premise WHERE id = ? AND user_id = ?";

        Integer count = jdbcTemplate.queryForObject(premiseCheckQuery, Integer.class, premiseId, userId);

        if (count == null || count == 0) {
            return Collections.emptyList();
        }

        String query = "SELECT * FROM premise_destination WHERE premise_id = ?";
        return jdbcTemplate.query(query, new DestinationMapper(), premiseId);
    }

    @Transactional
    public void update(Integer id, Integer annualAmount, BigDecimal repackingCost, BigDecimal disposalCost, BigDecimal handlingCost, Boolean isD2d, BigDecimal d2dRate, BigDecimal d2dLeadTime) {
        if (id == null) {
            throw new InvalidArgumentException("ID cannot be null");
        }

        StringBuilder sql = new StringBuilder("UPDATE premise_destination SET ");
        Map<String, Object> parameters = new HashMap<>();
        List<String> setClauses = new ArrayList<>();

        // Build dynamic SET clauses based on non-null parameters

        setClauses.add("repacking_cost = :repackingCost");
        parameters.put("repackingCost", repackingCost);


        setClauses.add("disposal_cost = :disposalCost");
        parameters.put("disposalCost", disposalCost);


        setClauses.add("handling_cost = :handlingCost");
        parameters.put("handlingCost", handlingCost);

        var setD2d = isD2d != null ? isD2d : false;
        setClauses.add("is_d2d = :isD2d");
        parameters.put("isD2d", setD2d);

        setClauses.add("rate_d2d = :d2dRate");
        parameters.put("d2dRate", setD2d ? d2dRate : null);

        setClauses.add("lead_time_d2d = :d2dLeadTime");
        parameters.put("d2dLeadTime", setD2d ? d2dLeadTime : null);


        if (annualAmount != null) {
            setClauses.add("annual_amount = :annualAmount");
            parameters.put("annualAmount", annualAmount);
        }

        // If no parameters to update, return early
        if (setClauses.isEmpty()) {
            return;
        }

        // Complete the SQL statement
        sql.append(String.join(", ", setClauses));
        sql.append(" WHERE id = :id");
        parameters.put("id", id);

        int rowsAffected = namedParameterJdbcTemplate.update(sql.toString(), parameters);

        if (rowsAffected == 0) {
            throw new DatabaseException("No premise_destination found with id: " + id);
        }
    }


    @Transactional
    public void deleteById(Integer id) {
        if (id == null) {
            return;
        }

        String sql = "DELETE FROM premise_destination WHERE id = ?";
        jdbcTemplate.update(sql, id);
    }

    @Transactional
    public Optional<Integer> getOwnerIdById(Integer id) {
        String query = """
                SELECT p.user_id AS user_id
                FROM premise_destination pd
                JOIN premise p ON pd.premise_id = p.id
                WHERE pd.id = ?""";

        List<Integer> userId = jdbcTemplate.query(query, (rs, rowNum) -> {
            return rs.getInt("user_id");
        }, id);

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

    @Transactional
    public Map<Integer, Integer> getOwnerIdsByIds(List<Integer> ids) {
        if (ids == null || ids.isEmpty()) {
            return Collections.emptyMap();
        }

        String placeholders = String.join(",", Collections.nCopies(ids.size(), "?"));
        String query = String.format("""
                SELECT pd.id AS pd_id, p.user_id AS user_id
                FROM premise_destination pd
                JOIN premise p ON pd.premise_id = p.id
                WHERE pd.id IN (%s)""", placeholders);

        return jdbcTemplate.query(query, rs -> {
            Map<Integer, Integer> result = new HashMap<>();
            while (rs.next()) {
                result.put(rs.getInt("pd_id"), rs.getInt("user_id"));
            }
            return result;
        }, ids.toArray());
    }

//    @Transactional
//    public List<Destination> getByPremiseIdsAndNodeId(List<Integer> premiseId, Integer nodeId, Integer userId) {
//        String placeholder = String.join(",", Collections.nCopies(premiseId.size(), "?"));
//        String query = "SELECT * FROM premise_destination JOIN premise ON premise_destination.premise_id = premise.id WHERE premise_destination.premise_id IN (" + placeholder + ") AND premise_destination.destination_node_id = ? AND premise.user_id = ?";
//
//        // Create array with all parameters
//        Object[] params = new Object[premiseId.size() + 2];
//        for (int i = 0; i < premiseId.size(); i++) {
//            params[i] = premiseId.get(i);
//        }
//        params[premiseId.size()] = nodeId;
//        params[premiseId.size() + 1] = userId;
//
//        return jdbcTemplate.query(query, new DestinationMapper(), params);
//    }

    @Transactional
    public Map<Integer, List<Destination>> getByPremiseIdsAndNodeIds(Map<Integer, List<Integer>> premiseToNodes, Integer userId) {
        if (premiseToNodes.isEmpty()) {
            return new HashMap<>();
        }

        // Flatten all premise IDs and node IDs for the query
        List<Integer> allPremiseIds = new ArrayList<>(premiseToNodes.keySet());
        Set<Integer> allNodeIds = premiseToNodes.values().stream()
                .flatMap(List::stream)
                .collect(Collectors.toSet());

        String premisePlaceholder = String.join(",", Collections.nCopies(allPremiseIds.size(), "?"));
        String nodePlaceholder = String.join(",", Collections.nCopies(allNodeIds.size(), "?"));

        String query = "SELECT * FROM premise_destination " +
                "JOIN premise ON premise_destination.premise_id = premise.id " +
                "WHERE premise_destination.premise_id IN (" + premisePlaceholder + ") " +
                "AND premise_destination.destination_node_id IN (" + nodePlaceholder + ") " +
                "AND premise.user_id = ?";

        // Create array with all parameters
        Object[] params = new Object[allPremiseIds.size() + allNodeIds.size() + 1];
        int index = 0;

        for (Integer premiseId : allPremiseIds) {
            params[index++] = premiseId;
        }
        for (Integer nodeId : allNodeIds) {
            params[index++] = nodeId;
        }
        params[index] = userId;

        List<Destination> allDestinations = jdbcTemplate.query(query, new DestinationMapper(), params);

        // Group destinations by premise ID and filter by the requested node IDs
        Map<Integer, List<Destination>> result = new HashMap<>();

        for (Map.Entry<Integer, List<Integer>> entry : premiseToNodes.entrySet()) {
            Integer premiseId = entry.getKey();
            Set<Integer> requestedNodeIds = new HashSet<>(entry.getValue());

            List<Destination> filteredDestinations = allDestinations.stream()
                    .filter(d -> d.getPremiseId().equals(premiseId) &&
                            requestedNodeIds.contains(d.getDestinationNodeId()))
                    .collect(Collectors.toList());

            result.put(premiseId, filteredDestinations);
        }

        return result;
    }

    @Transactional
    public Map<Integer, List<Destination>> getByPremiseIdsAndNodeIds(List<Integer> premiseIds, List<Integer> nodeIds, Integer userId) {

        Map<Integer, List<Destination>> destinations = new HashMap<>();

        for (Integer premiseId : premiseIds) {
            String placeholder = String.join(",", Collections.nCopies(nodeIds.size(), "?"));
            String query = "SELECT * FROM premise_destination JOIN premise ON premise_destination.premise_id = premise.id WHERE premise_destination.premise_id = ? AND premise_destination.destination_node_id IN  (" + placeholder + ") AND premise.user_id = ?";

            // Create array with all parameters
            Object[] params = new Object[nodeIds.size() + 2];
            for (int i = 0; i < nodeIds.size(); i++) {
                params[i] = nodeIds.get(i);
            }
            params[nodeIds.size()] = premiseId;
            params[nodeIds.size() + 1] = userId;
            destinations.put(premiseId, jdbcTemplate.query(query, new DestinationMapper(), params));
        }

        return destinations;
    }

    @Transactional
    public Integer insert(Destination destination) {
        KeyHolder keyHolder = new GeneratedKeyHolder();

        String query = "INSERT INTO premise_destination (annual_amount, premise_id, destination_node_id, country_id, rate_d2d, lead_time_d2d, is_d2d, repacking_cost, handling_cost, disposal_cost, geo_lat, geo_lng) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";

        jdbcTemplate.update(connection -> {
            var ps = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS);

            if (destination.getAnnualAmount() == null)
                ps.setNull(1, java.sql.Types.INTEGER);
            else
                ps.setInt(1, destination.getAnnualAmount());

            ps.setInt(2, destination.getPremiseId());
            ps.setInt(3, destination.getDestinationNodeId());
            ps.setInt(4, destination.getCountryId());
            ps.setBigDecimal(5, destination.getRateD2d());

            if (destination.getLeadTimeD2d() == null)
                ps.setNull(6, java.sql.Types.INTEGER);
            else
                ps.setInt(6, destination.getLeadTimeD2d());

            ps.setBoolean(7, destination.getD2d());

            ps.setBigDecimal(8, destination.getRepackingCost());
            ps.setBigDecimal(9, destination.getHandlingCost());
            ps.setBigDecimal(10, destination.getDisposalCost());

            ps.setBigDecimal(11, destination.getGeoLat());
            ps.setBigDecimal(12, destination.getGeoLng());

            return ps;
        }, keyHolder);

        return keyHolder.getKey() != null ? keyHolder.getKey().intValue() : null;
    }

    @Transactional
    public void duplicate(Integer fromId, Integer toId) {

    }

    @Transactional
    public void checkOwner(Integer id, Integer userId) {
        var ownerId = getOwnerIdById(id);

        if (ownerId.isEmpty() || !ownerId.get().equals(userId)) {
            throw new ForbiddenException("Access violation. Accessing destination with id " + id + " (owner: " + ownerId.orElse(null) + " user: " + userId + ")");
        }
    }

    @Transactional
    public void checkOwner(List<Integer> ids, Integer userId) {
        if (ids == null || ids.isEmpty()) {
            return;
        }

        Map<Integer, Integer> ownerMap = getOwnerIdsByIds(ids);
        List<String> violations = new ArrayList<>();

        for (Integer id : ids) {
            Integer ownerId = ownerMap.get(id);

            if (ownerId == null || !ownerId.equals(userId)) {
                violations.add("id " + id + " (owner: " + ownerId + " user: " + userId + ")");
            }
        }

        if (!violations.isEmpty()) {
            throw new ForbiddenException("Access violation. Accessing destinations: " +
                    String.join(", ", violations));
        }
    }


    private static class DestinationMapper implements RowMapper<Destination> {
        @Override
        public Destination mapRow(ResultSet rs, int rowNum) throws SQLException {
            Destination entity = new Destination();

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

            var amount = rs.getInt("annual_amount");
            entity.setAnnualAmount(rs.wasNull() ? null : amount);

            entity.setPremiseId(rs.getInt("premise_id"));
            entity.setDestinationNodeId(rs.getInt("destination_node_id"));
            entity.setRateD2d(rs.getBigDecimal("rate_d2d"));
            entity.setD2d(rs.getBoolean("is_d2d"));
            entity.setLeadTimeD2d(rs.getInt("lead_time_d2d"));
            entity.setRepackingCost(rs.getBigDecimal("repacking_cost"));
            entity.setHandlingCost(rs.getBigDecimal("handling_cost"));
            entity.setDisposalCost(rs.getBigDecimal("disposal_cost"));
            entity.setGeoLat(rs.getBigDecimal("geo_lat"));
            entity.setGeoLng(rs.getBigDecimal("geo_lng"));
            entity.setCountryId(rs.getInt("country_id"));

            return entity;
        }
    }
}
