"""New command - create a new sandbox with worktree and container.

Migrated from commands/new.sh (1,343 lines) + lib/args.sh (281 lines).
Performs the following sequence:
  1. Parse arguments or run guided wizard
  2. Handle --last / --preset flags
  3. Resolve repository input (URL, local path, or shorthand)
  4. Generate branch name if not provided
  5. Validate inputs (git URL, network mode, working dir, sparse, mounts)
  6. Check API keys unless --skip-key-check
  7. Check image freshness
  8. Check Docker network capacity
  9. Clone/fetch bare repository
  10. Create worktree with optional sparse checkout
  11. Setup override file with mounts, network, Claude home, SSH, timezone
  12. Pre-populate foundry global skills
  13. Start containers via compose_up
  14. Register with proxy (if credential isolation)
  15. Copy configs to container
  16. Handle --copy paths
  17. Install pip requirements
  18. Apply network restrictions
  19. Save metadata and last command
  20. Prompt for IDE launch
  21. Attach to tmux session
"""

from __future__ import annotations

import os
import sys
from dataclasses import dataclass

import click
from click.core import ParameterSource

from foundry_sandbox.paths import (
    find_next_sandbox_name,
    repo_url_to_bare_path,
    resolve_ssh_agent_sock,
    sandbox_name as _helpers_sandbox_name,
)
from foundry_sandbox.utils import flag_enabled as _saved_flag_enabled
from foundry_sandbox.commands.new_setup import _SetupError, _new_setup, _rollback_new
from foundry_sandbox.commands.new_wizard import _guided_new
from foundry_sandbox.commands.new_resolver import (
    _resolve_repo_input,
    _generate_branch_name,
    _branch_exists_on_remote,
    _detect_remote_default_branch,
)
from foundry_sandbox.commands.new_validation import _validate_preconditions, _validate_working_dir, _validate_mounts
from foundry_sandbox.api_keys import has_opencode_key, has_zai_key
from foundry_sandbox.paths import derive_sandbox_paths
from foundry_sandbox.state import save_last_cast_new, save_cast_preset, load_last_cast_new, load_cast_preset, save_last_attach
from foundry_sandbox import tmux
from foundry_sandbox.utils import environment_scope, log_error, log_info, log_section, log_warn
from foundry_sandbox.validate import validate_sandbox_name


@dataclass
class NewDefaults:
    """Resolved default values for the ``new`` command parameters."""

    repo: str
    branch: str
    from_branch: str
    mounts: tuple[str, ...]
    copies: tuple[str, ...]
    network: str
    with_ssh: bool
    with_opencode: bool
    with_zai: bool
    wd: str
    sparse: bool
    pip_requirements: str
    allow_pr: bool
    pre_foundry: bool


def _apply_saved_new_defaults(
    saved: dict[str, object],
    *,
    explicit_params: set[str],
    repo: str,
    branch: str,
    from_branch: str,
    mounts: tuple[str, ...],
    copies: tuple[str, ...],
    network: str,
    with_ssh: bool,
    with_opencode: bool,
    with_zai: bool,
    wd: str,
    sparse: bool,
    pip_requirements: str,
    allow_pr: bool,
    pre_foundry: bool,
) -> NewDefaults:
    """Apply saved/preset values for parameters not explicitly set by the user."""
    if "repo" not in explicit_params:
        repo = str(saved.get("repo", "") or "")
    if "branch" not in explicit_params:
        branch = str(saved.get("branch", "") or "")
    if "from_branch" not in explicit_params:
        from_branch = str(saved.get("from_branch", "") or "")
    if "network" not in explicit_params:
        network = str(saved.get("network_mode", "") or "")
    if "wd" not in explicit_params:
        wd = str(saved.get("working_dir", "") or "")
    if "sparse" not in explicit_params:
        sparse = _saved_flag_enabled(saved.get("sparse", False))
    if "pip_requirements" not in explicit_params:
        pip_requirements = str(saved.get("pip_requirements", "") or "")
    if "allow_pr" not in explicit_params:
        allow_pr = _saved_flag_enabled(saved.get("allow_pr", False))
    if "pre_foundry" not in explicit_params:
        pre_foundry = _saved_flag_enabled(saved.get("pre_foundry", False))
    if "with_ssh" not in explicit_params:
        with_ssh = _saved_flag_enabled(saved.get("sync_ssh", False))
    if "with_opencode" not in explicit_params:
        with_opencode = _saved_flag_enabled(saved.get("enable_opencode", False))
    if "with_zai" not in explicit_params:
        with_zai = _saved_flag_enabled(saved.get("enable_zai", False))

    if "mounts" not in explicit_params:
        saved_mounts = saved.get("mounts", [])
        if isinstance(saved_mounts, list) and all(isinstance(v, str) for v in saved_mounts):
            mounts = tuple(saved_mounts)
    if "copies" not in explicit_params:
        saved_copies = saved.get("copies", [])
        if isinstance(saved_copies, list) and all(isinstance(v, str) for v in saved_copies):
            copies = tuple(saved_copies)

    return NewDefaults(
        repo=repo,
        branch=branch,
        from_branch=from_branch,
        mounts=mounts,
        copies=copies,
        network=network,
        with_ssh=with_ssh,
        with_opencode=with_opencode,
        with_zai=with_zai,
        wd=wd,
        sparse=sparse,
        pip_requirements=pip_requirements,
        allow_pr=allow_pr,
        pre_foundry=pre_foundry,
    )


def _load_and_apply_defaults(
    data: dict[str, object] | None,
    label: str,
    *,
    explicit_params: set[str],
    repo: str,
    branch: str,
    from_branch: str,
    mounts: tuple[str, ...],
    copies: tuple[str, ...],
    network: str,
    with_ssh: bool,
    with_opencode: bool,
    with_zai: bool,
    wd: str,
    sparse: bool,
    pip_requirements: str,
    allow_pr: bool,
    pre_foundry: bool,
) -> NewDefaults:
    """Load saved/preset data, validate it, echo a banner, and apply defaults.

    Args:
        data: Loaded JSON data (from last command or preset), or ``None``.
        label: Human-readable label for error/banner (e.g. ``"last command"``).

    Returns:
        A :class:`NewDefaults` with merged values.

    Raises:
        SystemExit: If *data* is ``None``.
    """
    if not data:
        log_error(f"No previous '{label}' found")
        sys.exit(1)
    click.echo()
    click.echo(f"Repeating {label}")
    click.echo()
    return _apply_saved_new_defaults(
        data,
        explicit_params=explicit_params,
        repo=repo,
        branch=branch,
        from_branch=from_branch,
        mounts=mounts,
        copies=copies,
        network=network,
        with_ssh=with_ssh,
        with_opencode=with_opencode,
        with_zai=with_zai,
        wd=wd,
        sparse=sparse,
        pip_requirements=pip_requirements,
        allow_pr=allow_pr,
        pre_foundry=pre_foundry,
    )


def _persist_sandbox_state(
    repo_url: str,
    branch: str,
    from_branch: str,
    wd: str,
    sparse: bool,
    pip_requirements: str,
    allow_pr: bool,
    pre_foundry: bool,
    network_mode: str,
    sync_ssh_enabled: bool,
    with_opencode: bool,
    with_zai: bool,
    mounts: tuple[str, ...],
    copies: tuple[str, ...],
    save_as: str,
    name: str,
) -> None:
    """Save last command, preset (if --save-as), and last attach state.

    Args:
        repo_url: Repository URL.
        branch: Sandbox branch name.
        from_branch: Base branch name.
        wd: Working directory.
        sparse: Whether sparse checkout is enabled.
        pip_requirements: Pip requirements path.
        allow_pr: Whether PR operations are allowed.
        pre_foundry: Whether to upgrade foundry-mcp to pre-release.
        network_mode: Network mode string.
        sync_ssh_enabled: Whether SSH sync is enabled.
        with_opencode: Whether OpenCode is enabled.
        with_zai: Whether ZAI is enabled.
        mounts: Mount specifications.
        copies: Copy specifications.
        save_as: Preset name to save as (empty to skip).
        name: Sandbox name.
    """
    save_last_cast_new(
        repo=repo_url,
        branch=branch,
        from_branch=from_branch or "",
        working_dir=wd or "",
        sparse=sparse,
        pip_requirements=pip_requirements or "",
        allow_pr=allow_pr,
        pre_foundry=pre_foundry,
        network_mode=network_mode or "",
        sync_ssh=sync_ssh_enabled,
        enable_opencode=with_opencode,
        enable_zai=with_zai,
        mounts=list(mounts),
        copies=list(copies),
    )

    save_last_attach(name)

    if save_as:
        save_cast_preset(
            preset_name=save_as,
            repo=repo_url,
            branch=branch,
            from_branch=from_branch or "",
            working_dir=wd or "",
            sparse=sparse,
            pip_requirements=pip_requirements or "",
            allow_pr=allow_pr,
            pre_foundry=pre_foundry,
            network_mode=network_mode or "",
            sync_ssh=sync_ssh_enabled,
            enable_opencode=with_opencode,
            enable_zai=with_zai,
            mounts=list(mounts),
            copies=list(copies),
        )


def _handle_new_ide_and_attach(
    name: str,
    worktree_path: str,
    container: str,
    wd: str,
    no_ide: bool,
    with_ide: str,
    ide_only: str,
) -> None:
    """Handle IDE launch and tmux attach after sandbox creation.

    Args:
        name: Sandbox name.
        worktree_path: Path to worktree directory.
        container: Container name prefix.
        wd: Working directory inside container.
        no_ide: Whether --no-ide flag was set.
        with_ide: Value of --with-ide option.
        ide_only: Value of --ide-only option.
    """
    skip_terminal = False

    if sys.stdin.isatty():
        from foundry_sandbox.ide import auto_launch_ide, prompt_ide_selection

        if no_ide:
            input("Press Enter to launch... ")
        elif with_ide:
            if auto_launch_ide(with_ide, worktree_path):
                if ide_only:
                    skip_terminal = True
                    click.echo()
                    click.echo(f"IDE launched. Run 'cast attach {name}' for terminal.")
                else:
                    input("Press Enter to launch terminal... ")
            else:
                input("Press Enter to launch... ")
        elif ide_only:
            prompt_ide_selection(worktree_path)
            skip_terminal = True
            click.echo()
            click.echo("  Run this in your IDE's terminal to connect:")
            click.echo()
            click.echo(f"    cast attach {name}")
            click.echo()
        else:
            ide_was_launched = prompt_ide_selection(worktree_path)
            if ide_was_launched:
                click.echo()
                click.echo("  Run this in your IDE's terminal to connect:")
                click.echo()
                click.echo(f"    cast attach {name}")
                click.echo()
                skip_terminal = True
            else:
                input("Press Enter to launch terminal... ")

    if not skip_terminal and sys.stdin.isatty():
        tmux.attach(name, f"{container}-dev-1", worktree_path, wd or "")


# ---------------------------------------------------------------------------
# Command Implementation
# ---------------------------------------------------------------------------


@click.command()
@click.argument("repo", required=False, default="")
@click.argument("branch", required=False, default="")
@click.argument("from_branch", required=False, default="")
@click.option("--last", is_flag=True, help="Repeat last sandbox creation")
@click.option("--preset", metavar="NAME", help="Use saved preset")
@click.option("--mount", "-v", "mounts", multiple=True, help="Mount host:container[:ro]")
@click.option("--copy", "-c", "copies", multiple=True, help="Copy host:container (once at creation)")
@click.option("--network", "-n", metavar="MODE", help="Network mode (limited, host-only, none)")
@click.option("--with-ssh", is_flag=True, help="Enable SSH agent forwarding")
@click.option("--with-opencode", is_flag=True, help="Enable OpenCode setup")
@click.option("--with-zai", is_flag=True, help="Enable ZAI Claude alias")
@click.option("--no-isolate-credentials", is_flag=True, help="Disable credential isolation")
@click.option("--wd", metavar="PATH", help="Working directory (relative)")
@click.option("--sparse", is_flag=True, help="Enable sparse checkout (requires --wd)")
@click.option("--pip-requirements", "-r", metavar="PATH", help="Install Python packages from requirements.txt")
@click.option("--allow-pr", "--with-pr", is_flag=True, help="Allow PR operations")
@click.option("--pre-foundry", is_flag=True, help="Upgrade foundry-mcp to pre-release in sandbox")
@click.option("--save-as", metavar="NAME", help="Save configuration as named preset")
@click.option("--with-ide", metavar="NAME", help="Launch IDE after creation")
@click.option("--ide-only", metavar="NAME", help="Launch IDE only (no terminal)")
@click.option("--no-ide", is_flag=True, help="Skip IDE prompt")
@click.option("--skip-key-check", is_flag=True, help="Skip API key validation")
@click.option("--allow-dangerous-mount", is_flag=True, help="Bypass credential directory protection")
@click.option("--from", "from_flag", metavar="BRANCH", help="Base branch (alias for from_branch arg)")
@click.option("--name", "name_override", metavar="NAME", help="Override auto-generated sandbox name")
@click.option("--anthropic-base-url", metavar="URL", help="Override Anthropic API base URL")
@click.pass_context
def new(
    ctx: click.Context,
    repo: str,
    branch: str,
    from_branch: str,
    last: bool,
    preset: str,
    mounts: tuple[str, ...],
    copies: tuple[str, ...],
    network: str,
    with_ssh: bool,
    with_opencode: bool,
    with_zai: bool,
    no_isolate_credentials: bool,
    wd: str,
    sparse: bool,
    pip_requirements: str,
    allow_pr: bool,
    pre_foundry: bool,
    save_as: str,
    with_ide: str,
    ide_only: str,
    no_ide: bool,
    skip_key_check: bool,
    allow_dangerous_mount: bool,
    from_flag: str,
    name_override: str,
    anthropic_base_url: str,
) -> None:
    """Create a new sandbox with worktree and container."""

    if last and preset:
        log_error("Options --last and --preset cannot be used together")
        sys.exit(1)

    # Handle --from flag
    if from_flag and not from_branch:
        from_branch = from_flag

    explicit_param_names = {
        "repo",
        "branch",
        "from_branch",
        "mounts",
        "copies",
        "network",
        "with_ssh",
        "with_opencode",
        "with_zai",
        "wd",
        "sparse",
        "pip_requirements",
        "allow_pr",
        "pre_foundry",
    }
    explicit_params = {
        name for name in explicit_param_names
        if ctx.get_parameter_source(name) == ParameterSource.COMMANDLINE
    }
    if ctx.get_parameter_source("from_flag") == ParameterSource.COMMANDLINE:
        explicit_params.add("from_branch")

    # No args mode - run guided wizard
    if not repo and not last and not preset:
        wiz = _guided_new()
        repo = wiz.repo
        branch = wiz.branch
        from_branch = wiz.from_branch
        wd = wiz.working_dir
        sparse = wiz.sparse
        pip_requirements = wiz.pip_requirements
        allow_pr = wiz.allow_pr
        pre_foundry = wiz.pre_foundry

    # Handle --last / --preset flags
    if last or preset:
        if last:
            saved_data = load_last_cast_new()
            label = "cast new' command"
        else:
            saved_data = load_cast_preset(preset)
            label = f"preset '{preset}'"
        _defaults = _load_and_apply_defaults(
            saved_data,
            label,
            explicit_params=explicit_params,
            repo=repo,
            branch=branch,
            from_branch=from_branch,
            mounts=mounts,
            copies=copies,
            network=network,
            with_ssh=with_ssh,
            with_opencode=with_opencode,
            with_zai=with_zai,
            wd=wd,
            sparse=sparse,
            pip_requirements=pip_requirements,
            allow_pr=allow_pr,
            pre_foundry=pre_foundry,
        )
        repo = _defaults.repo
        branch = _defaults.branch
        from_branch = _defaults.from_branch
        mounts = _defaults.mounts
        copies = _defaults.copies
        network = _defaults.network
        with_ssh = _defaults.with_ssh
        with_opencode = _defaults.with_opencode
        with_zai = _defaults.with_zai
        wd = _defaults.wd
        sparse = _defaults.sparse
        pip_requirements = _defaults.pip_requirements
        allow_pr = _defaults.allow_pr
        pre_foundry = _defaults.pre_foundry

    # Resolve repo input
    if not repo:
        click.echo("Error: Repository required")
        sys.exit(1)

    repo_url, repo_root, repo_display, current_branch = _resolve_repo_input(repo)

    if not repo_url:
        log_error(f"Not a git repository: {repo}")
        sys.exit(1)

    # Handle local repo defaults
    if repo_root:
        if not current_branch or current_branch == "HEAD":
            log_error("Repository is in a detached HEAD state; specify a base branch.")
            sys.exit(1)

        if not branch and not from_branch:
            if _branch_exists_on_remote(repo_root, current_branch):
                from_branch = current_branch
            else:
                from_branch = _detect_remote_default_branch(repo_root)
                log_warn(
                    f"Current branch '{current_branch}' not found on remote; "
                    f"using '{from_branch}' as base."
                )

    # Generate branch name if not provided
    if not branch:
        branch = _generate_branch_name(repo_url, from_branch or "main")
        from_branch = from_branch or "main"

    # Validate working directory
    if wd:
        ok, msg = _validate_working_dir(wd)
        if not ok:
            log_error(msg)
            sys.exit(1)
        wd = wd.lstrip("./")

    # Validate sparse requires wd
    if sparse and not wd:
        log_error("--sparse requires --wd to specify which directory to include")
        sys.exit(1)

    # Expand repo URL shorthand
    if not repo_url.startswith(("http://", "https://", "git@")) and "://" not in repo_url and not repo_url.startswith("/"):
        repo_url = f"https://github.com/{repo_url}"

    # Validate preconditions: git URL, API keys, copies, image, network
    isolate_credentials = not no_isolate_credentials
    _validate_preconditions(ctx, repo_url, copies, skip_key_check, isolate_credentials)

    # Setup network and SSH flags
    network_mode = network or os.environ.get("SANDBOX_NETWORK_MODE", "limited")
    sync_ssh = "1" if with_ssh else "0"
    ssh_mode = "always" if with_ssh else "disabled"

    # Resolve SSH agent socket
    ssh_agent_sock = ""
    if sync_ssh == "1":
        ssh_agent_sock = resolve_ssh_agent_sock()
        if not ssh_agent_sock:
            log_warn("SSH agent not detected; SSH forwarding disabled (agent-only mode).")
            sync_ssh = "0"
    sync_ssh_enabled = sync_ssh == "1"

    # Generate sandbox name
    bare_path = repo_url_to_bare_path(repo_url)
    if name_override:
        name = name_override
    else:
        name = _helpers_sandbox_name(bare_path, branch)

    valid_name, name_error = validate_sandbox_name(name)
    if not valid_name:
        log_error(f"Invalid sandbox name '{name}': {name_error}")
        sys.exit(1)

    # Auto-generate unique name for --last / --preset.
    # Save the base name and branch before suffixing so retries can
    # re-derive cleanly from the originals.
    base_name = name
    base_branch = branch
    if last or preset:
        name = find_next_sandbox_name(base_name)
        if name != base_name:
            suffix = name[len(base_name):]
            branch = f"{base_branch}{suffix}"

    # Atomically claim the sandbox name by creating the config directory.
    # This eliminates the TOCTOU race between find_next_sandbox_name() and
    # the actual creation.  The config dir (not worktree dir) is the claim
    # marker because `git worktree add` requires its target to not exist.
    # The existing _rollback_new() already cleans up config dirs on failure.
    paths = derive_sandbox_paths(name)
    worktree_path = paths.worktree_path
    container = paths.container_name
    override_file = paths.override_file
    claude_config_path = paths.claude_config_path

    _MAX_NAME_RETRIES = 5
    _seen_names = {name}
    for _attempt in range(_MAX_NAME_RETRIES):
        try:
            os.makedirs(claude_config_path, exist_ok=False)
            break  # Successfully claimed the name
        except FileExistsError:
            if not (last or preset):
                log_error(f"Sandbox name collision: '{name}' already exists")
                sys.exit(1)
            # Retry with next suffix for --last / --preset
            name = find_next_sandbox_name(base_name)
            if name in _seen_names:
                log_error(f"Name generation loop: '{name}' already tried")
                sys.exit(1)
            _seen_names.add(name)
            if name != base_name:
                suffix = name[len(base_name):]
                branch = f"{base_branch}{suffix}"
            else:
                branch = base_branch
            paths = derive_sandbox_paths(name)
            worktree_path = paths.worktree_path
            container = paths.container_name
            override_file = paths.override_file
            claude_config_path = paths.claude_config_path
    else:
        log_error(f"Could not claim a unique sandbox name after {_MAX_NAME_RETRIES} attempts")
        sys.exit(1)

    # Validate mount paths
    if not allow_dangerous_mount:
        ok, msg = _validate_mounts(mounts)
        if not ok:
            log_error(msg)
            click.echo("Use --allow-dangerous-mount to bypass this check (not recommended)")
            sys.exit(1)

    # Setup opt-in tool enablement
    enable_opencode_flag = "0"
    enable_zai_flag = "0"

    if with_opencode:
        if has_opencode_key():
            enable_opencode_flag = "1"
        else:
            log_warn("OpenCode requested but auth file not found; skipping OpenCode setup.")

    if with_zai:
        if has_zai_key():
            enable_zai_flag = "1"
        else:
            log_error("--with-zai requires ZHIPU_API_KEY to be set in your environment.")
            click.echo("  Set it with: export ZHIPU_API_KEY=<your-key>")
            sys.exit(1)

    # Build scoped env updates
    _scope_env: dict[str, str] = {
        "SANDBOX_ENABLE_OPENCODE": enable_opencode_flag,
        "SANDBOX_ENABLE_ZAI": enable_zai_flag,
    }
    if enable_zai_flag != "1":
        _scope_env["ZHIPU_API_KEY"] = ""

    # Save and restore environment around mutations
    with environment_scope(_scope_env):
        # Start creation
        click.echo()
        click.echo(f"Setting up your sandbox: {name}")
        log_section("Repository")

        # -- Begin resource creation (wrapped for rollback on failure) --
        try:
            _new_setup(
                repo_url=repo_url,
                bare_path=bare_path,
                worktree_path=worktree_path,
                branch=branch,
                from_branch=from_branch or "",
                sparse=sparse,
                wd=wd or "",
                claude_config_path=claude_config_path,
                override_file=override_file,
                name=name,
                container=container,
                mounts=list(mounts),
                copies=list(copies),
                allow_dangerous_mount=allow_dangerous_mount,
                network_mode=network_mode or "",
                sync_ssh_enabled=sync_ssh_enabled,
                ssh_agent_sock=ssh_agent_sock,
                ssh_mode=ssh_mode,
                isolate_credentials=isolate_credentials,
                allow_pr=allow_pr,
                pip_requirements=pip_requirements or "",
                pre_foundry=pre_foundry,
                enable_opencode_flag=enable_opencode_flag,
                enable_zai_flag=enable_zai_flag,
                anthropic_base_url=anthropic_base_url or "",
            )
        except _SetupError as exc:
            log_error(str(exc))
            log_info("Cleaning up partial sandbox resources...")
            _rollback_new(worktree_path, claude_config_path, container, override_file)
            sys.exit(1)
        except SystemExit:
            raise
        except Exception as exc:
            log_error(f"Sandbox creation failed: {exc}")
            # Surface Docker Compose stderr (CalledProcessError.stderr) so CI
            # logs show the actual failure reason instead of just the exit code.
            stderr = getattr(exc, "stderr", None)
            if stderr:
                import textwrap
                log_error(textwrap.indent(stderr.strip(), "  "))
            log_info("Cleaning up partial sandbox resources...")
            _rollback_new(worktree_path, claude_config_path, container, override_file)
            sys.exit(1)

        # Save state: last command, preset, last attach
        _persist_sandbox_state(
            repo_url=repo_url,
            branch=branch,
            from_branch=from_branch,
            wd=wd,
            sparse=sparse,
            pip_requirements=pip_requirements,
            allow_pr=allow_pr,
            pre_foundry=pre_foundry,
            network_mode=network_mode,
            sync_ssh_enabled=sync_ssh_enabled,
            with_opencode=with_opencode,
            with_zai=with_zai,
            mounts=mounts,
            copies=copies,
            save_as=save_as,
            name=name,
        )

        # Success message
        click.echo()
        click.echo("✓ Your sandbox is ready!")
        click.echo()
        click.echo(f"  Sandbox    {name}")
        click.echo(f"  Worktree   {worktree_path}")
        click.echo()
        click.echo("  Commands:")
        click.echo(f"    cast attach {name}   - reconnect later")
        click.echo("    cast reattach       - reconnect (auto-detects sandbox in worktree)")
        click.echo(f"    cast stop {name}     - pause the sandbox")
        click.echo(f"    cast destroy {name}  - remove completely")
        click.echo("    cast repeat         - repeat this setup")
        click.echo()

        # IDE launch and tmux attach
        _handle_new_ide_and_attach(
            name, str(worktree_path), container, wd, no_ide, with_ide, ide_only,
        )
