pipeline {
    agent {
        node {
            label '构建机1'
            customWorkspace '/home/nanxunai/goujian/workspace/jizhang-android-release'
        }
    }

    parameters {
        string(name: 'API_BASE_URL', defaultValue: 'http://192.168.5.8:5000', description: '内测后端 HTTP(S) 地址；Webhook 自动构建使用默认局域网地址')
    }

    options {
        timestamps()
        disableConcurrentBuilds()
        skipDefaultCheckout(true)
        timeout(time: 180, unit: 'MINUTES')
        buildDiscarder(logRotator(daysToKeepStr: '30', numToKeepStr: '20'))
    }

    environment {
        PATH = '/home/nanxunai/.local/bin:/home/nanxunai/flutter/bin:/home/nanxunai/Android/Sdk/platform-tools:/home/nanxunai/Android/Sdk/cmdline-tools/latest/bin:/usr/local/bin:/usr/bin:/bin'
        JAVA_HOME = '/home/nanxunai/.local/lib/jvm/zulu-21'
        ANDROID_HOME = '/home/nanxunai/Android/Sdk'
        ANDROID_SDK_ROOT = '/home/nanxunai/Android/Sdk'
        FLUTTER_ROOT = '/home/nanxunai/flutter'
        GIT_EXEC_PATH = '/home/nanxunai/.local/usr/lib/git-core'
        LD_LIBRARY_PATH = '/home/nanxunai/.local/usr/lib/x86_64-linux-gnu'
        PUB_HOSTED_URL = 'https://pub.flutter-io.cn'
        FLUTTER_STORAGE_BASE_URL = 'https://storage.flutter-io.cn'
        HTTP_PROXY = 'http://192.168.5.200:7890'
        HTTPS_PROXY = 'http://192.168.5.200:7890'
        http_proxy = 'http://192.168.5.200:7890'
        https_proxy = 'http://192.168.5.200:7890'
        NO_PROXY = '127.0.0.1,localhost,192.168.5.8,.nxsir.cn,pub.flutter-io.cn,storage.flutter-io.cn'
        no_proxy = '127.0.0.1,localhost,192.168.5.8,.nxsir.cn,pub.flutter-io.cn,storage.flutter-io.cn'
        // Keep dependency caches outside cleanWs. The build node already owns
        // these standard user caches, while concurrent builds are disabled.
        GRADLE_USER_HOME = '/home/nanxunai/.gradle'
        PUB_CACHE = '/home/nanxunai/.pub-cache'
        OPENLIST_CREDENTIALS = 'openlist_key'
        OPENLIST_BASE_URL = 'https://openlist.nxsir.cn'
        OPENLIST_REMOTE_DIR = '/yidongpan/构建产物/jizhang'
    }

    stages {
        stage('Checkout') {
            steps {
                deleteDir()
                checkout scm
            }
        }

        stage('Metadata And Preflight') {
            steps {
                script {
                    env.PUBSPEC_VERSION = sh(script: "sed -n 's/^version:[[:space:]]*//p' frontend/pubspec.yaml | head -n 1", returnStdout: true).trim()
                    def versionParts = env.PUBSPEC_VERSION.tokenize('+')
                    if (versionParts.size() != 2) { error("Invalid pubspec version: ${env.PUBSPEC_VERSION}") }
                    env.APP_VERSION = versionParts[0]
                    env.BASE_VERSION_CODE = versionParts[1]
                    env.FLUTTER_BUILD_NUMBER = "${versionParts[1].toInteger() + env.BUILD_NUMBER.toInteger()}"
                    // Flutter's split-per-abi Gradle configuration assigns arm64-v8a
                    // the 2xxx versionCode range while preserving the build-number suffix.
                    env.ANDROID_VERSION_CODE = "${env.FLUTTER_BUILD_NUMBER.toInteger() + 2000}"
                    env.SHORT_SHA = sh(script: 'git rev-parse --short=8 HEAD', returnStdout: true).trim()
                    env.IMMUTABLE_TAG = "${env.APP_VERSION}-internal-b${env.BUILD_NUMBER}-${env.SHORT_SHA}"
                    env.APK_BASENAME = "JiZhi-${env.IMMUTABLE_TAG}-vc${env.ANDROID_VERSION_CODE}-arm64-v8a.apk"
                    env.APK_PATH = "${env.WORKSPACE}/artifacts/${env.APK_BASENAME}"
                    env.MANIFEST_PATH = "${env.WORKSPACE}/artifacts/JiZhi-${env.IMMUTABLE_TAG}-build-manifest.json"
                    currentBuild.displayName = "#${env.BUILD_NUMBER} ${env.IMMUTABLE_TAG}"
                    currentBuild.description = "versionCode ${env.ANDROID_VERSION_CODE} · ${params.API_BASE_URL}"
                }
                sh '''
                    set -euo pipefail
                    test "$(uname -m)" = x86_64
                    for command_name in git java curl python3 sha256sum; do command -v "$command_name" >/dev/null; done
                    test -x "$FLUTTER_ROOT/bin/flutter"
                    test -x "$JAVA_HOME/bin/jar"
                    test -x "$ANDROID_HOME/build-tools/36.1.0/apksigner"
                    test -x "$ANDROID_HOME/build-tools/36.1.0/aapt2"
                    python3 - "$API_BASE_URL" <<'PY'
import sys, urllib.parse
value = sys.argv[1].strip()
parsed = urllib.parse.urlparse(value)
if parsed.scheme not in {'http','https'} or not parsed.hostname or parsed.username or parsed.password:
    raise SystemExit(f'invalid API_BASE_URL: {value}')
PY
                    mkdir -p "$GRADLE_USER_HOME" "$PUB_CACHE" artifacts
                    available_kb=$(df -Pk "$WORKSPACE" | awk 'NR == 2 { print $4 }')
                    test "$available_kb" -ge 4194304
                '''
            }
        }

        stage('Analyze And Test') {
            steps {
                dir('frontend') {
                    sh '''
                        set -euo pipefail
                        "$FLUTTER_ROOT/bin/flutter" config --no-analytics
                        "$FLUTTER_ROOT/bin/flutter" pub get
                    '''
                    sh '''
                        set -euo pipefail
                        # sqlite3 native hooks use a workspace-local cache, which cleanWs
                        # removes. Prime it from a verified node-local cache so a transient
                        # GitHub connection failure cannot block tests or the Gradle build.
                        sqlite_package_dir=$(python3 - <<'PY'
import json, pathlib, urllib.parse
config = json.loads(pathlib.Path('.dart_tool/package_config.json').read_text(encoding='utf-8'))
package = next(item for item in config['packages'] if item['name'] == 'sqlite3')
uri = urllib.parse.urlparse(package['rootUri'])
if uri.scheme != 'file':
    raise SystemExit(f"unexpected sqlite3 package URI: {package['rootUri']}")
print(pathlib.Path(urllib.parse.unquote(uri.path)).resolve())
PY
                        )
                        sqlite_release=$(basename "$sqlite_package_dir")

                        prime_sqlcipher() {
                            sqlite_asset="$1"
                            sqlite_hash=$(sed -n "s/.*'$sqlite_asset': '\\([0-9a-f]\\{64\\}\\)'.*/\\1/p" \
                              "$sqlite_package_dir/lib/src/hook/asset_hashes.dart")
                            test "${#sqlite_hash}" -eq 64

                            sqlite_cache_dir="/home/nanxunai/.cache/jizhang/sqlite3/$sqlite_hash"
                            sqlite_cache_file="$sqlite_cache_dir/libsqlcipher.so"
                            sqlite_cache_tmp="$sqlite_cache_file.tmp"
                            mkdir -p "$sqlite_cache_dir"
                            if ! printf '%s  %s\n' "$sqlite_hash" "$sqlite_cache_file" | sha256sum -c -; then
                                rm -f "$sqlite_cache_tmp"
                                curl --fail --location --retry 8 --retry-all-errors --retry-delay 3 \
                                  --connect-timeout 20 --max-time 600 \
                                  --output "$sqlite_cache_tmp" \
                                  "https://github.com/simolus3/sqlite3.dart/releases/download/$sqlite_release/$sqlite_asset"
                                printf '%s  %s\n' "$sqlite_hash" "$sqlite_cache_tmp" | sha256sum -c -
                                mv -f "$sqlite_cache_tmp" "$sqlite_cache_file"
                            fi

                            sqlite_hash_prefix=$(printf '%.8s' "$sqlite_hash")
                            sqlite_hook_dir=".dart_tool/hooks_runner/shared/sqlite3/build/download-$sqlite_hash_prefix"
                            mkdir -p "$sqlite_hook_dir"
                            cp "$sqlite_cache_file" "$sqlite_hook_dir/libsqlcipher.so"
                        }

                        prime_sqlcipher libsqlcipher.x64.linux.so
                        prime_sqlcipher libsqlcipher.arm64.android.so
                    '''
                    sh '''
                        set -euo pipefail
                        # Keep warnings/errors fatal while allowing the repository's existing
                        # informational style lints to be cleaned up independently.
                        "$FLUTTER_ROOT/bin/flutter" analyze --no-pub --no-fatal-infos
                    '''
                    retry(3) {
                        sh '"$FLUTTER_ROOT/bin/flutter" test --no-pub'
                    }
                }
            }
        }

        stage('Build arm64 Internal Release') {
            steps {
                dir('frontend') {
                    retry(3) {
                        sh '''
                            set -euo pipefail
                            "$FLUTTER_ROOT/bin/flutter" build apk \
                              --flavor internal --release --target-platform android-arm64 --split-per-abi \
                              --build-name "$APP_VERSION" --build-number "$FLUTTER_BUILD_NUMBER" \
                              --dart-define=INTERNAL_BUILD=true \
                              --dart-define="API_BASE_URL=$API_BASE_URL" \
                              --dart-define="APP_VERSION=$IMMUTABLE_TAG"
                            source_apk=$(find build/app/outputs/flutter-apk -maxdepth 1 -type f \
                              -name '*arm64-v8a*internal*release.apk' -print -quit)
                            test -s "$source_apk"
                            cp "$source_apk" "$APK_PATH"
                        '''
                    }
                }
                sh '''
                    set -euo pipefail
                    "$ANDROID_HOME/build-tools/36.1.0/apksigner" verify --verbose "$APK_PATH"
                    badging=$("$ANDROID_HOME/build-tools/36.1.0/aapt2" dump badging "$APK_PATH")
                    printf '%s\n' "$badging" | grep -q "package: name='com.nx.miaoji.internal'"
                    printf '%s\n' "$badging" | grep -q "versionCode='$ANDROID_VERSION_CODE'"
                    "$JAVA_HOME/bin/jar" tf "$APK_PATH" >artifacts/apk-files.txt
                    grep -q 'lib/arm64-v8a/' artifacts/apk-files.txt
                    if grep -Eq 'lib/(x86|x86_64|armeabi-v7a)/' artifacts/apk-files.txt; then
                      echo 'APK contains a non-arm64 native ABI' >&2
                      exit 1
                    fi
                    sha256sum "$APK_PATH" >"$APK_PATH.sha256"
                    apk_sha=$(awk '{print $1}' "$APK_PATH.sha256"); export apk_sha
                    python3 - <<'PY'
import json, os, pathlib, subprocess
path = pathlib.Path(os.environ['MANIFEST_PATH'])
payload = {
  'project':'jizhang','buildNumber':os.environ['BUILD_NUMBER'],
  'commit':subprocess.check_output(['git','rev-parse','HEAD'],text=True).strip(),
  'productVersion':os.environ['APP_VERSION'],'versionName':os.environ['APP_VERSION']+'-internal',
  'versionCode':int(os.environ['ANDROID_VERSION_CODE']),'displayVersion':os.environ['IMMUTABLE_TAG'],
  'apiBaseUrl':os.environ['API_BASE_URL'],'abi':'arm64-v8a',
  'apk':pathlib.Path(os.environ['APK_PATH']).name,'apkSha256':os.environ['apk_sha']
}
path.write_text(json.dumps(payload,ensure_ascii=False,indent=2)+chr(10),encoding='utf-8')
PY
                    sha256sum "$MANIFEST_PATH" >"$MANIFEST_PATH.sha256"
                '''
            }
        }

        stage('Upload Artifacts') {
            steps {
                withCredentials([usernamePassword(credentialsId: "${OPENLIST_CREDENTIALS}", usernameVariable: 'OPENLIST_USERNAME', passwordVariable: 'OPENLIST_PASSWORD')]) {
                    sh '''
                        set -euo pipefail
                        for artifact in "$APK_PATH" "$APK_PATH.sha256" "$MANIFEST_PATH" "$MANIFEST_PATH.sha256"; do
                          ./scripts/upload-openlist-artifact.sh "$artifact" "$OPENLIST_REMOTE_DIR"
                        done
                    '''
                }
            }
        }
    }

    post {
        success { archiveArtifacts artifacts: 'artifacts/*.apk,artifacts/*.sha256,artifacts/*.json', fingerprint: true }
        cleanup {
            script {
                // A build can be aborted while still waiting for an executor. In that
                // case Jenkins has no FilePath context and cleanWs would mask the
                // original result with MissingContextVariableException.
                if (env.NODE_NAME?.trim()) {
                    cleanWs(deleteDirs: true, notFailBuild: true)
                }
            }
        }
    }
}
