'''opencos.utils.ssh_helper -- remote EDA execution via SSH

Invoked when `eda` is called with --ssh <user>@<host>[:/optional/path].

The :/path is optional.  When omitted (or when you want a neutral scratch space),
the remote sandbox is placed under the user's home directory on the remote host:
  ~/eda.work/<uuid>.ssh/
When :/path is given it is used as the base instead:
  /path/eda.work/<uuid>.ssh/
Either way the sandbox is ephemeral -- created, used, and deleted per job.

High-level flow:
  1. parse_ssh_arg()           -- split "user@host[:/path]" into (user, host, path|None)
  2. _get_remote_home()        -- resolve $HOME on remote (used when no path given)
  3. remote_check_eda()        -- verify `eda` is reachable on the remote host
  4. remote_check_tool()       -- verify the requested --tool exe is on the remote
  5. _stage_files()            -- collect source files → flat local staging dir via ExportHelper
  6. _ship_to_remote()         -- tar-pipe staging dir to a fresh ephemeral remote job dir
  7. run_eda_remote()          -- cd remote_job_dir && eda <args>; stream output
  8. _fetch_results()          -- tar-pipe remote eda.work/ back to local eda.work/
  9. _cleanup_remote()         -- ssh rm -rf remote_job_dir  (ephemeral, always removed)
'''

import json
import os
import re
import shlex
import subprocess
import tempfile
import uuid

from opencos import util, files, eda_config, __version__
from opencos.util import Colors
from opencos.export_helper import ExportHelper
from opencos.utils import subprocess_helpers
from opencos.utils.status_constants import EDA_SSH_ERROR_CODE, EDA_COMMAND_OR_ARGS_ERROR, \
    EDA_COMMAND_MISSING_TOP


# ---------------------------------------------------------------------------
# Parsing
# ---------------------------------------------------------------------------

SSH_ARG_RE = re.compile(
    r'^(?P<user>[^@]+)@(?P<host>[^:/]+)(?::(?P<path>.+))?$'
)

SSH_EXE = files.safe_shutil_which('ssh')
TAR_EXE = files.safe_shutil_which('tar')

MIN_VERSION = '0.4.4'

# hold path to remote eda, remote eda version, remote known tools
# per user@host entry:
REMOTE_CACHE = {}

def _init_remote_cache(user:str, host: str) -> str:
    '''Creates a REMOTE_CACHE entry if it doesn't already exist'''
    key = f'{user}@{host}'
    if key not in REMOTE_CACHE:
        REMOTE_CACHE[key] = {
            'eda': {
                'exe': '',
                'version': '',
            },
            'tools': {
            },
        }
    return key

def _version_ok(ver: str) -> bool:
    '''Checks if version can handle ssh remote flow'''
    if not ver:
        return False

    parts = ver.split('.')
    if parts and len(parts) >= 3:
        if any((int(parts[0]) > 0,
                int(parts[1]) > 4,
                int(parts[1]) == 4 and int(parts[2]) >= 4)):
            # 1+.x.x, 0.5.x, or 0.4.4+
            return True

    return False


def parse_ssh_arg(ssh_arg: str) -> tuple:
    '''Parse "user@host[:/some/path]" into (user, host, path_or_None).

    The :/path portion is optional.  When omitted, path is returned as None
    and the caller should use the remote home directory instead.

    Raises ValueError with a human-readable message on bad format.
    '''
    m = SSH_ARG_RE.match(ssh_arg.strip())
    if not m:
        raise ValueError(
            f'--ssh value must be in the form user@host or user@host:/path, got: {ssh_arg!r}'
        )
    user = m.group('user')
    host = m.group('host')
    path = m.group('path')  # None when no :/path was given
    if path is not None and not path.startswith('/'):
        raise ValueError(
            f'--ssh path must be absolute (start with /), got: {path!r}'
        )
    return user, host, path


# ---------------------------------------------------------------------------
# SSH helpers
# ---------------------------------------------------------------------------

def _prepend_setup_cmd(cmd: str, *setup_cmds) -> str:
    '''Prepends the cmd with setup_cmds'''
    if not setup_cmds:
        return cmd
    return ' && '.join(list(filter(None, setup_cmds)) + [cmd])

def _ssh_base(user: str, host: str) -> list:
    '''Returns the base ssh command list for a given user@host.'''
    if not SSH_EXE:
        util.error('"ssh" not present in PATH, cannot use "eda --ssh=SSH_VALUE")',
                   error_code=EDA_SSH_ERROR_CODE)
        return []
    return [
        SSH_EXE,
        '-o', 'BatchMode=yes',
        '-o', 'ConnectTimeout=10',
        f'{user}@{host}',
    ]


def _ssh_cmd(user: str, host: str, remote_cmd: str) -> list:
    '''Build an SSH command that runs remote_cmd via "bash -lc" so ~/.bashrc is sourced.

    This ensures tools added to PATH in .bashrc are available on the remote.
    '''
    assert _ssh_base
    return _ssh_base(user, host) + [f'bash -lc {_shell_quote(remote_cmd)}']


def _run_local(
        cmd: list, *, capture: bool = False, check: bool = True
) -> subprocess.CompletedProcess:
    '''Run a local command. When capture=False output streams to the terminal.'''
    util.debug(f'ssh_helper: running local cmd: {" ".join(str(c) for c in cmd)}')
    kwargs = subprocess_helpers.get_kwargs(work_dir='', shell=False, is_subprocess_popen=False)
    if capture:
        kwargs['capture_output'] = True
        kwargs['text'] = True
    # Note - we do not have subprocess_helpers PID management for subprocess.run where
    # we capture text from STDOUT.
    return subprocess.run(cmd, check=check, **kwargs)


def remote_ensure_path(user: str, host: str, path: str) -> None:
    '''Create <path> on the remote if it does not exist.

    Raises RuntimeError if the remote is unreachable or mkdir fails.
    '''
    util.info(f'ssh_helper: ensuring remote path exists: {user}@{host}:{path}')
    cmd = _ssh_cmd(user, host, f'mkdir -p {path!r}')
    try:
        _run_local(cmd, capture=True)
    except subprocess.CalledProcessError as e:
        util.error(
            f'ssh_helper: cannot reach {user}@{host} or create {path}: {e}',
            error_code=EDA_SSH_ERROR_CODE
        )


def _get_remote_home(user: str, host: str) -> str:
    '''Return the absolute home directory path on the remote host.

    Runs "echo $HOME" via bash -lc so the shell expands the variable.
    Raises RuntimeError if the remote is unreachable or $HOME is empty.
    '''
    cmd = _ssh_cmd(user, host, 'echo $HOME')
    try:
        result = _run_local(cmd, capture=True)
        home = result.stdout.strip()
    except subprocess.CalledProcessError as e:
        util.error(
            f'ssh_helper: cannot reach {user}@{host} to determine $HOME: {e}',
            error_code=EDA_SSH_ERROR_CODE
        )
    if not home or not home.startswith('/'):
        util.error(
            f'ssh_helper: unexpected $HOME value on {user}@{host}: {home!r}',
            error_code=EDA_SSH_ERROR_CODE
        )
    return home


def remote_check_eda(user: str, host: str, setup_cmd: str = '') -> str:
    '''Returns the remote path to the eda executable, or empty string if not found.'''
    ret = ''
    which_cmd = _prepend_setup_cmd('which eda 2>/dev/null || echo ""', setup_cmd)
    cmd = _ssh_cmd(user, host, which_cmd)
    try:
        result = _run_local(cmd, capture=True)
        ret = _last_nonempty_line(result.stdout)
    except subprocess.CalledProcessError:
        ret = ''

    key = _init_remote_cache(user, host)
    REMOTE_CACHE[key]['eda']['exe'] = ret
    return ret


def remote_eda_version(user: str, host: str, setup_cmd: str = '') -> str:
    '''Get remote eda version'''
    eda_ver_cmd = _prepend_setup_cmd('eda --version', setup_cmd)
    cmd = _ssh_cmd(user, host, eda_ver_cmd)
    ver = ''
    try:
        result = _run_local(cmd, capture=True)
        ver = _last_nonempty_line(result.stdout)
        if not ver:
            util.warning(
                f'ssh_helper: {user}@{host} -- eda version: {ver=} -- from {result.stdout}'
            )
    except subprocess.CalledProcessError as e:
        util.warning(f'ssh_helper: {user}@{host} -- eda version: {ver=} -- has error ({e})')
        ver = ''

    if not ver:
        util.warning(f'ssh_helper: {user}@{host} -- eda version: {ver=} -- from {result.stdout}')
        return ver

    parts = ver.split()
    if len(parts) < 2:
        return ver

    ver = parts[1]
    key = _init_remote_cache(user, host)
    REMOTE_CACHE[key]['eda']['version'] = ver
    util.debug(f'ssh_helper: {user}@{host} -- eda version: {ver}')
    return ver


def get_remote_tools(user: str, host: str, setup_cmd: str = '') -> dict:
    '''
    Runs "eda show-tools" on remote (if opencos-eda >= 0.4.4) to get JSON

    data of all tools/commands supported. If show-tools fails, then we will use
    "eda --help" and attempt to parse from STDOUT.

    Also sets in REMOTE_KNOWN_TOOLS
    '''
    key = _init_remote_cache(user, host)

    data = None
    if 'tools' in REMOTE_CACHE[key].get('tools', {}):
        data = REMOTE_CACHE[key]['tools']
    else:
        ver = remote_eda_version(user, host, setup_cmd)
        show_tools_works = False
        if ver:
            show_tools_works = _version_ok(ver)
            if __version__ != ver:
                util.warning(
                    f'Local "eda" version is {Colors.byellow}{__version__}{Colors.yellow}',
                    f', remote is {Colors.byellow}{ver}')

        if show_tools_works:
            try:
                cmd = _ssh_cmd(
                    user, host,
                    _prepend_setup_cmd('eda show-tools --quiet --json-to-stdout', setup_cmd)
                )
                result = _run_local(cmd, capture=True)
                show_tools_data = json.loads(result.stdout)
            except Exception as e:
                util.error(f'Cannot get tools information on remote {key}: {cmd} ({e})')

            if 'tools' in show_tools_data:
                REMOTE_CACHE[key]['tools'] = show_tools_data['tools'].copy()
                data = REMOTE_CACHE[key]['tools']
            else:
                util.error(
                    f'Cannot find tool information from: {key}: {cmd}; got -- {show_tools_data}'
                )

        else:
            util.error(f'SSH requires eda >= 0.4.4 to get tool information on remote {key}')
            data = None

    return data


def remote_check_tool(user: str, host: str, tool_name: str, setup_cmd: str = '') -> str:
    '''Returns the remote exe path for tool_name (first exe name in PATH), or empty string.

    Uses a simple heuristic: the tool exe has the same name as the tool key.
    For tools like slang_yosys, we check "yosys".
    '''
    # TODO(drew): we can likely use `eda show-tools [--args]` to get this in a JSON
    # usable format from the remote host

    data = get_remote_tools(user, host, setup_cmd)
    exe = None
    if tool_name in data:
        exe = data[tool_name].get('path', '')

    if not exe:
        util.error(f'tool {tool_name} does not appear to exist on remote {user}@{host}')
        return ''

    return exe


def _last_nonempty_line(text: str) -> str:
    '''Return the last non-empty line from text (setup commands may produce leading output).'''
    for line in reversed(text.splitlines()):
        stripped = line.strip()
        if stripped:
            return stripped
    return ''


# ---------------------------------------------------------------------------
# File staging (ExportHelper → flat local dir)
# ---------------------------------------------------------------------------

def _get_target_name(cmd_obj) -> str:
    '''Gets member 'target' or args['top'], returns empty str otherwise'''
    target = getattr(cmd_obj, 'target', '')
    if not target:
        target = cmd_obj.args.get('top', '')
    return target

def _stage_files(cmd_obj, staging_dir: str) -> int:
    '''Collect all source files into staging_dir using ExportHelper.

    ExportHelper creates a flat self-contained directory: all source files by
    basename, with `include` paths rewritten to be relative (no subdirectory
    prefixes), plus a new DEPS.yml with incdirs:[.] and the target key.

    Returns 0 on success, non-zero on error.
    '''
    target = _get_target_name(cmd_obj)
    if not target:
        target = cmd_obj.args.get('top', '')
        util.warning(f'ssh_helper: target not found, using "top" value: {target}')

    if not target:
        return util.error(
            'ssh_helper: cannot determine target for staging (self.target is empty).',
            error_code=EDA_COMMAND_OR_ARGS_ERROR
        )

    if not cmd_obj.args.get('top', ''):
        return util.error(
            'ssh_helper: --top not set after DEPS parsing, cannot stage files.'
            ' Ensure your DEPS.yml target has a `top:` field.',
            error_code=EDA_COMMAND_MISSING_TOP
        )

    util.info(f'ssh_helper: staging files for {target=} → {staging_dir}')

    # TODO(drew): confirm this works, ExportHelper may need allow-listed args per tool
    export = ExportHelper(
        cmd_design_obj=cmd_obj,
        eda_command=cmd_obj.command_name,
        out_dir=staging_dir,
        target=target,
    )
    # staging_dir already exists (created by TemporaryDirectory); ExportHelper's
    # create_out_dir() would try to mkdir it, but write_files_to_out_dir() and
    # create_deps_yml_in_out_dir() work fine with an existing directory.
    export.write_files_to_out_dir()
    export.create_deps_yml_in_out_dir()

    staged = os.listdir(staging_dir)
    util.info(f'ssh_helper: staged {len(staged)} files: {staged}')
    return 0


# ---------------------------------------------------------------------------
# File transfer (tar+pipe over SSH)
# ---------------------------------------------------------------------------

def _ship_to_remote(user: str, host: str, staging_dir: str, remote_job_dir: str) -> int:
    '''Ship staging_dir contents to a fresh remote_job_dir via tar+ssh pipe.

    Creates remote_job_dir on the remote host, then streams a tar archive of
    staging_dir over SSH so no intermediate copy is needed.

    Returns 0 on success, non-zero on error.
    '''
    util.info(f'ssh_helper: shipping files to {user}@{host}:{remote_job_dir}')
    try:
        remote_ensure_path(user, host, remote_job_dir)
    except RuntimeError as e:
        return util.error(str(e), error_code=EDA_SSH_ERROR_CODE)

    if not TAR_EXE:
        util.error(
            '"tar" is not present in PATH, cannot use ssh to send file to remote',
            error_code=EDA_SSH_ERROR_CODE
        )

    # --no-xattrs: macOS bsdtar would otherwise embed LIBARCHIVE.xattr.* PAXHEADER entries
    # for files that carry extended attributes (e.g. com.apple.provenance), which causes
    # harmless but noisy "Ignoring unknown extended header keyword" warnings when Linux tar
    # extracts them.  The flag is a no-op on Linux tar, so safe to pass always.
    tar_cmd = [TAR_EXE, '--no-xattrs', '-C', staging_dir, '-czf', '-', '.']
    # TODO(drew): might need to get the remote's TAR_EXE, currently assumes 'tar'
    ssh_cmd = _ssh_base(user, host) + [f'tar -C {remote_job_dir!r} -xzf -']

    # TODO(drew): lost support for --subprocess-timeout and --subprocess-progress-timeout.
    # pylint: disable=consider-using-with
    tar_proc_kwargs = subprocess_helpers.get_kwargs(work_dir='')
    tar_proc_kwargs['stdout'] = subprocess.PIPE
    tar_proc = subprocess.Popen(tar_cmd, **tar_proc_kwargs)
    subprocess_helpers.add_running_parent_pid(tar_proc.pid)

    ssh_proc_kwargs = subprocess_helpers.get_kwargs(work_dir='', is_subprocess_popen=False)
    ssh_proc_kwargs.update({
        'stdin': tar_proc.stdout,
    })
    ssh_proc = subprocess.run(ssh_cmd, **ssh_proc_kwargs, check=False)
    tar_proc.stdout.close()
    tar_proc.wait()
    subprocess_helpers.remove_completed_parent_pid(tar_proc.pid)

    if tar_proc.returncode != 0 or ssh_proc.returncode != 0:
        return util.error(
            f'ssh_helper: file transfer failed '
            f'(tar rc={tar_proc.returncode}, ssh rc={ssh_proc.returncode})',
            error_code=EDA_SSH_ERROR_CODE
        )
    util.info('ssh_helper: files shipped successfully.')
    return 0


def _fetch_results(user: str, host: str, remote_job_dir: str, local_eda_work: str) -> None:
    '''Fetch job artifacts from the remote back to local_eda_work.

    Uses tar+pipe to bring back the entire remote eda.work/ tree.  Because the remote
    job directory is an ephemeral sandbox (created fresh for this job, removed afterwards),
    eda.work/ contains only the results from this specific run — there are no stale files.

    After the transfer, reads the local artifacts.json (if present) to report what was
    produced.
    '''
    remote_work = f'{remote_job_dir}/eda.work'

    # Check whether eda.work was created at all.
    check = _run_local(
        _ssh_base(user, host) + [
            f'bash -lc {_shell_quote("[ -d " + remote_work + " ] && echo exists || echo absent")}'
        ],
        capture=True, check=False
    )
    if 'absent' in check.stdout:
        util.warning('ssh_helper: no remote eda.work/ found — nothing to fetch.')
        return

    util.info(f'ssh_helper: fetching results from {user}@{host}:{remote_work}')
    os.makedirs(local_eda_work, exist_ok=True)

    # Stream the entire eda.work/ tree back via tar+pipe.
    ssh_send_cmd = _ssh_base(user, host) + [f'tar -C {remote_work!r} -czf - .']
    # TODO(drew): lost support for --subprocess-timeout and --subprocess-progress-timeout.
    # pylint: disable=consider-using-with
    ssh_proc_kwargs = subprocess_helpers.get_kwargs(work_dir='')
    ssh_proc_kwargs['stdout'] = subprocess.PIPE
    ssh_proc = subprocess.Popen(ssh_send_cmd, **ssh_proc_kwargs)
    subprocess_helpers.add_running_parent_pid(ssh_proc.pid)

    tar_proc_kwargs = subprocess_helpers.get_kwargs(work_dir='', is_subprocess_popen=False)
    tar_proc_kwargs.update({
        'stdin': ssh_proc.stdout,
    })
    tar_proc = subprocess.run(
        ['tar', '-C', local_eda_work, '-xzf', '-'],
        check=False, **tar_proc_kwargs
    )

    ssh_proc.stdout.close()
    ssh_proc.wait()
    subprocess_helpers.remove_completed_parent_pid(ssh_proc.pid)
    if tar_proc.returncode != 0:
        util.warning(f'ssh_helper: eda.work/ fetch had errors (rc={tar_proc.returncode}).')


def _cleanup_remote(user: str, host: str, remote_job_dir: str) -> None:
    '''Remove the ephemeral remote job directory.'''
    if not remote_job_dir:
        return
    util.info(f'ssh_helper: cleaning up remote sandbox: {user}@{host}:{remote_job_dir}')
    _run_local(
        _ssh_cmd(user, host, f'rm -rf {remote_job_dir!r}'),
        capture=True, check=False
    )


# ---------------------------------------------------------------------------
# Remote eda invocation
# ---------------------------------------------------------------------------

def _build_remote_eda_args(
        eda_non_positional_args: list, target_key: str,
        run_no_color: bool = False
) -> list:
    '''Strip --ssh from original_args; normalise target token to target_key.

    Handles "--ssh=foo" and "--ssh foo" styles.

    The remote staging dir has a flat DEPS.yml — files and the target are referenced
    by basename only.  Replace any path-qualified target token with just target_key:
      "path/to/DEPS.yml::a01"    (DEPS-file-qualified)
      "A01_some_dir/a01"         (directory-prefixed, e.g. from eda multi)
    '''

    out = []
    skip_next = False
    for tok in eda_non_positional_args + [target_key]:
        if skip_next:
            skip_next = False
            continue
        if run_no_color and tok in ('--color', '--no-color'):
            continue # strip
        if tok in ('--ssh', '--ssh-setup'):
            skip_next = True
            continue
        if tok.startswith('--ssh=') or tok.startswith('--ssh-setup='):
            continue
        # Replace path-qualified target tokens with just the target key.
        # Leaving this, but we've already stripped the positional args
        # hence the naming eda_non_positional_args:
        if target_key and not tok.startswith('-') and (
            tok == target_key or
            tok.endswith('/' + target_key) or
            tok.endswith('::' + target_key)
        ):
            out.append(target_key)
            continue
        out.append(tok)

    if run_no_color:
        out.append('--no-color')
    return out


def run_eda_remote(
        user: str, host: str, remote_path: str,
        remote_eda_args: list,
        remote_eda_exe: str = 'eda',
        setup_cmd: str = '',
        timeout: int = 0,
        progress_timeout: int = 0
) -> int:
    '''Run eda on the remote host. Streams stdout/stderr to local terminal.

    Returns the remote eda exit code.
    '''
    eda_cmd = ' '.join([remote_eda_exe] + [_shell_quote(a) for a in remote_eda_args])
    parts = []
    if setup_cmd:
        parts.append(setup_cmd)
    parts.append(f'cd {remote_path!r}')
    parts.append(eda_cmd)
    remote_shell_cmd = ' && '.join(parts)

    ssh_cmd = _ssh_cmd(user, host, remote_shell_cmd)

    util.info(f'ssh_helper: running on {user}@{host}: {eda_cmd}')
    util.info(f'ssh_helper: remote working directory: {remote_path}')

    _, _, rc = subprocess_helpers.subprocess_run_background(
        work_dir='',
        command_list = ssh_cmd,
        background=False,
        shell=False, tee_fpath='',
        timeout=timeout, timeout_str='',
        progress_timeout=progress_timeout, progress_timeout_str='',
        force_no_color=False # attempt to keep colors for background=False
    )
    return rc


def _shell_quote(s: str, shlex_quote: bool = False) -> str:
    '''Minimal shell quoting: wraps in single quotes if the token contains spaces or specials.'''
    if shlex_quote:
        return shlex.quote(s)
    if re.search(r'[\s\'\"\\$`&|<>(){}]', s):
        return "'" + s.replace("'", "'\\''") + "'"
    return s


def _get_modified_args_list( # pylint: disable=too-many-branches
        args: dict, modified_args: dict, warn: bool = True, filter_args=None
) -> list:
    '''Givens args and modified_args (dict), return a list of str suitable for

    command line args. Reminder that modified args is {key_name: bool} (value True if modified)
    '''
    ret = []
    for arg, modified in modified_args.items():
        if not arg or not modified:
            continue
        if arg in filter_args:
            continue


        value = args[arg]
        if value is None:
            util.error(f'arg {arg} has been modified to None, cannot be re-applied as',
                       'command line arg')
        elif isinstance(value, dict):
            if warn:
                util.warning(f'arg {arg} is dict, {value=} cannot be re-applied as command line')
        elif isinstance(value, list):
            for x in value:
                ret.append(f'--{arg}={x}')
        elif isinstance(value, bool):
            if value:
                ret.append(f'--{arg}')
            else:
                ret.append(f'--no-{arg}')
        elif isinstance(value, int):
            ret.append(f'--{arg}={value}')
        elif isinstance(value, str):
            ret.append(f'--{arg}={_shell_quote(value)}')
        else:
            util.error(f'arg {arg} has {value=} {type(value)=}, cannot re-apply this type for',
                       'command line arg')


    return ret


def _get_eda_modified_args_list( # pylint: disable=dangerous-default-value
        cmd_obj, warn: bool = True, filter_args=None
) -> list:
    '''Examines cmd_obj.modified_args, eda_config, and util, and returns a list of str args

    suitable to re-apply args via ssh on remote host, this way expanded items
    like --input-file are not passed to remote.

    Note this does not include the eda COMMAND [SUBCOMMANDS]
    '''

    # NOTE(drew): even this is not perfect, would need to get various util.args
    # that were modified, loaded --env-file(s)
    ret = []

    if util.env_files_loaded:
        util.warning(
            '--env-file(s) have been loaded, these are not supported by SSH',
            f'and will not be sent to the remote host as eda args: {util.env_files_loaded}'
        )

    # Re-create the eda_config, util, and cmd_obj args.
    ret.extend(eda_config.get_config_yml_args_for_flist())
    ret.extend(
        _get_modified_args_list(
            util.args, util.modified_args, warn=warn, filter_args=filter_args
        )
    )
    ret.extend(
        _get_modified_args_list(
            cmd_obj.args, cmd_obj.modified_args, warn=warn,
            filter_args=list(util.modified_args.keys())
        )
    )

    return ret



# ---------------------------------------------------------------------------
# Main entry point (called from eda.py after process_tokens())
# ---------------------------------------------------------------------------

def run_ssh_command( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
        cmd_obj, local_cwd: str
) -> int:
    '''Ship source files, run eda remotely, fetch artifacts, clean up remote sandbox.

    Called from eda.py after process_tokens() has populated the file lists (files_sv,
    files_v, incdirs, defines, etc.) but before local tool execution.

    cmd_obj  -- CommandDesign instance with populated file lists
    local_cwd -- local working directory when eda was invoked (used for result placement)

    Returns a bash-style exit code (0 = pass).
    '''
    config = cmd_obj.config
    ssh_arg = config.get('ssh', '')
    ssh_setup = config.get('ssh_setup', '')

    # Note - the eda_orignal_args are literally what was on the command line,
    # so this does not include --input-file items resolved/expanded. So if you
    # created ./my_eda_ssh.f, and ran: eda sim mytarget -f=my_eda_ssh.f, it
    # would not work. We instead re-create from util, eda_config, cmd_obj modified_args.
    original_args = config.get('eda_original_args', [])

    # Get the modified args from
    cmd_obj_modified_args = _get_eda_modified_args_list(cmd_obj)
    tool = cmd_obj.args.get('tool', '') or config.get('ssh_tool', '')

    if not tool:
        return util.error(
            '--ssh requires --tool to be specified explicitly '
            '(remote tool discovery is not performed locally). '
            'Example: eda sim --tool vivado --ssh user@host',
            error_code=EDA_SSH_ERROR_CODE
        )

    try:
        user, host, explicit_path = parse_ssh_arg(ssh_arg)
    except ValueError as e:
        return util.error(str(e), error_code=EDA_SSH_ERROR_CODE)

    if explicit_path:
        remote_base_path = explicit_path
    else:
        try:
            remote_home = _get_remote_home(user, host)
        except RuntimeError as e:
            return util.error(str(e), error_code=EDA_SSH_ERROR_CODE)
        remote_base_path = remote_home

    util.info(
        f'ssh_helper: {Colors.bcyan}--ssh{Colors.off_green} mode:'
        f' {Colors.byellow}{user}@{host}'
        + (f'{Colors.off_green} (base: {remote_base_path})' if explicit_path
           else f'{Colors.off_green} (base: {remote_base_path} from $HOME)')
    )

    # --- Verify remote eda ---
    remote_eda_exe = remote_check_eda(user, host, setup_cmd=ssh_setup)
    if not remote_eda_exe:
        return util.error(
            f'ssh_helper: `eda` was not found in PATH on {user}@{host}.'
            ' Please install opencos-eda on the remote host'
            ' (e.g. uv tool install opencos-eda, pip install opencos-eda, or activate'
            ' the appropriate venv).',
            error_code=EDA_SSH_ERROR_CODE
        )
    util.info(f'ssh_helper: remote eda found: {remote_eda_exe}')

    # --- Verify remote tool ---
    tool_name = tool.split('=')[0]
    remote_tool_exe = remote_check_tool(user, host, tool_name, setup_cmd=ssh_setup)
    if remote_tool_exe:
        util.info(
            f'ssh_helper: remote {Colors.off_bgreen}{tool_name}{Colors.info}'
            f'{Colors.off_green} found: {remote_tool_exe}'
        )
    else:
        util.warning(
            f'ssh_helper: tool {tool_name!r} was not found on {user}@{host}.'
            ' The remote eda run may fail if the tool is not in PATH.'
        )

    # --- Create unique ephemeral remote job directory ---
    job_id = uuid.uuid4().hex[:12]
    remote_job_dir = f'{remote_base_path}/eda.work/{job_id}.ssh'
    util.info(f'ssh_helper: remote job dir: {remote_job_dir}')

    # --- Stage files locally, ship to remote, run, fetch, clean up ---
    with tempfile.TemporaryDirectory(prefix='eda_ssh_stage_') as staging_dir:
        rc = _stage_files(cmd_obj, staging_dir)
        if rc != 0:
            return rc

        rc = _ship_to_remote(user, host, staging_dir, remote_job_dir)
        if rc != 0:
            _cleanup_remote(user, host, remote_job_dir)
            return rc

    target_key = _get_target_name(cmd_obj)

    commands = []
    if c := getattr(cmd_obj, 'command_name', ''):
        commands.append(c)
    if c := getattr(cmd_obj, 'subcommand', ''):
        commands.append(c)

    if not commands:
        util.error(f'Could not find the commands to run: {original_args=}',
                   f'fished out {cmd_obj_modified_args=} and {target_key=}, but {commands=}')
    if not target_key:
        util.error(f'Could not find the exported target to run: {original_args=}',
                   f'fished out {cmd_obj_modified_args=} and {commands=}, but {target_key=}')

    remote_args = commands + _build_remote_eda_args(
        cmd_obj_modified_args, target_key, run_no_color=False
    )

    util.debug(f'ssh_helper.py: run_eda_remote -- {remote_args=}')

    rc = run_eda_remote(
        user=user,
        host=host,
        remote_path=remote_job_dir,
        remote_eda_args=remote_args,
        remote_eda_exe=remote_eda_exe,
        setup_cmd=ssh_setup,
        timeout=cmd_obj.args.get('subprocess-timeout', 0),
        progress_timeout=cmd_obj.args.get('subprocess-progress_timeout', 0)
    )

    local_eda_work = os.path.join(local_cwd, 'eda.work')
    _fetch_results(user, host, remote_job_dir, local_eda_work)

    _cleanup_remote(user, host, remote_job_dir)

    if rc == 0:
        util.info(
            f'ssh_helper: {Colors.off_bgreen}remote eda completed successfully.'
            f'{Colors.info} Results in local eda.work/'
        )
    else:
        util.error(
            f'ssh_helper: remote eda exited with code {rc}.'
            ' Check output above for details.',
            error_code=EDA_SSH_ERROR_CODE
        )

    return rc
