#!/usr/bin/env bash
#
# scripts/ci-android-sign.sh
# ---------------------------------------------------------------------------
# Wire a RELEASE signing config into a freshly-prebuilt Android project.
#
# Run this AFTER `expo prebuild --platform android` has generated ./android,
# and BEFORE `./gradlew assembleRelease`.
#
# It does two things, both IDEMPOTENTLY (safe to re-run):
#
#   (a) Decodes the base64 keystore from $ANDROID_KEYSTORE_BASE64 into
#       android/app/wisp.keystore.
#
#   (b) Patches android/app/build.gradle to:
#         - add a `signingConfigs.release { ... }` block that reads the keystore
#           path + passwords from Gradle properties (which we source from env),
#         - point `buildTypes.release.signingConfig` at it.
#       The patch is marker-based: it only injects once, and re-running detects
#       the marker and skips.
#
# Secrets / env consumed (provided by CI as repository secrets, exported into
# the build container's environment):
#       ANDROID_KEYSTORE_BASE64     base64 of the .keystore file
#       ANDROID_KEYSTORE_PASSWORD   keystore (store) password
#       ANDROID_KEY_ALIAS           key alias (we generate keystores with alias 'wisp')
#       ANDROID_KEY_PASSWORD        key (alias) password
#
# Generate the keystore + learn the exact secret-setting commands with
# scripts/gen-keystore.sh. Full runbook: docs/DEPLOY.md.
# ---------------------------------------------------------------------------

# Strict mode: exit on error, error on unset vars, fail pipelines on any stage.
set -euo pipefail

# --- Resolve paths relative to the repo root (this script lives in scripts/) ---
# So the script works regardless of the caller's current working directory.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." >/dev/null 2>&1 && pwd)"

ANDROID_DIR="${REPO_ROOT}/android"
APP_DIR="${ANDROID_DIR}/app"
GRADLE_FILE="${APP_DIR}/build.gradle"
# Keep the keystore filename in sync with the signingConfigs block we inject.
KEYSTORE_FILE="${APP_DIR}/wisp.keystore"
# Unique marker so we can detect a prior injection and stay idempotent.
MARKER="// WISP_RELEASE_SIGNING (managed by scripts/ci-android-sign.sh)"

# --- Small logging helpers --------------------------------------------------
log()  { printf '[ci-android-sign] %s\n' "$*"; }
fail() { printf '[ci-android-sign] ERROR: %s\n' "$*" >&2; exit 1; }

# --- Preconditions ----------------------------------------------------------
# The android project must already exist (run `expo prebuild` first).
[[ -d "${ANDROID_DIR}" ]] || fail "android/ not found. Run 'expo prebuild --platform android' first."
[[ -f "${GRADLE_FILE}" ]] || fail "Not found: ${GRADLE_FILE}"

# Validate required secrets are present (don't print their values).
: "${ANDROID_KEYSTORE_BASE64:?ANDROID_KEYSTORE_BASE64 is required (base64 of the keystore)}"
: "${ANDROID_KEYSTORE_PASSWORD:?ANDROID_KEYSTORE_PASSWORD is required}"
: "${ANDROID_KEY_ALIAS:?ANDROID_KEY_ALIAS is required}"
: "${ANDROID_KEY_PASSWORD:?ANDROID_KEY_PASSWORD is required}"

# ---------------------------------------------------------------------------
# (a) Decode the keystore from base64 into android/app/wisp.keystore
# ---------------------------------------------------------------------------
log "Decoding keystore -> ${KEYSTORE_FILE}"
# `base64 -d` reads the secret from stdin. We feed it via printf to avoid an
# "Argument list too long" / arg-leak from putting the blob on the command line.
# Tolerate base64 with or without newlines/whitespace (-w0 producers vs. wrapped).
printf '%s' "${ANDROID_KEYSTORE_BASE64}" | base64 -d > "${KEYSTORE_FILE}" \
    || fail "Failed to base64-decode ANDROID_KEYSTORE_BASE64"

# Sanity check: a real JKS/PKCS12 keystore is more than a few bytes.
if [[ ! -s "${KEYSTORE_FILE}" ]] || [[ "$(wc -c < "${KEYSTORE_FILE}")" -lt 256 ]]; then
    fail "Decoded keystore looks too small — is ANDROID_KEYSTORE_BASE64 correct?"
fi
# Lock down the keystore file permissions (best effort).
chmod 600 "${KEYSTORE_FILE}" || true
log "Keystore decoded ($(wc -c < "${KEYSTORE_FILE}") bytes)."

# ---------------------------------------------------------------------------
# (b1) Make the signing credentials available to Gradle.
# ---------------------------------------------------------------------------
# We DON'T hardcode passwords in build.gradle. Instead the injected Groovy reads
# project properties (project.findProperty(...)) which we supply by writing to
# the project-local gradle.properties. This keeps secrets out of the source and
# out of the process list. `assembleRelease` also still works if a CI step
# passes the same values via -P flags.
#
# We APPEND our properties under a marker and strip any previous managed block
# first, so re-runs don't accumulate duplicates.
GRADLE_PROPS="${ANDROID_DIR}/gradle.properties"
PROPS_MARKER="# WISP_RELEASE_SIGNING_PROPS (managed by scripts/ci-android-sign.sh)"

touch "${GRADLE_PROPS}"
# Remove any previously-managed block (from the marker line to EOF). Using a temp
# file keeps this portable across GNU/BSD sed.
if grep -qF "${PROPS_MARKER}" "${GRADLE_PROPS}"; then
    log "Refreshing managed signing props in gradle.properties"
    # Print everything BEFORE the marker line, then we re-append the fresh block.
    awk -v m="${PROPS_MARKER}" 'index($0, m){found=1} !found{print}' \
        "${GRADLE_PROPS}" > "${GRADLE_PROPS}.tmp"
    mv "${GRADLE_PROPS}.tmp" "${GRADLE_PROPS}"
fi

# Append the fresh managed properties block. The store FILE path is relative to
# the app module (android/app), so just the filename is correct.
{
    printf '\n%s\n' "${PROPS_MARKER}"
    printf 'WISP_RELEASE_STORE_FILE=wisp.keystore\n'
    printf 'WISP_RELEASE_STORE_PASSWORD=%s\n' "${ANDROID_KEYSTORE_PASSWORD}"
    printf 'WISP_RELEASE_KEY_ALIAS=%s\n' "${ANDROID_KEY_ALIAS}"
    printf 'WISP_RELEASE_KEY_PASSWORD=%s\n' "${ANDROID_KEY_PASSWORD}"
} >> "${GRADLE_PROPS}"
chmod 600 "${GRADLE_PROPS}" || true

# ---------------------------------------------------------------------------
# (b2) Inject the signingConfigs.release block + wire buildTypes.release.
# ---------------------------------------------------------------------------
# Idempotency: if our marker is already present, the gradle file is patched.
if grep -qF "${MARKER}" "${GRADLE_FILE}"; then
    log "build.gradle already patched (marker present) — skipping injection."
    exit 0
fi

# The Groovy snippet we add INSIDE the existing `signingConfigs { ... }` block.
# It reads from the gradle.properties values written above. We use def-with-
# fallback so a missing property yields a clear gradle error rather than NPE.
# `IFS=` keeps `read` from trimming the leading indentation of the first line.
IFS= read -r -d '' RELEASE_SIGNING_CONFIG <<'GROOVY' || true
        release {
            // -- WISP managed release signing config --
            // Values come from android/gradle.properties (written by
            // scripts/ci-android-sign.sh from CI secrets). storeFile is resolved
            // relative to this module (android/app).
            def wispStoreFile = project.findProperty('WISP_RELEASE_STORE_FILE') ?: 'wisp.keystore'
            storeFile file(wispStoreFile)
            storePassword project.findProperty('WISP_RELEASE_STORE_PASSWORD')
            keyAlias project.findProperty('WISP_RELEASE_KEY_ALIAS')
            keyPassword project.findProperty('WISP_RELEASE_KEY_PASSWORD')
        }
GROOVY

# We patch with a small, robust Node script (Node is present in the build image
# via Bun/Expo tooling, and string surgery is far safer in JS than in sed for
# multi-line, brace-sensitive Groovy). It performs two edits:
#
#   1. Insert the `release { ... }` config as the FIRST entry inside the
#      `signingConfigs {` block (the prebuild template ships a `debug` config
#      there; we add `release` alongside it).
#   2. Inside the `buildTypes { ... }`'s `release { ... }` block SPECIFICALLY,
#      set `signingConfig signingConfigs.release`. We must scope to the release
#      block via brace matching: a naive global replace of
#      `signingConfig signingConfigs.debug` would also (wrongly) rewrite the
#      DEBUG build type, which legitimately keeps the debug signing config.
#      (The Expo/RN template defaults release to the debug signing config so that
#      `assembleRelease` works out-of-the-box; we override only the release one.)
#
# Both edits are guarded so the script fails LOUDLY if the expected anchors are
# missing (e.g. a future template change), rather than silently producing an
# unsigned/debug-signed APK.
log "Patching ${GRADLE_FILE}"

WISP_MARKER="${MARKER}" \
WISP_RELEASE_BLOCK="${RELEASE_SIGNING_CONFIG}" \
WISP_GRADLE_FILE="${GRADLE_FILE}" \
node <<'NODE'
const fs = require('fs');

const file   = process.env.WISP_GRADLE_FILE;
const marker = process.env.WISP_MARKER;
const block  = process.env.WISP_RELEASE_BLOCK;

let src = fs.readFileSync(file, 'utf8');

// --- Edit 1: insert `release { ... }` inside `signingConfigs { ... }` --------
// Match the literal `signingConfigs {` opener (Groovy DSL). We insert right
// after the opening brace so our release config sits beside the template's
// `debug` config.
const scRe = /signingConfigs\s*\{/;
if (!scRe.test(src)) {
  console.error("[ci-android-sign] Could not find `signingConfigs {` in build.gradle.");
  process.exit(1);
}
src = src.replace(
  scRe,
  (m) => `${m}\n        ${marker}\n${block}`
);

// --- Helper: find the body span of `<keyword> {` via brace matching ----------
// Returns {open, close} string indices of the block's content (between the
// matching braces) for the FIRST occurrence of `<keyword> {` starting at/after
// `fromIndex`, or null if not found / unbalanced. This lets us scope edits to a
// specific nested block (e.g. the `release` block INSIDE `buildTypes`) instead
// of relying on greedy regexes that cross block boundaries.
function findBlockBody(text, keyword, fromIndex = 0) {
  const opener = new RegExp(`\\b${keyword}\\s*\\{`, 'g');
  opener.lastIndex = fromIndex;
  const m = opener.exec(text);
  if (!m) return null;
  // Index of the `{` that opens the block.
  let i = m.index + m[0].length - 1;
  let depth = 0;
  for (; i < text.length; i++) {
    const ch = text[i];
    if (ch === '{') depth++;
    else if (ch === '}') {
      depth--;
      if (depth === 0) {
        // body spans (just after the opening brace) .. (this closing brace)
        return { open: m.index + m[0].length, close: i };
      }
    }
  }
  return null; // unbalanced braces
}

// --- Edit 2: point the RELEASE buildType at signingConfigs.release -----------
// Scope to buildTypes { ... } first, then to its release { ... } sub-block.
const buildTypes = findBlockBody(src, 'buildTypes');
if (!buildTypes) {
  console.error("[ci-android-sign] Could not find buildTypes { } in build.gradle.");
  process.exit(1);
}
const release = findBlockBody(src, 'release', buildTypes.open);
// Guard: the release block must lie INSIDE buildTypes (defensive against a
// future template where `release` appears elsewhere first).
if (!release || release.close > buildTypes.close) {
  console.error("[ci-android-sign] Could not find buildTypes.release { } in build.gradle.");
  process.exit(1);
}

let releaseBody = src.slice(release.open, release.close);
if (/signingConfig\s+signingConfigs\.debug/.test(releaseBody)) {
  // Normal case: swap the template default (debug) for our release config.
  releaseBody = releaseBody.replace(
    /signingConfig\s+signingConfigs\.debug/,
    'signingConfig signingConfigs.release'
  );
} else if (/signingConfig\s+signingConfigs\.release/.test(releaseBody)) {
  // Already points at release (e.g. a template that ships it) — nothing to do.
} else {
  // No signingConfig line present — inject one as the first statement.
  releaseBody = '\n            signingConfig signingConfigs.release' + releaseBody;
}
src = src.slice(0, release.open) + releaseBody + src.slice(release.close);

fs.writeFileSync(file, src);
console.log("[ci-android-sign] build.gradle patched: added signingConfigs.release and wired release buildType.");
NODE

log "Done. Release signing is configured. You can now run: (cd android && ./gradlew assembleRelease)"
