#!/usr/bin/env bash
set -euo pipefail

REPO="imjszhang/js-eyes"
SKILL_NAME="js-eyes"
SITE_URL="https://js-eyes.com"
INSTALL_DIR="${JS_EYES_DIR:-./skills}"
SUB_SKILL="${JS_EYES_SKILL:-${1:-}}"
SKIP_NATIVE_HOST="${JS_EYES_SKIP_NATIVE_HOST:-0}"
SKIP_TOKEN_INIT="${JS_EYES_SKIP_TOKEN_INIT:-0}"
for arg in "$@"; do
  if [ "$arg" = "--skip-native-host" ]; then
    SKIP_NATIVE_HOST=1
  fi
  if [ "$arg" = "--skip-token-init" ]; then
    SKIP_TOKEN_INIT=1
  fi
done

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
info()  { printf "${CYAN}[info]${NC}  %s\n" "$1"; }
ok()    { printf "${GREEN}[ok]${NC}    %s\n" "$1"; }
warn()  { printf "${YELLOW}[warn]${NC}  %s\n" "$1"; }
err()   { printf "${RED}[error]${NC} %s\n" "$1" >&2; }

http_get() {
  if command -v curl >/dev/null 2>&1; then curl -fsSL "$1"
  elif command -v wget >/dev/null 2>&1; then wget -qO- "$1"
  else err "curl or wget is required."; exit 1; fi
}

try_download() {
  local dest="$1"; shift
  for url in "$@"; do
    info "Trying: ${url}"
    if command -v curl >/dev/null 2>&1; then
      curl -fsSL --connect-timeout 10 "$url" -o "$dest" 2>/dev/null && return 0
    elif command -v wget >/dev/null 2>&1; then
      wget --timeout=10 -qO "$dest" "$url" 2>/dev/null && return 0
    fi
    warn "Failed, trying next source..."
  done
  return 1
}

confirm() {
  if [ "${JS_EYES_FORCE:-}" = "1" ]; then return 0; fi
  printf "  %s [y/N] " "$1"
  if [ -t 0 ]; then read -r reply
  elif [ -e /dev/tty ]; then read -r reply < /dev/tty
  else
    warn "Non-interactive shell; refusing to proceed (set JS_EYES_FORCE=1 to override)."
    return 1
  fi
  [ "$reply" = "y" ] || [ "$reply" = "Y" ]
}

verify_sha256() {
  local file="$1" expected="$2"
  if [ -z "$expected" ] || [ "$expected" = "null" ]; then
    warn "No sha256 in registry for this download; refusing to proceed without integrity hash."
    return 1
  fi
  local actual=""
  if command -v shasum >/dev/null 2>&1; then
    actual=$(shasum -a 256 "$file" | awk '{print $1}')
  elif command -v sha256sum >/dev/null 2>&1; then
    actual=$(sha256sum "$file" | awk '{print $1}')
  else
    err "Neither shasum nor sha256sum is installed; cannot verify integrity."
    return 1
  fi
  if [ "$actual" != "$expected" ]; then
    err "SHA-256 mismatch: expected ${expected}, got ${actual}"
    return 1
  fi
  ok "SHA-256 verified (${actual})"
}

run_npm_ci() {
  local dir="$1"
  if [ ! -f "${dir}/package.json" ]; then return 0; fi
  if [ ! -f "${dir}/package-lock.json" ]; then
    err "${dir} missing package-lock.json; refusing 'npm install' (set JS_EYES_REQUIRE_LOCKFILE=0 to relax)."
    if [ "${JS_EYES_REQUIRE_LOCKFILE:-1}" = "0" ]; then
      warn "Falling back to 'npm install --ignore-scripts --no-audit --no-fund'"
      (cd "$dir" && npm install --ignore-scripts --no-audit --no-fund)
      return $?
    fi
    return 1
  fi
  (cd "$dir" && npm ci --ignore-scripts --no-audit --no-fund)
}

# ── Prerequisites ─────────────────────────────────────────────────────
command -v node >/dev/null 2>&1 || { err "Node.js is required. Install: https://nodejs.org/"; exit 1; }
command -v npm  >/dev/null 2>&1 || { err "npm is required."; exit 1; }

# ══════════════════════════════════════════════════════════════════════
# Sub-skill install / update
#   curl ... | JS_EYES_SKILL=<id> bash       # install or upgrade one
#   curl ... | JS_EYES_SKILL=all bash        # upgrade every installed sub-skill
#   bash -s -- <skill-id>                    # same as JS_EYES_SKILL=<id>
# ══════════════════════════════════════════════════════════════════════

# Read a JSON value via node (usage: json_get <json> <jsPath>)
json_get_skill_field() {
  node -e "
    let r; try { r = JSON.parse(process.argv[1]); } catch (_) { process.exit(0); }
    const s = r.skills && r.skills.find(x => x.id === process.argv[2]);
    if (s && s[process.argv[3]] != null) console.log(s[process.argv[3]]);
  " "$1" "$2" "$3" 2>/dev/null || true
}

json_get_parent_version() {
  node -e "
    let r; try { r = JSON.parse(process.argv[1]); } catch (_) { process.exit(0); }
    if (r.parentSkill && r.parentSkill.version) console.log(r.parentSkill.version);
  " "$1" 2>/dev/null || true
}

# Compare two semver strings. Echo '<', '=' or '>' (a vs b).
compare_semver() {
  node -e "
    function p(v){const m=String(v||'').trim().match(/^v?(\\d+)\\.(\\d+)\\.(\\d+)/);if(!m)return null;return [+m[1],+m[2],+m[3]];}
    const a=p(process.argv[1]),b=p(process.argv[2]);
    if(!a&&!b){console.log('=');process.exit(0);}
    if(!a){console.log('<');process.exit(0);}
    if(!b){console.log('>');process.exit(0);}
    for(let i=0;i<3;i++){if(a[i]!==b[i]){console.log(a[i]<b[i]?'<':'>');process.exit(0);}}
    console.log('=');
  " "$1" "$2" 2>/dev/null || echo '='
}

read_local_version() {
  local pkg="$1/package.json"
  [ -f "$pkg" ] || return 0
  node -e "
    try { const j = JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')); if (j.version) console.log(j.version); } catch (_) {}
  " "$pkg" 2>/dev/null || true
}

install_one_sub_skill() {
  local sid="$1" registry_json="$2" parent_version="$3"
  local target="${JS_EYES_ROOT}/skills/${sid}"

  local _download_urls_raw
  _download_urls_raw=$(node -e "
    let r; try { r = JSON.parse(process.argv[1]); } catch (_) { process.exit(1); }
    const s = r.skills && r.skills.find(x => x.id === process.argv[2]);
    if (!s) process.exit(1);
    const urls = [s.downloadUrl];
    if (s.downloadUrlFallback && !/(refs\\/heads\\/)?main(?=[\\/?])/.test(s.downloadUrlFallback)) {
      urls.push(s.downloadUrlFallback);
    }
    console.log(urls.join('\n'));
  " "$registry_json" "$sid" 2>/dev/null) || true

  if [ -z "$_download_urls_raw" ]; then
    err "Skill '${sid}' not found in registry."
    return 1
  fi

  local expected_sha remote_version min_parent
  expected_sha=$(json_get_skill_field "$registry_json" "$sid" sha256)
  remote_version=$(json_get_skill_field "$registry_json" "$sid" version)
  min_parent=$(json_get_skill_field "$registry_json" "$sid" minParentVersion)

  local local_version=""
  if [ -d "$target" ]; then
    local_version=$(read_local_version "$target")
  fi

  if [ -n "$local_version" ] && [ -n "$remote_version" ]; then
    case $(compare_semver "$local_version" "$remote_version") in
      '=')
        ok "${sid} is up to date (${local_version})"
        return 0
        ;;
      '>')
        info "${sid} local (${local_version}) is newer than registry (${remote_version}); skipping."
        return 0
        ;;
      '<')
        info "Upgrading ${sid}: ${local_version} -> ${remote_version}"
        ;;
    esac
  else
    info "Installing extension skill: ${sid}${remote_version:+ (${remote_version})}"
  fi

  if [ -n "$min_parent" ] && [ -n "$parent_version" ]; then
    if [ "$(compare_semver "$parent_version" "$min_parent")" = '<' ]; then
      err "${sid} requires parent js-eyes >= ${min_parent}, but current parent is ${parent_version}."
      err "Upgrade the parent skill first: curl -fsSL ${SITE_URL}/install.sh | bash"
      return 1
    fi
  fi

  local download_urls=()
  while IFS= read -r _line; do
    [ -n "$_line" ] && download_urls+=("$_line")
  done <<< "$_download_urls_raw"

  if [ -d "$target" ] && [ -z "$local_version" ]; then
    # Existing directory but unreadable/missing version: confirm before overwriting.
    warn "Directory already exists: ${target}"
    confirm "Overwrite?" || { info "Aborted."; return 0; }
  fi

  local _tmpdir
  _tmpdir=$(mktemp -d)
  # Per-invocation trap (outer trap resets on function exit)

  local skill_zip="${_tmpdir}/skill.zip"
  info "Downloading ${sid}..."
  if ! try_download "$skill_zip" "${download_urls[@]}"; then
    err "Failed to download skill bundle."
    rm -rf "$_tmpdir"
    return 1
  fi

  if ! verify_sha256 "$skill_zip" "$expected_sha"; then
    err "Refusing to install skill without verified SHA-256."
    rm -rf "$_tmpdir"
    return 1
  fi

  info "Extracting..."
  local extract_dir="${_tmpdir}/extracted"
  mkdir -p "$extract_dir"
  if command -v unzip >/dev/null 2>&1; then
    unzip -qo "$skill_zip" -d "$extract_dir"
  elif command -v python3 >/dev/null 2>&1; then
    python3 -c "import zipfile,sys; zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])" "$skill_zip" "$extract_dir"
  else
    err "unzip or python3 is required."
    rm -rf "$_tmpdir"
    return 1
  fi

  find "$extract_dir" -type f -exec chmod 644 {} + 2>/dev/null || true
  find "$extract_dir" -type d -exec chmod 755 {} + 2>/dev/null || true

  if [ -f "${extract_dir}/package.json" ]; then
    info "Installing dependencies (npm ci --ignore-scripts)..."
    if ! run_npm_ci "$extract_dir"; then
      err "Dependency installation failed."
      rm -rf "$_tmpdir"
      return 1
    fi
  fi

  rm -rf "$target"
  mkdir -p "$(dirname "$target")"
  mv "$extract_dir" "$target"
  rm -rf "$_tmpdir"

  local absolute_target plugin_path
  absolute_target=$(cd "$target" && pwd)
  plugin_path="${absolute_target}/openclaw-plugin"

  ok "${sid} installed to: ${absolute_target}"
  if [ -n "$local_version" ]; then
    return 0
  fi
  echo ""
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  echo "  Next: register the plugin in ~/.openclaw/openclaw.json"
  echo ""
  echo "  Add to plugins.load.paths:"
  echo "    \"${plugin_path}\""
  echo ""
  echo "  Add to plugins.entries:"
  echo "    \"${sid}\": { \"enabled\": true }"
  echo ""
  echo "  Then restart OpenClaw."
  echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
}

if [ -n "$SUB_SKILL" ]; then
  JS_EYES_ROOT="${INSTALL_DIR}/${SKILL_NAME}"
  if [ ! -d "$JS_EYES_ROOT" ]; then
    err "JS Eyes is not installed at ${JS_EYES_ROOT}."
    err "Install js-eyes first: curl -fsSL ${SITE_URL}/install.sh | bash"
    exit 1
  fi

  info "Fetching skill registry..."
  REGISTRY_JSON=$(http_get "${SITE_URL}/skills.json" 2>/dev/null || true)

  if [ -z "$REGISTRY_JSON" ]; then
    err "Could not fetch skill registry from ${SITE_URL}/skills.json"
    exit 1
  fi

  PARENT_VERSION=$(read_local_version "$JS_EYES_ROOT")
  [ -n "$PARENT_VERSION" ] || PARENT_VERSION=$(json_get_parent_version "$REGISTRY_JSON")

  if [ "$SUB_SKILL" = "all" ]; then
    if [ ! -d "${JS_EYES_ROOT}/skills" ]; then
      err "No skills directory at ${JS_EYES_ROOT}/skills; nothing to update."
      exit 1
    fi
    any=0
    failed=0
    for entry in "${JS_EYES_ROOT}/skills"/*/; do
      [ -d "$entry" ] || continue
      any=1
      sid=$(basename "$entry")
      if ! install_one_sub_skill "$sid" "$REGISTRY_JSON" "$PARENT_VERSION"; then
        failed=$((failed + 1))
      fi
    done
    if [ "$any" = 0 ]; then
      info "No sub-skills installed under ${JS_EYES_ROOT}/skills."
    fi
    [ "$failed" = 0 ] || exit 1
    exit 0
  fi

  install_one_sub_skill "$SUB_SKILL" "$REGISTRY_JSON" "$PARENT_VERSION" || exit 1
  exit 0
fi

# ══════════════════════════════════════════════════════════════════════
# Main skill install (no argument)
# ══════════════════════════════════════════════════════════════════════

# ── Resolve latest version ────────────────────────────────────────────
info "Fetching latest release info..."
TAG=$(http_get "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null \
      | grep '"tag_name"' | head -1 | sed 's/.*"\(v[^"]*\)".*/\1/' || true)

if [ -n "$TAG" ]; then
  info "Latest version: ${TAG}"
else
  warn "Could not determine latest release — using latest available."
fi

# ── Prepare target directory ──────────────────────────────────────────
TARGET="${INSTALL_DIR}/${SKILL_NAME}"

if [ -d "$TARGET" ]; then
  warn "Directory already exists: ${TARGET}"
  confirm "Overwrite?" || { info "Aborted."; exit 0; }
  rm -rf "$TARGET"
fi

mkdir -p "$TARGET"

# ── Download with multi-source fallback ───────────────────────────────
_tmpdir=$(mktemp -d)
trap 'rm -rf "$_tmpdir"' EXIT

SKILL_ZIP="${_tmpdir}/skill.zip"
SKILL_ZIP_SHA256_URL="${SITE_URL}/js-eyes-skill.zip.sha256"
urls_skill_zip=("${SITE_URL}/js-eyes-skill.zip")
if [ -n "$TAG" ]; then
  VERSION="${TAG#v}"
  urls_skill_zip+=("https://github.com/${REPO}/releases/download/${TAG}/js-eyes-skill-v${VERSION}.zip")
  SKILL_ZIP_SHA256_URL="https://github.com/${REPO}/releases/download/${TAG}/js-eyes-skill-v${VERSION}.zip.sha256"
fi

info "Downloading skill bundle..."

if ! try_download "$SKILL_ZIP" "${urls_skill_zip[@]}"; then
  err "All download sources failed. Check your network and try again."
  exit 1
fi

EXPECTED_SHA256_LINE=$(http_get "${SKILL_ZIP_SHA256_URL}" 2>/dev/null || true)
EXPECTED_SHA256=$(printf '%s' "${EXPECTED_SHA256_LINE}" | awk 'NR==1{print $1}')
if ! verify_sha256 "$SKILL_ZIP" "$EXPECTED_SHA256"; then
  err "Refusing to install js-eyes without verified SHA-256."
  err "Expected sha file URL: ${SKILL_ZIP_SHA256_URL}"
  exit 1
fi

# ── Extract ───────────────────────────────────────────────────────────
info "Extracting skill bundle..."

extract_zip() {
  local zipfile="$1" dest="$2"
  if command -v unzip >/dev/null 2>&1; then
    unzip -qo "$zipfile" -d "$dest"
  elif command -v python3 >/dev/null 2>&1; then
    python3 -c "import zipfile,sys; zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])" "$zipfile" "$dest"
  else
    err "unzip or python3 is required to extract the skill bundle zip."; exit 1
  fi
}

extract_zip "$SKILL_ZIP" "$TARGET"

# Fix permissions: OpenClaw rejects world-writable plugin paths
find "$TARGET" -type f -exec chmod 644 {} + 2>/dev/null || true
find "$TARGET" -type d -exec chmod 755 {} + 2>/dev/null || true

# ── Install dependencies ──────────────────────────────────────────────
info "Installing dependencies (npm ci --ignore-scripts)..."
if ! run_npm_ci "$TARGET"; then
  err "Dependency installation failed."
  exit 1
fi

# ── Done ──────────────────────────────────────────────────────────────
ABSOLUTE_TARGET=$(cd "$TARGET" && pwd)
PLUGIN_PATH="${ABSOLUTE_TARGET}/openclaw-plugin"

ok "JS Eyes installed to: ${ABSOLUTE_TARGET}"

# Initialize the server token *before* the native-host install so that the
# host process has something to read on first popup "Sync Token From Host"
# attempt. `server token init` is idempotent (no-op if file already exists),
# so re-running install.sh is safe.
if [ "${SKIP_TOKEN_INIT}" != "1" ]; then
  if command -v npx >/dev/null 2>&1; then
    info "Initializing local server token (skip with --skip-token-init)..."
    if npx --yes js-eyes server token init >/dev/null 2>&1; then
      ok "Server token initialized at ~/.js-eyes/runtime/server.token."
    else
      warn "Server token init failed; run 'npx js-eyes server token init' later (or start OpenClaw to let the server auto-create it)."
    fi
  else
    warn "npx not found; skipping server token initialization. Run 'js-eyes server token init' (or start OpenClaw) before using Sync Token From Host."
  fi
fi

if [ "${SKIP_NATIVE_HOST}" != "1" ]; then
  if command -v npx >/dev/null 2>&1; then
    info "Registering browser native-messaging host (skip with --skip-native-host)..."
    if npx --yes js-eyes native-host install --browser all >/dev/null 2>&1; then
      ok "Native messaging host registered (Chrome + Firefox)."
    else
      warn "Native messaging host registration failed; run 'npx js-eyes native-host install' later."
    fi
  else
    warn "npx not found; skipping native-messaging host registration."
  fi
fi

echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  Next: register the plugin in ~/.openclaw/openclaw.json"
echo ""
echo "  Add to plugins.load.paths:"
echo "    \"${PLUGIN_PATH}\""
echo ""
echo "  Add to plugins.entries:"
echo "    \"js-eyes\": {"
echo "      \"enabled\": true,"
echo "      \"config\": { \"serverPort\": 18080, \"autoStartServer\": true }"
echo "    }"
echo ""
echo "  Then restart OpenClaw."
echo ""
echo "  Tip: in the browser extension popup, restart the browser first, then"
echo "       click \"Sync Token From Host / 从本机同步\" to auto-populate the"
echo "       server token. If it reports 'token-missing', start OpenClaw or"
echo "       run \`js-eyes server token init\` so the token file exists."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
