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

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

    environment {
        PATH = '/home/nanxunai/.local/bin:/usr/local/bin:/usr/bin:/bin'
        REGISTRY_URL = 'reg.nxsir.cn'
        API_IMAGE_REPO = 'reg.nxsir.cn/live_recorder/app-api'
        WEB_IMAGE_REPO = 'reg.nxsir.cn/live_recorder/app-web'
        HARBOR_CREDENTIALS = 'harbor_key'
        OPENLIST_CREDENTIALS = 'openlist_key'
        NODE_CREDENTIALS = 'bbb939ea-4f01-4b47-aecb-c5ee2a551ef4'
        OPENLIST_BASE_URL = 'https://openlist.nxsir.cn'
        OPENLIST_REMOTE_DIR = '/yidongpan/构建产物/liverecorder'

        FNPACK_BIN = '/home/nanxunai/.local/bin/fnpack'
        FNPACK_SHA256 = '54b97fa7b70968c4d05c79840f5daeff508957d0bb2062fdb0376d00d9615c93'
        DOTNET = '/home/nanxunai/.local/bin/dotnet'

        BUILDER_NAME = "liverecorder-${BUILD_NUMBER}"
        BUILDKIT_IMAGE = 'docker.m.daocloud.io/moby/buildkit:buildx-stable-1'
        BINFMT_IMAGE = 'docker.m.daocloud.io/tonistiigi/binfmt:latest'
        ARM_TEST_IMAGE = 'docker.m.daocloud.io/library/alpine:latest'
        NODE_BUILD_IMAGE = 'docker.m.daocloud.io/library/node:22-alpine'
        NGINX_IMAGE = 'docker.m.daocloud.io/library/nginx:1.27-alpine'

        HTTP_PROXY_URL = 'http://192.168.5.200:7890'
        NO_PROXY_HOSTS = '127.0.0.1,localhost,reg.nxsir.cn,gitea.nxsir.cn,openlist.nxsir.cn,mirrors.tuna.tsinghua.edu.cn,.tsinghua.edu.cn,mcr.microsoft.com'

        CI_ROOT = "${WORKSPACE}/.ci"
        DOCKER_CONFIG = "${WORKSPACE}/.ci/docker"
        NUGET_PACKAGES = "${WORKSPACE}/.ci/nuget-packages"
        NUGET_HTTP_CACHE_PATH = "${WORKSPACE}/.ci/nuget-http"
        NUGET_PLUGINS_CACHE_PATH = "${WORKSPACE}/.ci/nuget-plugins"
        DOTNET_CLI_HOME = "${WORKSPACE}/.ci/dotnet-home"
        NPM_CONFIG_CACHE = "${WORKSPACE}/.ci/npm-cache"
        LIVERECORDER_NUGET_FEED = "${WORKSPACE}/.ci/nuget-feed"
        LIVERECORDER_BUILD_TMPDIR = "${WORKSPACE}/.ci/fnos-tmp"
        LIVERECORDER_VERIFY_TMPDIR = "${WORKSPACE}/.ci/fnos-verify"
        LIVERECORDER_SKIP_NPM_CI = '1'
    }

    stages {
        stage('Checkout') {
            steps {
                withCredentials([usernamePassword(
                    credentialsId: "${NODE_CREDENTIALS}",
                    usernameVariable: 'JENKINS_NODE_USERNAME',
                    passwordVariable: 'JENKINS_NODE_PASSWORD'
                )]) {
                    sh '''
                        set -euo pipefail
                        if [ -d "$WORKSPACE/.ci" ]; then
                          printf '%s\n' "$JENKINS_NODE_PASSWORD" | \
                            sudo -S -p '' chown -R -- "$(id -u):$(id -g)" "$WORKSPACE/.ci"
                        fi
                    '''
                }
                deleteDir()
                checkout scm
            }
        }

        stage('Metadata And Preflight') {
            steps {
                script {
                    env.APP_VERSION = sh(
                        script: "sed -n 's/^version=//p' fnos/manifest | head -n 1",
                        returnStdout: true
                    ).trim()
                    env.SHORT_SHA = sh(script: 'git rev-parse --short=8 HEAD', returnStdout: true).trim()
                    if (!env.APP_VERSION || !env.SHORT_SHA) {
                        error('Unable to resolve fnOS version or Git commit.')
                    }
                    env.FNOS_VERSION = sh(
                        script: '''
                            set -euo pipefail
                            base=$(sed -n 's/^version=//p' fnos/manifest | head -n 1)
                            major=${base%%.*}
                            remainder=${base#*.}
                            minor=${remainder%%.*}
                            patch=${remainder#*.}
                            test "$base" = "$major.$minor.$patch" || {
                              echo "Invalid base version: $base" >&2
                              exit 1
                            }
                            case "$major.$minor.$patch" in
                              *[!0-9.]*|.*|*..*|*.) echo "Invalid base version: $base" >&2; exit 1 ;;
                            esac
                            test "$BUILD_NUMBER" -lt 100000 || {
                              echo 'BUILD_NUMBER must remain below 100000 for fnOS version ordering.' >&2
                              exit 1
                            }
                            ci_patch=$((patch * 100000 + BUILD_NUMBER))
                            printf '%s.%s.%s\n' "$major" "$minor" "$ci_patch"
                        ''',
                        returnStdout: true
                    ).trim()
                    env.IMMUTABLE_TAG = "${env.APP_VERSION}-b${env.BUILD_NUMBER}-${env.SHORT_SHA}"
                    env.FPK_BASENAME = "liverecorder-${env.FNOS_VERSION}-x86_64.fpk"
                    env.FPK_PATH = "${env.WORKSPACE}/artifacts/ci/${env.FPK_BASENAME}"
                    currentBuild.displayName = "#${env.BUILD_NUMBER} ${env.APP_VERSION} ${env.SHORT_SHA}"
                    currentBuild.description = "${env.IMMUTABLE_TAG}"
                }

                withCredentials([usernamePassword(
                    credentialsId: "${NODE_CREDENTIALS}",
                    usernameVariable: 'JENKINS_NODE_USERNAME',
                    passwordVariable: 'JENKINS_NODE_PASSWORD'
                )]) {
                    sh '''
                        set -euo pipefail
                        test "$(uname -m)" = x86_64
                        for command_name in git sudo docker curl python3 npm node sha256sum stat tar; do
                          command -v "$command_name" >/dev/null
                        done
                        test -x "$DOTNET"
                        test -x "$FNPACK_BIN"
                        printf '%s  %s\n' "$FNPACK_SHA256" "$FNPACK_BIN" | sha256sum --check --status
                        ./scripts/ci-docker.sh version >/dev/null
                        ./scripts/ci-docker.sh buildx version

                        available_kb=$(df -Pk "$WORKSPACE" | awk 'NR == 2 { print $4 }')
                        if [ "$available_kb" -lt 2097152 ]; then
                          echo "At least 2 GiB free workspace disk is required; available KiB: $available_kb" >&2
                          exit 1
                        fi

                        mkdir -p \
                          "$DOCKER_CONFIG" \
                          "$NUGET_PACKAGES" \
                          "$NUGET_HTTP_CACHE_PATH" \
                          "$NUGET_PLUGINS_CACHE_PATH" \
                          "$DOTNET_CLI_HOME" \
                          "$NPM_CONFIG_CACHE" \
                          "$LIVERECORDER_NUGET_FEED" \
                          "$LIVERECORDER_BUILD_TMPDIR" \
                          "$LIVERECORDER_VERIFY_TMPDIR" \
                          "$(dirname -- "$FPK_PATH")"
                        chmod 700 "$DOCKER_CONFIG"
                    '''
                }
            }
        }

        stage('Restore And Test') {
            steps {
                sh '''
                    set -euo pipefail
                    npm ci --prefix frontend
                    npm run build --prefix frontend
                    "$DOTNET" restore LiveRecorder.sln --disable-parallel
                    "$DOTNET" test LiveRecorder.sln -c Release --no-restore --logger 'console;verbosity=normal' /maxcpucount:1
                '''
            }
        }

        stage('Build fnOS x64') {
            steps {
                sh '''
                    set -euo pipefail
                    PACKAGE_VERSION="$FNOS_VERSION" FNPACK="$FNPACK_BIN" \
                      ./scripts/build-fnos-package.sh "$FPK_PATH"
                    ./scripts/verify-fnos-package.sh "$FPK_PATH" "$FNOS_VERSION"
                    test -s "$FPK_PATH.sha256"
                '''
            }
        }

        stage('Prepare Docker Builder') {
            steps {
                withCredentials([usernamePassword(
                    credentialsId: "${NODE_CREDENTIALS}",
                    usernameVariable: 'JENKINS_NODE_USERNAME',
                    passwordVariable: 'JENKINS_NODE_PASSWORD'
                )]) {
                    sh '''
                        set -euo pipefail
                        if ! ./scripts/ci-docker.sh run --rm --platform linux/arm64 "$ARM_TEST_IMAGE" uname -m 2>/dev/null | grep -q aarch64; then
                          ./scripts/ci-docker.sh run --privileged --rm "$BINFMT_IMAGE" --install arm64
                        fi
                        ./scripts/ci-docker.sh run --rm --platform linux/arm64 "$ARM_TEST_IMAGE" uname -m | grep -q aarch64

                        ./scripts/ci-docker.sh buildx rm -f "$BUILDER_NAME" >/dev/null 2>&1 || true
                        ./scripts/ci-docker.sh buildx create \
                          --name "$BUILDER_NAME" \
                          --driver docker-container \
                          --driver-opt "image=$BUILDKIT_IMAGE" \
                          --driver-opt network=host \
                          --driver-opt "env.http_proxy=$HTTP_PROXY_URL" \
                          --driver-opt "env.https_proxy=$HTTP_PROXY_URL" \
                          --driver-opt "env.HTTP_PROXY=$HTTP_PROXY_URL" \
                          --driver-opt "env.HTTPS_PROXY=$HTTP_PROXY_URL" \
                          --driver-opt "env.no_proxy=reg.nxsir.cn" \
                          --driver-opt "env.NO_PROXY=reg.nxsir.cn" \
                          --use
                        ./scripts/ci-docker.sh buildx inspect "$BUILDER_NAME" --bootstrap
                    '''
                }
            }
        }

        stage('Prepare Harbor Authentication') {
            steps {
                withCredentials([usernamePassword(
                    credentialsId: "${HARBOR_CREDENTIALS}",
                    usernameVariable: 'HARBOR_USERNAME',
                    passwordVariable: 'HARBOR_PASSWORD'
                )]) {
                    sh '''
                        set -euo pipefail
                        set +x
                        auth=$(printf '%s:%s' "$HARBOR_USERNAME" "$HARBOR_PASSWORD" | base64 -w0)
                        printf '{"auths":{"%s":{"auth":"%s"}}}\n' "$REGISTRY_URL" "$auth" >"$DOCKER_CONFIG/config.json"
                        chmod 600 "$DOCKER_CONFIG/config.json"
                        unset auth
                        set -x
                    '''
                }
            }
        }

        stage('Build And Push amd64') {
            steps {
                withCredentials([usernamePassword(
                    credentialsId: "${NODE_CREDENTIALS}",
                    usernameVariable: 'JENKINS_NODE_USERNAME',
                    passwordVariable: 'JENKINS_NODE_PASSWORD'
                )]) {
                sh '''
                    set -euo pipefail
                    arch=amd64
                    platform=linux/amd64
                    api_ref="$API_IMAGE_REPO:$IMMUTABLE_TAG-$arch"
                    web_ref="$WEB_IMAGE_REPO:$IMMUTABLE_TAG-$arch"

                    ./scripts/ci-docker.sh buildx build \
                      --builder "$BUILDER_NAME" --platform "$platform" --network host \
                      --progress=plain --provenance=false --no-cache \
                      --build-arg HTTP_PROXY="$HTTP_PROXY_URL" \
                      --build-arg HTTPS_PROXY="$HTTP_PROXY_URL" \
                      --build-arg NO_PROXY="$NO_PROXY_HOSTS" \
                      --build-arg http_proxy="$HTTP_PROXY_URL" \
                      --build-arg https_proxy="$HTTP_PROXY_URL" \
                      --build-arg no_proxy="$NO_PROXY_HOSTS" \
                      -f src/LiveRecorder.WebApi/Dockerfile -t "$api_ref" --push .
                    ./scripts/ci-docker.sh buildx prune \
                      --builder "$BUILDER_NAME" --all --force

                    ./scripts/ci-docker.sh buildx build \
                      --builder "$BUILDER_NAME" --platform "$platform" --network host \
                      --progress=plain --provenance=false --no-cache \
                      --build-arg NODE_IMAGE="$NODE_BUILD_IMAGE" \
                      --build-arg NGINX_IMAGE="$NGINX_IMAGE" \
                      --build-arg HTTP_PROXY="$HTTP_PROXY_URL" \
                      --build-arg HTTPS_PROXY="$HTTP_PROXY_URL" \
                      --build-arg NO_PROXY="$NO_PROXY_HOSTS" \
                      --build-arg http_proxy="$HTTP_PROXY_URL" \
                      --build-arg https_proxy="$HTTP_PROXY_URL" \
                      --build-arg no_proxy="$NO_PROXY_HOSTS" \
                      --build-arg VITE_API_BASE_URL=/api \
                      -f frontend/Dockerfile -t "$web_ref" --push frontend
                    ./scripts/ci-docker.sh buildx prune \
                      --builder "$BUILDER_NAME" --all --force
                '''
                }
            }
        }

        stage('Build And Push arm64') {
            steps {
                withCredentials([usernamePassword(
                    credentialsId: "${NODE_CREDENTIALS}",
                    usernameVariable: 'JENKINS_NODE_USERNAME',
                    passwordVariable: 'JENKINS_NODE_PASSWORD'
                )]) {
                sh '''
                    set -euo pipefail
                    arch=arm64
                    platform=linux/arm64
                    api_ref="$API_IMAGE_REPO:$IMMUTABLE_TAG-$arch"
                    web_ref="$WEB_IMAGE_REPO:$IMMUTABLE_TAG-$arch"

                    ./scripts/ci-docker.sh buildx build \
                      --builder "$BUILDER_NAME" --platform "$platform" --network host \
                      --progress=plain --provenance=false --no-cache \
                      --build-arg HTTP_PROXY="$HTTP_PROXY_URL" \
                      --build-arg HTTPS_PROXY="$HTTP_PROXY_URL" \
                      --build-arg NO_PROXY="$NO_PROXY_HOSTS" \
                      --build-arg http_proxy="$HTTP_PROXY_URL" \
                      --build-arg https_proxy="$HTTP_PROXY_URL" \
                      --build-arg no_proxy="$NO_PROXY_HOSTS" \
                      -f src/LiveRecorder.WebApi/Dockerfile -t "$api_ref" --push .
                    ./scripts/ci-docker.sh buildx prune \
                      --builder "$BUILDER_NAME" --all --force

                    ./scripts/ci-docker.sh buildx build \
                      --builder "$BUILDER_NAME" --platform "$platform" --network host \
                      --progress=plain --provenance=false --no-cache \
                      --build-arg NODE_IMAGE="$NODE_BUILD_IMAGE" \
                      --build-arg NGINX_IMAGE="$NGINX_IMAGE" \
                      --build-arg HTTP_PROXY="$HTTP_PROXY_URL" \
                      --build-arg HTTPS_PROXY="$HTTP_PROXY_URL" \
                      --build-arg NO_PROXY="$NO_PROXY_HOSTS" \
                      --build-arg http_proxy="$HTTP_PROXY_URL" \
                      --build-arg https_proxy="$HTTP_PROXY_URL" \
                      --build-arg no_proxy="$NO_PROXY_HOSTS" \
                      --build-arg VITE_API_BASE_URL=/api \
                      -f frontend/Dockerfile -t "$web_ref" --push frontend
                    ./scripts/ci-docker.sh buildx prune \
                      --builder "$BUILDER_NAME" --all --force
                '''
                }
            }
        }

        stage('Publish Multi-arch Manifests') {
            steps {
                withCredentials([usernamePassword(
                    credentialsId: "${NODE_CREDENTIALS}",
                    usernameVariable: 'JENKINS_NODE_USERNAME',
                    passwordVariable: 'JENKINS_NODE_PASSWORD'
                )]) {
                sh '''
                    set -euo pipefail
                    for repository in "$API_IMAGE_REPO" "$WEB_IMAGE_REPO"; do
                      immutable_ref="$repository:$IMMUTABLE_TAG"
                      ./scripts/ci-docker.sh buildx imagetools create \
                        --tag "$immutable_ref" \
                        "$immutable_ref-amd64" "$immutable_ref-arm64"

                      manifest=$(./scripts/ci-docker.sh buildx imagetools inspect --raw "$immutable_ref")
                      printf '%s' "$manifest" | grep -q '"architecture"[[:space:]]*:[[:space:]]*"amd64"'
                      printf '%s' "$manifest" | grep -q '"architecture"[[:space:]]*:[[:space:]]*"arm64"'
                    done

                    ./scripts/ci-docker.sh buildx imagetools create \
                      --tag "$API_IMAGE_REPO:$APP_VERSION" --tag "$API_IMAGE_REPO:latest" \
                      "$API_IMAGE_REPO:$IMMUTABLE_TAG"
                    ./scripts/ci-docker.sh buildx imagetools create \
                      --tag "$WEB_IMAGE_REPO:$APP_VERSION" --tag "$WEB_IMAGE_REPO:latest" \
                      "$WEB_IMAGE_REPO:$IMMUTABLE_TAG"
                '''
                }
            }
        }

        stage('Upload fnOS Artifact') {
            steps {
                withCredentials([usernamePassword(
                    credentialsId: "${OPENLIST_CREDENTIALS}",
                    usernameVariable: 'OPENLIST_USERNAME',
                    passwordVariable: 'OPENLIST_PASSWORD'
                )]) {
                    sh '''
                        set -euo pipefail
                        ./scripts/upload-openlist-artifact.sh "$FPK_PATH" "$OPENLIST_REMOTE_DIR"
                        ./scripts/upload-openlist-artifact.sh "$FPK_PATH.sha256" "$OPENLIST_REMOTE_DIR"
                    '''
                }
            }
        }

        stage('Build Summary') {
            steps {
                sh '''
                    set -euo pipefail
                    printf '%s\n' \
                      "Commit: $(git rev-parse HEAD)" \
                      "Product version: $APP_VERSION" \
                      "fnOS package version: $FNOS_VERSION" \
                      "fnOS: $FPK_BASENAME" \
                      "Checksum: $(awk '{print $1}' "$FPK_PATH.sha256")" \
                      "API amd64: $API_IMAGE_REPO:$IMMUTABLE_TAG-amd64" \
                      "API arm64: $API_IMAGE_REPO:$IMMUTABLE_TAG-arm64" \
                      "API multi-arch: $API_IMAGE_REPO:$IMMUTABLE_TAG" \
                      "Web amd64: $WEB_IMAGE_REPO:$IMMUTABLE_TAG-amd64" \
                      "Web arm64: $WEB_IMAGE_REPO:$IMMUTABLE_TAG-arm64" \
                      "Web multi-arch: $WEB_IMAGE_REPO:$IMMUTABLE_TAG" \
                      "OpenList: $OPENLIST_BASE_URL$OPENLIST_REMOTE_DIR/$FPK_BASENAME"
                '''
            }
        }
    }

    post {
        success {
            echo "LiveRecorder ${env.IMMUTABLE_TAG} published successfully."
        }
        failure {
            echo 'LiveRecorder pipeline failed; stable Docker tags were only updated if all architecture builds succeeded.'
        }
        always {
            withCredentials([usernamePassword(
                credentialsId: "${NODE_CREDENTIALS}",
                usernameVariable: 'JENKINS_NODE_USERNAME',
                passwordVariable: 'JENKINS_NODE_PASSWORD'
            )]) {
                sh '''
                    set +e
                    if [ -x ./scripts/ci-docker.sh ] && [ -n "${BUILDER_NAME:-}" ]; then
                      ./scripts/ci-docker.sh buildx rm -f "$BUILDER_NAME" >/dev/null 2>&1 || true
                    fi
                    if [ -x ./scripts/ci-docker.sh ] && [ -n "${IMMUTABLE_TAG:-}" ]; then
                      for repository in "$API_IMAGE_REPO" "$WEB_IMAGE_REPO"; do
                        for suffix in amd64 arm64; do
                          ./scripts/ci-docker.sh rmi "$repository:$IMMUTABLE_TAG-$suffix" >/dev/null 2>&1 || true
                        done
                        ./scripts/ci-docker.sh manifest rm "$repository:$IMMUTABLE_TAG" >/dev/null 2>&1 || true
                      done
                    fi
                    if [ -f "$DOCKER_CONFIG/config.json" ]; then
                      : >"$DOCKER_CONFIG/config.json"
                    fi
                    if [ -d "${CI_ROOT:-}" ]; then
                      printf '%s\n' "$JENKINS_NODE_PASSWORD" | \
                        sudo -S -p '' chown -R -- "$(id -u):$(id -g)" "$CI_ROOT" || true
                    fi
                '''
            }
            cleanWs(deleteDirs: true, notFailBuild: true)
        }
    }
}
