#!/usr/bin/env bash
# Screensie guided installer. Generated from InstallerScripts/linux-setup.py.
# Download this file, then run: bash screensie-install.sh
set -euo pipefail
set +x
diagnose() {
  echo 'Screensie Linux launch diagnostics (no passwords or account settings)'
  uname -srmo
  echo 'Launcher locations:'
  type -a screensie || true
  echo 'Installed Debian package:'
  dpkg-query -W -f='${Status} ${Version} ${Architecture}\n' screensie 2>/dev/null || true
  printf 'Desktop session: %s; Wayland display present: ' "${XDG_SESSION_TYPE:-unknown}"
  [[ -n "${WAYLAND_DISPLAY:-}" ]] && echo yes || echo no
  echo 'Screensie service state:'
  systemctl is-active screensie-agent.service screensietech.service 2>/dev/null || true
  echo 'Electron processes for the current user (PID and executable only):'
  local pid
  while read -r pid; do
    [[ -n "$pid" ]] || continue
    printf '%s ' "$pid"
    readlink "/proc/$pid/exe" || true
  done < <(pgrep -u "$(id -u)" -x electron || true)
  local runtime=/opt/screensie/Client/runtime/node_modules/electron/dist
  echo 'Installed client files:'
  ls -ld "$runtime/electron" "$runtime/chrome-sandbox" /opt/screensie/Client/resources/app/dist/index.html 2>/dev/null || true
  if [[ -x "$runtime/electron" ]]; then
    echo 'Missing runtime libraries (empty means none detected):'
    ldd "$runtime/electron" 2>/dev/null | grep 'not found' || true
  fi
  echo 'Next: run /usr/bin/screensie --enable-logging=stderr as your desktop user.'
}
main() {
  case "${1:-}" in --help|-h)
    printf '%s\n' 'Screensie Ubuntu/Debian setup: bash screensie-install.sh [--yes | --configure-only | --diagnose]' 'Advanced setup asks for optional account email, group, and a hidden static device password.'
    return ;;
    --diagnose) diagnose; return ;;
    ''|--yes|--configure-only) ;;
    *) echo 'Unknown setup option. Use --help.' >&2; return 1 ;;
  esac
  [[ "$(uname -s)" == Linux ]] || { echo 'This installer requires Ubuntu/Debian Linux.' >&2; return 1; }
  command -v apt-get >/dev/null && command -v dpkg >/dev/null || { echo 'Ubuntu/Debian with apt is required.' >&2; return 1; }
  [[ "$(dpkg --print-architecture)" == amd64 ]] || { echo 'This release requires Intel/AMD 64-bit Linux (amd64).' >&2; return 1; }
  local -a elevate=()
  if [[ "$(id -u)" != 0 ]]; then
    command -v sudo >/dev/null || { echo 'Administrator access through sudo is required.' >&2; return 1; }
    elevate=(sudo)
  fi
  if ! command -v python3 >/dev/null; then
    "${elevate[@]}" apt-get -o DPkg::Lock::Timeout=120 update
    "${elevate[@]}" apt-get -o DPkg::Lock::Timeout=120 install -y python3 ca-certificates
  fi
  local setup
  setup=$(cat <<'SCREENSIE_SETUP_PY'
"""Interactive Debian installer. Also embedded in the public Bash bootstrap.

Credentials are entered locally, never command-line arguments or diagnostic output.
Package installation remains owned by apt/dpkg so later upgrades keep working.
"""
import argparse
import base64
import getpass
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import stat
import subprocess
import sys
import tempfile
import urllib.request
import urllib.parse
import warnings

ORIGIN = "https://screensie.com"
ENV_FILE = Path("/etc/screensie/screensie-agent.env")
MAX_PACKAGE = 1024 * 1024 * 1024


class SetupError(ValueError):
    """A fixed, credential-free message suitable for displaying to the user."""


def run(*args):
    subprocess.run(args, check=True)


class HTTPSOnly(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        # A redirected installer must remain on the release server over TLS.
        if urllib.parse.urlsplit(newurl).scheme != "https" or urllib.parse.urlsplit(newurl).netloc != "screensie.com":
            raise SetupError("Download redirected outside the Screensie release server")
        return super().redirect_request(req, fp, code, msg, headers, newurl)


def download(url, target=None, limit=2 * 1024 * 1024):
    opener = urllib.request.build_opener(HTTPSOnly())
    with opener.open(url, timeout=60) as response:
        total, chunks = 0, []
        output = target.open("wb") if target else None
        try:
            while True:
                chunk = response.read(1024 * 1024)
                if not chunk:
                    break
                total += len(chunk)
                if total > limit:
                    raise SetupError("Download exceeds its published size limit")
                if output:
                    output.write(chunk)
                else:
                    chunks.append(chunk)
        finally:
            if output:
                output.close()
    return b"".join(chunks) if not target else total


def package_entry(manifest):
    entries = [entry for key in ("downloads", "aliases")
               for entry in manifest.get(key, [])
               if isinstance(entry, dict) and entry.get("file") == "public/downloads/linux/screensielinuxsetup.deb"]
    if not entries or any(entry != entries[0] for entry in entries):
        raise SetupError("The server has not published a matching Debian installer")
    entry = entries[0]
    if not re.fullmatch(r"[a-fA-F0-9]{64}", str(entry.get("sha256", ""))) or type(entry.get("bytes")) is not int or not 65536 < entry["bytes"] <= MAX_PACKAGE:
        raise SetupError("Invalid Debian release metadata")
    return entry


def verify_package(file, entry):
    digest = hashlib.sha256()
    with file.open("rb") as stream:
        if stream.read(8) != b"!<arch>\n":
            raise SetupError("Downloaded file is not a Debian package")
        stream.seek(0)
        while chunk := stream.read(1024 * 1024):
            digest.update(chunk)
    if file.stat().st_size != entry["bytes"] or digest.hexdigest() != entry["sha256"].lower():
        raise SetupError("Installer integrity check failed; no package was installed. Retry to fetch the current release")


def valid_text(value, maximum):
    return len(value) <= maximum and not any(ord(c) < 32 or 127 <= ord(c) <= 159 for c in value)


def valid_password(value):
    return valid_text(value, 256) and len(value) >= 8 and bool(re.search(r"[A-Z]", value)) and bool(re.search(r"[0-9]", value)) and bool(re.search(r"[^A-Za-z0-9]", value))


def collect_settings(ask, secret):
    values = {}
    while True:
        answer = ask("Advanced installation options? [y/N]: ").strip().lower()
        if answer in ("", "n", "no"):
            return values
        if answer in ("y", "yes"):
            break
        print("Enter y or n.")
    print("Optional fields: leave blank to keep existing settings, or use defaults on a new installation.")
    while True:
        email = ask("Screensie account email (optional): ").strip()
        if not email or (valid_text(email, 254) and re.fullmatch(r"[^\s@]+@[^\s@]+\.[^\s@]+", email)):
            break
        print("Enter a valid account email, or leave it blank.")
    if email:
        values["SCREENSIE_DEPLOYMENT_ACCOUNT_B64"] = email
        while True:
            group = ask("Group to add this computer to (optional): ").strip()
            if valid_text(group, 128):
                break
            print("Use a group name of at most 128 characters without control characters.")
        if group:
            values["SCREENSIE_DEPLOYMENT_GROUP_B64"] = group
    print("The static device password is for remote access, not your Screensie account password.")
    while True:
        password = secret("Static device password (blank to keep existing/default): ")
        if not password:
            break
        if not valid_password(password):
            print("Use 8–256 characters, including an uppercase letter, a number and a special character.")
            continue
        if password != secret("Confirm static device password: "):
            print("Passwords did not match. Try again.")
            continue
        values["SCREENSIE_DEVICE_PASSWORD_B64"] = password
        break
    return values


def merge_settings(original, values):
    # Never source/evaluate the environment file. Preserve all untargeted settings.
    lines = [line for line in original.splitlines() if line.partition("=")[0] not in values]
    lines += [key + "=" + base64.b64encode(value.encode("utf-8")).decode("ascii") for key, value in values.items()]
    return "\n".join(lines) + "\n"


def save_settings(values, file=ENV_FILE):
    if not values:
        return False
    # Root-owned ancestors prevent replacement of the temporary file or target.
    for ancestor in [*reversed(file.parent.parents), file.parent]:
        info = ancestor.lstat()
        if not stat.S_ISDIR(info.st_mode) or info.st_uid != 0 or info.st_mode & 0o022:
            raise SetupError("Configuration directory must be a root-owned, non-writable directory")
    info = file.lstat()
    if not stat.S_ISREG(info.st_mode) or info.st_uid != 0 or info.st_mode & 0o022:
        raise SetupError("Configuration must be a root-owned regular file")
    original = file.read_text(encoding="utf-8")
    replacement = merge_settings(original, values)
    if replacement == original:
        return False
    fd, temporary = tempfile.mkstemp(prefix=".setup-", dir=file.parent)
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as output:
            output.write(replacement)
            output.flush()
            os.fsync(output.fileno())
        os.replace(temporary, file)
    finally:
        if os.path.exists(temporary):
            os.unlink(temporary)
    return True


def check_platform():
    if sys.platform != "linux" or not shutil.which("apt-get") or not shutil.which("dpkg"):
        raise SetupError("Guided installation supports Ubuntu/Debian with apt and systemd")
    if subprocess.check_output(["dpkg", "--print-architecture"], text=True).strip() != "amd64":
        raise SetupError("This release supports Intel/AMD 64-bit Linux (amd64), not ARM")
    if not Path("/run/systemd/system").is_dir():
        raise SetupError("Install on a normally booted systemd Linux PC, not a container or chroot")


def main():
    parser = argparse.ArgumentParser(description="Screensie guided Ubuntu/Debian installation")
    parser.add_argument("--yes", action="store_true", help="Install without prompts; preserve existing enrollment and password")
    parser.add_argument("--configure-only", action="store_true", help="Configure the installed client without downloading or reinstalling it")
    args = parser.parse_args()
    check_platform()
    if os.geteuid() != 0:
        raise SetupError("The Bash bootstrap must obtain administrator permission first")
    values = {}
    tty = None
    tty_output = None
    try:
        if args.configure_only and not ENV_FILE.is_file():
            raise SetupError("Screensie is not installed yet. Run setup without --configure-only first")
        if not args.yes:
            # A terminal is non-seekable; r+ would request a BufferedRandom
            # stream and can fail before the first setup prompt on Linux.
            tty = open("/dev/tty", "r")
            tty_output = open("/dev/tty", "w")
            def ask(prompt):
                tty_output.write(prompt)
                tty_output.flush()
                line = tty.readline()
                if not line:
                    raise KeyboardInterrupt
                return line.rstrip("\n")
            def secret(prompt):
                # Refuse an echoing fallback rather than print private credentials.
                with warnings.catch_warnings():
                    warnings.simplefilter("error", getpass.GetPassWarning)
                    return getpass.getpass(prompt, stream=tty_output)
            print("Screensie setup — Ubuntu / Debian (64-bit)")
            values = collect_settings(ask, secret)
            prompt = "Apply these settings to the installed computer? [Y/n]: " if args.configure_only else "Install Screensie and required dependencies now? [Y/n]: "
            if ask(prompt).strip().lower() not in ("", "y", "yes"):
                print("Installation cancelled. No changes made.")
                return
        if args.configure_only:
            if save_settings(values):
                run("systemctl", "restart", "screensie-agent.service")
                print("Settings saved. The agent will request account/group enrollment from the server.")
            else:
                print("No settings changed.")
            return
        print("Checking the current Screensie release...")
        entry = package_entry(json.loads(download(ORIGIN + "/downloads/index.json")))
        with tempfile.TemporaryDirectory(prefix="screensie-setup-") as directory:
            package = Path(directory) / "screensie.deb"
            print("Downloading the installer...")
            download(ORIGIN + "/downloads/linux/screensielinuxsetup.deb", package, entry["bytes"])
            verify_package(package, entry)
            print("Installer verified. Installing system dependencies and Screensie...")
            run("apt-get", "-o", "DPkg::Lock::Timeout=120", "update")
            run("apt-get", "-o", "DPkg::Lock::Timeout=120", "install", "-y", "--reinstall", str(package))
        if save_settings(values):
            run("systemctl", "restart", "screensie-agent.service")
        print("Screensie is installed. Open Screensie from your application menu as your normal desktop user.")
        if values.get("SCREENSIE_DEPLOYMENT_ACCOUNT_B64"):
            print("Account/group settings saved. Enrollment is completed by the agent when the server accepts it.")
        print("On Wayland, approve the desktop sharing/input prompt when requested.")
        print("If no window appears, run bash screensie-install.sh --diagnose and share its output with support.")
    finally:
        values.clear()
        if tty:
            tty.close()
        if tty_output:
            tty_output.close()


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\nSetup cancelled.", file=sys.stderr)
        sys.exit(130)
    except Exception as error:
        # Do not serialize exception arguments: they can contain private input.
        if isinstance(error, SetupError):
            print("Screensie setup: " + str(error), file=sys.stderr)
        else:
            print("Screensie setup did not finish (" + type(error).__name__ + "). Review the package-manager output, check your connection, and retry. Existing identity/settings are preserved.", file=sys.stderr)
        sys.exit(1)

SCREENSIE_SETUP_PY
)
  "${elevate[@]}" python3 -I -c "$setup" "$@"
}
main "$@"
