package com.foxx.androidcast.network;

import com.foxx.androidcast.AppPreferences;

import org.json.JSONObject;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Backend heartbeat NTP correction (developer setting).
 * When |skew| exceeds {@link #SYNC_THRESHOLD_MS}, stores offset for app-local "corrected" time.
 * Does not change the system clock.
 */
public final class BackendNtpSync {
    /** ±3 minutes — sync when skew exceeds this. */
    public static final long SYNC_THRESHOLD_MS = 3L * 60L * 1000L;

    private static final Pattern TZ_OFFSET = Pattern.compile("([+-])(\\d{2})(\\d{2})");

    private BackendNtpSync() {}

    public enum Action {
        DISABLED,
        WITHIN_THRESHOLD,
        SYNCED,
        FAILED
    }

    public static final class Result {
        public final Action action;
        public final long skewMs;
        public final long appliedOffsetMs;
        public final String serverWallBeTz;
        public final String localWallBeTz;
        public final String detail;

        Result(Action action, long skewMs, long appliedOffsetMs,
                String serverWallBeTz, String localWallBeTz, String detail) {
            this.action = action;
            this.skewMs = skewMs;
            this.appliedOffsetMs = appliedOffsetMs;
            this.serverWallBeTz = serverWallBeTz;
            this.localWallBeTz = localWallBeTz;
            this.detail = detail;
        }
    }

    /** Corrected wall time for diagnostics when sync is active. */
    public static long correctedNowMs(android.content.Context context) {
        if (!AppPreferences.isDevBackendNtpSyncEnabled(context)) {
            return System.currentTimeMillis();
        }
        return System.currentTimeMillis() + AppPreferences.getDevBackendNtpOffsetMs(context);
    }

    /**
     * @param requestStartMs wall clock when POST started
     * @param requestEndMs wall clock when response body was read
     * @param clientEpochSec epoch seconds sent in heartbeat.date
     */
    public static Result evaluateResponse(
            android.content.Context context,
            long requestStartMs,
            long requestEndMs,
            long clientEpochSec,
            String responseBody) {
        if (!AppPreferences.isDevBackendNtpSyncEnabled(context)) {
            return new Result(Action.DISABLED, 0L, 0L, "", "", "dev setting off");
        }
        try {
            JSONObject root = new JSONObject(responseBody != null ? responseBody : "{}");
            JSONObject hb = root.optJSONObject("heartbeat");
            if (hb == null) {
                return fail("missing heartbeat object");
            }
            long serverEpochSec = hb.optLong("server_date", 0L);
            String serverTz = hb.optString("server_tz", "");
            if (serverEpochSec <= 0L) {
                return fail("missing server_date");
            }
            TimeZone beTz = parseBeTimeZone(serverTz);
            long skewMs = computeSkewMs(requestStartMs, requestEndMs, serverEpochSec);
            long serverMs = serverEpochSec * 1000L;
            long localMidMs = requestStartMs + Math.max(0L, requestEndMs - requestStartMs) / 2L;

            long correctionFromBe = hb.has("correction_seconds")
                    ? hb.optLong("correction_seconds", 0L) * 1000L
                    : (serverEpochSec - clientEpochSec) * 1000L;

            String serverWall = formatWallClock(serverMs, beTz);
            String localWallInBeTz = formatWallClock(localMidMs, beTz);
            String summary = String.format(Locale.US,
                    "skew=%d ms (BE correction=%d ms) · BE %s · local→BE %s",
                    skewMs, correctionFromBe, serverWall, localWallInBeTz);

            if (Math.abs(skewMs) > SYNC_THRESHOLD_MS) {
                AppPreferences.setDevBackendNtpOffsetMs(context, skewMs);
                AppPreferences.setDevBackendNtpLastSyncMs(context, System.currentTimeMillis());
                AppPreferences.setDevBackendNtpLastSkewMs(context, skewMs);
                return new Result(Action.SYNCED, skewMs, skewMs,
                        serverWall, localWallInBeTz, summary + " · offset stored");
            }
            AppPreferences.setDevBackendNtpLastSkewMs(context, skewMs);
            return new Result(Action.WITHIN_THRESHOLD, skewMs,
                    AppPreferences.getDevBackendNtpOffsetMs(context),
                    serverWall, localWallInBeTz, summary + " · within ±3 min");
        } catch (Exception e) {
            return fail(e.getClass().getSimpleName() + ": " + e.getMessage());
        }
    }

    private static Result fail(String detail) {
        return new Result(Action.FAILED, 0L, 0L, "", "", detail);
    }

    static long computeSkewMs(long requestStartMs, long requestEndMs, long serverEpochSec) {
        long serverMs = serverEpochSec * 1000L;
        long localMidMs = requestStartMs + Math.max(0L, requestEndMs - requestStartMs) / 2L;
        return serverMs - localMidMs;
    }

    static boolean shouldSync(long skewMs) {
        return Math.abs(skewMs) > SYNC_THRESHOLD_MS;
    }

    static TimeZone parseBeTimeZone(String serverTz) {
        if (serverTz == null || serverTz.isEmpty()) {
            return TimeZone.getDefault();
        }
        Matcher m = TZ_OFFSET.matcher(serverTz.trim());
        if (!m.matches()) {
            return TimeZone.getDefault();
        }
        String sign = m.group(1);
        int hours = Integer.parseInt(m.group(2));
        int mins = Integer.parseInt(m.group(3));
        String id = String.format(Locale.US, "GMT%s%02d:%02d", sign, hours, mins);
        return TimeZone.getTimeZone(id);
    }

    private static String formatWallClock(long epochMs, TimeZone tz) {
        SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US);
        fmt.setTimeZone(tz);
        return fmt.format(new Date(epochMs));
    }
}
