package com.secureline.server;

import android.app.Notification;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.SystemClock;
import android.util.Slog;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

final class SecureLineNotificationProtection {

    interface Callbacks {
        void onMessageWipeTriggered(String reason);
    }

    private static final String TAG = "SecureLineNotifProtect";
    private static final long TRIGGER_DEBOUNCE_MS = 10_000L;
    private static final int MIN_SECRET_LENGTH = 12;
    private static final int MAX_CANDIDATES = 256;

    private final android.content.Context mContext;
    private final Callbacks mCallbacks;

    private long mLastTriggerMs = 0;

    SecureLineNotificationProtection(
            android.content.Context context,
            Callbacks callbacks
    ) {
        mContext = context;
        mCallbacks = callbacks;
    }

    void onNotificationPosted(
            String packageName,
            Notification notification
    ) {
        try {
            if (notification == null || notification.extras == null) {
                return;
            }

            if (!SecureLineNotificationWipeStore.isEnabled(mContext)) {
                return;
            }

            final String storedHash =
                    SecureLineNotificationWipeStore.getSecretHash(mContext);

            if (storedHash == null || storedHash.isEmpty()) {
                return;
            }

            final List<String> candidates = extractCandidates(notification);

            for (String candidate : candidates) {
                if (candidate == null) {
                    continue;
                }

                final String clean = candidate.trim();
                if (clean.length() < MIN_SECRET_LENGTH) {
                    continue;
                }

                if (storedHash.equals(
                        SecureLineNotificationWipeStore.sha256(clean))) {
                    trigger(packageName);
                    return;
                }
            }
        } catch (Throwable t) {
            // Never log notification contents or the configured wipe secret.
            Slog.e(TAG, "Notification wipe scan failed", t);
        }
    }

    private void trigger(String packageName) {
        final long now = SystemClock.elapsedRealtime();

        if ((now - mLastTriggerMs) < TRIGGER_DEBOUNCE_MS) {
            return;
        }

        mLastTriggerMs = now;

        Slog.e(TAG,
                "Message Wipe Protection triggered by notification from "
                        + packageName);

        mCallbacks.onMessageWipeTriggered("message_wipe_notification");
    }

    private static List<String> extractCandidates(Notification notification) {
        final LinkedHashSet<String> out = new LinkedHashSet<>();
        final Bundle extras = notification.extras;

        // Standard text fields used by normal, big-text and conversation notifications.
        addTextAndVariants(out, extras.getCharSequence(Notification.EXTRA_TITLE));
        addTextAndVariants(out, extras.getCharSequence(Notification.EXTRA_TEXT));
        addTextAndVariants(out, extras.getCharSequence(Notification.EXTRA_BIG_TEXT));
        addTextAndVariants(out, extras.getCharSequence(Notification.EXTRA_SUB_TEXT));
        addTextAndVariants(out, extras.getCharSequence(Notification.EXTRA_INFO_TEXT));
        addTextAndVariants(out, extras.getCharSequence(Notification.EXTRA_SUMMARY_TEXT));
        addTextAndVariants(out, extras.getCharSequence(Notification.EXTRA_TITLE_BIG));
        addTextAndVariants(out, extras.getCharSequence(Notification.EXTRA_CONVERSATION_TITLE));

        final CharSequence[] lines =
                extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
        if (lines != null) {
            for (CharSequence line : lines) {
                addTextAndVariants(out, line);
                if (out.size() >= MAX_CANDIDATES) break;
            }
        }

        // MessagingStyle is the important path for modern SMS/messaging clients.
        final Parcelable[] messages =
                extras.getParcelableArray(Notification.EXTRA_MESSAGES);
        if (messages != null) {
            final List<Notification.MessagingStyle.Message> parsed =
                    Notification.MessagingStyle.Message
                            .getMessagesFromBundleArray(messages);

            if (parsed != null) {
                for (Notification.MessagingStyle.Message msg : parsed) {
                    if (msg == null) continue;
                    addTextAndVariants(out, msg.getText());
                    if (out.size() >= MAX_CANDIDATES) break;
                }
            }
        }

        // Some messaging apps expose historic messages separately.
        final Parcelable[] historicMessages =
                extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
        if (historicMessages != null && out.size() < MAX_CANDIDATES) {
            final List<Notification.MessagingStyle.Message> parsedHistoric =
                    Notification.MessagingStyle.Message
                            .getMessagesFromBundleArray(historicMessages);

            if (parsedHistoric != null) {
                for (Notification.MessagingStyle.Message msg : parsedHistoric) {
                    if (msg == null) continue;
                    addTextAndVariants(out, msg.getText());
                    if (out.size() >= MAX_CANDIDATES) break;
                }
            }
        }

        return new ArrayList<>(out);
    }

    private static void addTextAndVariants(Set<String> out, CharSequence value) {
        if (value == null || out.size() >= MAX_CANDIDATES) {
            return;
        }

        final String raw = value.toString();
        if (raw.isBlank()) {
            return;
        }

        final String trimmed = raw.trim();
        addCandidate(out, trimmed);

        // The configured wipe secret cannot contain whitespace. Splitting therefore makes
        // notifications such as "Sender: LEMENZO123456" match the secret token exactly.
        final String[] tokens = raw.split("\\s+");
        for (String token : tokens) {
            if (out.size() >= MAX_CANDIDATES) break;
            if (token == null || token.isBlank()) continue;

            final String cleanToken = token.trim();
            addCandidate(out, cleanToken);

            // Also consider common presentation punctuation around a token, e.g. "CODE" or CODE.
            // The unmodified token is retained first, so secrets which intentionally contain
            // punctuation still work.
            final String unwrapped = stripPresentationPunctuation(cleanToken);
            if (!unwrapped.equals(cleanToken)) {
                addCandidate(out, unwrapped);
            }
        }
    }

    private static void addCandidate(Set<String> out, String candidate) {
        if (candidate == null || candidate.isBlank() || out.size() >= MAX_CANDIDATES) {
            return;
        }
        out.add(candidate);
    }

    private static String stripPresentationPunctuation(String value) {
        int start = 0;
        int end = value.length();

        while (start < end && isPresentationPunctuation(value.charAt(start))) {
            start++;
        }
        while (end > start && isPresentationPunctuation(value.charAt(end - 1))) {
            end--;
        }

        return value.substring(start, end);
    }

    private static boolean isPresentationPunctuation(char c) {
        switch (c) {
            case '"':
            case '\'':
            case '`':
            case ',':
            case '.':
            case ':':
            case ';':
            case '!':
            case '?':
            case '(':
            case ')':
            case '[':
            case ']':
            case '{':
            case '}':
            case '<':
            case '>':
                return true;
            default:
                return false;
        }
    }
}
