// SPDX-License-Identifier: GPL-3.0-or-later

import java.io.FileInputStream
import java.util.Properties

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
    alias(libs.plugins.kotlin.serialization)
    alias(libs.plugins.ksp)
    alias(libs.plugins.hilt)
    jacoco
}

jacoco {
    toolVersion = "0.8.12"
}

// Release signing material (PLAN.md §12) is never committed. It is read from, in
// order of precedence: a local gitignored `keystore.properties` at the repo root,
// then environment variables (how CI injects the decoded keystore + secrets). When
// none is present, the release build is simply left unsigned — `assembleDebug` and
// the PR gate are unaffected, so contributors without the keystore can still build.
val keystorePropsFile = rootProject.file("keystore.properties")
val keystoreProps = Properties().apply {
    if (keystorePropsFile.exists()) FileInputStream(keystorePropsFile).use { load(it) }
}
fun signingValue(propKey: String, envKey: String): String? =
    keystoreProps.getProperty(propKey) ?: System.getenv(envKey)

val ksStoreFilePath = signingValue("storeFile", "ANDROID_KEYSTORE_FILE")
val ksStorePassword = signingValue("storePassword", "ANDROID_KEYSTORE_PASSWORD")
val ksKeyAlias = signingValue("keyAlias", "ANDROID_KEY_ALIAS")
val ksKeyPassword = signingValue("keyPassword", "ANDROID_KEY_PASSWORD")
val hasReleaseSigning = ksStoreFilePath != null && ksStorePassword != null &&
    ksKeyAlias != null && ksKeyPassword != null

android {
    namespace = "com.runicgateway.app"
    compileSdk = 35

    defaultConfig {
        // Target application id per docs/android/PLAN.md §13 (pending runicgateway.app domain).
        applicationId = "com.runicgateway.app"
        minSdk = 29
        targetSdk = 35
        // These committed defaults are the version source of truth (PLAN.md §10).
        // release.yml's conventional-commit engine bumps versionName here and commits
        // it on release; versionCode is derived from it (major*10000+minor*100+patch)
        // so it stays monotonic. Both remain overridable via -P for local/manual builds.
        versionCode = (project.findProperty("versionCode") as String?)?.toIntOrNull() ?: 1
        versionName = (project.findProperty("versionName") as String?)?.takeIf { it.isNotBlank() } ?: "0.1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

        // Android App Links host (docs/android/APP_LINKS.md). autoVerify needs a
        // *literal* host at build time, so a single multi-tenant APK cannot verify
        // open-ended shard domains: App Links are a build-time opt-in. Left empty for
        // the generic build (custom scheme only); a white-label/first-party build
        // bakes one host with `-PappLinkHost=play.myshard.com`.
        //   • BuildConfig.APP_LINK_HOST — SsoAuthManager reads it to pick the redirect.
        //   • manifestPlaceholder appLinkHost — substituted into the intent-filter host;
        //     empty falls back to the reserved `.invalid` sentinel so the autoVerify
        //     filter is inert (matches no real link, never verifies).
        val appLinkHost = (project.findProperty("appLinkHost") as String?)?.trim().orEmpty()
        buildConfigField("String", "APP_LINK_HOST", "\"$appLinkHost\"")
        manifestPlaceholders["appLinkHost"] = appLinkHost.ifBlank { "runic-gateway.invalid" }
    }

    signingConfigs {
        if (hasReleaseSigning) {
            create("release") {
                storeFile = file(ksStoreFilePath!!)
                storePassword = ksStorePassword
                keyAlias = ksKeyAlias
                keyPassword = ksKeyPassword
                // Sign with both v1 (JAR) and v2 (APK) schemes for broad compatibility.
                enableV1Signing = true
                enableV2Signing = true
            }
        }
    }

    buildTypes {
        debug {
            // Produce a JaCoCo .exec from JVM unit tests so SonarQube receives real
            // coverage (§12.1). Debug-only: the scan analyses the debug variant.
            enableUnitTestCoverage = true
        }
        release {
            // R8 full-mode minify + resource shrink (§7: no offline cache, so a lean
            // release APK). Keep rules live in proguard-rules.pro.
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro",
            )
            // Signed only when the keystore material is present (local or CI); an
            // unsigned APK is produced otherwise. The direct-APK release (§10) runs
            // through release.yml, which supplies the keystore from a Gitea secret.
            if (hasReleaseSigning) {
                signingConfig = signingConfigs.getByName("release")
            }
        }
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    kotlinOptions {
        jvmTarget = "17"
    }

    buildFeatures {
        compose = true
        buildConfig = true
    }

    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
}

dependencies {
    // Core / lifecycle / activity
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.androidx.lifecycle.runtime.compose)
    implementation(libs.androidx.lifecycle.viewmodel.compose)
    implementation(libs.androidx.activity.compose)

    // Compose (BOM-managed versions)
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.compose.ui)
    implementation(libs.androidx.compose.ui.graphics)
    implementation(libs.androidx.compose.ui.tooling.preview)
    implementation(libs.androidx.compose.material3)
    implementation(libs.androidx.compose.material.icons.core)
    implementation(libs.androidx.navigation.compose)
    debugImplementation(libs.androidx.compose.ui.tooling)
    debugImplementation(libs.androidx.compose.ui.test.manifest)

    // DI
    implementation(libs.hilt.android)
    ksp(libs.hilt.compiler)
    implementation(libs.androidx.hilt.navigation.compose)

    // Networking (wired for M1+; declared now so the stack resolves)
    implementation(libs.retrofit)
    implementation(platform(libs.okhttp.bom))
    implementation(libs.okhttp)
    implementation(libs.okhttp.logging.interceptor)
    implementation(libs.okhttp.sse)
    implementation(libs.kotlinx.serialization.json)
    implementation(libs.retrofit.kotlinx.serialization.converter)

    // Storage
    implementation(libs.androidx.datastore.preferences)
    implementation(libs.androidx.security.crypto)

    // Web hand-off (Chrome Custom Tabs) for register / invite / reset / SSO (§4.2)
    implementation(libs.androidx.browser)

    // Images
    implementation(libs.coil.compose)

    // Unit tests
    testImplementation(libs.junit)
    testImplementation(libs.kotlinx.coroutines.test)

    // Instrumented tests
    androidTestImplementation(libs.androidx.test.ext.junit)
    androidTestImplementation(libs.androidx.espresso.core)
    androidTestImplementation(platform(libs.androidx.compose.bom))
    androidTestImplementation(libs.androidx.compose.ui.test.junit4)
}

// JaCoCo XML coverage from the JVM unit tests, consumed by SonarQube (§12.1). Generated,
// DI (Hilt), and Compose-scaffold classes are excluded so they don't dilute the number;
// pure-@Composable UI is excluded on the Sonar side (sonar.coverage.exclusions) because
// JVM unit tests can't execute composable bodies without Robolectric.
tasks.register<JacocoReport>("jacocoTestReport") {
    dependsOn("testDebugUnitTest")
    group = "verification"
    description = "Generates JaCoCo XML/HTML coverage for the debug unit tests."

    reports {
        xml.required.set(true)
        html.required.set(true)
    }

    val coverageExcludes = listOf(
        "**/R.class", "**/R$*.class", "**/BuildConfig.*", "**/Manifest*.*",
        "**/*_Hilt*.*", "**/Hilt_*.*", "**/*_Factory*.*", "**/*_MembersInjector*.*",
        "**/*_Impl*.*", "**/di/**", "**/*Module.*", "**/*Module$*.*",
        "**/*ComposableSingletons*.*", "**/ComposableSingletons$*.*",
    )
    val buildDirFile = layout.buildDirectory.get().asFile
    classDirectories.setFrom(
        fileTree("$buildDirFile/tmp/kotlin-classes/debug") { exclude(coverageExcludes) },
    )
    sourceDirectories.setFrom(files("src/main/java", "src/main/kotlin"))
    executionData.setFrom(
        fileTree(buildDirFile) {
            include("outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec")
        },
    )
}
