package chat.vojo.app;

import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;

import androidx.core.app.NotificationCompat;
import androidx.core.app.RemoteInput;

import org.json.JSONObject;
import org.json.JSONException;

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

/**
 * Handles the inline-reply RemoteInput action on a per-room MessagingStyle
 * notification.
 *
 * Flow:
 *   1. User taps reply, types text, presses send → broadcast fires here.
 *   2. We immediately append the outgoing message to RoomMessageCache and
 *      re-post the notification (instant UX feedback — the message appears
 *      as a self-Person bubble in the conversation while the HTTP is in
 *      flight).
 *   3. PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message/{txnId}
 *      with {msgtype: "m.text", body}. Uses the vojo_poll_state token (same
 *      storage as Worker / MarkAsReadReceiver — single credential lifecycle).
 *   4. On 2xx: nothing further; the JS sync echo will eventually replace
 *      the local-echo bubble in-app.
 *   5. On non-2xx or thrown: post a small error notification "Could not
 *      send your reply" so the user knows to retry from in-app — better
 *      than silently swallowing the message.
 *
 * E2EE rooms are guarded UP-STREAM in VojoFirebaseMessagingService.
 * renderMessageNotification: we don't even attach the reply action when
 * RoomMetadata.isEncrypted is true. So this receiver never has to encrypt.
 * Defense in depth: if a stale notification with the action ever survives
 * an encryption flip we still detect the failure as a non-2xx HTTP and
 * surface the error notification rather than sending cleartext (which
 * Synapse would in any case reject for an encrypted room).
 *
 * Null-credential edge case: post the error notification so the user
 * notices and retries in-app. Same logic as a network failure.
 */
public class ReplyReceiver extends BroadcastReceiver {

    public static final String ACTION_REPLY = "chat.vojo.app.REPLY";
    public static final String EXTRA_ROOM_ID = "room_id";
    public static final String KEY_TEXT_REPLY = "vojo.text_reply";

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

    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);
        if (roomId == null || roomId.isEmpty()) {
            Log.w(TAG, "onReceive: missing room_id, abort");
            return;
        }

        Bundle remote = RemoteInput.getResultsFromIntent(intent);
        if (remote == null) {
            Log.w(TAG, "onReceive: no RemoteInput results");
            return;
        }
        CharSequence reply = remote.getCharSequence(KEY_TEXT_REPLY);
        if (reply == null) {
            Log.w(TAG, "onReceive: RemoteInput missing text");
            return;
        }
        final String text = reply.toString().trim();
        if (text.isEmpty()) return;

        final Context appContext = context.getApplicationContext();

        // Pre-flight validation BEFORE the optimistic echo. Posting a self
        // bubble first and then immediately stacking an error notif on top
        // is jarring UX; for predictable failures (logged out, freshly
        // encrypted room) we'd rather skip the echo and only surface the
        // error.
        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, surfacing error notif");
            postReplyError(appContext, roomId);
            return;
        }

        // Race guard for E2EE flip: the per-room metadata snapshot is
        // refreshed by JS on m.room.encryption Timeline events, but a push
        // delivered in the narrow window between the encryption state
        // landing and the dump completing could still expose the reply
        // action on a freshly-encrypted room. Re-read the snapshot
        // synchronously here — Synapse does NOT enforce "no cleartext in
        // encrypted rooms" at the spec level, so without this guard we'd
        // leak the user's reply into an E2EE timeline as plaintext.
        if (isRoomEncryptedAtSendTime(prefs, roomId)) {
            Log.w(TAG, "onReceive: room flipped to encrypted between render and send, abort");
            postReplyError(appContext, roomId);
            return;
        }

        // Optimistic local echo — appends a self-Person message to the
        // conversation and re-posts, so the user sees their reply in the
        // shade before the HTTP completes. Only happens after pre-flight
        // checks pass so the user doesn't see an echo for a reply we know
        // will fail.
        long now = System.currentTimeMillis();
        VojoFirebaseMessagingService.appendOutgoingMessage(appContext, roomId, text, now);

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

    private int sendReply(
        Context context,
        String baseUrl,
        String accessToken,
        String roomId,
        String txnId,
        String text
    ) throws IOException {
        String url = trimTrailingSlash(baseUrl)
            + "/_matrix/client/v3/rooms/"
            + URLEncoder.encode(roomId, "UTF-8")
            + "/send/m.room.message/"
            + URLEncoder.encode(txnId, "UTF-8");

        JSONObject body;
        try {
            body = new JSONObject();
            body.put("msgtype", "m.text");
            body.put("body", text);
        } catch (org.json.JSONException je) {
            // JSONObject.put only throws on NaN/Inf doubles, neither of
            // which we use — but keep the type contract honest.
            throw new IOException("payload encode failed", je);
        }
        byte[] payload = body.toString().getBytes("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("PUT");
            conn.setRequestProperty("Authorization", "Bearer " + accessToken);
            conn.setRequestProperty("Content-Type", "application/json");
            conn.setConnectTimeout(CONNECT_TIMEOUT_MS);
            conn.setReadTimeout(READ_TIMEOUT_MS);
            conn.setDoOutput(true);
            conn.setFixedLengthStreamingMode(payload.length);
            try (OutputStream os = conn.getOutputStream()) {
                os.write(payload);
            }
            return conn.getResponseCode();
        } finally {
            conn.disconnect();
        }
    }

    /**
     * Surface a short error notification when the reply HTTP fails so the
     * user knows the message did NOT land server-side and can retry from
     * within the app. Posted on the DM channel as a one-shot. Unique notif
     * id per room so it can't clobber the room's conversation slot.
     */
    private static void postReplyError(Context ctx, String roomId) {
        NotificationManager nm = (NotificationManager)
            ctx.getSystemService(Context.NOTIFICATION_SERVICE);
        if (nm == null) return;
        try {
            String channel = VojoFirebaseMessagingService.CHANNEL_ID_DM;
            NotificationCompat.Builder b = new NotificationCompat.Builder(ctx, channel)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentTitle(PushStrings.replyFailed(ctx))
                .setContentText(PushStrings.replyFailed(ctx))
                .setAutoCancel(true)
                .setPriority(NotificationCompat.PRIORITY_DEFAULT);
            int errId = ("replyErr_" + roomId).hashCode();
            nm.notify(errId, b.build());
        } catch (Throwable t) {
            Log.w(TAG, "reply error notif failed", t);
        }
    }

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

    /**
     * Synchronous re-check of the room's encryption flag at send time.
     * Mirrors VojoFirebaseMessagingService.loadRoomMetadata's tolerant
     * parse: legacy string-shape entries and missing flags both default
     * to encrypted=true (privacy-first — refusing a reply on a falsely-
     * flagged room is harmless; sending cleartext into a truly encrypted
     * room is a privacy leak).
     */
    private static boolean isRoomEncryptedAtSendTime(SharedPreferences prefs, String roomId) {
        String raw = prefs.getString(VojoPollWorker.KEY_ROOM_NAMES, null);
        if (raw == null || raw.isEmpty()) return true;
        try {
            JSONObject map = new JSONObject(raw);
            if (!map.has(roomId) || map.isNull(roomId)) return true;
            JSONObject obj = map.optJSONObject(roomId);
            if (obj == null) {
                // Legacy string-shape predates the encryption flag —
                // assume encrypted to err on the side of privacy.
                return true;
            }
            return obj.optBoolean("isEncrypted", true);
        } catch (JSONException je) {
            return true;
        }
    }
}
