package com.lemenzo.messenger.data;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.Telephony;
import android.util.Log;
import com.lemenzo.messenger.model.ConversationSummary;
import com.lemenzo.messenger.model.MessageItem;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public final class MessageRepository {
    private static final String TAG = "LemenzoMessengerRepo";
    private final Context appContext;
    private final ContentResolver resolver;
    private final ContactResolver contacts;
    private final ConcurrentHashMap<String,String> canonicalAddressCache = new ConcurrentHashMap<>();

    public MessageRepository(Context context) {
        appContext = context.getApplicationContext();
        resolver = appContext.getContentResolver();
        contacts = new ContactResolver(appContext);
    }

    public void clearContactCache() {
        contacts.clearCache();
    }

    public List<ConversationSummary> conversations(String filter) {
        Map<Long, ConversationSummary> latest = new HashMap<>();
        String query = filter == null ? "" : filter.trim().toLowerCase(Locale.ROOT);

        if (!loadCombinedConversationRows(latest, query)) {
            loadSmsConversationRows(latest, query);
            loadMmsConversationRows(latest, query);
        }
        mergePendingConversations(latest, query);
        populateUnreadCounts(latest);
        populateContactPresentation(latest);

        List<ConversationSummary> out = new ArrayList<>(latest.values());
        out.sort((a, b) -> Long.compare(b.date, a.date));
        return out;
    }

    private boolean loadCombinedConversationRows(
            Map<Long, ConversationSummary> latest,
            String query) {
        Uri conversations = Uri.parse("content://mms-sms/conversations?simple=true");
        String[] projection = {"_id", "date", "recipient_ids", "snippet", "read", "message_count"};
        try (Cursor c = resolver.query(
                conversations,
                projection,
                null,
                null,
                "date DESC")) {
            if (c == null) return false;
            while (c.moveToNext()) {
                // The Telephony provider intentionally keeps empty thread rows until callers
                // delete content://mms-sms/conversations/obsolete. Never surface such an
                // orphan as an "Unknown" conversation while cleanup is pending.
                if (c.getInt(5) <= 0) continue;
                long threadId = c.getLong(0);
                long date = c.getLong(1);
                String address = canonicalAddresses(c.getString(2));
                if (address == null || address.trim().isEmpty()) {
                    address = addressForThread(threadId);
                }
                String snippet = c.getString(3);
                if (!matchesConversationQuery(query, address, snippet)) continue;

                ConversationSummary item = new ConversationSummary();
                item.threadId = threadId;
                item.address = address == null ? "" : address.trim();
                item.snippet = empty(snippet, "");
                item.date = date;
                item.unreadCount = c.getInt(4) == 0 ? 1 : 0;
                latest.put(threadId, item);
            }
            return true;
        } catch (RuntimeException e) {
            Log.w(TAG, "Combined conversation provider unavailable; falling back", e);
            latest.clear();
            return false;
        }
    }

    private String canonicalAddresses(String recipientIds) {
        if (recipientIds == null || recipientIds.trim().isEmpty()) return "";
        StringBuilder out = new StringBuilder();
        String[] ids = recipientIds.trim().split("\\s+");
        for (String id : ids) {
            if (id.isEmpty()) continue;
            String address = canonicalAddressCache.get(id);
            if (address == null) {
                Uri uri = Uri.parse("content://mms-sms/canonical-addresses/" + Uri.encode(id));
                String resolvedAddress = "";
                try (Cursor c = resolver.query(uri, new String[]{"address"}, null, null, null)) {
                    if (c != null && c.moveToFirst() && c.getString(0) != null) {
                        resolvedAddress = c.getString(0).trim();
                    }
                } catch (RuntimeException e) {
                    Log.w(TAG, "Canonical address lookup failed", e);
                }
                canonicalAddressCache.put(id, resolvedAddress);
                address = resolvedAddress;
            }
            if (!address.isEmpty()) {
                if (out.length() > 0) out.append(", ");
                out.append(address);
            }
        }
        return out.toString();
    }

    public String addressForThread(long threadId) {
        try (Cursor c = resolver.query(
                Telephony.Sms.CONTENT_URI,
                new String[]{Telephony.Sms.ADDRESS},
                Telephony.Sms.THREAD_ID + "=?",
                new String[]{Long.toString(threadId)},
                Telephony.Sms.DATE + " DESC")) {
            if (c != null && c.moveToFirst()) {
                String value = c.getString(0);
                if (value != null && !value.trim().isEmpty()) return value.trim();
            }
        } catch (RuntimeException e) {
            Log.w(TAG, "SMS thread address lookup failed", e);
        }

        try (Cursor c = resolver.query(
                Telephony.Mms.CONTENT_URI,
                new String[]{Telephony.Mms._ID, Telephony.Mms.MESSAGE_BOX},
                Telephony.Mms.THREAD_ID + "=?",
                new String[]{Long.toString(threadId)},
                Telephony.Mms.DATE + " DESC")) {
            if (c != null && c.moveToFirst()) {
                long id = c.getLong(0);
                int box = c.getInt(1);
                boolean outgoing = box == Telephony.Mms.MESSAGE_BOX_SENT
                        || box == Telephony.Mms.MESSAGE_BOX_OUTBOX;
                String value = mmsParticipants(id, outgoing);
                if (value != null && !value.trim().isEmpty()) return value.trim();
            }
        } catch (RuntimeException e) {
            Log.w(TAG, "MMS thread address lookup failed", e);
        }
        return "";
    }

    private void loadSmsConversationRows(
            Map<Long, ConversationSummary> latest,
            String query) {
        String[] projection = {
                Telephony.Sms.THREAD_ID,
                Telephony.Sms.ADDRESS,
                Telephony.Sms.BODY,
                Telephony.Sms.DATE,
                Telephony.Sms.READ
        };

        try (Cursor c = resolver.query(
                Telephony.Sms.CONTENT_URI,
                projection,
                null,
                null,
                Telephony.Sms.DATE + " DESC")) {
            if (c == null) return;
            while (c.moveToNext()) {
                long threadId = c.getLong(0);
                long date = c.getLong(3);
                ConversationSummary existing = latest.get(threadId);
                if (existing != null && existing.date >= date) continue;

                String address = c.getString(1);
                String body = c.getString(2);
                if (!matchesConversationQuery(query, address, body)) continue;

                ConversationSummary item = new ConversationSummary();
                item.threadId = threadId;
                item.address = address == null ? "" : address.trim();
                item.snippet = empty(body, "");
                item.date = date;
                item.unreadCount = c.getInt(4) == 0 ? 1 : 0;
                latest.put(threadId, item);
            }
        } catch (RuntimeException e) {
            Log.w(TAG, "Message provider operation failed", e);
        }
    }

    private void loadMmsConversationRows(
            Map<Long, ConversationSummary> latest,
            String query) {
        String[] projection = {
                Telephony.Mms._ID,
                Telephony.Mms.THREAD_ID,
                Telephony.Mms.DATE,
                Telephony.Mms.READ,
                Telephony.Mms.SUBJECT,
                Telephony.Mms.MESSAGE_BOX
        };

        try (Cursor c = resolver.query(
                Telephony.Mms.CONTENT_URI,
                projection,
                null,
                null,
                Telephony.Mms.DATE + " DESC")) {
            if (c == null) return;
            while (c.moveToNext()) {
                long msgId = c.getLong(0);
                long threadId = c.getLong(1);
                long date = c.getLong(2) * 1000L;
                ConversationSummary existing = latest.get(threadId);
                if (existing != null && existing.date >= date) continue;

                int messageBox = c.getInt(5);
                boolean outgoing = messageBox == Telephony.Mms.MESSAGE_BOX_SENT
                        || messageBox == Telephony.Mms.MESSAGE_BOX_OUTBOX;
                String address = mmsParticipants(msgId, outgoing);
                String subject = c.getString(4);
                String snippet = mmsText(msgId);
                if (snippet.isEmpty()) snippet = empty(subject, "MMS");
                if (!matchesConversationQuery(query, address, snippet)) continue;

                ConversationSummary item = new ConversationSummary();
                item.threadId = threadId;
                item.address = address == null ? "" : address.trim();
                item.snippet = snippet;
                item.date = date;
                item.unreadCount = c.getInt(3) == 0 ? 1 : 0;
                latest.put(threadId, item);
            }
        } catch (RuntimeException e) {
            Log.w(TAG, "Message provider operation failed", e);
        }
    }

    private void mergePendingConversations(Map<Long, ConversationSummary> latest, String query) {
        PendingMessageStore store = new PendingMessageStore(appContext);
        for (com.lemenzo.messenger.model.PendingMessage pending : store.all()) {
            if (pending.address == null || pending.address.trim().isEmpty()) continue;
            if (com.lemenzo.messenger.sms.MultipartSendTracker.isBroadcastToken(appContext, pending.token)) continue;
            String address = pending.address.trim();
            String status;
            if (pending.state == com.lemenzo.messenger.model.PendingMessage.STATE_FAILED) {
                status = appContext.getString(com.lemenzo.messenger.R.string.status_failed);
            } else if (pending.state == com.lemenzo.messenger.model.PendingMessage.STATE_SENT) {
                status = appContext.getString(com.lemenzo.messenger.R.string.status_sent);
            } else {
                status = appContext.getString(com.lemenzo.messenger.R.string.status_sending);
            }
            String body = pending.body == null ? "" : pending.body.trim();
            String snippet = body.isEmpty() ? status : status + " · " + body;
            if (!matchesConversationQuery(query, address, snippet)) continue;
            long threadId = threadIdForRecipients(address);
            if (threadId < 0) continue;
            ConversationSummary existing = latest.get(threadId);
            if (existing != null && existing.date >= pending.date) continue;
            ConversationSummary item = new ConversationSummary();
            item.threadId = threadId;
            item.address = address;
            item.snippet = snippet;
            item.date = pending.date;
            item.unreadCount = existing == null ? 0 : existing.unreadCount;
            latest.put(threadId, item);
        }
    }

    public long threadIdForRecipients(String address) {
        if (address == null || address.trim().isEmpty()) return -1;
        try {
            String[] parts = address.split("[,;]");
            java.util.HashSet<String> recipients = new java.util.HashSet<>();
            for (String part : parts) {
                String value = part.trim();
                if (!value.isEmpty()) recipients.add(value);
            }
            if (recipients.isEmpty()) return -1;
            return Telephony.Threads.getOrCreateThreadId(appContext, recipients);
        } catch (RuntimeException e) {
            Log.w(TAG, "Unable to resolve SMS/MMS thread id", e);
            return -1;
        }
    }

    private boolean matchesConversationQuery(String query, String address, String snippet) {
        if (query == null || query.isEmpty()) return true;
        if (matches(query, address, snippet)) return true;
        return matches(query, contacts.displayName(address));
    }

    private void populateContactPresentation(Map<Long, ConversationSummary> latest) {
        java.util.ArrayList<String> addresses = new java.util.ArrayList<>();
        for (ConversationSummary item : latest.values()) {
            if (item.address != null && !item.address.isEmpty() && !item.address.contains(", ")) {
                addresses.add(item.address);
            }
        }
        contacts.prefetchPresentations(addresses);
        for (ConversationSummary item : latest.values()) {
            if (item.address == null || item.address.isEmpty() || item.address.contains(", ")) {
                item.displayName = item.address == null ? "" : item.address;
                item.photoUri = null;
                continue;
            }
            item.displayName = contacts.displayName(item.address);
            Uri photo = contacts.photoUri(item.address);
            item.photoUri = photo == null ? null : photo.toString();
        }
    }

    private void populateUnreadCounts(Map<Long, ConversationSummary> latest) {
        for (ConversationSummary item : latest.values()) item.unreadCount = 0;
        countUnread(Telephony.Sms.CONTENT_URI, Telephony.Sms.THREAD_ID, latest);
        countUnread(Telephony.Mms.CONTENT_URI, Telephony.Mms.THREAD_ID, latest);
    }

    private void countUnread(Uri uri, String threadColumn, Map<Long, ConversationSummary> latest) {
        try (Cursor c = resolver.query(
                uri,
                new String[]{threadColumn, "read"},
                "read=0",
                null,
                null)) {
            if (c == null) return;
            while (c.moveToNext()) {
                ConversationSummary item = latest.get(c.getLong(0));
                if (item != null) item.unreadCount++;
            }
        } catch (RuntimeException e) {
            Log.w(TAG, "Unable to count unread messages", e);
        }
    }

    /**
     * Load only a bounded window from a thread. The provider cursor is consumed newest-first
     * and stopped as soon as enough candidates are available. This avoids creating thousands
     * of MessageItem objects during normal app startup.
     */
    public MessagePage messagesPage(long threadId, int limit) {
        return messagesPage(threadId, Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE, limit);
    }

    /**
     * Stable keyset pagination across SMS and MMS. Date alone is not a safe cursor because
     * many imported messages and MMS rows can share the same timestamp. Per-protocol ids
     * break those ties deterministically without OFFSET scans.
     */
    public MessagePage messagesPage(long threadId, long beforeDate,
            long beforeSmsId, long beforeMmsId, int limit) {
        int safeLimit = Math.max(50, Math.min(limit, 2000));
        ArrayList<MessageItem> candidates = new ArrayList<>();
        boolean smsMore = loadSmsMessagePage(candidates, threadId, beforeDate, beforeSmsId, safeLimit + 1);
        boolean mmsMore = loadMmsMessagePage(candidates, threadId, beforeDate, beforeMmsId, safeLimit + 1);

        candidates.sort((a, b) -> {
            int byDate = Long.compare(b.date, a.date);
            if (byDate != 0) return byDate;
            int byProtocol = Integer.compare(a.protocol, b.protocol);
            if (byProtocol != 0) return byProtocol;
            return Long.compare(b.id, a.id);
        });

        boolean hasOlder = smsMore || mmsMore || candidates.size() > safeLimit;
        if (candidates.size() > safeLimit) {
            candidates.subList(safeLimit, candidates.size()).clear();
        }

        long nextDate = Long.MAX_VALUE;
        long nextSmsId = Long.MAX_VALUE;
        long nextMmsId = Long.MAX_VALUE;
        if (!candidates.isEmpty()) {
            nextDate = candidates.get(candidates.size() - 1).date;
            // If a page ends on the same timestamp as the previous page, a protocol may
            // contribute zero rows to this particular merged page. Preserve that protocol's
            // prior id cursor instead of resetting it to MAX_VALUE, otherwise already-consumed
            // rows at the same timestamp become eligible again and paging can loop.
            if (nextDate == beforeDate) {
                nextSmsId = beforeSmsId;
                nextMmsId = beforeMmsId;
            }
            for (MessageItem item : candidates) {
                if (item.date != nextDate) continue;
                if (item.protocol == MessageItem.PROTOCOL_SMS) {
                    nextSmsId = Math.min(nextSmsId, item.id);
                } else if (item.protocol == MessageItem.PROTOCOL_MMS) {
                    nextMmsId = Math.min(nextMmsId, item.id);
                }
            }
        }

        candidates.sort((a, b) -> {
            int byDate = Long.compare(a.date, b.date);
            if (byDate != 0) return byDate;
            int byProtocol = Integer.compare(a.protocol, b.protocol);
            if (byProtocol != 0) return byProtocol;
            return Long.compare(a.id, b.id);
        });
        return new MessagePage(candidates, hasOlder, nextDate, nextSmsId, nextMmsId);
    }

    private boolean loadSmsMessagePage(List<MessageItem> out, long threadId,
            long beforeDate, long beforeId, int cap) {
        String[] projection = {
                Telephony.Sms._ID, Telephony.Sms.ADDRESS, Telephony.Sms.BODY,
                Telephony.Sms.DATE, Telephony.Sms.TYPE
        };
        String selection = Telephony.Sms.THREAD_ID + "=?";
        ArrayList<String> args = new ArrayList<>();
        args.add(Long.toString(threadId));
        if (beforeDate != Long.MAX_VALUE) {
            selection += " AND (" + Telephony.Sms.DATE + "<? OR (" + Telephony.Sms.DATE
                    + "=? AND " + Telephony.Sms._ID + "<?))";
            args.add(Long.toString(beforeDate));
            args.add(Long.toString(beforeDate));
            args.add(Long.toString(beforeId));
        }
        int seen = 0;
        try (Cursor c = resolver.query(Telephony.Sms.CONTENT_URI, projection, selection,
                args.toArray(new String[0]), Telephony.Sms.DATE + " DESC, " + Telephony.Sms._ID + " DESC")) {
            if (c == null) return false;
            while (c.moveToNext()) {
                seen++;
                if (seen > cap) return true;
                MessageItem m = new MessageItem();
                m.id = c.getLong(0);
                m.address = c.getString(1);
                m.body = c.getString(2);
                m.date = c.getLong(3);
                m.type = c.getInt(4);
                m.protocol = MessageItem.PROTOCOL_SMS;
                m.outgoing = m.type == Telephony.Sms.MESSAGE_TYPE_SENT
                        || m.type == Telephony.Sms.MESSAGE_TYPE_OUTBOX
                        || m.type == Telephony.Sms.MESSAGE_TYPE_QUEUED;
                out.add(m);
                if (seen >= cap) return c.moveToNext();
            }
        } catch (RuntimeException e) {
            Log.w(TAG, "Paged SMS query failed", e);
        }
        return false;
    }

    private boolean loadMmsMessagePage(List<MessageItem> out, long threadId,
            long beforeDate, long beforeId, int cap) {
        String[] projection = {Telephony.Mms._ID, Telephony.Mms.DATE,
                Telephony.Mms.MESSAGE_BOX, Telephony.Mms.SUBJECT};
        String selection = Telephony.Mms.THREAD_ID + "=?";
        ArrayList<String> args = new ArrayList<>();
        args.add(Long.toString(threadId));
        if (beforeDate != Long.MAX_VALUE) {
            long second = Math.max(0L, beforeDate / 1000L);
            if (beforeDate % 1000L == 0L) {
                selection += " AND (" + Telephony.Mms.DATE + "<? OR (" + Telephony.Mms.DATE
                        + "=? AND " + Telephony.Mms._ID + "<?))";
                args.add(Long.toString(second));
                args.add(Long.toString(second));
                args.add(Long.toString(beforeId));
            } else {
                // An MMS timestamp is second-granular. If the mixed cursor falls inside a
                // second because of an SMS millisecond timestamp, MMS rows from that whole
                // second are still older than the cursor and must remain eligible.
                selection += " AND " + Telephony.Mms.DATE + "<=?";
                args.add(Long.toString(second));
            }
        }
        int seen = 0;
        try (Cursor c = resolver.query(Telephony.Mms.CONTENT_URI, projection, selection,
                args.toArray(new String[0]), Telephony.Mms.DATE + " DESC, " + Telephony.Mms._ID + " DESC")) {
            if (c == null) return false;
            while (c.moveToNext()) {
                seen++;
                if (seen > cap) return true;
                long msgId = c.getLong(0);
                MessageItem m = new MessageItem();
                m.id = msgId;
                m.date = c.getLong(1) * 1000L;
                int box = c.getInt(2);
                m.type = box;
                m.protocol = MessageItem.PROTOCOL_MMS;
                m.outgoing = box == Telephony.Mms.MESSAGE_BOX_SENT || box == Telephony.Mms.MESSAGE_BOX_OUTBOX;
                m.subject = c.getString(3);
                m.address = mmsParticipants(msgId, m.outgoing);
                m.body = mmsText(msgId);
                MediaPart media = firstMediaPart(msgId);
                if (media != null) { m.mediaUri = media.uri; m.mediaType = media.type; }
                out.add(m);
                if (seen >= cap) return c.moveToNext();
            }
        } catch (RuntimeException e) {
            Log.w(TAG, "Paged MMS query failed", e);
        }
        return false;
    }

    public List<MessageItem> messages(long threadId) {
        List<MessageItem> out = new ArrayList<>();
        if (threadId < 0) return out;
        loadSmsMessages(out, threadId);
        loadMmsMessages(out, threadId);
        out.sort(Comparator.comparingLong(m -> m.date));
        return out;
    }

    private void loadSmsMessages(List<MessageItem> out, long threadId) {
        String[] projection = {
                Telephony.Sms._ID,
                Telephony.Sms.ADDRESS,
                Telephony.Sms.BODY,
                Telephony.Sms.DATE,
                Telephony.Sms.TYPE
        };
        try (Cursor c = resolver.query(
                Telephony.Sms.CONTENT_URI,
                projection,
                Telephony.Sms.THREAD_ID + "=?",
                new String[]{Long.toString(threadId)},
                Telephony.Sms.DATE + " ASC")) {
            if (c == null) return;
            while (c.moveToNext()) {
                MessageItem m = new MessageItem();
                m.id = c.getLong(0);
                m.address = c.getString(1);
                m.body = c.getString(2);
                m.date = c.getLong(3);
                m.type = c.getInt(4);
                m.protocol = MessageItem.PROTOCOL_SMS;
                m.outgoing = m.type == Telephony.Sms.MESSAGE_TYPE_SENT
                        || m.type == Telephony.Sms.MESSAGE_TYPE_OUTBOX
                        || m.type == Telephony.Sms.MESSAGE_TYPE_QUEUED;
                out.add(m);
            }
        } catch (RuntimeException e) {
            Log.w(TAG, "Message provider operation failed", e);
        }
    }

    private void loadMmsMessages(List<MessageItem> out, long threadId) {
        String[] projection = {
                Telephony.Mms._ID,
                Telephony.Mms.DATE,
                Telephony.Mms.MESSAGE_BOX,
                Telephony.Mms.SUBJECT
        };
        try (Cursor c = resolver.query(
                Telephony.Mms.CONTENT_URI,
                projection,
                Telephony.Mms.THREAD_ID + "=?",
                new String[]{Long.toString(threadId)},
                Telephony.Mms.DATE + " ASC")) {
            if (c == null) return;
            while (c.moveToNext()) {
                long msgId = c.getLong(0);
                MessageItem m = new MessageItem();
                m.id = msgId;
                m.date = c.getLong(1) * 1000L;
                int box = c.getInt(2);
                m.type = box;
                m.protocol = MessageItem.PROTOCOL_MMS;
                m.outgoing = box == Telephony.Mms.MESSAGE_BOX_SENT
                        || box == Telephony.Mms.MESSAGE_BOX_OUTBOX;
                m.subject = c.getString(3);
                m.address = mmsParticipants(msgId, m.outgoing);
                m.body = mmsText(msgId);

                MediaPart media = firstMediaPart(msgId);
                if (media != null) {
                    m.mediaUri = media.uri;
                    m.mediaType = media.type;
                }
                out.add(m);
            }
        } catch (RuntimeException e) {
            Log.w(TAG, "Message provider operation failed", e);
        }
    }

    private String mmsParticipants(long msgId, boolean outgoing) {
        Uri uri = Uri.parse("content://mms/" + msgId + "/addr");
        StringBuilder out = new StringBuilder();
        try (Cursor c = resolver.query(uri, new String[]{"address", "type"}, null, null, null)) {
            if (c != null) {
                while (c.moveToNext()) {
                    String address = c.getString(0);
                    int type = c.getInt(1);
                    // PduHeaders.FROM=137, TO=151, CC=130. Ignore insert-address-token.
                    boolean wanted = outgoing ? (type == 151 || type == 130) : type == 137;
                    if (!wanted || address == null || address.isEmpty()
                            || "insert-address-token".equalsIgnoreCase(address)) continue;
                    if (out.length() > 0) out.append(", ");
                    out.append(address);
                }
            }
        } catch (RuntimeException e) { Log.w(TAG, "Message provider operation failed", e); }
        return out.toString();
    }

    private String mmsText(long msgId) {
        Uri uri = Uri.parse("content://mms/" + msgId + "/part");
        StringBuilder out = new StringBuilder();
        try (Cursor c = resolver.query(
                uri,
                new String[]{"ct", "text"},
                "ct='text/plain'",
                null,
                null)) {
            if (c != null) {
                while (c.moveToNext()) {
                    String text = c.getString(1);
                    if (text != null && !text.isEmpty()) {
                        if (out.length() > 0) out.append('\n');
                        out.append(text);
                    }
                }
            }
        } catch (RuntimeException e) {
            Log.w(TAG, "Message provider operation failed", e);
        }
        return out.toString();
    }

    private MediaPart firstMediaPart(long msgId) {
        Uri uri = Uri.parse("content://mms/" + msgId + "/part");
        try (Cursor c = resolver.query(
                uri,
                new String[]{"_id", "ct"},
                null,
                null,
                null)) {
            if (c != null) {
                while (c.moveToNext()) {
                    String type = c.getString(1);
                    if (type != null && (type.startsWith("image/")
                            || type.startsWith("video/")
                            || type.startsWith("audio/"))) {
                        MediaPart part = new MediaPart();
                        part.type = type;
                        part.uri = Uri.parse("content://mms/part/" + c.getLong(0));
                        return part;
                    }
                }
            }
        } catch (RuntimeException e) {
            Log.w(TAG, "Message provider operation failed", e);
        }
        return null;
    }

    public void markThreadRead(long threadId) {
        if (threadId < 0) return;
        ContentValues values = new ContentValues();
        values.put("read", 1);
        values.put("seen", 1);
        try {
            resolver.update(
                    Telephony.Sms.CONTENT_URI,
                    values,
                    Telephony.Sms.THREAD_ID + "=?",
                    new String[]{Long.toString(threadId)});
        } catch (RuntimeException e) {
            Log.w(TAG, "Message provider operation failed", e);
        }
        try {
            resolver.update(
                    Telephony.Mms.CONTENT_URI,
                    values,
                    Telephony.Mms.THREAD_ID + "=?",
                    new String[]{Long.toString(threadId)});
        } catch (RuntimeException e) {
            Log.w(TAG, "Message provider operation failed", e);
        }
    }

    public boolean deleteMessage(MessageItem message) {
        if (message == null || message.id < 0) return false;
        Uri base = message.protocol == MessageItem.PROTOCOL_MMS
                ? Telephony.Mms.CONTENT_URI
                : Telephony.Sms.CONTENT_URI;
        Uri target = Uri.withAppendedPath(base, Long.toString(message.id));
        try {
            return resolver.delete(target, null, null) > 0;
        } catch (RuntimeException e) {
            Log.w(TAG, "Unable to delete message", e);
            return false;
        }
    }

    public boolean deleteConversation(long threadId) {
        if (threadId < 0) return false;

        int deleted = 0;
        boolean unifiedDeleteAttempted = false;

        // Use the platform's unified MMS/SMS conversation endpoint. It deletes every SMS/MMS
        // row belonging to this thread and updates Telephony's thread metadata consistently.
        // Deleting the SMS and MMS tables independently can leave an empty row in `threads`,
        // which is then rendered as an address-less "Unknown" conversation.
        Uri conversationUri = Uri.withAppendedPath(
                Uri.parse("content://mms-sms/conversations"), Long.toString(threadId));
        try {
            unifiedDeleteAttempted = true;
            deleted += resolver.delete(conversationUri, null, null);
        } catch (RuntimeException e) {
            // Keep a compatibility fallback for provider variants which reject the unified URI.
            Log.w(TAG, "Unified conversation delete failed; falling back", e);
        }

        if (!unifiedDeleteAttempted || deleted == 0) {
            String[] args = {Long.toString(threadId)};
            try {
                deleted += resolver.delete(
                        Telephony.Sms.CONTENT_URI,
                        Telephony.Sms.THREAD_ID + "=?",
                        args);
            } catch (RuntimeException e) {
                Log.w(TAG, "Unable to delete SMS conversation", e);
            }
            try {
                deleted += resolver.delete(
                        Telephony.Mms.CONTENT_URI,
                        Telephony.Mms.THREAD_ID + "=?",
                        args);
            } catch (RuntimeException e) {
                Log.w(TAG, "Unable to delete MMS conversation", e);
            }
        }

        // Android deliberately does not auto-delete empty rows from the threads table. The
        // TelephonyProvider exposes this endpoint specifically for cleaning them up. Counting
        // this as a successful delete also repairs orphan "Unknown" rows created by old builds,
        // where the underlying SMS/MMS rows have already been removed.
        try {
            deleted += resolver.delete(
                    Uri.parse("content://mms-sms/conversations/obsolete"), null, null);
        } catch (RuntimeException e) {
            Log.w(TAG, "Unable to clean obsolete message threads", e);
        }

        return deleted > 0;
    }

    public long threadIdForMessage(Uri messageUri) {
        return queryThreadId(messageUri);
    }

    public long threadIdForMms(Uri messageUri) {
        return queryThreadId(messageUri);
    }

    private long queryThreadId(Uri uri) {
        if (uri == null) return -1;
        try (Cursor c = resolver.query(
                uri,
                new String[]{"thread_id"},
                null,
                null,
                null)) {
            if (c != null && c.moveToFirst()) return c.getLong(0);
        } catch (RuntimeException e) {
            Log.w(TAG, "Message provider operation failed", e);
        }
        return -1;
    }

    public Uri insertInbox(String address, String body, long date, int subscriptionId) {
        ContentValues values = new ContentValues();
        values.put(Telephony.Sms.ADDRESS, address);
        values.put(Telephony.Sms.BODY, body);
        values.put(Telephony.Sms.DATE, date);
        values.put(Telephony.Sms.READ, 0);
        values.put(Telephony.Sms.SEEN, 0);
        values.put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_INBOX);
        putSubscriptionId(values, subscriptionId);
        return resolver.insert(Telephony.Sms.Inbox.CONTENT_URI, values);
    }

    public Uri insertSent(String address, String body, long date, int subscriptionId) {
        ContentValues values = new ContentValues();
        values.put(Telephony.Sms.ADDRESS, address);
        values.put(Telephony.Sms.BODY, body);
        values.put(Telephony.Sms.DATE, date);
        values.put(Telephony.Sms.READ, 1);
        values.put(Telephony.Sms.SEEN, 1);
        values.put(Telephony.Sms.TYPE, Telephony.Sms.MESSAGE_TYPE_SENT);
        putSubscriptionId(values, subscriptionId);
        return resolver.insert(Telephony.Sms.Sent.CONTENT_URI, values);
    }

    private static void putSubscriptionId(ContentValues values, int subscriptionId) {
        if (subscriptionId != android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
            // Telephony.TextBasedSmsColumns.SUBSCRIPTION_ID is the provider's sub_id column.
            values.put(Telephony.Sms.SUBSCRIPTION_ID, subscriptionId);
        }
    }

    private static boolean matches(String q, String... values) {
        if (q == null || q.isEmpty()) return true;
        for (String value : values) {
            if (value != null && value.toLowerCase(Locale.ROOT).contains(q)) return true;
        }
        return false;
    }

    private static String empty(String value, String fallback) {
        return value == null || value.isEmpty() ? fallback : value;
    }

    private static final class MediaPart {
        Uri uri;
        String type;
    }
}
