#!/usr/bin/env bash
# Safely unpack a verified jan scripts bundle.
set -euo pipefail

ZIP="${1:?usage: jan-install.sh <bundle.zip> [install-dir]}"
INSTALL_DIR="${2:-${JAN_INSTALL_DIR:-$HOME/.config/jan/scripts}}"

command -v python3 >/dev/null 2>&1 || {
  echo "error: python3 is required for safe bundle verification and extraction" >&2
  exit 1
}

python3 - "$ZIP" "$INSTALL_DIR" <<'PY'
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import shutil
import stat
import sys
import tempfile
import uuid
import zipfile

archive = Path(sys.argv[1]).expanduser()
install = Path(sys.argv[2]).expanduser().absolute()
if install == Path(install.anchor):
    raise SystemExit(f"error: refusing to install over filesystem root: {install}")

MAX_MANIFEST_SIZE = 1024 * 1024
MAX_MEMBER_SIZE = 128 * 1024 * 1024
MAX_TOTAL_SIZE = 512 * 1024 * 1024


def member_path(name: str) -> PurePosixPath:
    if not name or "\\" in name:
        raise ValueError(f"unsafe ZIP member path: {name!r}")
    path = PurePosixPath(name)
    if path.is_absolute() or any(part in ("", ".", "..") for part in path.parts):
        raise ValueError(f"unsafe ZIP member path: {name!r}")
    if str(path) != name.rstrip("/"):
        raise ValueError(f"non-canonical ZIP member path: {name!r}")
    return path


def remove_path(path: Path) -> None:
    if path.is_symlink() or path.is_file():
        path.unlink()
    elif path.exists():
        shutil.rmtree(path)


try:
    zf = zipfile.ZipFile(archive)
except (OSError, zipfile.BadZipFile) as error:
    raise SystemExit(f"error: cannot open bundle {archive}: {error}")

staging = None
backup = None
try:
    infos = zf.infolist()
    by_name = {}
    total_size = 0
    for info in infos:
        path = member_path(info.filename)
        canonical_name = str(path)
        if canonical_name in by_name:
            raise ValueError(f"duplicate ZIP member: {canonical_name}")

        mode = info.external_attr >> 16
        kind = stat.S_IFMT(mode)
        if kind not in (0, stat.S_IFREG, stat.S_IFDIR):
            raise ValueError(f"unsupported ZIP member type: {canonical_name}")
        if info.is_dir() != (kind == stat.S_IFDIR) and kind != 0:
            raise ValueError(f"inconsistent ZIP member type: {canonical_name}")
        if info.file_size > MAX_MEMBER_SIZE:
            raise ValueError(f"ZIP member too large: {canonical_name}")
        total_size += info.file_size
        if total_size > MAX_TOTAL_SIZE:
            raise ValueError("bundle exceeds extraction size limit")
        by_name[canonical_name] = info

    manifest_info = by_name.get("manifest.json")
    if manifest_info is None or manifest_info.is_dir():
        raise ValueError("bundle is missing root manifest.json")
    if manifest_info.file_size > MAX_MANIFEST_SIZE:
        raise ValueError("manifest.json is too large")
    try:
        manifest = json.loads(zf.read(manifest_info))
    except (UnicodeDecodeError, json.JSONDecodeError) as error:
        raise ValueError(f"invalid manifest.json: {error}") from error

    files = manifest.get("files")
    root_yaml = manifest.get("root_yaml")
    if not isinstance(files, dict) or not files:
        raise ValueError("manifest.json must contain a non-empty files object")
    if not isinstance(root_yaml, str) or root_yaml not in files:
        raise ValueError("manifest root_yaml must identify a listed file")

    listed = set()
    for name, expected in files.items():
        canonical_name = str(member_path(name))
        if canonical_name != name or not isinstance(expected, dict):
            raise ValueError(f"invalid manifest file entry: {name!r}")
        digest = expected.get("sha256")
        size = expected.get("size")
        if (
            not isinstance(digest, str)
            or len(digest) != 64
            or any(c not in "0123456789abcdefABCDEF" for c in digest)
            or not isinstance(size, int)
            or isinstance(size, bool)
            or size < 0
        ):
            raise ValueError(f"invalid manifest hash/size for {name}")
        info = by_name.get(name)
        if info is None or info.is_dir():
            raise ValueError(f"manifest file missing from ZIP: {name}")
        if info.file_size != size:
            raise ValueError(f"manifest size mismatch for {name}")
        listed.add(name)

    required_metadata = {"manifest.json", "env.sh"}
    missing_metadata = required_metadata - set(by_name)
    if missing_metadata:
        raise ValueError(
            "bundle missing required metadata: " + ", ".join(sorted(missing_metadata))
        )

    allowed_dirs = set()
    for name in listed | required_metadata:
        parent = PurePosixPath(name).parent
        while str(parent) != ".":
            allowed_dirs.add(str(parent))
            parent = parent.parent
    for name, info in by_name.items():
        if info.is_dir():
            if name not in allowed_dirs:
                raise ValueError(f"unlisted directory in bundle: {name}")
        elif name not in listed and name not in required_metadata:
            raise ValueError(f"unlisted file in bundle: {name}")

    install.parent.mkdir(parents=True, exist_ok=True)
    staging = Path(tempfile.mkdtemp(prefix=".jan-install-", dir=install.parent))

    for name, info in by_name.items():
        target = staging.joinpath(*PurePosixPath(name).parts)
        if info.is_dir():
            target.mkdir(parents=True, exist_ok=True)
            continue
        target.parent.mkdir(parents=True, exist_ok=True)
        digest = hashlib.sha256()
        size = 0
        with zf.open(info) as source, target.open("xb") as destination:
            while chunk := source.read(1024 * 1024):
                size += len(chunk)
                if size > MAX_MEMBER_SIZE:
                    raise ValueError(f"ZIP member expanded past limit: {name}")
                digest.update(chunk)
                destination.write(chunk)
        if name in files:
            expected = files[name]
            if size != expected["size"] or digest.hexdigest().lower() != expected["sha256"].lower():
                raise ValueError(f"manifest verification failed for {name}")

    if install.exists() or install.is_symlink():
        backup = install.parent / f".jan-backup-{uuid.uuid4().hex}"
        os.replace(install, backup)
    try:
        os.replace(staging, install)
        staging = None
    except BaseException:
        if backup is not None and not install.exists():
            os.replace(backup, install)
            backup = None
        raise
    if backup is not None:
        remove_path(backup)
        backup = None
except (OSError, ValueError, zipfile.BadZipFile) as error:
    raise SystemExit(f"error: bundle verification/extraction failed: {error}")
finally:
    zf.close()
    if staging is not None:
        remove_path(staging)
    if backup is not None:
        # Installation succeeded but cleanup failed; leave the backup rather than
        # deleting an unknown path during exception handling.
        print(f"warning: previous install retained at {backup}", file=sys.stderr)
PY

echo "Installed to $INSTALL_DIR"
echo "Run: jan use \"$INSTALL_DIR\""
echo "Then: jan --help"
