def APP_NAME = 'frontend-solfege'
def REPO_NAME = 'orchard-suite'
def SLACK_NOTIFY_CHANNEL = '#osp-alerts'
def QA_AUTH0_ORG_ID = 'org_vdEfyCAGWYnTkX6G'
def QA_AUTH0_CLIENT_ID = 'p59ItpwcDfNoLCHCQy3XsrbmYGqxkvXC'
def PROD_AUTH0_ORG_ID = 'org_5iwzH8TjVGfIko8n'
def POEDITOR_PROJECT_ID = '484437'

def solfegeDeploy(Map params){
    // clone repos used by the generate:usage script in the build step
    // ('git' is not included in the node docker image)
    dir('apps/frontend-solfege') {
        if(!fileExists('.cache/usage')) {
            sh 'mkdir -p .cache/usage && chmod -R a=rwx .cache/usage'
        }

        sh './scripts/usage/cloneRepos.sh'
    }

    // build solfege
    withCredentials([string(credentialsId: 'poeditor-api-token', variable: 'POEDITOR_API_TOKEN')]) {
        nodeSh (
            user: 'root', // NOTE: corepack needs 'root' to install pnpm.
            envVars: suiteAppBuild.DEFAULT_ENV_VARS + suiteAppBuild.DEFAULT_VARS_BY_ENV[params.env] + params.envVars + [POEDITOR_API_TOKEN: "\${POEDITOR_API_TOKEN}"],
            script: """
                # install pnpm and dependencies
                corepack enable
                pnpm config set "//npm.pkg.github.com/:_authToken" "\$GITHUB_NPM_TOKEN"
                pnpm install --frozen-lockfile

                # build frontend-cli
                pnpm --filter "*field-validator" build
                pnpm --filter "*frontend-cli-vite" build
                pnpm --filter "*frontend-cli" build

                # sync translations
                pnpm run --filter "*suite-components" i18n:${params.syncI18n ? 'sync' : 'download'}
                pnpm run --filter "*suite-frontend" i18n:${params.syncI18n ? 'sync' : 'download'}

                # build suite packages
                pnpm run --filter "*suite-icons" build:icons${params.rebuildIcons ? ':update' : ''}
                pnpm run --filter "*suite-icons" build:esm
                pnpm run --filter "*suite-frontend" build:esm
                pnpm run --filter "*suite-components" build:esm
                pnpm run --filter "*suite-testing" build:esm

                # build solfege
                cd apps/frontend-solfege
                pnpm run generate:usage
                pnpm run generate:docs
                pnpm run generate:manifest

                pnpm frontend build ${params.publicPath ? '--public-path ' + params.publicPath : ''}
            """
        )
    }

    // deploy solfege to cdn
    dir('apps/frontend-solfege') {
        suiteAppPublish (
            env: params.env,
            appName: 'frontend-solfege',
            subpath: params.cdnSubpath
        )
    }

    // deploy suite-icons assets to cdn
    dir('packages/suite-icons') {
        suiteAppPublish (
            env: params.env,
            appName: 'assets'
        )
    }
}

def solfegeE2eTests(Map params){
    dir('apps/frontend-solfege/e2e'){
        withCredentials([aws(credentialsId: "cucumber-tests")]) {
            withSecrets(secrets:[
                [id: 'qa/cypress-configs/cypress.env.json', environmentVariable: 'CYPRESS_ENV_JSON'],
                [id: 'qa/e2e-test-secrets/user-data/solfege-user-data', environmentVariable: 'CYPRESS_USERS_JSON']
            ]) {
                writeFile file: 'cypress.env.json', text: env.CYPRESS_ENV_JSON
                writeFile file: 'cypress/fixtures/users.json', text: env.CYPRESS_USERS_JSON
            }
        }
        nodeSh(
            user: 'root',
            script: """
                npm install --frozen-lockfile
            """
        )
        withEnv([
            "CYPRESS_BASE_URL=${params.url}"
        ]){
            sh './scripts/run.sh'
        }
    }
}

def deploySuitePackages(Map params){
    sh './scripts/createSuiteTags.sh'
    withCredentials([string(credentialsId: 'poeditor-api-token', variable: 'POEDITOR_API_TOKEN')]) {
        nodeSh (
            user: 'root',
            envVars: [
                POEDITOR_PROJECT_ID: params.poeditorProjectId,
                POEDITOR_API_TOKEN: "\${POEDITOR_API_TOKEN}"
            ],
            script: """
                set -e
                corepack enable
                pnpm config set "//npm.pkg.github.com/:_authToken" "\$GITHUB_NPM_TOKEN"

                # jenkins reuses the workspace, wipe node_modules so the install isnt stale
                rm -rf node_modules
                pnpm install --frozen-lockfile

                # log resolved typescript (TS migration visibility)
                pnpm exec tsc --version

                # clean packages
                pnpm run --filter "@theorchard/suite-*" clean

                # build frontend-cli
                pnpm run --filter "*frontend-cli-i18n" build

                # sync translations
                pnpm run --filter "*suite-components" i18n:download
                pnpm run --filter "*suite-frontend" i18n:download

                pnpm run --filter "@theorchard/suite-*" build

                # guard: abort if any published suite package is missing its built dist
                node scripts/verify-suite-dist.mjs

                pnpm publish --recursive --no-git-checks --tag ${params.tag} --filter "@theorchard/suite-*" --registry https://npm.pkg.github.com
            """
        )
    }
}

pipeline {
    agent {
        label 'aws'
    }

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

    parameters {
        booleanParam(name: 'DEPLOY_TO_PROD', defaultValue: true, description: 'Deploy to prod (master pushes only). Uncheck to skip the prod deploy + publish stages.')
        string(name: 'SHARED_LIBRARIES_VERSION', defaultValue: 'master', description: 'The version of the Jenkins shared libraries to use. Can be a branch, tag or Git revision.')
        booleanParam(name: 'RUN_VULN_SCAN', defaultValue: false, description: 'Run only the vulnerability scan (skips build & deploy). For on-demand one-off scans.')
        string(name: 'VULN_WINDOW_HOURS', defaultValue: '24', description: 'Vulnerability scan: only report alerts opened within this many hours. Raise it for a one-off "what is further ahead" scan.')
        choice(name: 'VULN_MIN_SEVERITY', choices: ['HIGH', 'CRITICAL', 'MODERATE', 'LOW'], description: 'Vulnerability scan: report this severity or higher (so HIGH covers HIGH + CRITICAL). Lower it for a broader scan.')
    }

    triggers {
        // On-demand scan: comment one of these on a PR to run a scan-only build that
        // replies on the PR and #osp-alerts:
        //   run vulnerability scan            last 24h, HIGH+
        //   run vulnerability scan 48h        last 48h
        //   run vulnerability scan 720h low   last 30 days, LOW+
        //   run vulnerability scan full       entire open backlog, all severities
        issueCommentTrigger('.*retest please.*|.*deploy pri.*|.*run vulnerability scan.*')
        // Daily 06:01 UTC scan, master only (build & deploy stages skip on timer/scan builds).
        cron(env.BRANCH_NAME == 'master' ? '1 6 * * *' : '')
    }

    environment {
        PRI_DOMAIN = "${APP_NAME}-${env.CHANGE_ID}.pullrequests.qaorch.com"
    }

    stages {
        stage('Load Shared Libraries') {
            steps {
                library "jenkins-global-libraries@${params.SHARED_LIBRARIES_VERSION}"
            }
        }
        stage('Daily Vulnerability Scan') {
            when {
                expression { isVulnScanBuild() }
            }
            steps {
                // Window/severity come from the build params or the triggering comment.
                script {
                    def scan = vulnScanArgs(
                        env.GITHUB_COMMENT ?: '',
                        params.VULN_WINDOW_HOURS,
                        params.VULN_MIN_SEVERITY
                    )
                    // drop any root-owned artifacts a prior run left, so nodeShs chmod doesnt choke on them
                    sh 'rm -f .vuln-digest.txt .vuln-slack.json .vuln-comment.json'
                    // `pnpm update --prod -r` resolves to max-in-range == what a consumer's fresh
                    // install gets, so vuln-scan can tell consumer-stuck vulns from self-healing ones.
                    nodeSh(
                        user: 'root',
                        envVars: [
                            VULN_WINDOW_HOURS: scan.window,
                            VULN_MIN_SEVERITY: scan.severity
                        ],
                        script: '''
                            corepack enable
                            pnpm config set "//npm.pkg.github.com/:_authToken" "$GITHUB_NPM_TOKEN"
                            pnpm update --prod -r
                            node scripts/vuln-scan.mjs
                        '''
                    )
                    def digest = readFile('.vuln-digest.txt').trim()
                    def slackChunks = readJSON(file: '.vuln-slack.json')
                    if (isScheduledScan()) {
                        // daily timer: post each chunk to Slack (silent when nothing new)
                        for (int i = 0; i < slackChunks.size(); i++) {
                            slackNotify channel: SLACK_NOTIFY_CHANNEL, message: slackChunks[i]
                        }
                    } else {
                        // on-demand: post every Slack chunk, and the full table to the PR
                        if (slackChunks) {
                            for (int i = 0; i < slackChunks.size(); i++) {
                                slackNotify channel: SLACK_NOTIFY_CHANNEL, message: slackChunks[i]
                            }
                        } else {
                            slackNotify channel: SLACK_NOTIFY_CHANNEL, message: 'No vulnerabilities matched the scan window/severity.'
                        }
                        if (env.CHANGE_ID) {
                            // githubPostPrComment breaks on multi-line bodies; post via writeJSON + curl.
                            def message = digest ?: 'No vulnerabilities matched the scan window/severity.'
                            writeJSON file: '.vuln-comment.json', json: [body: message]
                            def apiUrl = "https://api.github.com/repos/theorchard/${REPO_NAME}/issues/${env.CHANGE_ID}/comments"
                            withCredentials([string(credentialsId: 'orchardci-webhook-token', variable: 'GITHUB_TOKEN')]) {
                                sh """
                                    set +x
                                    curl -sS --fail -X POST -H "Authorization: token \$GITHUB_TOKEN" \\
                                        -H "Accept: application/vnd.github+json" \\
                                        -H "X-GitHub-Api-Version: 2022-11-28" \\
                                        -d @.vuln-comment.json \\
                                        "${apiUrl}"
                                """
                            }
                        }
                    }
                }
            }
            post {
                always {
                    // same cleanup after the run; theyre root-owned but the workspace dir is jenkins-writable
                    sh 'rm -f .vuln-digest.txt .vuln-slack.json .vuln-comment.json'
                }
            }
        }
        stage('Validate Software Catalog Definition') {
            when { expression { !isVulnScanBuild() } }
            steps {
                script {
                    def missingCatalogs = []
                    withModifiedFunctions(checkout: true) { packageName ->
                        def catalogFile = "packages/${packageName}/software-catalog.yaml"
                        if (fileExists(catalogFile)) {
                            datadogSoftwareCatalogValidate(servicePath: "packages/${packageName}")
                        } else {
                            echo "software-catalog.yaml is required for the package: ${packageName}"
                            missingCatalogs << packageName
                        }
                    }
                    if (missingCatalogs) {
                        error("Missing software-catalog.yaml for packages: ${missingCatalogs.join(', ')}")
                    }
                }
            }
            post {
                failure {
                    script {
                        if (env.BRANCH_NAME == 'master') {
                            slackNotify channel: SLACK_NOTIFY_CHANNEL, message: "Datadog Software catalog validation FAILED for ${REPO_NAME}. Please check the pipeline logs."
                        }
                    }
                }
            }
        }
        stage('Initialize monorepo') {
            when { expression { !isVulnScanBuild() } }
            steps {
                script {
                    // pre-create directories so non-root users can write to them
                    sh """
                        mkdir -p .docker
                        mkdir -p apps/frontend-solfege/.cache
                    """
                }
            }
        }
        stage('Solfêge Unit and Style Tests') {
            when {
                allOf {
                    expression { !isVulnScanBuild() }
                    not {
                        environment name: 'GITHUB_COMMENT', value: 'deploy pri'
                    }
                }
            }
            steps {
                nodeSh(
                    user: 'root',
                    script: """
                        corepack enable
                        pnpm config set "//npm.pkg.github.com/:_authToken" "\$GITHUB_NPM_TOKEN"
                        pnpm install --frozen-lockfile

                        pnpm --filter "*field-validator" build
                        pnpm --filter "*suite-icons" build:icons
                        pnpm --filter "*suite-icons" build:esm
                        pnpm --filter "*suite-components" build:esm
                        pnpm --filter "*suite-frontend" build:esm
                        pnpm --filter "*suite-testing" build:esm

                        pnpm --filter "*frontend-solfege" test
                    """
                )
            }
        }
        stage('Deploy Solfêge PR Instance') {
            when {
                expression {
                    return !isVulnScanBuild() && env.CHANGE_ID && (env.GITHUB_COMMENT == 'deploy pri' || env.BRANCH_NAME.startsWith('PATCH-'))
                }
            }
            steps {
                solfegeDeploy (
                    env: 'qa',
                    publicPath: "https://qa-cdn.theorchard.io/${APP_NAME}/prs/${env.CHANGE_ID}/",
                    cdnSubpath: "prs/${env.CHANGE_ID}",
                    rebuildIcons: true,
                    envVars: [
                        GRAPHQL_URL: 'https://grassproxy.pullrequests.qaorch.com/graphql-router/graphql',
                        AUTH0_CLIENT_ID: QA_AUTH0_CLIENT_ID,
                        AUTH0_ORG_ID: QA_AUTH0_ORG_ID,
                        AUTH0_STORE_TOKEN_IN_CLIENT: 'true',
                        AUTH0_REDIRECT_URI: "https://pullrequests.qaorch.com/auth0Redirect?to=${env.PRI_DOMAIN}",
                        POEDITOR_PROJECT_ID: POEDITOR_PROJECT_ID,
                        SEGMENT_WRITE_KEY: 's7zPjwqrfweYBLC61GKBegysWLtqKPtv',
                        SENTRY_DSN: 'https://a45c5578f6854d72b3b354fd39a56690@o22178.ingest.sentry.io/5394756'
                    ]
                )
                // TODO: update cdnInvalidate step to support PRI instances
                // cdnInvalidate env: 'qa', appName: APP_NAME, subpath: "prs/${env.CHANGE_ID}"
            }
            post {
                success {
                    githubPostPrComment(
                        repo: REPO_NAME,
                        message: "🚀 PR-Instance successfully deployed to: https://${env.PRI_DOMAIN}"
                    )
                }
                failure {
                    githubPostPrComment(
                        repo: REPO_NAME,
                        message: "❌ PR-Instance failed deploying to: https://${env.PRI_DOMAIN}"
                    )
                }
            }
        }
        stage('Solfêge PR Instance E2E Tests') {
            when {
                expression {
                    return !isVulnScanBuild() && env.CHANGE_ID && (env.GITHUB_COMMENT == 'deploy pri' || env.BRANCH_NAME.startsWith('PATCH-'))
                }
            }
            steps {
                solfegeE2eTests([url: "https://${env.PRI_DOMAIN}"])
            }
            post {
                success {
                    githubPostPrComment(
                        repo: REPO_NAME,
                        message: "✅ PR-Instance e2e tests succeeded"
                    )
                }
                failure {
                    githubPostPrComment(
                        repo: REPO_NAME,
                        message: "❌ PR-Instance e2e tests failed"
                    )
                }
            }
        }
        stage('Deploy Solfêge to QA') {
            when {
                allOf {
                    branch 'master'
                    expression { !isVulnScanBuild() }
                }
            }
            steps {
                solfegeDeploy (
                    env: 'qa',
                    syncI18n: true,
                    envVars: [
                        AUTH0_CLIENT_ID: QA_AUTH0_CLIENT_ID,
                        AUTH0_ORG_ID: QA_AUTH0_ORG_ID,
                        AUTH0_STORE_TOKEN_IN_CLIENT: 'true',
                        GRAPHQL_URL: 'https://qa-ows-grass.theorchard.io/graphql-router/graphql',
                        POEDITOR_PROJECT_ID: POEDITOR_PROJECT_ID,
                        SEGMENT_WRITE_KEY: 's7zPjwqrfweYBLC61GKBegysWLtqKPtv',
                        SENTRY_DSN: 'https://a45c5578f6854d72b3b354fd39a56690@o22178.ingest.sentry.io/5394756'
                    ]
                )
            }
        }
        stage('Invalidate Solfêge QA CDN') {
            when {
                allOf {
                    branch 'master'
                    expression { !isVulnScanBuild() }
                }
            }
            steps {
                cdnInvalidate env: 'qa', appName: APP_NAME
            }
        }
        stage('Solfêge E2E Tests') {
            when {
                allOf {
                    branch 'master'
                    expression { !isVulnScanBuild() }
                }
            }
            steps {
                solfegeE2eTests([url: 'https://solfege.qaorch.com'])
            }
            post {
                regression {
                    slackNotify channel: SLACK_NOTIFY_CHANNEL, stepName: "e2e"
                }
                fixed {
                    slackNotify channel: SLACK_NOTIFY_CHANNEL, stepName: "e2e"
                }
            }
        }
        stage('Deploy Assets to UAT CDN') {
            when {
                allOf {
                    branch 'master'
                    expression { !isVulnScanBuild() }
                }
            }
            steps {
                // deploy suite-icons assets to cdn
                dir('packages/suite-icons') {
                    suiteAppPublish (
                        env: 'uat',
                        appName: 'assets'
                    )
                }
            }
        }
        stage('Deploy suite packages') {
            when {
                allOf {
                    branch 'master'
                    expression { !isVulnScanBuild() }
                    expression { deployToProd() }
                }
            }
            steps {
                deploySuitePackages(
                    tag: 'latest',
                    poeditorProjectId: POEDITOR_PROJECT_ID
                )
            }
        }
        stage('Deploy patched suite packages') {
            when {
                expression {
                    return !isVulnScanBuild() && !env.CHANGE_ID && env.BRANCH_NAME.startsWith('PATCH-')
                }
            }
            steps {
                deploySuitePackages(
                    tag: 'patch',
                    poeditorProjectId: POEDITOR_PROJECT_ID
                )
            }
        }
        stage('Deploy Solfêge to Prod'){
            when {
                allOf {
                    branch 'master'
                    expression { !isVulnScanBuild() }
                    expression { deployToProd() }
                }
            }
            steps {
                solfegeDeploy (
                    env: 'prod',
                    envVars: [
                        AUTH0_CLIENT_ID: 'bm8EvN2AHLtCT8XCdqRckR8TfRNSrkKH',
                        AUTH0_ORG_ID: PROD_AUTH0_ORG_ID,
                        AUTH0_STORE_TOKEN_IN_CLIENT: 'true',
                        POEDITOR_PROJECT_ID: POEDITOR_PROJECT_ID,
                        SEGMENT_WRITE_KEY: 'X5rj1IXLFY2g6pCGJgr4OETyF71xKEZT',
                        SENTRY_DSN: 'https://2cd38eaf76eb460ea6a73edbdb3b0c74@sentry.io/4504884447543296'
                    ]
                )
                script {
                    def modifiedPaths = getModifiedPaths(basePath: 'packages')
                    modifiedPaths.each { pkg ->
                        def catalogFile = "packages/${pkg}/software-catalog.yaml"
                        if (fileExists(catalogFile)) {
                            datadogSoftwareCatalogPublish(serviceDefinitionFilePath: catalogFile)
                        }
                    }
                }
            }
            post {
                failure {
                    slackNotify channel: SLACK_NOTIFY_CHANNEL, message: "Datadog Software catalog publish FAILED for ${REPO_NAME}. Please check the pipeline logs."
                }
            }
        }
        stage('Invalidate Solfêge Prod CDN') {
            when {
                allOf {
                    branch 'master'
                    expression { !isVulnScanBuild() }
                    expression { deployToProd() }
                }
            }
            steps {
                cdnInvalidate env: 'prod', appName: APP_NAME
            }
        }
    }
    post {
        failure {
            cleanWs();

            script {
                // a failed daily scan must still alert (the scan only Slacks on success).
                if (env.BRANCH_NAME == 'master') {
                    slackNotify channel: SLACK_NOTIFY_CHANNEL
                }
            }
        }
    }
}

def isScheduledScan() {
    return !currentBuild.getBuildCauses('hudson.triggers.TimerTrigger$TimerTriggerCause').isEmpty()
}

// A scan-only build (daily timer / RUN_VULN_SCAN / "run vulnerability scan" comment); build & deploy stages skip these.
def isVulnScanBuild() {
    return isScheduledScan() ||
        params.RUN_VULN_SCAN ||
        (env.GITHUB_COMMENT ?: '').contains('run vulnerability scan')
}

// `|| 'Yes'` covers a restarted pre-migration build still carrying the choice string (cant use plain
// truthiness, old 'No' is truthy too); drop to `params.DEPLOY_TO_PROD` after a few clean full runs.
def deployToProd() {
    return params.DEPLOY_TO_PROD == true || params.DEPLOY_TO_PROD == 'Yes'
}

// Overrides from a "run vulnerability scan ..." comment ("48h", a severity, or "full"); else build params.
@NonCPS
def vulnScanArgs(String comment, String defaultWindow, String defaultSeverity) {
    boolean full = (comment =~ /(?i)\b(full|all|backlog)\b/).find()
    def hours = (comment =~ /(\d+)\s*h\b/)
    def severity = (comment =~ /(?i)\b(critical|high|moderate|low)\b/)
    return [
        window: hours.find() ? hours.group(1) : (full ? '0' : defaultWindow),
        severity: severity.find() ? severity.group(1).toUpperCase() : (full ? 'LOW' : defaultSeverity)
    ]
}

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

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