"""Bridge: install the config-driven framework runtime (hooks/lib/skills/config).

The mustache layer (`bootstrap.py`) scaffolds the *team* (charter, roster cards,
trust matrix). This module installs the *runtime* — the genericised hooks, libs,
the lifecycle state machine, the `wave-lifecycle` skill, `framework.config.json`,
and the dispatcher wiring in `settings.json` — by invoking the framework's own
deterministic installer (`framework/install/bootstrap.py`) against the same
target. That keeps a single source of truth for the install logic (no duplicated
copy/merge code) and lets `2real-team init` lay down a complete, runnable
`.claude/` instead of templates alone.

The framework assets are bundled into the wheel under `_bundled/framework/`
(see `hatch_build.py`); in a source checkout they resolve to the repo-root
`framework/`. If neither is found the bridge degrades gracefully (returns None)
so `init` still completes its team scaffolding.

`--no-team` is always passed: the CLI already wrote its own roster cards, so the
framework installer lays down only hooks/lib/skills/config (commit-identity
enforcement stays opt-in — enable it later via the framework installer's roster
generation, which writes the `roster.json` the gate reads).
"""

from __future__ import annotations

import subprocess
import sys
from pathlib import Path

_PKG_DIR = Path(__file__).resolve().parent


def resolve_framework_root() -> Path | None:
    """Locate the framework root (the dir holding ``install/`` + ``assets/``).

    Prefers the wheel-bundled copy; falls back to the repo-checkout layout
    (``src/real_team`` → up 3 → repo root → ``framework``). Returns None when
    neither has a runnable ``install/bootstrap.py``.
    """
    candidates = [
        _PKG_DIR / "_bundled" / "framework",
        _PKG_DIR.parents[2] / "framework",
    ]
    for root in candidates:
        if (root / "install" / "bootstrap.py").is_file():
            return root
    return None


def install_framework(
    target: Path,
    *,
    owner: str | None = None,
    shell: str = "bash",
    reviewers: int | None = None,
    merge_model: str | None = None,
    install_config: Path | str | None = None,
    with_ontology: bool | None = None,
    dry_run: bool = False,
) -> subprocess.CompletedProcess | None:
    """Install the framework runtime into ``target`` via its own bootstrapper.

    ``install_config`` forwards a unified YAML install config so the
    bootstrapper resolves every install-time decision (ontology, pre_push,
    children, …) from the same file the CLI read. The subprocess always runs
    ``--non-interactive`` — it can never prompt.

    ``with_ontology`` is tri-state: ``True`` passes ``--with-ontology``,
    ``False`` passes ``--no-ontology``, and ``None`` (default) passes neither
    so the bootstrapper resolves ``ontology.enabled`` from the install config
    (flags > user YAML > shipped default ON). This keeps a CLI default from
    silently overriding a forwarded YAML.

    Returns the CompletedProcess (inspect ``.returncode`` / ``.stdout``), or
    None when the framework assets could not be located (caller should report a
    soft notice, not fail).
    """
    root = resolve_framework_root()
    if root is None:
        return None

    cmd: list[str] = [
        sys.executable,
        str(root / "install" / "bootstrap.py"),
        str(target),
        "--no-team",
        "--non-interactive",
        "--shell",
        shell,
    ]
    if install_config:
        cmd += ["--install-config", str(install_config)]
    if owner:
        cmd += ["--owner", owner]
    if reviewers is not None:
        cmd += ["--reviewers", str(reviewers)]
    if merge_model:
        cmd += ["--merge-model", merge_model]
    if with_ontology is True:
        cmd += ["--with-ontology"]
    elif with_ontology is False:
        cmd += ["--no-ontology"]
    if dry_run:
        cmd += ["--dry-run"]

    return subprocess.run(cmd, capture_output=True, text=True)


def uninstall_framework(
    target: Path,
    *,
    dry_run: bool = False,
    non_interactive: bool = True,
) -> subprocess.CompletedProcess | None:
    """Reverse a framework install in ``target`` via ``framework/install/uninstall.py``.

    The counterpart to :func:`install_framework`: locates the same framework root (wheel-bundled
    copy first, source checkout as fallback) and runs its deterministic uninstaller. Defaults to
    ``non_interactive=True`` so the packaged command never hangs on a prompt; pass
    ``non_interactive=False`` to let the uninstaller ask for confirmation on a TTY.

    Returns the CompletedProcess (inspect ``.returncode`` / ``.stdout``), or None when the
    framework assets could not be located (caller should report a soft notice, not fail).
    """
    root = resolve_framework_root()
    if root is None:
        return None

    cmd: list[str] = [sys.executable, str(root / "install" / "uninstall.py"), str(target)]
    if non_interactive:
        cmd += ["--non-interactive"]
    if dry_run:
        cmd += ["--dry-run"]
    return subprocess.run(cmd, capture_output=True, text=True)


def restore_framework(
    target: Path,
    *,
    dry_run: bool = False,
    non_interactive: bool = True,
) -> subprocess.CompletedProcess | None:
    """Restore a repo's pre-install originals via ``framework/install/restore.py``.

    Undoes the install's tracked changes to files it displaced/overwrote — the consented archive
    (moved originals) and the ``.bak`` backups of files it rewrote — WITHOUT removing the
    framework's own install set (that is :func:`uninstall_framework`'s job). Locates the same
    framework root (wheel-bundled copy first, source checkout as fallback) and runs the
    deterministic restore command. Defaults to ``non_interactive=True`` so the packaged command
    never hangs on a prompt; pass ``non_interactive=False`` to let it ask for confirmation on a TTY.

    Returns the CompletedProcess (inspect ``.returncode`` / ``.stdout``), or None when the framework
    assets could not be located (caller should report a soft notice, not fail).
    """
    root = resolve_framework_root()
    if root is None:
        return None

    cmd: list[str] = [sys.executable, str(root / "install" / "restore.py"), str(target)]
    if non_interactive:
        cmd += ["--non-interactive"]
    if dry_run:
        cmd += ["--dry-run"]
    return subprocess.run(cmd, capture_output=True, text=True)


def stage_install_framework(
    target: Path,
    *,
    branch: str | None = None,
    owner: str | None = None,
    shell: str = "bash",
    merge_model: str | None = None,
    install_config: Path | str | None = None,
    with_ontology: bool | None = None,
    expect: str | None = None,
    dry_run: bool = False,
    non_interactive: bool = True,
    return_to_original: bool = False,
) -> subprocess.CompletedProcess | None:
    """Stage a framework install onto a throwaway git branch via ``install/install_branch.py``.

    The git-native sibling of :func:`restore_framework`: rather than installing onto the working
    tree (undo later via the manifest), it creates a dedicated install branch off HEAD, runs the
    ordinary bootstrapper there, and commits the install as one commit — so the operator can
    ``git merge`` it when satisfied or ``git branch -D`` to walk away. Refuses (exit 1) in a
    non-git repo or dirty tree.

    The staging flags (``branch``/``dry_run``/``non_interactive``/``return_to_original``) are
    consumed by ``install_branch.py``; the install-shape flags (``owner``/``shell``/``merge_model``
    /``expect``/… — the same set :func:`install_framework` builds, plus ``expect``) pass THROUGH
    it to the bootstrapper that runs on the branch. ``--no-team`` is always forwarded (the CLI owns
    roster scaffolding), as in :func:`install_framework`.

    ``expect`` forwards ``--expect`` (``fresh``/``existing``/``any``) to the staged bootstrap's
    fresh-vs-existing gate. Staging by definition targets a repo that already has a HEAD commit
    (there is nothing to branch from otherwise), so callers should default this to ``"any"`` —
    the shipped bootstrapper default (``fresh``) refuses every realistic install-branch target.

    Returns the CompletedProcess, or None when the framework assets could not be located.
    """
    root = resolve_framework_root()
    if root is None:
        return None

    cmd: list[str] = [sys.executable, str(root / "install" / "install_branch.py"), str(target)]
    # Staging flags consumed by install_branch.py itself.
    if branch:
        cmd += ["--branch", branch]
    if dry_run:
        cmd += ["--dry-run"]
    if non_interactive:
        cmd += ["--non-interactive"]
    if return_to_original:
        cmd += ["--return"]
    # Install-shape flags that pass THROUGH to the bootstrapper run on the branch (mirrors
    # install_framework's flag construction).
    cmd += ["--no-team", "--shell", shell]
    if install_config:
        cmd += ["--install-config", str(install_config)]
    if owner:
        cmd += ["--owner", owner]
    if merge_model:
        cmd += ["--merge-model", merge_model]
    if with_ontology is True:
        cmd += ["--with-ontology"]
    elif with_ontology is False:
        cmd += ["--no-ontology"]
    if expect:
        cmd += ["--expect", expect]

    return subprocess.run(cmd, capture_output=True, text=True)