// Pipeline CI/CD — Aduanasoft Anexo76
// Ejecuta: tests → security scan → docker build/push → E2E → deploy dev → smoke test
//
// Credenciales requeridas en Jenkins:
//   - harbor-credentials        : username/password para dev.aduanasoft.com
//   - a76-public-url-dev        : Secret Text — URL pública base (ej. https://anexo76-dev.aduanasoft.com)
//   - dev-server-user           : Secret Text — usuario SSH del servidor dev
//   - dev-server-host           : Secret Text — hostname/IP del servidor dev
//   - dev-server-ssh            : SSH Username with Private Key — llave privada para deploy
//   - a76-e2e-credentials       : Username with password — usuario de prueba en Workspace
//
// El agente Jenkins solo necesita: Docker CLI

pipeline {
    agent any

    environment {
        HARBOR_REGISTRY  = 'dev.aduanasoft.com'
        IMAGE_BACKEND    = "${HARBOR_REGISTRY}/anexo76/backend"
        IMAGE_FRONTEND   = "${HARBOR_REGISTRY}/anexo76/frontend"
        PYTHON_IMAGE     = 'python:3.12-slim'
        // Debe coincidir con @playwright/test del frontend (ver frontend/pnpm-lock.yaml)
        PLAYWRIGHT_IMAGE = 'mcr.microsoft.com/playwright:v1.56.1-noble'
        DEPLOY_PATH      = '~/projects/anexo76'
        // URL de Keycloak/Workspace — Keycloak vive en Workspace, no en el servidor de Anexo76
        KC_URL           = 'https://workspace.aduanasoft.com/kcauth'
    }

    options {
        timeout(time: 45, unit: 'MINUTES')
        buildDiscarder(logRotator(numToKeepStr: '10'))
        disableConcurrentBuilds()
        timestamps()
    }

    stages {

        // ── Tests (backend y frontend unit en paralelo) ───────────────────────
        stage('Tests') {
            parallel {

                stage('Test — Backend') {
                    steps {
                        sh '''
                            set -euo pipefail
                            C="a76-test-py-${BUILD_NUMBER}"
                            DB_C="a76-test-db-${BUILD_NUMBER}"
                            NET="a76-test-net-${BUILD_NUMBER}"
                            DB_NAME="anexo76_test"
                            DB_USER="anexo76"
                            DB_PASS="anexo76"
                            TEST_DB_URL="postgresql://${DB_USER}:${DB_PASS}@${DB_C}:5432/${DB_NAME}"

                            cleanup() {
                                docker rm -f "$C"    >/dev/null 2>&1 || true
                                docker rm -f "$DB_C" >/dev/null 2>&1 || true
                                docker network rm "$NET" >/dev/null 2>&1 || true
                            }
                            trap cleanup EXIT

                            docker network rm "$NET" >/dev/null 2>&1 || true
                            docker network create "$NET"

                            docker rm -f "$DB_C" >/dev/null 2>&1 || true
                            docker run -d --name "$DB_C" \
                                --network "$NET" \
                                -e POSTGRES_DB="$DB_NAME" \
                                -e POSTGRES_USER="$DB_USER" \
                                -e POSTGRES_PASSWORD="$DB_PASS" \
                                postgres:16-alpine

                            # Esperar a que Postgres esté listo
                            READY=0
                            for i in $(seq 1 30); do
                                if docker exec "$DB_C" pg_isready -U "$DB_USER" -d "$DB_NAME" >/dev/null 2>&1; then
                                    READY=1; break
                                fi
                                sleep 2
                            done
                            if [ "$READY" != "1" ]; then
                                echo "ERROR: Postgres no quedó listo en 60s."
                                exit 1
                            fi

                            docker exec "$DB_C" psql -U "$DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1 -c "
                                CREATE SCHEMA IF NOT EXISTS core;
                                CREATE SCHEMA IF NOT EXISTS a24;
                                CREATE SCHEMA IF NOT EXISTS a76;
                                CREATE SCHEMA IF NOT EXISTS public;
                            "

                            docker rm -f "$C" >/dev/null 2>&1 || true
                            docker run -d --name "$C" --network "$NET" "$PYTHON_IMAGE" sleep infinity
                            docker cp "$WORKSPACE/backend/." "$C:/app"

                            docker exec \
                                -e TEST_DATABASE_URL="$TEST_DB_URL" \
                                -w /app "$C" bash -lc '
                                    set -euxo pipefail
                                    pip install --quiet -r requirements.txt

                                    # Prueba de migraciones: upgrade → downgrade → upgrade
                                    # Verifica que todos los down() sean reversibles
                                    echo "--- alembic upgrade head ---"
                                    alembic upgrade head
                                    echo "--- alembic downgrade base ---"
                                    alembic downgrade base
                                    echo "--- alembic upgrade head (restaurar para tests) ---"
                                    alembic upgrade head

                                    # --cov=api,core: mide solo código fuente (excluye alembic, tests, layouts)
                                    # setup.cfg contiene los patrones omit — ver backend/setup.cfg
                                    # --cov-fail-under=25: umbral real actual (deuda técnica documentada)
                                    #   objetivo según estándares: 80% — incrementar conforme se agreguen tests
                                    pytest tests/ \
                                        --cov=api \
                                        --cov=core \
                                        --cov-fail-under=25 \
                                        --cov-report=term-missing \
                                        --cov-report=xml \
                                        --junitxml=test-results.xml \
                                        -v -ra
                                '
                            docker cp "$C:/app/test-results.xml" "$WORKSPACE/backend/test-results.xml"
                            docker cp "$C:/app/coverage.xml"     "$WORKSPACE/backend/coverage.xml"
                        '''
                    }
                    post {
                        always {
                            junit allowEmptyResults: true, testResults: 'backend/test-results.xml'
                            archiveArtifacts artifacts: 'backend/coverage.xml', allowEmptyArchive: true
                        }
                    }
                }

                stage('Test — Frontend (Vitest)') {
                    steps {
                        sh '''
                            set -euo pipefail
                            C="a76-test-fe-${BUILD_NUMBER}"
                            cleanup() { docker rm -f "$C" >/dev/null 2>&1 || true; }
                            trap cleanup EXIT

                            docker rm -f "$C" >/dev/null 2>&1 || true
                            docker run -d --name "$C" "$PLAYWRIGHT_IMAGE" sleep infinity
                            docker exec "$C" mkdir -p /workspace
                            docker cp "$WORKSPACE/." "$C:/workspace"

                            docker exec \
                                -e CI=true \
                                -e NODE_ENV=test \
                                -e "JENKINS_URL=${JENKINS_URL}" \
                                -e JENKINS_VITEST_FULL=1 \
                                -w /workspace/frontend \
                                "$C" bash -lc '
                                    set -euxo pipefail
                                    npm install -g pnpm@9 --quiet
                                    pnpm install --frozen-lockfile
                                    pnpm run i18n:compile
                                    pnpm run test:unit -- --run
                                '
                        '''
                    }
                }

            }
        }

        // ── Escaneo de vulnerabilidades ───────────────────────────────────────
        stage('Security Scan') {
            parallel {
                stage('pip-audit') {
                    steps {
                        sh '''
                            set -euo pipefail
                            C="a76-audit-py-${BUILD_NUMBER}"
                            cleanup() { docker rm -f "$C" >/dev/null 2>&1 || true; }
                            trap cleanup EXIT

                            docker rm -f "$C" >/dev/null 2>&1 || true
                            docker run -d --name "$C" "$PYTHON_IMAGE" sleep infinity
                            docker cp "$WORKSPACE/backend/requirements.txt" "$C:/requirements.txt"

                            docker exec "$C" bash -lc '
                                pip install --quiet pip-audit
                                pip-audit -r /requirements.txt -f json -o /pip-audit-report.json || true
                            '
                            docker cp "$C:/pip-audit-report.json" "$WORKSPACE/pip-audit-report.json"
                        '''
                        archiveArtifacts artifacts: 'pip-audit-report.json', allowEmptyArchive: true
                    }
                }

                stage('npm audit') {
                    steps {
                        sh '''
                            set -euo pipefail
                            C="a76-audit-fe-${BUILD_NUMBER}"
                            cleanup() { docker rm -f "$C" >/dev/null 2>&1 || true; }
                            trap cleanup EXIT

                            docker rm -f "$C" >/dev/null 2>&1 || true
                            docker run -d --name "$C" "$PLAYWRIGHT_IMAGE" sleep infinity
                            docker cp "$WORKSPACE/frontend/package.json"   "$C:/package.json"
                            docker cp "$WORKSPACE/frontend/pnpm-lock.yaml" "$C:/pnpm-lock.yaml" 2>/dev/null || true

                            docker exec -w / "$C" bash -lc '
                                npm audit --audit-level=high || true
                            '
                        '''
                    }
                }
            }
        }

        // ── Generación de versión ─────────────────────────────────────────────
        stage('Generate version') {
            when {
                anyOf { branch 'main'; branch 'development' }
            }
            steps {
                script {
                    def year      = sh(returnStdout: true, script: 'date +%y').trim()
                    def month     = sh(returnStdout: true, script: 'date +%m').trim()
                    def shortHash = sh(returnStdout: true, script: 'git rev-parse --short=8 HEAD').trim()

                    if (env.BRANCH_NAME == 'main') {
                        def lastTag = sh(returnStdout: true,
                            script: 'git describe --tags --abbrev=0 2>/dev/null || true').trim()
                        def commitCount = lastTag
                            ? sh(returnStdout: true, script: "git rev-list ${lastTag}..HEAD --count").trim()
                            : sh(returnStdout: true, script: 'git rev-list --count HEAD').trim()
                        if (commitCount == '0') commitCount = '1'
                        env.APP_VERSION = "${year}.${month}.1.${commitCount}"
                    } else {
                        env.APP_VERSION = "${year}.${month}.1.${shortHash}"
                    }

                    echo "VERSION: ${env.APP_VERSION}"
                }
            }
        }

        // ── Docker Build + Push ───────────────────────────────────────────────
        stage('Docker Build') {
            when {
                anyOf { branch 'main'; branch 'development' }
            }
            steps {
                script {
                    withCredentials([
                        usernamePassword(
                            credentialsId: 'harbor-credentials',
                            usernameVariable: 'HARBOR_USER',
                            passwordVariable: 'HARBOR_PASS'
                        )
                    ]) {
                        sh "echo \"\${HARBOR_PASS}\" | docker login ${HARBOR_REGISTRY} -u \"\${HARBOR_USER}\" --password-stdin"

                        sh """
                            docker build --progress=plain \
                                --build-arg APP_VERSION=${env.APP_VERSION} \
                                -t ${IMAGE_BACKEND}:${env.APP_VERSION} \
                                -t ${IMAGE_BACKEND}:latest \
                                -f backend/Dockerfile \
                                backend/
                        """

                        withCredentials([
                            string(credentialsId: 'a76-public-url-dev', variable: 'A76_URL')
                        ]) {
                            sh """
                                docker build --progress=plain \
                                    --build-arg VITE_API_URL=\${A76_URL}/api/ \
                                    --build-arg VITE_KEYCLOAK_URL=${KC_URL} \
                                    --build-arg VITE_KEYCLOAK_CLIENT_ID=anexo76-frontend \
                                    --build-arg INTERNAL_API_URL=http://backend:3467/api/ \
                                    -t ${IMAGE_FRONTEND}:${env.APP_VERSION} \
                                    -t ${IMAGE_FRONTEND}:latest \
                                    -f frontend/Dockerfile.prod \
                                    frontend/
                            """
                        }

                        sh """
                            docker push ${IMAGE_BACKEND}:${env.APP_VERSION}
                            docker push ${IMAGE_BACKEND}:latest
                            docker push ${IMAGE_FRONTEND}:${env.APP_VERSION}
                            docker push ${IMAGE_FRONTEND}:latest
                        """
                    }
                }
            }
        }

        // ── E2E (Playwright) — contra la imagen recién buildeada ────────────────
        // Levanta un stack efímero (docker-compose.e2e.yml) con la imagen del backend
        // recién pusheada a Harbor y una imagen del frontend buildeada localmente con
        // VITE_API_URL=http://localhost:8000/api/ (la imagen de prod tiene la URL de dev
        // bakeada en el bundle, ver Dockerfile.prod:19-20). Si E2E falla, el deploy
        // no ocurre — el guardrail actúa sobre el código que se va a desplegar,
        // no sobre la versión viva.
        stage('E2E (Playwright)') {
            when { branch 'development' }
            steps {
                withCredentials([
                    usernamePassword(
                        credentialsId: 'a76-e2e-credentials',
                        usernameVariable: 'E2E_USER',
                        passwordVariable: 'E2E_PASS'
                    )
                ]) {
                    sh '''
                        set -euo pipefail
                        export DOCKER_BUILDKIT=1
                        PROJECT="a76-e2e-${BUILD_NUMBER}"
                        FE_IMAGE_E2E="${IMAGE_FRONTEND}:e2e-${BUILD_NUMBER}"
                        BE_IMAGE_E2E="${IMAGE_BACKEND}:${APP_VERSION}"
                        PLAYWRIGHT_C="a76-test-e2e-${BUILD_NUMBER}"
                        BUILD_LOG="${WORKSPACE}/e2e-frontend-build.log"
                        PREP_LOG="${WORKSPACE}/e2e-playwright-prep.log"

                        export E2E_BACKEND_IMAGE="$BE_IMAGE_E2E"
                        export E2E_FRONTEND_IMAGE="$FE_IMAGE_E2E"

                        cleanup() {
                            echo "=== E2E cleanup ==="
                            docker rm -f "$PLAYWRIGHT_C" >/dev/null 2>&1 || true
                            docker compose -p "$PROJECT" -f docker-compose.e2e.yml down -v --remove-orphans >/dev/null 2>&1 || true
                            docker rmi -f "$FE_IMAGE_E2E" >/dev/null 2>&1 || true
                            rm -f "$BUILD_LOG" "$PREP_LOG" || true
                        }
                        trap cleanup EXIT

                        # ── PARALELO #1 ─────────────────────────────────────────────────
                        # Lanzar en background el build del frontend temporal Y el stack
                        # de dependencias. El build es lo más lento (~2-4 min); mientras
                        # corre, levantamos postgres+minio+valkey y aplicamos migraciones.
                        # ────────────────────────────────────────────────────────────────

                        # Build temporal del frontend con VITE_API_URL=localhost.
                        # --cache-from reusa la layer de pnpm install de la imagen de prod
                        # recién buildeada (las deps son idénticas, solo cambia VITE_API_URL).
                        # No se pushea a Harbor; solo existe en este agente durante el stage.
                        ( docker build --progress=plain \
                            --cache-from "${IMAGE_FRONTEND}:latest" \
                            --build-arg BUILDKIT_INLINE_CACHE=1 \
                            --build-arg VITE_API_URL=http://localhost:8000/api/ \
                            --build-arg VITE_KEYCLOAK_URL=${KC_URL} \
                            --build-arg VITE_KEYCLOAK_CLIENT_ID=anexo76-frontend \
                            --build-arg INTERNAL_API_URL=http://backend:8000/api/ \
                            -t "$FE_IMAGE_E2E" \
                            -f frontend/Dockerfile.prod \
                            frontend/ ) > "$BUILD_LOG" 2>&1 &
                        BUILD_PID=$!

                        # En foreground: levantar dependencias y correr migraciones
                        docker compose -p "$PROJECT" -f docker-compose.e2e.yml up -d postgres-a76 minio valkey

                        # Esperar postgres healthy (timeout 60s)
                        READY=0
                        for i in $(seq 1 30); do
                            STATUS=$(docker inspect -f '{{.State.Health.Status}}' \
                                "$(docker compose -p "$PROJECT" -f docker-compose.e2e.yml ps -q postgres-a76)" 2>/dev/null || echo "starting")
                            if [ "$STATUS" = "healthy" ]; then READY=1; break; fi
                            sleep 2
                        done
                        if [ "$READY" != "1" ]; then
                            echo "ERROR: Postgres no quedó healthy en 60s"
                            docker compose -p "$PROJECT" -f docker-compose.e2e.yml logs postgres-a76 || true
                            exit 1
                        fi

                        # Crear schemas que el backend espera (mismo patrón que Test — Backend)
                        DB_C=$(docker compose -p "$PROJECT" -f docker-compose.e2e.yml ps -q postgres-a76)
                        docker exec "$DB_C" psql -U postgres -d anexo76_core -v ON_ERROR_STOP=1 -c "
                            CREATE SCHEMA IF NOT EXISTS core;
                            CREATE SCHEMA IF NOT EXISTS a24;
                            CREATE SCHEMA IF NOT EXISTS a76;
                            CREATE SCHEMA IF NOT EXISTS public;
                        "

                        # `run --rm --no-deps` ejecuta alembic con la imagen del backend SIN
                        # arrancar los servicios dependientes (ya están corriendo).
                        docker compose -p "$PROJECT" -f docker-compose.e2e.yml run --rm --no-deps \
                            --entrypoint "" backend alembic upgrade head

                        # Esperar a que termine el build del frontend (si no terminó ya)
                        echo "Esperando build temporal del frontend..."
                        if ! wait "$BUILD_PID"; then
                            echo "ERROR: build del frontend E2E falló — log:"
                            cat "$BUILD_LOG" || true
                            exit 1
                        fi
                        echo "✓ Build temporal del frontend completado"

                        # ── Arrancar backend y frontend ─────────────────────────────────
                        docker compose -p "$PROJECT" -f docker-compose.e2e.yml up -d backend frontend

                        # ── PARALELO #2 ─────────────────────────────────────────────────
                        # Mientras backend+frontend se vuelven healthy (~40-80s),
                        # preparar el contenedor Playwright en background.
                        # ────────────────────────────────────────────────────────────────
                        docker rm -f "$PLAYWRIGHT_C" >/dev/null 2>&1 || true
                        # --network host: para que "localhost:5173" dentro del contenedor
                        # de Playwright resuelva a los puertos publicados por el stack.
                        # Necesario porque ORIGIN=http://localhost:5173 y el callback de
                        # Keycloak redirige a esa URL exacta.
                        docker run -d --name "$PLAYWRIGHT_C" --network host "$PLAYWRIGHT_IMAGE" sleep infinity
                        docker exec "$PLAYWRIGHT_C" mkdir -p /workspace
                        docker cp "$WORKSPACE/." "$PLAYWRIGHT_C:/workspace"

                        ( docker exec -w /workspace/frontend "$PLAYWRIGHT_C" bash -lc '
                            set -euxo pipefail
                            npm install -g pnpm@9 --quiet
                            pnpm install --frozen-lockfile
                            pnpm run i18n:compile
                        ' ) > "$PREP_LOG" 2>&1 &
                        PREP_PID=$!

                        # En foreground: esperar backend y frontend healthy
                        for SVC in backend frontend; do
                            READY=0
                            for i in $(seq 1 60); do
                                CID=$(docker compose -p "$PROJECT" -f docker-compose.e2e.yml ps -q "$SVC" 2>/dev/null)
                                if [ -n "$CID" ]; then
                                    STATUS=$(docker inspect -f '{{.State.Health.Status}}' "$CID" 2>/dev/null || echo "starting")
                                    if [ "$STATUS" = "healthy" ]; then READY=1; break; fi
                                fi
                                sleep 2
                            done
                            if [ "$READY" != "1" ]; then
                                echo "ERROR: $SVC no quedó healthy en 120s"
                                docker compose -p "$PROJECT" -f docker-compose.e2e.yml logs "$SVC" || true
                                exit 1
                            fi
                        done

                        # Esperar a que termine la preparación de Playwright
                        echo "Esperando preparación de Playwright..."
                        if ! wait "$PREP_PID"; then
                            echo "ERROR: preparación de Playwright falló — log:"
                            cat "$PREP_LOG" || true
                            exit 1
                        fi
                        echo "✓ Playwright listo"

                        # ── Correr tests ────────────────────────────────────────────────
                        E2E_EXIT=0
                        docker exec \
                            -e CI=true \
                            -e "JENKINS_URL=${JENKINS_URL}" \
                            -e "PLAYWRIGHT_TEST_BASE_URL=http://localhost:5173" \
                            -e "E2E_TEST_USER=${E2E_USER}" \
                            -e "E2E_TEST_PASSWORD=${E2E_PASS}" \
                            -e "PLAYWRIGHT_JUNIT_OUTPUT_NAME=playwright-results.xml" \
                            -w /workspace/frontend \
                            "$PLAYWRIGHT_C" \
                            pnpm exec playwright test \
                                --reporter=junit,html \
                                --trace=retain-on-failure || E2E_EXIT=$?

                        # Copiar artefactos antes de salir (pase o falle)
                        docker cp "$PLAYWRIGHT_C:/workspace/frontend/playwright-results.xml" \
                            "$WORKSPACE/frontend/playwright-results.xml" 2>/dev/null || true
                        docker cp "$PLAYWRIGHT_C:/workspace/frontend/playwright-report" \
                            "$WORKSPACE/frontend/playwright-report" 2>/dev/null || true
                        # test-results contiene trace.zip + error-context.md + screenshots por test fallido
                        docker cp "$PLAYWRIGHT_C:/workspace/frontend/test-results" \
                            "$WORKSPACE/frontend/test-results" 2>/dev/null || true

                        exit "$E2E_EXIT"
                    '''
                }
            }
            post {
                always {
                    junit allowEmptyResults: true, testResults: 'frontend/playwright-results.xml'
                    archiveArtifacts artifacts: 'frontend/playwright-report/**', allowEmptyArchive: true
                    // Artefactos de debug: trace.zip (abrir con `pnpm exec playwright show-trace`),
                    // error-context.md (snapshot del DOM al fallo) y screenshots por test fallido.
                    archiveArtifacts artifacts: 'frontend/test-results/**', allowEmptyArchive: true
                }
            }
        }

        // ── Deploy Dev ────────────────────────────────────────────────────────
        stage('Deploy — Dev') {
            when { branch 'development' }
            steps {
                withCredentials([
                    usernamePassword(
                        credentialsId: 'harbor-credentials',
                        usernameVariable: 'HARBOR_USER',
                        passwordVariable: 'HARBOR_PASS'
                    ),
                    string(credentialsId: 'dev-server-user', variable: 'DEV_SERVER_USER'),
                    string(credentialsId: 'dev-server-host', variable: 'DEPLOY_HOST'),
                    sshUserPrivateKey(
                        credentialsId: 'dev-server-ssh',
                        keyFileVariable: 'SSH_KEY',
                        usernameVariable: 'SSH_USER',
                        passphraseVariable: 'SSH_PASSPHRASE'
                    )
                ]) {
                    script {
                        def deployHost = env.DEPLOY_HOST?.trim()
                        def knownHostsFile = "${env.WORKSPACE}/.jenkins-a76-known_hosts"
                        sh "ssh-keyscan -T 15 -H '${deployHost}' > '${knownHostsFile}'"
                        def remote = [
                            name         : 'dev-server',
                            host         : deployHost,
                            user         : env.DEV_SERVER_USER ?: env.SSH_USER,
                            identityFile : env.SSH_KEY,
                            passphrase   : env.SSH_PASSPHRASE,
                            allowAnyHosts: false,
                            knownHosts   : knownHostsFile
                        ]
                        sshCommand remote: remote, command: """
                            set -euo pipefail
                            cd ${DEPLOY_PATH}
                            APP_VERSION=${env.APP_VERSION} docker compose -f docker-compose.prod.yml pull
                            APP_VERSION=${env.APP_VERSION} docker compose -f docker-compose.prod.yml up -d || {
                                echo '=== logs anexo76-backend ==='
                                docker logs --tail 100 anexo76-backend 2>&1 || true
                                echo '=== docker compose ps ==='
                                docker compose -f docker-compose.prod.yml ps -a 2>&1 || true
                                exit 1
                            }
                            docker image prune -f
                        """
                    }
                }
            }
        }

        // ── Smoke Test ────────────────────────────────────────────────────────
        stage('Smoke Test') {
            when { branch 'development' }
            steps {
                withCredentials([
                    string(credentialsId: 'dev-server-host', variable: 'DEPLOY_HOST'),
                    sshUserPrivateKey(
                        credentialsId: 'dev-server-ssh',
                        keyFileVariable: 'SSH_KEY',
                        usernameVariable: 'SSH_USER',
                        passphraseVariable: 'SSH_PASSPHRASE'
                    )
                ]) {
                    script {
                        def deployHost = env.DEPLOY_HOST?.trim()
                        def knownHostsFile = "${env.WORKSPACE}/.jenkins-a76-known_hosts"
                        sh "ssh-keyscan -T 15 -H '${deployHost}' > '${knownHostsFile}'"
                        def remote = [
                            name         : 'dev-server',
                            host         : deployHost,
                            user         : env.SSH_USER,
                            identityFile : env.SSH_KEY,
                            passphrase   : env.SSH_PASSPHRASE,
                            allowAnyHosts: false,
                            knownHosts   : knownHostsFile
                        ]
                        sshCommand remote: remote, command: '''
                            set -euo pipefail
                            for i in $(seq 1 12); do
                                STATUS=$(curl -sf -o /dev/null -w "%{http_code}" http://localhost:8000/api/health 2>/dev/null || true)
                                if [ "$STATUS" = "200" ]; then
                                    echo "Smoke test OK — HTTP $STATUS"
                                    exit 0
                                fi
                                echo "Intento $i/12 — HTTP ${STATUS:-000} — reintentando en 10s..."
                                sleep 10
                            done
                            echo "ERROR: Smoke test fallido tras 120s"
                            exit 1
                        '''
                    }
                }
            }
        }
    }

    post {
        always {
            sh "docker logout ${HARBOR_REGISTRY} || true"
        }
        success {
            echo "Pipeline OK — ${env.BRANCH_NAME} #${env.BUILD_NUMBER} (${env.APP_VERSION ?: 'n/a'})"
        }
        failure {
            echo "PIPELINE FALLIDO — ${env.BRANCH_NAME} #${env.BUILD_NUMBER}"
        }
    }
}
