# Manual signed-APK build for the standalone Android app (RuStore). Runs ONLY from master, ONLY on
# workflow_dispatch with confirm=build — the same deliberate-manual shape as prod-deploy.yaml, never on
# a PR. It builds the native-flavoured SPA, bundles the offline dictionaries into the APK assets, and
# assembles a release APK, uploaded as a run artifact (RuStore upload stays manual for the MVP).
#
# Signing degrades gracefully: with the ANDROID_RUSTORE_* secrets present the APK is signed; without
# them build.gradle produces an UNSIGNED release APK (so a dry run still proves the whole pipeline).
# The keystore is a publication prerequisite — see deploy/README.md (Android build/release runbook).
# The toolchain is self-provisioned here (JDK 21 + a cached Android SDK), so the runner host needs
# nothing pre-installed.
name: android-build
run-name: "android build ${{ github.sha }}"

on:
  workflow_dispatch:
    inputs:
      confirm:
        description: 'Type "build" to confirm an APK build from master.'
        required: true
        default: ""

permissions:
  contents: read

env:
  NO_COLOR: "1"
  # The dictionary release, one source of truth (same Gitea variable the backend image + CI use). It
  # both fetches the DAWGs and labels the bundled files (VITE_DICT_VERSION must equal __DICT_VERSION__).
  DICT_VERSION: ${{ vars.DICT_VERSION }}
  VITE_DICT_VERSION: ${{ vars.DICT_VERSION }}
  # Hide in-app purchases in the RuStore MVP (RuStore, not Google Play — VITE_GP_BUILD stays unset).
  VITE_PAYMENTS_DISABLED: "1"
  # The update overlay's store target; empty until publication (the button no-ops, and the version gate
  # is dormant in the MVP so it never fires). Set the ANDROID_RUSTORE_URL variable when the app is live.
  VITE_RUSTORE_URL: ${{ vars.ANDROID_RUSTORE_URL }}
  # Host-executor runner: the Android SDK is pre-installed on the host. Override ANDROID_SDK_DIR if it
  # lives elsewhere (the default matches deploy/README.md's install path).
  ANDROID_HOME: ${{ vars.ANDROID_SDK_DIR || '/opt/android-sdk' }}
  ANDROID_SDK_ROOT: ${{ vars.ANDROID_SDK_DIR || '/opt/android-sdk' }}

jobs:
  build:
    if: ${{ github.ref == 'refs/heads/master' && inputs.confirm == 'build' }}
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      # Host-executor runner: the Android SDK is pre-installed on the host (JDK 21 comes from setup-java
      # below). Fail fast + legibly if the runner user cannot read/execute it or a needed package is
      # missing — this doubles as the runner-access check (deploy/README.md).
      - name: Verify the host Android SDK
        run: |
          sm="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager"
          if [ ! -x "$sm" ]; then
            echo "::error::sdkmanager not found/executable at $sm — set the ANDROID_SDK_DIR variable if the SDK lives elsewhere, or grant the runner user read+exec: sudo chmod -R a+rX \"$ANDROID_HOME\""; exit 1
          fi
          for pkg in "platforms/android-36" "build-tools"; do
            if [ ! -d "$ANDROID_HOME/$pkg" ]; then
              echo "::error::missing $ANDROID_HOME/$pkg — run: \"$sm\" 'platforms;android-36' 'build-tools;36.0.0'"; exit 1
            fi
          done
          echo "Android SDK OK at $ANDROID_HOME"; "$sm" --version

      - name: Compute version + native build env
        id: prep
        env:
          PUBLIC_BASE_URL: ${{ vars.PROD_PUBLIC_BASE_URL }}
        run: |
          # A store release must sit on an exact vMAJOR.MINOR.PATCH tag (G tags before dispatch) so the
          # versionCode is deterministic and strictly increasing across uploads. Refuse anything else
          # rather than derive a versionCode from a "-N-gSHA" describe.
          desc="$(git describe --tags --exact-match 2>/dev/null || true)"
          case "$desc" in
            v[0-9]*.[0-9]*.[0-9]*) ;;
            *) echo "::error::HEAD is not on a clean vX.Y.Z tag (git describe --exact-match = '${desc:-none}'); tag the release first"; exit 1 ;;
          esac
          v="${desc#v}"
          IFS=. read -r MA MI PA <<< "$v"
          # 10# forces base-10 so a zero-padded part is never read as octal.
          code=$(( 10#$MA * 1000000 + 10#$MI * 1000 + 10#$PA ))
          # The native SPA talks to the production origin (reuse the prod public base URL); strip any
          # trailing slash so the Connect endpoint never doubles it.
          gateway="${PUBLIC_BASE_URL%/}"
          {
            echo "tag=$desc"
            echo "name=$v"
            echo "code=$code"
            echo "gateway=$gateway"
          } >> "$GITHUB_OUTPUT"
          echo "release $desc -> versionName $v versionCode $code, gateway $gateway"

      # Same release + fetch as the Go/UI jobs in ci.yaml — the bundled dicts come from this tarball,
      # NOT the scrabble-solver sibling (ui is a Node project outside go.work).
      - name: Fetch dictionary DAWGs
        run: |
          mkdir -p "${GITHUB_WORKSPACE}/dawg"
          curl -fsSL -o /tmp/dawg.tar.gz "https://gitea.iliadenisov.ru/developer/scrabble-dictionary/releases/download/${DICT_VERSION}/scrabble-dawg-${DICT_VERSION}.tar.gz"
          tar xzf /tmp/dawg.tar.gz -C "${GITHUB_WORKSPACE}/dawg"
          ls -la "${GITHUB_WORKSPACE}/dawg"

      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: 22

      - name: Install pnpm
        run: npm install -g pnpm@11.0.9

      - name: Install deps
        working-directory: ui
        run: pnpm install --frozen-lockfile

      - name: Build the SPA (native flavour)
        working-directory: ui
        env:
          VITE_GATEWAY_URL: ${{ steps.prep.outputs.gateway }}
          VITE_APP_VERSION: ${{ steps.prep.outputs.tag }}
        run: pnpm run build

      # Copy the release DAWGs into dist/dict/<variant>@<version>.dawg for the offline-first bundled tier
      # (after the build, before cap sync copies dist/ into the native assets).
      - name: Bundle the dictionaries
        working-directory: ui
        env:
          DICT_DIR: ${{ github.workspace }}/dawg
        run: node scripts/bundle-dicts.mjs

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "21"

      # Local cap binary (dodges the corepack pre-flight flake); syncs dist/ (incl. dict/) + native deps.
      - name: Sync the native project
        working-directory: ui
        run: node_modules/.bin/cap sync android

      - name: Decode the release keystore
        id: keystore
        env:
          ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_RUSTORE_KEYSTORE_BASE64 }}
        run: |
          if [ -n "$ANDROID_KEYSTORE_BASE64" ]; then
            printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 -d > "${GITHUB_WORKSPACE}/release.jks"
            echo "file=${GITHUB_WORKSPACE}/release.jks" >> "$GITHUB_OUTPUT"
            echo "keystore decoded -> signed release build"
          else
            echo "file=" >> "$GITHUB_OUTPUT"
            echo "::warning::ANDROID_RUSTORE_KEYSTORE_BASE64 secret is not set — building an UNSIGNED release APK (not installable/publishable)"
          fi

      - name: Assemble the release APK
        working-directory: ui/android
        env:
          ANDROID_KEYSTORE_FILE: ${{ steps.keystore.outputs.file }}
          ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_RUSTORE_KEYSTORE_PASSWORD }}
          ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_RUSTORE_KEY_ALIAS }}
          ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_RUSTORE_KEY_PASSWORD }}
        run: ./gradlew assembleRelease -PversionCode=${{ steps.prep.outputs.code }} -PversionName=${{ steps.prep.outputs.name }} --console=plain

      - name: Upload the APK artifact
        uses: actions/upload-artifact@v4
        with:
          name: erudit-${{ steps.prep.outputs.name }}-apk
          path: ui/android/app/build/outputs/apk/release/*.apk
          if-no-files-found: error
