package de.avatic.lcc.repositories.error;


import de.avatic.lcc.model.db.error.SysError;
import de.avatic.lcc.model.db.error.SysErrorTraceItem;
import de.avatic.lcc.model.db.error.SysErrorType;
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.*;
import java.util.stream.Collectors;

@Repository
public class SysErrorRepository {

    private final JdbcTemplate jdbcTemplate;
    private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;

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

    @Transactional
    public Integer insert(SysError error) {

        String errorSql = "INSERT INTO sys_error (user_id, title, code, message, pinia, calculation_job_id, bulk_operation_id, type, request) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";

        KeyHolder keyHolder = new GeneratedKeyHolder();

        jdbcTemplate.update(connection -> {
            PreparedStatement ps = connection.prepareStatement(errorSql, Statement.RETURN_GENERATED_KEYS);
            ps.setObject(1, error.getUserId()); // Use setObject for nullable Integer
            ps.setString(2, error.getTitle());
            ps.setString(3, error.getCode());
            ps.setString(4, error.getMessage());
            ps.setString(5, error.getPinia());
            ps.setObject(6, error.getCalculationJobId()); // Use setObject for nullable Integer
            ps.setObject(7, error.getBulkOperationId()); // Use setObject for nullable Integer
            ps.setString(8, error.getType().name());
            ps.setString(9, error.getRequest());
            return ps;
        }, keyHolder);

        // Get the generated error ID
        Integer errorId = Objects.requireNonNull(keyHolder.getKey()).intValue();

        // Insert trace items if they exist
        if (error.getTrace() != null && !error.getTrace().isEmpty()) {
            insertTraceItems(errorId, error.getTrace());
        }

        return errorId;

    }

    @Transactional
    public void insert(List<SysError> errors) {
        // First insert the sys_error records
        String errorSql = "INSERT INTO sys_error (user_id, title, code, message, pinia, calculation_job_id, bulk_operation_id, type, request) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";

        KeyHolder keyHolder = new GeneratedKeyHolder();

        for (SysError error : errors) {
            jdbcTemplate.update(connection -> {
                PreparedStatement ps = connection.prepareStatement(errorSql, Statement.RETURN_GENERATED_KEYS);
                ps.setObject(1, error.getUserId()); // Use setObject for nullable Integer
                ps.setString(2, error.getTitle());
                ps.setString(3, error.getCode());
                ps.setString(4, error.getMessage());
                ps.setString(5, error.getPinia());
                ps.setObject(6, error.getCalculationJobId()); // Use setObject for nullable Integer
                ps.setObject(7, error.getBulkOperationId()); // Use setObject for nullable Integer
                ps.setString(8, error.getType().name());
                ps.setString(9, error.getRequest());
                return ps;
            }, keyHolder);

            // Get the generated error ID
            Integer errorId = Objects.requireNonNull(keyHolder.getKey()).intValue();

            // Insert trace items if they exist
            if (error.getTrace() != null && !error.getTrace().isEmpty()) {
                insertTraceItems(errorId, error.getTrace());
            }
        }
    }

    private void insertTraceItems(Integer errorId, List<SysErrorTraceItem> traceItems) {
        String traceSql = "INSERT INTO sys_error_trace_item (error_id, line, file, method, fullPath) VALUES (?, ?, ?, ?, ?)";

        jdbcTemplate.batchUpdate(traceSql, traceItems, traceItems.size(),
                (ps, traceItem) -> {
                    ps.setInt(1, errorId);
                    ps.setObject(2, traceItem.getLine()); // Use setObject for nullable Integer
                    ps.setString(3, traceItem.getFile());
                    ps.setString(4, traceItem.getMethod());
                    ps.setString(5, traceItem.getFullPath());
                });
    }

    @Transactional
    public SearchQueryResult<SysError> listErrors(Optional<String> filter, SearchQueryPagination pagination) {
        StringBuilder whereClause = new StringBuilder();
        MapSqlParameterSource parameters = new MapSqlParameterSource();

        // Build WHERE clause if filter is provided
        if (filter.isPresent() && !filter.get().trim().isEmpty()) {
            String filterValue = "%" + filter.get().trim() + "%";
            whereClause.append(" WHERE (e.title LIKE :filter OR e.message LIKE :filter OR e.code LIKE :filter)");
            parameters.addValue("filter", filterValue);
        }

        // Count total elements
        String countSql = "SELECT COUNT(*) FROM sys_error e" + whereClause;
        Integer totalElements = namedParameterJdbcTemplate.queryForObject(countSql, parameters, Integer.class);

        // Build main query with pagination
        String sql = """
                SELECT e.id, e.user_id, e.title, e.code, e.message, e.pinia,
                       e.calculation_job_id, e.bulk_operation_id, e.type, e.created_at, e.request
                FROM sys_error e
                """ + whereClause + """
                ORDER BY e.created_at DESC
                LIMIT :limit OFFSET :offset
                """;

        // Add pagination parameters
        parameters.addValue("limit", pagination.getLimit());
        parameters.addValue("offset", pagination.getOffset());

        // Execute query
        List<SysError> errors = namedParameterJdbcTemplate.query(sql, parameters, new SysErrorMapper());

        // Load trace items for each error
        if (!errors.isEmpty()) {
            loadTraceItemsForErrors(errors);
        }

        return new SearchQueryResult<>(errors, pagination.getPage(), totalElements, pagination.getLimit());
    }

    private void loadTraceItemsForErrors(List<SysError> errors) {
        // Get all error IDs
        List<Integer> errorIds = errors.stream()
                .map(SysError::getId)
                .toList();

        if (errorIds.isEmpty()) {
            return;
        }

        String traceSql = """
                SELECT error_id, id, line, file, method, fullPath
                FROM sys_error_trace_item
                WHERE error_id IN (:errorIds)
                ORDER BY error_id, id
                """;

        MapSqlParameterSource traceParameters = new MapSqlParameterSource("errorIds", errorIds);

        // Query trace items
        List<SysErrorTraceItem> allTraceItems = namedParameterJdbcTemplate.query(
                traceSql,
                traceParameters,
                new SysErrorTraceItemMapper()
        );

        // Group trace items by error ID
        Map<Integer, List<SysErrorTraceItem>> traceItemsByErrorId = allTraceItems.stream()
                .collect(Collectors.groupingBy(SysErrorTraceItem::getErrorId));

        // Assign trace items to their respective errors
        errors.forEach(error -> {
            List<SysErrorTraceItem> traceItems = traceItemsByErrorId.getOrDefault(error.getId(), new ArrayList<>());
            error.setTrace(traceItems);
        });
    }

    public Optional<SysError> getByBulkOperationId(Integer id) {

        String sql = """
                SELECT * FROM sys_error WHERE bulk_operation_id = :id
                """;

        MapSqlParameterSource parameters = new MapSqlParameterSource("id", id);

        List<SysError> errors = namedParameterJdbcTemplate.query(sql, parameters, new SysErrorMapper());

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

        SysError error = errors.getFirst();
        loadTraceItemsForErrors(List.of(error));
        return Optional.of(error);
    }

    public static class SysErrorMapper implements RowMapper<SysError> {

        @Override
        public SysError mapRow(ResultSet rs, int rowNum) throws SQLException {
            SysError error = new SysError();
            error.setId(rs.getInt("id"));
            error.setUserId(rs.getObject("user_id", Integer.class));
            error.setTitle(rs.getString("title"));
            error.setCode(rs.getString("code"));
            error.setMessage(rs.getString("message"));
            error.setPinia(rs.getString("pinia"));
            error.setRequest(rs.getString("request"));
            error.setCalculationJobId(rs.getObject("calculation_job_id", Integer.class));
            error.setBulkOperationId(rs.getObject("bulk_operation_id", Integer.class));
            error.setType(SysErrorType.valueOf(rs.getString("type")));
            error.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
            return error;
        }
    }

    public static class SysErrorTraceItemMapper implements RowMapper<SysErrorTraceItem> {

        @Override
        public SysErrorTraceItem mapRow(ResultSet rs, int rowNum) throws SQLException {
            SysErrorTraceItem traceItem = new SysErrorTraceItem();
            traceItem.setErrorId(rs.getInt("error_id"));
            traceItem.setId(rs.getInt("id"));
            traceItem.setLine(rs.getObject("line", Integer.class));
            traceItem.setFile(rs.getString("file"));
            traceItem.setMethod(rs.getString("method"));
            traceItem.setFullPath(rs.getString("fullPath"));

            return traceItem;
        }
    }

}
