"""
Wrapper around a subset of the subprocess module,
that uses bwrap (bubblewrap) when it is available.

Instead of importing subprocess, other modules should use this as follows:

  from . import subprocess
"""

import os
import shutil
import subprocess
import tempfile
import functools
from typing import Optional, List


__all__ = ['PIPE', 'run', 'CalledProcessError']
PIPE = subprocess.PIPE
CalledProcessError = subprocess.CalledProcessError

# pylint: disable=subprocess-run-check


@functools.lru_cache(maxsize=None)
def _get_bwrap_path() -> str:
    which_path = shutil.which('bwrap')
    if which_path:
        return which_path

    raise RuntimeError("Unable to find bwrap")  # pragma: no cover


def _get_bwrap_args(tempdir: str,
                    input_filename: str,
                    output_filename: Optional[str] = None) -> List[str]:
    ro_bind_args = []
    cwd = os.getcwd()

    # XXX: use --ro-bind-try once all supported platforms
    # have a bubblewrap recent enough to support it.
    ro_bind_dirs = ['/usr', '/lib', '/lib64', '/bin', '/sbin', '/etc/alternatives', cwd]
    for bind_dir in ro_bind_dirs:
        if os.path.isdir(bind_dir):  # pragma: no cover
            ro_bind_args.extend(['--ro-bind', bind_dir, bind_dir])

    ro_bind_files = ['/etc/ld.so.cache']
    for bind_file in ro_bind_files:
        if os.path.isfile(bind_file):  # pragma: no cover
            ro_bind_args.extend(['--ro-bind', bind_file, bind_file])

    args = ro_bind_args + \
        ['--dev', '/dev',
         '--proc', '/proc',
         '--chdir', cwd,
         '--unshare-user-try',
         '--unshare-ipc',
         '--unshare-pid',
         '--unshare-net',
         '--unshare-uts',
         '--unshare-cgroup-try',
         '--new-session',
         '--cap-drop', 'all',
         # XXX: enable --die-with-parent once all supported platforms have
         # a bubblewrap recent enough to support it.
         # '--die-with-parent',
        ]

    if output_filename:
        # Mount an empty temporary directory where the sandboxed
        # process will create its output file
        output_dirname = os.path.dirname(os.path.abspath(output_filename))
        args.extend(['--bind', tempdir, output_dirname])

    absolute_input_filename = os.path.abspath(input_filename)
    args.extend(['--ro-bind', absolute_input_filename, absolute_input_filename])

    return args


def run(args: List[str],
        input_filename: str,
        output_filename: Optional[str] = None,
        **kwargs) -> subprocess.CompletedProcess:
    """Wrapper around `subprocess.run`, that uses bwrap (bubblewrap) if it
    is available.

    Extra supported keyword arguments:

     - `input_filename`, made available read-only in the sandbox
     - `output_filename`, where the file created by the sandboxed process
       is copied upon successful completion; an empty temporary directory
       is made visible as the parent directory of this file in the sandbox.
       Optional: one valid use case is to invoke an external process
       to inspect metadata present in a file.
    """
    try:
        bwrap_path = _get_bwrap_path()
    except RuntimeError:  # pragma: no cover
        # bubblewrap is not installed ⇒ short-circuit
        return subprocess.run(args, **kwargs)

    with tempfile.TemporaryDirectory() as tempdir:
        prefix_args = [bwrap_path] + \
            _get_bwrap_args(input_filename=input_filename,
                            output_filename=output_filename,
                            tempdir=tempdir)
        completed_process = subprocess.run(prefix_args + args, **kwargs)
        if output_filename and completed_process.returncode == 0:
            shutil.copy(os.path.join(tempdir, os.path.basename(output_filename)),
                        output_filename)

        return completed_process
