import java.util.Properties

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("org.jetbrains.kotlin.plugin.compose")
    id("org.jetbrains.kotlin.plugin.serialization")
    id("androidx.baselineprofile")
}

// Set in gradle.properties (or ~/.gradle/gradle.properties, or -Pmemby.serverUrl=...).
// Blank means "no hardwired server": the setup screen asks the user for an address.
val embyServerUrl: String = (project.findProperty("memby.serverUrl") as String?).orEmpty().trim()

// The Memby gateway container. When set, the client talks to it instead of Emby and
// becomes a thin renderer; blank keeps the direct-to-Emby path above.
val membyGatewayUrl: String = (project.findProperty("memby.gatewayUrl") as String?).orEmpty().trim()

// Kept in BuildConfig so the TV can show the exact corresponding-source location and
// the complete legal documents offline. Deployments can override the public source URL
// without changing application code.
val membySourceUrl: String =
    (project.findProperty("memby.sourceUrl") as String?)
        ?.trim()
        ?.takeIf(String::isNotEmpty)
        ?: "https://g.sublogue.com/admin/memby"

fun buildConfigString(value: String): String =
    "\"" + value
        .replace("\\", "\\\\")
        .replace("\"", "\\\"")
        .replace("\r\n", "\\n")
        .replace("\n", "\\n") + "\""

// The About page's version history. Kept as one checked-in document rather than a Kotlin
// list so a release only edits CHANGELOG.md, and the TV shows the history offline.
val changelogText = rootProject.file("CHANGELOG.md").readText()
val gplLicenseText = rootProject.file("LICENSE").readText()
val projectNoticeText =
    rootProject.file("NOTICE").readText()
        .replace("https://g.sublogue.com/admin/memby", membySourceUrl)

// A release workflow can derive the app version from its Git tag without editing the
// source tree. Local builds keep using the checked-in default.
val defaultVersionName = "0.2.51"
val membyVersionName: String =
    (project.findProperty("memby.versionName") as String?)
        ?.trim()
        ?.takeIf { it.matches(Regex("""\d+\.\d+\.\d+""")) }
        ?: defaultVersionName
val membyVersionParts = membyVersionName.split('.').map(String::toInt)
val membyVersionCode =
    membyVersionParts[0] * 10_000 + membyVersionParts[1] * 100 + membyVersionParts[2]

/** Reads a key from local.properties, which is gitignored and holds machine secrets. */
val localProperties = Properties().apply {
    val file = rootProject.file("local.properties")
    if (file.exists()) {
        file.inputStream().use { load(it) }
    }
}

fun localProperty(key: String): String? =
    localProperties.getProperty(key)?.trim()?.takeIf { it.isNotEmpty() }

android {
    namespace = "com.ponzischeme89.memby"
    compileSdk = 35

    defaultConfig {
        // Matches the Kotlin package. Changed from com.mattcohen.embyclientsname at
        // v0.1.53: a new applicationId installs as a separate app, so that release
        // required uninstalling the old one and signing in again.
        applicationId = "com.ponzischeme89.memby"
        minSdk = 23
        targetSdk = 35
        // Derived from one version string so CI cannot publish a versionName/versionCode
        // pair that Android later refuses to install.
        versionCode = membyVersionCode
        versionName = membyVersionName

        // Every native library in this APK is packaged uncompressed (minSdk 23 means
        // extractNativeLibs=false), so an ABI nobody runs is dead weight carried through
        // every sideload. Android TV is ARM: the x86 slices existed only for the emulator,
        // which is not how this app is ever tested. Removing them is not a compatibility
        // decision to revisit — adding a native dependency is.
        ndk {
            abiFilters += listOf("arm64-v8a", "armeabi-v7a")
        }

        buildConfigField("String", "EMBY_SERVER_URL", "\"${embyServerUrl.replace("\"", "\\\"")}\"")
        buildConfigField("String", "MEMBY_GATEWAY_URL", "\"${membyGatewayUrl.replace("\"", "\\\"")}\"")
        buildConfigField("String", "SOURCE_CODE_URL", buildConfigString(membySourceUrl))
        buildConfigField("String", "GPL_LICENSE_TEXT", buildConfigString(gplLicenseText))
        buildConfigField("String", "PROJECT_NOTICE_TEXT", buildConfigString(projectNoticeText))
        buildConfigField("String", "CHANGELOG_TEXT", buildConfigString(changelogText))
    }

    // Release signing. Android identifies an app by (applicationId, signing key), so
    // every update must be signed with the SAME key — a different one is rejected as a
    // different app and forces users to uninstall first. Keep the keystore and these
    // credentials off the repo: set them in local.properties (gitignored) or the
    // environment. Without them, `assembleRelease` still builds but stays unsigned.
    val keystorePath = localProperty("memby.keystore") ?: System.getenv("MEMBY_KEYSTORE")
    val hasKeystore = !keystorePath.isNullOrBlank() && file(keystorePath).exists()

    signingConfigs {
        if (hasKeystore) {
            create("release") {
                storeFile = file(keystorePath!!)
                storePassword = localProperty("memby.keystorePassword") ?: System.getenv("MEMBY_KEYSTORE_PASSWORD")
                keyAlias = localProperty("memby.keyAlias") ?: System.getenv("MEMBY_KEY_ALIAS") ?: "memby"
                keyPassword = localProperty("memby.keyPassword") ?: System.getenv("MEMBY_KEY_PASSWORD")
            }
        }
    }

    buildTypes {
        release {
            // R8 is the single biggest cold-start lever on the weak TV boxes this ships
            // to: a smaller dex is less to load and verify before the first frame. The
            // keep rules in proguard-rules.pro are what stop it stripping the
            // kotlinx.serialization models the gateway contract depends on — if you add a
            // @Serializable package, add it there too.
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
            if (hasKeystore) {
                signingConfig = signingConfigs.getByName("release")
            } else {
                logger.warn(
                    "Memby: no release keystore configured (memby.keystore). " +
                        "assembleRelease will produce an UNSIGNED apk that cannot be installed. " +
                        "See README > Distributing builds.",
                )
            }
        }
    }

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

    buildFeatures {
        compose = true
        buildConfig = true
    }

    lint {
        // media3 marks some APIs (e.g. PlayerView) with a Lint-based @UnstableApi
        // opt-in check; don't let it fail the build.
        abortOnError = false
    }

    testOptions {
        unitTests {
            // Robolectric needs the merged resources to inflate anything; only the
            // screenshot tests use them.
            isIncludeAndroidResources = true

            // Roborazzi writes PNGs only in record mode. These images are artifacts to
            // look at, not checked-in goldens to diff against, so recording is always on
            // — a screenshot test that silently captures nothing is worse than none.
            all {
                it.systemProperty("roborazzi.test.record", "true")
            }
        }
    }

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

// Outside `android { }` on purpose: the Kotlin compiler's own settings belong to the Kotlin
// plugin, not to AGP's extension, which is why `kotlinOptions` inside it is deprecated.
// Keep in step with compileOptions above — a jvmTarget below the Java target fails the
// build the moment Kotlin has to read a class Java compiled.
kotlin {
    compilerOptions {
        jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
    }
}

dependencies {
    val composeBom = platform("androidx.compose:compose-bom:2024.12.01")
    implementation(composeBom)

    // Core / lifecycle / activity
    implementation("androidx.core:core-ktx:1.15.0")
    implementation("androidx.activity:activity-compose:1.9.3")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
    implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
    implementation("androidx.work:work-runtime-ktx:2.10.0")
    // ProcessLifecycleOwner: lets the status poll stop while no Memby screen is on top,
    // instead of hitting the gateway every ten seconds for as long as the process lives.
    implementation("androidx.lifecycle:lifecycle-process:2.8.7")
    implementation("androidx.savedstate:savedstate-ktx:1.2.1")
    // Measurement only: JankStats is enabled by PerformanceMonitor for debug builds.
    implementation("androidx.metrics:metrics-performance:1.0.0")
    // Installs the baseline profile below. Without it the profile is only honoured on
    // API 31+; a TV box on Android 9-11 — most of the installed base — would get nothing.
    implementation("androidx.profileinstaller:profileinstaller:1.4.1")
    // Names the playback launch phases in a systrace so :benchmark can measure them.
    // Free when tracing is off, which is every build a viewer ever runs.
    implementation("androidx.tracing:tracing-ktx:1.2.0")

    // Compose (versions from BOM)
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.foundation:foundation")
    implementation("androidx.compose.material:material-icons-extended")

    // Compose for TV
    implementation("androidx.tv:tv-material:1.0.0")

    // Image loading
    implementation("io.coil-kt:coil-compose:2.7.0")

    // Networking + JSON
    implementation("com.squareup.retrofit2:retrofit:2.11.0")
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
    implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")

    // DataStore (persisted settings)
    implementation("androidx.datastore:datastore-preferences:1.1.1")

    // Media3 / ExoPlayer for in-app playback
    implementation("androidx.media3:media3-exoplayer:1.5.1")
    implementation("androidx.media3:media3-exoplayer-hls:1.5.1")
    implementation("androidx.media3:media3-ui:1.5.1")
    // Lets the player pull its bytes through the app's one OkHttp stack instead of
    // media3's own HttpURLConnection client — see ui/player/PlayerEngine.kt.
    implementation("androidx.media3:media3-datasource-okhttp:1.5.1")

    // Moonfin's Android TV backend keeps a software audio renderer behind Media3 so a
    // surround track that is not bitstreamed is decoded to PCM instead of making the
    // server re-encode the video beside it. This build targets Media3 1.5.x; Jellyfin's
    // 1.5.0 extension is the matching published binary for that line.
    //
    // This is the ONLY native dependency the app carries, and it is 1.5MB per ABI because
    // it links just the audio decoders it needs. It replaced a libmpv software-decoding
    // fallback (io.github.abdallahmehiz:mpv-android-lib) that shipped a whole FFmpeg,
    // libc++ and a 6MB subtitle font — 155MB of native code across four ABIs, which took
    // the release APK from 3.1MB to 168MB and made every sideload a several-minute
    // affair. A last-resort video path is not worth fifty times the app. If one is wanted
    // again, it belongs behind a separately downloaded split, not in the base APK.
    implementation("org.jellyfin.media3:media3-ffmpeg-decoder:1.5.0+1")

    debugImplementation("androidx.compose.ui:ui-tooling")

    // Ahead-of-time compiles the startup + first-scroll path. Regenerate against a real
    // TV with `.\gradlew.bat :app:generateReleaseBaselineProfile`; the result is checked
    // in under app/src/release/generated/baselineProfiles so ordinary release builds
    // don't need a device.
    baselineProfile(project(":benchmark"))

    testImplementation("junit:junit:4.13.2")

    // Screenshot rendering only. Everything else under app/src/test stays plain JUnit
    // with no Android on the classpath — see the note in CLAUDE.md. Rendering a
    // composable is the one thing that genuinely cannot be done that way, and these are
    // confined to *ScreenshotTest.kt files.
    testImplementation("org.robolectric:robolectric:4.14.1")
    testImplementation("androidx.test.ext:junit:1.2.1")
    testImplementation("io.github.takahirom.roborazzi:roborazzi:1.32.2")
    testImplementation("io.github.takahirom.roborazzi:roborazzi-compose:1.32.2")
    testImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-test-manifest")
}
