package com.grigowashere.aismap.ble.hub;

import android.util.Log;

/**
 * Tracks clock offset between the AIS Hub server ("hub time") and this device
 * ("device wall clock"). Used to translate absolute server timestamps
 * (`ts`, `server_time`, `last_dynamic_ts`, ...) into device-local epoch seconds
 * so that stale-data logic (which works against {@code LocalDateTime.now()})
 * stays correct even when the two clocks disagree.
 *
 * <p>Thread-safety: all state is static/volatile; updates are cheap and safe
 * from any thread.
 */
public final class HubTimeSync {

    private static final String TAG = "HubTimeSync";

    /**
     * Minimum server epoch we consider plausible (2010-01-01). Anything below is
     * ignored.
     */
    private static final double MIN_EPOCH = 1262304000.0;

    /**
     * Clamp absurd offsets (1 year) to protect from a single bad sample.
     */
    private static final double MAX_ABS_OFFSET_SEC = 365L * 24 * 3600;

    private static volatile boolean hasOffset = false;
    /** offsetSec = serverTime - deviceTime; deviceTime = serverTime - offsetSec. */
    private static volatile double offsetSec = 0.0;
    private static volatile long lastUpdateElapsedRealtimeMs = 0L;

    private HubTimeSync() {}

    /**
     * Feeds a server timestamp (epoch seconds, possibly milliseconds) and the
     * device wall-clock moment when it was observed. Values that look broken
     * are dropped.
     */
    public static void updateFromServerSeconds(double serverEpochSec) {
        if (Double.isNaN(serverEpochSec) || Double.isInfinite(serverEpochSec)) return;
        // Some payloads send milliseconds in a double; normalize.
        if (serverEpochSec > 1e12) serverEpochSec /= 1000.0;
        if (serverEpochSec < MIN_EPOCH) return;
        double deviceNowSec = System.currentTimeMillis() / 1000.0;
        double newOffset = serverEpochSec - deviceNowSec;
        if (Math.abs(newOffset) > MAX_ABS_OFFSET_SEC) {
            Log.w(TAG, "Ignoring implausible offset " + newOffset + "s (server=" + serverEpochSec + ")");
            return;
        }
        // Light exponential smoothing to absorb jitter without lagging a real clock drift.
        if (hasOffset) {
            offsetSec = 0.2 * newOffset + 0.8 * offsetSec;
        } else {
            offsetSec = newOffset;
        }
        hasOffset = true;
        lastUpdateElapsedRealtimeMs = android.os.SystemClock.elapsedRealtime();
    }

    public static boolean hasOffset() {
        return hasOffset;
    }

    public static double getOffsetSec() {
        return offsetSec;
    }

    /**
     * Converts a server-side epoch-seconds timestamp into a device-side
     * epoch-seconds timestamp using the learned offset. If no offset has been
     * observed yet, returns {@code serverEpochSec} unchanged (best effort).
     */
    public static double toDeviceEpochSeconds(double serverEpochSec) {
        if (Double.isNaN(serverEpochSec) || serverEpochSec <= 0) return serverEpochSec;
        if (serverEpochSec > 1e12) serverEpochSec /= 1000.0;
        if (!hasOffset) return serverEpochSec;
        return serverEpochSec - offsetSec;
    }

    /**
     * Reset state (e.g. on BLE stop). Primarily useful for tests.
     */
    public static void reset() {
        hasOffset = false;
        offsetSec = 0.0;
        lastUpdateElapsedRealtimeMs = 0L;
    }
}
