package org.thoughtcrime.securesms;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
import android.net.NetworkCapabilities;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.app.NotificationManagerCompat;
import androidx.multidex.MultiDexApplication;
import androidx.work.Constraints;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import chat.delta.rpc.Rpc;
import chat.delta.rpc.RpcException;
import com.b44t.messenger.DcAccounts;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcEventChannel;
import com.b44t.messenger.DcEventEmitter;
import com.b44t.messenger.FFITransport;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.concurrent.TimeUnit;
import org.thoughtcrime.securesms.calls.CallCoordinator;
import org.thoughtcrime.securesms.connect.AccountManager;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.connect.FetchWorker;
import org.thoughtcrime.securesms.connect.ForegroundDetector;
import org.thoughtcrime.securesms.connect.KeepAliveService;
import org.thoughtcrime.securesms.connect.NetworkStateReceiver;
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.notifications.FcmReceiveService;
import org.thoughtcrime.securesms.notifications.InChatSounds;
import org.thoughtcrime.securesms.notifications.NotificationCenter;
import org.thoughtcrime.securesms.util.AndroidSignalProtocolLogger;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.Prefs;
import org.thoughtcrime.securesms.util.SignalProtocolLoggerProvider;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.webxdc.WebxdcGarbageCollectionWorker;

public class ApplicationContext extends MultiDexApplication {
  private static final String TAG = "ApplicationContext";
  private static final Object initLock = new Object();
  private static volatile boolean isInitialized = false;

  private static DcAccounts dcAccounts;
  private Rpc rpc;
  private DcContext dcContext;

  private DcEventCenter eventCenter;
  private NotificationCenter notificationCenter;
  private JobManager jobManager;

  private int debugOnAvailableCount;
  private int debugOnBlockedStatusChangedCount;
  private int debugOnCapabilitiesChangedCount;
  private int debugOnLinkPropertiesChangedCount;

  public static ApplicationContext getInstance(@NonNull Context context) {
    return (ApplicationContext) context.getApplicationContext();
  }

  private static void ensureInitialized() {
    synchronized (initLock) {
      while (!isInitialized) {
        try {
          initLock.wait();
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
          throw new RuntimeException("Interrupted while waiting for initialization", e);
        }
      }
    }
  }

  /**
   * Get DcAccounts instance, waiting for initialization if necessary. This method is thread-safe
   * and will block until initialization is complete.
   */
  public static DcAccounts getDcAccounts() {
    ensureInitialized();
    return dcAccounts;
  }

  /**
   * Get Rpc instance, waiting for initialization if necessary. This method is thread-safe and will
   * block until initialization is complete.
   */
  public Rpc getRpc() {
    ensureInitialized();
    return rpc;
  }

  /**
   * Get DcContext instance, waiting for initialization if necessary. This method is thread-safe and
   * will block until initialization is complete.
   */
  public DcContext getDcContext() {
    ensureInitialized();
    return dcContext;
  }

  /**
   * Set DcContext instance. This should only be called by AccountManager when switching accounts,
   * which only happens after initial initialization is complete. This method is thread-safe but
   * does NOT trigger initialization or notify waiting threads.
   */
  public void setDcContext(DcContext dcContext) {
    synchronized (initLock) {
      this.dcContext = dcContext;
    }
  }

  /**
   * Get DcEventCenter instance, waiting for initialization if necessary. This method is thread-safe
   * and will block until initialization is complete.
   */
  public DcEventCenter getEventCenter() {
    ensureInitialized();
    return eventCenter;
  }

  /**
   * Get NotificationCenter instance, waiting for initialization if necessary. This method is
   * thread-safe and will block until initialization is complete.
   */
  public NotificationCenter getNotificationCenter() {
    ensureInitialized();
    return notificationCenter;
  }

  @Override
  public void onCreate() {
    super.onCreate();

    Thread.setDefaultUncaughtExceptionHandler(
        (thread, throwable) -> {
          StringWriter stringWriter = new StringWriter();
          throwable.printStackTrace(new PrintWriter(stringWriter, true));
          String errorMsg =
              "Android " + Build.VERSION.RELEASE + ":\n" + stringWriter.getBuffer().toString();
          errorMsg += "\n" + LogViewFragment.grabLogcat();
          String subject =
              "ArcaneChat " + BuildConfig.VERSION_NAME + "-" + BuildConfig.FLAVOR + " Crash Report";
          Intent intent = new Intent(android.content.Intent.ACTION_SEND);
          intent.setType("text/plain");
          intent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
          intent.putExtra(android.content.Intent.EXTRA_TEXT, subject + "\n\n" + errorMsg);
          intent.putExtra(Intent.EXTRA_EMAIL, "adb@merlinux.eu");
          Intent chooser = Intent.createChooser(intent, subject);
          chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
          chooser.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
          startActivity(chooser);

          try {
            ApplicationContext.this.finalize();
          } catch (Throwable e) {
          }
        });

    // if (LeakCanary.isInAnalyzerProcess(this)) {
    //   // This process is dedicated to LeakCanary for heap analysis.
    //   // You should not init your app in this process.
    //   return;
    // }
    // LeakCanary.install(this);

    Log.i("DeltaChat", "++++++++++++++++++ ApplicationContext.onCreate() ++++++++++++++++++");

    System.loadLibrary("native-utils");

    // Initialize DcAccounts in background to avoid ANR during SQL migrations
    Util.runOnBackground(
        () -> {
          synchronized (initLock) {
            try {
              DcEventChannel eventChannel = new DcEventChannel();
              DcEventEmitter emitter = eventChannel.getEventEmitter();
              eventCenter = new DcEventCenter(this);

              new Thread(
                      () -> {
                        Log.i(TAG, "Starting event loop");
                        while (true) {
                          DcEvent event = emitter.getNextEvent();
                          if (event == null) {
                            break;
                          }
                          if (isInitialized) {
                            eventCenter.handleEvent(event);
                          } else {
                            // not fully initialized, only handle logging events,
                            // ex. account migrations during DcAccounts initialization
                            eventCenter.handleLogging(event);
                          }
                        }
                        Log.i("DeltaChat", "shutting down event handler");
                      },
                      "eventThread")
                  .start();

              dcAccounts =
                  new DcAccounts(
                      new File(getFilesDir(), "accounts").getAbsolutePath(), eventChannel);
              Log.i(TAG, "DcAccounts created");
              rpc = new Rpc(new FFITransport(dcAccounts.getJsonrpcInstance()));
              Log.i(TAG, "Rpc created");
              AccountManager.getInstance().migrateToDcAccounts(this, dcAccounts);

              int[] allAccounts = dcAccounts.getAll();
              Log.i(TAG, "Number of profiles: " + allAccounts.length);
              for (int accountId : allAccounts) {
                DcContext ac = dcAccounts.getAccount(accountId);
                if (!ac.isOpen()) {
                  try {
                    DatabaseSecret secret =
                        DatabaseSecretProvider.getOrCreateDatabaseSecret(this, accountId);
                    boolean res = ac.open(secret.asString());
                    if (res)
                      Log.i(
                          TAG,
                          "Successfully opened account "
                              + accountId
                              + ", path: "
                              + ac.getBlobdir());
                    else
                      Log.e(
                          TAG, "Error opening account " + accountId + ", path: " + ac.getBlobdir());
                  } catch (Exception e) {
                    Log.e(
                        TAG,
                        "Failed to open account "
                            + accountId
                            + ", path: "
                            + ac.getBlobdir()
                            + ": "
                            + e);
                    e.printStackTrace();
                  }
                }

                // 2025-12-16: The setting was removed.
                // Revert it to the default if it was changed in the past.
                ac.setConfigInt("webxdc_realtime_enabled", 1);
              }
              if (allAccounts.length == 0) {
                try {
                  rpc.addAccount();
                } catch (RpcException e) {
                  e.printStackTrace();
                }
              }
              dcContext = dcAccounts.getSelectedAccount();
              notificationCenter = new NotificationCenter(this);

              isInitialized = true;
              initLock.notifyAll();
              Log.i(TAG, "DcAccounts initialization complete");

              // set translations before starting I/O to avoid sending untranslated MDNs (issue
              // #2288)
              DcHelper.setStockTranslations(this);

              dcAccounts.startIo();
            } catch (Exception e) {
              Log.e(TAG, "Fatal error during DcAccounts initialization", e);
              // Mark as initialized even on error to avoid deadlock
              isInitialized = true;
              initLock.notifyAll();
              throw new RuntimeException("Failed to initialize DcAccounts", e);
            }
          }
        });

    // October-2025 migration: delete deprecated "permanent channel" id
    NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
    notificationManager.deleteNotificationChannel("dc_foreground_notification_ch");
    // end October-2025 migration

    new ForegroundDetector(ApplicationContext.getInstance(this));

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
      ConnectivityManager connectivityManager =
          (ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE);
      connectivityManager.registerDefaultNetworkCallback(
          new ConnectivityManager.NetworkCallback() {
            @Override
            public void onAvailable(@NonNull android.net.Network network) {
              Log.i(
                  "DeltaChat",
                  "++++++++++++++++++ NetworkCallback.onAvailable() #" + debugOnAvailableCount++);
              // onBlockedStatusChanged is only available on API 29+
              if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
                getDcAccounts().maybeNetwork();
              }
            }

            @Override
            public void onBlockedStatusChanged(
                @NonNull android.net.Network network, boolean blocked) {
              Log.i(
                  "DeltaChat",
                  "++++++++++++++++++ NetworkCallback.onBlockedStatusChanged("
                      + blocked
                      + ") #"
                      + debugOnBlockedStatusChangedCount++);
              if (!blocked) {
                getDcAccounts().maybeNetwork();
              }
            }

            @Override
            public void onCapabilitiesChanged(
                @NonNull android.net.Network network, NetworkCapabilities networkCapabilities) {
              // usually called after onAvailable(), so a maybeNetwork seems contraproductive
              Log.i(
                  "DeltaChat",
                  "++++++++++++++++++ NetworkCallback.onCapabilitiesChanged() #"
                      + debugOnCapabilitiesChangedCount++);
            }

            @Override
            public void onLinkPropertiesChanged(
                @NonNull android.net.Network network, LinkProperties linkProperties) {
              Log.i(
                  "DeltaChat",
                  "++++++++++++++++++ NetworkCallback.onLinkPropertiesChanged() #"
                      + debugOnLinkPropertiesChangedCount++);
            }
          });
    } // no else: use old method for debugging
    BroadcastReceiver networkStateReceiver = new NetworkStateReceiver();
    registerReceiver(
        networkStateReceiver,
        new IntentFilter(android.net.ConnectivityManager.CONNECTIVITY_ACTION));

    KeepAliveService.maybeStartSelf(this);

    initializeLogging();
    initializeJobManager();
    InChatSounds.getInstance(this);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      EglUtils.getEglBase();
      CallCoordinator.getInstance(this);
    }

    DynamicTheme.setDefaultDayNightMode(this);

    IntentFilter filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
    registerReceiver(
        new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
            Util.localeChanged();
            DcHelper.setStockTranslations(context);
          }
        },
        filter);

    AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);

    if (Prefs.isPushEnabled(this)) {
      FcmReceiveService.register(this);
    } else {
      Log.i(TAG, "FCM disabled at build time");
      // MAYBE TODO: i think the ApplicationContext is also created
      // when the app is stated by FetchWorker timeouts.
      // in this case, the normal threads shall not be started.
      Constraints constraints =
          new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build();
      PeriodicWorkRequest fetchWorkRequest =
          new PeriodicWorkRequest.Builder(
                  FetchWorker.class,
                  PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, // usually 15 minutes
                  TimeUnit.MILLISECONDS,
                  PeriodicWorkRequest
                      .MIN_PERIODIC_FLEX_MILLIS, // the start may be preferred by up to 5 minutes,
                  // so we run every 10-15 minutes
                  TimeUnit.MILLISECONDS)
              .setConstraints(constraints)
              .build();
      WorkManager.getInstance(this)
          .enqueueUniquePeriodicWork(
              "FetchWorker", ExistingPeriodicWorkPolicy.KEEP, fetchWorkRequest);
    }

    PeriodicWorkRequest webxdcGarbageCollectionRequest =
        new PeriodicWorkRequest.Builder(
                WebxdcGarbageCollectionWorker.class,
                PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS,
                TimeUnit.MILLISECONDS,
                PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS,
                TimeUnit.MILLISECONDS)
            .build();
    WorkManager.getInstance(this)
        .enqueueUniquePeriodicWork(
            "WebxdcGarbageCollectionWorker",
            ExistingPeriodicWorkPolicy.KEEP,
            webxdcGarbageCollectionRequest);

    Log.i("DeltaChat", "+++++++++++ ApplicationContext.onCreate() finished ++++++++++");
  }

  @Override
  public void onTerminate() {
    super.onTerminate();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      EglUtils.release();
    }
  }

  public JobManager getJobManager() {
    return jobManager;
  }

  private void initializeLogging() {
    SignalProtocolLoggerProvider.setProvider(new AndroidSignalProtocolLogger());
  }

  private void initializeJobManager() {
    this.jobManager = new JobManager(this, 5);
  }
}
