String GITHUB_REPOSITORY = 'lambda-notifications'
String ECR_ACCOUNT_ID = '086679231553'
List<String> AWS_REGIONS = ['us-east-1']
String SLACK_NOTIFICATIONS_CHANNEL = '#notifications'
String ACCOUNT_ID = '437795906767'
String DEPLOYMENT_ROLE = 'prod-jenkins-aws-pipeline-agent'
String INTEGRATION_TEST_ROLE = 'qa-lambda-notifications-integration-test-role'
String SESSION_NAME = 'qa-lambda-notifications-integration-test'
String POEDITOR_PROJECT_ID = '337709'

@groovy.transform.Field
List<String> functionsToBuild = null

@groovy.transform.Field
String lambdaNamePrefix = 'lambda'


pipeline {
    agent {
        label 'aws'
    }

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

    parameters {
        booleanParam(name: 'DEPLOY_TO_PROD', defaultValue: true, description: 'Whether or not to deploy to prod.')
        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 {
					forEachFunction([:]) { functionName ->
						echo "Running Compliance Checks for ${functionName}"
						complianceChecks()
					}
				}
            }
        }

        stage('Validate Software Catalog Definition') {
            steps {
				script {
					forEachFunction([changeDir: false]) { functionName ->
						echo "Validating Software Catalog Definition for ${functionName}"
						datadogSoftwareCatalogValidate(servicePath: "lambda/${functionName}")
					}
				}
            }
        }

        stage('Unit Tests and Style Checks') {
            steps {
				script {
					forEachFunction([:]) { functionName ->
						echo "Running Unit Tests and Style Checks for ${functionName}"
						try {
							sh "docker compose run --rm --build lint-and-test"
						}
						finally {
							xunit tools: [JUnit(pattern: 'reports/pyunit.xml', deleteOutputFiles: true, failIfNotNew: true, stopProcessingIfError: true)]
                    		recordCoverage(tools: [[parser: 'COBERTURA', pattern: 'reports/coverage.xml']], enabledForFailure: true, failOnError: true, sourceDirectories: [[path: "src"]], id: "${normalizeFunctionName(functionName)}-coverage", name: "${functionName} Coverage Results")
						}
					}
				}
            }
        }

        stage('Static Application Security Tests') {
            steps {
				script {
					forEachFunction([:]) { functionName ->
						echo "Running Static App Security Tests for ${functionName}"
						sastTests(projectDir: functionName)
					}
				}
            }
        }

        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 {
					forEachFunction(changeDir: false) { functionName ->
						echo "Building image for ${functionName}"
						withCredentials([string(credentialsId: 'poeditor-api-token', variable: 'POEDITOR_API_TOKEN')]) {
							dockerToEcr awsRegions: AWS_REGIONS,
								ecrAccountId: ECR_ACCOUNT_ID,
								imageName: getImageName(functionName),
								imageTag: env.GIT_COMMIT,
								dockerBuildContext: "lambda/${functionName}",
								dockerBuildFile: "lambda/${functionName}/Dockerfile",
								dockerBuildTarget: 'main',
								dockerBuildArgs: [POEDITOR_PROJECT_ID: POEDITOR_PROJECT_ID],
								dockerBuildSecrets: [[id: 'poeditor-api-token', env: 'POEDITOR_API_TOKEN']]
						}
					}
				}
            }
        }

		stage('Scan Docker Images') {
            when {
                anyOf {
                    branch 'master'
                    expression { env.GITHUB_COMMENT =~ 'build docker' }
                }
            }
            steps {
				script {
					forEachFunction([changeDir: false]) { functionName ->
						echo "Scanning Docker Images for ${functionName}"
						dockerScan awsRegion: AWS_REGIONS[0],
							ecrAccountId: ECR_ACCOUNT_ID,
							imageName: getImageName(functionName),
							imageTag: env.GIT_COMMIT,
							failBuild: false
					}
				}
            }
        }

        stage('Deploy to QA') {
            when {
                branch 'master'
            }
            steps {
				script {
					forEachFunction([changeDir: false]) { functionName ->
						echo "Deploying to QA for ${functionName}"
						lambdaDeploy awsRegions: AWS_REGIONS,
							imageTag: env.GIT_COMMIT,
							imageName: getImageName(functionName),
							functionName: getLambdaName('qa', functionName),
							ecrRegistryAccountId: ECR_ACCOUNT_ID,
							awsDeploymentTargetAccountId: ACCOUNT_ID,
							awsDeploymentRoleName: DEPLOYMENT_ROLE
					}
				}
            }
        }

        stage('Integration Tests') {
			when {
                anyOf {
                    branch 'master'
                    expression { env.GITHUB_COMMENT =~ 'run integration test' }
                }
            }
            steps {
				script {
					forEachFunction([:]) { functionName ->
						echo "Running Integarion Tests for ${functionName}"
						withAWS(role: INTEGRATION_TEST_ROLE, roleAccount: ACCOUNT_ID, roleSessionName: SESSION_NAME, useNode: true) {
							sh "docker compose run --rm --build integration-test"
						}
					}
				}
			}
		}

        stage('Deploy to PROD') {
            when {
                allOf {
                    branch 'master'
                    expression { params.DEPLOY_TO_PROD }
                }
            }
            steps {
				script {
					forEachFunction([changeDir: false]) { functionName ->
						echo "Deploying to PROD for ${functionName}"
						lambdaDeploy awsRegions: AWS_REGIONS,
							imageTag: env.GIT_COMMIT,
							imageName: getImageName(functionName),
		                    functionName: getLambdaName('prod', functionName),
							ecrRegistryAccountId: ECR_ACCOUNT_ID,
							awsDeploymentTargetAccountId: ACCOUNT_ID,
							awsDeploymentRoleName: DEPLOYMENT_ROLE
						datadogSoftwareCatalogPublish(servicePath: "lambda/${functionName}")
					}
				}
            }
        }
    }

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

def getFunctionsToBuild() {
    if (this.@functionsToBuild != null) {
        return this.@functionsToBuild
    }

    def lambdaFunctionName = params.LAMBDA_FUNCTION_NAME ?: null

    if (lambdaFunctionName) {
        this.@functionsToBuild = [lambdaFunctionName]
    } else {
        def lambdaDirectories = findFiles(glob: '**/Dockerfile')
        this.@functionsToBuild = getModifiedFunctions branchName: env.BRANCH_NAME,
            previousSuccessfulCommit: env.GIT_PREVIOUS_SUCCESSFUL_COMMIT,
            lambdaDirectories: lambdaDirectories
    }

    if (this.@functionsToBuild) {
        currentBuild.description = this.@functionsToBuild.join(', ')
    }

    return this.@functionsToBuild
}

def normalizeFunctionName(String functionName) {
	return functionName.replaceAll('_', '-')
}

def getImageName(String functionName) {
	return "${this.@lambdaNamePrefix}-${normalizeFunctionName(functionName)}"
}

def getLambdaName(String environment, String functionName) {
	return "${environment}-${normalizeFunctionName(functionName)}"
}

def forEachFunction(Map args, Closure steps) {
	boolean changeDir = args.get('changeDir', true)
	def functionNames = getFunctionsToBuild()

    def parallelBranches = [:]

    functionNames.each { functionName ->
    	if (changeDir) {
			parallelBranches[functionName] = {
				dir("lambda/${functionName}") {
					steps(functionName)
				}
			}
		}
		else {
			parallelBranches[functionName] = {
				steps(functionName)
			}
		}
    }

    parallel parallelBranches
}
