String GITHUB_REPOSITORY = 'lambda-auth0-m2m'
String ECR_ACCOUNT_ID = '086679231553'
List<String> AWS_REGIONS = ['us-east-1']
String SLACK_NOTIFICATIONS_CHANNEL = '#permissions-platform-alerts'
String QA_ACCOUNT_ID = '591204808501'
String QA_DEPLOYMENT_ROLE = 'qa-jenkins-pipeline-deploy-role'
String PROD_ACCOUNT_ID = '031099521156'
String PROD_DEPLOYMENT_ROLE = 'prod-jenkins-aws-pipeline-agent'


/**
 * Map of AWS accounts in which lambda-auth0-m2m functions are deployed.
 *
 * Configuration values:
 *  - accountId (required): The AWS account ID
 *  - deploymentRole (required): The role to use to deploy services in that account
 */
AWS_ACCOUNTS = [
    'orcd-permissions-platform-qa'  : [
        accountId: '591204808501',
        deploymentRole: 'qa-jenkins-pipeline-deploy-role'
    ],
    'orcd-permissions-platform-prod': [
        accountId: '031099521156',
        deploymentRole: 'prod-jenkins-pipeline-deploy-role'
    ],
    'orcd-prod': [
        accountId: '437795906767',
        deploymentRole: 'prod-jenkins-aws-pipeline-agent'
    ],
    'orcd-fansifter-qa': [
        accountId: '285943604611',
        deploymentRole: 'qa-jenkins-pipeline-deploy-role'
    ],
    'orcd-fansifter-prod': [
        accountId: '737325105821',
        deploymentRole: 'prod-jenkins-pipeline-deploy-role'
    ],
    'orcd-songwhip-qa': [
        accountId: '619719722105',
        deploymentRole: 'qa-songwhip-jenkins-pipeline-deploy-role'
    ],
    'orcd-songwhip-prod': [
        accountId: '926734670777',
        deploymentRole: 'prod-songwhip-jenkins-pipeline-deploy-role'
    ],
    'orcd-abacus-qa': [
        accountId: '989790945997',
        deploymentRole: 'qa-jenkins-pipeline-deploy-role'
    ],
    'orcd-abacus-prod': [
        accountId: '375914681009',
        deploymentRole: 'prod-jenkins-pipeline-deploy-role'
    ],
    'orcd-abacus-uat': [
        accountId: '989790945997',
        deploymentRole: 'uat-jenkins-pipeline-deploy-role'
    ],
    'orcd-supply-chain-qa': [
        accountId: '311141540202',
        deploymentRole: 'qa-jenkins-pipeline-deploy-role'
    ],
    'orcd-supply-chain-prod': [
        accountId: '710271914708',
        deploymentRole: 'prod-jenkins-pipeline-deploy-role'
    ],
    'orcd-ecommerce-qa': [
        accountId: '811473064144',
        deploymentRole: 'qa-jenkins-pipeline-deploy-role'
    ],
    'orcd-ecommerce-prod': [
        accountId: '997286397029',
        deploymentRole: 'prod-jenkins-pipeline-deploy-role'
    ]
]

/**
 * Map of functions names to their configuration.
 *
 * Configuration values:
 *  - dir (required): Path to the lambda function's directory
 *  - prodAccounts (required): The name of the production accounts for this service. This must map to one of the accounts defined in the ACCOUNTS mapping above.
 *  - qaAccounts (required): The name of the QA accounts for this service. This must map to one of the accounts defined in the ACCOUNTS mapping above.
 */
FUNCTIONS = [
    'auth0_m2m_token_secret_rotation': [
        dir: 'lambda/auth0_m2m_token_secret_rotation',
        qaAccounts: [
            'orcd-permissions-platform-qa',
            'orcd-fansifter-qa',
            'orcd-songwhip-qa',
            'orcd-abacus-qa',
            'orcd-prod',
            'orcd-supply-chain-qa',
            'orcd-ecommerce-qa',
        ],
        prodAccounts: [
            'orcd-permissions-platform-prod',
            'orcd-fansifter-prod',
            'orcd-songwhip-prod',
            'orcd-abacus-prod',
            'orcd-prod',
            'orcd-supply-chain-prod',
            'orcd-ecommerce-prod',
        ],
        uatAccounts: [
            'orcd-abacus-uat',
        ],
        isolatedIntegrationTest: [
            account: 'orcd-permissions-platform-qa',
            environment: 'test',
            testRole: 'test-lambda-auth0-m2m-token-secret-rotation-integration-test',
        ]
    ],
]

@groovy.transform.Field
Map functionsToBuild = null

pipeline {
    agent any

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

    parameters {
        booleanParam(name: 'DEPLOY_TO_PROD', defaultValue: true, description: 'Whether or not to deploy to prod.')
        booleanParam(name: 'DEPLOY_TO_UAT', defaultValue: false, description: 'Whether or not to deploy to uat.')
        booleanParam(name: 'PUBLISH_NEW_VERSION', defaultValue: false, description: 'Whether to publish a new version of the lambda function(s)')
        string(name: 'LAMBDA_FUNCTION_NAME', defaultValue: '', description: 'Name of the Lambda function to build and deploy')
        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(getFunctionsToBuild().collectEntries { functionName, config ->
                        return [
                            (functionName): {
                                echo "Running Compliance Checks for ${functionName}"
                                dir(config['dir']) {
                                    complianceChecks()
                                }
                            }
                        ]
                    })
                }
            }
        }

        stage('Validate Software Catalog Definitions') {
            steps {
                script {
                    parallel(getFunctionsToBuild().collectEntries { functionName, config ->
                        return [
                            (functionName): {
                                echo "Validating Software Catalog definition for ${functionName}"
                                datadogSoftwareCatalogValidate(servicePath: config['dir'])
                            }
                        ]
                    })
                }
            }
        }

        stage('Unit Tests and Style Checks') {
            steps {
                script {
                    parallel(getFunctionsToBuild().collectEntries { functionName, config ->
                        return [
                            (functionName): {
                                echo "Running Unit Tests and Style Checks for ${functionName}"
                                dir(config['dir']) {
                                    withEcr {
                                        sh "docker compose run --rm --build lint-and-test"
                                    }
                                }
                            }
                        ]
                    })
                }
            }
            post {
                cleanup {
                    script {
                        parallel(getFunctionsToBuild().collectEntries { functionName, config ->
                            return [
                                (functionName): {
                                    echo "Shutting down ${functionName} docker container"
                                    dir(config['dir']) {
                                        sh "docker compose down -v"
                                    }
                                }
                            ]
                        })
                    }
                }
            }
        }

        stage('Static Application Security Tests') {
            steps {
                script {
                    parallel(getFunctionsToBuild().collectEntries { functionName, config ->
                        return [
                            (functionName): {
                                echo "Running Static App Security Tests for ${functionName}"
                                sastTests(projectDir: config['dir'])
                            }
                        ]
                    })
                }
            }
        }

        stage('Sonar Scan and Analysis') {
            when {
                branch 'master'
            }
            steps {
                script {
                    echo "Running Sonar scan for ${GITHUB_REPOSITORY}"
                    sonarScan project: GITHUB_REPOSITORY, language: 'py'
                }
            }
        }

        stage('Create a Release') {
            when {
                anyOf {
                    branch 'master'
                    expression { env.GITHUB_COMMENT =~ 'build docker' }
                }
            }
            steps {
                script {
                    parallel(getFunctionsToBuild().collectEntries { functionName, config ->
                        return [
                            (functionName): {
                                echo "Building for ${functionName}"
                                dockerToEcr awsRegions: AWS_REGIONS,
                                    ecrAccountId: ECR_ACCOUNT_ID,
                                    imageName: "lambda-${functionName.replaceAll('_', '-')}",
                                    imageTag: env.GIT_COMMIT,
                                    dockerBuildContext: config['dir'],
                                    dockerBuildFile: "${config['dir']}/Dockerfile",
                                    dockerBuildTarget: 'deploy'
                            }
                        ]
                    })
                }
            }
        }

        stage('Isolated Integration Tests') {
            when {
                branch 'master'
            }
            steps {
                script {
                    parallel(getFunctionsToBuild().collectEntries { functionName, config ->
                        return [
                            (functionName): {
                                isolatedTestConfig = config['isolatedIntegrationTest']
                                if (isolatedTestConfig != null) {
                                    echo "Preparing to run isolated integration test on ${functionName} in ${isolatedTestConfig['account']}"

                                    awsAccountConfig = AWS_ACCOUNTS[isolatedTestConfig['account']]
                                    echo "Deploying ${isolatedTestConfig['environment']} ${functionName} to ${isolatedTestConfig['account']} (${awsAccountConfig['accountId']}) with ${awsAccountConfig['deploymentRole']}"

                                    lambdaDeploy environment: 'test',
                                        awsRegions: AWS_REGIONS,
                                        imageTag: env.GIT_COMMIT,
                                        imageName: "lambda-${functionName.replaceAll('_', '-')}",
                                        ecrRegistryAccountId: ECR_ACCOUNT_ID,
                                        awsDeploymentTargetAccountId: awsAccountConfig['accountId'],
                                        awsDeploymentRoleName: awsAccountConfig['deploymentRole'],
                                        publishVersion: params.PUBLISH_NEW_VERSION

                                    dir(config['dir']) {
                                        testRole = isolatedTestConfig['testRole']
                                        echo "Running integration test against isolated test environment as ${testRole}"
                                        withAWS(role: testRole, roleAccount: awsAccountConfig['accountId'], roleSessionName: testRole, useNode: true) {
                                            sh "make ci_test_integration"
                                        }
                                    }
                                }
                            }
                        ]
                    })
                }
            }
            post {
                cleanup {
                    script {
                        parallel(getFunctionsToBuild().collectEntries { functionName, config ->
                            return [
                                (functionName): {
                                    echo "Shutting down ${functionName} docker container"
                                    dir(config['dir']) {
                                        sh "docker compose down -v"
                                    }
                                }
                            ]
                        })
                    }
                }
            }
        }

        stage('Deploy to QA') {
            when {
                branch 'master'
            }
            steps {
                script {
                    parallel(
                        getFunctionsToBuild().collectEntries { functionName, config ->
                            return [
                                (functionName): {
                                    def awsAccounts = config.get('qaAccounts', [])
                                    for (int i = 0; i < awsAccounts.size(); i++) {
                                        def awsAccount = awsAccounts[i]
                                        dir(config['dir']) {
                                            def awsAccountConfig = AWS_ACCOUNTS[awsAccount]
                                            echo "Deploying ${functionName} to ${awsAccount} (${awsAccountConfig['accountId']}) with ${awsAccountConfig['deploymentRole']}"
                                            lambdaDeploy environment: 'qa',
                                                awsRegions: AWS_REGIONS,
                                                imageTag: env.GIT_COMMIT,
                                                imageName: "lambda-${functionName.replaceAll('_', '-')}",
                                                ecrRegistryAccountId: ECR_ACCOUNT_ID,
                                                awsDeploymentTargetAccountId: awsAccountConfig['accountId'],
                                                awsDeploymentRoleName: awsAccountConfig['deploymentRole'],
                                                publishVersion: params.PUBLISH_NEW_VERSION
                                        }
                                    }
                                }
                            ]
                        }
                    )
                }
            }
        }

        stage('Scan Docker Images') {
            when {
                anyOf {
                    branch 'master'
                    expression { env.GITHUB_COMMENT =~ 'build docker' }
                }
            }
            steps {
                script {
                    parallel(getFunctionsToBuild().collectEntries { functionName, config ->
                        return [
                            (functionName): {
                                echo "Scanning Docker Images for ${functionName}"
                                dir(config['dir']) {
                                    dockerScan awsRegion: AWS_REGIONS[0],
                                        ecrAccountId: ECR_ACCOUNT_ID,
                                        imageName: "lambda-${functionName.replaceAll('_', '-')}",
                                        imageTag: env.GIT_COMMIT,
                                        vulnerabilitiesToIgnore: [
                                            'CVE-2025-22871', // present in the AWS lambda base image
                                        ]
                                }
                            }
                        ]
                    })
                }
            }
        }

        stage('Deploy to UAT') {
            when {
                allOf {
                    branch 'master'
                    expression { params.DEPLOY_TO_UAT }
                }
            }
            steps {
                script {
                    parallel(
                        getFunctionsToBuild().collectEntries { functionName, config ->
                            return [
                                (functionName): {
                                    def awsAccounts = config.get('uatAccounts', [])
                                    for (int i = 0; i < awsAccounts.size(); i++) {
                                        def awsAccount = awsAccounts[i]
                                        dir(config['dir']) {
                                            def awsAccountConfig = AWS_ACCOUNTS[awsAccount]
                                            echo "Deploying ${functionName} to ${awsAccount} (${awsAccountConfig['accountId']}) with ${awsAccountConfig['deploymentRole']}"
                                            lambdaDeploy environment: 'uat',
                                                awsRegions: AWS_REGIONS,
                                                imageTag: env.GIT_COMMIT,
                                                imageName: "lambda-${functionName.replaceAll('_', '-')}",
                                                ecrRegistryAccountId: ECR_ACCOUNT_ID,
                                                awsDeploymentTargetAccountId: awsAccountConfig['accountId'],
                                                awsDeploymentRoleName: awsAccountConfig['deploymentRole'],
                                                publishVersion: params.PUBLISH_NEW_VERSION
                                        }
                                    }
                                }
                            ]
                        }
                    )
                }
            }
        }

        stage('Deploy to PROD') {
            when {
                allOf {
                    branch 'master'
                    expression { params.DEPLOY_TO_PROD }
                }
            }
            steps {
                script {
                    parallel(getFunctionsToBuild().collectEntries { functionName, config ->
                        return [
                            (functionName): {
                            def awsAccounts = config.get('prodAccounts', [])
                                for (int i = 0; i < awsAccounts.size(); i++) {
                                    def awsAccount = awsAccounts[i]
                                    dir(config['dir']) {
                                        def awsAccountConfig = AWS_ACCOUNTS[awsAccount]
                                        echo "Deploying ${functionName} to ${awsAccount} (${awsAccountConfig['accountId']}) with ${awsAccountConfig['deploymentRole']}"
                                        lambdaDeploy environment: 'prod',
                                            awsRegions: AWS_REGIONS,
                                            imageTag: env.GIT_COMMIT,
                                            imageName: "lambda-${functionName.replaceAll('_', '-')}",
                                            ecrRegistryAccountId: ECR_ACCOUNT_ID,
                                            awsDeploymentTargetAccountId: awsAccountConfig['accountId'],
                                            awsDeploymentRoleName: awsAccountConfig['deploymentRole'],
                                            publishVersion: params.PUBLISH_NEW_VERSION
                                    }
                                }
                            }
                        ]
                    })
                }
            }
        }

        stage('Publish Software Catalog Definitions') {
            when {
                allOf {
                    branch 'master'
                    expression { params.DEPLOY_TO_PROD }
                }
            }
            steps {
                script {
                    parallel(getFunctionsToBuild().collectEntries { functionName, config ->
                        return [
                            (functionName): {
                                echo "Publishing Software Catalog definition for ${functionName}"
                                datadogSoftwareCatalogPublish(servicePath: config['dir'])
                            }
                        ]
                    })
                }
            }
        }
    }

    post {
        always {
            script {
                if (env.BRANCH_NAME == 'master') {
                    slackNotify channel: SLACK_NOTIFICATIONS_CHANNEL
                }
            }
        }
        cleanup {
            cleanWs()
        }
    }
}


/**
 * Returns a list of lambda function directories which should be
 * built as part of the Jenkinsfile pipeline
 *
 * A function should be built if:
 * 1. It was specified as the LAMBDA_FUNCTION_NAME parameter when kicking off the build
 * 2. As part of the commit being deployed, the lambda function directory had a change
 */
def getFunctionsToBuild() {
    if (this.@functionsToBuild != null) {
        return this.@functionsToBuild
    }

    // Initialize to empty
    this.@functionsToBuild = [:]

    def lambdaFunctionName = params.LAMBDA_FUNCTION_NAME ?: null

    if (lambdaFunctionName) {
        this.@functionsToBuild << FUNCTIONS.find{ it.key == lambdaFunctionName }
    } else {
        def functionPaths = FUNCTIONS.collect{ it.value['dir'] }
        def modifiedPaths = getModifiedPaths(paths: functionPaths)
        this.@functionsToBuild << FUNCTIONS.findAll{ modifiedPaths.contains(it.value['dir']) }
    }

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

    return this.@functionsToBuild
}
