#!/usr/bin/env bash
# Install one verified standalone cohort. Core execution needs no JS runtime.
# Explicit prerelease: curl -fsSL <versioned-installer-url> | KAYA_VERSION=0.1.0-rc.1 bash
set -euo pipefail
export LC_ALL=C

KAYA_VERSION="${KAYA_VERSION:-latest}"
KAYA_RELEASE_BASE="${KAYA_RELEASE_BASE:-https://storage.googleapis.com/assets.kaya-lang.org/releases}"
KAYA_INSTALL_DIR="${KAYA_INSTALL_DIR:-$HOME/.kaya}"
KAYA_RELEASE_BASE="${KAYA_RELEASE_BASE%/}"
KAYA_INSTALL_DIR="${KAYA_INSTALL_DIR%/}"
work=""
new_cohort=""
pointer_stage=""
install_lock=""
created_links=""
committed=0

info() { printf 'kaya: %s\n' "$*"; }
die() { printf 'kaya: %s\n' "$*" >&2; exit 1; }
cleanup() {
  # A signal can arrive immediately after rename and before committed=1.
  # Never remove a cohort already selected by the atomic pointer operation.
  if [ -n "$new_cohort" ] && [ -L "$KAYA_INSTALL_DIR/current" ] && [ "$(readlink "$KAYA_INSTALL_DIR/current")" = "versions/${new_cohort##*/}" ]; then
    committed=1
  fi
  if [ "$committed" -eq 0 ]; then
    for name in $created_links; do
      if [ -L "$KAYA_INSTALL_DIR/bin/$name" ] && [ "$(readlink "$KAYA_INSTALL_DIR/bin/$name")" = "../current/bin/$name" ]; then
        rm -f -- "$KAYA_INSTALL_DIR/bin/$name"
      fi
    done
    if [ -n "$new_cohort" ]; then rm -rf -- "$new_cohort"; fi
  fi
  if [ -n "$pointer_stage" ]; then rm -rf -- "$pointer_stage"; fi
  if [ -n "$work" ]; then rm -rf -- "$work"; fi
  if [ -n "$install_lock" ]; then rmdir -- "$install_lock" || true; fi
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM

valid_version() {
  awk -v version="$1" 'BEGIN {
    if (length(version)>128 || version !~ /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/) exit 1;
    sub(/\+.*/, "", version);
    if (version ~ /-/) {
      sub(/^[^-]*-/, "", version); n=split(version, pieces, "[.]");
      for (i=1;i<=n;i++) if (pieces[i] ~ /^[0-9]+$/ && length(pieces[i])>1 && substr(pieces[i],1,1)=="0") exit 1;
    }
  }'
}

sha_file() {
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$1" | awk '{print $1}'
  else
    shasum -a 256 "$1" | awk '{print $1}'
  fi
}

fetch() {
  local url="$1" destination="$2" limit="${3:-4194304}"
  curl -fSL --proto '=https' --proto-redir '=https' --tlsv1.2 --max-filesize "$limit" \
    -o "$destination" "$url" || die "could not download $url"
  [ "$(wc -c < "$destination" | tr -d ' ')" -le "$limit" ] || die "download exceeds its declared limit"
}

# Read JSON without a language runtime. The complete document is parsed, duplicate
# keys are refused, and only typed, unescaped identity fields leave the parser.
# Metadata is bounded; values never become shell code or command arguments via eval.
json_fields() {
  awk -v kind="$2" -v target="${3:-}" '
  function fail(message) { bad=1; print "kaya: invalid " kind " metadata: " message > "/dev/stderr"; exit 1 }
  function whitespace() { while (substr(input,pos,1) ~ /[ \t\r\n]/ && pos<=length(input)) pos++ }
  function string(    result,c,e,hex) {
    if (substr(input,pos++,1)!="\"") fail("expected string"); result=""; was_escaped=0;
    while (pos<=length(input)) {
      c=substr(input,pos++,1);
      if (c=="\"") return result;
      if (c ~ /[[:cntrl:]]/) fail("unescaped control character");
      if (c=="\\") {
        was_escaped=1; e=substr(input,pos++,1);
        if (e=="u") {
          hex=substr(input,pos,4); if (length(hex)!=4 || hex !~ /^[0-9A-Fa-f]+$/) fail("invalid unicode escape");
          result=result "\\u" hex; pos+=4;
        } else if (e ~ /^["\\\/bfnrt]$/) result=result "\\" e;
        else fail("invalid string escape");
      } else result=result c;
    }
    fail("unterminated string");
  }
  function value(path,depth,    c,key,child,number,array_index) {
    if (depth>64 || ++nodes>100000) fail("document limit");
    whitespace(); c=substr(input,pos,1);
    if (path in types) fail("duplicate key");
    if (c=="{") {
      types[path]="object"; pos++; whitespace();
      if (substr(input,pos,1)=="}") {pos++; return}
      while (1) {
        whitespace(); key=string(); if (was_escaped) fail("escaped object key");
        whitespace(); if (substr(input,pos++,1)!=":") fail("expected colon");
        child=path SUBSEP key; value(child,depth+1); whitespace(); c=substr(input,pos++,1);
        if (c=="}") return; if (c!=",") fail("expected object separator");
      }
    } else if (c=="[") {
      types[path]="array"; pos++; whitespace(); array_index=0;
      if (substr(input,pos,1)=="]") {pos++; return}
      while (1) {
        value(path SUBSEP array_index++,depth+1); whitespace(); c=substr(input,pos++,1);
        if (c=="]") return; if (c!=",") fail("expected array separator");
      }
    } else if (c=="\"") {
      types[path]="string"; values[path]=string(); escapes[path]=was_escaped;
    } else if (match(substr(input,pos),/^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/)) {
      number=substr(input,pos,RLENGTH); pos+=RLENGTH; types[path]="number"; values[path]=number;
    } else if (substr(input,pos,4)=="true" || substr(input,pos,4)=="null") {
      types[path]=substr(input,pos,4); pos+=4;
    } else if (substr(input,pos,5)=="false") {types[path]="false"; pos+=5}
    else fail("expected value");
  }
  function text(path) {
    if (types[path]!="string" || escapes[path]) fail("missing or escaped identity field");
    return values[path];
  }
  {input=input $0 "\n"; if (length(input)>4194304) fail("document too large")}
  END {
    if (bad) exit 1; pos=1; value("",0); whitespace();
    if (pos<=length(input) || types[""]!="object") fail("trailing input or non-object root");
    if (types[SUBSEP "schemaVersion"]!="number" || values[SUBSEP "schemaVersion"]!="1") fail("unsupported schemaVersion");
    if (kind=="channels") {
      if (types[SUBSEP "stable"]!="object") fail("stable channel unavailable");
      version=text(SUBSEP "stable" SUBSEP "version"); hash=text(SUBSEP "stable" SUBSEP "manifestSha256");
      if (length(hash)!=64 || hash !~ /^[0-9a-f]+$/) fail("invalid stable manifest digest");
      print version; print hash;
    } else {
      version=text(SUBSEP "version"); source=text(SUBSEP "sourceCommit");
      if (length(source)!=40 || source !~ /^[0-9a-f]+$/) fail("invalid sourceCommit");
      if (text(SUBSEP "profile")!="standalone" || text(SUBSEP "protocolVersion")!="2.0.0" || text(SUBSEP "httpBindingVersion")!="2.0.0") fail("unsupported profile or protocol");
      if (types[SUBSEP "platforms"]!="object") fail("platform map missing");
      row=SUBSEP "platforms" SUBSEP target;
      if (types[row]!="object" || text(row SUBSEP "status")!="supported") fail("platform " target " is unavailable or gated");
      archive=text(row SUBSEP "archive"); hash=text(row SUBSEP "sha256"); size=values[row SUBSEP "size"];
      if (length(hash)!=64 || hash !~ /^[0-9a-f]+$/) fail("invalid archive digest");
      if (types[row SUBSEP "size"]!="number" || size !~ /^[1-9][0-9]*$/ || length(size)>12) fail("invalid archive size");
      print version; print source; print archive; print hash; print size;
    }
  }' "$1"
}

# Check every checksum row for canonical unique safe names before selecting one.
# The format is shared by the outer publication and exhaustive archive inventory.
checksum_rows() {
  awk '
  function fail() {print "kaya: invalid checksum inventory" > "/dev/stderr"; bad=1; exit 1}
  {
    hash=substr($0,1,64); name=substr($0,67);
    if (length(hash)!=64 || hash !~ /^[0-9a-f]+$/ || substr($0,65,2)!="  " || name=="" || name !~ /^[A-Za-z0-9_.+\/-]+$/ || name ~ /^\// || name ~ /\/$/ || name ~ /\/\// || name ~ /(^|\/)\.\.?($|\/)/ || seen[name]++) fail();
    print hash "  " name;
  }
  END {if (bad || NR==0) exit 1}' "$1"
}

checksum_for() {
  awk -v name="$2" '$2==name {count++; hash=$1} END {if (count!=1) exit 1; print hash}' "$1"
}

validate_archive() {
  tar -tzf "$work/archive.tar.gz" > "$work/members" || die "could not list standalone archive"
  tar -tvzf "$work/archive.tar.gz" > "$work/member-details" || die "could not inspect standalone archive"
  awk '
  function fail(message) {print "kaya: invalid archive: " message > "/dev/stderr"; bad=1; exit 1}
  function allowed(name) {
    return name=="bin/kaya" || name=="bin/kayad" || name=="bin/kaya-tool-server" || name=="LICENSE.txt" || name=="README.md" || name=="RELEASE_NOTES.md" || name=="FILE_SHA256SUMS" || name ~ /^THIRD_PARTY_NOTICES\/(jieba-MIT\.txt|OpenCC-Apache-2\.0\.txt|pinyin-pro-MIT\.txt|chinese-conv-MIT\.txt|zhuyin-ISC\.txt|pagedjs-MIT\.txt|mermaid-MIT\.txt|rmcp-MIT\.txt)$/ || name ~ /^THIRD_PARTY_NOTICES\/rust\/[^/]+\/[^/]+/;
  }
  NR==FNR {
    name=$0;
    if (name !~ /^[A-Za-z0-9_.+\/-]+$/ || name ~ /^\// || name ~ /\/\// || name ~ /(^|\/)\.\.?($|\/)/) fail("unsafe member path");
    sub(/\/$/,"",name); if (seen[name]++) fail("duplicate member"); names[FNR]=name; count=FNR; next;
  }
  {
    name=$NF; sub(/\/$/,"",name); mode=$1;
    if (name!=names[FNR]) fail("ambiguous member listing");
    if (substr(mode,1,1)=="d") {
      if (mode!="drwxr-xr-x") fail("directory mode"); directories[name]=1;
    } else {
      if (!allowed(name)) fail("undeclared payload");
      expected=name ~ /^bin\// ? "-rwxr-xr-x" : "-rw-r--r--";
      if (mode!=expected) fail("links, special members or unexpected mode");
      files[name]=1;
    }
  }
  END {
    if (bad) exit 1;
    if (FNR!=count || count==0) fail("incomplete member listing");
    split("bin/kaya bin/kayad bin/kaya-tool-server LICENSE.txt README.md RELEASE_NOTES.md FILE_SHA256SUMS THIRD_PARTY_NOTICES/jieba-MIT.txt THIRD_PARTY_NOTICES/OpenCC-Apache-2.0.txt THIRD_PARTY_NOTICES/pinyin-pro-MIT.txt THIRD_PARTY_NOTICES/chinese-conv-MIT.txt THIRD_PARTY_NOTICES/zhuyin-ISC.txt THIRD_PARTY_NOTICES/pagedjs-MIT.txt THIRD_PARTY_NOTICES/mermaid-MIT.txt THIRD_PARTY_NOTICES/rmcp-MIT.txt",required," ");
    for (i in required) if (!(required[i] in files)) fail("required member missing");
    for (directory in directories) {
      used=0; for (file in files) if (index(file,directory "/")==1) used=1;
      if (!used) fail("undeclared empty directory");
    }
    for (file in files) print file;
  }' "$work/members" "$work/member-details" | sort > "$work/archive-files" || die "standalone archive membership refused"
}

verify_payload() {
  local payload="$1" expected actual
  checksum_rows "$payload/FILE_SHA256SUMS" > "$work/inner-rows" || die "archive checksum inventory refused"
  awk '{print $2}' "$work/inner-rows" | sort > "$work/inner-members"
  grep -v '^FILE_SHA256SUMS$' "$work/archive-files" > "$work/expected-inner-members"
  cmp -s "$work/inner-members" "$work/expected-inner-members" || die "archive checksum inventory is not exhaustive"
  while IFS=' ' read -r expected relative; do
    [ -f "$payload/$relative" ] && [ ! -L "$payload/$relative" ] || die "archive file is not regular"
    actual="$(sha_file "$payload/$relative")"
    [ "$actual" = "$expected" ] || die "archive member checksum mismatch: $relative"
  done < "$work/inner-rows"
}

ensure_path() {
  case ":$PATH:" in *":$KAYA_INSTALL_DIR/bin:"*) return 0 ;; esac
  local rc="" line path_prefix shell_name="${SHELL:-}"
  printf -v path_prefix '%q' "$KAYA_INSTALL_DIR/bin"
  line="export PATH=$path_prefix:\$PATH"
  case "${shell_name##*/}" in
    zsh) rc="$HOME/.zshrc" ;;
    bash) rc="$HOME/.bashrc" ;;
    fish) rc="$HOME/.config/fish/config.fish"; line="set -gx PATH $path_prefix \$PATH" ;;
  esac
  if [ -n "$rc" ]; then
    if mkdir -p "$(dirname "$rc")" && { grep -qsF "$KAYA_INSTALL_DIR/bin" "$rc" 2>/dev/null || printf '\n# Added by the Kaya installer\n%s\n' "$line" >> "$rc"; }; then
      info "added $KAYA_INSTALL_DIR/bin to PATH in $rc"
    else
      info "installation is complete; add $KAYA_INSTALL_DIR/bin to PATH manually"
    fi
  else
    info "add $KAYA_INSTALL_DIR/bin to PATH to use kaya"
  fi
}

main() {
  local os arch target requested stable_hash="" version source archive_relative archive_hash archive_size expected_hash actual_hash actual_size name existing cohort pending
  for name in awk curl tar gzip mktemp sort cmp grep find chmod cp ln mkdir mv rm rmdir readlink sed stat uname wc tr; do
    command -v "$name" >/dev/null 2>&1 || die "$name is required by the installer"
  done
  command -v sha256sum >/dev/null 2>&1 || command -v shasum >/dev/null 2>&1 || die "sha256sum or shasum is required"
  [[ "$KAYA_RELEASE_BASE" =~ ^https://[A-Za-z0-9.-]+(:[0-9]+)?(/[A-Za-z0-9._~/-]+)?$ ]] || die "KAYA_RELEASE_BASE must be an HTTPS URL without credentials, queries or fragments"
  [ -n "$KAYA_INSTALL_DIR" ] && [ "$KAYA_INSTALL_DIR" != / ] || die "invalid installation directory"
  case "$KAYA_INSTALL_DIR" in *$'\n'*|*$'\r'*) die "invalid installation directory" ;; esac
  os="$(uname -s)"; arch="$(uname -m)"
  case "$os" in Linux) os=linux ;; Darwin) os=darwin ;; *) die "unsupported OS; Windows requires install.ps1" ;; esac
  case "$arch" in x86_64|amd64) arch=x64 ;; arm64|aarch64) arch=arm64 ;; *) die "unsupported architecture: $arch" ;; esac
  target="$os-$arch"
  work="$(mktemp -d "${TMPDIR:-/tmp}/kaya-install.XXXXXXXX")"
  requested="$KAYA_VERSION"
  if [ "$requested" = latest ]; then
    fetch "$KAYA_RELEASE_BASE/kaya/channels.json" "$work/channels.json"
    json_fields "$work/channels.json" channels > "$work/channel-fields" || die "stable channel refused"
    { IFS= read -r requested; IFS= read -r stable_hash; } < "$work/channel-fields"
    valid_version "$requested" && [[ "${requested%%+*}" != *-* ]] || die "stable channel must name a final semantic version"
  else
    valid_version "$requested" || die "KAYA_VERSION must be an explicit semantic version or latest; dev is unavailable"
  fi
  version="$requested"
  fetch "$KAYA_RELEASE_BASE/kaya/v$version/release.json" "$work/release.json"
  if [ -n "$stable_hash" ]; then
    [ "$(sha_file "$work/release.json")" = "$stable_hash" ] || die "stable manifest checksum mismatch"
  fi
  json_fields "$work/release.json" release "$target" > "$work/release-fields" || die "release manifest refused"
  { IFS= read -r requested; IFS= read -r source; IFS= read -r archive_relative; IFS= read -r archive_hash; IFS= read -r archive_size; } < "$work/release-fields"
  [ "$requested" = "$version" ] || die "release version differs from requested version"
  [ "$archive_relative" = "standalone/$target/kaya-$version-$target.tar.gz" ] || die "archive path differs from the standalone contract"
  fetch "$KAYA_RELEASE_BASE/kaya/v$version/SHA256SUMS" "$work/SHA256SUMS"
  checksum_rows "$work/SHA256SUMS" > "$work/outer-rows" || die "publication checksum inventory refused"
  expected_hash="$(checksum_for "$work/outer-rows" release.json)" || die "release manifest checksum missing"
  [ "$(sha_file "$work/release.json")" = "$expected_hash" ] || die "release manifest checksum mismatch"
  expected_hash="$(checksum_for "$work/outer-rows" "$archive_relative")" || die "archive checksum missing"
  [ "$expected_hash" = "$archive_hash" ] || die "archive manifest and publication checksums differ"
  fetch "$KAYA_RELEASE_BASE/kaya/v$version/$archive_relative" "$work/archive.tar.gz" "$archive_size"
  actual_hash="$(sha_file "$work/archive.tar.gz")"; actual_size="$(wc -c < "$work/archive.tar.gz" | tr -d ' ')"
  [ "$actual_hash" = "$archive_hash" ] && [ "$actual_size" = "$archive_size" ] || die "standalone archive checksum or size mismatch"
  validate_archive
  awk '{path=$0; while (sub(/\/[^/]*$/,"",path)) print path}' "$work/archive-files" | sort -u > "$work/archive-directories"
  mkdir "$work/payload"
  tar -xzf "$work/archive.tar.gz" -C "$work/payload" --no-same-owner --no-same-permissions || die "standalone extraction failed"
  verify_payload "$work/payload"

  # Everything above is disposable staging. Refuse unfamiliar active layouts;
  # an existing flat install needs a stopped daemon and an explicit migration.
  [ ! -L "$KAYA_INSTALL_DIR/bin" ] && [ ! -L "$KAYA_INSTALL_DIR/versions" ] || die "installation directories must not be symlinks"
  for name in kaya kayad kaya-tool-server; do
    if [ -e "$KAYA_INSTALL_DIR/bin/$name" ] || [ -L "$KAYA_INSTALL_DIR/bin/$name" ]; then
      [ -L "$KAYA_INSTALL_DIR/bin/$name" ] && [ "$(readlink "$KAYA_INSTALL_DIR/bin/$name")" = "../current/bin/$name" ] || die "legacy flat install: stop kayad and choose a fresh KAYA_INSTALL_DIR; previous files are unchanged"
    fi
  done
  if [ -e "$KAYA_INSTALL_DIR/current" ] || [ -L "$KAYA_INSTALL_DIR/current" ]; then
    [ -L "$KAYA_INSTALL_DIR/current" ] || die "current cohort selector is not a symlink"
    existing="$(readlink "$KAYA_INSTALL_DIR/current")"
    [[ "$existing" =~ ^versions/[A-Za-z0-9.+_-]+$ ]] || die "current cohort selector is outside versions"
  fi
  mkdir -p "$KAYA_INSTALL_DIR"
  mkdir "$KAYA_INSTALL_DIR/.install-lock" 2>/dev/null || die "another installation is active or interrupted; inspect .install-lock before retrying"
  install_lock="$KAYA_INSTALL_DIR/.install-lock"
  mkdir -p "$KAYA_INSTALL_DIR/bin" "$KAYA_INSTALL_DIR/versions"
  cohort="$version-$archive_hash"
  if [ -e "$KAYA_INSTALL_DIR/versions/$cohort" ] || [ -L "$KAYA_INSTALL_DIR/versions/$cohort" ]; then
    pending="$KAYA_INSTALL_DIR/versions/$cohort"
    [ -d "$pending" ] && [ ! -L "$pending" ] || die "immutable installation cohort is not a directory"
    [ -z "$(find "$pending" ! -type d ! -type f -print)" ] || die "immutable installation cohort contains non-regular members"
    (cd "$pending" && find . -type f -print | sed 's@^\./@@' | sort) > "$work/existing-cohort-files"
    cmp -s "$work/archive-files" "$work/existing-cohort-files" || die "immutable installation cohort membership differs"
    (cd "$pending" && find . -type d -print | sed '/^\.$/d; s@^\./@@' | sort) > "$work/existing-cohort-directories"
    cmp -s "$work/archive-directories" "$work/existing-cohort-directories" || die "immutable installation cohort directories differ"
    while IFS= read -r name; do
      cmp -s "$pending/$name" "$work/payload/$name" || die "immutable installation cohort bytes differ"
      if [ "$os" = linux ]; then existing="$(stat -c '%a' "$pending/$name")"; else existing="$(stat -f '%Lp' "$pending/$name")"; fi
      case "$name" in bin/*) [ "$existing" = 755 ] ;; *) [ "$existing" = 644 ] ;; esac || die "immutable installation cohort mode differs"
    done < "$work/archive-files"
    while IFS= read -r name; do
      if [ "$os" = linux ]; then existing="$(stat -c '%a' "$pending/$name")"; else existing="$(stat -f '%Lp' "$pending/$name")"; fi
      [ "$existing" = 755 ] || die "immutable installation directory mode differs"
    done < "$work/archive-directories"
  else
    pending="$(mktemp -d "$KAYA_INSTALL_DIR/versions/.install.XXXXXXXX")"
    new_cohort="$pending"
    cp -R "$work/payload/." "$pending/"
    find "$pending" -type d -exec chmod 755 {} +
    find "$pending" -type f -exec chmod 644 {} +
    chmod 755 "$pending/bin/kaya" "$pending/bin/kayad" "$pending/bin/kaya-tool-server"
    mv "$pending" "$KAYA_INSTALL_DIR/versions/$cohort"
    new_cohort="$KAYA_INSTALL_DIR/versions/$cohort"
  fi
  for name in kaya kayad kaya-tool-server; do
    if [ ! -L "$KAYA_INSTALL_DIR/bin/$name" ]; then
      created_links="$created_links $name"
      ln -s "../current/bin/$name" "$KAYA_INSTALL_DIR/bin/$name"
    fi
  done
  pointer_stage="$(mktemp -d "$KAYA_INSTALL_DIR/.current.XXXXXXXX")"
  ln -s "versions/$cohort" "$pointer_stage/pointer"
  if [ "$os" = linux ]; then
    mv -Tf "$pointer_stage/pointer" "$KAYA_INSTALL_DIR/current"
  else
    mv -fh "$pointer_stage/pointer" "$KAYA_INSTALL_DIR/current"
  fi
  committed=1
  new_cohort=""
  info "installed Kaya $version ($target), source $source"
  ensure_path
}
main "$@"
