pipeline {
    // agent none at the top so the approval stage can wait without holding an
    // executor. Stages that need a workspace declare their own agent.
    agent none

    environment {
        // Everything Docker-related happens on the NAS over SSH: Jenkins has no
        // daemon of its own. Absolute paths are required — the remote
        // non-interactive PATH does not include /usr/local/bin.
        NAS_HOST = '192.168.1.140'
        NAS_USER = 'jenkins-ci'
        NAS_DOCKER = '/usr/local/bin/docker'
        NAS_COMPOSE = '/usr/local/bin/docker-compose'
        NAS_BACKUP = '/volume1/scripts/backup_finanzas_db.sh'
        NAS_NETWORK = 'finanzas_default'
        CI_DIR = '/volume1/docker/ci/expenses_manager'
        COMPOSE_DIR = '/volume1/docker/finanzas'
        APP_ENV = '/volume1/docker/finanzas/app/expenses_manager/.env'
        SSH_OPTS = '-o StrictHostKeyChecking=accept-new'
        IMAGE = 'expenses-manager'
    }

    options {
        buildDiscarder(logRotator(numToKeepStr: '30'))
    }

    stages {

        // Nested stages share one agent allocation.
        stage('CI') {
            agent any
            options { timeout(time: 40, unit: 'MINUTES') }
            stages {

                stage('Build images') {
                    steps {
                        sshagent(credentials: ['nas-ssh']) {
                            sh '''
                                SHA=$(echo "${GIT_COMMIT}" | cut -c1-7)
                                TAG=$(echo "${BRANCH_NAME}" | tr '/' '-')

                                # Ship the workspace rather than cloning again on the
                                # NAS: what gets built is then provably the same code
                                # this build checked out.
                                ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
                                    "rm -rf ${CI_DIR} && mkdir -p ${CI_DIR}"

                                tar -czf - --exclude=./.git . \
                                    | ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
                                        "tar -xzf - -C ${CI_DIR}"

                                # The test image is the runtime image plus test
                                # dependencies, so the second build reuses every layer
                                # of the first.
                                ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
                                    "cd ${CI_DIR} && sudo ${NAS_DOCKER} build \
                                        --target runtime \
                                        -t ${IMAGE}:${SHA} -t ${IMAGE}:${TAG} ."

                                ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
                                    "cd ${CI_DIR} && sudo ${NAS_DOCKER} build \
                                        --target test \
                                        -t ${IMAGE}-test:${SHA} ."
                            '''
                        }
                    }
                }

                // Catches models changed without generating the migration. This
                // never touches a database, so SQLite is enough here.
                stage('Check migrations') {
                    steps {
                        sshagent(credentials: ['nas-ssh']) {
                            sh '''
                                SHA=$(echo "${GIT_COMMIT}" | cut -c1-7)
                                ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
                                    "sudo ${NAS_DOCKER} run --rm \
                                        -e SECRET_KEY=ci-only-value \
                                        -e DEBUG=False \
                                        -e ALLOWED_HOSTS=localhost,testserver \
                                        -e DJANGO_SETTINGS_MODULE=expenses_manager.settings_test \
                                        -e DB_ENGINE=sqlite3 \
                                        ${IMAGE}-test:${SHA} \
                                        python manage.py makemigrations --check --dry-run"
                            '''
                        }
                    }
                }

                stage('Run tests') {
                    steps {
                        sshagent(credentials: ['nas-ssh']) {
                            // Tests run against PostgreSQL, the engine production
                            // uses. SQLite is only a local-development convenience
                            // and the two have already differed once (default
                            // ordering), so a green SQLite run proves little.
                            sh '''
                                ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
                                    "sudo ${NAS_DOCKER} network create ci-net-${BUILD_NUMBER} \
                                     && sudo ${NAS_DOCKER} run -d \
                                            --name ci-db-${BUILD_NUMBER} \
                                            --network ci-net-${BUILD_NUMBER} \
                                            -e POSTGRES_DB=ci \
                                            -e POSTGRES_USER=ci \
                                            -e POSTGRES_PASSWORD=ci-only-password \
                                            postgres:15"
                            '''

                            sh '''
                                ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
                                    "for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do \
                                        sudo ${NAS_DOCKER} exec ci-db-${BUILD_NUMBER} \
                                            pg_isready -U ci -d ci > /dev/null 2>&1 && exit 0; \
                                        sleep 2; \
                                     done; \
                                     echo 'Postgres no arranco a tiempo'; exit 1"
                            '''

                            sh '''
                                SHA=$(echo "${GIT_COMMIT}" | cut -c1-7)
                                ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
                                    "sudo ${NAS_DOCKER} run --rm \
                                        --network ci-net-${BUILD_NUMBER} \
                                        -e SECRET_KEY=ci-only-value \
                                        -e DEBUG=False \
                                        -e ALLOWED_HOSTS=localhost,testserver \
                                        -e DJANGO_SETTINGS_MODULE=expenses_manager.settings_test \
                                        -e DB_ENGINE=postgresql \
                                        -e DB_NAME=ci \
                                        -e DB_USER=ci \
                                        -e DB_PASSWORD=ci-only-password \
                                        -e DB_HOST=ci-db-${BUILD_NUMBER} \
                                        -e DB_PORT=5432 \
                                        ${IMAGE}-test:${SHA} \
                                        sh -c 'pytest --cov --junitxml=/tmp/junit.xml >&2; \
                                               rc=\\$?; cat /tmp/junit.xml; exit \\$rc'" \
                                    > junit.xml
                            '''
                        }
                    }
                    post {
                        always {
                            // allowEmptyResults: false is deliberate. A build where
                            // pytest died before collecting anything produces no XML,
                            // and that must fail rather than pass quietly.
                            junit allowEmptyResults: false, testResults: 'junit.xml'

                            // The throwaway database goes away whatever happened, so a
                            // failed build does not leave containers behind.
                            sshagent(credentials: ['nas-ssh']) {
                                sh '''
                                    ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
                                        "sudo ${NAS_DOCKER} rm -f ci-db-${BUILD_NUMBER} > /dev/null 2>&1; \
                                         sudo ${NAS_DOCKER} network rm ci-net-${BUILD_NUMBER} > /dev/null 2>&1; \
                                         exit 0"
                                '''
                            }
                        }
                    }
                }

                stage('Smoke test image') {
                    steps {
                        sshagent(credentials: ['nas-ssh']) {
                            sh '''
                                SHA=$(echo "${GIT_COMMIT}" | cut -c1-7)
                                ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
                                    "sudo ${NAS_DOCKER} run --rm \
                                        -e SECRET_KEY=smoke-test-only \
                                        -e DEBUG=False \
                                        -e ALLOWED_HOSTS=localhost \
                                        -e DB_ENGINE=sqlite3 \
                                        ${IMAGE}:${SHA} python manage.py check"
                            '''

                            // A migrations directory without __init__.py is a namespace
                            // package, and Django deliberately ignores those: the app
                            // then appears to have no migrations at all, migrate --check
                            // always reports nothing pending, and a real migration would
                            // silently never be applied.
                            sh '''
                                SHA=$(echo "${GIT_COMMIT}" | cut -c1-7)
                                ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
                                    "sudo ${NAS_DOCKER} run --rm \
                                        -e SECRET_KEY=smoke-test-only \
                                        -e DEBUG=False \
                                        -e ALLOWED_HOSTS=localhost \
                                        -e DB_ENGINE=sqlite3 \
                                        ${IMAGE}:${SHA} python manage.py showmigrations expenses" \
                                    | grep -q '0001_initial'
                            '''
                        }
                    }
                }

                stage('Prune old images') {
                    steps {
                        sshagent(credentials: ['nas-ssh']) {
                            // docker images lists newest first, so anything past the
                            // fifth is an older build. Branch tags are kept because
                            // they always point at a recent image, and the image the
                            // running container uses cannot be removed anyway.
                            sh '''
                                ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
                                    "for repo in ${IMAGE} ${IMAGE}-test; do \
                                        sudo ${NAS_DOCKER} images \\$repo --format '{{.Repository}}:{{.Tag}}' \
                                            | grep -v ':dev' | grep -v ':main' | grep -v '<none>' \
                                            | tail -n +6 \
                                            | while read img; do \
                                                sudo ${NAS_DOCKER} rmi \\$img > /dev/null 2>&1; \
                                              done; \
                                     done; \
                                     sudo ${NAS_DOCKER} image prune -f > /dev/null; \
                                     exit 0"
                            '''
                        }
                    }
                }
            }
        }

        // Read-only: nothing is changed on the NAS by this stage.
        stage('Detect migrations') {
            agent any
            when { branch 'main' }
            steps {
                sshagent(credentials: ['nas-ssh']) {
                    script {
                        // migrate --check exits non-zero when migrations are unapplied.
                        def rc = sh(returnStatus: true, script: '''
                            SHA=$(echo "${GIT_COMMIT}" | cut -c1-7)
                            ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
                                "sudo ${NAS_DOCKER} run --rm \
                                    --network ${NAS_NETWORK} \
                                    --env-file ${APP_ENV} \
                                    ${IMAGE}:${SHA} python manage.py migrate --check"
                        ''')
                        env.MIGRATIONS_PENDING = (rc == 0) ? 'false' : 'true'
                        echo "Migraciones pendientes: ${env.MIGRATIONS_PENDING}"

                        if (rc != 0) {
                            // Printing the plan also proves the database is reachable.
                            // If the non-zero status above was a connection failure
                            // rather than pending migrations, this fails loudly instead
                            // of asking for approval on a false premise.
                            sh '''
                                SHA=$(echo "${GIT_COMMIT}" | cut -c1-7)
                                ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
                                    "sudo ${NAS_DOCKER} run --rm \
                                        --network ${NAS_NETWORK} \
                                        --env-file ${APP_ENV} \
                                        ${IMAGE}:${SHA} python manage.py migrate --plan"
                            '''
                        }
                    }
                }
            }
        }

        // No agent: this waits without occupying an executor.
        stage('Aprobar migraciones') {
            when {
                allOf {
                    branch 'main'
                    expression { env.MIGRATIONS_PENDING == 'true' }
                }
            }
            steps {
                notifyTelegram("⏸️ Esperando aprobacion",
                               "Hay migraciones pendientes. Entra desde el enlace para revisar el plan y aprobar el despliegue. Si no respondes en 2 horas, el despliegue se cancela.")
                timeout(time: 2, unit: 'HOURS') {
                    input message: 'Hay migraciones pendientes. Revisa el plan en el log del stage anterior.',
                          ok: 'Migrar y desplegar'
                }
            }
        }

        stage('Deploy') {
            agent any
            when { branch 'main' }
            options { timeout(time: 15, unit: 'MINUTES') }
            steps {
                sshagent(credentials: ['nas-ssh']) {
                    sh '''
                        SHA=$(echo "${GIT_COMMIT}" | cut -c1-7)

                        # Always back up, even with no migrations pending: this is the
                        # restore point for this particular deployment.
                        ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} "sudo ${NAS_BACKUP}"

                        # Migrate with the new image before the running container is
                        # replaced, so the app never serves new code against the old
                        # schema. A no-op when nothing is pending.
                        ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
                            "sudo ${NAS_DOCKER} run --rm \
                                --network ${NAS_NETWORK} \
                                --env-file ${APP_ENV} \
                                ${IMAGE}:${SHA} python manage.py migrate --noinput"

                        # Point compose at the image that was just tested, then swap the
                        # container. Writing the tag to .env leaves a readable record of
                        # what is deployed and makes rollback a one-line edit.
                        ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
                            "echo WEB_IMAGE=${IMAGE}:${SHA} > ${COMPOSE_DIR}/.env \
                             && cd ${COMPOSE_DIR} \
                             && sudo ${NAS_COMPOSE} up -d web"
                    '''

                    script {
                        // The Host header must match ALLOWED_HOSTS or Django answers 400
                        // regardless of health. A 301 is expected: SECURE_SSL_REDIRECT
                        // sends plain HTTP to HTTPS, and reaching that redirect already
                        // proves the app processed the request.
                        def code = sh(returnStdout: true, script: '''
                            ssh ${SSH_OPTS} ${NAS_USER}@${NAS_HOST} \
                                "sleep 10 && curl -s -o /dev/null -w '%{http_code}' \
                                    -H 'Host: finanzas.kuijper.es' \
                                    http://127.0.0.1:8001/"
                        ''').trim()

                        echo "Respuesta HTTP: ${code}"
                        if (!(code in ['200', '301', '302'])) {
                            error("La aplicacion no responde correctamente (HTTP ${code})")
                        }
                    }
                }
            }
            post {
                success {
                    notifyTelegram("🚀 Desplegado", "")
                }
            }
        }
    }

    post {
        failure { notifyTelegram("❌ FALLO", "") }
        fixed   { notifyTelegram("✅ Recuperado", "") }
        aborted { notifyTelegram("⚠️ Abortado", "") }
    }
}

// Sends a one-line build summary to Telegram, with an optional extra paragraph.
// The node block is required because the pipeline declares "agent none", so post
// actions have no workspace of their own. Both parameters are mandatory to avoid
// Groovy default arguments, which Jenkins' CPS interpreter handles poorly.
void notifyTelegram(String status, String extra) {
    node {
        withCredentials([
            string(credentialsId: 'telegram-bot-token', variable: 'TG_TOKEN'),
            string(credentialsId: 'telegram-chat-id', variable: 'TG_CHAT')
        ]) {
            sh """
                curl -s -X POST "https://api.telegram.org/bot\${TG_TOKEN}/sendMessage" \
                    --data-urlencode "chat_id=\${TG_CHAT}" \
                    --data-urlencode "text=${status} ${env.JOB_NAME} #${env.BUILD_NUMBER}
Rama: ${env.BRANCH_NAME ?: 'main'}
${env.BUILD_URL}
${extra}" > /dev/null
            """
        }
    }
}