package com.lingniu.ingest.vehiclestat;

import org.springframework.dao.DuplicateKeyException;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;

import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.OptionalDouble;

public final class JdbcVehicleStatMetricRepository implements VehicleStatRepository {

    private static final String DAILY_MILEAGE_KEY = "daily_mileage_km";
    private static final String DAILY_MILEAGE_UNIT = "km";
    private static final String DAILY_MILEAGE_START_TOTAL_KEY = "daily_mileage_start_total_km";
    private static final String DAILY_MILEAGE_LATEST_TOTAL_KEY = "daily_mileage_latest_total_km";
    private static final String FIRST_TOTAL_COLUMN = "first_total_mileage_km";
    private static final String LATEST_TOTAL_COLUMN = "latest_total_mileage_km";

    private final JdbcTemplate jdbcTemplate;

    public JdbcVehicleStatMetricRepository(JdbcTemplate jdbcTemplate) {
        if (jdbcTemplate == null) {
            throw new IllegalArgumentException("jdbcTemplate must not be null");
        }
        this.jdbcTemplate = jdbcTemplate;
        ensureSchema();
    }

    @Override
    public Optional<VehicleDailyStatResult> findDailyStat(String vin, LocalDate statDate) {
        String normalizedVin = clean(vin);
        List<VehicleDailyStatResult> rows = jdbcTemplate.query("""
                SELECT metric_value, calculation_method
                FROM vehicle_stat_metric
                WHERE vin = ? AND stat_date = ? AND metric_key = ?
                """, (rs, rowNum) -> new VehicleDailyStatResult(
                normalizedVin,
                statDate,
                rs.getBigDecimal("metric_value") == null
                        ? OptionalDouble.empty()
                        : OptionalDouble.of(rs.getBigDecimal("metric_value").doubleValue()),
                DailyMileageStrategy.fromStorage(rs.getString("calculation_method"))),
                normalizedVin, Date.valueOf(statDate), DAILY_MILEAGE_KEY);
        return rows.stream().findFirst();
    }

    @Override
    public Optional<VehicleDailyStatResult> recordDailyMileageSample(String vin, LocalDate statDate, double totalMileageKm) {
        if (!Double.isFinite(totalMileageKm) || totalMileageKm < 0.0) {
            return Optional.empty();
        }
        String normalizedVin = clean(vin);
        Date date = Date.valueOf(statDate);
        String strategy = DailyMileageStrategy.JT808_TOTAL_MILEAGE_DIFF.name();
        DailyMileageState existingState = findDailyMileageState(normalizedVin, date).orElse(null);
        double firstTotalMileage = existingState == null || !Double.isFinite(existingState.firstTotalMileageKm())
                ? totalMileageKm
                : Math.min(existingState.firstTotalMileageKm(), totalMileageKm);
        double latestTotalMileage = existingState == null || !Double.isFinite(existingState.latestTotalMileageKm())
                ? totalMileageKm
                : Math.max(existingState.latestTotalMileageKm(), totalMileageKm);
        double dailyMileageKm = latestTotalMileage - firstTotalMileage;
        upsertDailyMileageMetric(normalizedVin, date, dailyMileageKm, DAILY_MILEAGE_UNIT, strategy,
                firstTotalMileage, latestTotalMileage);
        deleteLegacyMileageStateMetrics(normalizedVin, date);
        VehicleDailyStatResult result = new VehicleDailyStatResult(
                normalizedVin,
                statDate,
                OptionalDouble.of(dailyMileageKm),
                DailyMileageStrategy.JT808_TOTAL_MILEAGE_DIFF);
        return Optional.of(result);
    }

    private Optional<DailyMileageState> findDailyMileageState(String vin, Date statDate) {
        List<DailyMileageState> rows = jdbcTemplate.query("""
                SELECT first_total_mileage_km, latest_total_mileage_km
                FROM vehicle_stat_metric
                WHERE vin = ? AND stat_date = ? AND metric_key = ?
                """, (rs, rowNum) -> new DailyMileageState(
                nullableDouble(rs, FIRST_TOTAL_COLUMN),
                nullableDouble(rs, LATEST_TOTAL_COLUMN)), vin, statDate, DAILY_MILEAGE_KEY);
        return rows.stream()
                .filter(DailyMileageState::hasCompleteState)
                .findFirst();
    }

    private void upsertDailyMileageMetric(String vin, Date statDate, double value, String unit, String strategy,
                                          double firstTotalMileageKm, double latestTotalMileageKm) {
        try {
            jdbcTemplate.update("""
                    INSERT INTO vehicle_stat_metric
                    (vin, stat_date, metric_key, metric_value, metric_unit, calculation_method,
                     first_total_mileage_km, latest_total_mileage_km)
                    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                    """, vin, statDate, DAILY_MILEAGE_KEY, value, unit, strategy,
                    firstTotalMileageKm, latestTotalMileageKm);
        } catch (DuplicateKeyException ex) {
            jdbcTemplate.update("""
                    UPDATE vehicle_stat_metric
                    SET metric_value = ?,
                        metric_unit = ?,
                        calculation_method = ?,
                        first_total_mileage_km = ?,
                        latest_total_mileage_km = ?,
                        updated_at = CURRENT_TIMESTAMP
                    WHERE vin = ? AND stat_date = ? AND metric_key = ?
                    """, value, unit, strategy, firstTotalMileageKm, latestTotalMileageKm,
                    vin, statDate, DAILY_MILEAGE_KEY);
        }
    }

    private void deleteLegacyMileageStateMetrics(String vin, Date statDate) {
        jdbcTemplate.update("""
                DELETE FROM vehicle_stat_metric
                WHERE vin = ? AND stat_date = ? AND metric_key IN (?, ?)
                """, vin, statDate, DAILY_MILEAGE_START_TOTAL_KEY, DAILY_MILEAGE_LATEST_TOTAL_KEY);
    }

    private void ensureSchema() {
        jdbcTemplate.execute("""
                CREATE TABLE IF NOT EXISTS vehicle_stat_metric (
                  vin VARCHAR(64) NOT NULL,
                  stat_date DATE NOT NULL,
                  metric_key VARCHAR(64) NOT NULL,
                  metric_value DECIMAL(18,6) NULL,
                  metric_unit VARCHAR(16) NOT NULL,
                  calculation_method VARCHAR(64) NOT NULL,
                  first_total_mileage_km DECIMAL(18,6) NULL,
                  latest_total_mileage_km DECIMAL(18,6) NULL,
                  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                  PRIMARY KEY (vin, stat_date, metric_key)
                )
                """);
        ensureColumn(FIRST_TOTAL_COLUMN);
        ensureColumn(LATEST_TOTAL_COLUMN);
    }

    private void ensureColumn(String columnName) {
        if (columnExists(columnName)) {
            return;
        }
        jdbcTemplate.execute("ALTER TABLE vehicle_stat_metric ADD COLUMN " + columnName + " DECIMAL(18,6) NULL");
    }

    private boolean columnExists(String columnName) {
        ConnectionCallback<Boolean> callback = connection -> {
            try (ResultSet columns = connection.getMetaData().getColumns(
                    connection.getCatalog(), null, "vehicle_stat_metric", columnName)) {
                if (columns.next()) {
                    return true;
                }
            }
            try (ResultSet columns = connection.getMetaData().getColumns(
                    connection.getCatalog(), null, "VEHICLE_STAT_METRIC", columnName.toUpperCase())) {
                return columns.next();
            }
        };
        return Boolean.TRUE.equals(jdbcTemplate.execute(callback));
    }

    private static double nullableDouble(ResultSet rs, String columnName) throws SQLException {
        double value = rs.getDouble(columnName);
        return rs.wasNull() ? Double.NaN : value;
    }

    private static String clean(String value) {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("vin must not be blank");
        }
        return value.trim();
    }

    private record DailyMileageState(double firstTotalMileageKm, double latestTotalMileageKm) {
        boolean hasCompleteState() {
            return Double.isFinite(firstTotalMileageKm) && Double.isFinite(latestTotalMileageKm);
        }
    }
}
