GITHUB_REPOSITORY = 'hot-updater'
AWS_REGIONS = ['us-east-1']
ECR_ACCOUNT_ID = '086679231553'
AWS_ACCOUNTS = [
    'prod': [
        accountId: '588827373449',
        deploymentRole: 'prod-jenkins-pipeline-deploy-role'
    ],
    'qa': [
        accountId: '635057242373',
        deploymentRole: 'qa-jenkins-pipeline-deploy-role'
    ]
]

/**
 * Map of projects to their configuration. The key maps to the build context directory.
 * '.' means the repo root.
 *
 * Configuration values:
 *  - ecrRepo (required): The ECR repo to publish the image to
 *  - vulnerabilitiesToIgnore (optional): A list of security vulnerabilities to ignore from docker scan.
 *  - dockerScanFailBuild (optional): Boolean to decide if the pipeline should fail on docker scan errors. Default false.
 */
PROJECTS = [
    '.': [
        ecrRepo: 'hot-updater',
        vulnerabilitiesToIgnore: [
            'CVE-2026-27135',    // pkg:deb/debian/nghttp2@1.52.0-1%2Bdeb12u2?arch=amd64&distro=bookworm&epoch=0
            'CVE-2026-2219',     // pkg:deb/debian/dpkg@1.21.22?arch=amd64&distro=bookworm&epoch=0
        ],
        dockerScanFailBuild: false
    ],
]

/**
 * Map of service names to their configuration.
 *
 * Configuration values:
 *  - project (required): The project that this service is based on. Must map to one of the PROJECTS keys.
 *  - containerName (optional): The name of the main container in the task. Defaults to service name.
 *  - deployToProd (optional): Whether or not to deploy to prod. Takes precedence over DEPLOY_TO_PROD parameter.
 *  - deployToQA (optional): Whether or not to deploy to QA.
 *  - prodAccount (optional): The name of the production account. Must map to one of the ACCOUNTS keys.
 *  - qaAccount (optional): The name of the QA account. Must map to one of the ACCOUNTS keys.
 */
SERVICES = [
    'hot-updater-server': [
        project: '.',
        deployToProd: false,
        prodAccount: 'prod',
        deployToQA: true,
        qaAccount: 'qa',
        publishNpmPackage: false,
        buildDockerImage: true
    ],
]

/**
 * Map of packages to their configuration.
 * '.' means the root package (@theorchard/hot-updater including the CLI).
 *
 * Configuration values:
 *  - publishNpmPackage (required): Whether or not to publish the NPM package.
 *  - buildDockerImage (required): Whether or not to build the Docker image.
 */
PACKAGES = [
    '.': [ publishNpmPackage: true, buildDockerImage: false ],
]

@groovy.transform.Field
def projectsToBuild = null

@groovy.transform.Field
def servicesToDeploy = null

@groovy.transform.Field
def packagesToPublish = null

pipeline {
    agent any

    options {
        ansiColor('xterm')
        disableConcurrentBuilds()
        timestamps()
    }

    parameters {
        choice(name: 'SERVICE_NAME', choices: [''] + SERVICES.keySet(), description: 'The service to build and deploy')
        choice(name: 'PROJECT_NAME', choices: [''] + PROJECTS.keySet(), description: 'Build and deploy all services in this project if SERVICE_NAME is not provided.')
        choice(name: 'PACKAGE_NAME', choices: [''] + PACKAGES.keySet(), description: 'The package to publish')
        booleanParam(name: 'DEPLOY_TO_PROD', defaultValue: false, description: 'Whether or not to deploy to prod.')
        booleanParam(name: 'PUBLISH_NPM_PACKAGE', defaultValue: false, description: 'Whether or not to publish the NPM package.')
        string(name: 'SHARED_LIBRARIES_VERSION', defaultValue: 'master', description: 'The version of the Jenkins shared libraries to use. Can be a branch, tag, Git revision or PR ref (e.g. pull/PR_NUMBER/merge).')
    }

    triggers {
        issueCommentTrigger('.*(retest this please|build docker|publish package).*')
    }

    stages {
        stage('Load Shared Libraries') {
            steps {
                script {
                    GIT_HASH = env.GIT_COMMIT.take(7)
                    echo "GIT_HASH=${GIT_HASH}"
                }
                library "jenkins-global-libraries@${params.SHARED_LIBRARIES_VERSION}"
            }
        }
        stage('Compliance Checks') {
            steps {
                script {
                    parallel(getItemsToBuild().collectEntries { project, config ->
                        return [
                            (project): {
                                dir(project) {
                                    complianceChecks()
                                }
                            }
                        ]
                    })
                }
            }
        }
        stage('Validate Software Catalog Definition') {
            steps {
                datadogSoftwareCatalogValidate(servicePath: '.')
            }
        }
        stage('Unit Tests and Style Checks') {
            steps {
                script {
                    parallel(getItemsToBuild().collectEntries { project, config ->
                        return [
                            (project): {
                                dir(project) {
                                    withEnv(["GIT_HASH=${GIT_HASH}"]) {
                                        withEcr {
                                            sh '''
                                            docker compose up --build \
                                                --always-recreate-deps \
                                                --exit-code-from lint-and-test \
                                                --abort-on-container-exit \
                                                --remove-orphans \
                                                lint-and-test
                                            '''
                                            sh 'docker compose down --volumes --remove-orphans'
                                        }
                                    }
                                }
                            }
                        ]
                    })
                }
            }
            post {
                always {
                    script {
                        parallel(getItemsToBuild().collectEntries { project, config ->
                            return [
                                (project): {
                                    dir(project) {
                                        withEnv(["GIT_HASH=${GIT_HASH}"]) {
                                            withEcr {
                                                sh 'docker compose down --volumes --remove-orphans'
                                            }
                                        }
                                    }
                                }
                            ]
                        })
                    }
                }
            }
        }
        stage('Sonar Scan and Analysis') {
            when {
                branch 'master'
            }
            steps {
                script {
                    parallel(getItemsToBuild().collectEntries { project, config ->
                        return [
                            (project): {
                                dir(project) {
                                    sonarScan project: GITHUB_REPOSITORY, language: 'ts'
                                }
                            }
                        ]
                    })
                }
            }
        }
        stage('Create a Release') {
            when {
                anyOf {
                    branch 'master'
                    expression { env.GITHUB_COMMENT =~ 'build docker' }
                }
            }
            steps {
                script {
                    parallel(getProjectsToBuild().collectEntries { project, config ->
                        return [
                            (project): {
                                def buildDockerImage = config.get('buildDockerImage', true)

                                if (buildDockerImage) {
                                    def credentials = config['credentials'] ?: []
                                    def dockerBuildSecrets = config['dockerBuildSecrets'] ?: null

                                    withCredentials(credentials) {
                                        dockerToEcr awsRegions: AWS_REGIONS,
                                            ecrAccountId: ECR_ACCOUNT_ID,
                                            imageName: config['ecrRepo'],
                                            imageTag: env.GIT_COMMIT,
                                            dockerBuildContext: project,
                                            dockerBuildFile: "${project}/Dockerfile",
                                            dockerBuildTarget: 'deploy',
                                            dockerBuildArgs: ["GIT_HASH": GIT_HASH],
                                            dockerBuildSecrets: dockerBuildSecrets
                                    }
                                }
                            }
                        ]
                    })
                }
            }
        }
        stage('Scan Docker Images') {
            when {
                anyOf {
                    branch 'master'
                    expression { env.GITHUB_COMMENT =~ 'build docker' }
                }
            }
            steps {
                script {
                    parallel(getProjectsToBuild().collectEntries { project, config ->
                        return [
                            (project): {
                                def buildDockerImage = config.get('buildDockerImage', true)

                                if (buildDockerImage) {
                                    dockerScan awsRegion: AWS_REGIONS[0],
                                        ecrAccountId: ECR_ACCOUNT_ID,
                                        imageName: config['ecrRepo'],
                                        imageTag: env.GIT_COMMIT,
                                        vulnerabilitiesToIgnore: config.get('vulnerabilitiesToIgnore', []),
                                        failBuild: config.get('dockerScanFailBuild', false)
                                }
                            }
                        ]
                    })
                }
            }
        }
        stage('Deploy to QA') {
            when {
                anyOf {
                    branch 'master'
                    expression { env.GITHUB_COMMENT =~ 'build docker' }
                }
            }
            steps {
                script {
                    parallel(getServicesToDeploy().collectEntries { service, config ->
                        return [
                            (service): {
                                def deployToQA = config.get('deployToQA', true)

                                if (deployToQA) {
                                    def containerName = config.get('containerName', service)
                                    def awsAccount = config.get('qaAccount', 'qa')
                                    def accountConfig = AWS_ACCOUNTS[awsAccount]
                                    def projectConfig = PROJECTS[config['project']]

                                    fargateDeploy environment: 'qa',
                                        awsRegions: AWS_REGIONS,
                                        gitCommit: env.GIT_COMMIT,
                                        serviceName: service,
                                        containerName: containerName,
                                        imageNameOverride: projectConfig['ecrRepo'],
                                        ecrRegistryAccountId: ECR_ACCOUNT_ID,
                                        awsDeploymentTargetAccountId: accountConfig['accountId'],
                                        awsDeploymentRoleName: accountConfig['deploymentRole']
                                } else {
                                    echo "Not deploying ${service} to QA as this is explicitly disabled in the Jenkinsfile"
                                }
                            }
                        ]
                    })
                }
            }
        }
        stage('Deploy to Prod') {
            when {
                allOf {
                    branch 'master'
                    expression { params.DEPLOY_TO_PROD }
                }
            }
            steps {
                script {
                    parallel(getServicesToDeploy().collectEntries { service, config ->
                        return [
                            (service): {
                                def deployToProd = config.get('deployToProd', true)

                                if (deployToProd) {
                                    def containerName = config.get('containerName', service)
                                    def awsAccount = config.get('prodAccount', 'prod')
                                    def accountConfig = AWS_ACCOUNTS[awsAccount]
                                    def projectConfig = PROJECTS[config['project']]

                                    fargateDeploy environment: 'prod',
                                        awsRegions: AWS_REGIONS,
                                        gitCommit: env.GIT_COMMIT,
                                        serviceName: service,
                                        containerName: containerName,
                                        imageNameOverride: projectConfig['ecrRepo'],
                                        ecrRegistryAccountId: ECR_ACCOUNT_ID,
                                        awsDeploymentTargetAccountId: accountConfig['accountId'],
                                        awsDeploymentRoleName: accountConfig['deploymentRole']
                                } else {
                                    echo "Not deploying ${service} to prod as this is explicitly disabled in the Jenkinsfile"
                                }
                            }
                        ]
                    })
                    datadogSoftwareCatalogPublish(servicePath: '.')
                }
            }
        }
        stage('Publish NPM Package') {
            when {
                anyOf {
                    allOf {
                        branch 'master'
                        expression { params.PUBLISH_NPM_PACKAGE }
                    }
                    expression { env.GITHUB_COMMENT =~ 'publish package' }
                }
            }
            steps {
                script {
                    parallel(getPackagesToPublish().collectEntries { pkg, config ->
                        return [
                            (pkg): {
                                def publishNpmPackage = config.get('publishNpmPackage', false)

                                if (publishNpmPackage) {
                                    withCredentials([
                                        string(credentialsId: 'github_packages_token', variable: 'GITHUB_NPM_TOKEN')
                                    ]) {
                                        nodeSh(
                                            envVars: [
                                                GITHUB_NPM_TOKEN: "\${GITHUB_NPM_TOKEN}",
                                                PG_HOST: 'localhost',
                                                PG_PORT: '5432',
                                                PG_DATABASE: 'placeholder',
                                                PG_USER: 'placeholder',
                                                PG_PASSWORD: 'placeholder',
                                            ],
                                            nodeVersion: '22',
                                            script: """
                                                set -e
                                                cd ${pkg}/
                                                npm ci
                                                npm run build
                                                npm publish
                                            """
                                        )
                                    }
                                } else {
                                    echo "Not publishing ${pkg} to GitHub NPM registry as this is explicitly disabled in the Jenkinsfile"
                                }
                            }
                        ]
                    })
                }
            }
        }
    }

    post {
        cleanup {
            cleanWs()
        }
    }
}

def getProjectsToBuild() {
    if (this.@projectsToBuild != null) {
        return this.@projectsToBuild
    }

    if (params.SERVICE_NAME) {
        def service = SERVICES[params.SERVICE_NAME]
        this.@projectsToBuild = PROJECTS.findAll { it.key == service['project'] }
    } else if (params.PROJECT_NAME) {
        this.@projectsToBuild = PROJECTS.findAll { it.key == params.PROJECT_NAME }
    } else {
        def modifiedPaths = getModifiedPaths(
            paths: PROJECTS.keySet()
        )
        this.@projectsToBuild = PROJECTS.findAll { modifiedPaths.contains(it.key) }
    }

    return this.@projectsToBuild
}

def getServicesToDeploy() {
    if (this.@servicesToDeploy != null) {
        return this.@servicesToDeploy
    }

    if (params.SERVICE_NAME) {
        this.@servicesToDeploy = SERVICES.findAll { it.key == params.SERVICE_NAME }
    } else {
        def projectsToBuild = getProjectsToBuild().keySet()
        this.@servicesToDeploy = SERVICES.findAll { projectsToBuild.contains(it.value.project) }
    }

    if (this.@servicesToDeploy) {
        currentBuild.description = this.@servicesToDeploy.keySet().join('<br>')
    }

    return this.@servicesToDeploy
}

def getPackagesToPublish() {
    if (this.@packagesToPublish != null) {
        return this.@packagesToPublish
    }

    if (params.PACKAGE_NAME) {
        this.@packagesToPublish = PACKAGES.findAll { it.key == params.PACKAGE_NAME }
    } else {
        def modifiedPaths = getModifiedPaths(
            paths: PACKAGES.keySet()
        )
        this.@packagesToPublish = PACKAGES.findAll { modifiedPaths.contains(it.key) }
    }

    return this.@packagesToPublish
}

def getItemsToBuild() {
    return getProjectsToBuild().keySet() + getPackagesToPublish().keySet()
}
