// Jenkins pipeline: test the Device Cloud Platform (Python / uv workspace),
// build the deployable Docker image (Cloud API + Host Agent share one image;
// the entrypoint is selected via the Compose `command`), smoke-test the
// container, and optionally push it to a registry.
//
// Requires the Docker Pipeline plugin and a Docker-capable agent.
pipeline {
  agent any

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

  parameters {
    string(name: 'IMAGE_NAME',    defaultValue: 'q792602257/agentic-mobile-control', description: 'Image repository name')
    string(name: 'REGISTRY',     defaultValue: 'git.jerryyan.net',                  description: 'Registry host, e.g. registry.example.com. Empty = local build only.')
    string(name: 'UV_INDEX_URL', defaultValue: 'https://mirrors.aliyun.com/pypi/simple', description: 'Optional PyPI mirror index URL for uv sync (empty = uv default)')
    booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: 'Skip the uv sync/pytest stage (faster builds when only packaging)')
    booleanParam(name: 'PUSH',    defaultValue: true,     description: 'Push the image to REGISTRY after a successful build')

    // Mirror overrides passed to `docker build` as --build-arg. Defaults target
    // CN networks so Jenkins builds don't time out pulling from npmjs.org /
    // deb.debian.org / PyPI. Override or blank any of these to build against
    // the official upstreams. Base images (node, uv) are pinned in the
    // Dockerfile FROM lines and no longer overridable here.
    string(name: 'NPM_REGISTRY', defaultValue: 'https://registry.npmmirror.com', description: 'npm registry URL used by `npm ci`')
    string(name: 'APT_MIRROR',   defaultValue: 'mirrors.aliyun.com',           description: 'Debian apt mirror host (e.g. mirrors.aliyun.com). Empty = deb.debian.org')
  }

  environment {
    // Short commit for a traceable, immutable tag alongside :latest.
    GIT_SHA    = sh(script: 'git rev-parse --short HEAD 2>/dev/null || echo unknown', returnStdout: true).trim()
    IMAGE_TAG  = "${env.BUILD_NUMBER}-${GIT_SHA}"
    FULL_IMAGE = "${params.REGISTRY?.trim() ? params.REGISTRY.trim() + '/' : ''}${params.IMAGE_NAME}"
  }

  stages {
    stage('Checkout') {
      steps {
        checkout scm
        // Pre-create the host-side uv cache dir as the Jenkins user *before* the
        // 'Test' stage's docker agent bind-mounts it. If this dir doesn't exist
        // yet, Docker auto-creates it as root when the container starts, which
        // then blocks writes from the non-root user the container runs as.
        sh 'mkdir -p "$HOME/.cache/uv"'
      }
    }

    // Root, cloud-api, and host-agent suites, excluding tests that need real
    // hardware/PostgreSQL/device services (see tests/ pytest marker "integration").
    stage('Test') {
      when { expression { return !params.SKIP_TESTS } }
      agent {
        docker {
          image 'registry-ghcr.jerryyan.top/astral-sh/uv:python3.13-bookworm-slim'
          reuseNode true
          // The Docker Pipeline plugin runs the container as the Jenkins host
          // user (non-root), so /root isn't writable. Cache under /tmp, which is
          // world-writable, and mount the persistent host cache dir there.
          args '-v $HOME/.cache/uv:/tmp/uv-cache'
        }
      }
      environment {
        // Pin HOME/cache explicitly for the same reason as the cache mount above.
        HOME         = '/tmp'
        UV_CACHE_DIR = '/tmp/uv-cache'
        UV_LINK_MODE = 'copy'
        UV_INDEX_URL = "${params.UV_INDEX_URL}"
      }
      steps {
        sh 'uv sync --locked --all-packages'
        sh '''
          uv run pytest tests apps/cloud-api/tests apps/device-host-agent/tests \
            -m "not integration" -ra --junit-xml=test-results.xml
        '''
      }
      post {
        always {
          junit testResults: 'test-results.xml', allowEmptyResults: true
        }
      }
    }

    stage('Build image') {
      steps {
        script {
          // Pass every mirror override through as --build-arg. Empty values
          // are skipped so the Dockerfile ARG default applies.
          def buildArgs = []
          ["NPM_REGISTRY", "APT_MIRROR", "UV_INDEX_URL"].each { name ->
            def v = params[name]?.toString()?.trim()
            if (v) {
              buildArgs << "--build-arg ${name}=${v}"
            }
          }
          def dockerArgs = (buildArgs.join(' ') + ' .').trim()
          def img = docker.build("${FULL_IMAGE}:${IMAGE_TAG}", dockerArgs)
          img.tag('latest')
          env.BUILT_IMAGE = "${FULL_IMAGE}:${IMAGE_TAG}"
          echo "Built ${env.BUILT_IMAGE} (+ :latest)"
        }
      }
    }

    // Confirm both packaged console scripts start up before shipping the image.
    // A full boot needs PostgreSQL (cloud-api runs Alembic migrations on
    // start), so this checks the entrypoints resolve and import cleanly
    // instead of booting the real service.
    stage('Smoke test') {
      steps {
        sh '''
          set -eu
          docker run --rm "${BUILT_IMAGE}" device-cloud-api --help >/dev/null
          docker run --rm "${BUILT_IMAGE}" device-host-agent --help >/dev/null
          echo "smoke test OK"
        '''
      }
    }

    stage('Push') {
      when { expression { return params.PUSH } }
      steps {
        script {
          def registryUrl = params.REGISTRY?.trim() ? "https://${params.REGISTRY.trim()}" : ''
          docker.withRegistry(registryUrl, 'gitea-registry') {
            def img = docker.image("${FULL_IMAGE}:${IMAGE_TAG}")
            img.push()
            img.push('latest')
          }
          echo "Pushed ${FULL_IMAGE}:${IMAGE_TAG} and :latest"
        }
      }
    }
  }

  post {
    always {
      // Free local disk: drop this build's tag and any dangling layers.
      sh '''
        [ -n "${BUILT_IMAGE:-}" ] && docker rmi "${BUILT_IMAGE}" >/dev/null 2>&1 || true
        docker image prune -f >/dev/null 2>&1 || true
      '''
      cleanWs()
    }
    success { echo "OK: ${env.FULL_IMAGE}:${env.IMAGE_TAG}" }
    failure { echo 'Build failed — see stage logs above.' }
  }
}
