package org.thoughtcrime.securesms.notifications;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.NotificationManager;
import android.content.Context;
import android.database.Cursor;
import android.os.Build;
import android.provider.ContactsContract;

import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;

import org.signal.core.util.CursorUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.RecipientTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.signal.core.ui.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.signal.core.util.ServiceUtil;

import java.util.concurrent.TimeUnit;

public final class DoNotDisturbUtil {

  private static final String TAG = Log.tag(DoNotDisturbUtil.class);

  private DoNotDisturbUtil() {
  }

  /**
   * Checks whether the user should be disturbed with a call from the given recipient,
   * taking into account the recipient's mute and call notification settings as well as
   * the system Do Not Disturb state.
   *
   * For group recipients, only the system interruption filter is checked (no contact priority).
   * For 1:1 recipients, the full DND policy including contact priority is evaluated.
   */
  @WorkerThread
  @SuppressLint("SwitchIntDef")
  public static boolean shouldDisturbUserWithCall(@NonNull Context context, @NonNull Recipient recipient) {
    if (recipient.isMuted() && recipient.getCallNotificationSetting() == RecipientTable.NotificationSetting.DO_NOT_NOTIFY) {
      return false;
    }

    if (recipient.isGroup()) {
      return checkSystemDnd(context);
    } else {
      return checkSystemDndWithContactPriority(context, recipient);
    }
  }

  @WorkerThread
  @SuppressLint("SwitchIntDef")
  private static boolean checkSystemDnd(@NonNull Context context) {
    NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);

    switch (notificationManager.getCurrentInterruptionFilter()) {
      case NotificationManager.INTERRUPTION_FILTER_ALL:
      case NotificationManager.INTERRUPTION_FILTER_UNKNOWN:
        return true;
      default:
        return false;
    }
  }

  @WorkerThread
  @SuppressLint("SwitchIntDef")
  private static boolean checkSystemDndWithContactPriority(@NonNull Context context, @NonNull Recipient recipient) {
    NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);

    switch (notificationManager.getCurrentInterruptionFilter()) {
      case NotificationManager.INTERRUPTION_FILTER_ALL:
        return true;
      case NotificationManager.INTERRUPTION_FILTER_PRIORITY:
        return handlePriority(context, notificationManager, recipient);
      case NotificationManager.INTERRUPTION_FILTER_NONE:
      case NotificationManager.INTERRUPTION_FILTER_ALARMS:
        return false;
      default:
        Log.w(TAG, "Unknown interruption filter, will interrupt user: " + notificationManager.getCurrentInterruptionFilter());
        return true;
    }
  }

  private static boolean handlePriority(@NonNull Context context, @NonNull NotificationManager notificationManager, @NonNull Recipient recipient) {
    if (Build.VERSION.SDK_INT < 28 && !notificationManager.isNotificationPolicyAccessGranted()) {
      Log.w(TAG, "Notification Policy is not granted");
      return true;
    }

    final NotificationManager.Policy policy = notificationManager.getNotificationPolicy();
    if (policy == null) {
      Log.w(TAG, "Notification policy is null, likely in a private space. Allowing call to disturb user.");
      return true;
    }

    final boolean areCallsPrioritized   = (policy.priorityCategories & NotificationManager.Policy.PRIORITY_CATEGORY_CALLS) != 0;
    final boolean isRepeatCallerEnabled = (policy.priorityCategories & NotificationManager.Policy.PRIORITY_CATEGORY_REPEAT_CALLERS) != 0;

    if (!areCallsPrioritized && !isRepeatCallerEnabled) {
      return false;
    }

    final boolean isContactPriority = isContactPriority(context, recipient, policy.priorityCallSenders);
    final boolean isRepeatCaller    = isRepeatCaller(context, recipient);
    Log.i(TAG, "Handling priority - Contact priority: " + isContactPriority + ", Repeat caller: " + isRepeatCaller);

    if (areCallsPrioritized && !isRepeatCallerEnabled) {
      return isContactPriority;
    }

    if (!areCallsPrioritized) {
      return isRepeatCaller;
    }

    return isContactPriority || isRepeatCaller;
  }

  private static boolean isContactPriority(@NonNull Context context, @NonNull Recipient recipient, int priority) {
    if (!Permissions.hasAny(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
      return false;
    }

    boolean isSystemContact  = recipient.resolve().isSystemContact();
    boolean isContactStarred = isContactStarred(context, recipient);
    Log.i(TAG, "Checking contact priority - Priority " + priority + ", System contact: " + isSystemContact + ", Starred contact: " + isContactStarred);

    switch (priority) {
      case NotificationManager.Policy.PRIORITY_SENDERS_ANY:
        return true;
      case NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS:
        return isSystemContact;
      case NotificationManager.Policy.PRIORITY_SENDERS_STARRED:
        return isContactStarred;
    }

    Log.w(TAG, "Unknown priority " + priority);
    return true;
  }

  private static boolean isContactStarred(@NonNull Context context, @NonNull Recipient recipient) {
    if (!recipient.resolve().isSystemContact()) return false;

    //noinspection ConstantConditions
    try (Cursor cursor = context.getContentResolver().query(recipient.resolve().getContactUri(), new String[]{ContactsContract.Contacts.STARRED}, null, null, null)) {
      if (cursor == null || !cursor.moveToFirst()) return false;
      return CursorUtil.requireInt(cursor, ContactsContract.Contacts.STARRED) == 1;
    }
  }

  private static boolean isRepeatCaller(@NonNull Context context, @NonNull Recipient recipient) {
    return SignalDatabase.threads().hasCalledSince(recipient, System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(15));
  }

}
