package de.avatic.lcc.repositories;

import de.avatic.lcc.dto.generic.NodeType;
import de.avatic.lcc.model.db.ValidityTuple;
import de.avatic.lcc.model.db.nodes.Node;
import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
import de.avatic.lcc.repositories.pagination.SearchQueryResult;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.*;

@Repository
public class NodeRepository {


    private final JdbcTemplate jdbcTemplate;
    private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;

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

    @Transactional
    public Optional<Node> getById(Integer id) {

        String query = """
                SELECT *
                FROM node
                WHERE node.id = ?""";

        var node = jdbcTemplate.query(query, new NodeMapper(), id);

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

        return Optional.ofNullable(node.getFirst());
    }

    public List<Node> getByIds(List<Integer> nodeIds) {

        String placeholders = String.join(",", Collections.nCopies(nodeIds.size(), "?"));
        String query = """
                SELECT *
                FROM node
                WHERE node.id IN (""" + placeholders + ")";

        return nodeIds.isEmpty() ? Collections.emptyList() : jdbcTemplate.query(query, new NodeMapper(), nodeIds.toArray());
    }

    private List<Map<Integer, Integer>> getPredecessorsOf(Integer id) {

        String queryChains = """
                SELECT chain.id AS id FROM node_predecessor_chain AS chain WHERE chain.node_id = ? ORDER BY chain.id
                """;

        return jdbcTemplate.query(queryChains, (chainRs, rowNum) -> {

            Integer chainId = chainRs.getInt("id");

            String query = """
                    SELECT entry.node_id AS predecessor_node_id , entry.sequence_number as sequence_number
                    FROM node_predecessor_entry AS entry
                    WHERE entry.node_predecessor_chain_id = ? ORDER BY entry.sequence_number""";

            return jdbcTemplate.query(query, rs -> {
                Map<Integer, Integer> predecessors = new HashMap<>();

                while (rs.next()) {
                    predecessors.put(rs.getInt("sequence_number"), rs.getInt("predecessor_node_id"));
                }

                return predecessors;
            }, chainId);
        }, id);
    }

    private Collection<Integer> getOutboundCountriesOf(Integer id) {
        String query = """
                SELECT outbound_country_mapping.country_id
                FROM outbound_country_mapping
                WHERE outbound_country_mapping.node_id = ?""";

        return jdbcTemplate.queryForList(query, Integer.class, id);
    }

    @Transactional
    public SearchQueryResult<Node> listNodes(String filter, Boolean excludeDeprecated, SearchQueryPagination pagination) {
        String query = buildQuery(filter, excludeDeprecated, pagination);
        String countQuery = buildCountQuery(filter, excludeDeprecated);
        List<Node> entities = null;
        Integer totalCount = 0;

        if (filter == null) {
            entities = jdbcTemplate.query(query, new NodeMapper(), pagination.getLimit(), pagination.getOffset());
            totalCount = jdbcTemplate.queryForObject(countQuery, Integer.class);
        } else {
            entities = jdbcTemplate.query(query, new NodeMapper(), "%" + filter + "%", "%" + filter + "%", "%" + filter + "%", pagination.getLimit(), pagination.getOffset());
            totalCount = jdbcTemplate.queryForObject(countQuery, Integer.class, "%" + filter + "%", "%" + filter + "%", "%" + filter + "%");
        }


        return new SearchQueryResult<>(entities, pagination.getPage(), totalCount, pagination.getLimit());
    }

    private String buildCountQuery(String filter, boolean excludeDeprecated) {
        StringBuilder queryBuilder = new StringBuilder("""
                SELECT COUNT(*)
                FROM node
                LEFT JOIN country ON country.id = node.country_id
                WHERE 1=1""");

        if (excludeDeprecated) {
            queryBuilder.append(" AND node.is_deprecated = FALSE");
        }
        if (filter != null) {
            queryBuilder.append(" AND (node.name LIKE ? OR node.address LIKE ? OR country.iso_code LIKE ?)");
        }

        return queryBuilder.toString();
    }

    private String buildQuery(String filter, Boolean excludeDeprecated, SearchQueryPagination searchQueryPagination) {
        StringBuilder queryBuilder = new StringBuilder("""
                SELECT *
                FROM node
                LEFT JOIN country ON country.id = node.country_id
                WHERE 1=1
                """);

        if (excludeDeprecated) {
            queryBuilder.append(" AND node.is_deprecated = FALSE");
        }
        if (filter != null) {
            queryBuilder.append(" AND (node.name LIKE ? OR node.address LIKE ? OR country.iso_code LIKE ?)");
        }
        queryBuilder.append(" ORDER BY node.id LIMIT ? OFFSET ?");
        return queryBuilder.toString();
    }

    @Transactional
    public Optional<Integer> setDeprecatedById(Integer id) {
        String query = "UPDATE node SET is_deprecated = TRUE WHERE id = ?";

        // Mark all linked RouteNodes as outdated
        jdbcTemplate.update("UPDATE premise_route_node SET is_outdated = TRUE WHERE node_id = ?", id);


        return Optional.ofNullable(jdbcTemplate.update(query, id) == 0 ? null : id);
    }

    @Transactional
    public Optional<Integer> update(Node node) {
        if (node.getId() == null) {
            return Optional.empty();
        }

        String updateNodeSql = """
                UPDATE node SET
                    country_id = ?,
                    name = ?,
                    address = ?,
                    external_mapping_id = ?,
                    predecessor_required = ?,
                    is_destination = ?,
                    is_source = ?,
                    is_intermediate = ?,
                    geo_lat = ?,
                    geo_lng = ?,
                    is_deprecated = ?,
                    updated_at = CURRENT_TIMESTAMP
                WHERE id = ?
                """;

        int rowsUpdated = jdbcTemplate.update(updateNodeSql,
                node.getCountryId(),
                node.getName(),
                node.getAddress(),
                node.getExternalMappingId(),
                node.getPredecessorRequired(),
                node.getDestination(),
                node.getSource(),
                node.getIntermediate(),
                node.getGeoLat(),
                node.getGeoLng(),
                node.getDeprecated(),
                node.getId()
        );

        if (rowsUpdated == 0) {
            return Optional.empty();
        }

        if (node.getNodePredecessors() != null) {

            var deletePredecessorEntriesSql = """
                    DELETE npe FROM node_predecessor_entry npe
                    JOIN node_predecessor_chain npc ON npe.node_predecessor_chain_id = npc.id
                    WHERE npc.node_id = ?""";

            var deletePredecessorChainSql = """
                    DELETE FROM node_predecessor_chain WHERE node_id = ?
                    """;

            jdbcTemplate.update(deletePredecessorEntriesSql, node.getId());
            jdbcTemplate.update(deletePredecessorChainSql, node.getId());

            // Insert new predecessor chains
            for (Map<Integer, Integer> predecessorChain : node.getNodePredecessors()) {
                // Insert new chain
                KeyHolder chainKeyHolder = new GeneratedKeyHolder();
                jdbcTemplate.update(connection -> {
                    PreparedStatement ps = connection.prepareStatement(
                            "INSERT INTO node_predecessor_chain (node_id) VALUES (?)",
                            Statement.RETURN_GENERATED_KEYS);
                    ps.setInt(1, node.getId());
                    return ps;
                }, chainKeyHolder);

                Integer chainId = chainKeyHolder.getKey().intValue();

                // Insert entries for this chain
                for (Map.Entry<Integer, Integer> entry : predecessorChain.entrySet()) {
                    jdbcTemplate.update(
                            "INSERT INTO node_predecessor_entry (node_id, node_predecessor_chain_id, sequence_number) VALUES (?, ?, ?)",
                            entry.getValue(), chainId, entry.getKey()
                    );
                }
            }
        }

        jdbcTemplate.update("DELETE FROM outbound_country_mapping WHERE node_id = ?", node.getId());

        if (node.getOutboundCountries() != null) {
            for (Integer countryId : node.getOutboundCountries()) {
                jdbcTemplate.update(
                        "INSERT INTO outbound_country_mapping (node_id, country_id) VALUES (?, ?)",
                        node.getId(), countryId
                );
            }
        }

        // Mark all linked RouteNodes as outdated
        jdbcTemplate.update("UPDATE premise_route_node SET is_outdated = TRUE WHERE node_id = ?", node.getId());

        // Mark all distance matrix entries as stale
        jdbcTemplate.update("UPDATE distance_matrix SET state = 'STALE' WHERE ((from_node_id = ?) OR (to_node_id = ?))", node.getId(), node.getId());


        return Optional.of(node.getId());
    }

    public List<Node> searchNode(String filter, int limit, NodeType nodeType, boolean excludeDeprecated) {
        StringBuilder queryBuilder = new StringBuilder().append("SELECT * FROM node");
        List<Object> parameters = new ArrayList<>();

        boolean hasWhereClause = false;

        // Add filter conditions only if filter is not null
        if (filter != null && !filter.trim().isEmpty()) {
            queryBuilder.append(" WHERE (name LIKE ? OR address LIKE ?)");
            parameters.add("%" + filter + "%");
            parameters.add("%" + filter + "%");
            hasWhereClause = true;
        }

        // Add node type conditions
        if (nodeType != null) {
            if (!hasWhereClause) {
                queryBuilder.append(" WHERE ");
                hasWhereClause = true;
            } else {
                queryBuilder.append(" AND ");
            }

            if (nodeType.equals(NodeType.SOURCE)) {
                queryBuilder.append("is_source = true");
            } else if (nodeType.equals(NodeType.DESTINATION)) {
                queryBuilder.append("is_destination = true");
            } else if (nodeType.equals(NodeType.INTERMEDIATE)) {
                queryBuilder.append("is_intermediate = true");
            }
        }

        // Add deprecated filter
        if (excludeDeprecated) {
            if (!hasWhereClause) {
                queryBuilder.append(" WHERE ");
            } else {
                queryBuilder.append(" AND ");
            }
            queryBuilder.append("is_deprecated = false");
        }

        queryBuilder.append(" LIMIT ?");
        parameters.add(limit);

        return jdbcTemplate.query(queryBuilder.toString(), new NodeMapper(), parameters.toArray());
    }

    public List<Node> listAllNodes(boolean onlySources) {
        StringBuilder queryBuilder = new StringBuilder("SELECT * FROM node");
        if (onlySources) {
            queryBuilder.append(" WHERE is_source = true");
        }
        queryBuilder.append("  ORDER BY id");

        return jdbcTemplate.query(queryBuilder.toString(), new NodeMapper());
    }

    public Optional<Node> getByExternalMappingId(String mappingId) {
        String query = """
                SELECT *
                FROM node
                WHERE node.external_mapping_id = ?""";

        var node = jdbcTemplate.query(query, new NodeMapper(), mappingId);

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

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


    /**
     * Resolves chains of predecessors for a specified destination chain by its ID.
     * If the destination chain does not require predecessors, an empty list is returned.
     * Otherwise, it constructs a list of chains, where each chain represents a sequence of predecessor nodes.
     *
     * @param destinationId The ID of the destination chain whose predecessor chains need to be resolved.
     *                      Must not be null and must correspond to an existing chain.
     * @return A list of chains, where each chain is a list of nodes.
     * Each list represents a sequence of predecessor nodes for the given destination chain.
     * If the destination chain does not require predecessors, a list containing an empty list is returned.
     * @throws RuntimeException If a predecessor chain is not found for a given sequence number in the chain.
     */
    @Transactional
    public List<List<Node>> getChainsById(Integer destinationId) {
        List<List<Node>> resolvedChains = new ArrayList<>();

        /*
         * First we collect all chains. If chain usage is not mandatory we also add
         * an empty chain. So that the chains can be by-passed.
         */

        Node destination = getById(destinationId).orElseThrow();

        if (!destination.getPredecessorRequired())
            resolvedChains.add(Collections.emptyList());

        List<Map<Integer, Integer>> chains = destination.getNodePredecessors();

        chains.forEach(chain -> {

            var currentChain = new ArrayList<Node>(Collections.nCopies(chain.size(), null));
            resolvedChains.add(currentChain);

            /*
             * Going through the key set of the chain's HashMap and putting
             * the Nodes in order of the sequence numbers into a list.
             */

            chain.keySet().forEach(sequenceNumber -> {
                var predecessor = getById(chain.get(sequenceNumber));
                if (predecessor.isEmpty()) {
                    throw new RuntimeException("Predecessor not found for chain " + chain + " and sequence number " + sequenceNumber);
                }
                currentChain.set(sequenceNumber - 1, predecessor.get());
            });

        });


        return resolvedChains;
    }


    @Transactional
    public List<Node> getByDistance(Node node, Integer regionRadius) {

        String query = """
                SELECT * FROM node
                WHERE is_deprecated = FALSE AND id != ? AND
                      (
                        6371 * acos(
                            cos(radians(?)) *
                            cos(radians(geo_lat)) *
                            cos(radians(geo_lng) - radians(?)) +
                            sin(radians(?)) *
                            sin(radians(geo_lat))
                        )
                    ) <= ?
                """;

        return jdbcTemplate.query(query, new NodeMapper(), node.getId(), node.getGeoLat(), node.getGeoLng(), node.getGeoLat(), regionRadius);
    }


    /**
     * Retrieves a list of nodes that are outbound from a given country.
     * The method considers nodes that are either explicitly mapped to the given country
     * or are intermediate nodes within the same country.
     *
     * @param countryId The ID of the country for which outbound nodes are to be retrieved.
     *                  Must not be null.
     * @return A list of nodes that are outbound for the specified country.
     * Returns an empty list if no outbound nodes are found.
     */
    public List<Node> getAllOutboundFor(Integer countryId) {
        String query = """
                SELECT node.*
                FROM node
                LEFT JOIN outbound_country_mapping ON outbound_country_mapping.node_id = node.id
                WHERE node.is_deprecated = FALSE AND (outbound_country_mapping.country_id = ? OR (node.is_intermediate = TRUE AND node.country_id = ?))
                """;

        return jdbcTemplate.query(query, new NodeMapper(), countryId, countryId);
    }

    public List<Node> findNodeListsForReportingByPeriodId(Integer materialId, ValidityTuple tuple) {


        String suppliersSql = """
                SELECT DISTINCT n.*
                FROM premise p
                INNER JOIN calculation_job cj ON p.id = cj.premise_id
                INNER JOIN node n ON p.supplier_node_id = n.id
                WHERE p.material_id = ?
                AND cj.validity_period_id = ?
                AND cj.property_set_id = ?
                AND p.supplier_node_id IS NOT NULL
                AND cj.job_state = 'VALID'
                """;


        return jdbcTemplate.query(suppliersSql, new NodeMapper(), materialId, tuple.periodId(), tuple.propertySetId());
    }

    public Optional<Node> getByDestinationId(Integer id) {

        String query = "SELECT node.* FROM node INNER JOIN premise_destination WHERE node.id = premise_destination.destination_node_id AND premise_destination.id = ?";

        var node = jdbcTemplate.query(query, new NodeMapper(), id);

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

        return Optional.ofNullable(node.getFirst());

    }

    @Transactional
    public Map<String, Integer> getIdsByExternalMappingIds(List<String> mappingIds) {
        if (mappingIds == null || mappingIds.isEmpty()) {
            return new HashMap<>();
        }

        String sql = "SELECT external_mapping_id, id FROM node WHERE external_mapping_id IN (:mappingIds)";

        MapSqlParameterSource parameters = new MapSqlParameterSource();
        parameters.addValue("mappingIds", mappingIds);

        return namedParameterJdbcTemplate.query(sql, parameters, rs -> {
            Map<String, Integer> result = new HashMap<>();
            while (rs.next()) {
                result.put(rs.getString("external_mapping_id"), rs.getInt("id"));
            }
            return result;
        });
    }

    @Transactional
    public Integer insert(Node node) {


        // Insert the main node record
        String insertNodeSql = """
                INSERT INTO node (
                    country_id,
                    name,
                    address,
                    external_mapping_id,
                    predecessor_required,
                    is_destination,
                    is_source,
                    is_intermediate,
                    geo_lat,
                    geo_lng,
                    is_deprecated
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """;

        KeyHolder keyHolder = new GeneratedKeyHolder();

        int rowsInserted = jdbcTemplate.update(connection -> {
            PreparedStatement ps = connection.prepareStatement(insertNodeSql, Statement.RETURN_GENERATED_KEYS);
            ps.setObject(1, node.getCountryId());
            ps.setString(2, node.getName());
            ps.setString(3, node.getAddress());
            ps.setString(4, node.getExternalMappingId());
            ps.setBoolean(5, node.getPredecessorRequired() != null ? node.getPredecessorRequired() : false);
            ps.setBoolean(6, node.getDestination() != null ? node.getDestination() : false);
            ps.setBoolean(7, node.getSource() != null ? node.getSource() : false);
            ps.setBoolean(8, node.getIntermediate() != null ? node.getIntermediate() : false);
            ps.setBigDecimal(9, node.getGeoLat());
            ps.setBigDecimal(10, node.getGeoLng());
            ps.setBoolean(11, node.getDeprecated() != null ? node.getDeprecated() : false);
            return ps;
        }, keyHolder);

        if (rowsInserted == 0) {
            return null;
        }

        Integer nodeId = Objects.requireNonNull(keyHolder.getKey()).intValue();

        // Insert predecessors if they exist
        if (node.getNodePredecessors() != null) {
            for (Map<Integer, Integer> predecessorChain : node.getNodePredecessors()) {
                // Insert new chain
                KeyHolder chainKeyHolder = new GeneratedKeyHolder();
                jdbcTemplate.update(connection -> {
                    PreparedStatement ps = connection.prepareStatement(
                            "INSERT INTO node_predecessor_chain (node_id) VALUES (?)",
                            Statement.RETURN_GENERATED_KEYS);
                    ps.setInt(1, nodeId);
                    return ps;
                }, chainKeyHolder);

                Integer chainId = Objects.requireNonNull(chainKeyHolder.getKey()).intValue();

                // Insert entries for this chain
                for (Map.Entry<Integer, Integer> entry : predecessorChain.entrySet()) {
                    jdbcTemplate.update(
                            "INSERT INTO node_predecessor_entry (node_id, node_predecessor_chain_id, sequence_number) VALUES (?, ?, ?)",
                            entry.getValue(), chainId, entry.getKey()
                    );
                }
            }
        }

        // Insert outbound countries if they exist
        if (node.getOutboundCountries() != null) {
            for (Integer countryId : node.getOutboundCountries()) {
                jdbcTemplate.update(
                        "INSERT INTO outbound_country_mapping (node_id, country_id) VALUES (?, ?)",
                        nodeId, countryId
                );
            }
        }

        return nodeId;
    }

    private class NodeMapper implements RowMapper<Node> {

        @Override
        public Node mapRow(ResultSet rs, int rowNum) throws SQLException {
            var data = new Node();

            data.setId(rs.getInt("id"));
            data.setName(rs.getString("name"));
            data.setAddress(rs.getString("address"));
            data.setCountryId(rs.getInt("country_id"));
            data.setDestination(rs.getBoolean("is_destination"));
            data.setSource(rs.getBoolean("is_source"));
            data.setIntermediate(rs.getBoolean("is_intermediate"));
            data.setPredecessorRequired(rs.getBoolean("predecessor_required"));

            data.setGeoLat(rs.getBigDecimal("geo_lat"));
            data.setGeoLng(rs.getBigDecimal("geo_lng"));

            data.setDeprecated(rs.getBoolean("is_deprecated"));

            data.setExternalMappingId(rs.getString("external_mapping_id"));

            data.setNodePredecessors(getPredecessorsOf(data.getId()));
            data.setOutboundCountries(getOutboundCountriesOf(data.getId()));

            return data;
        }
    }
}
