@groovy.transform.Field String GITHUB_REPOSITORY = 'lambda-moneyhub'
@groovy.transform.Field String ECR_ACCOUNT_ID = '086679231553'
@groovy.transform.Field List<String> AWS_REGIONS = ['us-east-1']
@groovy.transform.Field String SLACK_NOTIFICATIONS_CHANNEL = '#moneyhub-alerts'
@groovy.transform.Field String QA_ACCOUNT_ID = '437795906767'
@groovy.transform.Field String QA_DEPLOYMENT_ROLE = 'prod-jenkins-aws-pipeline-agent'
@groovy.transform.Field String PROD_ACCOUNT_ID = '437795906767'
@groovy.transform.Field String PROD_DEPLOYMENT_ROLE = 'prod-jenkins-aws-pipeline-agent'
@groovy.transform.Field String BUILD_ROLE = 'qa-lambda-moneyhub-integration-test-role'
@groovy.transform.Field List<String> INTEGRATION_TESTS_FUNCTIONS = ['custom_reports', 'generate_attachments']
@groovy.transform.Field Boolean integrationTestsRan = false
@groovy.transform.Field Boolean rieTestsRan = false
@groovy.transform.Field List<String> RIE_TEST_ENV = [
    'SNOWFLAKE_ACCOUNT=sme-delphi',
    'SNOWFLAKE_DATABASE=ROYALTY_ACCOUNTING',
    'SNOWFLAKE_SCHEMA=QA',
    'SNOWFLAKE_ROLE=ROYALTYACCOUNTING_DB_QA_SCHEMA_READWRITE',
    'SNOWFLAKE_WAREHOUSE=QA_ETL_WAREHOUSE',
    'SNOWFLAKE_USER=LAMBDA_ROYALTY_ACCOUNTING',
    'OWS_PROFILE_TYPE=AbacusProfile',
    'OWS_PROFILE_ID=87918',
]

LAMBDA_ALIAS_MAP = [
    'custom_reports': '',
    'generate_attachments': '',
    'internal_attachments': 'provisioned',
]

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: 'RUN_RIE_TESTS', defaultValue: false, description: 'Run integration tests against a locally-built lambda image (RIE) without deploying to QA.')
        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|run rie tests).*')
    }

    stages {
        stage('Load Shared Libraries') {
            steps {
                library "jenkins-global-libraries@${params.SHARED_LIBRARIES_VERSION}"
            }
        }

        stage('Compliance Checks') {
            steps {
                script {
                    withModifiedFunctions(checkout: true) { functionName ->
                        echo "Running Compliance Checks for ${functionName}"
                        dir("lambda/${functionName}") {
                            complianceChecks()
                        }
                    }
                }
            }
        }

        stage('Validate Software Catalog Definitions') {
            steps {
                script {
                    withModifiedFunctions(checkout: true) { functionName ->
                        echo "Validating Software Catalog definition for ${functionName}"
                        datadogSoftwareCatalogValidate(servicePath: "lambda/${functionName}")
                    }
                }
            }
        }

        stage('Unit Tests and Style Checks') {
            steps {
                script {
                    withModifiedFunctions(checkout: true) { functionName ->
                        echo "Running Unit Tests and Style Checks for ${functionName}"
                        dir("lambda/${functionName}") {
                            withEcr {
                                sh "docker compose run --rm --build lint-and-test"
                            }
                        }
                    }
                }
            }
        }

        stage('RIE Integration Tests') {
            when {
                anyOf {
                    expression { params.RUN_RIE_TESTS }
                    expression { env.GITHUB_COMMENT =~ 'run rie tests' }
                }
            }
            steps {
                script {
                    runRieIntegrationTests()
                }
            }
            post {
                always {
                    script {
                        if (rieTestsRan) {
                            publishHTML([
                                reportName: 'RIE Integration Tests Report',
                                reportDir: 'tests/report',
                                reportFiles: 'report_rie_*.html',
                                keepAll: true,
                                alwaysLinkToLastBuild: false,
                                allowMissing: true
                            ])
                        }
                    }
                }
                failure {
                    echo "\nFOLLOW THIS LINK TO DOWNLOAD AND VIEW TEST REPORTS:"
                    echo "${env.JOB_URL}${env.BUILD_NUMBER}/RIE_20Integration_20Tests_20Report/*zip*/RIE_20Integration_20Tests_20Report.zip"
                    echo "------------------------------------\n\n"
                }
            }
        }

        stage('Static Application Security Tests') {
            steps {
                script {
                    withModifiedFunctions(checkout: true) { functionName ->
                        echo "Running Static App Security Tests for ${functionName}"
                        sastTests(projectDir: "lambda/${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 {
                    withModifiedFunctions(checkout: true) { functionName ->
                        echo "Building for ${functionName}"
                        dockerToEcr awsRegions: AWS_REGIONS,
                            ecrAccountId: ECR_ACCOUNT_ID,
                            imageName: "lambda-moneyhub-${functionName.replaceAll('_', '-')}",
                            imageTag: env.GIT_COMMIT,
                            dockerBuildContext: "lambda/${functionName}",
                            dockerBuildFile: "lambda/${functionName}/Dockerfile"
                    }
                }
            }
        }

        stage('Scan Docker Images') {
            when {
                anyOf {
                    branch 'master'
                    expression { env.GITHUB_COMMENT =~ 'build docker' }
                }
            }
            steps {
                script {
                    withModifiedFunctions(checkout: true) { functionName ->
                        echo "Scanning Docker Images for ${functionName}"
                        dir("lambda/${functionName}") {
                            dockerScan awsRegion: AWS_REGIONS[0],
                                ecrAccountId: ECR_ACCOUNT_ID,
                                imageName: "lambda-moneyhub-${functionName.replaceAll('_', '-')}",
                                imageTag: env.GIT_COMMIT,
                                vulnerabilitiesToIgnore: [
                                    // vulnerabilities with no new version available
                                    'CVE-2025-5222', // genrb
                                    'CVE-2025-22871',
                                    'CVE-2025-6965', // sqlite
                                    'CVE-2025-47907', // aws-lambda-rie
                                    'CVE-2025-61726', // aws-lambda-rie
                                    'CVE-2025-58188', //pkg:generic/aws-lambda-rie?go_toolchain=1.22.7
                                    'CVE-2025-61725',//pkg:generic/aws-lambda-rie?go_toolchain=1.22.7
                                    'CVE-2025-64720', // requires updated Amazon Linux 2 base image
                                    'CVE-2025-61729', // requires updated Lambda base image
                                    'CVE-2025-13151', // libtasn1 (requires updated Lambda base image)
                                    // temporarily ignoring
                                    'CVE-2025-22871',
                                    'CVE-2025-58188',
                                    'CVE-2025-58187',
                                    'CVE-2025-47907',
                                    'CVE-2025-61723',
                                    'CVE-2025-61725',
                                    'CVE-2025-24528', // krb5-libs (requires Amazon Linux 2 base image update)
                                    'CVE-2026-23949', // jaraco.context (transitive dependency, pinned but not overriding)
                                    'CVE-2025-68973', // gnupg vulnerability
                                    'CVE-2025-68121', // pkg:generic/aws-lambda-rie?go_toolchain=1.25.6 requires update in upstream
                                    'CVE-2026-25679', // go_toolchain 1.25.7 - requires new agent release
                                    'CVE-2026-27459', // pyOpenSSL - requires new agent release
                                    'CVE-2026-25646', // pkg:rpm/amazon/libpng@1.5.13-8.amzn2.0.5
                                    'CVE-2026-27135', // pkg:rpm/amazon/libnghttp2@1.41.0-1.amzn2.0.5
                                    'CVE-2026-33416', // pkg:rpm/amazon/libpng@1.5.13-8.amzn2.0.5
                                    'CVE-2026-25087', // pkg: pyarrow requires update to Python 3.12 or above
                                    'IN-DISCONTINUED-001', // Amazon Linux 2 reached end of support - requires updated Lambda base image
                                ]
                        }
                    }
                }
            }
        }

        stage('Deploy to QA') {
            when {
                branch 'master'
            }
            steps {
                script {
                    withModifiedFunctions(checkout: true) { functionName ->
                        echo "Deploying to QA for ${functionName}"
                        dir("lambda/${functionName}") {
                            lambdaDeploy environment: 'qa',
                                awsRegions: AWS_REGIONS,
                                imageTag: env.GIT_COMMIT,
                                imageName: "lambda-moneyhub-${functionName.replaceAll('_', '-')}",
                                ecrRegistryAccountId: ECR_ACCOUNT_ID,
                                awsDeploymentTargetAccountId: QA_ACCOUNT_ID,
                                awsDeploymentRoleName: QA_DEPLOYMENT_ROLE,
                                aliasName: LAMBDA_ALIAS_MAP.get(functionName),
                                publishVersion: LAMBDA_ALIAS_MAP.get(functionName) as Boolean
                        }
                    }
                }
            }
        }

        stage('Lambda Integration Tests') {
            when {
                branch 'master'
            }
            steps {
                script {
                    runLambdaIntegrationTests()
                }
            }
            post {
                always {
                    script {
                        if (integrationTestsRan) {
                            publishHTML([
                                reportName: "Integration Tests Report",
                                reportDir: 'tests/report',
                                reportFiles: "report_*.html",
                                keepAll: true,
                                alwaysLinkToLastBuild: false,
                                allowMissing: true
                            ])
                        }
                    }
                }
                failure {
                    echo "\nFOLLOW THIS LINK TO DOWNLOAD AND VIEW TEST REPORTS:"
                    echo "${env.JOB_URL}${env.BUILD_NUMBER}/Integration_20Tests_20Report/*zip*/Integration_20Tests_20Report.zip"
                    echo "------------------------------------\n\n"
                }
            }
        }

        stage('Deploy to PROD') {
            when {
                allOf {
                    branch 'master'
                    expression { params.DEPLOY_TO_PROD }
                }
            }
            steps {
                script {
                    withModifiedFunctions(checkout: true) { functionName ->
                        echo "Deploying to PROD for ${functionName}"
                        dir("lambda/${functionName}") {
                            lambdaDeploy environment: 'prod',
                                awsRegions: AWS_REGIONS,
                                imageTag: env.GIT_COMMIT,
                                imageName: "lambda-moneyhub-${functionName.replaceAll('_', '-')}",
                                ecrRegistryAccountId: ECR_ACCOUNT_ID,
                                awsDeploymentTargetAccountId: PROD_ACCOUNT_ID,
                                awsDeploymentRoleName: PROD_DEPLOYMENT_ROLE,
                                aliasName: LAMBDA_ALIAS_MAP.get(functionName),
                                publishVersion: LAMBDA_ALIAS_MAP.get(functionName) as Boolean
                        }
                        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
                }
            }
        }
    }
}

def runRieIntegrationTests() {
    def functionsToTest = getIntegrationTestFunctions()
    if (!functionsToTest) {
        echo "Skipping RIE tests: ${params.LAMBDA_FUNCTION_NAME} has no RIE tests"
        return
    }

    withEnv(RIE_TEST_ENV) {
        dir('tests') {
            sh 'make clean_html_report'
        }
        parallel(functionsToTest.collectEntries { functionName ->
            [(functionName): {
                // Pre-build with Jenkins agent ECR credentials, before
                // withAWS switches to BUILD_ROLE which may not have cross-account ECR access
                withEcr {
                    dir('tests') {
                        sh "docker compose --profile ${functionName} --project-name rie-${functionName} build"
                    }
                }
                withAWS(roleAccount: QA_ACCOUNT_ID, role: BUILD_ROLE, roleSessionName: env.BUILD_TAG, useNode: true) {
                    echo "Running RIE integration tests for ${functionName}"
                    dir('tests') {
                        try {
                            rieTestsRan = true
                            sh "make docker_rie_test function=${functionName} BUILD_FLAG=--no-build"
                        } finally {
                            sh "docker compose --profile ${functionName} --project-name rie-${functionName} down --remove-orphans || true"
                        }
                    }
                }
            }]
        })
    }
}

def runLambdaIntegrationTests() {
    def functionsToTest = getIntegrationTestFunctions()
    if (!functionsToTest) {
        echo "Skipping integration tests: ${params.LAMBDA_FUNCTION_NAME} has no integration tests"
        return
    }

    dir('tests') {
        sh 'make clean_html_report'
    }
    parallel(functionsToTest.collectEntries { functionName ->
        [(functionName): {
            withEcr {
                withAWS(roleAccount: QA_ACCOUNT_ID, role: BUILD_ROLE, roleSessionName: env.BUILD_TAG, useNode: true) {
                    echo "Running integration tests for ${functionName}"
                    dir('tests') {
                        try {
                            integrationTestsRan = true
                            sh "make docker_test_integration function=${functionName}"
                        } finally {
                            sh "docker compose --project-name ${functionName} down --remove-orphans"
                        }
                    }
                }
            }
        }]
    })
}

def getIntegrationTestFunctions() {
    if (params.LAMBDA_FUNCTION_NAME) {
        return [params.LAMBDA_FUNCTION_NAME].findAll { INTEGRATION_TESTS_FUNCTIONS.contains(it) }
    }
    def matcher = (env.GITHUB_COMMENT ?: '') =~ /run rie tests (\w+)/
    if (matcher) {
        return [matcher[0][1]].findAll { INTEGRATION_TESTS_FUNCTIONS.contains(it) }
    }
    matcher = null  // Matcher is not serializable; null before CPS checkpoint
    def modifiedLambdas = getMonorepoUtils().projectsToBuild
    if (modifiedLambdas) {
        return modifiedLambdas.findAll { INTEGRATION_TESTS_FUNCTIONS.contains(it) }
    }
    return INTEGRATION_TESTS_FUNCTIONS
}

def withModifiedFunctions(Map args = [:], Closure steps) {
    getMonorepoUtils(params.LAMBDA_FUNCTION_NAME ? [params.LAMBDA_FUNCTION_NAME] as Set : null).withModifiedProjects(args, steps)
}

def getMonorepoUtils(Set projectsToBuild = null) {
    return library("jenkins-global-libraries@${params.SHARED_LIBRARIES_VERSION}").com.sonymusic.MonorepoUtils.getInstance(
        steps: this,
        projectBasePath: 'lambda',
        projectsToBuild: projectsToBuild
    )
}
