#!/bin/bash
# Common build logic for all platforms.
# Platform scripts source this file and implement four functions:
#   install_system_deps()       — install OS packages (apt / brew / winget)
#   bundle_python_impl()        — bundle Python runtime
#   bundle_binaries_impl()      — bundle system binaries (ffmpeg etc.)
#   get_expected_artifacts()    — globs verify_artifacts checks at the end

set -euo pipefail

# Check if this is being sourced by a platform script
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    echo "Error: build-common.sh should not be run directly" >&2
    echo "Run ./build-release.sh instead" >&2
    exit 1
fi

# Script directory must be set by sourcing script
if [[ -z "${SCRIPT_DIR:-}" ]]; then
    echo "Error: SCRIPT_DIR not set by sourcing script" >&2
    exit 1
fi

# is_skipped_lib() — glibc/loader skip list, shared verbatim with
# bundle-binaries.sh so the bundler and the audit never disagree.
source "$SCRIPT_DIR/bundled-lib-skiplist.sh"

# Colors
if [[ -z "${RED:-}" ]]; then
    RED='\033[0;31m'
    GREEN='\033[0;32m'
    YELLOW='\033[1;33m'
    BLUE='\033[0;34m'
    NC='\033[0m'
fi

# Check that required variables are set
if [[ -z "${PROJECT_DIR:-}" ]]; then
    echo "Error: PROJECT_DIR not set by sourcing script" >&2
    exit 1
fi

if [[ -z "${PLATFORM:-}" ]]; then
    echo "Error: PLATFORM not set by sourcing script" >&2
    exit 1
fi

# Ensure platform is lowercase
PLATFORM="$(echo "$PLATFORM" | tr '[:upper:]' '[:lower:]')"

# Validate platform
if [[ ! "$PLATFORM" =~ ^(linux|macos|windows)$ ]]; then
    echo -e "${RED}Error: Invalid platform: $PLATFORM${NC}" >&2
    exit 1
fi

# Configuration file
CONFIG="$PROJECT_DIR/.build-config.json"
PARSE_CONFIG="$SCRIPT_DIR/parse-build-config.py"

# Check config file
if [[ ! -f "$CONFIG" ]]; then
    echo -e "${RED}Error: $CONFIG not found${NC}" >&2
    exit 1
fi

if ! python3 "$PARSE_CONFIG" "$CONFIG" >/dev/null; then
    echo -e "${RED}Error: $CONFIG is not valid JSON${NC}" >&2
    exit 1
fi

get_cfg() { python3 "$PARSE_CONFIG" "$CONFIG" "$1"; }

# --- Platform functions (to be implemented by platform scripts) ---
# Platform scripts MUST implement these three functions:
# install_system_deps()
# bundle_python_impl()
# bundle_binaries_impl()

# --- Common Build Steps ---

# Clone Slopsmith and plugins (shared across all platforms)
clone_slopsmith() {
	# RUNNER_TEMP on Windows runners is a native Windows path
	# (e.g. `D:\a\_temp`) that Git Bash / MSYS tools don't reliably
	# treat as a filesystem path. POSIX `/tmp/slopsmith` is the
	# default; if a non-POSIX environment really wants RUNNER_TEMP, it
	# can pass an explicit clone_dir argument (resolved via cygpath -u
	# on Windows if needed).
	local clone_dir="${1:-/tmp/slopsmith}"

	# Skip if already set for local development
	if [[ -n "${SLOPSMITH_DIR:-}" ]] && [[ -d "$SLOPSMITH_DIR" ]]; then
		echo "Using existing SLOPSMITH_DIR: $SLOPSMITH_DIR"
		return 0
	fi

	# Make the clone re-runnable: a leftover dir from a previous failed
	# build would otherwise abort `git clone`. CI runners start fresh so
	# this is purely a quality-of-life fix for local re-runs.
	if [[ -d "$clone_dir" ]]; then
		rm -rf "$clone_dir"
	fi

	# SLOPSMITH_REF selects the core branch/tag to bundle (set by the
	# Build workflow's slopsmith_ref input). Defaults to main so local
	# builds and the push/tag CI paths behave exactly as before.
	# --branch accepts either a branch or a tag, both shallow-cloneable.
	local slopsmith_ref="${SLOPSMITH_REF:-main}"
	local _auth=""
	[[ -n "${GH_CLONE_TOKEN:-}" ]] && _auth="x-access-token:${GH_CLONE_TOKEN}@"
	echo "Cloning Slopsmith repository (ref: ${slopsmith_ref})..."
	git clone --depth 1 --branch "$slopsmith_ref" "https://${_auth}github.com/got-feedback/feedback.git" "$clone_dir"

	# Remove broken symlinks from plugins dir
	find "$clone_dir/plugins" -maxdepth 1 -type l -delete 2>/dev/null || true

	# Clone bundled plugins. Format per entry:
	#   <owner>/<repo>[@<branch>][:<dirname>]
	# Dirname defaults to <repo> minus the "feedback-plugin-" prefix
	# with hyphens replaced by underscores (slopsmith treats plugin
	# directories as Python module names, which can't contain dashes).
	# Provide an explicit dirname after a colon for repos that don't
	# follow the feedback-plugin-* naming convention. An optional
	# @<branch> clones a non-default branch (used to ship in-review
	# plugin work in a feature-branch test build).
	cd "$clone_dir/plugins"
	local plugins=(
		# Bundled plugins — all under the got-feedback org after the migration.
		got-feedback/feedback-plugin-drum-highway-3d
		got-feedback/feedback-plugin-drums
		got-feedback/feedback-plugin-editor
		got-feedback/feedback-plugin-find-more
		got-feedback/feedback-plugin-flappy-bend
		got-feedback/feedback-plugin-fretboard
		got-feedback/feedback-plugin-guitar-theory
		got-feedback/feedback-plugin-invert-highway
		got-feedback/feedback-plugin-jumpingtab
		got-feedback/feedback-plugin-keys-highway-3d
		got-feedback/feedback-plugin-loosefolder:loose_folder
		got-feedback/feedback-plugin-lyrics-karaoke
		got-feedback/feedback-plugin-metronome
		got-feedback/feedback-plugin-midi
		got-feedback/feedback-plugin-multiplayer
		got-feedback/feedback-plugin-musicxml-import
		got-feedback/feedback-plugin-nam-tone
		got-feedback/feedback-plugin-notedetect
		got-feedback/feedback-plugin-piano
		got-feedback/feedback-plugin-practice
		got-feedback/feedback-plugin-sectionmap
		got-feedback/feedback-plugin-setlist
		got-feedback/feedback-plugin-song-preview
		got-feedback/feedback-plugin-splitscreen
		got-feedback/feedback-plugin-staffview
		got-feedback/feedback-plugin-stem-mixer
		got-feedback/feedback-plugin-stems
		got-feedback/feedback-plugin-stepmode
		got-feedback/feedback-plugin-strum-fighter
		got-feedback/feedback-plugin-studio
		got-feedback/feedback-plugin-tabview
		got-feedback/feedback-plugin-themes
		got-feedback/feedback-plugin-transpose-chords
		got-feedback/feedback-plugin-tutorials
		got-feedback/feedback-plugin-update-manager
		got-feedback/feedback-plugin-virtuoso
		# Rig Builder (NAM tone builder) — repo is not under the feedback-plugin-* prefix.
		got-feedback/rig_builder
	)

	local total=0
	local cloned=0
	for entry in "${plugins[@]}"; do
		total=$((total + 1))
		# Split off an optional ":<dirname>" then an optional "@<branch>".
		# Git branch names can't contain ':' so the dirname split is safe
		# to do first; what's left is "<owner>/<repo>" or "<owner>/<repo>@<branch>".
		local spec="$entry" dirname="" branch=""
		if [[ "$spec" == *:* ]]; then
			dirname="${spec##*:}"
			spec="${spec%%:*}"
		fi
		local owner_repo="$spec"
		if [[ "$spec" == *@* ]]; then
			branch="${spec##*@}"
			owner_repo="${spec%%@*}"
		fi
		if [[ -z "$dirname" ]]; then
			dirname="${owner_repo##*/}"
			dirname="${dirname#feedback-plugin-}"
			dirname="${dirname//-/_}"
		fi
		local clone_args=(--depth 1)
		[[ -n "$branch" ]] && clone_args+=(--branch "$branch")
		if git clone "${clone_args[@]}" "https://${_auth}github.com/${owner_repo}.git" "$dirname" 2>/dev/null; then
			cloned=$((cloned + 1))
		else
			echo " skipped ${owner_repo}${branch:+@$branch}"
		fi
	done

	# Strip dangling symlinks from the bundled tree. Some plugins ship
	# build-time symlinks into sources that aren't present at runtime (e.g.
	# rig_builder's vst/src/racks/DPF -> DISTRHO framework). A broken symlink
	# is harmless on Linux/squashfs, but on macOS it BREAKS codesign ("a
	# sealed resource is missing or invalid" -> the app reads as "damaged")
	# AND breaks `xattr -dr com.apple.quarantine` (it aborts on the dangling
	# link, so the quarantine-removal workaround can't complete). Remove them.
	local _dangling
	_dangling=$(find "$clone_dir" -type l ! -exec test -e {} \; -print 2>/dev/null | wc -l | tr -d ' ')
	if [[ "${_dangling:-0}" != "0" ]]; then
		find "$clone_dir" -type l ! -exec test -e {} \; -delete 2>/dev/null || true
		echo "Stripped ${_dangling} dangling symlink(s) from the bundled tree (macOS codesign safety)"
	fi

	export SLOPSMITH_DIR="$clone_dir"
	echo "Cloned ${cloned} of ${total} plugins"
	cd - >/dev/null
}


step=1

echo_validate_env() {
    echo -e "${BLUE}Step $step: Validating environment${NC}"
    step=$((step + 1))
}

echo_step() {
    echo -e "${BLUE}Step $step: $1${NC}"
    step=$((step + 1))
}

echo_summary() {
    echo -e "${GREEN}✓${NC} $1"
}

echo_warning() {
    echo -e "${YELLOW}!${NC} $1"
}

echo_error() {
    echo -e "${RED}✗${NC} $1"
}

validate_environment() {
    echo_validate_env

    NODE_VERSION=$(get_cfg .versions.node)
    PYTHON_VERSION=$(get_cfg .versions.python)

    echo "Platform: $PLATFORM"
    echo "Node: $NODE_VERSION"
    echo "Python: $PYTHON_VERSION"
    echo ""

    # Check Node.js
    if command -v node &>/dev/null; then
        INSTALLED_NODE=$(node -p "process.version.replace('v', '')")
        echo_summary "Found Node.js $INSTALLED_NODE"
    else
        echo_error "Node.js not found"
        exit 1
    fi

    # Check Python 3
    if command -v python3 &>/dev/null; then
        INSTALLED_PYTHON=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
        echo_summary "Found Python $INSTALLED_PYTHON"
    else
        echo_error "Python 3 not found"
        exit 1
    fi

    echo ""
}

install_npm_deps() {
    echo_step "Installing npm dependencies"
    npm install
    echo_summary "npm dependencies installed"
    echo ""
}

build_native_addons() {
    echo_step "Building native addons (audio engine)"
    npm run build:native
    echo_summary "Native addons built"
    echo ""
}

bundle_slopsmith() {
    echo_step "Bundling Slopsmith and plugins"
    npm run bundle:slopsmith
    echo_summary "Slopsmith bundled"
    echo ""
}

bundle_python() {
    mkdir -p "$PROJECT_DIR/resources"
    bundle_python_impl
    echo_summary "Python runtime bundled"
    echo ""
}

bundle_binaries() {
    mkdir -p "$PROJECT_DIR/resources/bin"
    bundle_binaries_impl
    echo_summary "System binaries bundled"
    echo ""
}

verify_bundled_binaries() {
  # Smoke test: verify bundled binaries are executable and can run
  local bin_dir="$PROJECT_DIR/resources/bin"
  local ext=""
  if [[ "$PLATFORM" == "windows" ]]; then
    ext=".exe"
  fi
  
  echo_step "Verifying bundled binaries"
  
  # Verify fluidsynth: supports --version
  local fs_path="$bin_dir/fluidsynth${ext}"
  if [[ ! -f "$fs_path" ]]; then
    echo_error "Missing bundled binary: $fs_path"
    exit 1
  fi
  if ! "$fs_path" --version >/dev/null 2>&1; then
    echo_error "Binary fluidsynth failed to execute"
    exit 1
  fi
  echo "  ✓ fluidsynth"
  
  # Verify ffmpeg: supports -version
  local ff_path="$bin_dir/ffmpeg${ext}"
  if [[ ! -f "$ff_path" ]]; then
    echo_error "Missing bundled binary: $ff_path"
    exit 1
  fi
  if ! "$ff_path" -version >/dev/null 2>&1; then
    echo_error "Binary ffmpeg failed to execute"
    exit 1
  fi
  echo "  ✓ ffmpeg"

  # Verify ffprobe: demucs spawns it before ffmpeg to read stream
  # metadata. Required on every platform because falling through to a
  # host-installed ffprobe makes stem splitting work on the build host
  # and silently fail on user machines without it.
  local ffp_path="$bin_dir/ffprobe${ext}"
  if [[ ! -f "$ffp_path" ]]; then
    echo_error "Missing bundled binary: $ffp_path"
    exit 1
  fi
  if ! "$ffp_path" -version >/dev/null 2>&1; then
    echo_error "Binary ffprobe failed to execute"
    exit 1
  fi
  echo "  ✓ ffprobe"
  
  # Verify vgmstream-cli: doesn't have --version, check it produces output with version
  local vgm_path="$bin_dir/vgmstream-cli${ext}"
  echo "  Checking vgmstream-cli at: $vgm_path"
  if [[ ! -f "$vgm_path" ]]; then
    echo_error "Missing bundled binary: $vgm_path"
    exit 1
  fi
  echo "  File exists, checking permissions:"
  ls -la "$vgm_path"
  # `file` is a diagnostic-only call — keep it best-effort so a minimal
  # base image (e.g. the Linux Docker builder, which doesn't ship the
  # libmagic-backed `file` binary) doesn't fail the build over a debug
  # line. The actual smoke test is the run-and-grep below.
  if command -v file >/dev/null 2>&1; then
    echo "  File type:"
    file "$vgm_path"
  fi
  echo "  Attempting to run vgmstream-cli..."
  local vgm_output
  local vgm_exit_code
  # vgmstream-cli with no args prints its version header then exits 1.
  # Capture the exit code via the if-branch so `set -e` doesn't trip
  # AND vgm_exit_code reflects the binary's real status (not the `|| true`
  # short-circuit that the previous form ended up reporting).
  if vgm_output=$("$vgm_path" 2>&1); then
    vgm_exit_code=0
  else
    vgm_exit_code=$?
  fi
  echo "  Exit code: $vgm_exit_code"
  echo "  Raw output:"
  echo "$vgm_output" | head -20
  echo "  Checking if output matches expected pattern..."
  if [[ -z "$vgm_output" ]]; then
    echo_error "Binary vgmstream-cli produced no output"
    exit 1
  fi
  if [[ ! "$vgm_output" =~ vgmstream.*CLI.*decoder ]]; then
    echo_error "Binary vgmstream-cli produced unexpected output"
    echo "  Expected pattern: vgmstream.*CLI.*decoder"
    echo "  Actual output (first 500 chars):"
    echo "${vgm_output:0:500}"
    exit 1
  fi
  echo "  ✓ vgmstream-cli"

  # On Linux, audit each bundled ELF binary's NEEDED entries: every
  # SONAME must either be in the glibc/loader skip list (delegated to
  # the user's libc) or sit next to the binary in resources/bin/.
  # The `-version` smoke tests above can't catch this on the build
  # host because /usr/lib happens to satisfy the deps — but on a user
  # machine with a different ffmpeg ABI the load fails at runtime
  # (issue #68 on Fedora 44 / Arch).
  if [[ "$PLATFORM" == "linux" ]]; then
    if ! command -v readelf >/dev/null 2>&1; then
      echo_error "readelf not found (apt: binutils) - required to audit bundled binaries' shared library deps"
      exit 1
    fi
    for bin in fluidsynth ffmpeg ffprobe vgmstream-cli; do
      audit_bundled_deps "$bin_dir/$bin" "$bin_dir" || exit 1
    done
    # Also audit each bundled .so's own NEEDED entries. ldd-on-the-top-
    # level-binary usually resolves the full transitive closure, but
    # dlopen-resolved deps and interposer libs can slip through that
    # traversal. Auditing every bundled .so closes the gap.
    for so in "$bin_dir"/*.so*; do
      [ -f "$so" ] || continue
      audit_bundled_deps "$so" "$bin_dir" || exit 1
    done
    echo "  ✓ shared-library audit"
  fi

  echo_summary "All bundled binaries verified"
  echo ""
}

# Asserts every NEEDED SONAME in $1 is either in the glibc skip list
# or present as a file in $2 (the bundle directory). Returns non-zero
# with a specific error if any SONAME is unsatisfied.
audit_bundled_deps() {
  local bin_path="$1"
  local bundle_dir="$2"
  local missing=()
  local soname
  while IFS= read -r soname; do
    [ -n "$soname" ] || continue
    is_skipped_lib "$soname" && continue
    [ -f "$bundle_dir/$soname" ] && continue
    missing+=("$soname")
  done < <(readelf -d "$bin_path" 2>/dev/null | awk -F'[][]' '/\(NEEDED\)/ {print $2}')

  if [ ${#missing[@]} -gt 0 ]; then
    echo_error "Bundled $(basename "$bin_path") needs shared libs that are not in resources/bin/:"
    for soname in "${missing[@]}"; do
      echo "    - $soname"
    done
    echo "  Fix: extend scripts/bundle-binaries.sh so these SONAMEs are bundled (or add them to the glibc skip list if they MUST come from the host libc)."
    return 1
  fi
}

bundle_soundfont() {
    echo_step "Bundling default soundfont"
    bash "$SCRIPT_DIR/bundle-soundfont.sh"
    echo_summary "Soundfont bundled"
    echo ""
}

build_typescript() {
    echo_step "Building TypeScript"
    npm run build:ts
    echo_summary "TypeScript built"
    echo ""
}

package_application() {
    echo_step "Packaging application"
    # Call electron-builder directly. The package.json `dist:*` scripts
    # chain `build:native && bundle && build:ts && electron-builder`,
    # but build-common.sh's main() has already run all three of those
    # explicitly. Going through `npm run dist:*` would re-run them, which
    # on macOS is wasteful: build:native rebuilds the native audio addon
    # that build-common.sh already produced.
    #
    # `--publish never` is required: on a tag push electron-builder
    # defaults to auto-publishing to GitHub Releases and then errors out
    # without GH_TOKEN. The workflow has a dedicated `release` job that
    # publishes via softprops/action-gh-release after artifact upload —
    # the build job just needs to produce artifacts, not publish them.
    local builder_platform
    case "$PLATFORM" in
        linux)   builder_platform="--linux" ;;
        macos)   builder_platform="--mac" ;;
        windows) builder_platform="--win" ;;
        *)
            echo_error "Unsupported packaging platform: $PLATFORM"
            exit 1
            ;;
    esac
    npx electron-builder "$builder_platform" --publish never
    echo_summary "Application packaged"
    echo ""
}

verify_artifacts() {
    echo_step "Verifying artifacts"

    ARTIFACTS_FOUND=0

    # Read patterns into array (avoid process substitution for CI compatibility)
  patterns=()
  tempfile=$(mktemp)
  get_expected_artifacts > "$tempfile"
  cat "$tempfile" >&2
  while IFS= read -r line; do
    patterns+=("$line")
  done < "$tempfile"
  rm -f "$tempfile"
  for pattern in "${patterns[@]}"; do
    shopt -s nullglob
    files=($pattern)
    shopt -u nullglob
    if [ ${#files[@]} -gt 0 ]; then
      ARTIFACTS_FOUND=1
      break
    fi
  done

    if [[ $ARTIFACTS_FOUND -eq 1 ]]; then
        echo_summary "Build successful!"
        echo ""
        ls -lh "$PROJECT_DIR/release/" 2>/dev/null | grep -v "^total" | awk 'NR > 1' | head -10 || true
    else
        echo_error "No artifacts found"
        if [[ -d "$PROJECT_DIR/release" ]]; then
            echo "Contents of release/:"
            ls -la "$PROJECT_DIR/release/" 2>&1 || echo "(directory empty)"
        else
            echo "release/ directory doesn't exist"
        fi
        exit 1
    fi
    echo ""
}

# Main entry point - platform scripts call this
main() {
    local start_time=$(date +%s)

    case "$PLATFORM" in
        linux|macos|windows)
            ;;
        *)
            echo_error "Unsupported platform: $PLATFORM"
            exit 1
            ;;
    esac

  # Verify that all required functions are defined by the sourcing platform script
  local missing_functions=()
  local required_funcs=(
    install_system_deps
    bundle_python_impl
    bundle_binaries_impl
    get_expected_artifacts
  )

  for func in "${required_funcs[@]}"; do
    if ! type "$func" &>/dev/null; then
      missing_functions+=("$func")
    fi
  done

  if [[ ${#missing_functions[@]} -gt 0 ]]; then
    echo_error "Required functions not defined by platform script:"
    for func in "${missing_functions[@]}"; do
      echo "  - $func"
    done
    exit 1
  fi

validate_environment
install_system_deps
install_npm_deps
# clone_slopsmith provides the slopsmith core + plugins that
# bundle_slopsmith packages into the app; run it before the build steps
# that consume $SLOPSMITH_DIR.
clone_slopsmith
build_native_addons
bundle_slopsmith
bundle_python
bundle_binaries
verify_bundled_binaries
bundle_soundfont
build_typescript
package_application
verify_artifacts

    local end_time=$(date +%s)
    local duration=$((end_time - start_time))

    echo -e "${GREEN}✓${NC} Build complete for $PLATFORM in ${duration}s"
    echo "Output: $PROJECT_DIR/release/"
}
