package chat.vojo.app;

import android.content.ContentResolver;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.OpenableColumns;
import android.util.Log;
import android.webkit.MimeTypeMap;

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

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * Receives ACTION_SEND / ACTION_SEND_MULTIPLE intents from the system share-
 * sheet and surfaces them to the WebView as a pending share that JS consumes
 * via {@code pickPendingShare()} (or reacts to via the {@code shareReceived}
 * event when the app was already in the foreground).
 *
 * Cold-start flow:
 *   1. Share-sheet → Vojo → MainActivity.onCreate → super.onCreate runs
 *      BridgeActivity.load(), which itself calls bridge.onNewIntent(getIntent())
 *      and fans the intent out to every plugin's handleOnNewIntent. So
 *      cold-start and warm-start share the SAME entry point — we don't
 *      double-process via handleOnStart.
 *   2. captureFromIntent copies payload bytes into the app cache and stashes
 *      the result in {@link #pendingShare}.
 *   3. JS booting up (Matrix client ready, user logged in) calls
 *      pickPendingShare(); receives the JSON; opens the room-picker UI. The
 *      shareReceived event fired here is dropped silently because no JS
 *      listener is attached yet — that's fine, pickPendingShare drains the
 *      slot regardless.
 *
 * Warm flow (app already running):
 *   1. Share-sheet → MainActivity.onNewIntent → BridgeActivity forwards to
 *      plugin.handleOnNewIntent(intent).
 *   2. We re-capture the payload AND emit {@code shareReceived} so JS can
 *      open the picker without polling.
 *
 * Why we copy to cache instead of handing JS a content:// URI:
 *   - WebView fetch() rejects content:// schemes outright, and
 *     `Capacitor.convertFileSrc()` only works on file paths.
 *   - The originating app holds the read-grant only for the lifetime of the
 *     launching task; routing the URI through JS+picker+RoomInput would race
 *     that grant on Android 14+.
 *   - Copying into our own cache means the share is self-contained: even if
 *     the user backgrounds Vojo for hours before picking a chat, the bytes
 *     are still there. We schedule no cleanup of our own — Android's cache
 *     eviction handles long-tail garbage.
 */
@CapacitorPlugin(name = "ShareTarget")
public class ShareTargetPlugin extends Plugin {

    private static final String TAG = "ShareTargetPlugin";
    private static final String SHARE_CACHE_SUBDIR = "shared";

    // Single-slot pending share. Multiple share-sheet invocations before JS
    // drains the slot collapse — the latest wins. JS contract is "consume
    // once, then it's gone" via pickPendingShare(consume=true). This matches
    // user intent: tapping share twice on different photos clearly means
    // "share THIS one now".
    private volatile JSObject pendingShare = null;

    @Override
    public void handleOnNewIntent(Intent intent) {
        super.handleOnNewIntent(intent);
        captureFromIntent(intent, /* notifyJs */ true);
    }

    @PluginMethod
    public void pickPendingShare(PluginCall call) {
        JSObject ret = new JSObject();
        JSObject snapshot = pendingShare;
        if (snapshot == null) {
            ret.put("empty", true);
        } else {
            // Default: consume on read. Lets us treat the slot like a one-shot
            // mailbox without an extra round-trip. Caller can pass consume=false
            // to peek (not used today, but cheap to keep).
            Boolean consume = call.getBoolean("consume", Boolean.TRUE);
            ret = snapshot;
            if (Boolean.TRUE.equals(consume)) {
                pendingShare = null;
            }
        }
        call.resolve(ret);
    }

    private void captureFromIntent(Intent intent, boolean notifyJs) {
        if (intent == null) return;
        String action = intent.getAction();
        if (action == null) return;

        // Capacitor's JSObject.put() silently swallows JSONException internally
        // (it wraps org.json.JSONObject and returns `this` on failure) so no
        // checked exception is thrown here — unlike the raw org.json API.
        JSObject share = new JSObject();
        share.put("empty", false);

        String text = intent.getStringExtra(Intent.EXTRA_TEXT);
        String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT);
        if (text != null && !text.isEmpty()) share.put("text", text);
        if (subject != null && !subject.isEmpty()) share.put("subject", subject);

        JSArray items = new JSArray();
        List<Uri> uris = new ArrayList<>();
        if (Intent.ACTION_SEND.equals(action)) {
            Uri uri;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                uri = intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri.class);
            } else {
                // Deprecated overload — required to read EXTRA_STREAM on
                // API ≤32, where the typed variant doesn't exist.
                //noinspection deprecation
                uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
            }
            if (uri != null) uris.add(uri);
        } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
            List<Uri> multi;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                multi = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri.class);
            } else {
                //noinspection deprecation
                multi = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
            }
            if (multi != null) uris.addAll(multi);
        }

        String intentMime = intent.getType();
        for (Uri uri : uris) {
            JSObject item = copyUriToCache(uri, intentMime);
            if (item != null) items.put(item);
        }
        share.put("items", items);

        // Drop pure-noise intents — neither text nor a successfully
        // copied file. Possible if a sender app handed us only a content://
        // URI we can't read (permission revoked) or an EXTRA_STREAM with a
        // null Uri. Keeps JS from showing an empty picker.
        if (text == null && subject == null && items.length() == 0) {
            Log.w(TAG, "Dropping share intent with no usable payload");
            return;
        }

        pendingShare = share;
        if (notifyJs) {
            notifyListeners("shareReceived", new JSObject());
        }
    }

    /**
     * Stream the content of {@code uri} into a fresh file under
     * cacheDir/shared/, then return {name, mimeType, size, path}. The path is
     * an absolute filesystem path — JS wraps it with
     * {@code Capacitor.convertFileSrc} before fetch().
     */
    private JSObject copyUriToCache(Uri uri, String fallbackMime) {
        if (uri == null) return null;
        ContentResolver resolver = getContext().getContentResolver();

        String name = queryDisplayName(resolver, uri);
        String mimeType = resolver.getType(uri);
        if (mimeType == null) mimeType = fallbackMime;
        if (mimeType == null) mimeType = "application/octet-stream";

        if (name == null || name.isEmpty()) {
            String ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
            name = "share-" + UUID.randomUUID() + (ext != null ? "." + ext : "");
        }

        File dir = new File(getContext().getCacheDir(), SHARE_CACHE_SUBDIR);
        // mkdirs returns false if the directory already exists — not an error.
        // The real failure mode is the I/O exception below on FileOutputStream
        // construction, which we surface.
        if (!dir.exists() && !dir.mkdirs()) {
            Log.e(TAG, "Could not create share cache dir: " + dir);
            return null;
        }
        // Prefix with UUID so a repeated share of "IMG_1234.jpg" doesn't
        // overwrite the previous payload while the user is still picking a
        // chat for the older one (e.g. Gallery → Vojo, see room-picker open,
        // background → Gallery → re-share same file → foreground Vojo). Both
        // payloads stay independently addressable.
        File out = new File(dir, UUID.randomUUID() + "_" + safeFileName(name));

        // Open the input first; if the sender's provider hands us back
        // null (revoked grant, gone-away ContentProvider, …) bail before
        // creating any on-disk file — otherwise the FileOutputStream
        // initializer below would create a zero-byte orphan we'd never
        // clean up (catch arm doesn't fire when we early-return).
        long size;
        try (InputStream in = resolver.openInputStream(uri)) {
            if (in == null) {
                Log.w(TAG, "openInputStream returned null for " + uri);
                return null;
            }
            try (FileOutputStream fos = new FileOutputStream(out)) {
                byte[] buf = new byte[64 * 1024];
                int n;
                long total = 0;
                while ((n = in.read(buf)) > 0) {
                    fos.write(buf, 0, n);
                    total += n;
                }
                size = total;
            }
        } catch (IOException e) {
            Log.e(TAG, "Failed to copy " + uri, e);
            // Drop the partial file so we don't surface a truncated
            // payload to JS as if it were valid.
            //noinspection ResultOfMethodCallIgnored
            out.delete();
            return null;
        }

        JSObject item = new JSObject();
        item.put("name", name);
        item.put("mimeType", mimeType);
        item.put("size", size);
        item.put("path", out.getAbsolutePath());
        return item;
    }

    private String queryDisplayName(ContentResolver resolver, Uri uri) {
        // ContentResolver.query throws if the provider rejects the URI scheme
        // (e.g. some senders pass a file:// directly — no provider involved).
        // Wrap in try/catch and fall back to the URI's last path segment.
        try (Cursor c = resolver.query(uri, new String[]{ OpenableColumns.DISPLAY_NAME }, null, null, null)) {
            if (c != null && c.moveToFirst()) {
                int idx = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
                if (idx >= 0) {
                    String name = c.getString(idx);
                    if (name != null && !name.isEmpty()) return name;
                }
            }
        } catch (Throwable t) {
            Log.d(TAG, "queryDisplayName failed for " + uri + ": " + t.getMessage());
        }
        String last = uri.getLastPathSegment();
        if (last != null && !last.isEmpty()) {
            // Strip any directory traversal a malicious sender might encode.
            int slash = last.lastIndexOf('/');
            return slash >= 0 ? last.substring(slash + 1) : last;
        }
        return null;
    }

    private static String safeFileName(String name) {
        // Strip path separators and trim length — the on-disk name is just an
        // identifier; the display name we return to JS preserves the user's
        // original filename verbatim. Trim from the tail so the recognisable
        // head ("IMG_2025_05_16…") survives and the extension is the part
        // that gets clipped on absurdly long names; the on-disk extension
        // doesn't matter because nothing inside Vojo dispatches on it (the
        // display name carries the real extension into JS).
        String stripped = name.replaceAll("[/\\\\]", "_");
        if (stripped.length() > 120) stripped = stripped.substring(0, 120);
        return stripped;
    }
}
