#!/usr/bin/env python3
"""
scripts/install_plugin.py
=========================
Install the GOLEM-3DMCP plugin into Rhino's startup scripts so that the
TCP server starts automatically every time Rhino opens.

What this script does
---------------------
1. Verifies that Rhino 8 is installed at /Applications/Rhino 8.app/.
2. Locates the best available Rhino Python scripts directory in order of
   preference:
     a) ~/.rhinocode/scripts/      (rhinocode per-user scripts)
     b) ~/.rhinocode/              (rhinocode root)
     c) ~/Library/Application Support/McNeel/Rhinoceros/8.0/scripts/
3. Writes a small startup shim (golem_autostart.py) that adds the project
   root to sys.path and calls startup.py._start().
4. If no writable scripts directory can be found, prints clear manual
   installation instructions.

Usage
-----
    python scripts/install_plugin.py [--project-root PATH] [--port PORT]
                                     [--uninstall] [--dry-run]
"""

from __future__ import annotations

import argparse
import os
import pathlib
import shutil
import sys

# ---------------------------------------------------------------------------
# ANSI colour helpers (no external dependencies)
# ---------------------------------------------------------------------------
_IS_TTY = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()


def _c(code: str, text: str) -> str:
    """Wrap *text* in an ANSI colour/style escape if the terminal supports it."""
    return f"\033[{code}m{text}\033[0m" if _IS_TTY else text


def ok(msg: str) -> None:
    print(_c("32", "[OK]   ") + msg)


def info(msg: str) -> None:
    print(_c("36", "[INFO] ") + msg)


def warn(msg: str) -> None:
    print(_c("33", "[WARN] ") + msg)


def error(msg: str) -> None:
    print(_c("31", "[ERR]  ") + msg, file=sys.stderr)


def header(msg: str) -> None:
    print()
    print(_c("1", msg))


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
RHINO8_APP = pathlib.Path("/Applications/Rhino 8.app")
RHINOWIP_APP = pathlib.Path("/Applications/RhinoWIP.app")

SHIM_FILENAME = "golem_autostart.py"

# Template for the startup shim written into Rhino's scripts folder.
# Uses old-style .format() so f-string braces in the embedded Python source
# do not conflict with our substitution variables.
_SHIM_TEMPLATE = '''\
#!/usr/bin/env python3
"""
GOLEM-3DMCP auto-start shim — generated by scripts/install_plugin.py
Do not edit manually; re-run install_plugin.py to regenerate.

This file is loaded by Rhino at startup via the rhinocode scripts mechanism
or by placing it in Rhino's Python scripts directory.
"""
import sys as _sys

_PROJECT_ROOT = r"{project_root}"
_HOST = "127.0.0.1"
_PORT = {port}

if _PROJECT_ROOT not in _sys.path:
    _sys.path.insert(0, _PROJECT_ROOT)

try:
    from rhino_plugin import startup as _golem_startup  # noqa: E402
    _golem_startup._start(_HOST, _PORT)
except Exception as _exc:
    print("GOLEM-3DMCP auto-start failed: {{exc}}".format(exc=_exc))
    print("  Project root : {{root}}".format(root=_PROJECT_ROOT))
    print("  Ensure the project root is correct and dependencies are installed.")
'''


# ---------------------------------------------------------------------------
# Helper: candidate scripts directories in priority order
# ---------------------------------------------------------------------------
def _candidate_dirs() -> list[pathlib.Path]:
    home = pathlib.Path.home()
    return [
        home / ".rhinocode" / "scripts",
        home / ".rhinocode",
        home / "Library" / "Application Support" / "McNeel" / "Rhinoceros" / "8.0" / "scripts",
        home / "Library" / "Application Support" / "McNeel" / "Rhinoceros" / "8.0" / "Plug-ins" / "IronPython" / "settings" / "lib",
    ]


def _find_scripts_dir() -> pathlib.Path | None:
    """Return the first writable Rhino scripts directory found, or None."""
    for d in _candidate_dirs():
        if d.exists() and os.access(d, os.W_OK):
            return d
        # Try to create it if the parent exists and is writable
        if not d.exists() and d.parent.exists() and os.access(d.parent, os.W_OK):
            try:
                d.mkdir(parents=True, exist_ok=True)
                info(f"Created scripts directory: {d}")
                return d
            except OSError:
                continue
    return None


# ---------------------------------------------------------------------------
# Core install / uninstall logic
# ---------------------------------------------------------------------------
def install(project_root: pathlib.Path, port: int, dry_run: bool) -> bool:
    """
    Write the startup shim into the best available Rhino scripts directory.

    Returns True on success, False if no writable location was found.
    """
    scripts_dir = _find_scripts_dir()
    if scripts_dir is None:
        return False

    shim_path = scripts_dir / SHIM_FILENAME
    shim_content = _SHIM_TEMPLATE.format(
        project_root=str(project_root),
        port=port,
    )

    if dry_run:
        info(f"[dry-run] Would write shim to: {shim_path}")
        info("[dry-run] Shim content:")
        for line in shim_content.splitlines():
            print(f"    {line}")
        return True

    try:
        shim_path.write_text(shim_content, encoding="utf-8")
        shim_path.chmod(0o644)
        ok(f"Startup shim written: {shim_path}")
        return True
    except OSError as exc:
        error(f"Could not write shim to {shim_path}: {exc}")
        return False


def uninstall(dry_run: bool) -> None:
    """Remove any previously installed startup shim from all candidate dirs."""
    found_any = False
    for d in _candidate_dirs():
        shim_path = d / SHIM_FILENAME
        if shim_path.exists():
            found_any = True
            if dry_run:
                info(f"[dry-run] Would remove: {shim_path}")
            else:
                try:
                    shim_path.unlink()
                    ok(f"Removed: {shim_path}")
                except OSError as exc:
                    error(f"Could not remove {shim_path}: {exc}")
    if not found_any:
        info("No installed startup shim found — nothing to uninstall.")


# ---------------------------------------------------------------------------
# Manual installation instructions (fallback)
# ---------------------------------------------------------------------------
def _print_manual_instructions(project_root: pathlib.Path, port: int) -> None:
    header("Manual Installation Instructions")
    print()
    print("  Automatic installation could not find a writable Rhino scripts")
    print("  directory.  Follow these steps to install GOLEM-3DMCP manually:")
    print()
    print("  Option A — Rhino Python Script Editor (recommended)")
    print("  -------------------------------------------------------")
    print("  1. Open Rhinoceros 8.")
    print("  2. Type 'EditPythonScript' in the Rhino command bar and press Enter.")
    print("  3. In the editor, open the following file:")
    print(f"       {project_root / 'rhino_plugin' / 'startup.py'}")
    print("  4. Press the Run button (or F5).")
    print()
    print("  Option B — Rhino Startup Scripts (permanent auto-load)")
    print("  -------------------------------------------------------")
    print("  1. In Rhino: Tools > Options > RhinoScript (or Python Script).")
    print("  2. Under 'Startup scripts', click 'Add'.")
    print(f"  3. Select:  {project_root / 'rhino_plugin' / 'startup.py'}")
    print("  4. Click OK and restart Rhino.")
    print()
    print("  Option C — rhinocode CLI")
    print("  -------------------------------------------------------")
    print("  If rhinocode is on your PATH:")
    print(f"    rhinocode script \"{project_root / 'rhino_plugin' / 'startup.py'}\"")
    print()
    print("  Option D — Create the scripts directory manually, then re-run")
    print("  -------------------------------------------------------")
    print("    mkdir -p ~/.rhinocode/scripts")
    print(f"    python {pathlib.Path(__file__).resolve()}")
    print()
    print(f"  Server will bind to: 127.0.0.1:{port}")
    print()


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def _parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        prog="install_plugin",
        description=(
            "Install the GOLEM-3DMCP plugin into Rhino's startup scripts.\n\n"
            "Writes a small shim that auto-starts the GOLEM TCP server every "
            "time Rhino opens."
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    # Default project root: two directories up from this script
    default_root = pathlib.Path(__file__).parent.parent.resolve()
    parser.add_argument(
        "--project-root",
        type=pathlib.Path,
        default=default_root,
        metavar="PATH",
        help=f"GOLEM-3DMCP project root directory (default: {default_root})",
    )
    parser.add_argument(
        "--port",
        type=int,
        default=9876,
        metavar="PORT",
        help="TCP port the GOLEM server will listen on (default: 9876)",
    )
    parser.add_argument(
        "--uninstall",
        action="store_true",
        help="Remove the installed startup shim instead of installing it",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Preview what would be done without writing or deleting any files",
    )
    return parser.parse_args()


def main() -> None:
    args = _parse_args()
    project_root: pathlib.Path = args.project_root.resolve()

    print()
    print(_c("1", "GOLEM-3DMCP — Rhino Plugin Installer"))
    print("=" * 44)

    # -----------------------------------------------------------------------
    # Uninstall path
    # -----------------------------------------------------------------------
    if args.uninstall:
        header("Uninstalling startup shim ...")
        uninstall(dry_run=args.dry_run)
        print()
        return

    # -----------------------------------------------------------------------
    # Verify Rhino 8 is installed
    # -----------------------------------------------------------------------
    header("Step 1 — Checking Rhino 8 installation")
    rhino_found = RHINO8_APP.exists() or RHINOWIP_APP.exists()
    if RHINO8_APP.exists():
        ok(f"Rhino 8 found: {RHINO8_APP}")
    elif RHINOWIP_APP.exists():
        ok(f"RhinoWIP found: {RHINOWIP_APP}")
    else:
        warn("Rhino 8 not found at /Applications/Rhino 8.app/")
        warn("Continuing — the shim can still be installed for future use.")

    # -----------------------------------------------------------------------
    # Verify project root looks correct
    # -----------------------------------------------------------------------
    header("Step 2 — Verifying project root")
    startup_py = project_root / "rhino_plugin" / "startup.py"
    if startup_py.exists():
        ok(f"Project root verified: {project_root}")
    else:
        error(f"startup.py not found at {startup_py}")
        error("Check --project-root or run this script from the GOLEM-3DMCP directory.")
        sys.exit(1)

    # -----------------------------------------------------------------------
    # Install the shim
    # -----------------------------------------------------------------------
    header("Step 3 — Installing startup shim")
    info(f"Target port : {args.port}")

    success = install(project_root, args.port, args.dry_run)

    if not success:
        warn("Could not find or create a writable Rhino scripts directory.")
        _print_manual_instructions(project_root, args.port)
        sys.exit(1)

    # -----------------------------------------------------------------------
    # Summary
    # -----------------------------------------------------------------------
    header("Installation complete.")
    print()
    print("  Next steps:")
    print()
    print("  1. Open (or restart) Rhinoceros 8.")
    print("     GOLEM-3DMCP will start automatically.")
    print()
    print("  2. Verify the server is running:")
    print(f"       python {project_root / 'scripts' / 'test_connection.py'}")
    print()
    print("  3. If the auto-start did not work, load manually from Rhino:")
    print(f"       rhinocode script \"{startup_py}\"")
    print()
    if args.dry_run:
        print(_c("33", "  (dry-run mode — no files were actually written)"))
        print()


if __name__ == "__main__":
    main()
