#!/usr/bin/env sh
set -eu

# KHAL app-kit installer. Human-facing endpoint is https://install.khal.ai/app-kit
# The endpoint may serve this script, but docs/examples should not use a redundant /install.sh path.

PRODUCT="app-kit"
TRUSTED_REPOSITORY="khal-os/app-kit"
COSIGN_CERTIFICATE_IDENTITY_REGEXP="^https://github.com/khal-os/app-kit/.github/workflows/.*@(refs/heads/(dev|main)|refs/tags/v[0-9].*)$"
COSIGN_CERTIFICATE_ISSUER="https://token.actions.githubusercontent.com"
DEFAULT_ENDPOINT="https://install.khal.ai/app-kit"
CHANNEL="${KHAL_APP_KIT_CHANNEL:-latest}"
VERSION_PIN="${KHAL_APP_KIT_VERSION:-}"
BASE_URL="${KHAL_APP_KIT_BASE_URL:-$DEFAULT_ENDPOINT}"
INSTALL_ROOT="${KHAL_APP_KIT_INSTALL_ROOT:-$HOME/.local/share/khal}"
BIN_DIR="${KHAL_APP_KIT_BIN_DIR:-$HOME/.local/bin}"
AUDIT_LOG="${KHAL_APP_KIT_AUDIT_LOG:-$HOME/.khal/install-audit.log}"
TMP_PARENT="${TMPDIR:-/tmp}"

say() { printf '%s\n' "$*"; }
err() { printf 'khal app-kit installer: %s\n' "$*" >&2; }
fail() { code="$1"; shift; err "$*"; exit "$code"; }

case "$BASE_URL" in
  */install.sh) fail 2 "use product endpoint https://install.khal.ai/app-kit, not /install.sh" ;;
esac
case "$BASE_URL" in
  https://install.khal.ai/*|http://127.0.0.1:*|http://localhost:*|file://*) ;;
  *) fail 2 "refusing non-KHAL installer endpoint: $BASE_URL" ;;
esac
case "$CHANNEL" in
  dev|homolog|latest|stable) ;;
  *) fail 2 "unsupported channel '$CHANNEL'" ;;
esac
if [ "$CHANNEL" = "stable" ]; then CHANNEL="latest"; fi

need_cmd() { command -v "$1" >/dev/null 2>&1; }
fetch() {
  url="$1"; dest="$2"
  case "$url" in
    file://*) cp "${url#file://}" "$dest" ;;
    http://*|https://*)
      if need_cmd curl; then curl -fsSL "$url" -o "$dest"; elif need_cmd wget; then wget -qO "$dest" "$url"; else fail 2 "curl or wget required"; fi
      ;;
    *) cp "$url" "$dest" ;;
  esac
}
sha256_file() {
  if need_cmd sha256sum; then sha256sum "$1" | awk '{print $1}'; elif need_cmd shasum; then shasum -a 256 "$1" | awk '{print $1}'; else fail 2 "sha256sum or shasum required"; fi
}
json_get_string() {
  key="$1"; file="$2"
  if need_cmd node; then
    node -e "const fs=require('fs'); const data=JSON.parse(fs.readFileSync(process.argv[2],'utf8')); const path=process.argv[1].split('.'); let cur=data; for (const p of path) cur=cur && cur[p]; if (typeof cur === 'string') process.stdout.write(cur);" "$key" "$file"
  else
    sed -n "s/.*\"$(printf '%s' "$key" | sed 's/.*\.//')\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$file" | head -1
  fi
}
select_asset_json() {
  manifest="$1"; current_platform="$2"
  if need_cmd node; then
    node - "$manifest" "$current_platform" <<'NODE'
const fs = require('fs');
const manifest = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const currentPlatform = process.argv[3];
const assets = Array.isArray(manifest.assets) ? manifest.assets : [];
const tarballs = assets.filter((item) => /\.(tgz|tar\.gz)$/.test(item.name || '') && !/installer/.test(item.name || ''));
const appKitTarballs = tarballs.filter((item) => /(^|[-_])app[-_]kit([-_.]|$)|^khal-os-app-kit-/.test(item.name || ''));
const candidates = appKitTarballs.length > 0 ? appKitTarballs : tarballs;
const hasPlatformMetadata = (item) => Boolean(item.platform || item.platforms || item.os || item.arch || item.libc);
const matchesPlatform = (item) => {
  if (item.platform === currentPlatform) return true;
  if (Array.isArray(item.platforms) && item.platforms.includes(currentPlatform)) return true;
  if (item.os || item.arch || item.libc) {
    const [os, arch, libc] = currentPlatform.split('-');
    return (!item.os || item.os === os) && (!item.arch || item.arch === arch) && (!item.libc || item.libc === libc);
  }
  return false;
};
const platformSpecific = candidates.filter(hasPlatformMetadata);
const exact = platformSpecific.filter(matchesPlatform);
let asset;
if (exact.length === 1) {
  asset = exact[0];
} else if (exact.length > 1) {
  console.error(`ambiguous assets for platform ${currentPlatform}`);
  process.exit(4);
} else if (platformSpecific.length > 0) {
  console.error(`manifest has platform-specific assets but none for ${currentPlatform}`);
  process.exit(5);
} else if (candidates.length === 1) {
  asset = candidates[0];
} else {
  console.error(`ambiguous generic assets for platform ${currentPlatform}`);
  process.exit(4);
}
if (!asset) process.exit(3);
process.stdout.write(JSON.stringify(asset));
NODE
  else
    fail 2 "node is required to parse release manifest assets"
  fi
}
json_field() {
  field="$1"
  node -e "const obj=JSON.parse(process.argv[1]); const value=obj[process.argv[2]]; if (value == null) process.exit(3); process.stdout.write(String(value));" "$2" "$field"
}
platform_id() {
  os=$(uname -s | tr '[:upper:]' '[:lower:]')
  arch=$(uname -m)
  case "$arch" in x86_64|amd64) arch=x64 ;; aarch64|arm64) arch=arm64 ;; esac
  libc=glibc
  if command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; then libc=musl; fi
  printf '%s-%s-%s' "$os" "$arch" "$libc"
}
verify_artifact() {
  artifact="$1"; expected_sha="$2"; manifest="$3"; artifact_url="$4"
  actual_sha=$(sha256_file "$artifact")
  [ "$actual_sha" = "$expected_sha" ] || fail 4 "sha256 mismatch for artifact"

  if need_cmd gh; then
    manifest_repo=$(json_get_string provenance.repository "$manifest" || true)
    if [ -n "$manifest_repo" ] && [ "$manifest_repo" != "$TRUSTED_REPOSITORY" ]; then
      fail 3 "manifest provenance repository mismatch: $manifest_repo"
    fi
    if gh attestation verify "$artifact" --repo "$TRUSTED_REPOSITORY" >/dev/null 2>&1; then
      say "Verified GitHub/Sigstore attestation for $artifact"
      return 0
    fi
  fi
  if need_cmd cosign; then
    sig="${artifact}.sig"
    cert="${artifact}.pem"
    if [ -n "$artifact_url" ]; then
      fetch "${artifact_url}.sig" "$sig" >/dev/null 2>&1 || true
      fetch "${artifact_url}.pem" "$cert" >/dev/null 2>&1 || true
    fi
    if [ -f "$sig" ] && [ -f "$cert" ] && cosign verify-blob --signature "$sig" --certificate "$cert" --certificate-identity-regexp "$COSIGN_CERTIFICATE_IDENTITY_REGEXP" --certificate-oidc-issuer "$COSIGN_CERTIFICATE_ISSUER" "$artifact" >/dev/null 2>&1; then
      say "Verified cosign signature for $artifact"
      return 0
    fi
  fi
  if [ "${INSECURE:-0}" = "1" ]; then
    mkdir -p "$(dirname "$AUDIT_LOG")"
    printf '%s channel=%s version=%s artifact=%s sha256=%s verifier=sha256-only endpoint=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$CHANNEL" "${VERSION_PIN:-manifest}" "$artifact" "$actual_sha" "$BASE_URL" >> "$AUDIT_LOG"
    err "INSECURE=1: falling back to sha256-only verification; audit logged at $AUDIT_LOG"
    return 0
  fi
  verification_mode=$(json_get_string verification.mode "$manifest" || true)
  verification_owner=$(json_get_string verification.owner "$manifest" || true)
  if [ "$verification_mode" = "sha256-khal-endpoint" ] && [ "$verification_owner" = "khal" ]; then
    case "$BASE_URL" in
      https://install.khal.ai/*)
        mkdir -p "$(dirname "$AUDIT_LOG")"
        printf '%s channel=%s version=%s artifact=%s sha256=%s verifier=sha256-khal-endpoint endpoint=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$CHANNEL" "${VERSION_PIN:-manifest}" "$artifact" "$actual_sha" "$BASE_URL" >> "$AUDIT_LOG"
        say "Verified SHA256 from KHAL-owned endpoint manifest for $artifact"
        return 0
        ;;
    esac
  fi
  fail 3 "attestation/signature verification failed; set INSECURE=1 only for audited break-glass installs"
}
ensure_runtime_dependencies() {
  check_dir="$1"
  if [ ! -f "$check_dir/package.json" ]; then return 0; fi
  deps=$(node -e "const p=require(process.argv[1]); process.stdout.write(Object.keys(p.dependencies||{}).join(' '));" "$check_dir/package.json" 2>/dev/null || true)
  if [ -z "$deps" ]; then return 0; fi
  for dep in $deps; do
    dep_path="$check_dir/node_modules/$dep/package.json"
    case "$dep" in
      @*/*) dep_path="$check_dir/node_modules/$dep/package.json" ;;
    esac
    [ -f "$dep_path" ] || fail 5 "release tarball missing vendored runtime dependency $dep; refuse to install unverified npm code at install time"
  done
}

install_artifact() {
  artifact="$1"; version="$2"
  dest="$INSTALL_ROOT/$version"
  staging="$INSTALL_ROOT/.staging-$version-$$"
  mkdir -p "$INSTALL_ROOT" "$BIN_DIR"
  rm -rf "$staging"
  mkdir -p "$staging"
  tar -xzf "$artifact" -C "$staging"
  pkg_dir="$staging/package"
  if [ ! -f "$pkg_dir/dist/index.js" ]; then
    found=$(find "$staging" -path '*/dist/index.js' -type f | head -1 || true)
    [ -n "$found" ] || fail 5 "artifact does not contain dist/index.js"
    pkg_dir=$(dirname "$(dirname "$found")")
  fi
  ensure_runtime_dependencies "$pkg_dir"
  rm -rf "$dest.tmp.$$"
  mv "$pkg_dir" "$dest.tmp.$$"
  rm -rf "$dest"
  mv "$dest.tmp.$$" "$dest"
  chmod +x "$dest/dist/index.js" || true
  ln -sfn "$dest/dist/index.js" "$BIN_DIR/khal.tmp.$$"
  mv -f "$BIN_DIR/khal.tmp.$$" "$BIN_DIR/khal"
  rm -rf "$staging"
}
run_khal_update() {
  if [ ! -x "$BIN_DIR/khal" ]; then
    fail 6 "khal update command unavailable after install"
  fi
  PATH="$BIN_DIR:$PATH" khal update --channel "$CHANNEL" --endpoint "$BASE_URL" || fail 6 "khal update failed for channel $CHANNEL endpoint $BASE_URL"
  mkdir -p "$(dirname "$AUDIT_LOG")"
  printf '%s action=khal-update channel=%s endpoint=%s command="khal update --channel %s --endpoint %s"\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$CHANNEL" "$BASE_URL" "$CHANNEL" "$BASE_URL" >> "$AUDIT_LOG"
}

platform=$(platform_id)
say "KHAL app-kit installer"
say "Endpoint: $BASE_URL"
say "Channel: $CHANNEL"
say "Platform: $platform"

workdir=$(mktemp -d "$TMP_PARENT/khal-app-kit-install.XXXXXX") || fail 1 "could not create temp dir"
trap 'rm -rf "$workdir"' EXIT INT TERM
manifest_url="$BASE_URL/.well-known/$CHANNEL.json"
if [ -n "$VERSION_PIN" ]; then manifest_url="$BASE_URL/.well-known/$VERSION_PIN.json"; fi
manifest="$workdir/manifest.json"
fetch "$manifest_url" "$manifest" || fail 1 "failed to fetch manifest $manifest_url"

manifest_endpoint=$(json_get_string install.endpoint "$manifest" || true)
if [ -n "$manifest_endpoint" ] && [ "$manifest_endpoint" != "$BASE_URL" ]; then
  fail 2 "manifest endpoint mismatch: $manifest_endpoint"
fi
version=$(json_get_string version "$manifest" || true)
[ -n "$version" ] || fail 2 "manifest missing version"
asset_json=$(select_asset_json "$manifest" "$platform") || fail 2 "manifest missing unambiguous app-kit tarball asset for $platform"
asset_url=$(json_field url "$asset_json")
asset_name=$(json_field name "$asset_json")
asset_sha=$(json_field sha256 "$asset_json")
artifact="$workdir/$asset_name"
fetch "$asset_url" "$artifact" || fail 1 "failed to fetch artifact $asset_url"
verify_artifact "$artifact" "$asset_sha" "$manifest" "$asset_url"
install_artifact "$artifact" "$version"
run_khal_update
say "Installed khal $version to $INSTALL_ROOT/$version"
say "Binary: $BIN_DIR/khal"
