GITHUB_REPOSITORY = 'code-push-server'
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 must map to a directory containing a Dockerfile.
 *
 * Configuration values:
 *  - ecrRepo (required): The ECR repo to publish the image to
 *  - credentials (optional): A list of Jenkins credentials to pass to the build
 *  - dockerBuildSecrets (optional): A list of secrets to pass to the Docker build.
 *  - 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 = [
    'api': [
        ecrRepo: 'code-push-server',
        vulnerabilitiesToIgnore: [],
        dockerScanFailBuild: false
    ],
]

/**
 * Map of service names to their configuration.
 *
 * Configuration values:
 *  - project (required): The project that this service is based on. This must map to one of the projects defined in the PROJECTS mapping above.
 *  - containerName (optional): The name of the main container in the task. Defaults to service name.
 *  - deployToProd (optional): Whether or not to deploy to prod. This takes precedence over the DEPLOY_TO_PROD parameter.
 *  - deployToQA (optional): Whether or not to deploy to QA.
 *  - integrationTestJob (optional): The name of an integration test job to run for this service.
 *  - integrationTestJobParameters (optional): A list of parameters to pass to the integration test job.
 *  - prodAccount (optional): The name of the production account for this service. This must map to one of the accounts defined in the ACCOUNTS mapping above.
 *  - qaAccount (optional): The name of the QA account for this service. This must map to one of the accounts defined in the ACCOUNTS mapping above.
 */
SERVICES = [
    'code-push-server' : [
        project: 'api',
        deployToProd: true,
        prodAccount: 'prod',
        deployToQA: true,
        qaAccount: 'qa',
        publishNpmPackage: false
    ],
]

/**
 * Map of packages to their configuration.
 *
 * Configuration values:
 * - publishNpmPackage (required): Whether or not to publish the NPM package.
 * - buildDockerImage (required): Whether or not to build the Docker image.
 */
PACKAGES = [
    'cli': [ 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 service in this connector type 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.*')
    }

    stages {
        stage('Load Shared Libraries') {
            steps {
                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: 'api')
            }
        }
        stage('Unit tests and Style Checks') {
            steps {
                script {
                    parallel(getItemsToBuild().collectEntries { project, config ->
                        return [
                            (project): {
                                dir(project) {
                                    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) {
                                        withEcr {
                                            sh 'docker compose down --volumes --remove-orphans'
                                        }
                                        publishHTML([
                                            allowMissing: true,
                                            alwaysLinkToLastBuild: false,
                                            icon: '',
                                            keepAll: true,
                                            reportDir: './mochawesome-report',
                                            reportFiles: 'mochawesome.html',
                                            reportName: 'mochawesome report',
                                            reportTitles: '',
                                            useWrapperFileDirectly: true
                                        ])
                                    }
                                }
                            ]
                        })
                    }
                }
            }
        }
        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',
                                            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) // Set this to false for now as there are a large number of pre-existing vulnerabilities
                                }
                            }
                        ]
                    })
                }
            }
        }
        stage('Deploy to QA') {
            when {
                branch 'master'
            }
            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: 'api')
                }
            }
        }
        stage('Publish NPM package') {
            when {
                allOf {
                    branch 'master'
                    expression { params.PUBLISH_NPM_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}",
                                            ],
                                            nodeVersion: '22',
                                            script: """
                                                    set -e
                                                    cd ${pkg}/
                                                    npm install
                                                    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) { // check ony if SERVICE_NAME is not provided.
        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()
}
