@echo off
powershell -NoProfile -ExecutionPolicy Bypass -Command "$f=Get-Content -Raw -LiteralPath '%~f0'; $m=[char]35+'==PS-BODY=='; $i=$f.LastIndexOf($m); if($i -lt 0){Write-Host 'PS body marker not found'; exit 1}; Invoke-Expression $f.Substring($i+$m.Length)"
echo.
pause
exit /b %ERRORLEVEL%
#==PS-BODY==
# ---------------------------------------------------------------------------
# ArcPy standalone bridge installer
# Wires a standalone Python to an installed ArcGIS Pro/Server so `import arcpy`
# works without the product's Conda environment.
# Optional environment overrides:
#   ARCPY_PRO_DIR         path to the ArcGIS install (folder with bin+Resources)
#   ARCPY_TARGET_PYTHON   path to the standalone python.exe to install into
# ---------------------------------------------------------------------------
$ErrorActionPreference = 'Stop'

function Info($m){ Write-Host "[*] $m" -ForegroundColor Cyan }
function Ok($m){ Write-Host "[OK] $m" -ForegroundColor Green }
function Warn($m){ Write-Host "[!] $m" -ForegroundColor Yellow }
function Die($m){ Write-Host "[X] $m" -ForegroundColor Red; exit 1 }

try {

Write-Host "=== ArcPy standalone bridge installer ===" -ForegroundColor White

# --- 1. Locate the ArcGIS product installation --------------------------------
$proDir = $env:ARCPY_PRO_DIR
if ($proDir -and -not (Test-Path $proDir)) { Warn "ARCPY_PRO_DIR '$proDir' does not exist; ignoring."; $proDir = $null }

if (-not $proDir) {
    $regPaths = @(
        'SOFTWARE\Esri\ArcGISPro',
        'SOFTWARE\Esri\ArcGISAllSource',
        'SOFTWARE\Esri\ArcGISRealityStudio',
        'SOFTWARE\Esri\Drone2Map'
    )
    foreach ($hive in @('HKCU:','HKLM:')) {
        foreach ($rp in $regPaths) {
            $full = "$hive\$rp"
            try {
                $val = (Get-ItemProperty -Path $full -Name InstallDir -ErrorAction Stop).InstallDir
                if ($val -and (Test-Path $val)) { $proDir = $val.TrimEnd('\'); break }
            } catch {}
        }
        if ($proDir) { break }
    }
}
if (-not $proDir -and (Test-Path 'C:\Program Files\ArcGIS\Pro')) { $proDir = 'C:\Program Files\ArcGIS\Pro' }
if (-not $proDir) { Die "Could not find an ArcGIS product installation. Install ArcGIS Pro first, or set ARCPY_PRO_DIR." }

if (-not (Test-Path (Join-Path $proDir 'Resources\ArcPy\arcpy'))) {
    Die "Found '$proDir' but it has no Resources\ArcPy\arcpy folder. Is this a valid ArcGIS install?"
}
Ok "ArcGIS install: $proDir"

# --- 2. Determine the product's Python major.minor ----------------------------
$envsRoot = Join-Path $proDir 'bin\Python\envs'
$prodPy = $null
$preferred = Join-Path $envsRoot 'arcgispro-py3\python.exe'
if (Test-Path $preferred) { $prodPy = $preferred }
elseif (Test-Path $envsRoot) {
    $prodPy = (Get-ChildItem $envsRoot -Directory | ForEach-Object { Join-Path $_.FullName 'python.exe' } | Where-Object { Test-Path $_ } | Select-Object -First 1)
}
if (-not $prodPy) { Die "Could not find the ArcGIS Python interpreter under $envsRoot." }
$prodVer = (& $prodPy -c "import sys;print('%d.%d'%sys.version_info[:2])").Trim()
Ok "ArcGIS Python version: $prodVer  ($prodPy)"

# --- 3. Find a matching standalone Python (not the ArcGIS one) -----------------
$minor = $prodVer.Split('.')[1]
$candidates = @()
if ($env:ARCPY_TARGET_PYTHON) { $candidates += $env:ARCPY_TARGET_PYTHON }
try { $candidates += (& py "-$prodVer" -c "import sys;print(sys.executable)" 2>$null) } catch {}
$candidates += "C:\Program Files\Python3$minor\python.exe"
$candidates += "$env:LOCALAPPDATA\Programs\Python\Python3$minor\python.exe"
try { $candidates += (where.exe python 2>$null) } catch {}

$targetPy = $null
foreach ($c in $candidates) {
    if (-not $c) { continue }
    $c = $c.Trim()
    if (-not (Test-Path $c)) { continue }
    $rc = (Resolve-Path $c).Path
    # must not be the ArcGIS-bundled interpreter
    if ($rc.ToLower().StartsWith($proDir.ToLower())) { continue }
    try { $v = (& $rc -c "import sys;print('%d.%d'%sys.version_info[:2])" 2>$null).Trim() } catch { continue }
    if ($v -eq $prodVer) { $targetPy = $rc; break }
}
if (-not $targetPy) {
    Die ("No standalone Python $prodVer found. Install python.org $prodVer, or set " +
         "ARCPY_TARGET_PYTHON to its python.exe. (Must match ArcGIS's $prodVer exactly.)")
}
Ok "Target standalone Python: $targetPy"

# --- 4. Write the bridge project to a temp folder -----------------------------
$build = Join-Path $env:TEMP ("arcpy-pro-bridge-build")
if (Test-Path $build) { Remove-Item $build -Recurse -Force }
New-Item -ItemType Directory -Path $build | Out-Null
Info "Generating project in $build"

$bootstrap = @'
# -*- coding: utf-8 -*-
"""
arcpy_pro_bootstrap
===================

Bridges a *standalone* CPython interpreter (e.g. python.org 3.13) to the ArcPy
package, dependencies and native DLLs that ship inside an ArcGIS Pro / Server
installation, **without** activating the product's Conda environment.

It works by locating the installed ArcGIS product (registry, then a few
fallbacks), then adding:

  * the product's Conda env ``Lib/site-packages``  -> numpy, arcgisscripting, ...
  * ``Resources/ArcPy``                            -> the ``arcpy`` package
  * ``Resources/ArcToolbox/Scripts``               -> script tools
to ``sys.path``, and registering the product ``bin`` / ``Library/bin`` folders as
native DLL search directories.

The wiring is performed *lazily*: a ``sys.meta_path`` finder is installed at
interpreter start-up (via the bundled ``.pth`` file) but the heavy path/DLL
changes only happen the first time you ``import arcpy`` (or ``arcgisscripting``).
Until then your interpreter is untouched, so normal use of this Python is
unaffected.

Compatibility requires the product's Python minor version to match this
interpreter's (ArcGIS Pro 3.6 ships CPython 3.13, matching standalone 3.13.x).
"""

import os
import sys
import warnings

__all__ = ["activate", "install_finder", "product_install_dir", "is_active"]
__version__ = "1.0.0"

# ArcGIS products that can host ArcPy, with the registry sub-key holding InstallDir.
_REG_PATHS = (
    ("Pro", r"SOFTWARE\Esri\ArcGISPro"),
    ("AllSource", r"SOFTWARE\Esri\ArcGISAllSource"),
    ("Reality", r"SOFTWARE\Esri\ArcGISRealityStudio"),
    ("Drone2Map", r"SOFTWARE\Esri\Drone2Map"),
    ("Server", r"SOFTWARE\Esri\ArcGIS_SxS_Server"),
)

# Modules whose import should trigger activation of the bridge.
_TRIGGER_ROOTS = ("arcpy", "arcgisscripting")

_activated = False


def is_active():
    """Return True once the bridge has wired the interpreter to ArcPy."""
    return _activated


def _from_registry():
    """Yield candidate install dirs from the Windows registry (HKCU then HKLM)."""
    try:
        import winreg
    except ImportError:
        return
    access = winreg.KEY_READ | winreg.KEY_WOW64_64KEY
    for root in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):
        for _product, reg_path in _REG_PATHS:
            try:
                with winreg.OpenKey(root, reg_path, 0, access) as key:
                    value = winreg.QueryValueEx(key, "InstallDir")[0]
            except OSError:
                continue
            if value:
                path = value
                if "ArcGIS_SxS_Server" in reg_path:
                    path = os.path.join(value, "framework", "runtime", "ArcGIS")
                if os.path.isdir(path):
                    yield path


def product_install_dir():
    """Locate the ArcGIS product installation directory.

    Resolution order:
      1. ``ARCPY_PRO_DIR`` environment variable (explicit override).
      2. Windows registry (Pro, AllSource, Reality, Drone2Map, Server).
      3. Common default install location.
    """
    override = os.environ.get("ARCPY_PRO_DIR")
    if override and os.path.isdir(override):
        return os.path.abspath(override)

    for candidate in _from_registry():
        return os.path.abspath(candidate)

    default = r"C:\Program Files\ArcGIS\Pro"
    if os.path.isdir(default):
        return default

    raise ImportError(
        "arcpy_pro_bootstrap: unable to locate an ArcGIS product installation. "
        "Set the ARCPY_PRO_DIR environment variable to the install directory "
        "(the folder containing 'bin' and 'Resources')."
    )


def _find_conda_env(install_dir):
    """Return the path to the product's Conda env that contains a python.exe."""
    envs_root = os.path.join(install_dir, "bin", "Python", "envs")
    preferred = os.path.join(envs_root, "arcgispro-py3")
    if os.path.isfile(os.path.join(preferred, "python.exe")):
        return preferred
    if os.path.isdir(envs_root):
        for name in sorted(os.listdir(envs_root)):
            candidate = os.path.join(envs_root, name)
            if os.path.isfile(os.path.join(candidate, "python.exe")):
                return candidate
    # Fall back to the preferred path even if python.exe is absent; callers will
    # still get site-packages if the layout is non-standard.
    return preferred


def _check_version(env_dir):
    """Warn loudly if the product Python minor version differs from this one."""
    pyver = "python{0}{1}.dll".format(*sys.version_info[:2])
    if not os.path.isfile(os.path.join(env_dir, pyver)):
        # The matching pythonXY.dll lives in the env root; if missing the ABI
        # almost certainly differs and the native .pyd files will fail to load.
        warnings.warn(
            "arcpy_pro_bootstrap: the ArcGIS Python environment does not appear "
            "to match this interpreter (expected {0}). ArcPy's compiled "
            "extensions may fail to import. Use a standalone Python whose "
            "major.minor matches the version shipped with ArcGIS "
            "Pro/Server.".format(pyver),
            RuntimeWarning,
            stacklevel=2,
        )


def activate():
    """Wire this interpreter to the installed ArcPy. Idempotent."""
    global _activated
    if _activated:
        return
    install_dir = product_install_dir()
    bin_dir = os.path.join(install_dir, "bin")
    env_dir = _find_conda_env(install_dir)

    _check_version(env_dir)

    # 1) Register native DLL search directories (Python 3.8+ requires this on
    #    Windows; PATH alone is no longer enough for extension dependencies).
    dll_dirs = [
        bin_dir,
        os.path.join(env_dir, "Library", "bin"),
        os.path.join(env_dir, "Library", "mkl"),
        os.path.join(env_dir, "Library", "usr", "bin"),
        os.path.join(env_dir, "DLLs"),
        os.path.join(env_dir, "bin"),
    ]
    for d in dll_dirs:
        if os.path.isdir(d):
            try:
                os.add_dll_directory(d)
            except OSError:
                pass

    # 2) Prepend the same folders to PATH (some ArcGIS components still read it).
    path_prefix = [
        env_dir,
        os.path.join(env_dir, "Library", "bin"),
        os.path.join(env_dir, "Scripts"),
        bin_dir,
        os.path.join(bin_dir, "Python", "Scripts"),
    ]
    existing = os.environ.get("PATH", "")
    os.environ["PATH"] = os.pathsep.join(
        [p for p in path_prefix if os.path.isdir(p)] + ([existing] if existing else [])
    )

    # 3) Environment variables ArcPy/ArcGIS expect.
    providers = os.path.join(env_dir, "Library", "bin", "providers")
    if os.path.isdir(providers):
        os.environ.setdefault("OPENSSL_MODULES", providers)
    os.environ.setdefault("FOR_DISABLE_CONSOLE_CTRL_HANDLER", "1")

    # 4) Put ArcPy, its script tools and the env's site-packages on sys.path.
    #    Prepended so the product's numpy/arcgisscripting win over anything the
    #    user may later install into this standalone interpreter.
    sys_path_prepend = [
        os.path.join(install_dir, "Resources", "ArcPy"),
        os.path.join(install_dir, "Resources", "ArcToolbox", "Scripts"),
        os.path.join(env_dir, "Lib", "site-packages"),
    ]
    for p in reversed(sys_path_prepend):
        if os.path.isdir(p) and p not in sys.path:
            sys.path.insert(0, p)

    if len(sys.argv) == 0:
        sys.argv = [""]

    _activated = True


def analysis_paths():
    """Return the static directories that hold ArcPy and its dependencies.

    These are the folders a static analyzer (VS Code Pylance / pyright) must see
    to resolve ``import arcpy`` and offer completions. Only existing dirs are
    returned.
    """
    install_dir = product_install_dir()
    env_dir = _find_conda_env(install_dir)
    candidates = [
        os.path.join(install_dir, "Resources", "ArcPy"),
        os.path.join(install_dir, "Resources", "ArcToolbox", "Scripts"),
        os.path.join(env_dir, "Lib", "site-packages"),
    ]
    return [p for p in candidates if os.path.isdir(p)]


def write_analysis_pth(target_dir=None):
    """Write ``arcpy_pro_paths.pth`` (plain directory lines) into site-packages.

    Unlike the executable bootstrap ``.pth``, this one lists real directories, so
    both Python's ``site`` module *and* static analyzers such as VS Code's
    Pylance/pyright add them to the search path. This is what makes VS Code
    recognize ``arcpy`` and show member completions. Returns the file path
    written, or ``None`` if no ArcGIS install could be located.
    """
    if target_dir is None:
        target_dir = os.path.dirname(os.path.abspath(__file__))
    try:
        lines = analysis_paths()
    except ImportError:
        return None
    if not lines:
        return None
    dest = os.path.join(target_dir, "arcpy_pro_paths.pth")
    with open(dest, "w", encoding="utf-8") as fh:
        fh.write("\n".join(lines) + "\n")
    return dest


class _QuietArcpyLoader:
    """Loader wrapper that hides ArcPy's cosmetic 'expects Conda' banner.

    ArcPy prints a multi-line notice (and raises a RuntimeWarning) at import time
    when it can't find a ``conda-meta`` folder next to ``sys.prefix``. ArcPy is
    fully functional regardless, so we swallow that one banner/warning during the
    module's execution and let everything else through untouched.
    """

    def __init__(self, inner):
        self._inner = inner

    def create_module(self, spec):
        return self._inner.create_module(spec)

    def exec_module(self, module):
        import io
        import contextlib

        captured = io.StringIO()
        with warnings.catch_warnings():
            warnings.filterwarnings(
                "ignore",
                message="arcpy expects to run within a Conda environment",
                category=RuntimeWarning,
            )
            with contextlib.redirect_stdout(captured):
                self._inner.exec_module(module)
        text = captured.getvalue()
        # Re-emit anything that wasn't the known Conda banner.
        if text and "expects to run inside a Conda environment" not in text:
            sys.stdout.write(text)

    def __getattr__(self, name):
        return getattr(self._inner, name)


class _ArcpyFinder:
    """meta_path finder that activates the bridge on first ArcPy-family import."""

    def find_spec(self, fullname, path=None, target=None):
        root = fullname.split(".", 1)[0]
        if root not in _TRIGGER_ROOTS:
            return None

        # Remove ourselves first so the real import machinery (and any recursive
        # imports triggered below) proceed normally and we only fire once.
        try:
            sys.meta_path.remove(self)
        except ValueError:
            pass

        try:
            activate()
        except Exception:
            # Never break interpreter import resolution; let the normal machinery
            # raise the natural ModuleNotFoundError if activation failed.
            return None

        from importlib.machinery import PathFinder

        spec = PathFinder.find_spec(fullname, path, target)
        if spec is not None and fullname == "arcpy" and spec.loader is not None:
            spec.loader = _QuietArcpyLoader(spec.loader)
        return spec


def install_finder():
    """Install the lazy import hook. Called from the bundled .pth at start-up."""
    if any(isinstance(f, _ArcpyFinder) for f in sys.meta_path):
        return
    sys.meta_path.insert(0, _ArcpyFinder())
'@

$pth = 'import arcpy_pro_bootstrap; arcpy_pro_bootstrap.install_finder()'

$pyproject = @'
[build-system]
requires = ["setuptools>=64"]
build-backend = "setuptools.build_meta"

[project]
name = "arcpy-pro-bridge"
version = "1.0.0"
description = "Use ArcPy from a standalone Python by bridging to an installed ArcGIS Pro/Server."
readme = "README.md"
requires-python = ">=3.9"
license = { text = "Proprietary - requires a licensed ArcGIS installation" }

[tool.setuptools]
py-modules = ["arcpy_pro_bootstrap"]
'@

$setup = @'
import os, shutil
from setuptools import setup
from setuptools.command.build_py import build_py
_PTH = "arcpy_pro_bootstrap.pth"
class build_py_with_pth(build_py):
    def run(self):
        super().run()
        src = os.path.join(os.path.dirname(os.path.abspath(__file__)), _PTH)
        shutil.copyfile(src, os.path.join(self.build_lib, _PTH))
setup(cmdclass={"build_py": build_py_with_pth})
'@

$readme = "arcpy-pro-bridge: bridges a standalone Python to an installed ArcGIS Pro so import arcpy works without the Conda environment."

# Write UTF-8 WITHOUT a BOM; a BOM makes tomllib reject pyproject.toml.
$utf8 = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText((Join-Path $build 'arcpy_pro_bootstrap.py'),  $bootstrap, $utf8)
[System.IO.File]::WriteAllText((Join-Path $build 'arcpy_pro_bootstrap.pth'), $pth,       $utf8)
[System.IO.File]::WriteAllText((Join-Path $build 'pyproject.toml'),          $pyproject, $utf8)
[System.IO.File]::WriteAllText((Join-Path $build 'setup.py'),                $setup,     $utf8)
[System.IO.File]::WriteAllText((Join-Path $build 'README.md'),               $readme,    $utf8)

# --- 5. Install via pip (fall back to direct copy if the build fails) ----------
Info "Running: pip install . (with $targetPy)"
$installed = $false
try {
    & $targetPy -m pip install $build --disable-pip-version-check
    if ($LASTEXITCODE -eq 0) { $installed = $true }
} catch {}

if (-not $installed) {
    Warn "pip install failed (often no internet for build isolation). Falling back to direct copy."
    $purelib = (& $targetPy -c "import sysconfig;print(sysconfig.get_path('purelib'))").Trim()
    $dest = $purelib
    try {
        $probe = Join-Path $purelib '.arcpy_write_test'
        New-Item -ItemType File -Path $probe -Force | Out-Null
        Remove-Item $probe -Force
    } catch {
        $dest = (& $targetPy -c "import site;print(site.getusersitepackages())").Trim()
        Warn "Main site-packages not writable; using user site: $dest"
    }
    if (-not (Test-Path $dest)) { New-Item -ItemType Directory -Path $dest -Force | Out-Null }
    Copy-Item (Join-Path $build 'arcpy_pro_bootstrap.py')  (Join-Path $dest 'arcpy_pro_bootstrap.py')  -Force
    Copy-Item (Join-Path $build 'arcpy_pro_bootstrap.pth') (Join-Path $dest 'arcpy_pro_bootstrap.pth') -Force
    $installed = $true
    Ok "Copied bridge files to $dest"
}

# --- 5b. Enable static-analysis / IntelliSense support (VS Code Pylance) ------
# Writes a plain-path .pth (real directories) that Pylance/pyright honor, so the
# editor resolves 'import arcpy' and shows completions. Reflects THIS machine.
Info "Enabling editor IntelliSense (writing analysis paths for Pylance)..."
try {
    $pth = (& $targetPy -c "import arcpy_pro_bootstrap as b; p=b.write_analysis_pth(); print(p or '')").Trim()
    if ($pth) { Ok "Analysis paths written: $pth" }
    else { Warn "Could not write analysis paths (ArcGIS not detected at runtime)." }
} catch {
    Warn "Editor-support step failed: $($_.Exception.Message)"
}

# --- 6. Verify import in a clean interpreter ----------------------------------
Info "Verifying 'import arcpy' ..."
Push-Location $env:TEMP
$test = "import arcpy; print('ProductInfo:', arcpy.ProductInfo()); print('SR:', arcpy.SpatialReference(4326).name)"
& $targetPy -c $test
$code = $LASTEXITCODE
Pop-Location

Remove-Item $build -Recurse -Force -ErrorAction SilentlyContinue

if ($code -eq 0) {
    Write-Host ""
    Ok "SUCCESS - 'import arcpy' works on $targetPy"
} else {
    Die "Bridge installed but 'import arcpy' failed. Check the output above."
}

} catch {
    Write-Host ""
    Die $_.Exception.Message
}

