package com.foxx.androidcast.crash;

/*********************************************************************
 * CrashReportBuilder.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 CrashReportBuilder
 * Contributors:
 *    - Anton Afanasyeu <a.afanasieff@gmail.com> (2 commits, 186 lines)
 *    - Cursor Agent (project assistant)
 * Digest: SHA256	8e12433dd47e8151a7041287f523a113487e2aa03fed928e1ac5ca02d373440f
**********************************************************************/
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;

import com.foxx.androidcast.BuildConfig;
import com.foxx.androidcast.stats.SessionStatsContext;
import com.foxx.androidcast.stats.SessionStatsRecorder;

import org.json.JSONArray;
import org.json.JSONObject;

import java.security.MessageDigest;
import java.util.Locale;

/** Builds anonymous crash report JSON (schema v1). */
public final class CrashReportBuilder {
    private CrashReportBuilder() {}

    public static JSONObject buildJavaCrash(Context context, Thread thread, Throwable throwable) {
        JSONObject root = baseReport(context, "java");
        try {
            JSONObject java = new JSONObject();
            java.put("thread", thread != null ? thread.getName() : "unknown");
            java.put("exception", throwable.getClass().getName());
            String msg = throwable.getMessage();
            if (msg != null) {
                java.put("message", msg);
            }
            JSONArray frames = new JSONArray();
            for (StackTraceElement el : throwable.getStackTrace()) {
                frames.put(el.toString());
            }
            Throwable cause = throwable.getCause();
            if (cause != null && cause != throwable) {
                java.put("cause", cause.getClass().getName() + ": " + cause.getMessage());
            }
            java.put("stack_frames", frames);
            root.put("java", java);
            root.put("fingerprint", fingerprintJava(throwable));
        } catch (Exception ignored) {
        }
        attachSessionContext(root);
        return root;
    }

    public static JSONObject buildNativeCrash(Context context, String signalName, String backtraceText) {
        JSONObject root = baseReport(context, "native");
        try {
            JSONObject nat = new JSONObject();
            nat.put("signal", signalName != null ? signalName : "UNKNOWN");
            JSONArray frames = new JSONArray();
            if (backtraceText != null) {
                for (String line : backtraceText.split("\n")) {
                    String t = line.trim();
                    if (!t.isEmpty()) {
                        frames.put(t);
                    }
                }
            }
            nat.put("backtrace", frames);
            root.put("native", nat);
            root.put("fingerprint", fingerprintNative(signalName, backtraceText));
        } catch (Exception ignored) {
        }
        attachSessionContext(root);
        return root;
    }

    private static JSONObject baseReport(Context context, String crashType) {
        JSONObject root = new JSONObject();
        try {
            long now = System.currentTimeMillis();
            String stamp = CrashReportStore.formatTimestamp(now);
            root.put("schema_version", 1);
            root.put("report_id", stamp);
            root.put("generated_at_epoch_ms", now);
            root.put("crash_type", crashType);
            root.put("scenario", CrashRuntimeContext.scenarioLabel());
            root.put("process", currentProcessName());
            root.put("device", deviceJson());
            root.put("app", appJson(context));
            root.put("build", buildJson());
            root.put("runtime_context", CrashRuntimeContext.snapshot(context));
        } catch (Exception ignored) {
        }
        return root;
    }

    private static String currentProcessName() {
        try {
            Class<?> at = Class.forName("android.app.ActivityThread");
            Object proc = at.getMethod("currentProcessName").invoke(null);
            if (proc instanceof String && !((String) proc).isEmpty()) {
                return (String) proc;
            }
        } catch (Exception ignored) {
        }
        return "main";
    }

    private static JSONObject deviceJson() throws Exception {
        JSONObject d = new JSONObject();
        d.put("manufacturer", Build.MANUFACTURER);
        d.put("brand", Build.BRAND);
        d.put("model", Build.MODEL);
        d.put("device", Build.DEVICE);
        d.put("product", Build.PRODUCT);
        d.put("sdk_int", Build.VERSION.SDK_INT);
        d.put("release", Build.VERSION.RELEASE);
        JSONArray abis = new JSONArray();
        for (String abi : Build.SUPPORTED_ABIS) {
            abis.put(abi);
        }
        d.put("abis", abis);
        return d;
    }

    private static JSONObject appJson(Context context) throws Exception {
        JSONObject a = new JSONObject();
        a.put("package", context.getPackageName());
        PackageManager pm = context.getPackageManager();
        PackageInfo info = pm.getPackageInfo(context.getPackageName(), 0);
        a.put("version_name", info.versionName != null ? info.versionName : "");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            a.put("version_code", info.getLongVersionCode());
        } else {
            a.put("version_code", info.versionCode);
        }
        return a;
    }

    private static JSONObject buildJson() throws Exception {
        JSONObject b = new JSONObject();
        b.put("debug", BuildConfig.DEBUG);
        b.put("git_commit", BuildConfig.GIT_COMMIT_SHORT);
        return b;
    }

    private static void attachSessionContext(JSONObject root) {
        try {
            SessionStatsRecorder recorder = SessionStatsContext.get();
            if (recorder == null) {
                return;
            }
            JSONObject ctx = new JSONObject();
            ctx.put("direction", recorder.getDirection());
            ctx.put("role", "recv".equals(recorder.getDirection()) ? "receiver" : "sender");
            root.put("session_context", ctx);
        } catch (Exception ignored) {
        }
    }

    static String fingerprintJava(Throwable t) {
        StringBuilder sb = new StringBuilder();
        sb.append(t.getClass().getName());
        StackTraceElement[] st = t.getStackTrace();
        int limit = Math.min(8, st.length);
        for (int i = 0; i < limit; i++) {
            sb.append('|').append(st[i].getClassName()).append('.').append(st[i].getMethodName());
        }
        return sha256Hex(sb.toString());
    }

    static String fingerprintNative(String signal, String backtrace) {
        StringBuilder sb = new StringBuilder();
        sb.append(signal != null ? signal : "");
        if (backtrace != null) {
            String[] lines = backtrace.split("\n");
            int limit = Math.min(6, lines.length);
            for (int i = 0; i < limit; i++) {
                sb.append('|').append(lines[i].trim());
            }
        }
        return sha256Hex(sb.toString());
    }

    private static String sha256Hex(String text) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] digest = md.digest(text.getBytes(java.nio.charset.StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder(digest.length * 2);
            for (byte b : digest) {
                sb.append(String.format(Locale.US, "%02x", b & 0xff));
            }
            return sb.toString();
        } catch (Exception e) {
            return "";
        }
    }
}
