#!python
"""Convert files between HESTIA format and other formats.

This script knows nothing about any individual converter. Formats, loaders and
per-converter options all come from the converter registry
(``hestia_earth.converters.base.registry``), which discovers them from each
converter's own ``converter.py``. Adding a converter therefore needs no edit
here -- see the converter blueprint.
"""
import argparse
import logging
import os
import sys

from hestia_earth.converters.base.chain import find_chain, run_chain
from hestia_earth.converters.base.registry import (
    HESTIA,
    discover,
    find,
    formats,
    load_callable,
)

SPECS = discover()
INPUT_FORMATS, OUTPUT_FORMATS = formats(SPECS)


def _add_general_arguments(parser):
    parser.add_argument('--input-file', type=str, help='Input file')
    parser.add_argument('--output-folder', type=str, required=True,
                        help='Output files folder')
    parser.add_argument('--input-format', type=str, required=True, choices=INPUT_FORMATS,
                        help='Input file format')
    parser.add_argument('--output-format', type=str, required=True, choices=OUTPUT_FORMATS,
                        help='Output file format')
    parser.add_argument('--mapping-files-directory', type=str, default='hestia-flowmaps',
                        help='Folder containing the mapping files in .csv format')
    parser.add_argument('--skip-existing', action='store_true',
                        help='Do not overwrite existing converted file.')
    parser.add_argument('--verbose', action='store_true', help='Enables verbose mode.')
    parser.add_argument('--debug-file', action='store_true',
                        help='Outputs conversion logs to debug file.')
    parser.add_argument('--filter-by-name', type=str, nargs='+', default=[],
                        help='Optional list of names to filter results on. Must be in quotes. '
                             'Can be used multiple times.')
    parser.add_argument('--hestia-impact-id', type=str, nargs='+',
                        help='Run conversion from HESTIA ImpactAssessment.')


def _option_kwargs(option):
    """Translate a registry Option into argparse keyword arguments."""
    kwargs = {'help': option.help, 'default': option.default}
    if option.action:
        kwargs['action'] = option.action
        return kwargs
    kwargs['type'] = option.type
    if option.choices:
        kwargs['choices'] = list(option.choices)
    if option.nargs:
        kwargs['nargs'] = option.nargs
    return kwargs


def _add_converter_arguments(parser):
    """Give every converter its own ``--<name>-<option>`` argument group."""
    for name, spec in sorted(SPECS.items()):
        if not spec.options:
            continue
        group = parser.add_argument_group(f'{spec.label} options')
        for option in spec.options:
            group.add_argument(f'--{name}-{option.name}', **_option_kwargs(option))


def _build_parser():
    parser = argparse.ArgumentParser(
        'Convert files between HESTIA format and other formats.',
        epilog='Supported conversions:\n' + '\n'.join(
            f'  {conversion.source} -> {conversion.target}  ({spec.summary})'
            for spec in sorted(SPECS.values(), key=lambda s: s.name)
            for conversion in spec.conversions
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    _add_general_arguments(parser)
    _add_converter_arguments(parser)
    return parser


def _load_hestia_impacts(impact_ids):
    from hestia_earth.converters.base.loaders import load_hestia_model_from_id
    return [load_hestia_model_from_id(impact_id) for impact_id in impact_ids]


def _load_hestia_jsonld(filepath):
    from hestia_earth.converters.base.loaders import load_hestia_model_from_file
    from pathlib import Path
    path = Path(filepath)
    return [load_hestia_model_from_file(path.parent, path)]


def _load_data(args, conversion, spec):
    """Load the input, by whichever route the arguments describe.

    ``--hestia-impact-id`` and a single ``.jsonld`` are HESTIA-specific shortcuts
    the registry does not model; everything else goes through the loader the
    conversion declares.
    """
    if args.input_format == HESTIA and args.hestia_impact_id:
        return _load_hestia_impacts(args.hestia_impact_id)
    if not args.input_file:
        raise SystemExit('--input-file is required (or --hestia-impact-id for HESTIA input)')
    if args.input_format == HESTIA and args.input_file.endswith('.jsonld'):
        return _load_hestia_jsonld(args.input_file)
    loader_options = (
        {'mapping_files_directory': args.mapping_files_directory}
        if conversion.load_needs_mapping_files else {}
    )
    try:
        return load_callable(conversion.load)(args.input_file, **loader_options)
    except ModuleNotFoundError as err:
        raise SystemExit(
            f"Please install 'hestia-earth-converters[{spec.extra}]' first ({err})."
        )


def _download_flowmaps_if_required(folder):
    if not os.path.exists(folder):
        logging.error('Flowmaps directory not found, downloading latest available')
        from hestia_earth.converters.utils.flowmaps import download_flowmaps
        download_flowmaps(folder)


def _convert_callable(spec, conversion):
    try:
        return load_callable(conversion.convert)
    except ModuleNotFoundError as err:
        raise SystemExit(
            f"Please install 'hestia-earth-converters[{spec.extra}]' first ({err})."
        )


def _chained(source, target):
    """Reach `target` from `source` by way of HESTIA, when nothing does it directly."""
    (spec, first), (second_spec, second) = find_chain(SPECS, source, target)
    # up front, so a missing extra is reported before the first leg writes anything
    _convert_callable(spec, first)
    _convert_callable(second_spec, second)
    return spec, first, lambda data, **options: run_chain(first, second, data, **options)


def _resolve(args):
    """The conversion to run, and the loader-bearing pair that reads its input."""
    try:
        spec, conversion = find(SPECS, args.input_format, args.output_format)
    except LookupError:
        return _chained(args.input_format, args.output_format)
    return spec, conversion, _convert_callable(spec, conversion)


def main():
    args = _build_parser().parse_args()

    try:
        spec, conversion, convert = _resolve(args)
    except LookupError as err:
        raise SystemExit(str(err))

    _download_flowmaps_if_required(args.mapping_files_directory)

    data = _load_data(args, conversion, spec)
    if not data:
        raise SystemExit(f'Could not load any data from {args.input_format} format.')

    convert(data, **vars(args))


if __name__ == '__main__':
    sys.exit(main())
