package com.lingniu.ingest.tdenginehistory;

import javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;

public final class TdengineJdbcHistoryReader implements TdengineHistoryReader {

    private final DataSource dataSource;
    private final TdengineHistoryQueries queries;

    public TdengineJdbcHistoryReader(DataSource dataSource, TdengineHistorySchema schema) {
        if (dataSource == null) {
            throw new IllegalArgumentException("dataSource must not be null");
        }
        this.dataSource = dataSource;
        this.queries = new TdengineHistoryQueries(schema);
    }

    @Override
    public TdenginePage<TdengineRawFrameRow> queryRawFrames(TdengineRawFrameQuery query) throws IOException {
        int pageSize = Math.min(query.limit(), 1000);
        TdengineQueryStatement statement = queries.rawFrames(query.withLimit(pageSize + 1));
        return readPage(statement, pageSize, this::rawFrame, row -> new TdenginePageCursor(row.ts(), row.frameId()));
    }

    @Override
    public TdenginePage<TdengineLocationRow> queryLocations(TdengineLocationQuery query) throws IOException {
        int pageSize = Math.min(query.limit(), 1000);
        TdengineQueryStatement statement = queries.locations(query.withLimit(pageSize + 1));
        return readPage(statement, pageSize, this::location, row -> new TdenginePageCursor(row.ts(), row.factId()));
    }

    @Override
    public List<TdengineLocationRow> queryLocationsByRawUri(String protocol, String rawUri, int limit)
            throws IOException {
        if (rawUri == null || rawUri.isBlank()) {
            return List.of();
        }
        TdengineQueryStatement statement = queries.locationsByRawUri(protocol, rawUri, limit);
        return readList(statement, this::location);
    }

    private <T> TdenginePage<T> readPage(TdengineQueryStatement statement,
                                         int pageSize,
                                         SqlRowMapper<T> mapper,
                                         Function<T, TdenginePageCursor> cursor) throws IOException {
        List<T> rows = new ArrayList<>();
        try (Connection connection = dataSource.getConnection();
             PreparedStatement prepared = connection.prepareStatement(statement.sql())) {
            bind(prepared, statement.values());
            try (ResultSet resultSet = prepared.executeQuery()) {
                while (resultSet.next()) {
                    rows.add(mapper.map(resultSet));
                }
            }
        } catch (SQLException e) {
            if (isTableMissing(e)) {
                return new TdenginePage<>(List.of(), Optional.empty());
            }
            throw new IOException("tdengine history query failed", e);
        }
        boolean hasMore = rows.size() > pageSize;
        List<T> items = hasMore ? List.copyOf(rows.subList(0, pageSize)) : List.copyOf(rows);
        Optional<TdenginePageCursor> next = hasMore && !items.isEmpty()
                ? Optional.of(cursor.apply(items.getLast()))
                : Optional.empty();
        return new TdenginePage<>(items, next);
    }

    private <T> List<T> readList(TdengineQueryStatement statement, SqlRowMapper<T> mapper) throws IOException {
        List<T> rows = new ArrayList<>();
        try (Connection connection = dataSource.getConnection();
             PreparedStatement prepared = connection.prepareStatement(statement.sql())) {
            bind(prepared, statement.values());
            try (ResultSet resultSet = prepared.executeQuery()) {
                while (resultSet.next()) {
                    rows.add(mapper.map(resultSet));
                }
            }
        } catch (SQLException e) {
            if (isTableMissing(e)) {
                return List.of();
            }
            throw new IOException("tdengine history query failed", e);
        }
        return List.copyOf(rows);
    }

    private static boolean isTableMissing(SQLException e) {
        for (Throwable current = e; current != null; current = current.getCause()) {
            String message = current.getMessage();
            if (message != null) {
                String normalized = message.toLowerCase();
                if (normalized.contains("table does not exist")
                        || normalized.contains("0x2603")
                        || normalized.contains("table not exist")) {
                    return true;
                }
            }
        }
        return false;
    }

    private static void bind(PreparedStatement prepared, List<Object> values) throws SQLException {
        for (int i = 0; i < values.size(); i++) {
            Object value = values.get(i);
            int parameterIndex = i + 1;
            if (value instanceof Instant instant) {
                prepared.setTimestamp(parameterIndex, Timestamp.from(instant));
            } else {
                prepared.setObject(parameterIndex, value);
            }
        }
    }

    private TdengineRawFrameRow rawFrame(ResultSet rs) throws SQLException {
        return new TdengineRawFrameRow(
                instant(rs, "ts"),
                rs.getString("frame_id"),
                instant(rs, "received_at"),
                rs.getInt("message_id"),
                rs.getInt("sub_type"),
                instant(rs, "event_time"),
                rs.getString("raw_uri"),
                rs.getString("checksum"),
                rs.getLong("raw_size_bytes"),
                rs.getString("parse_status"),
                rs.getString("parse_error"),
                rs.getString("peer"),
                rs.getString("metadata_json"),
                rs.getString("parsed_json"),
                rs.getString("protocol"),
                rs.getString("vehicle_key"),
                rs.getString("vin"),
                rs.getString("phone")
        );
    }

    private TdengineLocationRow location(ResultSet rs) throws SQLException {
        return new TdengineLocationRow(
                instant(rs, "ts"),
                rs.getString("fact_id"),
                rs.getString("frame_id"),
                instant(rs, "received_at"),
                rs.getDouble("longitude"),
                rs.getDouble("latitude"),
                rs.getDouble("altitude_m"),
                rs.getDouble("speed_kmh"),
                rs.getDouble("direction_deg"),
                rs.getLong("alarm_flag"),
                rs.getLong("status_flag"),
                nullableDouble(rs, "total_mileage_km"),
                rs.getString("raw_uri"),
                rs.getString("protocol"),
                rs.getString("vehicle_key"),
                rs.getString("vin"),
                rs.getString("phone")
        );
    }

    private static Instant instant(ResultSet rs, String column) throws SQLException {
        Timestamp timestamp = rs.getTimestamp(column);
        return timestamp == null ? null : timestamp.toInstant();
    }

    private static Double nullableDouble(ResultSet rs, String column) throws SQLException {
        Object value = rs.getObject(column);
        return value instanceof Number number ? number.doubleValue() : null;
    }

    @FunctionalInterface
    private interface SqlRowMapper<T> {
        T map(ResultSet rs) throws SQLException;
    }
}
