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

# ─────────────────────────────────────────────────────────────
# Publish OUTDATED @bytelyst/* packages to the Gitea npm registry.
#
# Uses a local manifest (.publish-manifest.json) to track content hashes
# of the last-published version.  Only packages whose built content has
# actually changed since the last publish are bumped and republished.
#
# Improvements over the previous version:
#   - Manifest-based comparison (no registry tarball downloads)
#   - Single pack per package (not double-pack)
#   - Single metadata fetch per package (cached in-process)
#   - No false-positive "OUTDATED" from pnpm-vs-npm pack differences
#   - No .npmrc overwrite bug
#
# Usage:
#   scripts/gitea/publish-outdated-packages.sh                # build + detect + publish
#   scripts/gitea/publish-outdated-packages.sh --dry-run      # build + detect only
#   scripts/gitea/publish-outdated-packages.sh --skip-build   # skip pnpm build
#   scripts/gitea/publish-outdated-packages.sh --filter @bytelyst/errors
#
# Requires: GITEA_NPM_TOKEN (env var or ~/.gitea_npm_token)
# ─────────────────────────────────────────────────────────────

REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
PACKAGES_DIR="$REPO_ROOT/packages"
MANIFEST_FILE="$REPO_ROOT/scripts/gitea/.publish-manifest.json"

# ── Network-aware Gitea resolution ─────────────────────────
NETWORK_MODE="${NETWORK:-home}"

if [ "$NETWORK_MODE" = "corp" ]; then
  GITEA_HOST="${GITEA_NPM_HOST:-localhost}"
  GITEA_PORT="${GITEA_NPM_PORT:-3300}"
  GITEA_BASE="http://${GITEA_HOST}:${GITEA_PORT}"
  IS_CORP=true
else
  if [ -n "${GITEA_NPM_HOST:-}" ] && [ "${GITEA_NPM_HOST}" != "localhost" ]; then
    GITEA_HOST="$GITEA_NPM_HOST"
  elif [ -f "$HOME/.gitea_vm_host" ]; then
    GITEA_HOST="$(cat "$HOME/.gitea_vm_host")"
  else
    GITEA_HOST="gitea.bytelyst.com"
  fi
  GITEA_PORT="${GITEA_NPM_PORT:-3300}"
  GITEA_BASE="http://${GITEA_HOST}:${GITEA_PORT}"
  IS_CORP=false
fi

REGISTRY_URL="${GITEA_NPM_REGISTRY_URL:-${GITEA_BASE}/api/packages/learning_ai_user/npm/}"
TOKEN="${GITEA_NPM_TOKEN:-}"
WORK_DIR="${TMPDIR:-/tmp}/bytelyst-publish-$$"
DRY_RUN=false
PACKAGE_FILTER=""
SKIP_BUILD=false

# Parse args
while [[ $# -gt 0 ]]; do
  case "$1" in
    --dry-run) DRY_RUN=true; shift ;;
    --filter)
      if [ -z "${2:-}" ]; then echo "ERROR: --filter requires a package name"; exit 1; fi
      PACKAGE_FILTER="$2"; shift 2 ;;
    --skip-build) SKIP_BUILD=true; shift ;;
    --help|-h)
      echo "Usage: $0 [--dry-run] [--skip-build] [--filter @bytelyst/name]"
      echo "  --dry-run     Detect outdated packages without publishing"
      echo "  --skip-build  Skip 'pnpm build' (assumes dist/ is current)"
      echo "  --filter NAME Only check/publish the named package"
      exit 0 ;;
    *) echo "Unknown arg: $1 (try --help)"; exit 1 ;;
  esac
done

# Resolve token
if [ -z "$TOKEN" ] && [ -f "$HOME/.gitea_npm_token" ]; then
  TOKEN="$(cat "$HOME/.gitea_npm_token")"
fi
if [ -z "$TOKEN" ]; then
  echo "ERROR: GITEA_NPM_TOKEN is required (env var or ~/.gitea_npm_token)"
  exit 1
fi

# Auth target for .npmrc
AUTH_TARGET="${REGISTRY_URL#http://}"
AUTH_TARGET="${AUTH_TARGET#https://}"

# Non-npm packages to skip
SKIP_DIRS="swift-platform-sdk swift-diagnostics kotlin-platform-sdk react-native-platform-sdk"

# Cleanup temp dir on exit
trap 'rm -rf "$WORK_DIR"' EXIT
mkdir -p "$WORK_DIR"

# Single .npmrc for all npm operations (auth + scoped registry + proxy override)
NPMRC_FILE="$WORK_DIR/.npmrc"
{
  printf '//%s:_authToken=%s\n' "$AUTH_TARGET" "$TOKEN"
  printf '@bytelyst:registry=%s\n' "$REGISTRY_URL"
  if [ "$IS_CORP" = true ]; then
    printf 'proxy=false\nhttps-proxy=false\n'
  fi
} > "$NPMRC_FILE"

# ── Helpers ────────────────────────────────────────────────

pkg_field() {
  node -e "process.stdout.write(String(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))['$1']||''))" "$2"
}

# Compute a deterministic content hash from an extracted tarball.
# Normalizes package.json (version→0.0.0, strips publishConfig) so that
# version bumps alone don't trigger re-publish.
# Uses relative paths and stdin hashing to ensure determinism across runs.
content_hash() {
  local extracted_dir="$1"  # contains package/ subdirectory
  local pkg_json="$extracted_dir/package/package.json"
  local norm; norm=$(mktemp)

  node -e "
    const fs=require('fs');
    const p=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));
    p.version='0.0.0'; delete p.publishConfig;
    // Normalize @bytelyst/* dep versions (pnpm resolves workspace:* to exact versions
    // which change on every bump — these shouldn't trigger re-publish)
    for(const s of ['dependencies','devDependencies','peerDependencies']){
      if(!p[s])continue;
      for(const [n]of Object.entries(p[s])){
        if(n.startsWith('@bytelyst/'))p[s][n]='*';
      }
    }
    fs.writeFileSync(process.argv[2],JSON.stringify(p));
  " "$pkg_json" "$norm"

  # Hash normalized package.json (via stdin — no path in output)
  # + all other files using relative paths (cd into package/ first)
  {
    echo "PKG $(shasum -a 256 < "$norm" | cut -d' ' -f1)"
    (cd "$extracted_dir/package" && find . -type f ! -name package.json -print0 \
      | sort -z | xargs -0 shasum -a 256 2>/dev/null) || true
  } | shasum -a 256 | cut -d' ' -f1
  rm -f "$norm"
}

# ── Manifest Operations ───────────────────────────────────

manifest_get_hash() {
  local name="$1"
  [ -f "$MANIFEST_FILE" ] || { echo ""; return; }
  node -e "
    try{const m=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));
    process.stdout.write(m[process.argv[2]]?.contentHash||'')}
    catch(e){process.stdout.write('')}
  " "$MANIFEST_FILE" "$name"
}

manifest_set() {
  local name="$1" version="$2" hash="$3"
  node -e "
    const fs=require('fs');
    let m={};try{m=JSON.parse(fs.readFileSync(process.argv[1],'utf8'))}catch(e){}
    m[process.argv[2]]={version:process.argv[3],contentHash:process.argv[4],
      publishedAt:new Date().toISOString()};
    const s=Object.fromEntries(Object.entries(m).sort(([a],[b])=>a.localeCompare(b)));
    fs.writeFileSync(process.argv[1],JSON.stringify(s,null,2)+'\n');
  " "$MANIFEST_FILE" "$name" "$version" "$hash"
}

# ── Registry Helpers (single fetch, cached) ───────────────

# Cache file: one line per package = "name:ver1,ver2,ver3"
REGISTRY_CACHE_FILE="$WORK_DIR/.registry-cache"
: > "$REGISTRY_CACHE_FILE"

registry_versions() {
  local name="$1"
  # Check cache first
  local cached
  cached=$(grep "^${name}:" "$REGISTRY_CACHE_FILE" 2>/dev/null | cut -d: -f2-) || true
  if [ -n "$cached" ]; then echo "$cached"; return; fi

  local encoded="${name/@/%40}"; encoded="${encoded//\//%2F}"
  local versions
  versions=$(curl -s -H "Authorization: token $TOKEN" \
    "${REGISTRY_URL}${encoded}" 2>/dev/null \
    | node -e "let d='';process.stdin.on('data',c=>d+=c);
      process.stdin.on('end',()=>{try{
        process.stdout.write(Object.keys(JSON.parse(d).versions||{}).join(','))
      }catch(e){process.stdout.write('')}})" 2>/dev/null) || true
  echo "${name}:${versions}" >> "$REGISTRY_CACHE_FILE"
  echo "$versions"
}

next_version() {
  local name="$1" current="$2"
  local versions; versions=$(registry_versions "$name")
  node -e "
    const pub=new Set((process.argv[1]||'').split(',').filter(Boolean));
    const p=process.argv[2].split('.').map(Number);
    p[2]++;while(pub.has(p.join('.'))){p[2]++}
    process.stdout.write(p.join('.'));
  " "$versions" "$current"
}

version_exists() {
  local name="$1" version="$2"
  local versions; versions=$(registry_versions "$name")
  echo ",$versions," | grep -q ",$version,"
}

# Run npm commands with proxy/env stripping for reliable localhost access
npm_clean() {
  (cd "$WORK_DIR" && env \
    -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY \
    -u npm_config_proxy -u npm_config_https_proxy \
    -u NPM_CONFIG_PROXY -u NPM_CONFIG_HTTPS_PROXY \
    -u NPM_CONFIG_REGISTRY -u NPM_CONFIG_STRICT_SSL \
    -u NPM_CONFIG_NOPROXY -u NODE_TLS_REJECT_UNAUTHORIZED \
    "$@")
}

# ── Main ───────────────────────────────────────────────────

echo "==> Network: $NETWORK_MODE ($( [ "$IS_CORP" = true ] && echo "corp — localhost tunnel" || echo "home — Azure VM" ))"
echo "    Registry: $REGISTRY_URL"
echo ""

# Phase 1: Build
if [ "$SKIP_BUILD" = false ]; then
  echo "==> Building all packages..."
  (cd "$REPO_ROOT" && pnpm build 2>&1 | tail -5)
  echo ""
fi

# Phase 2: Check each package against manifest
echo "==> Checking packages (manifest-based)..."
echo ""

# Arrays for tracking results
outdated_dirs=()
outdated_names=()
outdated_hashes=()  # content hash per outdated package (for manifest update after publish)
up_to_date=0
changed=0
skipped=0
errors=0

for pkg_json in "$PACKAGES_DIR"/*/package.json; do
  dir_name=$(basename "$(dirname "$pkg_json")")
  pkg_dir="$(dirname "$pkg_json")"

  # Skip native SDKs
  if echo "$SKIP_DIRS" | grep -qw "$dir_name"; then
    ((++skipped)); continue
  fi

  pkg_name="$(pkg_field name "$pkg_json")"
  pkg_version="$(pkg_field version "$pkg_json")"

  # Skip private packages
  private_flag="$(pkg_field private "$pkg_json")"
  if [ "$private_flag" = "true" ]; then
    ((++skipped)); continue
  fi

  # Apply filter
  if [ -n "$PACKAGE_FILTER" ] && [ "$pkg_name" != "$PACKAGE_FILTER" ]; then
    continue
  fi

  # Must have built output
  if [ ! -d "$pkg_dir/dist" ] && [ ! -d "$pkg_dir/generated" ]; then
    echo "  SKIP (no dist/): $pkg_name"
    ((++skipped)); continue
  fi

  # Pack locally (single pack — reused for publish if needed)
  safe_name="${pkg_name//@/}"; safe_name="${safe_name//\//-}"
  pack_dir="$WORK_DIR/$safe_name"
  rm -rf "$pack_dir"; mkdir -p "$pack_dir/extracted"

  if ! (cd "$pkg_dir" && pnpm pack --pack-destination "$pack_dir" >/dev/null 2>&1); then
    echo "  ERROR (pack): $pkg_name"
    ((++errors)); continue
  fi
  local_tgz="$(find "$pack_dir" -maxdepth 1 -name '*.tgz' | head -1)"
  if [ -z "$local_tgz" ]; then
    echo "  ERROR (no tgz): $pkg_name"
    ((++errors)); continue
  fi

  # Extract for content hashing (keep around for publish)
  tar xzf "$local_tgz" -C "$pack_dir/extracted" 2>/dev/null

  # Compute content hash
  local_hash="$(content_hash "$pack_dir/extracted")"
  manifest_hash="$(manifest_get_hash "$pkg_name")"

  if [ "$local_hash" = "$manifest_hash" ]; then
    echo "  UP-TO-DATE: $pkg_name@$pkg_version"
    ((++up_to_date))
    rm -rf "$pack_dir"  # free disk
  else
    if [ -z "$manifest_hash" ]; then
      echo "  NEW:        $pkg_name@$pkg_version"
    else
      echo "  CHANGED:    $pkg_name@$pkg_version"
    fi
    outdated_dirs+=("$pkg_dir")
    outdated_names+=("$pkg_name")
    outdated_hashes+=("$local_hash")
    ((++changed))
  fi
done

# ── Summary ────────────────────────────────────────────────

echo ""
echo "==> Summary"
echo "    Up-to-date: $up_to_date"
echo "    Changed:    $changed"
echo "    Skipped:    $skipped"
echo "    Errors:     $errors"
echo ""

if [ ${#outdated_dirs[@]} -eq 0 ]; then
  echo "All packages are up to date. Nothing to publish."
  exit 0
fi

echo "Packages to publish:"
for name in "${outdated_names[@]}"; do
  echo "  - $name"
done
echo ""

if [ "$DRY_RUN" = true ]; then
  echo "(dry-run mode — no changes made)"
  exit 0
fi

# Phase 3: Publish changed packages
echo "==> Publishing ${#outdated_dirs[@]} package(s)..."
echo ""

published=0
pub_errors=0
bumped_packages=()

for i in "${!outdated_dirs[@]}"; do
  pkg_dir="${outdated_dirs[$i]}"
  pkg_name="${outdated_names[$i]}"
  local_hash="${outdated_hashes[$i]}"
  pkg_version="$(pkg_field version "$pkg_dir/package.json")"
  safe_name="${pkg_name//@/}"; safe_name="${safe_name//\//-}"
  pack_dir="$WORK_DIR/$safe_name"

  echo "  [$((i+1))/${#outdated_dirs[@]}] $pkg_name@$pkg_version"

  # Find next available version if current already exists
  if version_exists "$pkg_name" "$pkg_version"; then
    new_version="$(next_version "$pkg_name" "$pkg_version")"
    echo "    Version $pkg_version exists, bumping to $new_version"

    # Update source package.json
    node -e "
      const fs=require('fs'),f=process.argv[1];
      const p=JSON.parse(fs.readFileSync(f,'utf8'));
      p.version=process.argv[2];
      fs.writeFileSync(f,JSON.stringify(p,null,2)+'\n');
    " "$pkg_dir/package.json" "$new_version"
    bumped_packages+=("$pkg_dir/package.json")
    pkg_version="$new_version"

    # Also update the extracted tarball's package.json (for repack)
    node -e "
      const fs=require('fs'),f=process.argv[1];
      const p=JSON.parse(fs.readFileSync(f,'utf8'));
      p.version=process.argv[2];
      fs.writeFileSync(f,JSON.stringify(p,null,2)+'\n');
    " "$pack_dir/extracted/package/package.json" "$new_version"
  fi

  # Strip publishConfig from extracted package.json (for npm repack)
  node -e "
    const fs=require('fs'),f=process.argv[1];
    const p=JSON.parse(fs.readFileSync(f,'utf8'));
    delete p.publishConfig;
    fs.writeFileSync(f,JSON.stringify(p,null,2)+'\n');
  " "$pack_dir/extracted/package/package.json"

  # Rewrite workspace:* protocol → resolved semver (addresses F16).
  #
  # pnpm pack DOES rewrite workspace: refs in its output tarball, BUT we
  # extract and re-pack with npm pack below (to work around a historical
  # Gitea-compat issue with pnpm's tarball format). npm pack does not
  # know about the workspace: protocol and passes it through literally,
  # so without this step the published tarball contains
  #   "@bytelyst/errors": "workspace:*"
  # which then fails 'pnpm install' inside any Docker build with
  #   ERR_PNPM_WORKSPACE_PKG_NOT_FOUND
  # because there is no workspace context inside the container.
  #
  # This rewriter mirrors what pnpm publish would have done: look up the
  # target package's current source version and write "^x.y.z".
  node -e "
    const fs=require('fs'),path=require('path');
    const pkgFile=process.argv[1];
    const packagesDir=process.argv[2];
    const p=JSON.parse(fs.readFileSync(pkgFile,'utf8'));
    let rewrites=0;
    for (const section of ['dependencies','peerDependencies','optionalDependencies','devDependencies']) {
      if (!p[section]) continue;
      for (const [name,ver] of Object.entries(p[section])) {
        if (typeof ver !== 'string' || !ver.startsWith('workspace:')) continue;
        // workspace:* | workspace:^ | workspace:~ | workspace:1.2.3
        const protoSpec=ver.slice('workspace:'.length);
        let targetDir;
        if (name.startsWith('@bytelyst/')) {
          targetDir=path.join(packagesDir,name.replace('@bytelyst/',''));
        } else {
          // Should not happen for @bytelyst/* publishes, but be safe
          console.error('  WARN: workspace: ref to non-@bytelyst package:',name);
          continue;
        }
        const targetPkgFile=path.join(targetDir,'package.json');
        if (!fs.existsSync(targetPkgFile)) {
          throw new Error('Cannot resolve workspace: ref '+name+' → '+targetPkgFile+' not found');
        }
        const targetVersion=JSON.parse(fs.readFileSync(targetPkgFile,'utf8')).version;
        let resolved;
        if (protoSpec === '*' || protoSpec === '^') resolved='^'+targetVersion;
        else if (protoSpec === '~') resolved='~'+targetVersion;
        else if (/^[0-9]/.test(protoSpec)) resolved=protoSpec;
        else resolved='^'+targetVersion;
        p[section][name]=resolved;
        rewrites++;
        console.error('    rewrote',section+'.'+name,':',ver,'→',resolved);
      }
    }
    if (rewrites > 0) {
      fs.writeFileSync(pkgFile,JSON.stringify(p,null,2)+'\n');
    }
  " "$pack_dir/extracted/package/package.json" "$PACKAGES_DIR"

  # Defense in depth: refuse to publish if any workspace: ref survived.
  if grep -q '"workspace:' "$pack_dir/extracted/package/package.json"; then
    echo "    ERROR: workspace: ref survived rewriter in $pkg_name (F16 guard)"
    grep -n '"workspace:' "$pack_dir/extracted/package/package.json" | head -5
    ((++pub_errors)); continue
  fi

  # Repack with npm (from already-extracted content — no second pnpm pack)
  rm -f "$pack_dir"/*.tgz
  if ! (cd "$pack_dir/extracted/package" && npm pack --pack-destination "$pack_dir" >/dev/null 2>&1); then
    echo "    ERROR: npm repack failed"
    ((++pub_errors)); continue
  fi

  final_tgz="$(find "$pack_dir" -maxdepth 1 -name '*.tgz' | head -1)"
  if [ -z "$final_tgz" ]; then
    echo "    ERROR: no tarball after repack"
    ((++pub_errors)); continue
  fi

  # Publish
  echo "    Publishing $pkg_name@$pkg_version..."
  if npm_clean npm publish "$final_tgz" \
    --registry "$REGISTRY_URL" \
    --userconfig "$NPMRC_FILE" 2>&1; then
    echo "    Published $pkg_name@$pkg_version"
    manifest_set "$pkg_name" "$pkg_version" "$local_hash"
    ((++published))
  else
    echo "    FAILED to publish $pkg_name@$pkg_version"
    ((++pub_errors))
  fi

  # Free disk
  rm -rf "$pack_dir"
  echo ""
done

echo "==> Done: $published published, $pub_errors failed"

if [ ${#bumped_packages[@]} -gt 0 ]; then
  echo ""
  echo "NOTE: ${#bumped_packages[@]} package(s) had their version bumped."
  echo "      Commit these + the manifest when ready:"
  for f in "${bumped_packages[@]}"; do
    echo "        $f"
  done
  echo "        $MANIFEST_FILE"
fi

exit $((pub_errors > 0 ? 1 : 0))
