"""Build script for compiling the launcher into a platform-specific binary.

This module is invoked by CI workflows via ``python -m launcher.build`` to
produce compiled ``.so`` extension modules (one per platform) and a standalone
wrapper script.  The compiled module is produced with Nuitka in ``--module``
mode so that the resulting ``.so`` can be imported directly by the wrapper.

Output directory: ``launcher/dist/``

Expected artifacts per platform:
    sf-linux-x86_64.so   + sf (wrapper script)   -- Linux x86_64
    sf-linux-arm64.so    + sf (wrapper script)   -- Linux arm64
    sf-darwin-x86_64.so                            -- macOS Intel
    sf-darwin-arm64.so                             -- macOS Apple Silicon

Usage:
    python -m launcher.build --release --version 6.5.3
"""

from __future__ import annotations

import argparse
import os
import platform
import re
import shutil
import subprocess
import sys
from pathlib import Path

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

# Repository root is two levels up from this file (launcher/build.py -> repo root).
REPO_ROOT: Path = Path(__file__).resolve().parent.parent

# Directory where the launcher package lives.
LAUNCHER_PKG_DIR: Path = REPO_ROOT / "launcher"

# Output directory for compiled artifacts.
DIST_DIR: Path = LAUNCHER_PKG_DIR / "dist"

# The __init__.py file whose __version__ will be updated.
VERSION_FILE: Path = LAUNCHER_PKG_DIR / "__init__.py"


# ---------------------------------------------------------------------------
# Platform detection
# ---------------------------------------------------------------------------


def get_os_name() -> str:
    """Return the normalised OS name (``linux`` or ``darwin``)."""
    system = platform.system().lower()
    if system == "linux":
        return "linux"
    if system == "darwin":
        return "darwin"
    raise RuntimeError(f"Unsupported operating system: {system}")


def get_arch() -> str:
    """Return the normalised CPU architecture (``x86_64`` or ``arm64``)."""
    machine = platform.machine().lower()
    if machine in ("x86_64", "amd64"):
        return "x86_64"
    if machine in ("arm64", "aarch64"):
        return "arm64"
    raise RuntimeError(f"Unsupported architecture: {machine}")


def get_platform_suffix() -> str:
    """Return the platform suffix used in artifact names (e.g. ``linux-x86_64``)."""
    return f"{get_os_name()}-{get_arch()}"


# ---------------------------------------------------------------------------
# Version management
# ---------------------------------------------------------------------------


def update_version(version: str) -> None:
    """Rewrite ``__version__`` in ``launcher/__init__.py``.

    Parameters
    ----------
    version:
        The new version string (without a ``v`` prefix).
    """
    content = VERSION_FILE.read_text(encoding="utf-8")
    new_content = re.sub(
        r'^__version__\s*=\s*".*"',
        f'__version__ = "{version}"',
        content,
        flags=re.MULTILINE,
    )
    if new_content == content:
        print(f"[warn] Version pattern not found in {VERSION_FILE}; skipping update.")
        return
    VERSION_FILE.write_text(new_content, encoding="utf-8")
    print(f"[version] Updated {VERSION_FILE} to {version}")


# ---------------------------------------------------------------------------
# Nuitka compilation
# ---------------------------------------------------------------------------


def _ensure_nuitka() -> None:
    """Install Nuitka if it is not already available."""
    try:
        import nuitka  # noqa: F401  # pyright: ignore[reportMissingImports]
    except ImportError:
        print("[build] Installing Nuitka...")
        subprocess.check_call(
            [sys.executable, "-m", "pip", "install", "nuitka", "ordered-set"],
            stdout=sys.stdout,
            stderr=sys.stderr,
        )


def compile_module() -> Path:
    """Compile the ``launcher`` package into a single ``.so`` extension module.

    Returns the path to the produced ``.so`` file inside the build work
    directory (before it is copied to ``launcher/dist/``).
    """
    _ensure_nuitka()

    build_dir = REPO_ROOT / "build" / "nuitka_build"
    build_dir.mkdir(parents=True, exist_ok=True)

    cmd = [
        sys.executable,
        "-m",
        "nuitka",
        "--module",
        str(LAUNCHER_PKG_DIR),
        f"--output-dir={build_dir}",
        "--remove-output",
        "--no-pyi-file",
    ]

    print(f"[build] Running: {' '.join(cmd)}")
    subprocess.check_call(cmd, cwd=str(REPO_ROOT), stdout=sys.stdout, stderr=sys.stderr)

    # Nuitka produces a file like ``launcher.cpython-312-x86_64-linux-gnu.so``
    # or ``launcher.cpython-312-darwin.so`` inside build_dir.
    so_files = list(build_dir.glob("launcher*.so"))
    if not so_files:
        # On some Nuitka versions the output may be a .pyd on Windows, but we
        # only target Linux and macOS.
        raise FileNotFoundError(
            f"Nuitka compilation succeeded but no .so file found in {build_dir}. Contents: {list(build_dir.iterdir())}"
        )

    # Take the first (and typically only) match.
    return so_files[0]


# ---------------------------------------------------------------------------
# Wrapper script generation
# ---------------------------------------------------------------------------

# The wrapper is a small self-contained Python script that:
#   1. Adds the directory containing the .so to sys.path.
#   2. Imports the compiled launcher module.
#   3. Delegates to launcher.cli:main().
#
# It is installed to ~/.tribunal/bin/tribunal by install.sh and used to
# bootstrap the launcher without requiring a full pip install.

WRAPPER_TEMPLATE = """\
#!/usr/bin/env python3
\"\"\"Tribunal launcher wrapper.

This script bootstraps the compiled launcher module (.so) that lives alongside
it in the same directory and delegates to the CLI entry point.
\"\"\"
import os
import sys

# Ensure the directory containing this script (and the .so) is on sys.path.
_here = os.path.dirname(os.path.abspath(__file__))
if _here not in sys.path:
    sys.path.insert(0, _here)

# Quick --version shortcut so install.sh can verify the binary.
if "--version" in sys.argv:
    from launcher import __version__
    print(f"Tribunal v{{__version__}}")
    sys.exit(0)

from launcher.cli import main

main()
"""


def write_wrapper(dest: Path) -> None:
    """Write the standalone wrapper script to *dest* and make it executable."""
    dest.write_text(WRAPPER_TEMPLATE, encoding="utf-8")
    dest.chmod(0o755)
    print(f"[build] Wrote wrapper: {dest}")


# ---------------------------------------------------------------------------
# Main build orchestration
# ---------------------------------------------------------------------------


def build(*, release: bool = False, version: str | None = None) -> None:
    """Run the full build pipeline.

    Parameters
    ----------
    release:
        When ``True``, perform a release-grade build (currently identical to a
        normal build, but kept for forward-compatibility with signing or
        optimisation flags).
    version:
        If provided, update ``launcher/__init__.py`` to this version before
        compiling.
    """
    if version:
        update_version(version)

    platform_suffix = get_platform_suffix()
    print(f"[build] Platform: {platform_suffix}")
    print(f"[build] Release mode: {release}")

    # Ensure a clean dist directory.
    if DIST_DIR.exists():
        shutil.rmtree(DIST_DIR)
    DIST_DIR.mkdir(parents=True)

    # Compile the launcher package into a .so module.
    so_path = compile_module()

    # Copy the .so to dist/ with the expected artifact name.
    artifact_name = f"sf-{platform_suffix}.so"
    dest_so = DIST_DIR / artifact_name
    shutil.copy2(str(so_path), str(dest_so))
    dest_so.chmod(0o755)
    print(f"[build] Artifact: {dest_so}  ({dest_so.stat().st_size:,} bytes)")

    # Generate the standalone wrapper script (named ``sf``).
    wrapper_path = DIST_DIR / "sf"
    write_wrapper(wrapper_path)

    # Summary
    print()
    print("[build] Build complete. Artifacts in launcher/dist/:")
    for f in sorted(DIST_DIR.iterdir()):
        print(f"  {f.name}  ({f.stat().st_size:,} bytes)")


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="python -m launcher.build",
        description="Compile the Tribunal launcher into a platform-specific binary.",
    )
    parser.add_argument(
        "--release",
        action="store_true",
        default=False,
        help="Perform a release build.",
    )
    parser.add_argument(
        "--version",
        type=str,
        default=None,
        help="Set the version string in launcher/__init__.py before building.",
    )
    return parser


def main(argv: list[str] | None = None) -> None:
    """CLI entry point."""
    parser = _build_parser()
    args = parser.parse_args(argv)

    try:
        build(release=args.release, version=args.version)
    except Exception as exc:
        print(f"[build] ERROR: {exc}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()
