#!/usr/bin/env bash
# manor-edge-install.sh — cross-platform (Linux + macOS terminal) MANOR edge
# installer. The Linux counterpart to ManorEdgeInstaller.command (the
# double-clickable macOS app): same key-then-handoff flow, but runs from a
# terminal on either OS. Downloaded from https://downloads.mymanor.click/
# manor-edge-install.sh (CDN) or https://mymanor.click/manor-edge-install.sh
# (committed fallback).
#
# Prompts for the single-use pairing key from your key link, then hands off to
# the server-generated install script
# (a TLS-restricted root download into a root-owned temporary directory) — all the heavy
# lifting (Node install, setup-linux-edge.sh / macOS setup, services, pairing)
# lives server-side, not here. Keep this file SMALL.
#
# No key yet? Press Return with no key to submit an access request instead
# (POST /public/registrations) — the same flow as https://mymanor.click/register.
#
# Meant to be DOWNLOADED then run (`bash manor-edge-install.sh`), never curl-piped.
# Pass a validated 32-hex key as argv for unattended use; otherwise it prompts on stdin. The
# launcher sends the fixed HTTPS URL over stdin to a clean root shim; curl reads
# it through a one-shot fd rather than argv or environment. The root shim then
# size-bounds, syntax-checks, executes, and removes the served script.
#
# ⚠ MUST STAY IN SYNC with ManorEdgeInstaller.command (macOS) in this same
# directory — the ADMIN_API, the request_access() body, the key validation, and
# the MANOR_SELF_SERVE=1 handoff are a shared contract. Change both together.

set -euo pipefail
PATH=/usr/bin:/bin:/usr/sbin:/sbin
export PATH

ADMIN_API="https://0zdeqnboxi.execute-api.us-east-1.amazonaws.com/prod"

request_access() {
  echo ""
  echo "Request MANOR access — every request is reviewed by a human, and"
  echo "approval emails you a single-use key link."
  echo ""
  printf "Your name: "
  read -r NAME
  printf "Your email: "
  read -r EMAIL
  # Public self-registration enters the standard MANOR Home service.
  TIER="shared"
  printf "City / street address: "
  read -r ADDRESS
  if [ -z "$NAME" ] || [ -z "$EMAIL" ] || [ -z "$ADDRESS" ]; then
    echo "ERROR: name, email, and address are required." >&2
    exit 2
  fi

  # Crude JSON hardening: drop quotes/backslashes rather than escape them.
  NAME="$(printf '%s' "$NAME" | tr -d '"\\')"
  EMAIL="$(printf '%s' "$EMAIL" | tr -d '"\\')"
  ADDRESS="$(printf '%s' "$ADDRESS" | tr -d '"\\')"

  BODY="{\"name\":\"$NAME\",\"email\":\"$EMAIL\",\"tier\":\"$TIER\",\"source\":\"installer\""
  if [ -n "$ADDRESS" ]; then BODY="$BODY,\"address\":\"$ADDRESS\""; fi
  BODY="$BODY}"

  TMP_RESP="$(mktemp "${TMPDIR:-/tmp}/manor-reg.XXXXXX")"
  HTTP_CODE="$(/usr/bin/curl --disable --proto '=https' --proto-redir '=https' --tlsv1.2 \
    --silent --show-error -o "$TMP_RESP" -w '%{http_code}' -X POST "$ADMIN_API/public/registrations" \
    -H 'Content-Type: application/json' --data "$BODY")" || {
    rm -f "$TMP_RESP"
    echo "ERROR: request failed — check your connection and try again." >&2
    exit 1
  }
  RESP="$(cat "$TMP_RESP")"; rm -f "$TMP_RESP"

  echo ""
  # Only a 2xx means the request was actually recorded. A throttle (429) or any
  # error must NOT be reported as success — otherwise the user believes they
  # registered when nothing was stored.
  case "$HTTP_CODE" in
    2*)
      if printf '%s' "$RESP" | grep -q '"duplicate"'; then
        echo "You already have a request in review — approval lands by email."
      else
        echo "Request received. Watch $EMAIL for your approval and key link."
      fi
      echo "Track status any time at https://mymanor.click/register"
      ;;
    429)
      echo "ERROR: the server is busy right now — wait a minute and run this again." >&2
      exit 1
      ;;
    *)
      echo "ERROR: the request did not go through (HTTP $HTTP_CODE)." >&2
      echo "Try again, or register at https://mymanor.click/register" >&2
      exit 1
      ;;
  esac
}

echo ""
echo "==================================================="
echo "  MANOR edge installer"
echo "==================================================="
echo ""

OS="$(/usr/bin/uname -s)"
case "$OS" in
  Linux|Darwin) ;;
  *)
    echo "ERROR: unsupported operating system '$OS'." >&2
    echo "MANOR edge runs on macOS (13+) or Linux (Debian, Ubuntu, or" >&2
    echo "Raspberry Pi OS with systemd)." >&2
    exit 2
    ;;
esac

if [ ! -x /usr/bin/curl ]; then
  if [ "$OS" = "Linux" ]; then
    echo "ERROR: curl not found — install it with 'sudo apt-get install -y curl' and retry." >&2
  else
    echo "ERROR: curl not found — install the Xcode command line tools and retry." >&2
  fi
  exit 2
fi

KEY="${1:-}"
if [ -z "$KEY" ]; then
  echo "Paste your MANOR pairing key (from your key link)."
  echo "No key yet? Just press Return to request access instead."
  printf "> "
  read -r KEY || KEY=""
fi
# Strip whitespace + lowercase so a padded copy-paste still validates.
KEY="$(printf '%s' "$KEY" | tr -d '[:space:]' | tr 'A-F' 'a-f')"

if [ -z "$KEY" ]; then
  request_access
  exit 0
fi

if ! printf '%s' "$KEY" | grep -Eq '^[a-f0-9]{32}$'; then
  echo "ERROR: that doesn't look like a MANOR pairing key (expected 32 hex characters)." >&2
  echo "Get your key from https://mymanor.click/account or open the setup link in your MANOR email." >&2
  exit 2
fi

echo ""
echo "Installing the MANOR edge — you'll be asked for your login password (sudo)."
echo "Keep this window open until it finishes."
echo ""

# Escalate directly into an isolated fixed Python shim. Only sudo's authenticated
# SUDO_USER/SUDO_UID/SUDO_GID tuple can select the service account; the caller
# cannot pass an ownership variable into the root installer.
MANOR_INSTALL_URL="$ADMIN_API/pair/install/$KEY"
ROOT_INSTALLER='set -euo pipefail
umask 077
install_dir=$(/usr/bin/mktemp -d /var/tmp/.manor-edge-install.XXXXXX)
script="$install_dir/install.sh"
cleanup() { /bin/rm -rf -- "$install_dir"; }
trap cleanup EXIT
trap '\''exit 130'\'' HUP INT TERM
/usr/bin/curl --disable --proto '\''=https'\'' --proto-redir '\''=https'\'' --tlsv1.2 --fail --silent --show-error --location --max-filesize 2097152 --config /dev/fd/3 --output "$script"
exec 3<&-
script_size=$(/usr/bin/wc -c < "$script" | /usr/bin/tr -d '\''[:space:]'\'')
if [ -z "$script_size" ] || [ "$script_size" -le 0 ] || [ "$script_size" -gt 2097152 ]; then
  echo "ERROR: served installer size is invalid." >&2
  exit 1
fi
/bin/chmod 0400 "$script"
/bin/bash --noprofile --norc -n "$script"
/bin/bash --noprofile --norc "$script"'

ROOT_IDENTITY_SHIM='import os
import json
import plistlib
import pwd
import re
import stat
import subprocess
import sys
from urllib.parse import urlsplit, urlunsplit

def fail(message):
    raise SystemExit("MANOR installer boundary: " + message)

if os.geteuid() != 0:
    fail("root is required")
name = os.environ.get("SUDO_USER", "")
uid_text = os.environ.get("SUDO_UID", "")
gid_text = os.environ.get("SUDO_GID", "")
if (not re.fullmatch(r"[A-Za-z_][A-Za-z0-9._-]{0,63}", name)
        or name in {"root", "daemon", "nobody"}
        or not re.fullmatch(r"[1-9][0-9]*", uid_text)
        or not re.fullmatch(r"[1-9][0-9]*", gid_text)):
    fail("sudo did not authenticate a non-root local account")
try:
    account = pwd.getpwnam(name)
except KeyError:
    fail("authenticated account does not exist")
uid = int(uid_text)
gid = int(gid_text)
if account.pw_uid != uid or account.pw_gid != gid:
    fail("authenticated account identity changed")
home = account.pw_dir
if (not home.startswith("/") or home == "/" or os.path.normpath(home) != home
        or any(ord(ch) < 32 for ch in home)):
    fail("authenticated account home is invalid")

if sys.platform.startswith("linux"):
    flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
    fd = os.open("/etc/passwd", flags)
    try:
        info = os.fstat(fd)
        if (not stat.S_ISREG(info.st_mode) or info.st_uid != 0
                or stat.S_IMODE(info.st_mode) & 0o022 or info.st_size > 1024 * 1024):
            fail("local passwd authority is unsafe")
        data = os.read(fd, info.st_size + 1).decode("utf-8")
    finally:
        os.close(fd)
    matches = [line.split(":") for line in data.splitlines() if line.split(":", 1)[0] == name]
    if (len(matches) != 1 or len(matches[0]) != 7 or matches[0][2] != uid_text
            or matches[0][3] != gid_text or matches[0][5] != home):
        fail("authenticated account is not uniquely local")
    root_home = "/root"
elif sys.platform == "darwin":
    result = subprocess.run(
        ["/usr/bin/dscl", "-plist", ".", "-read", "/Users/" + name],
        check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={})
    local = plistlib.loads(result.stdout)
    def values(key):
        value = local.get(key, [])
        if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
            fail("invalid local account record")
        return value
    if (values("dsAttrTypeStandard:AppleMetaNodeLocation") != ["/Local/Default"]
            or name not in values("dsAttrTypeStandard:RecordName")
            or values("dsAttrTypeStandard:UniqueID") != [uid_text]
            or values("dsAttrTypeStandard:PrimaryGroupID") != [gid_text]
            or values("dsAttrTypeStandard:NFSHomeDirectory") != [home]):
        fail("authenticated account is not uniquely local")
    root_home = "/var/root"
else:
    fail("unsupported platform")

home_info = os.lstat(home)
if (not stat.S_ISDIR(home_info.st_mode) or home_info.st_uid != uid
        or stat.S_IMODE(home_info.st_mode) & 0o022):
    fail("authenticated account home is unsafe")
raw_url = sys.stdin.buffer.read(4098)
if (len(raw_url) < 2 or len(raw_url) > 4097
        or not raw_url.endswith(b"\n") or raw_url.count(b"\n") != 1):
    fail("installer URL input is invalid")
try:
    url = raw_url[:-1].decode("utf-8")
except UnicodeDecodeError:
    fail("installer URL input is invalid")
parts = urlsplit(url)
if (parts.scheme != "https" or not parts.hostname or parts.username or parts.password
        or parts.fragment or urlunsplit(parts) != url or any(ord(ch) < 32 for ch in url)):
    fail("installer URL is not exact credential-free HTTPS")
curl_config = ("url = " + json.dumps(url, ensure_ascii=True) + "\n").encode("utf-8")
read_fd, write_fd = os.pipe()
try:
    offset = 0
    while offset < len(curl_config):
        written = os.write(write_fd, curl_config[offset:])
        if written < 1:
            fail("installer URL pipe made no progress")
        offset += written
finally:
    os.close(write_fd)
if read_fd != 3:
    os.dup2(read_fd, 3)
    os.close(read_fd)
os.set_inheritable(3, True)
os.execve("/usr/bin/env", [
    "env", "-i", "HOME=" + root_home, "PATH=/usr/bin:/bin:/usr/sbin:/sbin",
    "MANOR_SELF_SERVE=1", "MANOR_AUTHENTICATED_USER=" + name,
    "/bin/bash", "--noprofile", "--norc", "-c", sys.argv[1],
], {})'

printf '%s\n' "$MANOR_INSTALL_URL" | \
  /usr/bin/sudo /usr/bin/python3 -I -c "$ROOT_IDENTITY_SHIM" "$ROOT_INSTALLER"
