package com.foxx.androidcast.crash;

/*********************************************************************
 * CrashWatcherService.java
 * Created at: Wed 20 May 2026 14:31:55 +0200
 * Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
 * Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
 * class CrashWatcherService
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (2 commits, 139 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	4135badad895535cd2635563f9b7a87da7ee1fdf4b1dd757f596d7eef67e82f6
**********************************************************************/
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.os.SystemClock;
import android.util.Log;

import com.foxx.androidcast.util.IoUtils;

import org.json.JSONObject;

import java.io.File;
import java.io.FileInputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Standalone process ({@code :crashwatcher}) — uploads pending crash JSON even if the main
 * cast process died. Reads {@code settings.json} crash overrides on each sweep.
 */
public final class CrashWatcherService extends Service {
    private static final String TAG = "CrashWatcher";
    public static final String ACTION_UPLOAD_PENDING = "com.foxx.androidcast.crash.UPLOAD_PENDING";
    private static final int ALARM_REQUEST = 4102;
    private static final long IMMEDIATE_DEBOUNCE_MS = 30_000L;

    private static final AtomicBoolean SWEEP_RUNNING = new AtomicBoolean(false);
    private static volatile long lastImmediateRequestElapsedMs;

    /** One immediate sweep (debounced) plus the next periodic alarm. */
    public static void scheduleUploadSweep(Context context) {
        Context app = context.getApplicationContext();
        schedulePeriodicAlarm(app);
        requestImmediateSweep(app);
    }

    /** Alarm-only periodic upload; does not start the service now. */
    public static void schedulePeriodicAlarm(Context context) {
        Context app = context.getApplicationContext();
        CrashSettings settings = CrashSettingsStore.load(app);
        AlarmManager alarm = (AlarmManager) app.getSystemService(Context.ALARM_SERVICE);
        if (alarm == null) {
            return;
        }
        PendingIntent pi = pendingAlarmIntent(app);
        if (!settings.enabled) {
            alarm.cancel(pi);
            return;
        }
        long trigger = System.currentTimeMillis() + settings.uploadIntervalMs;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            alarm.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, trigger, pi);
        } else {
            alarm.set(AlarmManager.RTC_WAKEUP, trigger, pi);
        }
    }

    public static void requestImmediateSweep(Context context) {
        requestImmediateSweep(context, false);
    }

    /** Start a sweep now if one is not already running (coalesces duplicate requests). */
    public static void requestImmediateSweep(Context context, boolean bypassDebounce) {
        Context app = context.getApplicationContext();
        if (!CrashSettingsStore.load(app).enabled) {
            return;
        }
        if (!bypassDebounce) {
            long now = SystemClock.elapsedRealtime();
            synchronized (CrashWatcherService.class) {
                if (now - lastImmediateRequestElapsedMs < IMMEDIATE_DEBOUNCE_MS) {
                    return;
                }
                lastImmediateRequestElapsedMs = now;
            }
        }
        if (SWEEP_RUNNING.get()) {
            return;
        }
        try {
            Intent i = new Intent(app, CrashWatcherService.class);
            i.setAction(ACTION_UPLOAD_PENDING);
            app.startService(i);
        } catch (Exception e) {
            Log.w(TAG, "requestImmediateSweep: " + e.getMessage());
        }
    }

    private static PendingIntent pendingAlarmIntent(Context app) {
        Intent periodic = new Intent(app, CrashWatcherService.class);
        periodic.setAction(ACTION_UPLOAD_PENDING);
        return PendingIntent.getService(
                app,
                ALARM_REQUEST,
                periodic,
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (!SWEEP_RUNNING.compareAndSet(false, true)) {
            stopSelf(startId);
            return START_NOT_STICKY;
        }
        new Thread(() -> {
            try {
                runSweep();
            } finally {
                SWEEP_RUNNING.set(false);
                schedulePeriodicAlarm(getApplicationContext());
                stopSelf(startId);
            }
        }, "CrashWatcherSweep").start();
        return START_NOT_STICKY;
    }

    private void runSweep() {
        CrashSettings settings = CrashSettingsStore.load(this);
        if (!settings.enabled) {
            return;
        }
        ingestNativeStubs(getFilesDir());
        List<File> pending = CrashReportStore.listPending(getFilesDir());
        for (File file : pending) {
            JSONObject report = CrashReportStore.readJson(file);
            if (report == null) {
                file.delete();
                continue;
            }
            boolean ok = CrashUploadClient.upload(settings.uploadUrl, report);
            if (ok) {
                CrashReportStore.markUploaded(getFilesDir(), file, settings.pruneAfterUpload);
                Log.i(TAG, "Uploaded " + file.getName());
            } else {
                Log.w(TAG, "Upload failed for " + file.getName());
            }
        }
    }

    /** Converts native stub text files (written from JNI signal handler) into pending JSON. */
    private void ingestNativeStubs(File filesDir) {
        File nativeDir = new File(new File(filesDir, "crash_reports"), "native");
        if (!nativeDir.exists()) {
            return;
        }
        File[] stubs = nativeDir.listFiles((d, n) -> n.endsWith(".txt"));
        if (stubs == null) {
            return;
        }
        for (File stub : stubs) {
            try {
                byte[] buf;
                try (FileInputStream fis = new FileInputStream(stub)) {
                    buf = IoUtils.readAllBytes(fis);
                }
                String text = new String(buf, StandardCharsets.UTF_8);
                String signal = "UNKNOWN";
                int sigIdx = text.indexOf("signal=");
                if (sigIdx >= 0) {
                    int end = text.indexOf('\n', sigIdx);
                    signal = text.substring(sigIdx + 7, end > sigIdx ? end : text.length()).trim();
                }
                JSONObject report = CrashReportBuilder.buildNativeCrash(this, signal, text);
                long generatedAt = report.optLong("generated_at_epoch_ms", stub.lastModified());
                if (generatedAt <= 0L) {
                    generatedAt = System.currentTimeMillis();
                }
                File pending = CrashReportStore.newPendingFile(filesDir, generatedAt);
                report.put("report_id", pending.getName().substring(
                        CrashReportStore.PREFIX.length(),
                        pending.getName().length() - ".json".length()));
                report.put("report_file", pending.getName());
                CrashReportStore.writeJson(pending, report);
                stub.delete();
            } catch (Exception e) {
                Log.w(TAG, "ingest native stub failed: " + stub.getName());
            }
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}
