#!/usr/bin/env python3
"""Vendor bootstrap for file-remediation (Phase 1): Temurin JRE 21 + veraPDF +
opendataloader-pdf.

Installs into ~/Library/Application Support/asucb/vendor/<tool>-<version>/ — outside
the plugin root, so plugin zip updates never wipe it and re-runs are no-ops.
LibreOffice bootstrap is deferred until the first E1 export request (Phase 3).

Usage: python setup_remediation.py --check | --install [--yes]
Env: ASUCB_REMEDIATION_OFFLINE=1 skips network; --check reports vendored tools only,
     --install refuses.
"""

from __future__ import annotations

import argparse
import importlib.metadata
import json
import os
import platform
import subprocess
import sys
import tempfile
from pathlib import Path

VENDOR = Path.home() / "Library" / "Application Support" / "asucb" / "vendor"

# ---- Pins ---------------------------------------------------------------
JRE_VERSION = "21"
_ADOPTIUM_ARCH = {"arm64": "aarch64", "x86_64": "x64"}

VERAPDF_VERSION = "1.30.2"
VERAPDF_URL = (
    "https://software.verapdf.org/releases/1.30/"
    f"verapdf-greenfield-{VERAPDF_VERSION}-installer.zip"
)

# LibreOffice — deferred until the first Engine E1 export request (Phase 3).
# Vendored to <VENDOR>/libreoffice-<LIBREOFFICE_VERSION>/LibreOffice.app, a
# sibling layout to the JRE/veraPDF dirs above.
LIBREOFFICE_VERSION = "26.2.4"
_LO_ARCH = {"arm64": "aarch64", "x86_64": "x86_64"}


def _lo_arch():
    return _LO_ARCH.get(platform.machine(), platform.machine())


def libreoffice_url():
    v = LIBREOFFICE_VERSION
    arch = _lo_arch()
    return (
        f"https://download.documentfoundation.org/libreoffice/stable/{v}/mac/"
        f"{arch}/LibreOffice_{v}_MacOS_{arch}.dmg"
    )

# ponytail: unversioned pip pin — `pip index` version lookups are flaky in CI/offline
# envs. Pin an exact opendataloader-pdf version once a Phase 2 regression fixture
# exists to catch drift.
ODL_PIP = "opendataloader-pdf"

# Phase 2 deps — report-only in Phase 1 (missing is fine). pip name -> import name(s).
PHASE2_PIP_DEPS = {
    "pymupdf": ("pymupdf", "fitz"),
    "pikepdf": ("pikepdf",),
    "python-pptx": ("pptx",),
    "pyobjc-framework-Vision": ("Vision",),
}


def _adoptium_arch():
    return _ADOPTIUM_ARCH.get(platform.machine(), platform.machine())


def jre_url():
    return (
        f"https://api.adoptium.net/v3/binary/latest/{JRE_VERSION}/ga/mac/"
        f"{_adoptium_arch()}/jre/hotspot/normal/eclipse"
    )


def get_java():
    """Return the vendored `java` binary Path, or None if not installed."""
    matches = sorted(VENDOR.glob("jre-21*/Contents/Home/bin/java"))
    return matches[0] if matches and matches[0].exists() else None


def get_verapdf():
    """Return the vendored `verapdf` script Path, or None if not installed."""
    path = VENDOR / f"verapdf-{VERAPDF_VERSION}" / "verapdf"
    return path if path.exists() else None


def get_libreoffice():
    """Return the vendored `soffice` binary Path, or None if not installed."""
    path = (VENDOR / f"libreoffice-{LIBREOFFICE_VERSION}" / "LibreOffice.app" /
            "Contents" / "MacOS" / "soffice")
    return path if path.exists() else None


def _odl_present():
    try:
        import opendataloader_pdf  # noqa: F401
        return True
    except ImportError:
        return False


def _phase2_status():
    status = {}
    for pip_name, import_names in PHASE2_PIP_DEPS.items():
        present = False
        for mod in import_names:
            try:
                __import__(mod)
                present = True
                break
            except ImportError:
                continue
        status[pip_name] = present
    return status


def _java_healthy(java_bin):
    try:
        proc = subprocess.run(
            [str(java_bin), "-version"], capture_output=True, text=True, timeout=10
        )
        return proc.returncode == 0
    except Exception:
        return False


def check():
    """Return a health-check report dict. Never raises."""
    offline = os.environ.get("ASUCB_REMEDIATION_OFFLINE") == "1"
    java = get_java()
    verapdf = get_verapdf()
    libreoffice = get_libreoffice()
    report = {
        "offline": offline,
        "jre": {
            "present": bool(java),
            "path": str(java) if java else None,
            "healthy": bool(java) and _java_healthy(java),
        },
        "verapdf": {"present": bool(verapdf), "path": str(verapdf) if verapdf else None},
        "odl": {"present": _odl_present()},
        # report-only — never gates `ok` (deferred/opt-in, Phase 3 Engine E1)
        "libreoffice": {"present": bool(libreoffice), "path": str(libreoffice) if libreoffice else None},
        "phase2": _phase2_status(),
    }
    report["ok"] = (
        report["jre"]["present"] and report["verapdf"]["present"] and report["odl"]["present"]
    )
    return report


# ---- Install --------------------------------------------------------------

def _download(url, dest):
    subprocess.run(["curl", "-L", "--fail", "-o", str(dest), url], check=True)


def _install_jre(tmp_dir):
    if get_java():
        print("JRE 21: already installed, skipping")
        return
    archive = tmp_dir / "jre21.tar.gz"
    print(f"Downloading Temurin JRE {JRE_VERSION} ({_adoptium_arch()})...")
    _download(jre_url(), archive)
    VENDOR.mkdir(parents=True, exist_ok=True)
    extract_dir = VENDOR / f"jre-{JRE_VERSION}-{_adoptium_arch()}"
    extract_dir.mkdir(parents=True, exist_ok=True)
    subprocess.run(
        ["tar", "-xzf", str(archive), "-C", str(extract_dir), "--strip-components=1"],
        check=True,
    )
    if not get_java():
        raise RuntimeError("JRE install failed: java binary not found after extraction")
    print(f"JRE {JRE_VERSION} installed: {get_java()}")


def _verapdf_auto_install_xml(install_dir):
    return f"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<AutomatedInstallation langpack="eng">
    <com.izforge.izpack.panels.htmlhello.HTMLHelloPanel id="welcome"/>
    <com.izforge.izpack.panels.target.TargetPanel id="install_dir">
        <installpath>{install_dir}</installpath>
    </com.izforge.izpack.panels.target.TargetPanel>
    <com.izforge.izpack.panels.packs.PacksPanel id="sdk_pack_select">
        <pack index="0" name="veraPDF GUI" selected="true"/>
        <pack index="1" name="veraPDF Mac and *nix Scripts" selected="true"/>
        <pack index="2" name="veraPDF Validation model" selected="false"/>
        <pack index="3" name="veraPDF Documentation" selected="false"/>
        <pack index="4" name="veraPDF Sample Plugins" selected="false"/>
    </com.izforge.izpack.panels.packs.PacksPanel>
    <com.izforge.izpack.panels.install.InstallPanel id="install"/>
    <com.izforge.izpack.panels.finish.FinishPanel id="finish"/>
</AutomatedInstallation>
"""


def _install_verapdf(tmp_dir):
    if get_verapdf():
        print(f"veraPDF {VERAPDF_VERSION}: already installed, skipping")
        return
    java = get_java()
    if not java:
        raise RuntimeError("veraPDF install requires the vendored JRE — install JRE first")
    archive = tmp_dir / "verapdf-installer.zip"
    print(f"Downloading veraPDF {VERAPDF_VERSION}...")
    _download(VERAPDF_URL, archive)
    unzip_dir = tmp_dir / "verapdf-installer"
    subprocess.run(["unzip", "-q", str(archive), "-d", str(unzip_dir)], check=True)
    installer_jars = list(unzip_dir.rglob("verapdf-izpack-installer-*.jar"))
    if not installer_jars:
        raise RuntimeError("veraPDF installer jar not found in downloaded archive")
    install_dir = VENDOR / f"verapdf-{VERAPDF_VERSION}"
    auto_install_xml = tmp_dir / "auto-install.xml"
    auto_install_xml.write_text(_verapdf_auto_install_xml(install_dir), encoding="utf-8")
    subprocess.run([str(java), "-jar", str(installer_jars[0]), str(auto_install_xml)], check=True)
    if not get_verapdf():
        raise RuntimeError("veraPDF install failed: verapdf script not found after install")
    print(f"veraPDF {VERAPDF_VERSION} installed: {get_verapdf()}")


def _install_libreoffice(tmp_dir):
    if get_libreoffice():
        print(f"LibreOffice {LIBREOFFICE_VERSION}: already installed, skipping")
        return
    dmg = tmp_dir / "libreoffice.dmg"
    print(f"Downloading LibreOffice {LIBREOFFICE_VERSION} ({_lo_arch()})...")
    _download(libreoffice_url(), dmg)
    mount_point = tmp_dir / "lo-mount"
    mount_point.mkdir(parents=True, exist_ok=True)
    subprocess.run(
        ["hdiutil", "attach", "-nobrowse", "-mountpoint", str(mount_point), str(dmg)], check=True
    )
    try:
        apps = list(mount_point.glob("*.app"))
        if not apps:
            raise RuntimeError("LibreOffice.app not found in mounted dmg")
        install_dir = VENDOR / f"libreoffice-{LIBREOFFICE_VERSION}"
        install_dir.mkdir(parents=True, exist_ok=True)
        subprocess.run(["cp", "-R", str(apps[0]), str(install_dir / "LibreOffice.app")], check=True)
    finally:
        subprocess.run(["hdiutil", "detach", str(mount_point), "-quiet"], check=False)

    soffice = get_libreoffice()
    if not soffice:
        raise RuntimeError("LibreOffice install failed: soffice binary not found after install")
    proc = subprocess.run([str(soffice), "--version"], capture_output=True, text=True, timeout=30)
    if proc.returncode != 0:
        raise RuntimeError(f"LibreOffice --version check failed: {proc.stderr[:300]}")
    print(f"LibreOffice {LIBREOFFICE_VERSION} installed: {soffice}")


def install_libreoffice(auto_yes=False):
    """Separate opt-in install path — LibreOffice is NOT part of `install()`
    (deferred until the first Engine E1 export request, plan §5)."""
    if os.environ.get("ASUCB_REMEDIATION_OFFLINE") == "1":
        print("ASUCB_REMEDIATION_OFFLINE=1 — refusing to install (offline mode).")
        return 1
    print(f"Install plan:\n  LibreOffice {LIBREOFFICE_VERSION:<10} {libreoffice_url():<75} ~500 MB")
    if not auto_yes:
        reply = input("Proceed with download + install? [y/N] ").strip().lower()
        if reply not in ("y", "yes"):
            print("Aborted.")
            return 1
    VENDOR.mkdir(parents=True, exist_ok=True)
    with tempfile.TemporaryDirectory(dir=VENDOR) as tmp:
        _install_libreoffice(Path(tmp))
    return 0


def _install_odl():
    if _odl_present():
        print(f"{ODL_PIP}: already installed, skipping")
        return
    print(f"pip installing: {ODL_PIP}")
    subprocess.run([sys.executable, "-m", "pip", "install", ODL_PIP], check=True)


def _install_phase2_deps():
    """Install Phase 2 pip deps, pinned to whatever version is already
    installed (dev machines that bootstrapped ad hoc); unpinned otherwise."""
    specs = []
    for pip_name in PHASE2_PIP_DEPS:
        try:
            specs.append(f"{pip_name}=={importlib.metadata.version(pip_name)}")
        except importlib.metadata.PackageNotFoundError:
            # ponytail: no installed version to pin against — install unpinned
            # latest. Pin once a Phase 2 regression fixture needs a fixed version.
            specs.append(pip_name)
    print(f"pip installing (Phase 2 deps): {specs}")
    subprocess.run([sys.executable, "-m", "pip", "install", *specs], check=True)


def install(auto_yes=False):
    if os.environ.get("ASUCB_REMEDIATION_OFFLINE") == "1":
        print("ASUCB_REMEDIATION_OFFLINE=1 — refusing to install (offline mode).")
        return 1

    plan = [
        ("Temurin JRE 21", jre_url(), "~45 MB"),
        (f"veraPDF greenfield {VERAPDF_VERSION}", VERAPDF_URL, "~180 MB"),
        (f"{ODL_PIP} (pip)", "PyPI", "~30 MB"),
        ("Phase 2 deps (pymupdf/pikepdf/python-pptx/pyobjc-Vision, pip)", "PyPI", "~60 MB"),
    ]
    print("Install plan:")
    for name, source, size in plan:
        print(f"  {name:<32} {source:<75} {size}")
    if not auto_yes:
        reply = input("Proceed with download + install? [y/N] ").strip().lower()
        if reply not in ("y", "yes"):
            print("Aborted.")
            return 1

    VENDOR.mkdir(parents=True, exist_ok=True)
    with tempfile.TemporaryDirectory(dir=VENDOR) as tmp:
        tmp_dir = Path(tmp)
        _install_jre(tmp_dir)
        _install_verapdf(tmp_dir)
        _install_odl()
    _install_phase2_deps()
    print("Install complete. LibreOffice is not installed — deferred until the first "
          "E1 export request (Phase 3).")
    return 0


def main():
    parser = argparse.ArgumentParser(description="Vendor bootstrap for file-remediation")
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument("--check", action="store_true", help="Report vendored-tool health")
    group.add_argument("--install", action="store_true", help="Download + install vendored tools")
    group.add_argument("--install-libreoffice", action="store_true",
                        help="Download + install vendored LibreOffice (Phase 3 Engine E1 export)")
    parser.add_argument("--yes", action="store_true", help="Skip the install confirmation prompt")
    args = parser.parse_args()

    if args.check:
        if os.environ.get("ASUCB_REMEDIATION_OFFLINE") == "1":
            print("ASUCB_REMEDIATION_OFFLINE=1 — offline mode: reporting vendored tools only.")
        report = check()
        print(json.dumps(report, indent=2))
        sys.exit(0 if report["ok"] else 1)
    elif args.install_libreoffice:
        sys.exit(install_libreoffice(auto_yes=args.yes))
    else:
        sys.exit(install(auto_yes=args.yes))


if __name__ == "__main__":
    main()
