import com.android.build.api.variant.impl.VariantOutputImpl
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Properties

val dnsjavaInetAddressResolverService = "META-INF/services/java.net.spi.InetAddressResolverProvider"
val openClawAndroidVersionFile = rootProject.file("Config/Version.properties")
val thirdPartyLicensesDir = rootProject.file("THIRD_PARTY_LICENSES")
val openClawAndroidVersionProperties =
  Properties().apply {
    if (!openClawAndroidVersionFile.isFile) {
      error("Missing Android version properties. Run `pnpm android:version:sync`.")
    }
    openClawAndroidVersionFile.inputStream().use(::load)
  }

fun requireOpenClawAndroidVersionProperty(name: String): String =
  openClawAndroidVersionProperties.getProperty(name)?.trim()?.takeIf { it.isNotEmpty() }
    ?: error("Missing $name in Config/Version.properties. Run `pnpm android:version:sync`.")

val openClawAndroidVersionName = requireOpenClawAndroidVersionProperty("OPENCLAW_ANDROID_VERSION_NAME")
val openClawAndroidVersionCode =
  requireOpenClawAndroidVersionProperty("OPENCLAW_ANDROID_VERSION_CODE").toIntOrNull()
    ?: error("OPENCLAW_ANDROID_VERSION_CODE must be an integer in Config/Version.properties.")

fun optionalOpenClawBuildProperty(name: String): String? =
  providers
    .gradleProperty(name)
    .orNull
    ?.trim()
    ?.takeIf { it.isNotEmpty() }

val fullGitCommitPattern = Regex("^[a-f0-9]{40}$")
val buildTimestampFormatter =
  DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneOffset.UTC)
val explicitOpenClawBuildCommit =
  optionalOpenClawBuildProperty("openclawBuildCommit")
    ?.lowercase()
    ?.also { commit ->
      if (!fullGitCommitPattern.matches(commit)) {
        error("openclawBuildCommit must be a full 40-character hexadecimal Git commit.")
      }
    }

val explicitOpenClawBuildTimestamp =
  optionalOpenClawBuildProperty("openclawBuildTimestamp")
    ?.let { timestamp ->
      if (!Regex("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,3})?Z$").matches(timestamp)) {
        error("openclawBuildTimestamp must be an ISO-8601 UTC timestamp.")
      }
      val instant =
        runCatching { Instant.parse(timestamp) }
          .getOrElse { error("openclawBuildTimestamp must be an ISO-8601 UTC timestamp.") }
      buildTimestampFormatter.format(instant)
    }

val repositoryBuildCommit =
  if (explicitOpenClawBuildCommit == null) {
    runCatching {
      providers
        .exec {
          workingDir(rootProject.projectDir)
          commandLine("git", "rev-parse", "HEAD")
        }.standardOutput
        .asText
        .get()
        .trim()
        .lowercase()
        .takeIf(fullGitCommitPattern::matches)
    }.getOrNull()
  } else {
    null
  }

val openClawBuildCommit = explicitOpenClawBuildCommit ?: repositoryBuildCommit ?: "unknown"
// Keep every variant generated by one Gradle invocation on the same build instant.
val invocationBuildTimestamp =
  providers.provider { buildTimestampFormatter.format(Instant.now()) }.get()
val openClawBuildTimestamp = explicitOpenClawBuildTimestamp ?: invocationBuildTimestamp

val androidStoreFile = providers.gradleProperty("OPENCLAW_ANDROID_STORE_FILE").orNull?.takeIf { it.isNotBlank() }
val androidStorePassword = providers.gradleProperty("OPENCLAW_ANDROID_STORE_PASSWORD").orNull?.takeIf { it.isNotBlank() }
val androidKeyAlias = providers.gradleProperty("OPENCLAW_ANDROID_KEY_ALIAS").orNull?.takeIf { it.isNotBlank() }
val androidKeyPassword = providers.gradleProperty("OPENCLAW_ANDROID_KEY_PASSWORD").orNull?.takeIf { it.isNotBlank() }
val resolvedAndroidStoreFile =
  androidStoreFile?.let { storeFilePath ->
    if (storeFilePath.startsWith("~/")) {
      "${System.getProperty("user.home")}/${storeFilePath.removePrefix("~/")}"
    } else {
      storeFilePath
    }
  }

val hasAndroidReleaseSigning =
  listOf(resolvedAndroidStoreFile, androidStorePassword, androidKeyAlias, androidKeyPassword).all { it != null }

val wantsAndroidReleaseBuild =
  gradle.startParameter.taskNames.any { taskName ->
    taskName.contains("Release", ignoreCase = true) ||
      Regex("""(^|:)(bundle|assemble)$""").containsMatchIn(taskName)
  }
val missingAndroidBuildMetadata =
  explicitOpenClawBuildCommit == null || explicitOpenClawBuildTimestamp == null

if (wantsAndroidReleaseBuild && !hasAndroidReleaseSigning) {
  error(
    "Missing Android release signing properties. Set OPENCLAW_ANDROID_STORE_FILE, " +
      "OPENCLAW_ANDROID_STORE_PASSWORD, OPENCLAW_ANDROID_KEY_ALIAS, and " +
      "OPENCLAW_ANDROID_KEY_PASSWORD in ~/.gradle/gradle.properties.",
  )
}

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

ksp {
  arg("room.schemaLocation", "$projectDir/schemas")
}

android {
  namespace = "ai.openclaw.app"
  // AndroidX Core 1.19 and Lifecycle 2.11 require API 37 compilation.
  // targetSdk stays separate so runtime behavior changes remain an explicit migration.
  compileSdk = 37

  // Release signing is local-only; keep the keystore path and passwords out of the repo.
  signingConfigs {
    if (hasAndroidReleaseSigning) {
      create("release") {
        storeFile = project.file(checkNotNull(resolvedAndroidStoreFile))
        storePassword = checkNotNull(androidStorePassword)
        keyAlias = checkNotNull(androidKeyAlias)
        keyPassword = checkNotNull(androidKeyPassword)
      }
    }
  }

  sourceSets {
    getByName("main") {
      assets.directories.add("../../shared/OpenClawKit/Sources/OpenClawKit/Resources")
      assets.directories.add(thirdPartyLicensesDir.path)
    }
  }

  defaultConfig {
    applicationId = "ai.openclaw.app"
    minSdk = 31
    targetSdk = 36
    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    versionCode = openClawAndroidVersionCode
    versionName = openClawAndroidVersionName
    buildConfigField("String", "GIT_COMMIT", "\"$openClawBuildCommit\"")
    buildConfigField("String", "BUILD_TIMESTAMP", "\"$openClawBuildTimestamp\"")
    ndk {
      // Support all major ABIs — native libs are tiny (~47 KB per ABI)
      abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
    }
  }

  flavorDimensions += "store"

  productFlavors {
    create("play") {
      dimension = "store"
      manifestPlaceholders["nodeForegroundServiceType"] = "connectedDevice|microphone"
    }
    create("thirdParty") {
      dimension = "store"
      manifestPlaceholders["nodeForegroundServiceType"] = "connectedDevice|microphone|location"
    }
  }

  buildTypes {
    release {
      if (hasAndroidReleaseSigning) {
        signingConfig = signingConfigs.getByName("release")
      }
      isMinifyEnabled = true
      isShrinkResources = true
      ndk {
        debugSymbolLevel = "SYMBOL_TABLE"
      }
      proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
    }
    debug {
      isMinifyEnabled = false
    }
  }

  bundle {
    language {
      // The in-app picker can select a locale outside the device language list.
      // Without a Play Core download path, every translated resource must stay installed.
      enableSplit = false
    }
  }

  buildFeatures {
    compose = true
    buildConfig = true
  }

  androidResources {
    generateLocaleConfig = true
    localeFilters +=
      listOf(
        "ar",
        "de",
        "en",
        "es",
        "fa",
        "fr",
        "hi",
        "in",
        "it",
        "ja",
        "ko",
        "nl",
        "pl",
        "pt-rBR",
        "ru",
        "sv",
        "th",
        "tr",
        "uk",
        "vi",
        "zh-rCN",
        "zh-rTW",
      )
  }

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

  packaging {
    resources {
      excludes +=
        setOf(
          "/META-INF/{AL2.0,LGPL2.1}",
          "/META-INF/*.version",
          "/META-INF/LICENSE*.txt",
          "DebugProbesKt.bin",
          "kotlin-tooling-metadata.json",
          "org/bouncycastle/pqc/crypto/picnic/lowmcL1.bin.properties",
          "org/bouncycastle/pqc/crypto/picnic/lowmcL3.bin.properties",
          "org/bouncycastle/pqc/crypto/picnic/lowmcL5.bin.properties",
          "org/bouncycastle/x509/CertPathReviewerMessages*.properties",
        )
    }
  }

  lint {
    lintConfig = file("lint.xml")
    warningsAsErrors = true
  }

  testOptions {
    unitTests.isIncludeAndroidResources = true
  }
}

androidComponents {
  onVariants { variant ->
    variant.outputs
      .filterIsInstance<VariantOutputImpl>()
      .forEach { output ->
        val versionName = output.versionName.orNull ?: "0"
        val buildType = variant.buildType
        val flavorName = variant.flavorName?.takeIf { it.isNotBlank() }
        val outputFileName =
          if (flavorName == null) {
            "openclaw-$versionName-$buildType.apk"
          } else {
            "openclaw-$versionName-$flavorName-$buildType.apk"
          }
        output.outputFileName = outputFileName
      }
  }
}
kotlin {
  compilerOptions {
    jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
    allWarningsAsErrors.set(true)
  }
}

ktlint {
  android.set(true)
  ignoreFailures.set(false)
  filter {
    exclude("**/build/**")
  }
}

dependencies {
  val composeBom = platform(libs.androidx.compose.bom)
  implementation(composeBom)
  androidTestImplementation(composeBom)

  implementation(project(":wear-shared"))
  implementation(libs.play.services.wearable)

  implementation(libs.androidx.core.ktx)
  // AppCompat owns per-app locale persistence and Activity recreation on API 31-32.
  implementation(libs.androidx.appcompat)
  implementation(libs.androidx.lifecycle.runtime.ktx)
  implementation(libs.androidx.activity.compose)
  implementation(libs.androidx.webkit)

  implementation(libs.androidx.compose.ui)
  implementation(libs.androidx.compose.ui.tooling.preview)
  implementation(libs.androidx.compose.material3)
  implementation(libs.androidx.compose.material3.adaptive.navigation.suite)
  // material-icons-extended pulled in full icon set (~20 MB DEX). Only ~18 icons used.
  // R8 will tree-shake unused icons when minify is enabled on release builds.
  implementation(libs.androidx.compose.material.icons.extended)

  debugImplementation(libs.androidx.compose.ui.tooling)

  // Material Components (XML theme + resources)
  implementation(libs.material)

  implementation(libs.kotlinx.coroutines.android)
  implementation(libs.kotlinx.serialization.json)

  implementation(libs.androidx.security.crypto)
  // Room owns separate disposable gateway cache and durable client-state databases.
  implementation(libs.androidx.room.runtime)
  ksp(libs.androidx.room.compiler)
  implementation(libs.androidx.exifinterface)
  implementation(libs.okhttp)
  implementation(libs.media3.datasource.okhttp)
  implementation(libs.media3.exoplayer)
  implementation(libs.media3.session)
  implementation(libs.media3.ui)
  implementation(libs.bcprov)
  implementation(libs.coil.compose)
  implementation(libs.coil.svg)
  implementation(libs.commonmark)
  implementation(libs.commonmark.ext.autolink)
  implementation(libs.commonmark.ext.gfm.strikethrough)
  implementation(libs.commonmark.ext.gfm.tables)
  implementation(libs.commonmark.ext.task.list.items)

  // CameraX (for node.invoke camera.* parity)
  implementation(libs.androidx.camera.core)
  implementation(libs.androidx.camera.camera2)
  implementation(libs.androidx.camera.lifecycle)
  implementation(libs.androidx.camera.view)
  implementation(libs.androidx.camera.video)
  implementation(libs.barcode.scanning)

  // Unicast DNS-SD (Wide-Area Bonjour) for tailnet discovery domains.
  implementation(libs.dnsjava)

  testImplementation(libs.junit)
  testImplementation(libs.kotlinx.coroutines.test)
  testImplementation(libs.kotest.runner.junit5)
  testImplementation(libs.kotest.assertions.core)
  testImplementation(libs.mockwebserver)
  testImplementation(libs.robolectric)
  testImplementation(libs.androidx.compose.ui.test.junit4)
  testRuntimeOnly(libs.junit.vintage.engine)

  androidTestImplementation(libs.androidx.test.ext.junit)
  androidTestImplementation(libs.androidx.test.runner)
  androidTestImplementation(libs.androidx.uiautomator)
}

tasks.withType<Test>().configureEach {
  useJUnitPlatform()
  testLogging {
    events("failed")
    exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
  }
}

val validateOpenClawReleaseBuildMetadata =
  tasks.register("validateOpenClawReleaseBuildMetadata") {
    doLast {
      if (missingAndroidBuildMetadata) {
        error(
          "Android release builds require -PopenclawBuildCommit and -PopenclawBuildTimestamp. " +
            "Use the repository Android release helper.",
        )
      }
    }
  }

val validateThirdPartyLicenseAssets =
  tasks.register("validateThirdPartyLicenseAssets") {
    inputs.dir(thirdPartyLicensesDir)
    doLast {
      if (!thirdPartyLicensesDir.isDirectory) {
        error("Missing Android third-party license directory: ${thirdPartyLicensesDir.relativeTo(rootProject.projectDir)}")
      }
      val invalidFiles =
        thirdPartyLicensesDir
          .walkTopDown()
          .filter { file -> file.isFile && file.extension.lowercase() != "txt" }
          .map { file -> file.relativeTo(thirdPartyLicensesDir).path }
          .toList()

      if (invalidFiles.isNotEmpty()) {
        error(
          "Android third-party license assets must be .txt files:\n" +
            invalidFiles.joinToString(separator = "\n") { path -> "- $path" },
        )
      }
    }
  }

tasks.matching { task -> task.name == "preBuild" }.configureEach {
  dependsOn(validateThirdPartyLicenseAssets)
}

androidComponents {
  onVariants(selector().withBuildType("release")) { variant ->
    val variantName = variant.name
    val variantNameCapitalized = variantName.replaceFirstChar(Char::titlecase)
    val preBuildTaskName = "pre${variantNameCapitalized}Build"
    val stripTaskName = "strip${variantNameCapitalized}DnsjavaServiceDescriptor"
    val mergeTaskName = "merge${variantNameCapitalized}JavaResource"
    val minifyTaskName = "minify${variantNameCapitalized}WithR8"
    val mergedJar =
      layout.buildDirectory.file(
        "intermediates/merged_java_res/$variantName/$mergeTaskName/base.jar",
      )

    tasks.matching { task -> task.name == preBuildTaskName }.configureEach {
      dependsOn(validateOpenClawReleaseBuildMetadata)
    }

    val stripTask =
      tasks.register(stripTaskName) {
        inputs.file(mergedJar)
        outputs.file(mergedJar)

        doLast {
          val jarFile = mergedJar.get().asFile
          if (!jarFile.exists()) {
            return@doLast
          }

          val unpackDir = temporaryDir.resolve("merged-java-res")
          delete(unpackDir)
          copy {
            from(zipTree(jarFile))
            into(unpackDir)
            exclude(dnsjavaInetAddressResolverService)
          }
          delete(jarFile)
          ant.invokeMethod(
            "zip",
            mapOf(
              "destfile" to jarFile.absolutePath,
              "basedir" to unpackDir.absolutePath,
            ),
          )
        }
      }

    tasks.matching { it.name == mergeTaskName }.configureEach {
      finalizedBy(stripTask)
    }
    tasks.matching { it.name == minifyTaskName }.configureEach {
      dependsOn(stripTask)
    }
  }
}
