#!/bin/bash
set -euo pipefail
umask 077
TEMP=$(mktemp "${TMPDIR:-/tmp}/mbsrs-install.XXXXXXXX.py")
trap 'rm -f -- "$TEMP"' EXIT
cat > "$TEMP" <<'MBSRS_BOOTSTRAP_PY'
"""Signed release transport and atomic activation; Python stdlib + OpenSSL."""

import base64, fcntl, hashlib, json, os, pathlib, re, shutil, stat, subprocess, tempfile, time
import urllib.parse, urllib.request, zipfile
VERSION = '1.0.2'
DATA_FORMAT = 5
RELEASE_SEQUENCE = 3
RELEASE_URL = 'https://mbsrs.mailurity.fmopz.dev'

MAX_PACKAGE = 16 * 1024 * 1024
MAX_EXPANDED = 32 * 1024 * 1024


class UpdateError(Exception):
    pass


def require(ok, message):
    if not ok:
        raise UpdateError(message)


def https_url(url):
    p = urllib.parse.urlsplit(url)
    require(
        p.scheme == "https"
        and p.hostname
        and not p.username
        and not p.password
        and not p.fragment,
        "Release URL must use HTTPS without credentials or fragment",
    )
    return url


class HTTPSRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        https_url(newurl)
        return super().redirect_request(req, fp, code, msg, headers, newurl)


def download(url, limit, timeout=15):
    https_url(url)
    with urllib.request.build_opener(HTTPSRedirect()).open(
        url, timeout=timeout
    ) as response:
        data = response.read(limit + 1)
    require(len(data) <= limit, "Release download exceeds size limit")
    return data


def atomic_write(path, data, mode=0o600):
    path = pathlib.Path(path)
    path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
    fd, temp = tempfile.mkstemp(prefix=".new-", dir=str(path.parent))
    try:
        with os.fdopen(fd, "wb") as f:
            os.fchmod(f.fileno(), mode)
            f.write(data)
            f.flush()
            os.fsync(f.fileno())
        os.replace(temp, str(path))
        directory = os.open(str(path.parent), os.O_RDONLY | os.O_DIRECTORY)
        try:
            os.fsync(directory)
        finally:
            os.close(directory)
    finally:
        if os.path.exists(temp):
            os.unlink(temp)


def verify_manifest(data, signature, key):
    require(shutil.which("openssl"), "OpenSSL is required to verify release signatures")
    with tempfile.TemporaryDirectory(prefix="mbsrs-signature-") as tmp:
        root = pathlib.Path(tmp)
        (root / "manifest").write_bytes(data)
        (root / "sig").write_bytes(signature)
        result = subprocess.run(
            [
                "openssl",
                "dgst",
                "-sha256",
                "-verify",
                str(key),
                "-signature",
                str(root / "sig"),
                str(root / "manifest"),
            ],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            timeout=15,
        )
        require(result.returncode == 0, "Invalid release signature")
    obj = json.loads(data.decode("utf-8"))
    require(isinstance(obj, dict), "Invalid release manifest")
    require(
        re.fullmatch(r"\d+\.\d+\.\d+", obj.get("version", "")),
        "Invalid release version",
    )
    require(
        type(obj.get("sequence")) is int and obj["sequence"] > 0,
        "Invalid release sequence",
    )
    require(obj.get("data_format") == DATA_FORMAT, "Unsupported archive format")
    require(
        re.fullmatch(r"[0-9a-f]{64}", obj.get("sha256", "")), "Invalid package digest"
    )
    require(
        type(obj.get("size")) is int and 0 < obj["size"] <= MAX_PACKAGE,
        "Invalid package size",
    )
    require(isinstance(obj.get("archive_url"), str), "Missing release archive URL")
    https_url(obj["archive_url"])
    if obj.get("release_url"):
        https_url(obj["release_url"])
    return obj


def endpoint(root):
    path = root / "state/release-source.json"
    if path.exists():
        return https_url(json.loads(path.read_text())["url"]).rstrip("/")
    return RELEASE_URL


def fetch_manifest(root):
    url = endpoint(root)
    data = download(url + "/releases/latest.json", 65536)
    signature = download(url + "/releases/latest.json.sig", 4096)
    return (
        verify_manifest(data, signature, root / "release-public.pem"),
        data,
        signature,
    )


def unpack(data, folder, version):
    import io

    total = 0
    seen = set()
    prefix = "mbsrs-" + version + "/"
    with zipfile.ZipFile(io.BytesIO(data)) as archive:
        for entry in archive.infolist():
            require(entry.filename.startswith(prefix), "Unexpected package root")
            rel = entry.filename[len(prefix) :]
            if not rel and entry.is_dir():
                continue
            parts = rel.rstrip("/").split("/")
            require(
                rel
                and all(p not in ("", ".", "..") for p in parts)
                and "\\" not in rel
                and not any(ord(c) < 32 for c in rel),
                "Unsafe package path",
            )
            require(rel not in seen, "Duplicate package path")
            seen.add(rel)
            mode = entry.external_attr >> 16
            require(
                not stat.S_ISLNK(mode)
                and (stat.S_IFMT(mode) in (0, stat.S_IFREG, stat.S_IFDIR)),
                "Package links/special files are forbidden",
            )
            total += entry.file_size
            require(total <= MAX_EXPANDED, "Expanded package exceeds size limit")
            target = folder.joinpath(*parts)
            if entry.is_dir():
                target.mkdir(parents=True, exist_ok=True, mode=0o700)
                continue
            require(
                not rel.endswith(
                    ("mbsrs.conf", "mbsrs-mailboxes.txt", "sftp-password", "sftp-key")
                ),
                "Operational data is forbidden in releases",
            )
            target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
            with open(str(target), "xb") as f:
                f.write(archive.read(entry))
            target.chmod(0o700 if rel.endswith(".sh") else 0o600)
    for name in (
        "mbsrs_cli.py",
        "mbsrs_core.py",
        "mbsrs_version.py",
        "mbsrs-python.sh",
        "mbsrs-askpass.sh",
    ):
        require((folder / name).is_file(), "Incomplete release: " + name)


def set_current(root, release):
    require(
        release.parent == root / "releases"
        and release.is_dir()
        and not release.is_symlink(),
        "Invalid activation target",
    )
    temp = root / (".current-" + str(os.getpid()))
    if temp.exists() or temp.is_symlink():
        temp.unlink()
    temp.symlink_to(pathlib.Path("releases") / release.name)
    os.replace(str(temp), str(root / "current"))
    fd = os.open(str(root), os.O_RDONLY | os.O_DIRECTORY)
    try:
        os.fsync(fd)
    finally:
        os.close(fd)


def wait_export(root, timeout=120):
    # This is intentionally shared with every legacy 5.x export in this account.
    path = pathlib.Path.home() / ".local/state/mail-export/export.lock"
    path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
    f = open(str(path), "a")
    deadline = time.monotonic() + timeout
    while True:
        try:
            fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
            return f
        except BlockingIOError:
            if time.monotonic() >= deadline:
                f.close()
                raise UpdateError(
                    "An export is still active; retry update after it finishes"
                )
            time.sleep(0.25)


def check(root, force=False):
    stamp = root / "state/update-check.json"
    if not force and stamp.exists():
        try:
            if time.time() - json.loads(stamp.read_text())["checked"] < 86400:
                return
        except (ValueError, KeyError):
            pass
    try:
        obj, _, _ = fetch_manifest(root)
        if obj["sequence"] > RELEASE_SEQUENCE:
            print(
                "[UPDATE] MBSRS {} is available; run: {} update".format(
                    obj["version"], root / "mbsrs"
                )
            )
        elif force:
            print("MBSRS " + VERSION + " is current.")
    except Exception as e:
        if force:
            raise
        print("[WARN] Release check unavailable: " + str(e))
    finally:
        atomic_write(stamp, json.dumps({"checked": time.time()}).encode())


def install_update(root):
    obj, data, sig = fetch_manifest(root)
    receipt = root / "state/active-release.json"
    if receipt.exists():
        saved = json.loads(receipt.read_text())
        highest = verify_manifest(
            base64.b64decode(saved["manifest"]),
            base64.b64decode(saved["signature"]),
            root / "release-public.pem",
        )["sequence"]
        require(
            obj["sequence"] >= highest,
            "Release sequence is older than a previously installed signed release",
        )
    require(obj["sequence"] >= RELEASE_SEQUENCE, "Release sequence would go backwards")
    if obj["sequence"] == RELEASE_SEQUENCE:
        apply_release_source(root, data, sig)
        require(obj["version"] == VERSION, "Conflicting release sequence")
        print("MBSRS " + VERSION + " is current.")
        return
    payload = download(obj["archive_url"], MAX_PACKAGE, timeout=60)
    require(
        len(payload) == obj["size"]
        and hashlib.sha256(payload).hexdigest() == obj["sha256"],
        "Release package integrity failure",
    )
    lock = wait_export(root)
    try:
        release = root / "releases" / ("mbsrs-" + obj["version"])
        require(not release.is_symlink(), "Release folder cannot be a symlink")
        stage = pathlib.Path(
            tempfile.mkdtemp(prefix=".staging-", dir=str(root / "releases"))
        )
        old = (root / "current").resolve()
        try:
            unpack(payload, stage, obj["version"])
            env = os.environ.copy()
            env["MBSRS_HOME"] = str(root)
            result = subprocess.run(
                [
                    "/bin/bash",
                    str(stage / "mbsrs-python.sh"),
                    str(stage / "mbsrs_cli.py"),
                    "validate-release",
                    obj["version"],
                    str(obj["sequence"]),
                ],
                env=env,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                timeout=60,
            )
            require(
                result.returncode == 0,
                "New release validation failed: "
                + result.stderr.decode("utf-8", "replace"),
            )
            if release.exists():
                expected = {
                    str(p.relative_to(stage)): p.read_bytes()
                    for p in stage.rglob("*")
                    if p.is_file()
                }
                actual = {
                    str(p.relative_to(release)): p.read_bytes()
                    for p in release.rglob("*")
                    if p.is_file() and not p.is_symlink()
                }
                require(
                    expected == actual
                    and not any(p.is_symlink() for p in release.rglob("*")),
                    "Existing release differs from the verified package",
                )
            else:
                stage.rename(release)
            journal = {
                "previous": old.name,
                "current": release.name,
                "manifest": base64.b64encode(data).decode(),
                "signature": base64.b64encode(sig).decode(),
            }
            atomic_write(
                root / "state/update-transaction.json", json.dumps(journal).encode()
            )
            set_current(root, release)
            # Data/config migrations happen on first command in the newly activated release.
            atomic_write(
                root / "state/previous-release.json",
                json.dumps({"release": old.name}).encode(),
            )
            apply_release_source(root, data, sig)
            (root / "state/update-transaction.json").unlink()
            print(
                "Updated to MBSRS "
                + obj["version"]
                + ". Previous code retained; use mbsrs rollback if needed."
            )
        except BaseException:
            if (root / "current").resolve() != old:
                set_current(root, old)
            raise
        finally:
            if stage.exists():
                shutil.rmtree(str(stage))
    finally:
        lock.close()


def apply_release_source(root, data, sig):
    obj = verify_manifest(data, sig, root / "release-public.pem")
    atomic_write(
        root / "state/active-release.json",
        json.dumps(
            {
                "manifest": base64.b64encode(data).decode(),
                "signature": base64.b64encode(sig).decode(),
            }
        ).encode(),
    )
    if obj.get("release_url"):
        atomic_write(
            root / "state/release-source.json",
            json.dumps({"url": obj["release_url"]}).encode(),
        )


def reconcile_release_source(root):
    receipt = root / "state/active-release.json"
    if not receipt.exists():
        return
    saved = json.loads(receipt.read_text())
    data = base64.b64decode(saved["manifest"])
    sig = base64.b64decode(saved["signature"])
    obj = verify_manifest(data, sig, root / "release-public.pem")
    if obj.get("release_url"):
        path = root / "state/release-source.json"
        try:
            current = json.loads(path.read_text())["url"]
        except (OSError, ValueError, KeyError):
            current = None
        if current != obj["release_url"]:
            atomic_write(path, json.dumps({"url": obj["release_url"]}).encode())
            print("[MIGRATION] Release service URL reconciled from signed metadata.")


def recover_update(root):
    path = root / "state/update-transaction.json"
    if not path.exists():
        return
    txn = json.loads(path.read_text())
    current = (root / "current").resolve()
    require(
        txn["previous"].startswith("mbsrs-") and "/" not in txn["previous"],
        "Invalid previous release",
    )
    require(
        txn["current"].startswith("mbsrs-") and "/" not in txn["current"],
        "Invalid current release",
    )
    if current.name == txn["current"]:
        atomic_write(
            root / "state/previous-release.json",
            json.dumps({"release": txn["previous"]}).encode(),
        )
        apply_release_source(
            root, base64.b64decode(txn["manifest"]), base64.b64decode(txn["signature"])
        )
    else:
        require(current.name == txn["previous"], "Ambiguous interrupted update")
    path.unlink()
    print("[RECOVERY] Interrupted update reconciled.")


def rollback(root):
    path = root / "state/previous-release.json"
    require(path.exists(), "No previous release recorded")
    name = json.loads(path.read_text())["release"]
    require(re.fullmatch(r"mbsrs-\d+\.\d+\.\d+", name), "Invalid rollback release")
    lock = wait_export(root)
    try:
        old = (root / "current").resolve()
        previous = root / "releases" / name
        if (root / "config/mbsrs.conf").exists():
            env = os.environ.copy()
            env["MBSRS_HOME"] = str(root)
            result = subprocess.run(
                [
                    "/bin/bash",
                    str(previous / "mbsrs-python.sh"),
                    str(previous / "mbsrs_core.py"),
                    "--config-check",
                ],
                env=env,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                timeout=60,
            )
            require(
                result.returncode == 0,
                "Previous release does not accept this configuration; restore a compatible configuration before rollback. Active code was preserved.",
            )
        set_current(root, previous)
        atomic_write(path, json.dumps({"release": old.name}).encode())
        print("Activated " + name + ". Configuration and archive were preserved.")
    finally:
        lock.close()

PYTHON_SELECTOR = '#!/bin/bash\nset -euo pipefail\nexport PATH="/usr/local/bin:/usr/bin:/bin:$HOME/bin"\nfor candidate in "$(command -v python3 || true)" /opt/alt/python311/bin/python3; do\n    [[ -n "$candidate" && -x "$candidate" ]] || continue\n    if "$candidate" -B -c \'import sys; sys.exit(0 if sys.version_info >= (3,6) else 1)\' 2>/dev/null; then\n        exec "$candidate" -B "$@"\n    fi\ndone\necho \'[FAIL] Python 3.6+ not available (checked PATH and /opt/alt/python311/bin/python3)\' >&2\nexit 2\n'
# Appended to the release transport module by build_release.py.
TRUST_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAniJC6GCYxk4Rt7YlTa6X\nKgLJkTWBnsovnQempBut2Q3Hxaw5BIKFSkB0vxRvYRdcm3Mw1R8Z79cFoCSMUfb8\nHqD8wpJlobJ7PxvbXDJziG2ATr3QCy0aZrFF/vBQdONRTnA6gyvD9ouLSx6f5Rx+\n03LWiID6Y9W55DATxucYxRpn2C72KrQrDMl4JmBMFucOCD6zTpaUipJRjDWEo1IT\nIqlYZ5N+c8fr55+KxRocVEfFIypqQx24lc+3KIyn0B2hoDqXuJsmTyej6mIBPF53\nbltRyT6vllixbvi5cs/dka1h5KAuRiH0nv9NbeulGVGgjQvywUJFY1RRsd0aQvPN\n6J1xOvIA0q6PNoZ3dHesXO3nmQiuCJsmW8q4lzsjz6UfzTVSdvWL3tyTbmNj3Iwi\nkz8wTg1OvXpzUeoUKmxH9u8cq8AIu1sSdaZvOJbL/vipqVudzAwkzkn6XT6Rxh4M\niDD+i0HtLL3hiOrZq0/JwEOyMnLfVkaeoAjN+fCKBSk5AgMBAAE=\n-----END PUBLIC KEY-----\n'

LAUNCH = r"""
import fcntl, os, pathlib, subprocess, sys

root = pathlib.Path(__file__).resolve().parent
os.umask(0o077)
(root / "state").mkdir(exist_ok=True, mode=0o700)
action = sys.argv[1] if len(sys.argv) > 1 else "status"
lock = open(str(root / "state/lifecycle.lock"), "a")
fcntl.flock(
    lock,
    (
        fcntl.LOCK_EX
        if action in ("update", "rollback", "setup", "cron-install", "auth")
        else fcntl.LOCK_SH
    ),
)
env = os.environ.copy()
env["MBSRS_HOME"] = str(root)
try:
    result = subprocess.run(
        [
            "/bin/bash",
            str(root / "current/mbsrs-python.sh"),
            str(root / "current/mbsrs_cli.py"),
        ]
        + sys.argv[1:],
        env=env,
        pass_fds=(lock.fileno(),),
    )
    sys.exit(result.returncode)
finally:
    lock.close()
"""


def bootstrap():
    import argparse, sys

    ap = argparse.ArgumentParser(
        description="Install MBSRS privately in the source account"
    )
    ap.add_argument(
        "--prefix", type=pathlib.Path, default=pathlib.Path.home() / "mbsrs"
    )
    ap.add_argument("--migrate-from", type=pathlib.Path)
    ap.add_argument("--config", type=pathlib.Path)
    ap.add_argument("--non-interactive", action="store_true")
    ap.add_argument("--no-cron", action="store_true")
    ap.add_argument(
        "--download-only",
        action="store_true",
        help="Install code without configuring/exporting mail",
    )
    ap.add_argument("--document-root", type=pathlib.Path, action="append", default=[])
    args = ap.parse_args()
    os.umask(0o077)
    require(os.getuid() != 0, "Run as the source account, not root")
    prefix = args.prefix.expanduser()
    require(prefix.is_absolute(), "Install prefix must be absolute")
    require(
        prefix.resolve() == prefix, "Install prefix must be canonical, without symlinks"
    )
    root = prefix
    home = pathlib.Path.home().resolve()
    require(
        re.fullmatch(r"/[A-Za-z0-9_./-]+", str(root)),
        "Install path cannot contain spaces or shell metacharacters",
    )
    forbidden = [home / "mail", home / "public_html"] + [
        p.expanduser().resolve() for p in args.document_root
    ]
    for path in forbidden:
        require(
            not (root == path or path in root.parents or root in path.parents),
            "Install folder overlaps mail or DocumentRoot",
        )
    require(
        root != home and home in root.parents,
        "Install inside a private subfolder of this account HOME",
    )
    if root.exists():
        require(
            root.is_dir() and root.stat().st_uid == os.getuid(),
            "Install directory must belong to this account",
        )
        require(
            not any(root.iterdir()) or (root / "state/installation.json").is_file(),
            "Nonempty unmanaged install folder; choose another --prefix",
        )
    root.mkdir(parents=True, exist_ok=True, mode=0o700)
    root.chmod(0o700)
    for folder in ("config", "state", "logs", "releases"):
        (root / folder).mkdir(exist_ok=True, mode=0o700)
    life = open(str(root / "state/lifecycle.lock"), "a")
    fcntl.flock(life, fcntl.LOCK_EX)
    try:
        key = root / "release-public.pem"
        if key.exists():
            require(
                key.read_text() == TRUST_PUBLIC_KEY,
                "Pinned release key differs; refusing to replace it",
            )
        else:
            atomic_write(key, TRUST_PUBLIC_KEY.encode())
        # Marker allows a failed first install to resume without discarding configuration.
        atomic_write(
            root / "state/installation.json",
            json.dumps({"product": "mbsrs", "layout": 1}).encode(),
        )
        atomic_write(root / "mbsrs-python.sh", PYTHON_SELECTOR.encode(), 0o700)
        atomic_write(root / "launch.py", LAUNCH.encode())
        launcher = '#!/bin/bash\nset -euo pipefail\nDIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)\nexec /bin/bash "$DIR/mbsrs-python.sh" "$DIR/launch.py" "$@"\n'
        atomic_write(root / "mbsrs", launcher.encode(), 0o700)
        data = download(RELEASE_URL + "/releases/latest.json", 65536)
        sig = download(RELEASE_URL + "/releases/latest.json.sig", 4096)
        obj = verify_manifest(data, sig, key)
        current = root / "current"
        if current.is_symlink():
            require(
                current.resolve().parent == root / "releases",
                "Unexpected current release path",
            )
            print(
                "Existing installation kept. Use "
                + str(root / "mbsrs")
                + " update to update its code."
            )
        else:
            require(not current.exists(), "Unexpected current path")
            payload = download(obj["archive_url"], MAX_PACKAGE, timeout=60)
            require(
                len(payload) == obj["size"]
                and hashlib.sha256(payload).hexdigest() == obj["sha256"],
                "Package integrity failure",
            )
            release = root / "releases" / ("mbsrs-" + obj["version"])
            require(
                not release.exists(),
                "Release directory already exists; inspect failed install",
            )
            stage = pathlib.Path(
                tempfile.mkdtemp(prefix=".staging-", dir=str(root / "releases"))
            )
            try:
                unpack(payload, stage, obj["version"])
                result = subprocess.run(
                    [
                        "/bin/bash",
                        str(stage / "mbsrs-python.sh"),
                        str(stage / "mbsrs_cli.py"),
                        "version",
                    ],
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    timeout=30,
                )
                require(
                    result.returncode == 0
                    and ("MBSRS " + obj["version"] + " ").encode() in result.stdout,
                    "Release version validation failed",
                )
                stage.rename(release)
                set_current(root, release)
                apply_release_source(root, data, sig)
            finally:
                if stage.exists():
                    shutil.rmtree(str(stage))
        print("CLI installed: " + str(root / "mbsrs"))
    finally:
        life.close()
    if args.download_only:
        return
    command = [str(root / "mbsrs"), "setup"]
    if args.migrate_from:
        command += ["--migrate-from", str(args.migrate_from.expanduser())]
    if args.config:
        command += ["--config", str(args.config.expanduser())]
    if args.non_interactive:
        command += ["--non-interactive"]
    if args.no_cron:
        command += ["--no-cron"]
    sys.exit(subprocess.call(command))


if __name__ == "__main__":
    try:
        bootstrap()
    except (UpdateError, OSError, ValueError, subprocess.SubprocessError) as e:
        import sys

        print("[FAIL] " + str(e), file=sys.stderr)
        sys.exit(2)
    except KeyboardInterrupt:
        import sys

        print(
            "[FAIL] Interrupted; rerun installer or mbsrs setup to resume.",
            file=sys.stderr,
        )
        sys.exit(130)

MBSRS_BOOTSTRAP_PY
for candidate in "$(command -v python3 || true)" /opt/alt/python311/bin/python3; do
    [[ -n "$candidate" && -x "$candidate" ]] || continue
    if "$candidate" -B -c 'import sys; sys.exit(0 if sys.version_info >= (3,6) else 1)' 2>/dev/null; then
        "$candidate" -B "$TEMP" "$@"
        exit $?
    fi
done
echo '[FAIL] Python 3.6+ unavailable' >&2
exit 2
