#!/usr/bin/env bash
#
# build-xcframework.sh — build libvoicecat as a static .a slice (or slices) and stitch them
# into a VoiceCatCore.xcframework that the Swift Package at clients/apple/Package.swift
# consumes as a binary target.
#
# The XCFramework's Headers directory carries a generated module.modulemap alongside
# voicecat.h, so Swift gets a clean `import VoiceCatC` module (see docs/architecture.md §4 —
# "Swift / Apple. Import the C ABI via a module map"). core/include/ itself stays pure C;
# the module map is an Apple-packaging concern that lives only in the staged headers.
#
# Usage:
#   scripts/build-xcframework.sh                # macOS slice only (default, validated)
#   scripts/build-xcframework.sh --all          # macOS + iOS device + iOS sim (iOS still scaffolding)
#   scripts/build-xcframework.sh --preset apple-dev
#   scripts/build-xcframework.sh --no-configure # skip cmake configure, just rebuild + stitch
#
# Requires: VCPKG_ROOT set (or detectable from an existing build/apple-dev/CMakeCache.txt),
# Xcode + macOS SDK. iOS slices additionally require the iOS SDK. Install Homebrew
# autoconf-archive for vcpkg's libsodium port (see clients/apple/README.md).
#
# Mirrors the Windows client's convention: the native binary is a local build artifact, NOT
# committed — the C# project references build/windows-client/bin/voicecat.dll the same way
# this script's output at clients/apple/VoiceCatCore.xcframework is referenced by Package.swift.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# scripts/ is 3 levels below repo root: voice-cat/clients/apple/scripts/
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
APPLE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"

BUILD_MACOS=true
BUILD_IOS_DEVICE=false
BUILD_IOS_SIM=false
DO_CONFIGURE=true

while [[ $# -gt 0 ]]; do
    case "$1" in
        --all)        BUILD_MACOS=true; BUILD_IOS_DEVICE=true; BUILD_IOS_SIM=true; shift ;;
        --preset)     case "$2" in
                        apple-dev)     BUILD_MACOS=true;  BUILD_IOS_DEVICE=false; BUILD_IOS_SIM=false ;;
                        apple-ios)     BUILD_MACOS=false; BUILD_IOS_DEVICE=true;  BUILD_IOS_SIM=false ;;
                        apple-ios-sim) BUILD_MACOS=false; BUILD_IOS_DEVICE=false; BUILD_IOS_SIM=true  ;;
                        *) echo "unknown preset: $2" >&2; exit 2 ;;
                      esac; shift 2 ;;
        --no-configure) DO_CONFIGURE=false; shift ;;
        -h|--help)
            sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'
            exit 0 ;;
        *) echo "unknown arg: $1" >&2; exit 2 ;;
    esac
done

# ── Resolve VCPKG_ROOT ───────────────────────────────────────────────────────────
# The apple-dev CMake cache records the vcpkg root it was configured with (Z_VCPKG_ROOT_DIR);
# reuse that so a developer who already configured `cmake --preset dev` doesn't need VCPKG_ROOT
# in their shell env to run this script.
if [[ -z "${VCPKG_ROOT:-}" ]]; then
    cache="$REPO_ROOT/build/apple-dev/CMakeCache.txt"
    if [[ -f "$cache" ]]; then
        detected="$(grep -m1 '^Z_VCPKG_ROOT_DIR:INTERNAL=' "$cache" | cut -d= -f2-)"
        if [[ -n "$detected" && -d "$detected" ]]; then
            export VCPKG_ROOT="$detected"
        fi
    fi
fi
if [[ -z "${VCPKG_ROOT:-}" || ! -d "$VCPKG_ROOT" ]]; then
    echo "error: VCPKG_ROOT is not set or does not exist." >&2
    echo "       bootstrap vcpkg (https://vcpkg.io) then:  export VCPKG_ROOT=/path/to/vcpkg" >&2
    exit 1
fi
echo "[build-xcframework] VCPKG_ROOT=$VCPKG_ROOT"

# ── Build each requested slice ────────────────────────────────────────────────────
build_slice() {
    local preset="$1" slice_name="$2" vcpkg_triplet="$3"
    echo "[build-xcframework] === $slice_name: configure + build ($preset) ==="
    if $DO_CONFIGURE; then
        cmake --preset "$preset"
    fi
    cmake --build --preset "$preset"
    local lib="$REPO_ROOT/build/$preset/lib/libvoicecat.a"
    if [[ ! -f "$lib" ]]; then
        echo "error: expected output not found: $lib" >&2
        exit 1
    fi
    echo "[build-xcframework] $slice_name -> $lib ($(stat -f%z "$lib") bytes)"

    # The static libvoicecat.a only contains voicecat's own object files — vcpkg's static
    # deps (protobuf, mbedtls, libsodium, opus, sqlite3, spdlog, asio, abseil, …) are
    # separate .a files under vcpkg_installed/<triplet>/lib/. A Swift Package binary target
    # can only link ONE .a per XCFramework slice, so we merge them all into a single
    # self-contained "fat" static library using libtool -static. This is the Apple equivalent
    # of how the Windows client ships a single voicecat.dll with all deps statically linked
    # (via MinGW's -static flags in core/CMakeLists.txt).
    #
    # Without this, the final executable (test runner / app) would get undefined-symbol
    # linker errors for protobuf/mbedtls/sodium/opus/… symbols that libvoicecat.a references
    # but doesn't contain. See clients/apple/README.md "Fat static library" section.
    #
    # IMPORTANT: search only the TARGET triplet's lib dir, not all of vcpkg_installed/.
    # Cross-compile builds install a HOST triplet directory alongside the target (e.g.
    # arm64-osx/ next to arm64-ios/) — that host directory contains macOS binaries needed
    # to build protoc/etc at configure time. Merging those macOS .a files into the iOS
    # fat library triggers xcodebuild's "binaries with multiple platforms" rejection.
    local vcpkg_target_lib_dir="$REPO_ROOT/build/$preset/vcpkg_installed/$vcpkg_triplet/lib"
    local local_lib_dir="$REPO_ROOT/build/$preset/lib"
    local fat_lib="$REPO_ROOT/build/$preset/lib/libvoicecat-fat.a"
    echo "[build-xcframework] $slice_name: merging vcpkg + vendored deps into fat static lib (triplet: $vcpkg_triplet)"
    # Collect all .a files (libvoicecat.a + every vcpkg target .a + locally-built vendored static
    # libs). libtool -static concatenates object files from all input archives; duplicate-object
    # warnings are benign (the linker resolves duplicates at final link time). "no symbols"
    # warnings are for empty AVX2/AVX512 objects on arm64 — also benign.
    #
    # Two source dirs:
    #  - vcpkg_target_lib_dir: vcpkg-installed deps (protobuf, mbedtls, sodium, opus, …).
    #  - local_lib_dir: CMake static-lib targets built in this tree that are NOT vcpkg deps —
    #    e.g. the vendored RNNoise lib (third_party/rnnoise, VOICECAT_HAS_NS). Without it the
    #    fat lib references _rnnoise_* symbols that nothing defines → undefined-symbol link
    #    errors in Xcode. Exclude libvoicecat* (the main lib is $lib; libvoicecat-fat.a is the
    #    output we're building here).
    local all_libs=( "$lib" )
    while IFS= read -r f; do all_libs+=( "$f" ); done < <(find "$vcpkg_target_lib_dir" -name '*.a' -not -name 'libvoicecat*' | sort)
    while IFS= read -r f; do all_libs+=( "$f" ); done < <(find "$local_lib_dir" -maxdepth 1 -name '*.a' -not -name 'libvoicecat*' | sort)
    libtool -static -o "$fat_lib" "${all_libs[@]}" 2>&1 | grep -v 'has no symbols' || true
    echo "[build-xcframework] $slice_name -> $fat_lib ($(stat -f%z "$fat_lib") bytes, fat)"
}

args=()
if $BUILD_MACOS;      then build_slice apple-dev     "macOS (arm64-osx)"        "arm64-osx";           args+=( -library "$REPO_ROOT/build/apple-dev/lib/libvoicecat-fat.a"     -headers "$APPLE_DIR/.staged-headers/macos" ); fi
if $BUILD_IOS_DEVICE; then build_slice apple-ios     "iOS device (arm64-ios)"   "arm64-ios";           args+=( -library "$REPO_ROOT/build/apple-ios/lib/libvoicecat-fat.a"     -headers "$APPLE_DIR/.staged-headers/ios" ); fi
if $BUILD_IOS_SIM;    then build_slice apple-ios-sim "iOS sim (arm64-ios-sim)"  "arm64-ios-simulator"; args+=( -library "$REPO_ROOT/build/apple-ios-sim/lib/libvoicecat-fat.a" -headers "$APPLE_DIR/.staged-headers/ios-sim" ); fi

# ── Stage headers + module map ───────────────────────────────────────────────────
# Each slice gets its own headers dir (xcodebuild -create-xcframework requires a -headers
# per -library). The module map wraps voicecat.h as `module VoiceCatC` so Swift imports it
# as a clean named module rather than a Clang module inferred from the header path.
stage_headers() {
    local dest="$1"
    mkdir -p "$dest"
    cp "$REPO_ROOT/core/include/voicecat.h" "$dest/voicecat.h"
    cat > "$dest/module.modulemap" <<'MODULEMAP'
module VoiceCatC {
    header "voicecat.h"
    export *
}
MODULEMAP
    # voicecat.h is the single public C ABI header (core/include/ has nothing else); exposing
    # it as `module VoiceCatC` gives Swift a clean named import rather than a path-inferred
    # Clang module. The export * re-exports all C symbols for Swift access.
}

STAGED_ROOT="$APPLE_DIR/.staged-headers"
rm -rf "$STAGED_ROOT"
if $BUILD_MACOS;      then stage_headers "$STAGED_ROOT/macos";    fi
if $BUILD_IOS_DEVICE; then stage_headers "$STAGED_ROOT/ios";      fi
if $BUILD_IOS_SIM;    then stage_headers "$STAGED_ROOT/ios-sim";  fi

# ── Stitch the XCFramework ───────────────────────────────────────────────────────
OUTPUT="$APPLE_DIR/VoiceCatCore.xcframework"
echo "[build-xcframework] === stitching $OUTPUT ==="
rm -rf "$OUTPUT"
xcodebuild -create-xcframework "${args[@]}" -output "$OUTPUT"

# Clean up staged headers (the xcframework has its own copy now).
rm -rf "$STAGED_ROOT"

# ── Code-sign ────────────────────────────────────────────────────────────────────
# Xcode 15+ rejects unsigned binary XCFrameworks consumed as SwiftPM binary targets
# ("The Framework VoiceCatCore.xcframework is unsigned."). Sign with CODESIGN_IDENTITY
# if set, else the first "Apple Development" identity in the keychain, else ad-hoc ("-").
# The .xcframework is a local build artifact (gitignored), so signing with the local
# developer identity is fine and not committed.
SIGN_ID="${CODESIGN_IDENTITY:-}"
if [[ -z "$SIGN_ID" ]]; then
    SIGN_ID="$(security find-identity -v -p codesigning 2>/dev/null \
                | awk '/Apple Development/{print $2; exit}')"
fi
SIGN_ID="${SIGN_ID:--}"   # fall back to ad-hoc
codesign --force --timestamp=none --sign "$SIGN_ID" "$OUTPUT"
echo "[build-xcframework] signed with identity: $SIGN_ID"

echo "[build-xcframework] done -> $OUTPUT"
xcodebuild -list -xcframework "$OUTPUT" 2>/dev/null || true
