package chat.vojo.app;

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

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Handles the per-notification "Mark as read" action.
 *
 * Posts {@code POST /_matrix/client/v3/rooms/{roomId}/receipt/m.read/{eventId}}
 * using the access token saved by the polling lifecycle in
 * {@code vojo_poll_state} SharedPreferences (same storage VojoPollWorker uses;
 * keeps the credential lifecycle single-sourced). After a successful 2xx the
 * per-room MessagingStyle notification is dismissed and the
 * {@link RoomMessageCache} is cleared so the next push to that room starts a
 * fresh conversation rather than re-appending to the prior history.
 *
 * Dismiss policy: OPTIMISTIC. The per-room notification is dismissed
 * synchronously in onReceive — before the HTTP receipt PUT is even
 * attempted — so the user sees instant feedback. The async receipt POST
 * happens on a worker thread afterwards. This mirrors element-android's
 * NotificationBroadcastReceiver pattern and matches the user's mental
 * model ("I tapped, it should disappear immediately").
 *
 * Failure mode: on any non-2xx or thrown exception we accept that the
 * server-side read receipt did not land. We do NOT re-post the
 * notification or implement a flusher because:
 *   - the next room open from the JS app issues a fresh read-receipt
 *     for the latest visible event, catching up the server state
 *   - the in-app read-marker logic is the authoritative path; this
 *     receiver is a convenience for the shade-tap shortcut
 *   - accumulating tombstones in prefs (the CallDeclineReceiver pattern)
 *     would risk leaking historical eventIds the JS side would re-issue
 *     on app resume anyway
 *
 * Null-credential edge case (fresh install + first push before any
 * saveSession bridge): no token to use, we still dismiss the notification
 * locally so the user isn't stuck looking at a "stuck" Mark-as-read
 * button. The next room open from JS covers the server view.
 */
public class MarkAsReadReceiver extends BroadcastReceiver {

    public static final String ACTION_MARK_AS_READ = "chat.vojo.app.MARK_AS_READ";
    public static final String EXTRA_ROOM_ID = "room_id";
    public static final String EXTRA_EVENT_ID = "event_id";

    private static final int CONNECT_TIMEOUT_MS = 8_000;
    private static final int READ_TIMEOUT_MS = 8_000;
    private static final String TAG = "MarkAsReadRcvr";

    private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor();

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent == null) return;
        final String roomId = intent.getStringExtra(EXTRA_ROOM_ID);
        final String eventId = intent.getStringExtra(EXTRA_EVENT_ID);
        if (roomId == null || roomId.isEmpty()) {
            Log.w(TAG, "onReceive: missing room_id, abort");
            return;
        }

        final Context appContext = context.getApplicationContext();
        // Dismiss first for instant UX feedback — HTTP latency is irrelevant
        // to the perceived "marked as read" action.
        VojoFirebaseMessagingService.dismissRoomNotification(appContext, roomId);

        final SharedPreferences prefs = appContext.getSharedPreferences(
            VojoPollWorker.PREFS, Context.MODE_PRIVATE);
        final String token = prefs.getString(VojoPollWorker.KEY_ACCESS_TOKEN, null);
        final String homeserver = prefs.getString(VojoPollWorker.KEY_HOMESERVER_URL, null);
        if (token == null || token.isEmpty() || homeserver == null || homeserver.isEmpty()) {
            Log.w(TAG, "onReceive: no credentials in prefs, local dismiss only");
            return;
        }
        if (eventId == null || eventId.isEmpty()) {
            // Without an eventId we cannot issue a receipt PUT — the JS-side
            // read-marker handler will catch this up on the next room open.
            Log.w(TAG, "onReceive: no event_id, local dismiss only");
            return;
        }

        final PendingResult pendingResult = goAsync();
        EXECUTOR.execute(() -> {
            try {
                int status = sendReceipt(appContext, homeserver, token, roomId, eventId);
                if (status >= 200 && status < 300) {
                    if (BuildConfig.DEBUG) {
                        Log.d(TAG, "receipt ok status=" + status + " room=" + roomId);
                    }
                } else {
                    Log.w(TAG, "receipt non-2xx status=" + status + " room=" + roomId);
                }
            } catch (Throwable t) {
                Log.w(TAG, "receipt threw room=" + roomId, t);
            } finally {
                pendingResult.finish();
            }
        });
    }

    private int sendReceipt(
        Context context,
        String baseUrl,
        String accessToken,
        String roomId,
        String eventId
    ) throws IOException {
        String url = trimTrailingSlash(baseUrl)
            + "/_matrix/client/v3/rooms/"
            + URLEncoder.encode(roomId, "UTF-8")
            + "/receipt/m.read/"
            + URLEncoder.encode(eventId, "UTF-8");

        // Route through the loopback relay when a proxy is enabled (§15 #3).
        HttpURLConnection conn = ProxyRouting.openConnection(context, new URL(url));
        try {
            conn.setRequestMethod("POST");
            conn.setRequestProperty("Authorization", "Bearer " + accessToken);
            conn.setRequestProperty("Content-Type", "application/json");
            conn.setConnectTimeout(CONNECT_TIMEOUT_MS);
            conn.setReadTimeout(READ_TIMEOUT_MS);
            conn.setDoOutput(true);
            // Empty JSON body per spec; setFixedLengthStreamingMode keeps the
            // connection on the cached path instead of chunked-transfer fallback.
            byte[] payload = "{}".getBytes("UTF-8");
            conn.setFixedLengthStreamingMode(payload.length);
            try (java.io.OutputStream os = conn.getOutputStream()) {
                os.write(payload);
            }
            return conn.getResponseCode();
        } finally {
            conn.disconnect();
        }
    }

    private static String trimTrailingSlash(String s) {
        return (s != null && s.endsWith("/")) ? s.substring(0, s.length() - 1) : s;
    }
}
