/**
 * UAT: Checkout -> Maven -> (optional) push images to Harbor -> deploy jar + docker restart.
 *
 * Jenkins Job: Pipeline script from SCM
 * Script Path: docs/jenkins/uat/Jenkinsfile
 *
 * Harbor (153.68): when PUSH_TO_HARBOR=true, push e.g.
 *   39.105.153.68/alien_cloud/gateway:uat-latest
 * Before push: existing uat-latest is archived as uat-build-<BUILD_NUMBER> via Harbor API
 * (same digest, no docker pull/tag/push). New image is pushed only as uat-latest.
 * Prod promote: SOURCE_TAG=uat-latest.
 */

/** Normalize GIT_BRANCH: uat-20260202 (not origin/uat-20260202 or refs/heads/...) */
def normalizeGitBranch(String raw) {
    def b = (raw ?: 'uat-20260202').trim()
    if (!b) {
        return 'uat-20260202'
    }
    while (b.startsWith('refs/heads/')) {
        b = b.substring('refs/heads/'.length())
    }
    while (b.startsWith('origin/')) {
        b = b.substring('origin/'.length())
    }
    return b ?: 'uat-20260202'
}

/** HARBOR_PUSH_SCOPE: all-java-services | <repo>-only */
def filterHarborPushScope(List allServices, String scope) {
    def s = (scope ?: 'all-java-services').trim()
    if (s == 'all-java-services') {
        return allServices
    }
    if (s.endsWith('-only')) {
        def repo = s.substring(0, s.length() - '-only'.length())
        def picked = allServices.findAll { it.repo == repo }
        if (picked.isEmpty()) {
            error("Unknown HARBOR_PUSH_SCOPE: ${scope}")
        }
        return picked
    }
    error("Unknown HARBOR_PUSH_SCOPE: ${scope}")
}

/** Archive current uat-latest as uat-build-N via Harbor API (same digest, no layer re-upload). */
def archiveHarborLatestViaApi(def script, String reg, String proj, String repo, String latestTag, String buildTag) {
    script.sh """
        set -e
        REG='${reg}'
        PROJ='${proj}'
        REPO='${repo}'
        LATEST='${latestTag}'
        BUILD_TAG='${buildTag}'
        if ! command -v jq >/dev/null 2>&1; then
          echo '>>> WARN: jq missing, skip Harbor API archive for '\${REPO}
          exit 0
        fi
        enc_repo=\$(printf '%s' "\${REPO}" | jq -sRr @uri)
        if ! curl -fsS -u "\${HARBOR_USER}:\${HARBOR_PASS}" \\
          "http://\${REG}/api/v2.0/projects/\${PROJ}/repositories/\${enc_repo}/artifacts/\${LATEST}" >/dev/null 2>&1; then
          echo ">>> no prior \${LATEST} for \${REPO}, skip archive"
          exit 0
        fi
        echo ">>> archive previous \${LATEST} -> \${BUILD_TAG} (\${REPO}, Harbor API)"
        archive_code=\$(curl -sS -o /tmp/harbor_archive_\${REPO}.json -w '%{http_code}' \\
          -X POST -u "\${HARBOR_USER}:\${HARBOR_PASS}" \\
          "http://\${REG}/api/v2.0/projects/\${PROJ}/repositories/\${enc_repo}/artifacts/\${LATEST}/tags" \\
          -H 'Content-Type: application/json' \\
          -d "{\\"name\\":\\"\${BUILD_TAG}\\"}")
        case "\${archive_code}" in
          201|200)
            echo ">>> archived \${REPO}:\${BUILD_TAG} (same digest as prior \${LATEST})"
            ;;
          409)
            echo ">>> WARN: \${REPO}:\${BUILD_TAG} already exists, continue"
            ;;
          *)
            echo ">>> ERROR: Harbor archive \${REPO} HTTP \${archive_code}"
            cat /tmp/harbor_archive_\${REPO}.json 2>/dev/null || true
            exit 1
            ;;
        esac
    """
}

def pushOneHarborImage(def script, Map svc, String reg, String proj, String latestTag, String buildTag,
                      String baseImage, String dockerfile, String workspace) {
    def jarName = "${svc.module}-1.0.0.jar"
    def imageLatest = "${reg}/${proj}/${svc.repo}:${latestTag}"
    def withLibFlag = svc.withLib ? 'true' : 'false'
    script.sh """
        set -e
        test -f ${workspace}/${svc.module}/target/${jarName}
        cd ${workspace}/${svc.module}
        rm -rf .jenkins_docker_ctx && mkdir -p .jenkins_docker_ctx/lib
        cp -f target/${jarName} .jenkins_docker_ctx/${jarName}
        if [ "${withLibFlag}" = "true" ] && [ -d target/lib ]; then
          cp -rf target/lib/. .jenkins_docker_ctx/lib/
        else
          touch .jenkins_docker_ctx/lib/.keep
        fi
    """
    // Harbor API archive runs outside flock (HTTP only, safe to parallelize).
    archiveHarborLatestViaApi(script, reg, proj, svc.repo, latestTag, buildTag)
    // Parallel branches share one Docker daemon; serialize docker build/push to avoid containerd races.
    script.sh """
        set -e
        DOCKER_LOCK=/tmp/jenkins-alien-cloud-docker.lock
        flock "\${DOCKER_LOCK}" sh -c '
          set -e
          cd ${workspace}/${svc.module}/.jenkins_docker_ctx
          build_ok=0
          for attempt in 1 2 3; do
            if docker build -f ${workspace}/${dockerfile} \\
              --build-arg BASE_IMAGE=${baseImage} \\
              --build-arg JAR_FILE=${jarName} \\
              --build-arg SERVER_PORT=${svc.port} \\
              --build-arg WITH_LIB=${svc.withLib} \\
              -t ${imageLatest} .; then
              build_ok=1
              break
            fi
            echo ">>> WARN: docker build ${svc.repo} attempt \${attempt} failed, retrying..."
            sleep "\$((attempt * 5))"
          done
          if [ "\${build_ok}" -ne 1 ]; then
            echo ">>> ERROR: docker build ${svc.repo} failed after 3 attempts"
            exit 1
          fi
          docker push ${imageLatest}
          echo ">>> pushed ${imageLatest} (archived prior latest as ${buildTag} if any)"
          docker rmi ${imageLatest} 2>/dev/null || true
        '
        cd ${workspace}/${svc.module}
        rm -rf .jenkins_docker_ctx
    """
}

def deployOneUatService(def script, String moduleName, String dirName, String workspace) {
    def sourceJar = "${workspace}/${moduleName}/target/${moduleName}-1.0.0.jar"
    def sourceLib = "${workspace}/${moduleName}/target/lib"
    def targetDir = "/app_deploy_uat/${dirName}"
    script.sh """
        set -e
        echo ">>> Deploy module: ${moduleName}"
        if [ -f "${sourceJar}" ]; then
            mkdir -p "${targetDir}"
            if [ -d "${sourceLib}" ]; then
                rm -rf "${targetDir}/lib"
                cp -rf "${sourceLib}" "${targetDir}"
            fi
            cp -f "${sourceJar}" "${targetDir}/"
            if docker ps -a --format '{{.Names}}' | grep -wq "${dirName}"; then
                docker restart "${dirName}"
                echo ">>> [${dirName}] restarted"
            else
                echo ">>> [${dirName}] container missing, jar copied only"
            fi
        else
            echo ">>> [${dirName}] jar missing, skip"
        fi
    """
}

/** Delete oldest uat-build-* tags in Harbor, keep newest KEEP. Never deletes uat-latest or current build tag. */
def pruneHarborUatTags(def script, String reg, String proj, List repoNames, int keepCount, String tagPrefix, String currentBuildTag, String latestTag) {
    if (repoNames == null || repoNames.isEmpty() || keepCount < 1) {
        return
    }
    def repos = repoNames.join(' ')
    // POSIX sh only (Jenkins sh step); no mapfile / process substitution
    script.sh """
        set -e
        REG='${reg}'
        PROJ='${proj}'
        KEEP=${keepCount}
        PREFIX='${tagPrefix}'
        CURRENT='${currentBuildTag}'
        LATEST='${latestTag}'
        if ! command -v jq >/dev/null 2>&1; then
          echo '>>> Harbor prune skipped: jq not installed on Jenkins agent'
          exit 0
        fi
        for repo in ${repos}; do
          enc_repo=\$(printf '%s' "\${repo}" | jq -sRr @uri)
          tags_file=\$(mktemp)
          curl -fsS -u "\${HARBOR_USER}:\${HARBOR_PASS}" \\
            "http://\${REG}/api/v2.0/projects/\${PROJ}/repositories/\${enc_repo}/artifacts?page_size=100" \\
            | jq -r '.[] | .tags[]? | .name' | grep "^\${PREFIX}" | sort -t- -k3 -n > "\${tags_file}" || true
          count=\$(wc -l < "\${tags_file}" | tr -d ' ')
          echo ">>> prune \${repo}: \${count} tag(s) matching \${PREFIX}*"
          if [ "\${count}" -le "\${KEEP}" ]; then
            rm -f "\${tags_file}"
            continue
          fi
          del_count=\$((count - KEEP))
          deleted=0
          while IFS= read -r t; do
            if [ -z "\${t}" ]; then
              continue
            fi
            if [ "\${t}" = "\${CURRENT}" ] || [ "\${t}" = "\${LATEST}" ]; then
              continue
            fi
            if [ "\${deleted}" -ge "\${del_count}" ]; then
              break
            fi
            echo ">>> DELETE Harbor tag \${repo}:\${t}"
            if ! curl -fsS -X DELETE -u "\${HARBOR_USER}:\${HARBOR_PASS}" \\
              "http://\${REG}/api/v2.0/projects/\${PROJ}/repositories/\${enc_repo}/artifacts/\${t}/tags/\${t}"; then
              echo ">>> WARN: delete failed \${repo}:\${t} (check robot delete permission)"
            fi
            deleted=\$((deleted + 1))
          done < "\${tags_file}"
          rm -f "\${tags_file}"
        done
    """
}

pipeline {
    agent any

    options {
        buildDiscarder(logRotator(numToKeepStr: '5'))
        disableConcurrentBuilds()
        timestamps()
        timeout(time: 90, unit: 'MINUTES')
    }

    parameters {
        string(
                name: 'GIT_BRANCH',
                defaultValue: 'uat-20260202',
                trim: true,
                description: 'Git branch name only (e.g. uat-20260202). Do not prefix origin/'
        )
        booleanParam(name: 'FORCE_UPDATE', defaultValue: false, description: 'mvn -U (routine builds leave unchecked for speed)')
        booleanParam(name: 'ALLOW_SNAPSHOTS', defaultValue: true, description: 'allow SNAPSHOT deps')
        booleanParam(
                name: 'PUSH_TO_HARBOR',
                defaultValue: true,
                description: 'After Maven: docker build + push to Harbor (tags uat-latest and uat-build-<N>). Uncheck for jar-only UAT deploy.'
        )
        choice(
                name: 'HARBOR_PUSH_SCOPE',
                choices: [
                        'all-java-services',
                        'gateway-only',
                        'store-only',
                        'second-only',
                        'store-platform-only',
                        'lawyer-only',
                        'job-only',
                        'dining-only',
                ],
                description: 'Only when PUSH_TO_HARBOR=true; default=all seven; *-only=one service'
        )
        string(name: 'HARBOR_REGISTRY', defaultValue: '39.105.153.68', trim: true)
        string(name: 'HARBOR_PROJECT', defaultValue: 'alien_cloud', trim: true)
        booleanParam(
                name: 'HARBOR_PRUNE_OLD_TAGS',
                defaultValue: true,
                description: 'After push: delete old uat-build-* tags in Harbor, keep last N per repo (never deletes uat-latest)'
        )
        string(name: 'HARBOR_KEEP_TAG_COUNT', defaultValue: '10', trim: true,
                description: 'How many uat-build-* tags to keep per repository')
        booleanParam(
                name: 'HARBOR_PUSH_PARALLEL',
                defaultValue: true,
                description: 'Parallel per service (context prep + Harbor API archive); docker build/push serialized via flock'
        )
    }

    environment {
        MAVEN_HOME = tool '3.6.3'
        PATH = "${MAVEN_HOME}/bin:${env.PATH}"
        GIT_URL = 'http://8.152.195.41:3000/alien/alien_cloud'
        GIT_CREDENTIALS = 'zhanghaomimapingzheng'
        HARBOR_CREDENTIALS = 'harbor-robot-alien'
        UAT_HARBOR_LATEST_TAG = 'uat-latest'
        UAT_HARBOR_BUILD_TAG = "uat-build-${env.BUILD_NUMBER}"
        DOCKERFILE_JAVA = 'docs/jenkins/produ/docker/Dockerfile.java-service'
        MAVEN_LOCAL_REPO = '/var/jenkins_home/.m2/repository'
    }

    stages {
        stage('Checkout') {
            steps {
                script {
                    // Do not use env.GIT_BRANCH — Jenkins SCM plugin injects origin/<branch> and overwrites it.
                    def branch = normalizeGitBranch(params.GIT_BRANCH)
                    if (!branch) {
                        error('GIT_BRANCH is required')
                    }
                    env.UAT_GIT_BRANCH = branch
                    if (params.GIT_BRANCH?.trim() != branch) {
                        echo ">>> GIT_BRANCH normalized: '${params.GIT_BRANCH}' -> '${branch}'"
                    }
                    echo ">>> Checkout branch: ${branch}"
                    sh """
                        set -e
                        git fetch origin
                        git checkout -B ${branch} origin/${branch}
                        git log -1 --oneline
                    """
                }
            }
        }

        stage('Prepare Maven Settings') {
            steps {
                script {
                    writeFile file: 'settings.xml', text: """<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
    <mirrors>
        <mirror>
            <id>aliyun-central</id>
            <mirrorOf>central</mirrorOf>
            <name>Aliyun Central</name>
            <url>https://maven.aliyun.com/repository/central</url>
        </mirror>
    </mirrors>
    <profiles>
        <profile>
            <id>repo-mix</id>
            <repositories>
                <repository>
                    <id>aliyunmaven</id>
                    <name>Aliyun Maven</name>
                    <url>https://maven.aliyun.com/repository/public</url>
                    <releases><enabled>true</enabled><updatePolicy>daily</updatePolicy></releases>
                    <snapshots><enabled>false</enabled></snapshots>
                </repository>
                <repository>
                    <id>central</id>
                    <name>Maven Central</name>
                    <url>https://repo.maven.apache.org/maven2</url>
                    <releases><enabled>true</enabled><updatePolicy>daily</updatePolicy></releases>
                    <snapshots><enabled>false</enabled></snapshots>
                </repository>
                <repository>
                    <id>spring-milestones</id>
                    <name>Spring Milestones</name>
                    <url>https://repo.spring.io/milestone</url>
                    <releases><enabled>true</enabled><updatePolicy>daily</updatePolicy></releases>
                    <snapshots><enabled>false</enabled></snapshots>
                </repository>
                <repository>
                    <id>spring-snapshots</id>
                    <name>Spring Snapshots</name>
                    <url>https://repo.spring.io/snapshot</url>
                    <releases><enabled>false</enabled></releases>
                    <snapshots><enabled>true</enabled><updatePolicy>daily</updatePolicy></snapshots>
                </repository>
            </repositories>
            <pluginRepositories>
                <pluginRepository>
                    <id>aliyunmaven</id>
                    <url>https://maven.aliyun.com/repository/public</url>
                    <releases><enabled>true</enabled></releases>
                    <snapshots><enabled>false</enabled></snapshots>
                </pluginRepository>
                <pluginRepository>
                    <id>central</id>
                    <url>https://repo.maven.apache.org/maven2</url>
                    <releases><enabled>true</enabled></releases>
                    <snapshots><enabled>false</enabled></snapshots>
                </pluginRepository>
                <pluginRepository>
                    <id>spring-milestones</id>
                    <url>https://repo.spring.io/milestone</url>
                    <releases><enabled>true</enabled></releases>
                    <snapshots><enabled>false</enabled></snapshots>
                </pluginRepository>
            </pluginRepositories>
        </profile>
    </profiles>
    <activeProfiles>
        <activeProfile>repo-mix</activeProfile>
    </activeProfiles>
</settings>
"""
                }
            }
        }

        stage('Maven Build') {
            steps {
                script {
                    def updateFlag = params.FORCE_UPDATE ? '-U' : ''
                    retry(2) {
                        sh """
                            set -e
                            mvn -version
                            unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY all_proxy no_proxy NO_PROXY || true
                            export MAVEN_OPTS="-Xms512m -Xmx2048m -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true -Dmaven.wagon.http.ssl.ignore.validity.dates=true"
                            mkdir -p ${MAVEN_LOCAL_REPO}
                            mvn clean package -DskipTests -s settings.xml ${updateFlag} -e \\
                              -T 1C -Dmaven.artifact.threads=8 \\
                              -Dmaven.repo.local=${MAVEN_LOCAL_REPO}
                        """
                    }
                }
            }
        }

        stage('Push images to Harbor') {
            when {
                expression { return params.PUSH_TO_HARBOR == true }
            }
            steps {
                script {
                    def reg = params.HARBOR_REGISTRY.trim()
                    def proj = params.HARBOR_PROJECT.trim()
                    def latestTag = env.UAT_HARBOR_LATEST_TAG
                    def buildTag = env.UAT_HARBOR_BUILD_TAG
                    def baseImage = "${reg}/${proj}/base/openjdk8-ffmpeg:v1"
                    def dockerfile = env.DOCKERFILE_JAVA

                    def allHarborServices = [
                            [module: 'alien-gateway',        repo: 'gateway',        port: '8000',  withLib: false],
                            [module: 'alien-store',          repo: 'store',          port: '50014', withLib: true],
                            [module: 'alien-second',         repo: 'second',         port: '50015', withLib: false],
                            [module: 'alien-store-platform', repo: 'store-platform', port: '50016', withLib: false],
                            [module: 'alien-lawyer',         repo: 'lawyer',         port: '50017', withLib: true],
                            [module: 'alien-job',            repo: 'job',            port: '50108', withLib: false],
                            [module: 'alien-dining',         repo: 'dining',         port: '50019', withLib: false],
                    ]
                    def harborServices = filterHarborPushScope(allHarborServices, params.HARBOR_PUSH_SCOPE)
                    echo ">>> HARBOR_PUSH_SCOPE=${params.HARBOR_PUSH_SCOPE} repos=${harborServices*.repo.join(',')}"

                    withCredentials([usernamePassword(
                            credentialsId: env.HARBOR_CREDENTIALS,
                            usernameVariable: 'HARBOR_USER',
                            passwordVariable: 'HARBOR_PASS',
                    )]) {
                        sh """
                            set -e
                            echo "\${HARBOR_PASS}" | docker login ${reg} -u "\${HARBOR_USER}" --password-stdin
                            echo ">>> docker disk before Harbor push:"
                            df -h /var/lib/docker 2>/dev/null || df -h / || true
                            docker image prune -f 2>/dev/null || true
                        """
                        if (params.HARBOR_PUSH_PARALLEL) {
                            def pushBranches = [:]
                            harborServices.each { svc ->
                                def s = svc
                                pushBranches[s.repo] = {
                                    pushOneHarborImage(
                                        this, s, reg, proj, latestTag, buildTag,
                                        baseImage, dockerfile, env.WORKSPACE,
                                    )
                                }
                            }
                            parallel pushBranches
                        } else {
                            harborServices.each { svc ->
                                pushOneHarborImage(
                                    this, svc, reg, proj, latestTag, buildTag,
                                    baseImage, dockerfile, env.WORKSPACE,
                                )
                            }
                        }
                        sh """
                            echo ">>> docker disk after Harbor push:"
                            df -h /var/lib/docker 2>/dev/null || df -h / || true
                            docker image prune -f 2>/dev/null || true
                        """
                        if (params.HARBOR_PRUNE_OLD_TAGS == true) {
                            def keepN = (params.HARBOR_KEEP_TAG_COUNT ?: '10').trim() as int
                            catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE') {
                                pruneHarborUatTags(
                                    this, reg, proj, harborServices*.repo,
                                    keepN, 'uat-build-', buildTag, latestTag,
                                )
                            }
                        }
                    }
                    echo ">>> Harbor latest: ${env.UAT_HARBOR_LATEST_TAG}; archived tag this run: ${env.UAT_HARBOR_BUILD_TAG}"
                    echo ">>> Prod promote: SOURCE_TAG=${env.UAT_HARBOR_LATEST_TAG}"
                }
            }
        }

        stage('Deploy Services') {
            steps {
                script {
                    def services = [
                            'alien-gateway:gateway-uat',
                            'alien-job:job-uat',
                            'alien-lawyer:lawyer-uat',
                            'alien-second:second-uat',
                            'alien-store:store-uat',
                            'alien-dining:dining-uat',
                            'alien-store-platform:store-platform-uat',
                    ]

                    def deployBranches = [:]
                    services.each { item ->
                        def parts = item.split(':')
                        def moduleName = parts[0]
                        def dirName = parts[1]
                        deployBranches[dirName] = {
                            deployOneUatService(this, moduleName, dirName, env.WORKSPACE)
                        }
                    }
                    parallel deployBranches
                }
            }
        }
    }

    post {
        always {
            sh 'rm -f settings.xml || true'
            script {
                if (!params.PUSH_TO_HARBOR) {
                    echo '>>> Harbor push SKIPPED: PUSH_TO_HARBOR is false. On "Build with Parameters" check PUSH_TO_HARBOR.'
                }
            }
        }
        success {
            script {
                if (params.PUSH_TO_HARBOR) {
                    echo ">>> Harbor latest: ${env.UAT_HARBOR_LATEST_TAG}"
                    echo ">>> Prod promote: SOURCE_TAG=${env.UAT_HARBOR_LATEST_TAG}"
                }
            }
        }
    }
}
