"""
Support for the package-wide command-line interface (CLI).
"""

import datetime
import os
import typing

from . import etc
from . import measured
from . import parameter
from . import paths


class UserError(Exception):
    """Incorrect use of a function (probably from the CLI)."""


class CLIError(Exception):
    """Invalid data generated by the CLI."""


def run(mode: str, options: typing.Mapping):
    if mode == 'configfile':
        return run_configfile_subparser(options)
    raise CLIError(f"Unknown mode: {mode!r}")


def run_configfile_subparser(options: typing.Mapping):
    """Generate or compare configuration files."""
    if options.get('generate'):
        return generate_config_file(
            path=options['generate'],
            source=options['source'],
            verbose=options['verbose'],
        )
    if options.get('files'):
        return compare_config_files(
            files=options['files'],
            source=options['source'],
            diff=options['diff'],
            names=options['names'],
        )


_timestr = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')


def generate_config_file(
    path: paths.PathLike,
    source: paths.PathLike=None,
    verbose: bool=False,
) -> None:
    """Create a configuration file with default parameter values."""
    default = parameter.interface(srcdir=source)
    keys = sorted(parameter.ConfigurationC(source))
    header = [
        f"# EPREM default configuration file\n",
        f"# Generated at {_timestr}\n",
        f"# --------------------------------\n",
    ]
    values = [
        f"{key}={_get_arg_value(default[key])}\n"
        for key in keys
    ]
    footer = [
        f"# --------------------------------\n",
    ]
    filepath = paths.fullpath(path)
    with filepath.open('w') as fp:
        fp.writelines(header)
        fp.writelines(values)
        fp.writelines(footer)
    if verbose:
        print(f"Wrote default config file to {filepath}")


def compare_config_files(
    files: typing.Iterable[paths.PathLike],
    source: paths.PathLike=None,
    diff: bool=False,
    names: bool=False,
) -> None:
    """Compare values of EPREM configuration-file parameters.
    
    This method will print the name of each parameter and its default value, as
    well as the corresponding value contained in each configuration file in
    `files`. If `source` is given, it will read parameter names and default
    values from the version of `configuration.c` in `source`; otherwise it will
    use the values in the local database.
    """
    if not files:
        raise UserError("Nothing to compare") from None
    args = _build_arg_dict(files, source=source, diff=diff, names=names)
    topkeys = next(list(v.keys()) for v in args.values() if v)
    nonnull = (v for item in args.values() for v in item.values() if v)
    # width (i.e., string length) of the longest parameter
    pwidth = max(len(k) for k in args)
    # width (i.e., string length) of the longest file key
    lwidth = max(len(k) for k in topkeys)
    # width (i.e., string length) of the longest non-null value
    vwidth = max(len(v) for v in nonnull)
    # padded parameter-value width
    rwidth = vwidth + 2
    # amount of parameter-value right-justification
    jwidth = min(78, rwidth)
    # total width: either the longest parameter or the longest key-value combo.
    # - longest parameter produces groups like
    # * ParameterName
    # * -------------
    # * key     value
    # - longest key-value combo produces groups like
    # *    ParameterName
    # * -------------------
    # * key           value
    cwidth = max(pwidth, lwidth + jwidth)
    print()
    for key, item in args.items():
        print(str(key).center(cwidth))
        print('-' * cwidth)
        for k, v in item.items():
            print(f"{str(k).ljust(lwidth)}{str(v).rjust(cwidth-lwidth)}")
        print()


def _build_arg_dict(
    files: typing.Iterable[paths.PathLike],
    source: paths.PathLike=None,
    diff: bool=False,
    names: bool=False,
) -> typing.Dict[str, typing.Dict[str, typing.Any]]:
    """Build a `dict` of parameter values."""
    default = parameter.interface()
    normalized = _normalize_paths(files)
    if names:
        mapped = normalized
    else:
        mapped = {k: str(i) for i, k in enumerate(normalized)}
    targets = {
        relpath: parameter.interface(abspath)
        for abspath, relpath in mapped.items()
    }
    keys = sorted(parameter.ConfigurationC(source))
    built = {key: {} for key in keys}
    for key in keys:
        heading = f"  {key} (default: {_format_arg_string(default[key])})  "
        current = _map_parameter(key, targets)
        values = list(current.values())
        if not diff or _any_not_equal(values, default[key]):
            built[heading] = current.copy()
    if diff:
        return {k: v for k, v in built.items() if v}
    return built


def _map_parameter(
    key: str,
    targets: typing.Dict[str, parameter.Interface]
) -> typing.Dict[str, str]:
    """Build a dict of values for a single parameter."""
    d = {}
    for name, interface in targets.items():
        userdef = parameter.configfile(interface.config)
        if userdef.get(key):
            d[name] = _format_arg_string(interface.get(key))
        else:
            d[name] = '(default)'
    return d


def _any_not_equal(values: list, default):
    """Check for unequal values, excluding default flag."""
    replaced = values.copy()
    for i, vi in enumerate(values):
        if vi == '(default)':
            replaced[i] = _format_arg_string(default)
    v0 = replaced[0]
    return any(vi != v0 for vi in replaced)


def _normalize_paths(args: typing.Iterable[paths.PathLike]):
    """Compute full and unique relative paths."""
    full = {
        str(arg): str(paths.fullpath(arg, strict=True))
        for arg in args
    }
    common = os.path.commonpath(full.values())
    return {
        str(path): os.path.relpath(path, start=common)
        for path in full.values()
    }


def _format_arg_string(arg):
    """Format this argument for display."""
    if isinstance(arg, measured.Object):
        data = _get_arg_value(arg)
        if arg.unit == '1' or data is None:
            return f"{data!r}"
        if str(arg.unit) in {'d', 'day'}:
            return f"{data} 'day'"
        return f"{data} {str(arg.unit)!r}"
    return str(arg)


def _get_arg_value(arg):
    """Extract this argument's value, if necessary."""
    if isinstance(arg, measured.Object):
        return arg.data[0] if len(arg.data) == 1 else arg.data
    return arg


_docstring_replacements = {
    '`src`': 'SRC',
    '`source`': 'SRC',
    "`paths`": "the given paths",
    '`': "'",
    "the local database": str(parameter.SourceFile._db_path),
}

DESCRIPTIONS = {
    'configfile': etc.doc2help(
        run_configfile_subparser,
        mode='full',
        replacements=_docstring_replacements,
    ),
}

