@file:OptIn(gobley.gradle.InternalGobleyGradleApi::class)

import gobley.gradle.cargo.dsl.jvm
import gobley.gradle.cargo.tasks.CargoBuildTask
import gobley.gradle.cargo.tasks.CargoCheckTask
import gobley.gradle.GobleyHost
import gobley.gradle.rust.targets.RustAndroidTarget
import gobley.gradle.rust.targets.RustTarget
import gobley.gradle.Variant
import org.gradle.api.DefaultTask
import org.gradle.api.provider.ListProperty
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
import org.gradle.jvm.tasks.Jar
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties

abstract class VerifyHostCargoTaskSelection : DefaultTask() {
	@get:Input
	abstract val mismatches: ListProperty<String>

	@TaskAction
	fun verify() {
		check(mismatches.get().isEmpty()) {
			"Cargo tasks do not match the current host: ${mismatches.get().joinToString()}"
		}
	}
}

plugins {
	alias(libs.plugins.kotlinMultiplatform)
	alias(libs.plugins.androidLibrary)
	alias(libs.plugins.composeMultiplatform)
	alias(libs.plugins.composeCompiler)
	alias(libs.plugins.gobleyCargo)
	alias(libs.plugins.gobleyUniffi)
	alias(libs.plugins.kotlinAtomicfu)
}

val appVersion = rootProject.extra["vnidrop.productVersion"] as String
val desktopRustVariant = providers.gradleProperty("vnidrop.desktop.rustVariant")
	.map { value ->
		when (value.trim().lowercase()) {
			"debug" -> Variant.Debug
			"release" -> Variant.Release
			else -> error("vnidrop.desktop.rustVariant must be either debug or release")
		}
	}
	.orElse(Variant.Debug)

// Compile-time switches (gradle.properties or -P…).
// included=false: user-initiated bug reports use a NoOp transport (never sent).
// endpoint/key both empty: transport is NoOp (safe default until Cloudflare is deployed).
val diagnosticsIncluded: Boolean =
	(findProperty("vnidrop.diagnostics.included") as String?)?.toBooleanStrictOrNull() ?: false
val diagnosticsEndpoint: String =
	(findProperty("vnidrop.diagnostics.endpoint") as String?)?.trim().orEmpty()
val diagnosticsIngestKey: String =
	(findProperty("vnidrop.diagnostics.ingestKey") as String?)?.trim().orEmpty()
check(!diagnosticsIncluded || diagnosticsEndpoint.isEmpty() == diagnosticsIngestKey.isEmpty()) {
	"vnidrop.diagnostics.endpoint and vnidrop.diagnostics.ingestKey must be configured together"
}

val diagnosticsBuildConfigDir = layout.buildDirectory.dir("generated/diagnostics/commonMain/kotlin")
val generateDiagnosticsBuildConfig by tasks.registering {
	group = "build"
	description = "Generates DiagnosticsBuildConfig from vnidrop.diagnostics.* properties"
	val outputDir = diagnosticsBuildConfigDir
	val included = diagnosticsIncluded
	val endpoint = if (included) diagnosticsEndpoint else ""
	val ingestKey = if (included) diagnosticsIngestKey else ""
	inputs.property("vnidrop.diagnostics.included", included)
	inputs.property("vnidrop.diagnostics.endpoint", endpoint)
	inputs.property("vnidrop.diagnostics.ingestKey", ingestKey)
	outputs.dir(outputDir)
	doLast {
		val packageDir = outputDir.get().asFile.resolve("com/vnidrop/app/diagnostics")
		packageDir.mkdirs()
		fun esc(value: String): String = buildString {
			for (ch in value) {
				when (ch) {
					'\\' -> append("\\\\")
					'"' -> append("\\\"")
					'\n' -> append("\\n")
					'\r' -> append("\\r")
					'\t' -> append("\\t")
					'$' -> append("\\$")
					else -> append(ch)
				}
			}
		}
		packageDir.resolve("DiagnosticsBuildConfig.kt").writeText(
			"""
			|package com.vnidrop.app.diagnostics
			|
			|/**
			| * Generated by shared/build.gradle.kts from vnidrop.diagnostics.* properties.
			| *
			| * - included=false → no opt-in UI / telemetry / crash hooks
			| * - endpoint/key both empty → [NoOpDiagnosticsTransport] (no network)
			| */
			|object DiagnosticsBuildConfig {
			|	const val INCLUDED: Boolean = $included
			|	const val ENDPOINT: String = "${esc(endpoint)}"
			|	const val INGEST_KEY: String = "${esc(ingestKey)}"
			|}
			|
			""".trimMargin(),
		)
	}
}

// App-wide public constants (privacy policy URL, …) from the shared app.properties,
// so Apple and KMP read one source of truth instead of hardcoding values.
val appProperties = Properties().apply {
	rootProject.file("app.properties").inputStream().use(::load)
}
val privacyPolicyUrl: String = appProperties.getProperty("PRIVACY_POLICY_URL")?.trim().orEmpty()
check(privacyPolicyUrl.isNotEmpty()) { "PRIVACY_POLICY_URL must be set in app.properties" }

val appConfigDir = layout.buildDirectory.dir("generated/appconfig/commonMain/kotlin")
val generateAppConfig by tasks.registering {
	group = "build"
	description = "Generates AppConfig from the shared app.properties"
	val outputDir = appConfigDir
	val privacy = privacyPolicyUrl
	inputs.property("PRIVACY_POLICY_URL", privacy)
	outputs.dir(outputDir)
	doLast {
		val packageDir = outputDir.get().asFile.resolve("com/vnidrop/app")
		packageDir.mkdirs()
		fun esc(value: String): String = buildString {
			for (ch in value) {
				when (ch) {
					'\\' -> append("\\\\")
					'"' -> append("\\\"")
					'\n' -> append("\\n")
					'\r' -> append("\\r")
					'\t' -> append("\\t")
					'$' -> append("\\$")
					else -> append(ch)
				}
			}
		}
		packageDir.resolve("AppConfig.kt").writeText(
			"""
			|package com.vnidrop.app
			|
			|/**
			| * Generated by shared/build.gradle.kts from the shared app.properties.
			| * Single source of truth for app-wide public constants (also used by Apple).
			| */
			|object AppConfig {
			|	const val PRIVACY_POLICY_URL: String = "${esc(privacy)}"
			|}
			|
			""".trimMargin(),
		)
	}
}

kotlin {
	androidTarget {
		compilerOptions {
			jvmTarget = JvmTarget.JVM_11
		}
	}

	jvm()

	sourceSets {
		commonMain {
			kotlin.srcDir(files(diagnosticsBuildConfigDir).builtBy(generateDiagnosticsBuildConfig))
			kotlin.srcDir(files(appConfigDir).builtBy(generateAppConfig))
		}
		androidMain.dependencies {
			implementation(libs.androidx.activity.compose)
			implementation(libs.androidx.core.ktx)
			implementation(libs.google.code.scanner)
			implementation(libs.compose.uiToolingPreview)
		}
		jvmMain.dependencies {
			implementation(libs.filekit.dialogs)
			implementation(libs.jna.platform)
		}
		commonMain.dependencies {
			implementation(libs.compose.runtime)
			implementation(libs.compose.foundation)
			implementation(libs.compose.material3)
			implementation(libs.compose.ui)
			implementation(libs.compose.components.resources)
			implementation(libs.compose.uiToolingPreview)
			implementation(libs.androidx.lifecycle.viewmodelCompose)
			implementation(libs.androidx.lifecycle.runtimeCompose)
			implementation(libs.androidx.datastore)
			implementation(libs.androidx.datastore.preferences)
			implementation(libs.kotlinx.coroutinesCore)
			implementation(libs.qrcode.kotlin)
		}
		commonTest.dependencies {
			implementation(libs.kotlin.test)
			implementation(libs.kotlinx.coroutinesTest)
		}
		jvmTest.dependencies {
			implementation(compose.desktop.currentOs)
			implementation(libs.compose.uiTest)
		}
	}
}

dependencies {
	debugImplementation(libs.compose.uiTooling)
}

android {
	namespace = "com.vnidrop.app.shared"
	compileSdk = libs.versions.android.compileSdk.get().toInt()

	defaultConfig {
		minSdk = libs.versions.android.minSdk.get().toInt()
	}

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

val hostCargoTargets = buildSet<RustTarget> {
	add(GobleyHost.current.rustTarget)
	addAll(RustAndroidTarget.entries)
}

cargo {
	packageDirectory = layout.projectDirectory.dir("../crates/vnidrop")
	publishJvmArtifacts = true
	jvmVariant.set(desktopRustVariant)
	androidTargetsToBuild.set(setOf(RustAndroidTarget.Arm64, RustAndroidTarget.X64))
	builds.jvm {
		variants {
			// Desktop distributions are built per host. Do not publish disabled
			// cross-platform native jars into the app runtime classpath.
			embedRustLibrary.set(rustTarget == GobleyHost.current.rustTarget)
		}
	}
	builds.configureEach {
		val buildOnCurrentHost = rustTarget in hostCargoTargets
		installTargetBeforeBuild.set(buildOnCurrentHost)
		variants {
			buildTaskProvider.configure {
				enabled = buildOnCurrentHost
			}
			checkTaskProvider.configure {
				enabled = buildOnCurrentHost
			}
		}
	}
}

uniffi {
	generateFromLibrary {
		namespace = "vnidrop"
		build.set(GobleyHost.current.rustTarget)
		variant.set(desktopRustVariant)
	}
}

tasks.named<Jar>("jvmJar") {
	manifest.attributes["Implementation-Version"] = appVersion
}

val verifyHostCargoTaskSelection = tasks.register<VerifyHostCargoTaskSelection>(
	"verifyHostCargoTaskSelection"
) {
	group = "verification"
	description = "Verifies that Cargo tasks are enabled only for targets supported by this host."
	mismatches.convention(emptyList())
}

afterEvaluate {
	verifyHostCargoTaskSelection.configure {
		mismatches.set(buildList {
			tasks.withType<CargoBuildTask>().forEach {
				if (it.enabled != (it.target.get() in hostCargoTargets)) add(it.path)
			}
			tasks.withType<CargoCheckTask>().forEach {
				if (it.enabled != (it.target.get() in hostCargoTargets)) add(it.path)
			}
		})
	}
}

tasks.named("check") {
	dependsOn(verifyHostCargoTaskSelection)
}

tasks.configureEach {
	// Gobley does not currently treat every Rust source/API change as an input of
	// all platform cargo tasks. Without these inputs an incremental Android build
	// can package an older .so next to freshly generated UniFFI Kotlin bindings.
	if (name.startsWith("cargoBuild")) {
		inputs.files(
			fileTree(layout.projectDirectory.dir("../crates/vnidrop")) {
				include("Cargo.toml", "build.rs", "src/**/*.rs")
			},
			layout.projectDirectory.file("../Cargo.toml"),
			layout.projectDirectory.file("../Cargo.lock"),
		).withPathSensitivity(PathSensitivity.RELATIVE)
	}
}
