package chat.vojo.app;

import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;

import androidx.work.Constraints;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;

import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;

import java.util.concurrent.TimeUnit;

/**
 * JS ↔ Android bridge for the WorkManager-based polling fallback.
 *
 * Lifecycle:
 *   - JS calls saveSession({accessToken, homeserverUrl, userId}) on login,
 *     on push (re)enable, and on visibilitychange → visible (to recover a
 *     401-cleared credentials slot without a full remount).
 *   - JS calls schedule({intervalMinutes}) once push is enabled. Idempotent:
 *     KEEP policy means a second schedule() call against an already-enqueued
 *     worker is a no-op (the running period continues unchanged).
 *   - JS calls saveRoomNames({names}) on mount + visibilitychange → visible
 *     so VojoPollWorker has a local cache to resolve room_id → display name
 *     without making N extra GET /rooms/{id}/state/m.room.name requests.
 *     Brand-new rooms created between visibility events fall back to
 *     sender_display_name in the renderer.
 *   - JS calls cancel() + clearSession() on logout / push disable.
 *
 * Worker tag: a single unique periodic worker named UNIQUE_WORK_NAME — KEEP
 * policy prevents schedule churn from re-creating it. Cancel() removes it
 * by the same name.
 */
@CapacitorPlugin(name = "Polling")
public class PollingPlugin extends Plugin {

    private static final String TAG = "PollingPlugin";
    private static final String UNIQUE_WORK_NAME = "vojo_push_poll";

    // Android's hard floor for PeriodicWorkRequest. Requests with shorter
    // intervals are silently clamped to 15 minutes. We accept the requested
    // value from JS but enforce the floor here so misuse from JS doesn't
    // produce a silently-different behavior.
    private static final long MIN_INTERVAL_MINUTES = 15;

    @PluginMethod
    public void saveSession(PluginCall call) {
        String accessToken = call.getString("accessToken");
        String homeserverUrl = call.getString("homeserverUrl");
        if (accessToken == null || accessToken.isEmpty()
            || homeserverUrl == null || homeserverUrl.isEmpty()) {
            call.reject("missing_accessToken_or_homeserverUrl");
            return;
        }
        String userId = call.getString("userId");
        SharedPreferences prefs = getContext()
            .getSharedPreferences(VojoPollWorker.PREFS, Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = prefs.edit()
            .putString(VojoPollWorker.KEY_ACCESS_TOKEN, accessToken)
            .putString(VojoPollWorker.KEY_HOMESERVER_URL, homeserverUrl);
        if (userId != null && !userId.isEmpty()) {
            editor.putString(VojoPollWorker.KEY_USER_ID, userId);
        }
        // Seed the watermark to "now minus a small clock-skew buffer" on the
        // first saveSession after install / logout. Without seeding the
        // Worker's first fire sees watermark=0 and renders every historical
        // unread /notifications entry as a fresh push. The buffer covers the
        // case where the device clock runs ahead of the homeserver's clock —
        // event ts is server-side, so a too-fresh local seed would silently
        // skip recently-arrived events as "older than watermark" forever.
        // 60s tolerates typical NTP drift while still suppressing days-old
        // backlog on first enable. We seed only when the key is absent so
        // subsequent saveSession calls (token rotation, visibilitychange
        // re-bridge) don't reset live state.
        if (!prefs.contains(VojoPollWorker.KEY_LAST_SEEN_TS)) {
            editor.putLong(
                VojoPollWorker.KEY_LAST_SEEN_TS,
                System.currentTimeMillis() - SEED_CLOCK_SKEW_BUFFER_MS
            );
        }
        editor.apply();
        call.resolve();
    }

    private static final long SEED_CLOCK_SKEW_BUFFER_MS = 60_000L;

    @PluginMethod
    public void clearSession(PluginCall call) {
        getContext()
            .getSharedPreferences(VojoPollWorker.PREFS, Context.MODE_PRIVATE)
            .edit()
            .remove(VojoPollWorker.KEY_ACCESS_TOKEN)
            .remove(VojoPollWorker.KEY_HOMESERVER_URL)
            .remove(VojoPollWorker.KEY_USER_ID)
            .remove(VojoPollWorker.KEY_LAST_SEEN_TS)
            .remove(VojoPollWorker.KEY_DRAIN_CURSOR)
            .remove(VojoPollWorker.KEY_DRAIN_TARGET_TS)
            .remove(VojoPollWorker.KEY_NOTIFIED_IDS)
            .remove(VojoPollWorker.KEY_ROOM_NAMES)
            .remove(VojoPollWorker.KEY_USER_AVATARS)
            .apply();
        call.resolve();
    }

    /**
     * user_id → MXC avatar URL snapshot. Mirrors {@link #saveRoomNames} —
     * stored as a JSON blob in vojo_poll_state for the FCM service /
     * polling Worker / ReplyReceiver to consult via
     * VojoFirebaseMessagingService.lookupUserAvatarMxc. JS dumps on the
     * same lifecycle triggers as room names (mount, visibility resume,
     * m.direct change, m.room.encryption flip).
     */
    @PluginMethod
    public void saveUserAvatars(PluginCall call) {
        JSObject avatars = call.getObject("avatars");
        if (avatars == null) {
            call.reject("missing_avatars");
            return;
        }
        String serialized = avatars.toString();
        getContext()
            .getSharedPreferences(VojoPollWorker.PREFS, Context.MODE_PRIVATE)
            .edit()
            .putString(VojoPollWorker.KEY_USER_AVATARS, serialized)
            .apply();
        Log.i(TAG, "saveUserAvatars: " + avatars.length() + " entries, "
            + serialized.length() + " bytes");
        call.resolve();
    }

    @PluginMethod
    public void saveRoomNames(PluginCall call) {
        JSObject names = call.getObject("names");
        if (names == null) {
            // Empty map is also valid (user cleared all rooms) — JS passes
            // {} explicitly in that case; missing key is a contract bug.
            call.reject("missing_names");
            return;
        }
        // `JSObject extends JSONObject`, so names.toString() is already a
        // valid JSON serialisation of validated values — no need to re-parse
        // it through `new JSONObject(...)` just to re-serialise. Persist
        // verbatim.
        String serialized = names.toString();
        getContext()
            .getSharedPreferences(VojoPollWorker.PREFS, Context.MODE_PRIVATE)
            .edit()
            .putString(VojoPollWorker.KEY_ROOM_NAMES, serialized)
            .apply();
        Log.i(TAG, "saveRoomNames: " + names.length() + " entries, "
            + serialized.length() + " bytes");
        call.resolve();
    }

    @PluginMethod
    public void schedule(PluginCall call) {
        Integer intervalMinutes = call.getInt("intervalMinutes", 15);
        long interval = Math.max(MIN_INTERVAL_MINUTES, intervalMinutes != null ? intervalMinutes : 15);

        Constraints constraints = new Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build();

        PeriodicWorkRequest req = new PeriodicWorkRequest.Builder(
            VojoPollWorker.class, interval, TimeUnit.MINUTES
        )
            .setConstraints(constraints)
            .addTag("vojo_push_poll")
            .build();

        try {
            WorkManager.getInstance(getContext())
                .enqueueUniquePeriodicWork(
                    UNIQUE_WORK_NAME,
                    ExistingPeriodicWorkPolicy.KEEP,
                    req
                );
            Log.d(TAG, "scheduled periodic poll every " + interval + " minutes");
            call.resolve();
        } catch (Throwable t) {
            Log.w(TAG, "schedule failed", t);
            call.reject("schedule_failed: " + t.getMessage());
        }
    }

    /**
     * Dismiss the per-room MessagingStyle notification + clear the in-memory
     * RoomMessageCache for the room. Called from the JS receipt listener when
     * a server-side read receipt zeroes the unread count (the user read on
     * another device / tab). No-op if the notification was never posted or
     * has already been swiped away.
     */
    @PluginMethod
    public void dismissRoom(PluginCall call) {
        String roomId = call.getString("roomId");
        if (roomId == null || roomId.isEmpty()) {
            call.reject("missing_roomId");
            return;
        }
        VojoFirebaseMessagingService.dismissRoomNotification(getContext(), roomId);
        call.resolve();
    }

    @PluginMethod
    public void cancel(PluginCall call) {
        try {
            // Block on the Operation so callers awaiting cancel() see the
            // cancel committed to WorkManager's database before we resolve.
            // (NOTE: this does NOT interrupt a Worker that's already mid
            // doWork(); cooperative cancellation via isStopped() is owned
            // by VojoPollWorker itself.) Without this wait a fast
            // disable→reenable sequence races with ExistingPeriodicWorkPolicy.KEEP
            // — the second enqueueUniquePeriodicWork can land before the
            // cancel is committed and become a no-op. We're already off
            // the main thread (Capacitor dispatches plugin calls on its
            // own executor), so the blocking get() is safe here.
            WorkManager.getInstance(getContext())
                .cancelUniqueWork(UNIQUE_WORK_NAME)
                .getResult()
                .get();
            Log.d(TAG, "cancelled periodic poll");
            call.resolve();
        } catch (Throwable t) {
            Log.w(TAG, "cancel failed", t);
            call.reject("cancel_failed: " + t.getMessage());
        }
    }
}
