String GITHUB_REPOSITORY = 'lambda-vector'
String ECR_ACCOUNT_ID = '086679231553'
List<String> AWS_REGIONS = ['us-east-1']
String SLACK_NOTIFICATIONS_CHANNEL = '#distro-build-alerts'

AWS_DEPLOYMENT_TARGETS = [
    'prod': [
        'accountId': '437795906767',
        'deploymentRole': 'prod-jenkins-aws-pipeline-agent',
    ],
    'qa_supply_chain': [
        'accountId': '311141540202',
        'deploymentRole': 'qa-jenkins-pipeline-deploy-role',
    ],
]

FUNCTION_CONFIG = [
    'ar_to_ddb_sync': [
        type: 'lambda',
        vulnerabilitiesToIgnore: [],
        deploy: [
            qa: [
                awsDeploymentTargets: ['prod'],
                instances: ['lambda-vector-ar-to-ddb-sync'],
            ],
            prod: [
                awsDeploymentTargets: ['prod'],
                instances: ['lambda-vector-ar-to-ddb-sync'],
            ],
        ],
    ],
    'cleanup_efs': [
        type: 'lambda',
        vulnerabilitiesToIgnore: [],
        deploy: [
            qa: [
                awsDeploymentTargets: ['prod'],
                instances: ['lambda-vector-cleanup-efs-delivery'],
            ],
            prod: [
                awsDeploymentTargets: ['prod'],
                instances: [
                    'lambda-vector-cleanup-efs-delivery-audio-p1',
                    'lambda-vector-cleanup-efs-delivery-audio-p2',
                    'lambda-vector-cleanup-efs-delivery-audio-p3',
                    'lambda-vector-cleanup-efs-delivery-audio-p4',
                    'lambda-vector-cleanup-efs-delivery-audio-p5',
                    'lambda-vector-cleanup-efs-delivery-audio-p6',
                    'lambda-vector-cleanup-efs-delivery-video-p1',
                    'lambda-vector-cleanup-efs-delivery-video-p2',
                    'lambda-vector-cleanup-efs-delivery-video-p3',
                    'lambda-vector-cleanup-efs-delivery-video-p4',
                    'lambda-vector-cleanup-efs-delivery-video-p5',
                    'lambda-vector-cleanup-efs-delivery-video-p6',
                ],
            ],
        ],
    ],
    'close_batch': [
        type: 'lambda',
        vulnerabilitiesToIgnore: [],
        deploy: [
            qa: [
                awsDeploymentTargets: ['prod', 'qa_supply_chain'],
                instances: ['lambda-vector-close-batch'],
            ],
            prod: [
                awsDeploymentTargets: ['prod'],
                instances: ['lambda-vector-close-batch'],
            ],
        ],
    ],
    'ddb_streams_handler': [
        type: 'lambda',
        vulnerabilitiesToIgnore: [],
        deploy: [
            qa: [
                awsDeploymentTargets: ['prod', 'qa_supply_chain'],
                instances: ['lambda-vector-ddb-streams-handler'],
            ],
            prod: [
                awsDeploymentTargets: ['prod'],
                instances: ['lambda-vector-ddb-streams-handler'],
            ],
        ],
    ],
    'find_batch_to_close': [
        type: 'lambda',
        vulnerabilitiesToIgnore: [],
        deploy: [
            qa: [
                awsDeploymentTargets: ['prod', 'qa_supply_chain'],
                instances: ['lambda-vector-find-batch-to-close'],
            ],
            prod: [
                awsDeploymentTargets: ['prod'],
                instances: ['lambda-vector-find-batch-to-close'],
            ],
        ],
    ],
    'job_priority_rule_match': [
        type: 'lambda',
        vulnerabilitiesToIgnore: [],
        deploy: [
            qa: [
                awsDeploymentTargets: ['prod', 'qa_supply_chain'],
                instances: ['lambda-vector-job-priority-rule-match'],
            ],
            prod: [
                awsDeploymentTargets: ['prod'],
                instances: ['lambda-vector-job-priority-rule-match'],
            ],
        ],
    ],
    'queue_tagger': [
        type: 'lambda',
        vulnerabilitiesToIgnore: [],
        deploy: [
            qa: [
                awsDeploymentTargets: ['prod', 'qa_supply_chain'],
                instances: ['lambda-vector-queue-tagger'],
            ],
            prod: [
                awsDeploymentTargets: ['prod'],
                instances: ['lambda-vector-queue-tagger'],
            ],
        ],
    ],
    'ready_deliveries': [
        type: 'lambda',
        vulnerabilitiesToIgnore: [],
        deploy: [
            qa: [
                awsDeploymentTargets: ['prod'],
                instances: ['lambda-vector-ready-deliveries'],
            ],
            prod: [
                awsDeploymentTargets: ['prod'],
                instances: ['lambda-vector-ready-deliveries'],
            ],
        ],
    ],
    'ready_deliveries_manager': [
        type: 'lambda',
        vulnerabilitiesToIgnore: [],
        deploy: [
            qa: [
                awsDeploymentTargets: ['prod'],
                instances: ['lambda-vector-ready-deliveries-manager'],
            ],
            prod: [
                awsDeploymentTargets: ['prod'],
                instances: ['lambda-vector-ready-deliveries-manager'],
            ],
        ],
    ],
    'renew_manager': [
        type: 'task',
        basePath: 'fargate',
        deployType: 'UPDATE_SERVICE',
        verifyMode: 'TASK_RUNNING',
        dockerBuildTarget: 'deploy',
        vulnerabilitiesToIgnore: [],
        deploy: [
            qa: [
                awsDeploymentTargets: ['prod', 'qa_supply_chain'],
                instances: ['lambda-vector-renew-manager'],
            ],
            prod: [
                awsDeploymentTargets: ['prod'],
                instances: ['lambda-vector-renew-manager'],
            ],
        ],
    ],
    'save_to_dd_db': [
        type: 'lambda',
        vulnerabilitiesToIgnore: [],
        deploy: [
            qa: [
                awsDeploymentTargets: ['prod', 'qa_supply_chain'],
                instances: ['lambda-vector-save-to-dd-db'],
            ],
            prod: [
                awsDeploymentTargets: ['prod'],
                instances: ['lambda-vector-save-to-dd-db'],
            ],
        ],
    ],
    'throttler': [
        type: 'lambda',
        vulnerabilitiesToIgnore: [],
        deploy: [
            qa: [
                awsDeploymentTargets: ['prod', 'qa_supply_chain'],
                instances: ['lambda-vector-throttler'],
            ],
            prod: [
                awsDeploymentTargets: ['prod'],
                instances: ['lambda-vector-throttler'],
            ],
        ],
    ],
    'throttler_manager': [
        type: 'task',
        basePath: 'fargate',
        deployType: 'UPDATE_CLOUDWATCH_EVENT',
        verifyMode: 'OFF',
        dockerBuildTarget: 'deploy',
        vulnerabilitiesToIgnore: [],
        deploy: [
            qa: [
                awsDeploymentTargets: ['prod', 'qa_supply_chain'],
                instances: ['lambda-vector-throttler-manager'],
            ],
            prod: [
                awsDeploymentTargets: ['prod'],
                instances: ['lambda-vector-throttler-manager'],
            ],
        ],
    ],
]

pipeline {
    agent none

    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_NAMES',
            defaultValue: '',
            description: 'Comma-separated list of function/task directory names 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).'
        )
        booleanParam(
            name: 'VERIFY_BUILD',
            defaultValue: false,
            description: 'Force build and test step to verify health.'
        )
    }

    triggers {
        issueCommentTrigger('.*retest this please.*')
        parameterizedCron(env.BRANCH_NAME == 'master' ? '@weekly %DEPLOY_TO_PROD=false;VERIFY_BUILD=true;LAMBDA_FUNCTION_NAMES=*' : '')
    }

    stages {
        stage('Load Shared Libraries') {
            steps {
                library "jenkins-global-libraries@${params.SHARED_LIBRARIES_VERSION}"
            }
        }
        stage('Build Test and Scan') {
            when {
                expression { isPullRequest() || params.VERIFY_BUILD }
            }
            parallel {
                stage('Sonar Scan and Analysis') {
                    steps {
                        withModifiedFunctions(checkout: true) { functionName ->
                            script {
                                def config = FUNCTION_CONFIG[functionName]
                                def basePath = getBasePath(config)
                                echo "Running Sonar scan for ${functionName}"
                                sonarScan(
                                    project: "${GITHUB_REPOSITORY}-${sanitizeFunctionName(functionName)}",
                                    projectBaseDir: "${basePath}/${functionName}",
                                    language: 'py'
                                )
                            }
                        }
                    }
                }
                stage('Compliance Checks') {
                    agent any
                    steps {
                        complianceChecks()
                    }
                }
                stage('Validate Software Catalog Definition') {
                    steps {
                         withModifiedFunctions(checkout: true) { functionName ->
                            script {
                                def config = FUNCTION_CONFIG[functionName]
                                def basePath = getBasePath(config)
                                def catalogFile = "${basePath}/${functionName}/software-catalog.yaml"
                                echo "Validating Software Catalog: ${catalogFile}"
                                datadogSoftwareCatalogValidate(
                                    servicePath: "${basePath}/${functionName}",
                                    serviceDefinitionFilePath: catalogFile
                                )
                            }
                         }

                    }
                }
                stage('Unit Tests and Style Checks') {
                    steps {
                        withModifiedFunctions(checkout: true) { functionName ->
                            script {
                                def config = FUNCTION_CONFIG[functionName]
                                def basePath = getBasePath(config)
                                echo "Running Unit Tests and Style Checks for ${functionName}"
                                dir("${basePath}/${functionName}") {
                                    withEcr {
                                        withEnv(["COMPOSE_PROJECT_NAME=${env.BUILD_TAG.toLowerCase()}-${functionName}"]) {
                                            try {
                                                sh "docker compose run --rm --build unit-lint"
                                            }
                                            finally {
                                                sh "docker compose down -v"
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                stage('Static Application Security Tests') {
                    steps {
                        withModifiedFunctions(checkout: true) { functionName ->
                            script {
                                def config = FUNCTION_CONFIG[functionName]
                                def basePath = getBasePath(config)
                                echo "Running Static App Security Tests for ${functionName}"
                                sastTests(projectDir: "${basePath}/${functionName}")
                            }
                        }
                    }
                }
                stage('Create and Docker Scan a Release') {
                    steps {
                        withModifiedFunctions(checkout: true) { functionName ->
                            script {
                                def config = FUNCTION_CONFIG[functionName]
                                def sanitizedFunctionName = sanitizeFunctionName(functionName)
                                def imageName = "${GITHUB_REPOSITORY}-${sanitizedFunctionName}"
                                echo "Building for ${functionName}"
                                def basePath = getBasePath(config)
                                dockerToEcr(
                                    awsRegions: AWS_REGIONS,
                                    ecrAccountId: ECR_ACCOUNT_ID,
                                    imageName: imageName,
                                    imageTag: isPullRequest() ? env.BRANCH_NAME : "VERIFY-BUILD-${env.BUILD_NUMBER}",
                                    dockerBuildContext: "${basePath}/${functionName}",
                                    dockerBuildFile: "${basePath}/${functionName}/Dockerfile",
                                    dockerBuildTarget: config.dockerBuildTarget
                                )
                                dockerScan(
                                    awsRegion: AWS_REGIONS[0],
                                    ecrAccountId: ECR_ACCOUNT_ID,
                                    imageName: imageName,
                                    imageTag: isPullRequest() ? env.BRANCH_NAME : "VERIFY-BUILD-${env.BUILD_NUMBER}",
                                    vulnerabilitiesToIgnore: config.vulnerabilitiesToIgnore
                                )
                            }
                        }
                    }
                }
            }
        }
        stage('Retag and Docker Scan a Release'){
            when {
                expression { !isPullRequest() }
            }
            steps {
                withModifiedFunctions(checkout: true) { functionName ->
                    script {
                        def config = FUNCTION_CONFIG[functionName]
                        def sanitizedFunctionName = sanitizeFunctionName(functionName)
                        def imageName = "${GITHUB_REPOSITORY}-${sanitizedFunctionName}"
                        def commit = getCommitHash(functionName)
                        def prNumber = githubGetCommitPrs(
                            repo: GITHUB_REPOSITORY,
                            commitSha: commit
                        )[0].number
                        retagEcrImage(
                            awsRegions: AWS_REGIONS,
                            ecrAccountId: ECR_ACCOUNT_ID,
                            imageName: imageName,
                            imageTag: 'PR-' + prNumber,
                            newTag: commit
                        )
                        dockerScan(
                            awsRegion: AWS_REGIONS[0],
                            ecrAccountId: ECR_ACCOUNT_ID,
                            imageName: imageName,
                            imageTag: commit,
                            vulnerabilitiesToIgnore: config.vulnerabilitiesToIgnore
                        )
                    }
                }
            }
        }
        stage('Deploy to QA') {
            when {
                expression { !isPullRequest() }
            }
            steps {
                withModifiedFunctions(checkout: true) { functionName ->
                    script {
                        def config = FUNCTION_CONFIG[functionName]
                        def sanitizedFunctionName = sanitizeFunctionName(functionName)
                        def imageName = "${GITHUB_REPOSITORY}-${sanitizedFunctionName}"
                        def commit = getCommitHash(functionName)
                        def deployConfig = config.deploy.qa
                        if (config.type == 'task') {
                            echo "Updating QA task for ${functionName}"
                            deployConfig.awsDeploymentTargets.each { awsDeploymentTarget ->
                                fargateDeploy(
                                    awsRegions: AWS_REGIONS,
                                    gitCommit: commit,
                                    serviceName: "lambda-vector-${sanitizedFunctionName}",
                                    ecrRegistryAccountId: ECR_ACCOUNT_ID,
                                    deployType: config.deployType,
                                    verifyMode: config.verifyMode,
                                    imageNameOverride: "${GITHUB_REPOSITORY}-${sanitizedFunctionName}",
                                    environment: 'qa',
                                    awsDeploymentTargetAccountId: AWS_DEPLOYMENT_TARGETS[awsDeploymentTarget].accountId,
                                    awsDeploymentRoleName: AWS_DEPLOYMENT_TARGETS[awsDeploymentTarget].deploymentRole
                                )
                            }
                        }
                        else {
                            echo "Deploying to QA for ${functionName}"
                            dir("${getBasePath(config)}/${functionName}") {
                                deployConfig.instances.each { instanceName ->
                                    deployConfig.awsDeploymentTargets.each { awsDeploymentTarget ->
                                        def lambdaFn = "qa-${instanceName}"
                                        echo "Deploying QA ${lambdaFn}"
                                        lambdaDeploy(
                                            awsRegions: AWS_REGIONS,
                                            imageTag: commit,
                                            imageName: imageName,
                                            functionName: lambdaFn,
                                            ecrRegistryAccountId: ECR_ACCOUNT_ID,
                                            awsDeploymentTargetAccountId: AWS_DEPLOYMENT_TARGETS[awsDeploymentTarget].accountId,
                                            awsDeploymentRoleName: AWS_DEPLOYMENT_TARGETS[awsDeploymentTarget].deploymentRole
                                        )
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        stage('Integration Tests') {
            when {
                expression { !isPullRequest() }
            }
            steps {
                script {
                    echo "skipping due to cloudwatch rate limit"
                }
            }
        }
        stage('Deploy to PROD') {
            when {
                expression { params.DEPLOY_TO_PROD && !isPullRequest() }
            }
            steps {
                withModifiedFunctions(checkout: true) { functionName ->
                    script {
                        def config = FUNCTION_CONFIG[functionName]
                        def sanitizedFunctionName = sanitizeFunctionName(functionName)
                        def imageName = "${GITHUB_REPOSITORY}-${sanitizedFunctionName}"
                        def commit = getCommitHash(functionName)
                        def deployConfig = config.deploy.prod
                        if (config.type == 'task') {
                            echo "Updating PROD task for ${functionName}"
                            deployConfig.awsDeploymentTargets.each { awsDeploymentTarget ->
                                fargateDeploy(
                                    awsRegions: AWS_REGIONS,
                                    gitCommit: commit,
                                    serviceName: "lambda-vector-${sanitizedFunctionName}",
                                    ecrRegistryAccountId: ECR_ACCOUNT_ID,
                                    deployType: config.deployType,
                                    verifyMode: config.verifyMode,
                                    imageNameOverride: "${GITHUB_REPOSITORY}-${sanitizedFunctionName}",
                                    environment: 'prod',
                                    awsDeploymentTargetAccountId: AWS_DEPLOYMENT_TARGETS[awsDeploymentTarget].accountId,
                                    awsDeploymentRoleName: AWS_DEPLOYMENT_TARGETS[awsDeploymentTarget].deploymentRole
                                )
                            }
                        }
                        else {
                            echo "Deploying to PROD for ${functionName}"
                            dir("${getBasePath(config)}/${functionName}") {
                                deployConfig.instances.each { instanceName ->
                                    deployConfig.awsDeploymentTargets.each { awsDeploymentTarget ->
                                        def lambdaFn = "prod-${instanceName}"
                                        echo "Deploying PROD ${lambdaFn}"
                                        lambdaDeploy(
                                            awsRegions: AWS_REGIONS,
                                            imageTag: commit,
                                            imageName: imageName,
                                            functionName: lambdaFn,
                                            ecrRegistryAccountId: ECR_ACCOUNT_ID,
                                            awsDeploymentTargetAccountId: AWS_DEPLOYMENT_TARGETS[awsDeploymentTarget].accountId,
                                            awsDeploymentRoleName: AWS_DEPLOYMENT_TARGETS[awsDeploymentTarget].deploymentRole
                                        )
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        stage('Publish Software Catalog Definition') {
            when {
                expression { params.DEPLOY_TO_PROD && !isPullRequest() }
            }
            steps {
                withModifiedFunctions(checkout: true) { functionName ->
                    script {
                        def config = FUNCTION_CONFIG[functionName]
                        def basePath = getBasePath(config)
                        def catalogFile = "${basePath}/${functionName}/software-catalog.yaml"
                        getAllInstanceNames(config).each { serviceName ->
                            def tempFile = "${catalogFile}.${serviceName}"
                            sh "grep -q '^  name: ' ${catalogFile} && sed 's/^  name: .*/  name: ${serviceName}/' ${catalogFile} > ${tempFile}"
                            echo "Publishing Software Catalog: ${serviceName}"
                            datadogSoftwareCatalogPublish(
                                servicePath: "${basePath}/${functionName}",
                                serviceDefinitionFilePath: tempFile
                            )
                        }
                    }
                }
            }
        }
    }
    post {
        regression {
            script {
                if (!isPullRequest()) {
                    slackNotify channel: SLACK_NOTIFICATIONS_CHANNEL
                }
            }
        }
        fixed {
            script {
                if (!isPullRequest()) {
                    slackNotify channel: SLACK_NOTIFICATIONS_CHANNEL
                }
            }
        }
    }
}

def isPullRequest() {
    return env.BRANCH_NAME != 'master'
}

def getCommitHash(String functionName) {
    def basePath = getBasePath(FUNCTION_CONFIG[functionName])
    return sh(
        script: "git log -1 --pretty=format:'%H' -- ${basePath}/${functionName}",
        returnStdout: true
    ).trim()
}

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

def getAllInstanceNames(Map config) {
    return config.deploy.values().collect { it.instances }.flatten().unique()
}

def withModifiedFunctions(Map args = [:], Closure steps) {
    getLambdaMonorepoUtils().withModifiedProjects(args, steps)
    withModifiedFargateFunctions(args, steps)
}

def withModifiedFargateFunctions(Map args = [:], Closure body) {
    def namesToBuild = getFargateFunctionsToBuild()
    if (namesToBuild != null && namesToBuild.isEmpty()) return

    def resolved

    // Dedicated discovery node: one checkout to resolve which functions changed.
    node {
        cleanWs()
        def checkoutVars = checkout scm
        env.GIT_URL = checkoutVars.GIT_URL
        env.GIT_COMMIT = checkoutVars.GIT_COMMIT

        if (namesToBuild) {
            resolved = namesToBuild
        } else {
            // Mirror getModifiedPaths: on master compare against last successful
            // build; on a PR compare against the merge-base with master.
            // Use checkoutVars (not env) — env.GIT_PREVIOUS_SUCCESSFUL_COMMIT is
            // never populated on a fargate-only master build because the lambda
            // MonorepoUtils checkout wrapper (which exports scmVars to env) only
            // runs when at least one lambda/ project was modified.
            // Two-dot (..) rather than three-dot (...): the empty-tree SHA is a
            // tree object, not a commit, so symmetric difference fails with it.
            def base = isPullRequest()
                ? sh(script: 'git merge-base HEAD origin/master', returnStdout: true).trim()
                : (checkoutVars.GIT_PREVIOUS_SUCCESSFUL_COMMIT ?: '4b825dc642cb6eb9a060e54bf8d69288fbee4904')
            def output = sh(
                script: """#!/bin/bash
                    set -eo pipefail
                    git diff --name-only ${base}..HEAD -- fargate/ | cut -d/ -f2 | sort -u
                """,
                returnStdout: true
            ).trim()
            resolved = output ? output.split('\n').findAll { FUNCTION_CONFIG.containsKey(it) } : []
        }
    }

    echo "Fargate functions to build: ${resolved ?: '(none)'}"
    if (resolved) {
        def desc = currentBuild.description ?: ''
        if (!desc.contains(resolved[0])) {
            currentBuild.description = desc + resolved.collect { "<br>${it}" }.join('')
        }
        // Each function gets its own node for agent-lifecycle resilience: an
        // agent going offline mid-run only fails that branch, not all of them.
        stage("Fargate - ${env.STAGE_NAME}") {
            parallel resolved.collectEntries { functionName ->
                [(functionName): {
                    node {
                        cleanWs()
                        checkout scm
                        stage(functionName) { body(functionName) }
                    }
                }]
            }
        }
    }
}

def getFargateFunctionsToBuild() {
    if (!params.LAMBDA_FUNCTION_NAMES) return null
    if (params.LAMBDA_FUNCTION_NAMES == '*') {
        return FUNCTION_CONFIG.findAll { k, v -> v.basePath == 'fargate' }.keySet() as List
    }
    return params.LAMBDA_FUNCTION_NAMES.split(',').findAll { FUNCTION_CONFIG[it]?.basePath == 'fargate' }
}

def getLambdaMonorepoUtils() {
    return library("jenkins-global-libraries@${params.SHARED_LIBRARIES_VERSION}").com.sonymusic.MonorepoUtils.getInstance(
        steps: this,
        projectBasePath: 'lambda',
        projectsToBuild: params.LAMBDA_FUNCTION_NAMES
            ? params.LAMBDA_FUNCTION_NAMES.split(',').findAll { (FUNCTION_CONFIG[it]?.basePath ?: 'lambda') == 'lambda' } as Set
            : null
    )
}

def getBasePath(Map config) {
    return config?.basePath ?: 'lambda'
}
