import javax.inject.Inject
import org.gradle.process.ExecOperations

abstract class BundleDropNativeIdentityCommandExecutor {
  @Inject
  abstract ExecOperations getExecOperations()

  void execute(File workingDirectory, String executable, List<?> arguments) {
    execOperations.exec { spec ->
      spec.workingDir workingDirectory
      spec.executable executable
      spec.args arguments
    }.assertNormalExitValue()
  }
}

buildscript {
  ext.getExtOrDefault = {name ->
    return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['BundleDrop_' + name]
  }

  repositories {
    google()
    mavenCentral()
  }

  dependencies {
    classpath "com.android.tools.build:gradle:8.7.2"
    // noinspection DifferentKotlinGradleVersion
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
  }
}


apply plugin: "com.android.library"
// AGP 9 ships built-in Kotlin support and registers the `kotlin` extension
// itself. Applying the Kotlin plugin on top of it fails configuration with
// "Cannot add extension with name 'kotlin'". Only apply it when nothing has
// registered that extension yet.
if (project.extensions.findByName('kotlin') == null) {
    apply plugin: "kotlin-android"
}

if (project.hasProperty("standalone")) {
  apply plugin: "jacoco"
}

def getExtOrIntegerDefault(name) {
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["BundleDrop_" + name]).toInteger()
}

def supportsNamespace() {
  def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
  def major = parsed[0].toInteger()
  def minor = parsed[1].toInteger()

  // Namespace support was added in 7.3.0
  return (major == 7 && minor >= 3) || major >= 8
}

android {
  if (supportsNamespace()) {
    namespace "com.bundledrop"

    sourceSets {
      main {
        manifest.srcFile "src/main/AndroidManifestNew.xml"
      }
    }
  }

  compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")

  defaultConfig {
    minSdkVersion getExtOrIntegerDefault("minSdkVersion")
    targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

    externalNativeBuild {
      cmake {
        arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
      }
    }
  }

  buildTypes {
    release {
      minifyEnabled false
    }
  }

  lintOptions {
    disable "GradleCompatible"
  }

  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }

  externalNativeBuild {
    cmake {
      path "src/main/cpp/CMakeLists.txt"
      version "3.22.1"
    }
  }
}

if (!project.hasProperty("standalone") && rootProject.findProject(":bundledrop-expo") == null) {
  def bundleDropProjectRoot = rootProject.projectDir.parentFile.canonicalFile
  def bundleDropConfigFile = new File(bundleDropProjectRoot, "bundle.drop.config.js")
  if (bundleDropConfigFile.isFile()) {
    def bundleDropIdentityWriter = file(
      "../lib/CLI/scripts/native/write-runtime-identity.js"
    ).canonicalFile
    if (!bundleDropIdentityWriter.isFile()) {
      throw new GradleException(
        "Bundle Drop native runtime identity writer is missing: ${bundleDropIdentityWriter}"
      )
    }
    def bundleDropGeneratedAssets = file("${buildDir}/generated/bundleDropIdentity/assets")
    def bundleDropIdentityOutput = new File(
      bundleDropGeneratedAssets,
      "bundle-drop/build-identity.json"
    )
    android.sourceSets.main.assets.srcDir(bundleDropGeneratedAssets)
    def bundleDropCommandExecutor = objects.newInstance(
      BundleDropNativeIdentityCommandExecutor
    )
    def generateBundleDropNativeRuntimeIdentity = tasks.register(
      "generateBundleDropNativeRuntimeIdentity"
    ) { task ->
      task.inputs.file(bundleDropConfigFile)
      task.inputs.file(bundleDropIdentityWriter)
      task.outputs.file(bundleDropIdentityOutput)
      task.doLast {
        def nodeBinary = System.getenv("NODE_BINARY") ?: "node"
        bundleDropCommandExecutor.execute(bundleDropProjectRoot, nodeBinary, [
          bundleDropIdentityWriter.path,
          "--project-root", bundleDropProjectRoot.path,
          "--platform", "android",
          "--output", bundleDropIdentityOutput.path
        ])
      }
    }
    tasks.named("preBuild").configure { task ->
      task.dependsOn(generateBundleDropNativeRuntimeIdentity)
    }
  }
}

afterEvaluate {
  tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinTask ->
    def javaTask = tasks.findByName(kotlinTask.name.replace("Kotlin", "JavaWithJavac"))
    if (javaTask != null && javaTask.hasProperty("targetCompatibility")) {
      kotlinTask.kotlinOptions.jvmTarget = javaTask.targetCompatibility.toString()
    }
  }
}

repositories {
  mavenCentral()
  google()
}

if (project.hasProperty("standalone")) {
  // The standalone unit-test build resolves "react-android:+" to exercise the module
  // against the newest React Native. Gradle's "+" also matches pre-releases, and a
  // pre-release react-android (e.g. 0.87.0-rc.1) can ship newer Kotlin metadata than
  // the standalone Kotlin compiler (BundleDrop_kotlinVersion) can read, which breaks
  // compileDebugKotlin. Reject pre-release candidates so "+" keeps meaning "latest
  // stable" without pinning a specific version.
  configurations.all {
    resolutionStrategy.componentSelection.all { selection ->
      if (selection.candidate.group == "com.facebook.react"
          && selection.candidate.module == "react-android"
          && selection.candidate.version ==~ /(?i).*[-.](rc|alpha|beta|snapshot|dev|preview|m)\d*.*/) {
        selection.reject("pre-release React Native is unsupported by the pinned standalone Kotlin toolchain")
      }
    }
  }
}

def kotlin_version = getExtOrDefault("kotlinVersion")

dependencies {
  if (project.hasProperty("standalone")) {
    compileOnly "com.facebook.react:react-android:+"
  } else {
    implementation "com.facebook.react:react-android"
  }
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"

  if (project.hasProperty("standalone")) {
    testImplementation "junit:junit:4.13.2"
    testImplementation "com.squareup.okhttp3:okhttp:4.12.0"
    testImplementation "org.json:json:20231013"
    testImplementation "androidx.test:core:1.6.1"
    testImplementation "org.robolectric:robolectric:4.14.1"
    androidTestImplementation "junit:junit:4.13.2"
    androidTestImplementation "androidx.test:core:1.6.1"
    androidTestImplementation "androidx.test:runner:1.6.2"
    androidTestImplementation "androidx.test.ext:junit:1.2.1"
  }
}

// ---------------------------------------------------------------------------
// JaCoCo coverage for host-side unit tests (standalone only)
// ---------------------------------------------------------------------------

if (project.hasProperty("standalone")) {
  android.testOptions.unitTests.all {
    jacoco {
      includeNoLocationClasses = true
      excludes = ["jdk.internal.*"]
    }
  }

  def excludedFromCoverage = ["**/BundleDropModule*", "**/BundleDropPackage*"]

  tasks.register("jacocoTestReport", JacocoReport) {
    dependsOn "testDebugUnitTest"
    group = "verification"
    description = "Generate JaCoCo coverage report for debug unit tests"

    def mainSrc = "${project.projectDir}/src/main/java"
    def debugTree = fileTree(
      dir: "${buildDir}/tmp/kotlin-classes/debug",
      excludes: excludedFromCoverage
    )
    def execData = file("${buildDir}/jacoco/testDebugUnitTest.exec")

    sourceDirectories.setFrom(files(mainSrc))
    classDirectories.setFrom(debugTree)
    executionData.setFrom(files(execData))

    reports {
      html.required = true
      xml.required = true
    }
  }

  tasks.register("jacocoCoverageGate", JacocoCoverageVerification) {
    dependsOn "jacocoTestReport"
    group = "verification"
    description = "Enforce coverage thresholds on testable native classes"

    def debugTree = fileTree(
      dir: "${buildDir}/tmp/kotlin-classes/debug",
      excludes: excludedFromCoverage
    )
    def execData = file("${buildDir}/jacoco/testDebugUnitTest.exec")

    classDirectories.setFrom(debugTree)
    executionData.setFrom(files(execData))

    violationRules {
      rule {
        element = "CLASS"
        includes = ["com.bundledrop.BundleDropOtaResolver"]
        limit { counter = "LINE";   minimum = 0.90 }
        limit { counter = "BRANCH"; minimum = 0.85 }
      }
      rule {
        element = "CLASS"
        includes = ["com.bundledrop.BundleDropStartupRecoveryController"]
        limit { counter = "LINE";   minimum = 0.90 }
        limit { counter = "BRANCH"; minimum = 0.70 }
      }
      rule {
        element = "CLASS"
        includes = ["com.bundledrop.BundleDropFileOps"]
        limit { counter = "LINE";   minimum = 0.55 }
        limit { counter = "BRANCH"; minimum = 0.55 }
      }
    }
  }
}
