import groovy.json.JsonSlurper
import java.nio.file.Paths

ext._dependencies = [:]
ext._manifest = null

ext.autolinkingInfo = { buildDir ->
    def autolinking = file("${buildDir}/generated/rnta/autolinking.json")
    return new JsonSlurper().parseText(autolinking.text)
}

ext.checkEnvironment = { rootDir ->
    // Keep this in sync with the JS version in `android/gradle-wrapper.js`
    //
    // We have two implementations because there are currently two ways to build
    // the Android app. If built via `@react-native-community/cli`, the JS
    // script will be run and we can change Gradle version before it is
    // executed. If it's built with Gradle directly, it's already too late and
    // the best we can do is to warn the user.
    def gradleVersions = [
      [v(0, 76, 0), [v(8, 9, 0), "8.9"], [Integer.MAX_VALUE, ""]],  //  0.76: [8.9, *)
      [v(0, 75, 0), [v(8, 8, 0), "8.8"], [v(8, 9, 0), "8.8"]],      //  0.75: [8.8, 8.9)
      [v(0, 74, 0), [v(8, 6, 0), "8.6"], [v(8, 9, 0), "8.8"]],      //  0.74: [8.6, 8.9)
      [v(0, 73, 0), [v(8, 3, 0), "8.3"], [v(8, 9, 0), "8.8"]],      //  0.73: [8.3, 8.9)
      [v(0, 72, 0), [v(8, 1, 1), "8.1.1"], [v(8, 3, 0), "8.2.1"]],  //  0.72: [8.1.1, 8.3)
      [0, [v(7, 5, 1), "7.6.4"], [v(8, 0, 0), "7.6.4"]],            // <0.72: [7.5.1, 8.0.0)
    ]

    def warnGradle = { gradleVersion, reactNativeVersion ->
        def message = [
            "\n",
            "Gradle {} is recommended for React Native {}.\n",
            "> Run the following command in the 'android' folder to install a supported version:\n",
            "   > ./gradlew wrapper --gradle-version={}\n",
            "> If the command above fails, you can manually modify 'gradle/wrapper/gradle-wrapper.properties'.",
        ].join("")
        logger.error(message, gradleVersion, reactNativeVersion, gradleVersion)
    }

    // Gradle version can be found in the template:
    // https://github.com/facebook/react-native/blob/main/packages/react-native/template/android/gradle/wrapper/gradle-wrapper.properties
    def gradleVersion = toVersionNumber(gradle.gradleVersion)

    def reactNativeVersionString = getPackageVersion("react-native", rootDir)
    def reactNativeVersion = toVersionNumber(reactNativeVersionString)

    for (gradleVersionRange in gradleVersions) {
        def (rnVersion, lower, upper) = gradleVersionRange
        if (reactNativeVersion >= rnVersion) {
            if (gradleVersion < lower[0]) {
                warnGradle(lower[1], reactNativeVersionString)
            } else if (gradleVersion >= upper[0]) {
                warnGradle(upper[1], reactNativeVersionString)
            }
            return
        }
    }
}

ext.findFile = { fileName ->
    def currentDirPath = rootDir == null ? null : rootDir.toString()

    while (currentDirPath != null) {
        def currentDir = file(currentDirPath)
        def requestedFile = Paths.get(currentDirPath, fileName).toFile()

        if (requestedFile.exists()) {
            return requestedFile
        }

        currentDirPath = currentDir.getParent()
    }

    return null
}

/**
 * Finds the path of the installed npm package with the given name using Node's
 * module resolution algorithm, which searches "node_modules" directories up to
 * the file system root. This handles various cases, including:
 *
 *   - Working in the open-source RN repo:
 *       Gradle: /path/to/react-native/ReactAndroid
 *       Node module: /path/to/react-native/node_modules/[package]
 *
 *   - Installing RN as a dependency of an app and searching for hoisted
 *     dependencies:
 *       Gradle: /path/to/app/node_modules/react-native/ReactAndroid
 *       Node module: /path/to/app/node_modules/[package]
 *
 *   - Working in a larger repo (e.g., Facebook) that contains RN:
 *       Gradle: /path/to/repo/path/to/react-native/ReactAndroid
 *       Node module: /path/to/repo/node_modules/[package]
 *
 * The search begins at the given base directory (a File object). The returned
 * path is a string.
 */
ext.findNodeModulesPath = { packageName, baseDir ->
    def module = _dependencies[packageName]
    if (module != null) {
        return module.path
    }

    def basePath = baseDir.toPath().normalize()

    // Node's module resolution algorithm searches up to the root directory,
    // after which the base path will be null
    while (basePath) {
        def candidatePath = Paths.get(basePath.toString(), "node_modules", packageName)
        if (candidatePath.toFile().exists()) {
            // Resolve the real path in case we're dealing with symlinks
            def resolvedPath = candidatePath.toRealPath().toString()
            _dependencies[packageName] = [path: resolvedPath]
            return resolvedPath
        }

        basePath = basePath.getParent()
    }

    return null
}

ext.getAppName = {
    def manifest = getManifest()
    if (manifest != null) {
        def displayName = manifest["displayName"]
        if (displayName instanceof String) {
            return displayName
        }

        def name = manifest["name"]
        if (name instanceof String) {
            return name
        }
    }

    return "ReactTestApp"
}

ext.getApplicationId = {
    def manifest = getManifest()
    if (manifest != null) {
        def config = manifest["android"]
        if (config instanceof Object && config.containsKey("package")) {
            return config["package"]
        }
    }

    return "com.microsoft.reacttestapp"
}

ext.getArchitectures = { project ->
    def archs = project.findProperty("react.nativeArchitectures")
                    ?: project.findProperty("reactNativeArchitectures")
    return archs != null
        ? archs.split(",")
        : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}

ext.getKotlinVersion = { baseDir ->
    def fallbackVersion = "1.7.21"

    def packagePath = findNodeModulesPath("react-native", baseDir)
    def versionCatalog = file("${packagePath}/gradle/libs.versions.toml")
    if (!versionCatalog.exists()) {
        return fallbackVersion
    }

    def m = versionCatalog.text =~ /kotlin = "([.0-9]+)"/
    def version = m.size() > 0 ? m[0][1] : fallbackVersion
    logger.info("Detected Kotlin version: ${version}")
    return version
}

ext.getManifest = {
    if (_manifest == null) {
        def manifestFile = findFile("app.json")
        if (manifestFile == null) {
            return null
        }

        _manifest = new JsonSlurper().parseText(manifestFile.text)
    }
    return _manifest
}

ext.getPackageManifest = { packageName, baseDir ->
    def module = _dependencies[packageName]
    if (module != null && module.manifest != null) {
        return module.manifest
    }

    def packagePath = findNodeModulesPath(packageName, baseDir)
    def packageJson = file("${packagePath}/package.json")
    def manifest = new JsonSlurper().parseText(packageJson.text)
    _dependencies[packageName].manifest = manifest
    return manifest
}

ext.getPackageVersion = { packageName, baseDir ->
    def manifest = getPackageManifest(packageName, baseDir)
    return manifest["version"]
}

ext.getPackageVersionNumber = { packageName, baseDir ->
    return toVersionNumber(getPackageVersion(packageName, baseDir))
}

ext.getSigningConfigs = {
    def safeSetMap = { varName, map, prop, defaultVal ->
        map[varName] = prop.containsKey(varName) ? prop.get(varName) : defaultVal
    }

    def definedConfigs = new LinkedHashMap<String, Object>()
    def manifestFile = findFile("app.json")
    if (manifestFile != null) {
        def manifest = new JsonSlurper().parseText(manifestFile.text)

        if (!manifest["android"]) {
            return definedConfigs
        }

        def signingConfigs = manifest["android"]["signingConfigs"]
        if (signingConfigs) {
            signingConfigs.each { config ->
                def configName = config.key
                def props = config.value
                def pathStoreFile = props.containsKey("storeFile")
                    ? Paths.get(manifestFile.getParent(), props.get("storeFile")).normalize().toAbsolutePath()
                    : null
                if (pathStoreFile == null || !file(pathStoreFile).exists() || !file(pathStoreFile).isFile()) {
                    throw new FileNotFoundException("Signing storeFile for flavor ${configName} is missing: " + pathStoreFile)
                }

                def signConfig = new LinkedHashMap<String, Object>()
                safeSetMap("keyAlias", signConfig, props, "androiddebugkey")
                safeSetMap("keyPassword", signConfig, props, "android")
                safeSetMap("storePassword", signConfig, props, "android")
                signConfig["storeFile"] = pathStoreFile.toFile()
                definedConfigs[configName] = signConfig
            }
        }
    }

    return definedConfigs
}

ext.getSingleAppMode = {
    def manifest = getManifest()
    if (manifest != null) {
        def singleApp = manifest["singleApp"]
        if (singleApp instanceof String) {
            return singleApp
        }
    }

    return false
}

ext.getVersionCode = {
    def manifest = getManifest()
    if (manifest != null) {
        def config = manifest["android"]
        if (config instanceof Object && config.containsKey("versionCode")) {
            return config["versionCode"]
        }
    }

    return 1
}

ext.getVersionName = {
    def manifest = getManifest()
    if (manifest != null) {
        def version = manifest["version"]
        if (version instanceof String) {
            return version
        }
    }

    return "1.0"
}

ext.isBridgelessEnabled = { project, isNewArchEnabled ->
    if (isNewArchEnabled) {
        def bridgelessEnabled = project.findProperty("react.bridgelessEnabled")
                                    ?: project.findProperty("bridgelessEnabled")
        if (bridgelessEnabled != "false") {
            def version = getPackageVersionNumber("react-native", project.rootDir)
            def isSupported = version == 0 || version >= v(0, 73, 0)

            if (bridgelessEnabled == "true") {
                if (!isSupported) {
                    logger.warn([
                        "WARNING: react-native 0.73 or greater is required for",
                        "Bridgeless Mode — disable `bridgelessEnabled` in your",
                        "`gradle.properties` or upgrade `react-native`"
                    ].join(" "))
                }
                return isSupported
            }

            // https://github.com/facebook/react-native/commit/fe337f25be65b67dc3d8d99d26a61ffd26985dd8
            def isEnabledByDefault = version == 0 || version >= v(0, 74, 0)
            return isSupported && isEnabledByDefault
        }
    }
    return false
}

ext.isFabricEnabled = { project ->
    return isNewArchitectureEnabled(project)
}

ext.isNewArchitectureEnabled = { project ->
    def newArchEnabled = project.findProperty("react.newArchEnabled")
                             ?: project.findProperty("newArchEnabled")
    if (newArchEnabled == "true") {
        def version = getPackageVersionNumber("react-native", project.rootDir)
        def isSupported = version == 0 || version >= v(0, 71, 0)
        if (!isSupported) {
            throw new GradleException([
                "react-native 0.71 or greater is required for New Architecture",
                "— disable `newArchEnabled` in your `gradle.properties` or",
                "upgrade `react-native`"
            ].join(" "))
        }
        return isSupported
    }
    return false
}

ext.toVersionNumber = { version ->
    def (major, minor, patch) = version.findAll(/\d+/)
    return v(major as int, minor as int, (patch ?: 0) as int)
}

ext.v = { major, minor, patch ->
    return (major * 1000000) + (minor * 1000) + patch
}
