"""
Subsample sequences from an input dataset.

The input dataset can consist of a metadata file, a sequences file, or both.

See documentation page for details on configuration.
"""

import argparse
import os
import subprocess
import tempfile
import yaml
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from textwrap import dedent
from typing import Any, Dict, List, Optional, Set, Tuple, Union
from augur import filter as augur_filter
from augur.argparse_ import ExtendOverwriteDefault, SKIP_AUTO_DEFAULT_IN_HELP
from augur.errors import AugurError
from augur.io.metadata import DEFAULT_DELIMITERS, DEFAULT_ID_COLUMNS
from augur.io.print import print_err, indented_list
from augur.utils import augur
from augur.validate import load_json_schema, validate_json, ValidateError


BooleanFlags = Tuple[str, Optional[str]]
"""
Type for a boolean pair of augur filter command line flags that configure the
same option. When there is no second flag, absence of the first flag indicates
the default behavior.
"""

AugurFilterOption = Union[str, BooleanFlags]
"""
Type for an augur filter command line option. Either a single option or boolean
pair of flags.
"""

FilterArgs = Dict[str, Any]
"""
Augur filter arguments stored as a mapping from option to value.
"""


GLOBAL_CLI_OPTIONS: Dict[str, AugurFilterOption] = {
    "metadata": "--metadata",
    "metadata_chunk_size": "--metadata-chunk-size",
    "metadata_delimiters": "--metadata-delimiters",
    "metadata_id_columns": "--metadata-id-columns",
    "sequences": "--sequences",
    "sequence_index": "--sequence-index",
    "seed": "--subsample-seed",
}
"""
Mapping of argparse namespace variable name to augur filter option.
These are sent to both intermediate and final augur filter calls.
"""


FINAL_CLI_OPTIONS: Dict[str, AugurFilterOption] = {
    "output_metadata": "--output-metadata",
    "output_sequences": "--output-sequences",
    "output_log": "--output-log",
    "skip_checks": ("--skip-checks", None),
}
"""
Mapping of argparse namespace variable name to augur filter option.
These are sent to only the final augur filter call.
"""


# NOTE: If you edit any of these values, please re-run
# devel/regenerate-subsample-schema and commit the schema changes too.
SAMPLE_CONFIG: Dict[str, AugurFilterOption] = {
    "exclude": "--exclude",
    "exclude_all": ("--exclude-all", None),
    "exclude_ambiguous_dates_by": "--exclude-ambiguous-dates-by",
    "exclude_where": "--exclude-where",
    "include": "--include",
    "include_where": "--include-where",
    "min_date": "--min-date",
    "max_date": "--max-date",
    "min_length": "--min-length",
    "max_length": "--max-length",
    "non_nucleotide": ("--non-nucleotide", None),
    "query": "--query",
    "query_columns": "--query-columns",
    "group_by": "--group-by",
    "group_by_weights": "--group-by-weights",
    "probabilistic_sampling": ("--probabilistic-sampling", "--no-probabilistic-sampling"),
    "sequences_per_group": "--sequences-per-group",
    "max_sequences": "--subsample-max-sequences",
}
"""
Mapping of YAML configuration key name to augur filter option.
These are sent to only the intermediate augur filter calls.
"""


def register_parser(parent_subparsers: argparse._SubParsersAction) -> argparse.ArgumentParser:
    parser = parent_subparsers.add_parser("subsample", help=__doc__)

    input_group = parser.add_argument_group("Input options", "options related to input files")
    input_group.add_argument('--metadata', metavar="FILE", help="sequence metadata" + SKIP_AUTO_DEFAULT_IN_HELP)
    input_group.add_argument('--sequences', metavar="FILE", help="sequences in FASTA or VCF format. For large inputs, consider using --sequence-index in addition to this option." + SKIP_AUTO_DEFAULT_IN_HELP)
    input_group.add_argument('--sequence-index', metavar="FILE", help="sequence composition report generated by augur index. If not provided, an index will be created on the fly. This should be generated from the same file as --sequences." + SKIP_AUTO_DEFAULT_IN_HELP)
    input_group.add_argument('--metadata-chunk-size', metavar="N", type=int, default=100000, help="maximum number of metadata records to read into memory at a time. Increasing this number can reduce run times at the cost of more memory used.")
    input_group.add_argument('--metadata-id-columns', metavar="COLUMN", default=DEFAULT_ID_COLUMNS, nargs="+", action=ExtendOverwriteDefault, help="names of possible metadata columns containing identifier information, ordered by priority. Only one ID column will be inferred.")
    input_group.add_argument('--metadata-delimiters', metavar="CHARACTER", default=DEFAULT_DELIMITERS, nargs="+", action=ExtendOverwriteDefault, help="delimiters to accept when reading a metadata file. Only one delimiter will be inferred.")
    input_group.add_argument('--skip-checks', action='store_true', help="use this option to skip checking for duplicates in sequences and whether ids in metadata have a sequence entry. Can improve performance on large files. Note that this should only be used if you are sure there are no duplicate sequences or mismatched ids since they can lead to errors in downstream Augur commands.")

    config_group = parser.add_argument_group("Configuration options", "options related to configuration")
    config_group.add_argument("--config", metavar="FILE", required=True, help="augur subsample config file. The expected config options must be defined at the top level, or within a specific section using --config-section." + SKIP_AUTO_DEFAULT_IN_HELP)
    config_group.add_argument("--config-section", metavar="KEY", nargs="+", action=ExtendOverwriteDefault, help="Use a section of the file given to --config by listing the keys leading to the section. Provide one or more keys. (default: use the entire file)" + SKIP_AUTO_DEFAULT_IN_HELP)
    config_group.add_argument("--search-paths", "--search-path", metavar="DIR", nargs="+", action=ExtendOverwriteDefault,
        help=dedent(f"""\
            One or more directories to search for relative filepaths specified
            in the config file. If a file exists in multiple directories, only
            the file from the first directory will be used. This can also be set
            via the environment variable 'AUGUR_SEARCH_PATHS'. Specified
            directories will be considered before the defaults, which are:
            (1) directory containing the config file
            (2) current working directory""" + SKIP_AUTO_DEFAULT_IN_HELP))
    config_group.add_argument('--nthreads', metavar="N", type=int, default=1, help="Number of CPUs/cores/threads/jobs to utilize at once. For augur subsample, this means the number of samples to run simultaneously. Individual samples are limited to a single thread. The final augur filter call can take advantage of multiple threads.")
    config_group.add_argument('--seed', metavar="N", type=int, help="random number generator seed for reproducible outputs (with same input data)." + SKIP_AUTO_DEFAULT_IN_HELP)

    output_group = parser.add_argument_group("Output options", "options related to output files")
    output_group.add_argument("--output-metadata", metavar="FILE", help="output metadata file" + SKIP_AUTO_DEFAULT_IN_HELP)
    output_group.add_argument("--output-sequences", metavar="FILE", help="output sequences file" + SKIP_AUTO_DEFAULT_IN_HELP)
    output_group.add_argument('--output-log', help="Tab-delimited file to debug sequence inclusion in samples. All sequences have a row with filter=filter_by_exclude_all. The sequences included in the output each have an additional row per sample that included it (there may be multiple). These rows have filter=force_include_strains with kwargs pointing to a temporary file that hints at the intermediate sample it came from." + SKIP_AUTO_DEFAULT_IN_HELP)
    return parser


def run(args: argparse.Namespace) -> None:
    """Run augur subsample.

    This is implemented by calling augur filter once for each sample in the
    config (i.e. the intermediate calls), then one more time to combine the
    samples (i.e. the final call). It was inspired by several pathogen repos
    adopting a similar approach using Snakemake rules.

    Notes on performance:

    - If multiple intermediate calls use sequence-based filters and
      --sequence-index is not set, each call will build its own sequence index,
      meaning the same work is done at least twice. A more optimal approach
      would be to add a preliminary step to build the sequence index then pass
      it down to the intermediate calls. However, this complicates things and
      may not be worth it if sequence indexing is rewritten:
      <https://github.com/nextstrain/augur/issues/1846>

    - If multiple intermediate calls use the same default filters that
      significantly reduce the size of the initial input dataset, each call will
      go through the large input dataset and filter it with the same filters,
      meaning the same work is done at least twice. A more optimal approach
      would be to run the default options through an initial augur filter call.
      This would output a much smaller intermediate dataset that can be used by
      the intermediate calls. However, this complicates things and may not be
      worth it if a proper input reuse approach such as database/parquet file
      support is adopted: <https://github.com/nextstrain/augur/issues/1574>
    """

    # Load schema, parse and validate config.
    schema_validator = load_json_schema("schema-subsample-config.json")
    config = _parse_config(args.config, args.config_section, schema_validator)

    # Resolve filepaths.
    search_paths = _get_search_paths(args.config, args.search_paths)
    config, _ = _resolve_filepaths(config, search_paths, schema_validator.schema)

    # Construct argument lists for augur filter.

    defaults = config.get("defaults")
    samples: List[Sample] = []
    global_filter_args: FilterArgs = {}
    for cli_option, filter_option in GLOBAL_CLI_OPTIONS.items():
        if (value := getattr(args, cli_option)) is not None:
            _add_to_args(global_filter_args, filter_option, value)

    for name, options in config.get("samples", {}).items():
        options = _merge_options(options, defaults)
        sample = Sample(name, options, global_filter_args)
        samples.append(sample)

    final_filter_args: FilterArgs = {}
    for cli_option, filter_option in FINAL_CLI_OPTIONS.items():
        if (value := getattr(args, cli_option)) is not None:
            _add_to_args(final_filter_args, filter_option, value)

    # Run augur filter.

    if len(samples) == 1:
        # A single sample is translated to a single augur filter call.

        # The list of ids is useless in this case.
        samples[0].remove_output_strains()

        filter_args = {
            **samples[0].filter_args,
            **final_filter_args,
            "--nthreads": args.nthreads,
        }
        _run_final_filter(filter_args)
    else:
        # Multiple samples require multiple augur filter calls.
        try:
            # Run intermediate augur filter calls in parallel.
            with ThreadPoolExecutor(max_workers=args.nthreads) as executor:
                futures = [executor.submit(s.run) for s in samples]
                try:
                    for future in as_completed(futures):
                        future.result()
                except Exception:
                    # Note: There is a race condition where samples may start before
                    # failures in other samples are detected. On failure, we cancel
                    # remaining queued samples but already-running samples will
                    # complete. This provides partial fail-fast behavior while
                    # maintaining parallelism.
                    executor.shutdown(cancel_futures=True)
                    raise

            # Run the final augur filter call to combine the intermediate samples.
            final_filter_args.update(global_filter_args)
            final_filter_args.update({
                "--exclude-all": None,
                "--include": [s.output_strains for s in samples],
                "--nthreads": args.nthreads,
            })
            _run_final_filter(final_filter_args)
        finally:
            for sample in samples:
                sample.remove_output_strains()


def get_referenced_files(
    config_file: str,
    config_section: Optional[List[str]] = None,
    search_paths: Optional[List[str]] = None,
) -> Set[str]:
    """Get the files referenced in a subsample config file.

    Extracts and resolves all filepath values referenced in the config,
    including defaults and individual sample options.

    Parameters
    ----------
    config_file
        Path to the subsample config file.

    config_section
        Optional list of keys to navigate to a specific section of the config file.

    search_paths
        Optional list of directories to search for relative filepaths specified
        in the config file. If a file exists in multiple directories, only
        the file from the first directory will be used. This can also be set
        via the environment variable 'AUGUR_SEARCH_PATHS'. Specified
        directories will be considered before the defaults, which are:
        (1) directory containing the config file
        (2) current working directory

    Returns
    -------
    set
        Resolved filepaths
    """
    # Load schema, parse and validate config.
    schema_validator = load_json_schema("schema-subsample-config.json")
    config = _parse_config(config_file, config_section, schema_validator)

    # Resolve filepaths.
    search_path_objs = _get_search_paths(config_file, search_paths)
    config, filepaths = _resolve_filepaths(config, search_path_objs, schema_validator.schema)

    return set(filepaths)


def _parse_config(filename: str, config_section: Optional[List[str]], schema) -> Dict[str, Any]:
    # Create a custom YAML loader to treat timestamps as strings.
    class CustomLoader(yaml.SafeLoader):
        pass
    def string_constructor(loader, node):
        return loader.construct_scalar(node)
    CustomLoader.add_constructor('tag:yaml.org,2002:timestamp', string_constructor)

    with open(filename) as f:
        try:
            config = yaml.load(f, Loader=CustomLoader)
        except yaml.YAMLError as e:
            raise AugurError(f"The configuration file {filename!r} is not valid YAML.\n" + str(e)) from e

    # Handle --config-section.
    if config_section is not None:
        traversed_section = config

        for i, key in enumerate(config_section):
            if not isinstance(traversed_section, dict) or key not in traversed_section:
                traversed_path = ' → '.join(repr(key) for key in config_section[:i+1])
                raise AugurError(f"Config section {traversed_path} not found in {filename!r}")
            traversed_section = traversed_section[key]

        config = traversed_section

    # Validate against schema.
    try:
        validate_json(config, schema, filename)
    except ValidateError as e:
        raise AugurError(e)
    return config


def _get_search_paths(
    config_file: str,
    from_cli: List[str],
) -> List[Path]:
    """
    Returns the paths to search for relative filepaths in config.
    """
    default = [
        Path(config_file).parent,
        Path.cwd(),
    ]

    from_env = os.environ.get('AUGUR_SEARCH_PATHS')

    if from_cli:
        if from_env:
            print_err(dedent(f"""\
                WARNING: Both the command line argument --search-paths
                and the environment variable AUGUR_SEARCH_PATHS are set.
                Only the command line argument will be used."""))
        return [
            *(Path(p) for p in from_cli),
            *default,
        ]

    if from_env:
        return [
            *(Path(p) for p in from_env.split(':')),
            *default,
        ]

    return default


def _resolve_filepaths(
    config: Dict[str, Any],
    search_paths: List[Path],
    schema: Dict[str, Any],
    root_schema: Optional[Dict[str, Any]] = None,
) -> Tuple[Dict[str, Any], List[str]]:
    """
    Resolve filepaths in config.

    Recursively walks the config alongside the schema to determine which fields
    contain filepaths, resolves them, and collects the resolved filepaths.
    """
    if root_schema is None:
        root_schema = schema

    filepaths = []

    # Get properties schema for current section
    properties = schema.get("properties", {})
    pattern_properties = schema.get("patternProperties", {})

    for key, value in config.items():
        prop_schema = properties.get(key)

        if not prop_schema and pattern_properties:
            # Use first pattern property schema (for dynamic keys like samples)
            prop_schema = next(iter(pattern_properties.values()))

        # Get referenced property schema
        if ref := prop_schema.get("$ref"):
            prop_schema = _get_referenced_schema(ref, root_schema)

        # Resolve filepath
        if _is_filepath(prop_schema):
            if isinstance(value, list):
                config[key] = [str(_resolve_filepath(Path(v), search_paths)) for v in value]
                filepaths.extend(config[key])
            elif isinstance(value, str):
                config[key] = str(_resolve_filepath(Path(value), search_paths))
                filepaths.append(config[key])

        # Recurse into config section
        elif isinstance(value, dict):
            config[key], downstream_filepaths = _resolve_filepaths(
                value, search_paths, prop_schema, root_schema
            )
            filepaths.extend(downstream_filepaths)

    return config, filepaths


def _get_referenced_schema(
    ref: str,
    root_schema: Dict[str, Any],
) -> Dict[str, Any]:
    """
    Resolve a JSON schema reference. Example: '#/$defs/sampleProperties'
    """
    keys = ref.lstrip("#/").split("/")
    schema = root_schema
    for key in keys:
        schema = schema[key]
    return schema


def _is_filepath(prop_schema: Dict[str, Any]) -> bool:
    """
    Check if the property schema declares it is a filepath.
    """
    # Direct 'format: filepath'
    if prop_schema.get("format") == "filepath":
        return True

    # Check oneOf variants for 'format: filepath'
    if "oneOf" in prop_schema:
        for variant in prop_schema["oneOf"]:
            if variant.get("format") == "filepath":
                return True

    return False


def _resolve_filepath(
    path: Path,
    search_paths: List[Path],
) -> Path:
    """
    Resolve a filepath by searching through multiple directories.

    If the path is already absolute, verify it exists and return it.

    >>> import tempfile
    >>> from pathlib import Path
    >>> tmpdir1 = Path(tempfile.mkdtemp()).resolve()
    >>> tmpdir2 = Path(tempfile.mkdtemp()).resolve()
    >>> absolute_path = tmpdir1 / "file.txt"
    >>> with open(absolute_path, "w") as f: _ = f.write("test")
    >>> _resolve_filepath(absolute_path, []) == absolute_path
    True

    Otherwise, try resolving it relative to each directory in search_paths, in order.
    Return the first path that exists.

    >>> with open(tmpdir2 / "file.txt", "w") as f: _ = f.write("test")
    >>> result = _resolve_filepath(Path("file.txt"), [tmpdir1, tmpdir2])
    >>> result == tmpdir1 / "file.txt"
    True

    If an absolute path doesn't exist, raise an error.

    >>> _resolve_filepath(Path("/nonexistent/file.txt"), [tmpdir1, tmpdir2])
    Traceback (most recent call last):
      ...
    augur.errors.AugurError: File '/nonexistent/file.txt' does not exist.

    If the relative path doesn't exist anywhere, raise an error.

    >>> _resolve_filepath(Path("nonexistent.txt"), [tmpdir1, tmpdir2])
    Traceback (most recent call last):
      ...
    augur.errors.AugurError: File 'nonexistent.txt' not resolvable from any of the following paths:
    <BLANKLINE>
      ...
    """
    # Absolute path
    if path.is_absolute():
        if not path.exists():
            raise AugurError(f"File {str(path)!r} does not exist.")
        return path

    # Relative path
    for search_path in search_paths:
        resolved_path = (search_path / path).resolve()
        if resolved_path.exists():
            return resolved_path

    # File not found
    raise AugurError(dedent(f"""\
        File {str(path)!r} not resolvable from any of the following paths:

          {indented_list([str(p) for p in search_paths], '        ' + '  ')}"""))


def _merge_options(sample_options: Dict[str, Any], defaults: Optional[Dict[str, Any]]) -> Dict[str, Any]:
    """
    Merge sample options with default options, with sample options taking precedence.
    """
    if defaults is None:
        return sample_options

    return {**defaults, **sample_options}


class Sample:
    def __init__(self, name: str, config: Dict[str, Any], global_filter_args: FilterArgs) -> None:
        self.name = name
        self.output_strains = tempfile.NamedTemporaryFile(prefix=f"sample_{self.name}_", delete=False).name
        self.filter_args = self._construct_filter_args(config, global_filter_args)

    def _construct_filter_args(self, config: Dict[str, Any], global_filter_args: FilterArgs) -> FilterArgs:
        """
        Construct filter arguments from YAML config and global arguments.

        Extends global filter arguments:

        >>> global_args = {"--metadata": "test.tsv", "--sequences": "test.fasta"}
        >>> sample = Sample("test_sample", {}, global_args)
        >>> sample.filter_args["--metadata"]
        'test.tsv'
        >>> sample.filter_args["--sequences"]
        'test.fasta'

        Maps YAML config to filter arguments:

        >>> config = {"min_date": "2020-01-01", "group_by": ["region", "year"], "sequences_per_group": 5, "non_nucleotide": True, "probabilistic_sampling": False}
        >>> sample = Sample("test_sample", config, {})
        >>> sample.filter_args["--min-date"]
        '2020-01-01'
        >>> sample.filter_args["--group-by"]
        ['region', 'year']
        >>> sample.filter_args["--sequences-per-group"]
        5
        >>> "--non-nucleotide" in sample.filter_args
        True
        >>> "--no-probabilistic-sampling" in sample.filter_args
        True
        """
        filter_args = {
            # Checks are redundant across multiple calls with the same input.
            # Checks will run once on the final augur filter call, unless explicitly skipped.
            "--skip-checks": None,

            # Use a single thread for each sample to simplify multithreading across samples.
            "--nthreads": 1,

            # Store outcome as a list of ids to be used by --include in the final augur filter call.
            "--output-strains": self.output_strains
        }

        filter_args.update(global_filter_args)

        for config_key, value in config.items():
            filter_option = SAMPLE_CONFIG[config_key]
            _add_to_args(filter_args, filter_option, value)

        return filter_args

    def run(self) -> None:
        """Run augur filter as a subprocess.

        Notes:

        - A direct import of augur.filter in Python is not used because all
          samples would share the same sys.stderr, which causes interleaved
          messages when processes are run in parallel.
          This is also why _run_final_filter() isn't repurposed for use here.

        - shell=True is not used because it requires additional logic to
          carefully escape values such as "--metadata-delimiters , \t".
          This is also why run_shell_command() isn't used here.
        """
        result = subprocess.run(
            [*augur(shell=False), 'filter', *_args_dict_to_list(self.filter_args)],
            capture_output=True,
            text=True,
        )
        # Output console messages only after the command has completed so that
        # they are not mixed with messages from other samples running in
        # parallel.
        for line in result.stderr.strip().split('\n'):
            print_err(f"[{self.name}] {line}")

        if result.returncode != 0:
            error_msg = f"Sample {self.name!r} failed, see error above."
            raise AugurError(error_msg)

    def remove_output_strains(self):
        """Remove the augur filter arguments and temporary file."""
        del self.filter_args["--output-strains"]

        if os.path.exists(self.output_strains):
            os.unlink(self.output_strains)


def _add_to_args(args: FilterArgs, filter_option: AugurFilterOption, value: Any) -> None:
    """
    Add a filter option and its value to the arguments dictionary.

    Scalar values are added directly:

    >>> args = {}
    >>> _add_to_args(args, "--option", "value")
    >>> args
    {'--option': 'value'}

    List and tuple values are preserved:

    >>> args = {}
    >>> _add_to_args(args, "--option", ["a", "b", "c"])
    >>> args
    {'--option': ['a', 'b', 'c']}

    >>> args = {}
    >>> _add_to_args(args, "--option", ("a", "b"))
    >>> args
    {'--option': ('a', 'b')}

    Boolean true adds the true flag:

    >>> args = {}
    >>> _add_to_args(args, ("--enable", "--disable"), True)
    >>> args
    {'--enable': None}

    Boolean false adds the false flag if available:

    >>> args = {}
    >>> _add_to_args(args, ("--enable", "--disable"), False)
    >>> args
    {'--disable': None}

    Boolean false with no false flag adds nothing:

    >>> args = {}
    >>> _add_to_args(args, ("--enable", None), False)
    >>> args
    {}

    Multiple calls preserve all arguments:

    >>> args = {}
    >>> _add_to_args(args, "--option1", "a")
    >>> _add_to_args(args, "--option2", ["b", "c"])
    >>> _add_to_args(args, ("--option3", None), True)
    >>> args == {"--option1": "a", "--option2": ["b", "c"], "--option3": None}
    True
    """

    # Booleans are configured by one or two flags.
    if isinstance(value, bool):
        assert isinstance(filter_option, tuple) and len(filter_option) == 2
        true_flag, false_flag = filter_option
        if value is True:
            args[true_flag] = None
        else:
            assert value is False
            if false_flag is not None:
                args[false_flag] = None
            else:
                # No false flag implies a default value of false.
                pass
    else:
        # Everything else is configured by one option.
        assert isinstance(filter_option, str)

        # Store the value directly in the dictionary
        args[filter_option] = value


def _args_dict_to_list(args: FilterArgs) -> List[str]:
    """
    Convert an arguments dictionary to a list suitable for argparse and subprocesses.

    Options with no value are added as boolean flags:

    >>> _args_dict_to_list({"--option": None})
    ['--option']

    Scalar values are converted to strings and added with their option:

    >>> _args_dict_to_list({"--option": "value"})
    ['--option', 'value']

    >>> _args_dict_to_list({"--option": 5})
    ['--option', '5']

    List values are unpacked with the option followed by all values:

    >>> _args_dict_to_list({"--option": [1, 2, 3]})
    ['--option', '1', '2', '3']

    Tuple values are handled the same as lists:

    >>> _args_dict_to_list({"--option": (1, 2, 3)})
    ['--option', '1', '2', '3']

    Empty lists result in just the option name:

    >>> _args_dict_to_list({"--option": []})
    ['--option']
    """
    args_list: List[str] = []

    for filter_option, value in args.items():
        # No value means a boolean flag.
        if value is None:
            args_list.append(filter_option)

        # Lists and tuples are unpacked and given as strings.
        elif isinstance(value, (list, tuple)):
            args_list.extend([filter_option, *(str(v) for v in value)])

        # Everything else is scalar and given as strings.
        else:
            args_list.extend([filter_option, str(value)])

    return args_list


def _run_final_filter(filter_args: FilterArgs) -> None:
    """Run augur filter as part of the augur subsample process.

    Note: intermediate augur filter calls go through a separate subprocess to
    avoid interleaved messages during parallel execution - see Sample.run().
    This final call goes through the current Python process to take advantage of
    streamed messages.
    """
    parser = argparse.ArgumentParser()
    augur_filter.register_arguments(parser)
    args = parser.parse_args(_args_dict_to_list(filter_args))
    augur_filter.run(args)
