package de.storchp.fdroidbuildstatus.utils

import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Build.VERSION
import de.storchp.fdroidbuildstatus.BuildConfig
import de.storchp.fdroidbuildstatus.EXTRA_ERROR_TEXT
import de.storchp.fdroidbuildstatus.FdroidBuildStatusApplication
import de.storchp.fdroidbuildstatus.ShowErrorActivity
import java.io.PrintWriter
import java.io.StringWriter

class ExceptionHandler(
    private val context: Context,
    private val defaultExceptionHandler: Thread.UncaughtExceptionHandler
) : Thread.UncaughtExceptionHandler {
    override fun uncaughtException(thread: Thread, exception: Throwable) {
        try {
            val errorReport = generateErrorReport(formatException(thread, exception))
            val intent = Intent(context, ShowErrorActivity::class.java)
            intent.putExtra(EXTRA_ERROR_TEXT, errorReport)
            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
            context.startActivity(intent)
            // Pass exception to OS for graceful handling - OS will report it via ADB
            // and close all activities and services.
            defaultExceptionHandler.uncaughtException(thread, exception)
        } catch (fatalException: Exception) {
            // do not recurse into custom handler if exception is thrown during
            // exception handling. Pass this ultimate fatal exception to OS
            defaultExceptionHandler.uncaughtException(thread, fatalException)
        }
    }

    private fun formatException(thread: Thread, exception: Throwable): String {
        val stringBuilder = StringBuilder()
        stringBuilder.append(String.format("Exception in thread \"%s\": ", thread.name))

        // print available stacktrace
        val writer = StringWriter()
        exception.printStackTrace(PrintWriter(writer))
        stringBuilder.append(writer)
        return stringBuilder.toString()
    }

    private fun generateErrorReport(stackTrace: String): String {
        return """### App information
* ID: ${BuildConfig.APPLICATION_ID}
* Version: ${BuildConfig.VERSION_CODE} ${BuildConfig.VERSION_NAME}

### Device information
* Brand: ${Build.BRAND}
* Device: ${Build.DEVICE}
* Model: ${Build.MODEL}
* Id: ${Build.ID}
* Product: ${Build.PRODUCT}

### Firmware
* SDK: ${VERSION.SDK_INT}
* Release: ${VERSION.RELEASE}
* Incremental: ${VERSION.INCREMENTAL}

### Cause of error
<details>

```java
$stackTrace
```

</details>
"""
    }

}

fun attachExceptionHandler(fdroidBuildStatusApplication: FdroidBuildStatusApplication) {
    // handle crashes only outside the crash reporter activity/process
    if (!isCrashReportingProcess) {
        Thread.setDefaultUncaughtExceptionHandler(
            Thread.getDefaultUncaughtExceptionHandler()
                ?.let { ExceptionHandler(fdroidBuildStatusApplication, it) }
        )
    }
}

private val isCrashReportingProcess: Boolean
    get() {
        var processName: String? = ""
        if (VERSION.SDK_INT < Build.VERSION_CODES.P) {
            // Using the same technique as Application.getProcessName() for older devices
            // Using reflection since ActivityThread is an internal API
            try {
                @SuppressLint("PrivateApi") val activityThread =
                    Class.forName("android.app.ActivityThread")
                @SuppressLint("DiscouragedPrivateApi") val getProcessName =
                    activityThread.getDeclaredMethod("currentProcessName")
                processName = getProcessName.invoke(null) as String
            } catch (ignored: Exception) {
            }
        } else {
            processName = Application.getProcessName()
        }
        return (processName != null) && processName.endsWith(":crash")
    }
