package com.foxx.androidcast.ota;

/*********************************************************************
 * OtaUpdateService.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 OtaUpdateService
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (2 commits, 372 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	d3fc9f2f38c78893ac5e17c4cc715562d29c10763a11eb64548ba59d4d03dfff
**********************************************************************/
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.app.AlarmManager;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ResultReceiver;

import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;

import com.foxx.androidcast.AppPreferences;
import com.foxx.androidcast.BuildConfig;
import com.foxx.androidcast.MainActivity;
import com.foxx.androidcast.R;
import com.foxx.androidcast.util.CastBackgroundExecutor;

import java.io.File;
import java.util.List;

/**
 * Standalone OTA lifecycle: channel/manifest fetch, optional user prompt via broadcast,
 * and foreground APK download + install intent.
 */
public final class OtaUpdateService extends Service {
    public static final String ACTION_CHECK_ON_LAUNCH = "com.foxx.androidcast.ota.CHECK_ON_LAUNCH";
    public static final String ACTION_CHECK_MANUAL = "com.foxx.androidcast.ota.CHECK_MANUAL";
    public static final String ACTION_CHECK_PERIODIC = "com.foxx.androidcast.ota.CHECK_PERIODIC";
    public static final String ACTION_DOWNLOAD = "com.foxx.androidcast.ota.DOWNLOAD";

    public static final String EXTRA_MANIFEST_JSON = "manifestJson";
    public static final String EXTRA_RESULT_RECEIVER = "resultReceiver";
    public static final String EXTRA_SOURCE_URL = "sourceUrl";
    public static final String EXTRA_TRUSTED_SOURCE = "trustedSource";

    public static final String ACTION_UPDATE_AVAILABLE = "com.foxx.androidcast.ota.UPDATE_AVAILABLE";

    public static final String EXTRA_ERROR = "error";
    public static final String EXTRA_INSTALLED_VERSION_NAME = "installedVersionName";
    public static final String EXTRA_INSTALLED_VERSION_CODE = "installedVersionCode";

    public static final int RESULT_UP_TO_DATE = 1;
    public static final int RESULT_UPDATE_AVAILABLE = 2;
    public static final int RESULT_ERROR = 3;
    public static final int RESULT_DISMISSED = 4;

    private static final String CHANNEL_OTA = "ota_updates";
    private static final int NOTIFICATION_DOWNLOAD = 2001;

    public static void enqueueCheckOnLaunch(Context context) {
        Intent i = new Intent(context, OtaUpdateService.class);
        i.setAction(ACTION_CHECK_ON_LAUNCH);
        context.startService(i);
    }

    public static void enqueueCheckManual(Context context, @Nullable ResultReceiver resultReceiver) {
        Intent i = new Intent(context, OtaUpdateService.class);
        i.setAction(ACTION_CHECK_MANUAL);
        if (resultReceiver != null) {
            i.putExtra(EXTRA_RESULT_RECEIVER, resultReceiver);
        }
        context.startService(i);
    }

    public static void enqueueDownload(Context context, OtaManifest manifest) {
        enqueueDownload(context, manifest, false, "");
    }

    public static void enqueueDownload(Context context, OtaManifest manifest, boolean trustedSource,
            String sourceUrl) {
        if (manifest == null) {
            return;
        }
        Intent i = new Intent(context, OtaUpdateService.class);
        i.setAction(ACTION_DOWNLOAD);
        i.putExtra(EXTRA_MANIFEST_JSON, manifest.toJsonString());
        i.putExtra(EXTRA_TRUSTED_SOURCE, trustedSource);
        i.putExtra(EXTRA_SOURCE_URL, sourceUrl != null ? sourceUrl : "");
        ContextCompat.startForegroundService(context, i);
    }

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

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent == null || intent.getAction() == null) {
            stopSelf(startId);
            return START_NOT_STICKY;
        }
        String action = intent.getAction();
        if (ACTION_DOWNLOAD.equals(action)) {
            String json = intent.getStringExtra(EXTRA_MANIFEST_JSON);
            OtaManifest manifest = OtaManifest.fromJsonString(json);
            if (manifest == null || !manifest.isValid()) {
                stopSelf(startId);
                return START_NOT_STICKY;
            }
            boolean trustedSource = intent.getBooleanExtra(EXTRA_TRUSTED_SOURCE, false);
            String sourceUrl = intent.getStringExtra(EXTRA_SOURCE_URL);
            startDownloadJob(manifest, trustedSource, sourceUrl, startId);
            return START_NOT_STICKY;
        }
        CastBackgroundExecutor.execute(() -> {
            try {
                if (ACTION_CHECK_ON_LAUNCH.equals(action)) {
                    runCheckOnLaunch();
                } else if (ACTION_CHECK_MANUAL.equals(action)) {
                    ResultReceiver receiver = null;
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                        receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER, ResultReceiver.class);
                    } else {
                        receiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER);
                    }
                    runCheckManual(receiver);
                } else if (ACTION_CHECK_PERIODIC.equals(action)) {
                    runCheckPeriodic();
                }
            } finally {
                stopSelf(startId);
            }
        });
        return START_NOT_STICKY;
    }

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

    private void runCheckOnLaunch() {
        if (!AppPreferences.isOtaCheckOnLaunch(this)) {
            return;
        }
        OtaRuntimeSettings settings = OtaSettingsStore.load(this);
        scheduleNextCheck(settings.checkIntervalMs);
        if (settings.sourceUrls.isEmpty()) {
            return;
        }
        long now = System.currentTimeMillis();
        if (now - AppPreferences.getOtaLastCheckMs(this) < settings.checkIntervalMs) {
            return;
        }
        AppPreferences.setOtaLastCheckMs(this, now);
        OtaUpdateChecker.Result result = checkRoundRobin(settings);
        if (result.error != null || !result.updateAvailable || result.manifest == null) {
            return;
        }
        if (result.manifest.effectiveVersionCode() == AppPreferences.getOtaDismissedVersionCode(this)) {
            return;
        }
        sendUpdateBroadcast(result.manifest, result.sourceUrl, settings.trusted);
    }

    private void runCheckPeriodic() {
        OtaRuntimeSettings settings = OtaSettingsStore.load(this);
        scheduleNextCheck(settings.checkIntervalMs);
        if (!AppPreferences.isOtaCheckOnLaunch(this) || settings.sourceUrls.isEmpty()) {
            return;
        }
        long now = System.currentTimeMillis();
        if (now - AppPreferences.getOtaLastCheckMs(this) < settings.checkIntervalMs) {
            return;
        }
        AppPreferences.setOtaLastCheckMs(this, now);
        OtaUpdateChecker.Result result = checkRoundRobin(settings);
        if (result.error != null || !result.updateAvailable || result.manifest == null) {
            return;
        }
        if (result.manifest.effectiveVersionCode() == AppPreferences.getOtaDismissedVersionCode(this)) {
            return;
        }
        sendUpdateBroadcast(result.manifest, result.sourceUrl, settings.trusted);
    }

    private void runCheckManual(@Nullable ResultReceiver receiver) {
        OtaRuntimeSettings settings = OtaSettingsStore.load(this);
        scheduleNextCheck(settings.checkIntervalMs);
        if (settings.sourceUrls.isEmpty()) {
            deliver(receiver, RESULT_ERROR, bundleError(getString(R.string.ota_no_url)));
            return;
        }
        OtaUpdateChecker.Result result = checkRoundRobin(settings);
        if (result.error != null) {
            deliver(receiver, RESULT_ERROR, bundleError(result.error));
            return;
        }
        if (!result.updateAvailable || result.manifest == null) {
            Bundle b = new Bundle();
            b.putString(EXTRA_INSTALLED_VERSION_NAME, result.installedVersionName);
            b.putInt(EXTRA_INSTALLED_VERSION_CODE, result.installedVersionCode);
            deliver(receiver, RESULT_UP_TO_DATE, b);
            return;
        }
        if (result.manifest.effectiveVersionCode() == AppPreferences.getOtaDismissedVersionCode(this)) {
            deliver(receiver, RESULT_DISMISSED, Bundle.EMPTY);
            return;
        }
        Bundle b = new Bundle();
        b.putString(EXTRA_MANIFEST_JSON, result.manifest.toJsonString());
        b.putString(EXTRA_SOURCE_URL, result.sourceUrl);
        b.putBoolean(EXTRA_TRUSTED_SOURCE, settings.trusted);
        deliver(receiver, RESULT_UPDATE_AVAILABLE, b);
    }

    private static void deliver(@Nullable ResultReceiver receiver, int code, Bundle data) {
        if (receiver == null) {
            return;
        }
        receiver.send(code, data != null ? data : Bundle.EMPTY);
    }

    private Bundle bundleError(String message) {
        Bundle b = new Bundle();
        b.putString(EXTRA_ERROR, message);
        return b;
    }

    private OtaUpdateChecker.Result checkRoundRobin(OtaRuntimeSettings settings) {
        List<String> urls = settings.sourceUrls;
        if (urls.isEmpty()) {
            return OtaUpdateChecker.Result.error("", getString(R.string.ota_no_url));
        }
        int start = AppPreferences.getOtaRoundRobinIndex(this);
        if (start >= urls.size()) {
            start = 0;
        }
        int next = (start + 1) % urls.size();
        AppPreferences.setOtaRoundRobinIndex(this, next);
        OtaUpdateChecker.Result lastError = null;
        for (int i = 0; i < urls.size(); i++) {
            int idx = (start + i) % urls.size();
            String source = urls.get(idx);
            boolean strictVerification = !(settings.trusted && BuildConfig.DEBUG);
            OtaUpdateChecker.Result result = OtaUpdateChecker.check(this, source, strictVerification);
            if (result.error == null) {
                return result;
            }
            lastError = result;
        }
        if (lastError != null) {
            return lastError;
        }
        return OtaUpdateChecker.Result.error("", "No OTA sources configured");
    }

    private void sendUpdateBroadcast(OtaManifest manifest, String sourceUrl, boolean trustedSource) {
        Intent i = new Intent(ACTION_UPDATE_AVAILABLE);
        i.setPackage(getPackageName());
        i.putExtra(EXTRA_MANIFEST_JSON, manifest.toJsonString());
        i.putExtra(EXTRA_SOURCE_URL, sourceUrl != null ? sourceUrl : "");
        i.putExtra(EXTRA_TRUSTED_SOURCE, trustedSource);
        sendBroadcast(i);
    }

    private void startDownloadJob(OtaManifest manifest, boolean trustedSource, String sourceUrl, int startId) {
        new Thread(() -> {
            NotificationManager nm = getSystemService(NotificationManager.class);
            try {
                Notification initial = buildDownloadNotification(0, 0, true);
                startForeground(NOTIFICATION_DOWNLOAD, initial);
                OtaManifest checkedManifest = manifest;
                if (trustedSource && BuildConfig.DEBUG) {
                    checkedManifest = new OtaManifest(
                            manifest.major,
                            manifest.minor,
                            manifest.build,
                            manifest.versionCode,
                            manifest.versionName,
                            manifest.apkUrl,
                            manifest.signUrl,
                            "",
                            manifest.packageSizeBytes,
                            manifest.mandatory,
                            manifest.releaseNotes,
                            manifest.bundleUrl,
                            "",
                            manifest.bundleSizeBytes);
                }
                File apk = OtaApkInstaller.downloadApk(this, checkedManifest, (downloaded, total) -> {
                    Notification n = buildDownloadNotification(downloaded, total, total <= 0);
                    nm.notify(NOTIFICATION_DOWNLOAD, n);
                });
                stopForeground(STOP_FOREGROUND_REMOVE);
                nm.cancel(NOTIFICATION_DOWNLOAD);
                OtaApkInstaller.launchInstall(getApplicationContext(), apk);
            } catch (Exception e) {
                String msg = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
                Notification err = new NotificationCompat.Builder(this, CHANNEL_OTA)
                        .setSmallIcon(R.drawable.ic_notification)
                        .setContentTitle(getString(R.string.ota_download_title))
                        .setContentText(getString(R.string.ota_download_failed, msg))
                        .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                        .build();
                nm.notify(NOTIFICATION_DOWNLOAD + 1, err);
                stopForeground(STOP_FOREGROUND_REMOVE);
                nm.cancel(NOTIFICATION_DOWNLOAD);
            } finally {
                stopSelf(startId);
            }
        }, "OtaApkDownload").start();
    }

    private Notification buildDownloadNotification(long downloaded, long total, boolean indeterminate) {
        NotificationCompat.Builder b = new NotificationCompat.Builder(this, CHANNEL_OTA)
                .setSmallIcon(R.drawable.ic_notification)
                .setContentTitle(getString(R.string.ota_download_title))
                .setContentText(getString(R.string.ota_downloading))
                .setOnlyAlertOnce(true)
                .setOngoing(true)
                .setPriority(NotificationCompat.PRIORITY_LOW);
        if (indeterminate || total <= 0) {
            b.setProgress(0, 0, true);
        } else {
            int max = 100;
            int prog = (int) Math.min(max, downloaded * max / total);
            b.setProgress(max, prog, false);
        }
        return b.build();
    }

    private void ensureChannel() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return;
        }
        NotificationChannel ch = new NotificationChannel(
                CHANNEL_OTA,
                getString(R.string.ota_notification_channel_name),
                NotificationManager.IMPORTANCE_LOW);
        NotificationManager nm = getSystemService(NotificationManager.class);
        nm.createNotificationChannel(ch);
    }

    private void scheduleNextCheck(long delayMs) {
        AlarmManager alarm = getSystemService(AlarmManager.class);
        if (alarm == null) {
            return;
        }
        long triggerAt = System.currentTimeMillis() + Math.max(10_000L, delayMs);
        Intent intent = new Intent(this, OtaUpdateService.class);
        intent.setAction(ACTION_CHECK_PERIODIC);
        PendingIntent pi = PendingIntent.getService(
                this,
                3001,
                intent,
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            alarm.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAt, pi);
        } else {
            alarm.set(AlarmManager.RTC_WAKEUP, triggerAt, pi);
        }
    }
}
