// Jenkinsfile.ephemeral
//
// Standalone pipeline for ephemeral environment lifecycle.
// Actions: create, destroy, list, cleanup
//
// Triggers:
//   - PR comment: "ephemeral deploy [Nd]", "ephemeral destroy", "ephemeral list"
//   - Nightly cron: runs "cleanup" to reap expired TTLs
//   - Manual: via Jenkins job UI with parameters

// ── Constants ──────────────────────────────────────────────────────────────

String APP_NAME = 'Coda'
String GITHUB_REPOSITORY = 'ows-coda'
String SLACK_NOTIFICATIONS_CHANNEL = '#coda-devs'

// AWS
String AWS_REGION = 'us-east-1'
String ECR_ACCOUNT_ID = '086679231553'
String QA_ACCOUNT_ID = '437795906767'
String QA_DEPLOYMENT_ROLE = 'prod-jenkins-aws-pipeline-agent'
String QA_SECRETS_ROLE = 'qa-ows-coda-jenkins-secrets-role'
String QA_AUTH0_CLIENT_ID = 'wTcXYKnR8wk4gIe3C8dNGb4PbH0V3JJn'

// Terraform
String TERRAFORM_INFRA_REPO = 'git@github.com:theorchard/terraform-infra.git'
String TERRAFORM_DIR = 'qa/ows-coda'

// Ephemeral
int MAX_EPHEMERAL_ENVS = 10
String DEFAULT_TTL_DAYS = '7'

// ── Parsed state (set in Parse stage) ──────────────────────────────────────

String PARSED_ACTION = ''     // create, destroy, list, cleanup
String PARSED_ENV_NAME = ''   // e.g. "pr-42"
String PARSED_TTL_DAYS = ''   // e.g. "7"
String PARSED_PR_ID = ''      // e.g. "42" (single-PR only in Phase 1)

pipeline {
    agent any

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

    parameters {
        choice(name: 'ACTION', choices: ['create', 'destroy', 'list', 'cleanup'], description: 'Ephemeral environment action')
        string(name: 'PR_ID', defaultValue: '', description: 'PR number (e.g. 42). Auto-detected for comment triggers.')
        string(name: 'ENV_NAME', defaultValue: '', description: 'Environment name (e.g. pr-42). Auto-derived from PR_ID if empty.')
        choice(name: 'TTL', choices: ['7d', '1d', '15d', '30d'], description: 'Time-to-live for the environment')
        string(name: 'SHARED_LIBRARIES_VERSION', defaultValue: 'master', description: 'Jenkins shared libraries version')
    }

    triggers {
        issueCommentTrigger('.*ephemeral\\s+(deploy|destroy|list)(\\s+\\d+d)?.*')
        cron('H 3 * * *')  // nightly cleanup
    }

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

        stage('Parse') {
            steps {
                script {
                    // Determine if triggered by cron, comment, or manual
                    def isCron = currentBuild.getBuildCauses('hudson.triggers.TimerTrigger$TimerTriggerCause').size() > 0
                    def isComment = env.GITHUB_COMMENT?.trim()

                    if (isCron) {
                        PARSED_ACTION = 'cleanup'
                        echo "==> Cron trigger: running cleanup"
                    } else if (isComment) {
                        def match = (env.GITHUB_COMMENT =~ /ephemeral\s+(deploy|destroy|list)(\s+(\d+)d)?/)
                        if (!match) {
                            error "Could not parse comment: '${env.GITHUB_COMMENT}'"
                        }
                        def commentAction = match[0][1]
                        def commentTtl = match[0][3]

                        // Map comment verbs to internal actions
                        switch (commentAction) {
                            case 'deploy': PARSED_ACTION = 'create'; break
                            default:       PARSED_ACTION = commentAction; break
                        }

                        PARSED_PR_ID = env.CHANGE_ID ?: ''
                        if (!PARSED_PR_ID) {
                            error "Cannot determine PR ID from comment trigger. CHANGE_ID is not set."
                        }
                        if (!(PARSED_PR_ID ==~ /^\d+$/)) {
                            error "Invalid PR ID '${PARSED_PR_ID}'. Must be numeric."
                        }
                        PARSED_ENV_NAME = "pr-${PARSED_PR_ID}"
                        PARSED_TTL_DAYS = commentTtl ?: DEFAULT_TTL_DAYS

                        echo "==> Comment trigger: action=${PARSED_ACTION}, pr=${PARSED_PR_ID}, env=${PARSED_ENV_NAME}, ttl=${PARSED_TTL_DAYS}d"
                    } else {
                        // Manual trigger via parameters
                        PARSED_ACTION = params.ACTION
                        PARSED_PR_ID = params.PR_ID
                        PARSED_TTL_DAYS = params.TTL.replace('d', '')

                        if (PARSED_ACTION in ['create', 'destroy'] && !PARSED_PR_ID) {
                            error "PR_ID is required for ${PARSED_ACTION} action."
                        }
                        if (PARSED_PR_ID && !(PARSED_PR_ID ==~ /^\d+$/)) {
                            error "Invalid PR ID '${PARSED_PR_ID}'. Must be numeric."
                        }

                        PARSED_ENV_NAME = params.ENV_NAME ?: (PARSED_PR_ID ? "pr-${PARSED_PR_ID}" : '')

                        echo "==> Manual trigger: action=${PARSED_ACTION}, pr=${PARSED_PR_ID}, env=${PARSED_ENV_NAME}, ttl=${PARSED_TTL_DAYS}d"
                    }

                    // Validate env name format (for actions that need it)
                    if (PARSED_ACTION in ['create', 'destroy'] && PARSED_ENV_NAME) {
                        if (!(PARSED_ENV_NAME ==~ /^[a-z][a-z0-9-]{0,20}$/)) {
                            error "Invalid environment name '${PARSED_ENV_NAME}'. Must be lowercase alphanumeric with hyphens, 1-21 chars, starting with a letter."
                        }
                    }
                }
            }
        }

        stage('Check Capacity') {
            when {
                expression { PARSED_ACTION == 'create' }
            }
            steps {
                script {
                    def envs = listEphemeralEnvironments()
                    if (envs.size() >= MAX_EPHEMERAL_ENVS) {
                        def table = formatEnvTable(envs)
                        def msg = "**Cannot create ephemeral environment** — at capacity (${envs.size()}/${MAX_EPHEMERAL_ENVS}).\n\n" +
                                  "Active environments:\n\n${table}\n\n" +
                                  "Destroy an environment with `ephemeral destroy` or wait for TTL expiry."
                        if (PARSED_PR_ID) {
                            postPrComment(PARSED_PR_ID, msg)
                        }
                        error msg
                    }
                    echo "==> Capacity OK: ${envs.size()}/${MAX_EPHEMERAL_ENVS} environments in use"
                }
            }
        }

        stage('Duplicate Check') {
            when {
                expression { PARSED_ACTION == 'create' }
            }
            steps {
                script {
                    // Check via ECS tags
                    def envs = listEphemeralEnvironments()
                    def existing = envs.find { it.name == PARSED_ENV_NAME }

                    if (existing) {
                        def url = "https://qa-ows-coda-${PARSED_ENV_NAME}.theorchard.io"
                        def msg = "**Environment `${PARSED_ENV_NAME}` already exists** at ${url}.\n\n" +
                                  "Use `ephemeral destroy` first, or wait for the existing environment to expire."
                        if (PARSED_PR_ID) {
                            postPrComment(PARSED_PR_ID, msg)
                        }
                        error msg
                    }
                }
            }
        }

        stage('List') {
            when {
                expression { PARSED_ACTION == 'list' }
            }
            steps {
                script {
                    def envs = listEphemeralEnvironments()
                    def count = envs.size()
                    def body
                    if (count == 0) {
                        body = "**No active ephemeral environments** (0/${MAX_EPHEMERAL_ENVS})."
                    } else {
                        def table = formatEnvTable(envs)
                        body = "**Active ephemeral environments (${count}/${MAX_EPHEMERAL_ENVS}):**\n\n${table}"
                    }
                    echo body

                    if (PARSED_PR_ID) {
                        postPrComment(PARSED_PR_ID, body)
                    }
                }
            }
        }

        stage('Merge PR') {
            when {
                expression { PARSED_ACTION == 'create' }
            }
            steps {
                script {
                    echo "==> Merging PR #${PARSED_PR_ID} onto master"
                    sh """
                    git fetch origin master
                    git checkout -B ephemeral-build origin/master
                    git fetch origin pull/${PARSED_PR_ID}/head:pr-${PARSED_PR_ID}
                    git merge --no-edit pr-${PARSED_PR_ID}
                    """
                    echo "==> Merge successful"
                }
            }
        }

        stage('Run Checks') {
            when {
                expression { PARSED_ACTION == 'create' }
            }
            steps {
                withCredentials([string(credentialsId: 'github_packages_token', variable: 'GITHUB_NPM_TOKEN')]) {
                    withEcr {
                        sh '''
                        docker compose --profile test-unit up --build \
                        --exit-code-from lint-and-test \
                        --abort-on-container-exit \
                        --remove-orphans \
                        lint-and-test
                        '''
                    }
                }
                withCredentials([string(credentialsId: 'github_packages_token', variable: 'GITHUB_NPM_TOKEN')]) {
                    withEcr {
                        sh '''
                        MYSQL_HOST_PORT=0 \
                        docker compose --profile test-functional up --build \
                        --exit-code-from test-functional \
                        --abort-on-container-exit \
                        --remove-orphans \
                        test-functional
                        '''
                    }
                }
            }
        }

        stage('Build & Push') {
            when {
                expression { PARSED_ACTION == 'create' }
            }
            steps {
                withSecrets(awsAccountId: QA_ACCOUNT_ID, awsRole: QA_SECRETS_ROLE, secrets: [
                    [id: 'qa/ows-coda/SENTRY_AUTH_TOKEN', environmentVariable: 'SENTRY_AUTH_TOKEN'],
                    [id: 'qa/ows-coda/SENTRY_DSN', environmentVariable: 'SENTRY_DSN'],
                ]) {
                    suiteAppBuild(
                        env: 'qa',
                        detectPackageManager: true,
                        scriptNames: ['build:api', 'build:client'],
                        envVars: [
                            AUTH0_CLIENT_ID: QA_AUTH0_CLIENT_ID,
                            SENTRY_AUTH_TOKEN: env.SENTRY_AUTH_TOKEN,
                            SENTRY_DSN: env.SENTRY_DSN,
                            SENTRY_ORG: 'the-orchard',
                            SENTRY_PROJECT: 'qa-ows-coda',
                        ]
                    )
                }
                withCredentials([string(credentialsId: 'github_packages_token', variable: 'GITHUB_NPM_TOKEN')]) {
                    dockerToEcr awsRegions: [AWS_REGION], ecrAccountId: ECR_ACCOUNT_ID, imageName: GITHUB_REPOSITORY,
                        imageTag: env.GIT_COMMIT, dockerBuildSecrets: [[id: 'GITHUB_NPM_TOKEN', env: 'GITHUB_NPM_TOKEN']], dockerBuildTarget: 'deploy',
                        additionalContexts: ['client-dist': "${env.WORKSPACE}/apps/client/dist"]
                }
            }
        }

        stage('Terraform') {
            when {
                expression { PARSED_ACTION == 'create' }
            }
            steps {
                // Checkout terraform-infra
                dir('terraform-infra') {
                    git branch: 'master', credentialsId: 'github_ssh_key', url: TERRAFORM_INFRA_REPO
                }
                // Run create-ephemeral.sh
                dir("terraform-infra/${TERRAFORM_DIR}") {
                    sh """
                    ./scripts/create-ephemeral.sh '${PARSED_ENV_NAME}' \
                      --ttl-days '${PARSED_TTL_DAYS}' \
                      --pr-numbers '${PARSED_PR_ID}' \
                      --created-by '${env.CHANGE_AUTHOR ?: env.BUILD_USER_ID ?: "jenkins"}' \
                      --yes
                    """
                }
            }
        }

        stage('DB Setup') {
            when {
                expression { PARSED_ACTION == 'create' }
            }
            steps {
                script {
                    def dbName = "coda_${PARSED_ENV_NAME.replace('-', '_')}"

                    // Get RDS host from terraform output
                    def rdsHost = ''
                    dir("terraform-infra/${TERRAFORM_DIR}") {
                        // Switch to default workspace to read main env outputs
                        sh "terraform workspace select default"
                        rdsHost = sh(returnStdout: true, script: "terraform output -raw rds_cluster_endpoint").trim()
                        sh "terraform workspace select '${PARSED_ENV_NAME}'"
                    }

                    // Create database and run migrations
                    withSecrets(awsAccountId: QA_ACCOUNT_ID, awsRole: QA_SECRETS_ROLE, secrets: [
                        [id: 'qa/ows-coda/CODA_DB_EPHEMERAL_PASS', environmentVariable: 'CODA_DB_EPHEMERAL_PASS'],
                        [id: 'qa/ows-coda/CODA_DB_PASS', environmentVariable: 'CODA_DB_PASS'],
                    ]) {
                        sh """
                        echo "==> Creating database: ${dbName}"
                        mysql -h '${rdsHost}' -u coda_ephemeral -p"\${CODA_DB_EPHEMERAL_PASS}" \
                          -e "CREATE DATABASE IF NOT EXISTS \\\`${dbName}\\\`;"
                        """

                        // Run migrations using the deploy Docker image
                        def ecrImage = "${ECR_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${GITHUB_REPOSITORY}:${env.GIT_COMMIT}"
                        sh """
                        echo "==> Running Prisma migrations on ${dbName}"
                        docker run --rm \
                          -e DATABASE_URL="mysql://coda_svc:\${CODA_DB_PASS}@${rdsHost}:3306/${dbName}" \
                          '${ecrImage}' \
                          sh -c "pnpm --filter @ows-coda/db prisma migrate deploy"
                        """
                    }
                }
            }
        }

        stage('Terraform Destroy') {
            when {
                expression { PARSED_ACTION == 'destroy' }
            }
            steps {
                script {
                    // Verify environment exists
                    def envs = listEphemeralEnvironments()
                    def existing = envs.find { it.name == PARSED_ENV_NAME }
                    if (!existing) {
                        def msg = "**No ephemeral environment found** for `${PARSED_ENV_NAME}`."
                        if (PARSED_PR_ID) {
                            postPrComment(PARSED_PR_ID, msg)
                        }
                        error msg
                    }
                }

                // Checkout terraform-infra
                dir('terraform-infra') {
                    git branch: 'master', credentialsId: 'github_ssh_key', url: TERRAFORM_INFRA_REPO
                }

                // Get RDS host before destroying
                script {
                    dir("terraform-infra/${TERRAFORM_DIR}") {
                        sh "terraform init -input=false"
                        sh "terraform workspace select default"
                        env.EPHEMERAL_RDS_HOST = sh(returnStdout: true, script: "terraform output -raw rds_cluster_endpoint").trim()
                    }
                }

                // Run destroy with DB cleanup
                withSecrets(awsAccountId: QA_ACCOUNT_ID, awsRole: QA_SECRETS_ROLE, secrets: [
                    [id: 'qa/ows-coda/CODA_DB_EPHEMERAL_PASS', environmentVariable: 'CODA_DB_EPHEMERAL_PASS'],
                ]) {
                    dir("terraform-infra/${TERRAFORM_DIR}") {
                        sh """
                        ./scripts/destroy-ephemeral.sh '${PARSED_ENV_NAME}' \
                          --db-host '${env.EPHEMERAL_RDS_HOST}' \
                          --db-user 'coda_ephemeral' \
                          --db-pass "\${CODA_DB_EPHEMERAL_PASS}" \
                          --yes
                        """
                    }
                }
            }
        }

        stage('Cleanup') {
            when {
                expression { PARSED_ACTION == 'cleanup' }
            }
            steps {
                script {
                    def envs = listEphemeralEnvironments()
                    def now = new Date()
                    def destroyed = []

                    // Checkout terraform-infra (needed for destroy)
                    dir('terraform-infra') {
                        git branch: 'master', credentialsId: 'github_ssh_key', url: TERRAFORM_INFRA_REPO
                    }

                    // Get RDS host from default workspace
                    def rdsHost = ''
                    dir("terraform-infra/${TERRAFORM_DIR}") {
                        sh "terraform init -input=false"
                        sh "terraform workspace select default"
                        rdsHost = sh(returnStdout: true, script: "terraform output -raw rds_cluster_endpoint").trim()
                    }

                    for (ephEnv in envs) {
                        // Skip environments without TTL (manually created)
                        if (!ephEnv.ttl_expires_at) {
                            echo "==> Skipping ${ephEnv.name}: no TTL set (manual environment)"
                            continue
                        }

                        def expiresAt
                        try {
                            expiresAt = Date.parse("yyyy-MM-dd'T'HH:mm:ss'Z'", ephEnv.ttl_expires_at)
                        } catch (e) {
                            echo "==> Skipping ${ephEnv.name}: invalid ttl_expires_at '${ephEnv.ttl_expires_at}'"
                            continue
                        }

                        if (now.after(expiresAt)) {
                            echo "==> Reaping expired environment: ${ephEnv.name} (expired ${ephEnv.ttl_expires_at})"
                            try {
                                withSecrets(awsAccountId: QA_ACCOUNT_ID, awsRole: QA_SECRETS_ROLE, secrets: [
                                    [id: 'qa/ows-coda/CODA_DB_EPHEMERAL_PASS', environmentVariable: 'CODA_DB_EPHEMERAL_PASS'],
                                ]) {
                                    dir("terraform-infra/${TERRAFORM_DIR}") {
                                        sh """
                                        ./scripts/destroy-ephemeral.sh '${ephEnv.name}' \
                                          --db-host '${rdsHost}' \
                                          --db-user 'coda_ephemeral' \
                                          --db-pass "\${CODA_DB_EPHEMERAL_PASS}" \
                                          --yes
                                        """
                                    }
                                }
                                destroyed.add(ephEnv)

                                // Notify on associated PRs
                                if (ephEnv.pr_numbers) {
                                    for (prId in ephEnv.pr_numbers.split(',')) {
                                        try {
                                            postPrComment(prId.trim(),
                                                "**Ephemeral environment `${ephEnv.name}` expired and was automatically destroyed** (TTL: ${ephEnv.ttl_days}d)."
                                            )
                                        } catch (e) {
                                            echo "WARNING: Failed to post cleanup comment on PR #${prId}: ${e.message}"
                                        }
                                    }
                                }
                            } catch (e) {
                                echo "ERROR: Failed to destroy ${ephEnv.name}: ${e.message}"
                                // Continue with other environments
                            }
                        } else {
                            echo "==> ${ephEnv.name}: TTL OK (expires ${ephEnv.ttl_expires_at})"
                        }
                    }

                    if (destroyed.size() > 0) {
                        def names = destroyed.collect { it.name }.join(', ')
                        echo "==> Cleanup complete: destroyed ${destroyed.size()} environment(s): ${names}"
                    } else {
                        echo "==> Cleanup complete: no expired environments found"
                    }
                }
            }
        }

        stage('Notify') {
            when {
                expression { PARSED_ACTION in ['create', 'destroy'] }
            }
            steps {
                script {
                    if (PARSED_ACTION == 'create') {
                        def url = "https://qa-ows-coda-${PARSED_ENV_NAME}.theorchard.io"
                        def expiresAt = sh(
                            returnStdout: true,
                            script: "date -u -d '+${PARSED_TTL_DAYS} days' +%Y-%m-%d 2>/dev/null || date -u -v '+${PARSED_TTL_DAYS}d' +%Y-%m-%d"
                        ).trim()
                        def body = "**Ephemeral environment deployed!**\n\n" +
                                   "| | |\n|---|---|\n" +
                                   "| **URL** | ${url} |\n" +
                                   "| **Environment** | `${PARSED_ENV_NAME}` |\n" +
                                   "| **Commit** | `${env.GIT_COMMIT?.substring(0, 7)}` |\n" +
                                   "| **TTL** | ${PARSED_TTL_DAYS} days (expires ${expiresAt}) |\n" +
                                   "| **Build** | [#${env.BUILD_NUMBER}](${env.BUILD_URL}) |\n\n" +
                                   "Run `ephemeral destroy` to tear down early."

                        if (PARSED_PR_ID) {
                            postPrComment(PARSED_PR_ID, body)
                        }
                        echo "==> Environment ready: ${url}"
                    }

                    if (PARSED_ACTION == 'destroy') {
                        def body = "**Ephemeral environment `${PARSED_ENV_NAME}` destroyed.**\n\n" +
                                   "[Build logs](${env.BUILD_URL})"
                        if (PARSED_PR_ID) {
                            postPrComment(PARSED_PR_ID, body)
                        }
                        echo "==> Environment ${PARSED_ENV_NAME} destroyed"
                    }
                }
            }
        }
    }

    post {
        failure {
            script {
                // Post failure comment on PR if comment-triggered
                if (PARSED_PR_ID && PARSED_ACTION) {
                    try {
                        postPrComment(PARSED_PR_ID,
                            "**Ephemeral ${PARSED_ACTION} failed** for `${PARSED_ENV_NAME ?: 'unknown'}`.\n\n" +
                            "See [build logs](${env.BUILD_URL}) for details."
                        )
                    } catch (e) {
                        echo "WARNING: Failed to post failure comment: ${e.message}"
                    }
                }
            }
        }
        cleanup {
            cleanWs()
        }
    }
}

// ── Helper functions ───────────────────────────────────────────────────────

/**
 * Posts a comment on a GitHub PR.
 */
def postPrComment(String prId, String body) {
    withCredentials([string(credentialsId: 'github_api_token', variable: 'GH_TOKEN')]) {
        sh """
        curl -s -X POST \
          -H "Authorization: token \${GH_TOKEN}" \
          -H "Content-Type: application/json" \
          -d '${groovy.json.JsonOutput.toJson([body: body])}' \
          "https://api.github.com/repos/theorchard/${GITHUB_REPOSITORY}/issues/${prId}/comments"
        """
    }
}

/**
 * Lists all ECS services tagged ephemeral=true and returns a list of maps
 * with keys: name, arn, ttl_expires_at, ttl_days, pr_numbers, created_by.
 */
def listEphemeralEnvironments() {
    def services = []
    def arns = sh(
        returnStdout: true,
        script: """
        aws resourcegroupstaggingapi get-resources \
          --tag-filters Key=ephemeral,Values=true Key=service_name,Values=ows-coda \
          --resource-type-filters ecs:service \
          --region ${AWS_REGION} \
          --query 'ResourceTagMappingList[*].[ResourceARN,Tags]' \
          --output json
        """
    ).trim()

    def resources = readJSON text: arns
    for (resource in resources) {
        def arn = resource[0]
        def tags = resource[1]
        def tagMap = [:]
        for (tag in tags) {
            tagMap[tag.Key] = tag.Value
        }
        // Extract instance name from the service label tag or ARN
        def instance = tagMap.get('instance', '')
        if (instance) {
            services.add([
                name:           instance,
                arn:            arn,
                ttl_expires_at: tagMap.get('ttl_expires_at', ''),
                ttl_days:       tagMap.get('ttl_days', ''),
                pr_numbers:     tagMap.get('pr_numbers', ''),
                created_by:     tagMap.get('created_by', ''),
            ])
        }
    }
    return services
}

/**
 * Formats a list of ephemeral environments as a markdown table.
 */
def formatEnvTable(List environments) {
    def header = "| Name | URL | PRs | Expires | Created by |\n|------|-----|-----|---------|------------|\n"
    def rows = environments.collect { env ->
        def url = "https://qa-ows-coda-${env.name}.theorchard.io"
        def prs = env.pr_numbers ? env.pr_numbers.split(',').collect { "#${it}" }.join(', ') : '-'
        def expires = env.ttl_expires_at ? env.ttl_expires_at.substring(0, 10) : 'no TTL (manual)'
        def creator = env.created_by ?: '-'
        "| ${env.name} | ${url} | ${prs} | ${expires} | ${creator} |"
    }.join('\n')
    return header + rows
}
