#!/usr/bin/env bash
# codev-office offline installer (minimax-docx + minimax-xlsx + pptx-generator + minimax-pdf skills)
# — Ubuntu (x86_64) and macOS (arm64 + x86_64, auto-detected via uname -m).
# Run from the folder holding this script:
#   bash codev-office-macos-setup.sh
# The bundle zip is downloaded automatically from the internal storage host when
# it isn't already next to this script — copy it there manually for hosts that
# can't reach that host. It is extracted automatically on first run.
# There is ONE macOS script, but TWO bundles: it picks
# codev-office-macos-arm64.zip or codev-office-macos-x86_64.zip from `uname -m`,
# so a machine only downloads its own chip's installers (~1.6 GB instead of the
# ~3.1 GB the old combined bundle carried).
# Options:
#   --skip-verify    skip the end-to-end test documents
#   --skills-root D  where to install the skills
#                    (default: ~/.claude/skills — the global skills dir read
#                    by Claude Code and CoDev Code)
#   --force-skills   replace already-installed skills with the bundled
#                    versions (default: existing skill dirs are left untouched)
#
# .NET on Ubuntu installs per-user (no sudo required), plus one optional sudo
# prompt to symlink dotnet into /usr/local/bin so non-interactive shells find it.
# pandoc/LibreOffice on Ubuntu and the macOS .pkg installs use sudo and will
# prompt for an admin password when needed. Declining a sudo prompt skips that
# component; the only fatal case is the .NET SDK on macOS (it is required and
# has no non-sudo installer here).
set -Eeuo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# BUNDLE_NAME/BUNDLE_DIR are resolved after the arch detection below — the
# bundle is per-chip, and picking it needs `uname -m` plus the log helpers.

# Resolve the real user's home. Run under sudo, $HOME is root's (/var/root) and
# the skills — which the signed-in user, not root, needs to reach, and which
# Claude Code loads from that user's ~/.claude/skills — would be stranded there.
# $SUDO_USER names the invoking user; install under their home and chown the
# result back to them at the end. Run as a normal user (the documented way),
# this leaves everything on $HOME untouched.
REAL_HOME="$HOME"
REAL_USER=""
if [ "$(id -u)" -eq 0 ] && [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ]; then
  if command -v getent >/dev/null 2>&1; then
    sudo_home="$(getent passwd "$SUDO_USER" 2>/dev/null | cut -d: -f6 || true)"
  else
    sudo_home="$(dscl . -read "/Users/$SUDO_USER" NFSHomeDirectory 2>/dev/null | awk '{print $2}' || true)"
  fi
  if [ -n "$sudo_home" ] && [ -d "$sudo_home" ]; then
    REAL_HOME="$sudo_home"
    REAL_USER="$SUDO_USER"
  fi
fi
SKILLS_ROOT="$REAL_HOME/.claude/skills"
SKIP_VERIFY=0
FORCE_SKILLS=0

while [ $# -gt 0 ]; do
  case "$1" in
    --skip-verify) SKIP_VERIFY=1 ;;
    --skills-root) SKILLS_ROOT="$2"; shift ;;
    --force-skills) FORCE_SKILLS=1 ;;
    *) echo "unknown option: $1" >&2; exit 1 ;;
  esac
  shift
done

DOCX_SKILL_DIR="$SKILLS_ROOT/minimax-docx"
XLSX_SKILL_DIR="$SKILLS_ROOT/minimax-xlsx"

log()  { printf '\033[32m[OK]    %s\033[0m\n' "$*"; }
info() { printf '\033[36m[INFO]  %s\033[0m\n' "$*"; }
warn() { printf '\033[33m[WARN]  %s\033[0m\n' "$*"; }
fail() { printf '\033[31m[FAIL]  %s\033[0m\n' "$*"; exit 1; }

# Every run announces which script build it is (self-hash): a screenshot or
# transcript then identifies the exact published version, instead of guessing
# which fixes the machine actually ran with.
info "codev-office macos setup (script sha256 $(shasum -a 256 "$0" | cut -c1-12))"
# Under set -e an unguarded failing command ends the script; the ERR trap
# makes that death loud and located instead of silent.
trap 'fail "unexpected error at line $LINENO: $BASH_COMMAND"' ERR

OS="$(uname -s)"
ARCH="$(uname -m)"   # arm64 on Apple Silicon, x86_64 on Intel
case "$OS" in
  Linux|Darwin) info "Detected OS: $OS ($ARCH)" ;;
  *) fail "Unsupported OS: $OS" ;;
esac

# --- Which bundle this machine needs ---
# The macOS bundle used to be a single ~3.1 GB zip carrying BOTH chips' .NET
# SDK, LibreOffice, pandoc, Python, wheels and Chromium — every Mac downloaded
# roughly half a bundle it could never run. It is now split per chip (~1.6 GB
# each) and this script picks its own, which is why there is still only ONE
# macOS setup script: every arch-dependent step below already branched on
# `uname -m`, so the split adds no user-visible choice to get wrong.
case "$ARCH" in
  arm64|aarch64) BUNDLE_ARCH="arm64" ;;
  x86_64|amd64)  BUNDLE_ARCH="x86_64" ;;
  *) fail "Unsupported architecture: $ARCH (expected arm64 or x86_64)" ;;
esac
BUNDLE_NAME="codev-office-macos-$BUNDLE_ARCH"
# A legacy combined bundle already sitting next to the script is still used: it
# is a superset of what this chip needs (every glob below is arch-filtered), and
# a machine mid-rollout must not re-download 1.6 GB just because the name
# changed. Only when neither the new zip nor its extracted folder is present.
LEGACY_BUNDLE_NAME="codev-office-macos"
if [ ! -d "$SCRIPT_DIR/$BUNDLE_NAME" ] && [ ! -f "$SCRIPT_DIR/$BUNDLE_NAME.zip" ] \
   && { [ -d "$SCRIPT_DIR/$LEGACY_BUNDLE_NAME" ] || [ -f "$SCRIPT_DIR/$LEGACY_BUNDLE_NAME.zip" ]; }; then
  warn "Using the legacy combined bundle $LEGACY_BUNDLE_NAME (both chips in one zip) - delete it and re-run to fetch the smaller $BUNDLE_NAME instead"
  BUNDLE_NAME="$LEGACY_BUNDLE_NAME"
fi
BUNDLE_DIR="$SCRIPT_DIR/$BUNDLE_NAME"

# Needed to unpack the bundle zip and the skill zips; minimal server
# images may lack it (macOS ships it).
command -v unzip >/dev/null 2>&1 || fail "unzip not found - install it first (e.g. sudo apt install unzip from your internal mirror)"

# Where the bundle zip is published (internal network only).
DOWNLOAD_BASE="https://netmind.viettel.vn/codev-storage/codev-office"

# Download the bundle zip when it isn't already next to this script. Resumes
# an interrupted download on re-run (.partial); a manually copied zip is used
# as-is for hosts that cannot reach the storage host.
if [ ! -d "$BUNDLE_DIR" ] && [ ! -f "$BUNDLE_DIR.zip" ]; then
  info "Downloading $BUNDLE_NAME.zip (~1.6 GB - takes a while; re-run to resume if interrupted)..."
  if command -v curl >/dev/null 2>&1; then
    curl -fL -C - --progress-bar -o "$BUNDLE_DIR.zip.partial" "$DOWNLOAD_BASE/$BUNDLE_NAME.zip" \
      || fail "download failed - copy $BUNDLE_NAME.zip next to this script manually and re-run"
  elif command -v wget >/dev/null 2>&1; then
    wget -c -O "$BUNDLE_DIR.zip.partial" "$DOWNLOAD_BASE/$BUNDLE_NAME.zip" \
      || fail "download failed - copy $BUNDLE_NAME.zip next to this script manually and re-run"
  else
    fail "neither curl nor wget found - install one, or copy $BUNDLE_NAME.zip next to this script manually"
  fi
  mv "$BUNDLE_DIR.zip.partial" "$BUNDLE_DIR.zip"
  log "Downloaded $BUNDLE_NAME.zip"
fi

# The packages ship zipped — extract on first run, and RE-extract when the
# zip next to the script is a different publish than the extracted folder
# came from. codevhub re-downloads a republished bundle automatically, but a
# stale extracted tree would otherwise shadow the fresh zip forever (this
# shipped: a machine kept installing from an outdated wheels folder after
# the fixed bundle was already sitting next to the script).
SRC_MARK="$BUNDLE_DIR/.source-zip-sha256"
zip_sha=""
if [ -f "$BUNDLE_DIR.zip" ]; then
  info "Hashing $BUNDLE_NAME.zip (takes a moment)..."
  zip_sha="$(shasum -a 256 "$BUNDLE_DIR.zip" | awk '{print $1}')"
fi
if [ -d "$BUNDLE_DIR" ] && [ -n "$zip_sha" ] && [ "$(cat "$SRC_MARK" 2>/dev/null || true)" != "$zip_sha" ]; then
  warn "Extracted bundle does not match $BUNDLE_NAME.zip (republished bundle, or an extract from before this check) - re-extracting"
  rm -rf "$BUNDLE_DIR"
fi
if [ ! -d "$BUNDLE_DIR" ]; then
  [ -f "$BUNDLE_DIR.zip" ] || fail "$BUNDLE_NAME.zip not found next to this script"
  # A corrupted download (interrupted transfer, or a resume spliced across a
  # republished bundle) otherwise dies mid-extract in raw "bad zipfile
  # offset" noise — test the archive first and say what to do.
  info "Checking $BUNDLE_NAME.zip integrity (takes a moment)..."
  unzip -t -qq "$BUNDLE_DIR.zip" >/dev/null 2>&1 \
    || fail "$BUNDLE_NAME.zip is corrupt - delete it and re-run to download it again (codevhub skill office re-fetches it automatically)"
  info "Extracting $BUNDLE_NAME.zip..."
  if ! unzip -q "$BUNDLE_DIR.zip" -d "$SCRIPT_DIR"; then
    # Never leave a half-extracted tree behind: the extract-on-first-run
    # guard above would trust it on the next run and fail confusingly
    # further down (missing wheels, missing skill zips).
    rm -rf "$BUNDLE_DIR"
    fail "extracting $BUNDLE_NAME.zip failed - the zip may be corrupt or the disk full; delete it and re-run"
  fi
  [ -d "$BUNDLE_DIR" ] || fail "extracting $BUNDLE_NAME.zip did not produce $BUNDLE_NAME/"
  printf '%s\n' "$zip_sha" > "$SRC_MARK"
  log "Bundle extracted to $BUNDLE_DIR"
elif [ -n "$zip_sha" ]; then
  info "Using already-extracted bundle: $BUNDLE_DIR (matches $BUNDLE_NAME.zip)"
else
  info "Using already-extracted bundle: $BUNDLE_DIR"
fi

if [ "$(id -u)" -ne 0 ]; then
  if [ "$OS" = "Darwin" ]; then
    warn "The .NET SDK and pandoc installers use sudo - enter your admin password when prompted."
    warn "A declined prompt skips that component (fatal only for the .NET SDK)."
  else
    info "The .NET SDK installs per-user - no sudo needed. Only the optional pandoc/LibreOffice debs use sudo."
  fi
fi

# --- .NET SDK 8 (required) ---
# An 8.x SDK specifically, not ">= 8": the skill targets net8.0, and only SDK 8
# carries the net8.0 targeting packs (Microsoft.NETCore.App.Ref & friends)
# locally. A newer SDK alone tries to restore them from NuGet, which the
# offline feed does not carry (NU1101). SDKs install side-by-side, so adding
# SDK 8 next to a newer one is safe.
dotnet_ok=0
if command -v dotnet >/dev/null 2>&1; then
  if dotnet --list-sdks 2>/dev/null | grep -q '^8\.'; then
    log ".NET SDK 8 already installed"
    dotnet_ok=1
  elif dotnet --list-sdks 2>/dev/null | grep -q .; then
    info "Installed .NET SDK(s) lack an 8.x - installing SDK 8 side-by-side"
  fi
fi

if [ "$dotnet_ok" -eq 0 ]; then
  if [ "$OS" = "Linux" ]; then
    tarball="$(ls "$BUNDLE_DIR"/dotnet-sdk-*-linux-x64.tar.gz 2>/dev/null | head -1 || true)"
    [ -n "$tarball" ] || fail "dotnet-sdk-*-linux-x64.tar.gz not found in $BUNDLE_DIR"
    info "Installing .NET SDK to ~/.dotnet (per-user, no sudo)..."
    mkdir -p "$HOME/.dotnet"
    tar -xzf "$tarball" -C "$HOME/.dotnet"
    export DOTNET_ROOT="$HOME/.dotnet"
    export PATH="$HOME/.dotnet:$PATH"
    if ! grep -q 'DOTNET_ROOT=$HOME/.dotnet' "$HOME/.bashrc" 2>/dev/null; then
      {
        echo 'export DOTNET_ROOT=$HOME/.dotnet'
        echo 'export PATH=$HOME/.dotnet:$PATH'
      } >> "$HOME/.bashrc"
      info "Added DOTNET_ROOT/PATH exports to ~/.bashrc"
    fi
    # .NET needs ICU; minimal server images may lack it
    if command -v ldconfig >/dev/null 2>&1 && ! ldconfig -p 2>/dev/null | grep -q libicu; then
      export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
      echo 'export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1' >> "$HOME/.bashrc"
      warn "libicu not found - enabled DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1"
    fi
  else
    case "$ARCH" in
      arm64) pkg_pat="dotnet-sdk-*-osx-arm64.pkg" ;;
      *)     pkg_pat="dotnet-sdk-*-osx-x64.pkg" ;;
    esac
    pkg="$(ls "$BUNDLE_DIR"/$pkg_pat 2>/dev/null | head -1 || true)"
    [ -n "$pkg" ] || fail "$pkg_pat not found in $BUNDLE_DIR"
    info "Installing .NET SDK from $(basename "$pkg") (sudo will prompt)..."
    sudo installer -pkg "$pkg" -target / || fail "sudo declined or install failed - the .NET SDK is required"
    export DOTNET_ROOT="/usr/local/share/dotnet"
    export PATH="/usr/local/share/dotnet:$PATH"
  fi
  command -v dotnet >/dev/null 2>&1 || fail "dotnet not on PATH after install"
  log ".NET SDK $(dotnet --version) installed"
fi

# ~/.bashrc (Linux) and /etc/paths.d (macOS pkg) only apply to interactive or
# login shells — CoDev Code's shell tool runs /bin/sh non-interactively and
# never reads them. Symlink the dotnet muxer machine-wide so every shell finds
# it (it resolves its install root through the symlink). Declining sudo keeps
# the shell-profile-only install.
dotnet_real=""
if [ "$OS" = "Linux" ] && [ -x "$HOME/.dotnet/dotnet" ]; then dotnet_real="$HOME/.dotnet/dotnet"; fi
if [ "$OS" = "Darwin" ] && [ -x "/usr/local/share/dotnet/dotnet" ]; then dotnet_real="/usr/local/share/dotnet/dotnet"; fi
if [ -n "$dotnet_real" ] && [ ! -x /usr/local/bin/dotnet ]; then
  if sudo mkdir -p /usr/local/bin && sudo ln -sf "$dotnet_real" /usr/local/bin/dotnet; then
    log "Symlinked $dotnet_real -> /usr/local/bin/dotnet"
  else
    warn "Could not symlink dotnet to /usr/local/bin - dotnet will only be on PATH in interactive/login shells (CoDev Code's shell tool may not find it)"
  fi
fi

# --- Optional: pandoc + LibreOffice ---
if command -v pandoc >/dev/null 2>&1; then
  log "pandoc already installed"
elif [ "$OS" = "Linux" ]; then
  deb="$(ls "$BUNDLE_DIR"/pandoc-*-amd64.deb 2>/dev/null | head -1 || true)"
  if [ -n "$deb" ]; then
    info "Installing pandoc (sudo will prompt)..."
    sudo dpkg -i "$deb" && log "pandoc installed" || warn "pandoc install skipped or failed (optional, continuing)"
  fi
else
  case "$ARCH" in
    arm64) pkg_pat="pandoc-*-arm64-macOS.pkg" ;;
    *)     pkg_pat="pandoc-*-x86_64-macOS.pkg" ;;
  esac
  pkg="$(ls "$BUNDLE_DIR"/$pkg_pat 2>/dev/null | head -1 || true)"
  if [ -n "$pkg" ]; then
    info "Installing pandoc from $(basename "$pkg") (sudo will prompt)..."
    sudo installer -pkg "$pkg" -target / && log "pandoc installed" || warn "pandoc install skipped or failed (optional, continuing)"
  fi
fi

if command -v soffice >/dev/null 2>&1 || [ -x "/Applications/LibreOffice.app/Contents/MacOS/soffice" ]; then
  log "LibreOffice already installed"
elif [ "$OS" = "Linux" ]; then
  lo_tar="$(ls "$BUNDLE_DIR"/LibreOffice_*_Linux_x86-64_deb.tar.gz 2>/dev/null | head -1 || true)"
  if [ -n "$lo_tar" ]; then
    info "Installing LibreOffice (sudo will prompt, takes a few minutes)..."
    lo_tmp="$(mktemp -d)"
    tar -xzf "$lo_tar" -C "$lo_tmp"
    sudo dpkg -i "$lo_tmp"/LibreOffice_*/DEBS/*.deb && log "LibreOffice installed" || warn "LibreOffice install skipped or failed (optional, continuing)"
    rm -rf "$lo_tmp"
  fi
else
  case "$ARCH" in
    arm64) dmg_pat="LibreOffice_*_MacOS_aarch64.dmg" ;;
    *)     dmg_pat="LibreOffice_*_MacOS_x86-64.dmg" ;;
  esac
  dmg="$(ls "$BUNDLE_DIR"/$dmg_pat 2>/dev/null | head -1 || true)"
  if [ -n "$dmg" ]; then
    info "Installing LibreOffice from $(basename "$dmg")..."
    mount_point="$(hdiutil attach -nobrowse "$dmg" | tail -1 | awk -F'\t' '{print $NF}')"
    if [ -d "$mount_point/LibreOffice.app" ]; then
      if cp -R "$mount_point/LibreOffice.app" /Applications/; then
        # The copied app keeps its quarantine flag, and Gatekeeper blocks a
        # quarantined app's binaries from headless runs (doc_to_docx.sh calls
        # `soffice --headless` without ever launching the app in the GUI).
        xattr -dr com.apple.quarantine /Applications/LibreOffice.app 2>/dev/null || true
        log "LibreOffice installed to /Applications"
      else
        warn "LibreOffice copy skipped or failed (optional, continuing)"
      fi
    else
      warn "LibreOffice.app not found in dmg (optional, continuing)"
    fi
    hdiutil detach "$mount_point" -quiet || true
  fi
fi

# doc_to_docx.sh (docx skill) resolves LibreOffice solely via `soffice` on
# PATH; libreoffice_recalc.py (xlsx skill) also probes /opt and the macOS app
# bundle. The Linux debs only create a versioned /usr/local/bin/libreofficeX.Y
# symlink and the macOS app bundle adds nothing to PATH, so add a `soffice`
# symlink.
if ! command -v soffice >/dev/null 2>&1; then
  for p in /opt/libreoffice*/program/soffice /Applications/LibreOffice.app/Contents/MacOS/soffice; do
    if [ -x "$p" ]; then
      sudo mkdir -p /usr/local/bin
      if sudo ln -sf "$p" /usr/local/bin/soffice; then
        log "Symlinked $p -> /usr/local/bin/soffice"
      else
        warn "Could not create /usr/local/bin/soffice (doc_to_docx.sh needs soffice on PATH)"
      fi
      break
    fi
  done
fi

# --- Python + packages for the xlsx skill (pandas + openpyxl, offline) ---
# The xlsx skill's core pipeline (create/edit/validate) is pure stdlib; pandas
# and openpyxl are only needed for its READ/analysis path, so package failures
# warn instead of aborting. Wheels cover CPython 3.10-3.14 (64-bit).
# Python version by chip: 3.14 on Apple Silicon, 3.13 on Intel — onnxruntime
# (the native runtime behind markitdown's magika dependency) added CPython 3.14
# support in the same release that dropped Intel-mac wheels, so Intel + 3.14 can
# never install markitdown and would trip the fatal markitdown check below.
# Each per-chip bundle ships only its own pkg; the legacy combined bundle ships
# both, which is what the chip-preferred glob below picks between.
chip="$(uname -m)"
if [ "$OS" = "Darwin" ] && [ "$chip" = "x86_64" ]; then py_max=13; else py_max=14; fi
py_ok=0
if command -v python3 >/dev/null 2>&1; then
  py_minor="$(python3 -c 'import sys; print(sys.version_info[1])' 2>/dev/null || echo 0)"
  if [ "$py_minor" -ge 10 ] && [ "$py_minor" -le "$py_max" ]; then
    py_ok=1
    log "python3 3.$py_minor found"
  elif [ "$py_minor" -eq 14 ] && [ "$py_max" -eq 13 ]; then
    warn "python3 3.14 found, but Intel Macs have no onnxruntime/markitdown wheels for 3.14 - installing the bundled Python 3.13 alongside it"
  fi
fi
if [ "$py_ok" -eq 0 ] && [ "$OS" = "Darwin" ]; then
  if [ "$chip" = "x86_64" ]; then
    py_pkg="$(ls "$BUNDLE_DIR"/python-3.13.*-macos11.pkg 2>/dev/null | head -1 || true)"
  else
    py_pkg="$(ls "$BUNDLE_DIR"/python-3.14.*-macos11.pkg 2>/dev/null | head -1 || true)"
  fi
  # Fall back to any bundled pkg when the chip-preferred one is absent
  # (e.g. an older extracted bundle that predates the two-pkg layout).
  [ -n "$py_pkg" ] || py_pkg="$(ls "$BUNDLE_DIR"/python-3.*-macos11.pkg 2>/dev/null | head -1 || true)"
  if [ -n "$py_pkg" ]; then
    info "Installing Python from $(basename "$py_pkg") (sudo will prompt)..."
    if sudo installer -pkg "$py_pkg" -target /; then
      py_ver="$(basename "$py_pkg" | sed -E 's/^python-(3\.[0-9]+)\..*/\1/')"
      export PATH="/Library/Frameworks/Python.framework/Versions/$py_ver/bin:$PATH"
      command -v python3 >/dev/null 2>&1 && py_ok=1 && log "Python $(python3 --version 2>&1 | awk '{print $2}') installed"
    else
      warn "Python install skipped or failed - the xlsx skill requires python3"
    fi
  fi
fi
if [ "$py_ok" -eq 0 ] && [ "$OS" = "Linux" ]; then
  warn "no python3 3.10-3.14 found - install it from your internal mirror (the xlsx skill requires it)"
fi

if [ "$py_ok" -eq 1 ]; then
  if python3 -m pip --version >/dev/null 2>&1; then
    # Some distros mark the system Python externally managed (PEP 668);
    # retry with --break-system-packages only when the plain install refuses.
    if python3 -m pip install --user --no-index --find-links "$BUNDLE_DIR/python-wheels" pandas openpyxl >/dev/null 2>&1 \
       || python3 -m pip install --user --no-index --break-system-packages --find-links "$BUNDLE_DIR/python-wheels" pandas openpyxl >/dev/null 2>&1; then
      log "pandas + openpyxl installed from bundled wheels"
    else
      warn "pip install from bundled wheels failed - xlsx READ/analysis unavailable (core xlsx tasks still work)"
    fi
    # markitdown powers the pptx skill's read and required-QA paths, and its
    # defusedxml dependency is the skill's XML parser for pptx edits — the
    # pptx skill is unusable as specified without it, so failure is fatal.
    if python3 -m pip install --user --no-index --find-links "$BUNDLE_DIR/python-wheels" 'markitdown[pptx]' >/dev/null 2>&1 \
       || python3 -m pip install --user --no-index --break-system-packages --find-links "$BUNDLE_DIR/python-wheels" 'markitdown[pptx]' >/dev/null 2>&1; then
      log "markitdown installed from bundled wheels"
    else
      fail "markitdown install from bundled wheels failed - the pptx skill's read/QA/edit paths need it. If this extracted bundle predates the pptx skill, delete the extracted bundle folder and re-run to fetch a current one"
    fi
    # reportlab + pypdf are the pdf skill's core (body typesetting, fill,
    # merge, reformat) — the skill is unusable without them, so failure is
    # fatal. matplotlib is genuinely optional (chart/flowchart/math blocks
    # degrade to text), so it installs separately: its failure must not take
    # the required pair down with it.
    if python3 -m pip install --user --no-index --find-links "$BUNDLE_DIR/python-wheels" reportlab pypdf >/dev/null 2>&1 \
       || python3 -m pip install --user --no-index --break-system-packages --find-links "$BUNDLE_DIR/python-wheels" reportlab pypdf >/dev/null 2>&1; then
      log "reportlab + pypdf installed from bundled wheels"
    else
      fail "reportlab/pypdf install from bundled wheels failed - the pdf skill's create/fill/reformat paths need them. If this extracted bundle predates the pdf skill, delete the extracted bundle folder and re-run to fetch a current one"
    fi
    if python3 -m pip install --user --no-index --find-links "$BUNDLE_DIR/python-wheels" matplotlib >/dev/null 2>&1 \
       || python3 -m pip install --user --no-index --break-system-packages --find-links "$BUNDLE_DIR/python-wheels" matplotlib >/dev/null 2>&1; then
      log "matplotlib installed from bundled wheels"
    else
      warn "matplotlib install from bundled wheels failed - pdf chart/flowchart/math blocks degrade to text (continuing)"
    fi
  else
    warn "pip not available for python3 - install it, then run:"
    warn "  python3 -m pip install --user --no-index --find-links $BUNDLE_DIR/python-wheels pandas openpyxl 'markitdown[pptx]'"
  fi
fi

# --- Node.js + JS modules (pptx-generator + minimax-pdf skills, offline) ---
# Both skills read and edit documents with Python alone; Node.js (running
# PptxGenJS for pptx creation and Playwright for pdf cover rendering) is
# optional, so failures here warn instead of aborting.
node_ok=0
if command -v node >/dev/null 2>&1; then
  node_major="$(node -e 'console.log(process.versions.node.split(".")[0])' 2>/dev/null || echo 0)"
  if [ "$node_major" -ge 18 ]; then
    node_ok=1
    log "node $(node --version) found"
  else
    warn "node $(node --version 2>/dev/null) is older than v18 - Playwright (pdf covers) needs 18+ (leaving it untouched)"
  fi
elif [ "$OS" = "Linux" ]; then
  node_tar="$(ls "$BUNDLE_DIR"/node-v*-linux-x64.tar.gz 2>/dev/null | head -1 || true)"
  if [ -n "$node_tar" ]; then
    info "Installing Node.js to ~/.node (per-user, no sudo)..."
    mkdir -p "$HOME/.node"
    tar -xzf "$node_tar" -C "$HOME/.node" --strip-components=1
    export PATH="$HOME/.node/bin:$PATH"
    if ! grep -q 'PATH=$HOME/.node/bin:$PATH' "$HOME/.bashrc" 2>/dev/null; then
      echo 'export PATH=$HOME/.node/bin:$PATH' >> "$HOME/.bashrc"
      info "Added ~/.node/bin to PATH in ~/.bashrc"
    fi
    node_ok=1
    log "Node.js $(node --version) installed"
  else
    warn "node-v*-linux-x64.tar.gz not found in bundle - pptx creation unavailable"
  fi
else
  # The node.org macOS pkg is universal (arm64 + x86_64), one file for both.
  node_pkg="$(ls "$BUNDLE_DIR"/node-v*.pkg 2>/dev/null | head -1 || true)"
  if [ -n "$node_pkg" ]; then
    info "Installing Node.js from $(basename "$node_pkg") (sudo will prompt)..."
    if sudo installer -pkg "$node_pkg" -target /; then
      export PATH="/usr/local/bin:$PATH"
      command -v node >/dev/null 2>&1 && node_ok=1 && log "Node.js $(node --version) installed"
    else
      warn "Node.js install skipped or failed (pptx creation unavailable, continuing)"
    fi
  else
    warn "node-v*.pkg not found in bundle - pptx creation unavailable"
  fi
fi

# Like dotnet above: ~/.bashrc never reaches CoDev Code's non-interactive
# shell tool, so symlink node machine-wide. Declining sudo keeps the
# .bashrc-only install. (The macOS pkg already installs to /usr/local/bin.)
if [ "$OS" = "Linux" ] && [ -x "$HOME/.node/bin/node" ] && [ ! -x /usr/local/bin/node ]; then
  if sudo mkdir -p /usr/local/bin && sudo ln -sf "$HOME/.node/bin/node" /usr/local/bin/node && sudo ln -sf "$HOME/.node/bin/npm" /usr/local/bin/npm; then
    log "Symlinked ~/.node/bin/node -> /usr/local/bin/node (and npm)"
  else
    warn "Could not symlink node to /usr/local/bin - node will only be on PATH in interactive bash shells (CoDev Code's shell tool may not find it)"
  fi
fi

# PptxGenJS (pptx creation) and Playwright (pdf cover rendering) ship as
# prebuilt, pure-JS node_modules trees (no npm or registry access needed).
# They merge into one shared tree under ~/.codev-office, linked as
# ~/.node_modules — one of Node's global require() fallback folders — so
# require() resolves from any working directory without NODE_PATH (which,
# exported via a shell profile, would never reach CoDev Code's
# non-interactive shell tool).
modules_home="$REAL_HOME/.codev-office/node_modules"
# Expose it via NODE_PATH for this script's own verification runs: under sudo the
# ~/.node_modules link lives in the real user's home, not root's, so require()
# from a root-run `node` would otherwise miss it. This is process-local (not the
# shell-profile export the comment above rules out) and only affects the checks
# this script runs.
export NODE_PATH="$modules_home"
modules_staging="$(mktemp -d)"
modules_found=0
for mod_zip in pptxgenjs-node-modules.zip playwright-node-modules.zip; do
  if [ ! -f "$BUNDLE_DIR/$mod_zip" ]; then
    warn "$mod_zip not found in bundle - ${mod_zip%-node-modules.zip} will be unavailable"
    continue
  fi
  # Failure-guarded: these are optional components, and under set -e an
  # unguarded unzip/cp failure (corrupt inner zip, disk full) would abort
  # the whole install before the required steps ever ran.
  if ! unzip -q "$BUNDLE_DIR/$mod_zip" -d "$modules_staging/${mod_zip%.zip}"; then
    warn "unpacking $mod_zip failed - ${mod_zip%-node-modules.zip} will be unavailable (continuing; re-run to retry)"
    continue
  fi
  if [ -d "$modules_staging/${mod_zip%.zip}/node_modules" ]; then
    if mkdir -p "$modules_staging/merged" \
       && cp -R "$modules_staging/${mod_zip%.zip}/node_modules/." "$modules_staging/merged/"; then
      modules_found=1
      log "${mod_zip%-node-modules.zip} module tree unpacked"
    else
      warn "staging $mod_zip failed (disk full?) - ${mod_zip%-node-modules.zip} will be unavailable (continuing)"
    fi
  else
    warn "$mod_zip did not contain node_modules/ - skipping it"
  fi
done
if [ "$modules_found" -eq 1 ]; then
  mkdir -p "$REAL_HOME/.codev-office"
  rm -rf "$modules_home"
  if ! cp -R "$modules_staging/merged" "$modules_home"; then
    warn "installing the JS module tree to $modules_home failed (disk full?) - pptx creation and pdf covers unavailable (continuing; re-run to retry)"
  elif [ ! -e "$REAL_HOME/.node_modules" ] || [ "$(readlink "$REAL_HOME/.node_modules" 2>/dev/null)" = "$modules_home" ]; then
    ln -sfn "$modules_home" "$REAL_HOME/.node_modules"
    log "JS modules installed (~/.node_modules -> $modules_home)"
  else
    # A pre-existing real ~/.node_modules dir: merge into it instead of
    # replacing it (the uninstaller leaves this case alone).
    cp -R "$modules_home/." "$REAL_HOME/.node_modules/"
    log "JS modules copied into existing ~/.node_modules"
  fi
fi
rm -rf "$modules_staging"

# --- Chromium for Playwright (pdf cover rendering) ---
# Playwright launches the browser build matching its own version from its
# per-user cache; the bundle builder produces the module tree and this zip
# together (npx playwright install chromium, then zip the cache contents).
case "$OS" in
  Linux) pw_cache="$HOME/.cache/ms-playwright" ;;
  *)     pw_cache="$HOME/Library/Caches/ms-playwright" ;;
esac
# Playwright ships no Chromium build for macOS 13 or older — pdf cover
# rendering needs macOS 14+ (body-only PDFs, FILL and REFORMAT still work).
if [ "$OS" = "Darwin" ]; then
  macos_major="$(sw_vers -productVersion 2>/dev/null | cut -d. -f1)"
  case "$macos_major" in
    ''|*[!0-9]*) : ;;
    *) if [ "$macos_major" -lt 14 ]; then
         warn "macOS $(sw_vers -productVersion) detected - Playwright has no Chromium for macOS 13 or older; pdf cover rendering will not work on this machine (body-only PDFs still do)"
       fi ;;
  esac
fi
if ls "$pw_cache"/chromium-* >/dev/null 2>&1; then
  log "Playwright Chromium already present in $pw_cache"
elif [ -f "$BUNDLE_DIR/ms-playwright-chromium.zip" ]; then
  info "Unpacking Playwright Chromium to $pw_cache..."
  # Failure-guarded like the module trees above: optional component, must
  # not abort the install under set -e.
  if mkdir -p "$pw_cache" && unzip -q "$BUNDLE_DIR/ms-playwright-chromium.zip" -d "$pw_cache"; then
    log "Playwright Chromium unpacked"
  else
    warn "unpacking ms-playwright-chromium.zip failed - pdf cover rendering unavailable (continuing; re-run to retry, or when online: npx playwright install chromium)"
  fi
else
  warn "ms-playwright-chromium.zip not found in bundle - pdf cover rendering unavailable (when online: npx playwright install chromium)"
fi

# --- Offline NuGet feed ---
feed_src="$BUNDLE_DIR/nuget-offline-feed"
feed_dst="$REAL_HOME/nuget-offline-feed"
[ -d "$feed_src" ] || fail "nuget-offline-feed not found in $BUNDLE_DIR"
mkdir -p "$feed_dst"
cp "$feed_src"/*.nupkg "$feed_dst/"
log "NuGet feed copied to $feed_dst"

nuget_cfg="$REAL_HOME/.nuget/NuGet/NuGet.Config"
if [ -f "$nuget_cfg" ] && grep -qF "$feed_dst" "$nuget_cfg"; then
  # Already points at the offline feed (e.g. a previous run). Rewriting would
  # also clobber NuGet.Config.bak — the user's original pre-script config.
  log "NuGet.Config already uses the offline feed - leaving it (and the .bak) untouched"
else
  if [ -f "$nuget_cfg" ]; then
    cp "$nuget_cfg" "$nuget_cfg.bak"
    warn "Existing NuGet.Config backed up to NuGet.Config.bak"
  fi
  mkdir -p "$(dirname "$nuget_cfg")"
  cat > "$nuget_cfg" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="offline" value="$feed_dst" />
  </packageSources>
</configuration>
EOF
  log "NuGet.Config written (offline feed only, nuget.org disabled)"
fi

# --- Install the skills ---
# pptx-generator and minimax-pdf are newer than some published bundles — warn
# instead of aborting when their zips are missing from an extracted bundle.
for skill in minimax-docx minimax-xlsx pptx-generator minimax-pdf; do
  zip_file="$BUNDLE_DIR/$skill-skill.zip"
  dest="$SKILLS_ROOT/$skill"
  if [ ! -f "$zip_file" ]; then
    case "$skill" in
      pptx-generator|minimax-pdf) warn "$skill-skill.zip not in this bundle - delete the extracted bundle and re-run to fetch a current one"; continue ;;
      *) fail "$skill-skill.zip not found in $BUNDLE_DIR" ;;
    esac
  fi
  if [ -d "$dest" ] && [ "$FORCE_SKILLS" -eq 0 ]; then
    warn "Skill dir already exists, leaving it untouched (re-run with --force-skills to replace): $dest"
  else
    [ -d "$dest" ] && info "Replacing existing skill at $dest (--force-skills)"
    staging="$(mktemp -d)"
    unzip -q "$zip_file" -d "$staging"
    mkdir -p "$SKILLS_ROOT"
    rm -rf "$dest"
    mv "$staging/$skill" "$dest"
    rm -rf "$staging"
    log "Skill installed to $dest"
  fi
done

# Migrate away from the pre-.claude location: earlier installs put these four
# skills under ~/.config/codev/skills, which Claude Code cannot read. Now that
# they live in ~/.claude/skills, remove any stale copies so CoDev Code does not
# log a duplicate-name warning for each one. Only our four known skill dirs are
# touched, and never when --skills-root points back at the legacy dir.
LEGACY_SKILLS_ROOT="$REAL_HOME/.config/codev/skills"
if [ "$SKILLS_ROOT" != "$LEGACY_SKILLS_ROOT" ]; then
  for skill in minimax-docx minimax-xlsx pptx-generator minimax-pdf; do
    legacy_dest="$LEGACY_SKILLS_ROOT/$skill"
    if [ -d "$legacy_dest" ]; then
      rm -rf "$legacy_dest"
      info "Removed a stale skill from the old location: $legacy_dest"
    fi
  done
  rmdir "$LEGACY_SKILLS_ROOT" 2>/dev/null || true
fi

# --- Build the docx skill's CLI (offline restore) ---
cd "$DOCX_SKILL_DIR/scripts/dotnet"
# Pin the build to SDK 8. Without this, a newer SDK on the machine wins the
# version pick and tries to fetch the net8.0 targeting packs from NuGet —
# absent from the offline feed (NU1101). SDK 8 has them built in.
printf '%s\n' '{ "sdk": { "version": "8.0.100", "rollForward": "latestFeature" } }' > global.json
log "global.json written (build pinned to .NET SDK 8)"
info "Restoring NuGet packages from offline feed..."
# Restore the .csproj directly — SDK 8 does not understand the .slnx solution file.
# --configfile pins the relocated NuGet.Config: under sudo it lives in the real
# user's home, not the /var/root that a root-run dotnet would default to.
dotnet restore MiniMaxAIDocx.Cli/MiniMaxAIDocx.Cli.csproj --configfile "$nuget_cfg" || fail "restore failed"
dotnet build MiniMaxAIDocx.Cli/MiniMaxAIDocx.Cli.csproj --no-restore || fail "build failed"
log "Skill CLI built"

if [ "$SKIP_VERIFY" -eq 0 ]; then
  test_out="$(mktemp -d)/offline-test.docx"
  if dotnet run --project MiniMaxAIDocx.Cli --no-restore -- create --type report --output "$test_out" --title "Offline Test" && [ -f "$test_out" ]; then
    rm -f "$test_out"
    log "docx verification passed"
  else
    fail "docx verification failed"
  fi

  # xlsx: pack the bundled template and statically validate it (pure stdlib)
  if command -v python3 >/dev/null 2>&1; then
    xlsx_tmp="$(mktemp -d)/offline-test.xlsx"
    if python3 "$XLSX_SKILL_DIR/scripts/xlsx_pack.py" "$XLSX_SKILL_DIR/templates/minimal_xlsx" "$xlsx_tmp" >/dev/null \
       && python3 "$XLSX_SKILL_DIR/scripts/formula_check.py" "$xlsx_tmp" >/dev/null; then
      rm -f "$xlsx_tmp"
      log "xlsx verification passed"
    else
      fail "xlsx verification failed"
    fi
  else
    warn "python3 missing - skipped xlsx verification"
  fi

  # pptx Python side: markitdown (read/required-QA) and its defusedxml
  # dependency (the XML parser for pptx edits) must be importable — the
  # skill is unusable as specified without them, so this check is fatal.
  if [ -d "$SKILLS_ROOT/pptx-generator" ] && command -v python3 >/dev/null 2>&1; then
    if python3 -c "import markitdown, defusedxml" >/dev/null 2>&1; then
      log "pptx python deps verified (markitdown + defusedxml)"
    else
      fail "markitdown/defusedxml not importable - the pptx skill's read/QA/edit paths are unusable"
    fi
  fi
  # pptx Node side: generate a one-slide deck with PptxGenJS through the
  # global require() fallback (warn-level - Node.js installs best-effort),
  # then re-read it with markitdown - the skill's own required QA step.
  if [ -d "$SKILLS_ROOT/pptx-generator" ] && command -v node >/dev/null 2>&1; then
    pptx_tmp="$(mktemp -d)/offline-test.pptx"
    if PPTX_OUT="$pptx_tmp" node -e 'const P=require("pptxgenjs");const p=new P();p.layout="LAYOUT_16x9";p.addSlide().addText("Offline Test",{x:1,y:1,w:8,h:1,fontSize:24});p.writeFile({fileName:process.env.PPTX_OUT}).then(function(){process.exit(0);}).catch(function(e){console.error(e);process.exit(1);});' \
       && [ -f "$pptx_tmp" ]; then
      if command -v python3 >/dev/null 2>&1; then
        if python3 -m markitdown "$pptx_tmp" >/dev/null 2>&1; then
          log "pptx verification passed (deck compiled + markitdown QA extraction)"
        else
          fail "markitdown could not read the test deck - the pptx skill's required QA step is broken"
        fi
      else
        log "pptx verification passed (deck compiled; python3 missing so the markitdown QA step was not exercised)"
      fi
      rm -f "$pptx_tmp"
    else
      warn "pptx verification failed - Node.js or the PptxGenJS module tree is not usable (docx/xlsx unaffected)"
    fi
  fi

  # pdf: the skill's own environment check (exit 0 = all deps present;
  # warn-level - the pdf components install best-effort)
  if [ -d "$SKILLS_ROOT/minimax-pdf" ]; then
    if bash "$SKILLS_ROOT/minimax-pdf/scripts/make.sh" check >/dev/null 2>&1; then
      # make.sh check verifies the Playwright module but not the browser
      # binary — confirm the Chromium build is actually in the cache.
      if ls "$pw_cache"/chromium-*/INSTALLATION_COMPLETE >/dev/null 2>&1; then
        log "pdf verification passed (make.sh check + Chromium present)"
      else
        warn "pdf deps present but Playwright's Chromium build is missing from $pw_cache - cover rendering will fail (when online: npx playwright install chromium)"
      fi
    else
      warn "pdf environment incomplete - run: bash $SKILLS_ROOT/minimax-pdf/scripts/make.sh check"
    fi
  fi

  log "Verification passed - codev-office skills are ready (restart CoDev Code to pick them up)"
fi

# Everything installed while running as root landed under the real user's home
# (resolved above) but owned by root — hand it all back so the user (and Claude
# Code / CoDev Code running as them) can read and update it: the skills, the
# NuGet offline feed + config the docx skill's later restores need, and the JS
# module tree with its ~/.node_modules require() link.
if [ -n "$REAL_USER" ]; then
  chown "$REAL_USER" "$REAL_HOME/.claude" 2>/dev/null || true
  chown -R "$REAL_USER" "$SKILLS_ROOT" 2>/dev/null || true
  chown -R "$REAL_USER" "$feed_dst" 2>/dev/null || true
  chown -R "$REAL_USER" "$REAL_HOME/.nuget" 2>/dev/null || true
  chown -R "$REAL_USER" "$REAL_HOME/.codev-office" 2>/dev/null || true
  chown -h "$REAL_USER" "$REAL_HOME/.node_modules" 2>/dev/null || true
  log "Skills, NuGet feed and JS modules installed under $REAL_HOME and owned by $REAL_USER (not root)"
fi
