# This file exists within 'dob-viewer':
#
#   https://github.com/hotoffthehamster/dob-viewer
#
# Copyright © 2018-2020 Landon Bouma. All rights reserved.
#
# This program is free software:  you can redistribute it  and/or  modify it under the
# terms of the GNU General Public License as published by the Free Software Foundation,
# either version 3  of the License,  or  (at your option)  any later version  (GPLv3+).
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY;  without even the implied warranty of MERCHANTABILITY or  FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU  General  Public  License  for  more  details.
#
# If you lost the GNU General Public License that ships with this software
# repository (read the 'LICENSE' file), see <http://www.gnu.org/licenses/>.

"""A time tracker for the command line. Utilizing the power of nark."""

from gettext import gettext as _

import copy
import re
import sys
from datetime import datetime, time, timedelta

from nark.helpers.parsing import parse_factoid

from dob_bright.termio import (
    attr,
    barf_and_exit,
    bg,
    click_echo,
    echo_block_header,
    fg
)

from .fact_dressed import FactDressed
from .fix_times import (
    DEFAULT_SQUASH_SEP,
    mend_facts_times,
    must_complete_times,
    reduce_time_hint,
    then_extend_fact
)
from .parse_mistakes import prepare_log_msg

__all__ = (
    'parse_input',
)


def parse_input(controller, file_in=None, progress=None):
    """
    Import Facts from STDIN or a file.
    """

    # MAYBE/2018-05-16 00:11: (lb): Parse whole file before prompting.
    #                               Allow --yes to work here, too?
    #                               Not sure...
    #   For now, we'll just read one fact at a time.

    def _parse_input():
        raw_facts = parse_facts_input(file_in)
        new_facts = must_hydrate_facts(raw_facts)
        conflicts = must_complete_times(controller, new_facts, progress=progress)
        controller.affirm(not conflicts)  # (lb): 2019-01-19: This happen?
        repair_shoulder_fact_times(new_facts, raw_facts)
        must_not_conflict_existing(new_facts)
        return new_facts

    # ***

    def parse_facts_input(file_in):
        # A few reminders:
        # - On `dob import`, file_in is None -- but on `dob import -`,
        #   Click sets file_in to <stdin> _io.TextIOWrapper.
        # - On `echo '...' | dob import` → isatty() is False, but
        #   on `dob import | cat`        → isatty() is True.
        # - Dob will print help on `dob import`, but if the user runs
        #   `dob import -` then dob waits for input from the user, and
        #   processes it line by line (whenever the user his ENTER).
        #   This is expected behavior -- that's what the special `-`
        #   filename convention does in *nix -- but I thought I'm spell
        #   it out, in case it feels odd to new developers.
        input_f = file_or_stdin(file_in)
        raw_facts = parse_facts_from_stream(input_f)
        return raw_facts

    def file_or_stdin(file_in):
        if file_in is not None:
            return file_in
        return sys.stdin

    def parse_facts_from_stream(input_f):
        # Track current empty line run count, to know when to check if line
        # starts a new Fact. Keep at -1 until we've seen the first fact.
        bl_count = -1

        line_num = 0

        # Coalesce each Fact, line by line.
        current_fact_dict = None
        accumulated_fact = []
        unprocessed_facts = []

        progress and progress.click_echo_current_task(_('Parsing factoids...'))
        for line in input_f:
            line_num += 1
            (
                bl_count, processed,
            ) = gobble_blank_or_continued(line, accumulated_fact, bl_count)
            if processed:
                continue
            fact_dict = gobble_if_not_new_fact(line, accumulated_fact, bl_count)
            if fact_dict is None:
                bl_count = 0
                continue
            fact_dict['parsed_source.line_num'] = line_num
            fact_dict['parsed_source.line_raw'] = line
            if not accumulated_fact:
                # First Fact.
                assert current_fact_dict is None
                assert not accumulated_fact
                current_fact_dict = fact_dict
                accumulated_fact.append(line)
                bl_count = 0
                continue
            else:
                assert current_fact_dict is not None
                assert accumulated_fact

            # Woot, woot! We parsed a complete Fact.
            if current_fact_dict:
                unprocessed_facts.append((current_fact_dict, accumulated_fact,))
            current_fact_dict = fact_dict
            accumulated_fact = [line, ]
            bl_count = 0

        # end: for

        if accumulated_fact:
            unprocessed_facts.append((current_fact_dict, accumulated_fact,))
        else:
            msg = _('What is this, an empty file?')
            barf_and_exit(msg)

        return unprocessed_facts

    def gobble_blank_or_continued(line, accumulated_fact, blank_line_count):
        processed = False
        if blank_line_count < 0:
            # Start of file.
            assert blank_line_count == -1
            if not line.strip():
                controller.client_logger.debug(_('Skip premature blank line'))
                # Skip blank lines pre-Facts.
                processed = True
            # else, we found the first non-blank line. We now
            #   expect to find the date:time & meta, or death.
        elif not line.strip():  # remove trailing newline
            controller.client_logger.debug(_('- Blank line in desc.'))
            accumulated_fact.append(line)
            blank_line_count += 1
            processed = True
        elif blank_line_count == 0:
            controller.client_logger.debug(_('- Part of desc.:\n') + line.strip())
            accumulated_fact.append(line)
            processed = True
        elif re.match(r'^\s', line):
            controller.client_logger.debug(_('- Leading whitesp.:\n') + line.strip())
            accumulated_fact.append(line)
            processed = True
        # else, any content that follows blank line(s) is either:
        # (1) more content; (2) a new Fact; or (3) Fact separator.
        return (blank_line_count, processed)

    def gobble_if_not_new_fact(line, accumulated_fact, blank_line_count):
        fact_dict = dissect_meta_line(line)
        if fact_dict is None:
            # If not parsed, and first line we've seen, die.
            missing_fact_must_come_early(line, blank_line_count)
            # else, more content, or a Fact separator.
            controller.client_logger.debug(_('- More desc.: ') + line.strip())
            accumulated_fact.append(line)
        return fact_dict

    def missing_fact_must_come_early(line, blank_line_count):
        if blank_line_count == -1:
            msg = (
                '{}: {}{}{}{}{}\n'
                .format(
                    _('The first nonempty line is not a recognized Fact.'),
                    bg('white'), fg('black'), attr('underlined'),
                    line.strip(),
                    attr('reset'),
                )
            )
            barf_and_exit(msg)

    # FIXME/2019-01-21: Document all the different usage, both in test, and README.
    # E.g., "then: I did this" should be same as "at +0: I did this"
    #   but if you specify start you don't need colon
    #       "then 2019-01-21 23:47 I did this" should also work...
    #   FIXME: Parse "then" expecting 1+ datetimes, or "then:" expecting none.
    #   FIXME: Parse "still" expecting 1+ datetimes, or "still:" expecting none.
    # FIXME: Three tests:
    #           ``still <time-spec> <desc>``
    #       vs. ``still: <desc>``
    #       vs. ``still blah``
    RE_TIME_HINT = re.compile(
        # SYNC_ME: RE_TIME_HINT, TIME_HINT_MAP, and @generate_add_fact_command's.
        r'^('
            # Skipping: Doesn't make sense: '(?P<verify_none>on|now)'
            # MAYBE/2019-01-22: Is "between" okay here?
            #   We don't have a Click alias for it, so
            #   there's a `dob from` command, but not `dob between`,
            #   so maybe we want to remove "between" from here.
            '(?P<verify_both>from|between)'
            '|(?P<verify_start>at)'  # noqa: E131
            '|(?P<verify_end>to|until)'
            '|(?P<verify_then_none>then:)'
            '|(?P<verify_then_some>then)'
            '|(?P<verify_still_none>still:)'
            '|(?P<verify_still_some>still)'
            # NOTE: Require colon postfix for options w/o time component.
            # NOTE: 'now' would be confusing and conflict with other usage,
            #       (at least I think it would?). E.g., do not do this:
            #         '|(?P<verify_after>after:|since:|next:|now:)'
            '|(?P<verify_after>after:|since:|next:)'
        ' )',  # NOTE The SPACE CHARACTER following THE DIRECTIVE!
        re.IGNORECASE,
    )

    def dissect_meta_line(line):
        sussed_hint, time_hint, line = suss_the_time_format(line)
        fact_dict, err = run_parser(line, time_hint)
        if not fact_dict['start'] and not fact_dict['end']:
            controller.client_logger.debug(_('unmeta: {}'.format(err)))
            fact_dict = None
        else:
            # The user could specify, e.g., just a single datetime (followed
            # by a description), and we can fill in the other datetime and
            # ask the user for more details about the fact.
            #   Not always True:  assert not err
            controller.client_logger.debug(_(
                # Including the description is too verbose:
                #   'new fact_dict: {}'.format(fact_dict)
                # so let's make a complicated dictionary comprehension instead.
                'new fact_dict: {}'.format(
                    {
                        key: val if key != 'description'
                        else val[:10] + ((len(val) > 10) and '...' or '')
                        for key, val in fact_dict.items()
                    }
                )
            ))
        return fact_dict

    def suss_the_time_format(line):
        sussed_hint = ''
        match = RE_TIME_HINT.match(line)
        if match is not None:
            for hint, matched in match.groupdict().items():
                if matched is not None:
                    assert not sussed_hint
                    sussed_hint = hint
                    # Remove the time hint prefix.
                    line = RE_TIME_HINT.sub('', line).lstrip()
        time_hint = sussed_hint or 'verify_start'
        controller.client_logger.debug(_(
            'time_hint: {}{}'.format(
                time_hint, ' [sussed]' if sussed_hint else ' [default]',
            )
        ))
        return sussed_hint, time_hint, line

    def run_parser(line, time_hint):
        # Parse the line as if it were a new fact. And don't strip the line.
        # Parser expects metadata to be separated from description (so leave
        # the newline). And we can be strict and require that the date data
        # starts the line.

        # Per dob-insert commands, parser expects iterable.
        factoid = (line,)

        use_hint = reduce_time_hint(time_hint)

        fact_dict, err = parse_factoid(
            factoid=factoid,
            time_hint=use_hint,
            # MEH: (lb): Should we leave hash_stamps='#@' ?
            #   I sorta like using the proper tag symbol
            #   when not worried about shell interpolation.
            hash_stamps='#',
            # We'll handle errors ourselves, later, in bulk, either
            # `--ask`'ing the user for more Fact details, or barfing
            # all the errors and exiting.
            lenient=True,
        )

        fact_dict_set_time_hint(fact_dict, time_hint)

        return fact_dict, err

    # FIXME/DRY: See create.py/transcode.py (places that use "+0").
    def fact_dict_set_time_hint(fact_dict, time_hint):
        fact_dict['time_hint'] = time_hint
        if time_hint in ('verify_after', 'verify_then_none', 'verify_still_none'):
            assert not fact_dict['start'] and not fact_dict['end']
            # (lb): How's this for a hack!?
            fact_dict['start'] = "+0"

    # ***

    def must_hydrate_facts(raw_facts):
        new_facts, hydrate_errs = hydrate_facts(raw_facts)
        must_hydrated_all_facts(hydrate_errs)
        return new_facts

    def hydrate_facts(raw_facts):
        new_facts = []
        hydrate_errs = []
        temp_id = -1
        progress and progress.click_echo_current_task(_('Hydrating Facts...'))
        for fact_dict, accumulated_fact in raw_facts:
            add_hydration_warnings(fact_dict, hydrate_errs)
            hydrate_description(fact_dict, accumulated_fact)
            new_fact, err_msg = create_fact_from_parsed_dict(fact_dict)
            if new_fact:
                assert not err_msg
                time_hint = fact_dict['time_hint']
                temp_id = add_new_fact(new_fact, time_hint, temp_id, new_facts)
            else:
                assert not new_fact
                hydrate_errs.append(err_msg)
        return new_facts, hydrate_errs

    def add_hydration_warnings(fact_dict, hydrate_errs):
        if not fact_dict['warnings']:
            return
        fact_warnings = '\n  '.join(fact_dict['warnings'])
        display_dict = copy.copy(fact_dict)
        del display_dict['warnings']
        err_msg = prepare_log_msg(display_dict, fact_warnings)
        hydrate_errs.append(err_msg)

    # Horizontal rule separator matches same character repeated at least thrice.
    # FIXME/2018-05-18: (lb): Document: HR is any repeated one of -, =, #, |.
    FACT_SEP_HR = re.compile(r'^([-=#|])\1{2}\1*$')

    def hydrate_description(fact_dict, accumulated_fact):
        # The first entry is the meta line, so ignore it.
        desc_lines = accumulated_fact[1:]
        # However, the user is allowed to start the description
        # on the meta line.
        if fact_dict['description']:
            # Don't forget the newline -- it got culled (strip()ped) on parse.
            desc_lines.insert(0, fact_dict['description'] + "\n")

        cull_factless_fact_separator(desc_lines)

        # desc_lines are already newlined.
        desc = ''.join(desc_lines).strip()

        fact_dict['description'] = desc

    def cull_factless_fact_separator(desc_lines):
        # To make import file more readable, user can add
        # separator line between facts. Cull it if found.
        # MAYBE/2018-05-18: (lb): Make this operation optional?
        while len(desc_lines) > 0 and not desc_lines[-1].strip():
            desc_lines.pop()

        if (
            len(desc_lines) > 1
            and not desc_lines[-2].strip()
            and FACT_SEP_HR.match(desc_lines[-1])
        ):
            # Remove optional horizontal rule, which user
            # can use to visual separate lines in import file.
            desc_lines.pop()

        return desc_lines

    def create_fact_from_parsed_dict(fact_dict):
        new_fact = None
        err_msg = None
        try:
            new_fact = FactDressed.create_from_parsed_fact(
                fact_dict,
                lenient=True,
                line_num=fact_dict['parsed_source.line_num'],
                line_raw=fact_dict['parsed_source.line_raw'],
            )
        except ValueError as err:
            err_msg = prepare_log_msg(fact_dict, str(err))
        # (lb): Returning a tuple that smells like Golang: (fact, err, ).
        return new_fact, err_msg

    def add_new_fact(new_fact, time_hint, temp_id, new_facts):
        if new_fact is None:
            return temp_id
        add_facts = maybe_squash_extend_prev(new_fact, time_hint, new_facts)
        for add_fact in add_facts:
            temp_id = add_new_fact_maybe(add_fact, temp_id, new_facts)
        return temp_id

    def maybe_squash_extend_prev(new_fact, time_hint, new_facts):
        if time_hint not in [
            'verify_end',
            'verify_then_none',
            'verify_then_some',
            'verify_still_none',
            'verify_still_some',
        ]:
            return [new_fact]
        return squash_extend_if_prev(new_fact, time_hint, new_facts)

    def squash_extend_if_prev(new_fact, time_hint, new_facts):
        prev_fact = new_facts[-1] if new_facts else None
        new_new_facts = squend_check_noop_or_extend(prev_fact, new_fact, time_hint)
        if new_new_facts is not None:
            return new_new_facts
        return squend_squash_or_extend(prev_fact, new_fact, time_hint, new_facts)

    def squend_check_noop_or_extend(prev_fact, new_fact, time_hint):
        if prev_fact is None:
            # First fact in import. Will check store later for boundary fact.
            return [new_fact]
        elif prev_fact.end is not None:
            return maybe_set_start_maybe_extend_prev(
                prev_fact, new_fact, time_hint,
            )
        elif prev_fact.start is None:
            return [new_fact]
        elif new_fact.start and new_fact.end:
            # This fact is a `then ... to`, so really just a `from/between`.
            return [new_fact]
        # E.g., prev_fact is at-fact, with no end; and new_fact is to-fact,
        # with no start. Return None, and caller will squend_squash_or_extend.
        assert not new_fact.deleted
        return None

    def squend_squash_or_extend(prev_fact, new_fact, time_hint, new_facts):
        squash_facts_maybe(new_fact, prev_fact)
        if not new_fact.deleted:
            return [new_fact]

        prev_or_new_fact = maybe_extend_fact(controller, prev_fact, time_hint)
        # Return None is prev fact was extended.
        if prev_or_new_fact is not new_fact:
            assert prev_or_new_fact is new_facts[-1]
        return [prev_or_new_fact] if prev_or_new_fact is new_fact else []

    def maybe_set_start_maybe_extend_prev(prev_fact, new_fact, time_hint):
        if time_hint in [
            'verify_then_none',
            'verify_then_some',
            'verify_still_none',
            'verify_still_some',
        ]:
            extended_fact = then_extend_fact(controller, prev_fact)
            extended_fact.end = new_fact.start
            if time_hint in ['verify_still_none', 'verify_still_some']:
                new_fact.activity = prev_fact.activity
                new_fact.tags = list(prev_fact.tags)
            return [extended_fact, new_fact]
        else:
            assert time_hint == 'verify_end'  # ``to``
            assert new_fact.start is None
            # Fix it in post, er, fix_delta_time_relative.
            new_fact.start = "+0"
            return [new_fact]

    def squash_facts_maybe(new_fact, prev_fact):
        assert new_fact.pk is None
        # 2018-06-30: (lb): Ug. Why does this squash logic seem so forced?
        new_fact.pk = -1  # To appease squash().
        prev_fact.squash(new_fact, DEFAULT_SQUASH_SEP)
        new_fact.pk = None

    def maybe_extend_fact(controller, new_fact, time_hint):
        if time_hint not in ['verify_then_none', 'verify_then_some']:
            return new_fact
        return then_extend_fact(controller, new_fact)

    def add_new_fact_maybe(new_fact, temp_id, new_facts):
        if not new_fact or new_fact.deleted:
            return temp_id
        new_fact.pk = temp_id
        new_facts.append(new_fact)
        temp_id -= 1
        return temp_id

    # ***

    def must_hydrated_all_facts(hydrate_errs):
        if not hydrate_errs:
            return
        for err_msg in hydrate_errs:
            click_echo()
            click_echo(err_msg)
            click_echo()
        msg = (_(
            'Please fix your import data and try again.'
            ' Scroll up for details.'
        ))
        barf_and_exit(msg)

    # ***

    def repair_shoulder_fact_times(new_facts, raw_facts):
        # If user did not specify first fact start time, and/or last fact's
        # end time, fix_times.must_complete_times will have assigned times.
        # Note that there are a number of places where two facts may be merged:
        # - Earlier during this import task, under add_new_fact(), squash and
        #   extend code is used.
        # - Here, but just on the first and final import facts, to sew them
        #   into the fabric of the facts in the store.
        # - In many of the fix_times functions:
        #     unite_and_stretch, insert_forcefully, and resolve_overlapping.
        # (lb): Maybe someday we can combine this code better. The way different
        #   features were grown, however, resulted in squash code in a few places.

        if not new_facts[0].start:
            # User did not specify first fact's start, and we did not find
            # an antecedent fact in the data store, so, what? Set earliest
            # time ever? Clip to midnight that day?
            new_facts[0].start = datetime.combine(new_facts[0].end, time.min)
            if new_facts[0].start == new_facts[0].end:
                new_facts[0].start = new_facts[0].end - timedelta(days=1)
        elif not raw_facts[0][0]['start']:
            new_facts[0].start = None

        controller.affirm(new_facts[-1].end)
        if not raw_facts[-1][0]['end']:
            new_facts[-1].end = None

    # ***

    def must_not_conflict_existing(new_facts):
        # (lb): Yuck. Sorry about this. Totally polluting what was a small
        # function with lots of progress-output overhead.
        task_descrip = _('Verifying times nonconflicting')
        if progress is not None:
            term_width, dot_count, fact_sep = progress.start_crude_progressor(
                task_descrip,
            )

        could_be_more = fix_range_conflicts_easy(new_facts)

        all_conflicts = []
        for idx, fact in enumerate(new_facts):
            if progress is not None:
                term_width, dot_count, fact_sep = progress.step_crude_progressor(
                    task_descrip, term_width, dot_count, fact_sep,
                )

            if idx == len(new_facts) - 1:
                time_hint = 'verify_last'
            elif idx == 0 and not fact.start:
                time_hint = 'verify_end'
            else:
                time_hint = 'verify_both'

            conflicts = mend_facts_times(
                controller,
                fact,
                time_hint=time_hint,
                skip_store=not could_be_more,
            )
            assert not fact.deleted  # Only on squash, which shouldn't happen.

            if conflicts:
                all_conflicts.append((fact, conflicts,))

        progress and progress.click_echo_current_task('')

        # Handling anything other than the ongoing (active) fact seems
        # beyond the scope of the import function. dob is not a conflict
        # resolution application!
        barf_on_overlapping_facts_old(all_conflicts)

    def fix_range_conflicts_easy(new_facts):
        """
        Check time range of facts being imported against store.
        Returns True if time range overlaps with any existing facts.
        The active fact overlaps if it starts during the time range.
        """
        def _fix_range_conflicts_easy():
            first_time = new_facts[0].start or new_facts[0].end
            final_time = new_facts[-1].end or new_facts[-1].start
            # There's a compelling function, controller.facts.strictly_during,
            # but it ignores overlapping shoulder facts (and the active fact).
            # Check instead manually against the fact before the last time,
            # and also the fact after the first time.
            ante_fact = controller.facts.antecedent(ref_time=final_time)
            seqt_fact = controller.facts.subsequent(ref_time=first_time)
            check_all = False
            check_all = easy_fix_final(check_all, seqt_fact, final_time)
            check_all = easy_fix_first(check_all, ante_fact, first_time)
            return check_all

        def easy_fix_final(check_all, seqt_fact, final_time):
            if not seqt_fact:
                return check_all
            if seqt_fact.start < final_time:
                # Fact found in store ends after first_time,
                #  and starts before final_time.
                return True
            if new_facts[-1].end:
                # The final import fact ends on or before
                #  the start of the after-fact.
                return check_all
            # If here, the final import fact is endless.
            # Obvious path is to end it using start of first fact after.
            if new_facts[-1].start == seqt_fact.start:
                # The ongoing final import fact starts at same time as final store fact.
                if not seqt_fact.end:
                    # To test this path: have ongoing fact in import with same start as
                    # ongoing (active) fact in the store.
                    # (lb): We could end the existing fact, making it momentaneous,
                    # but that seems extreme; feels more natural to combine the 2
                    # facts. -- Alternatively, we could toss an error. But I could
                    # see just combining the 2 facts, seems like a pretty rare case
                    # anyway.
                    seqt_fact.squash(new_facts[-1], DEFAULT_SQUASH_SEP)
                    new_facts[-1] = seqt_fact
                    return check_all
                if not controller.config['time.allow_momentaneous']:
                    # User is not allowing momentaneous Facts, so disallow.
                    # Return True and we'll run thorough conflict check
                    # (and exit on error instead of continuing import).
                    return True
                # The final import fact is endless, but it conflicts with a
                # fact in the store. And the user is down with momentaneous
                # so crown it as such.
                # FIXME/2019-01-20 01:09: TESTME: Momentaneous feature.
                #  (I think it works in db and code, but no way for user
                #  to view in the UI. So they're essentially really well
                #  hidden. Which is a design feature, maybe?)
            new_facts[-1].end = seqt_fact.start
            return check_all

        def easy_fix_first(check_all, ante_fact, first_time):
            if not ante_fact:
                return check_all
            if ante_fact.end:
                if not new_facts[0].start:
                    # I.e., a to-Fact is 1st Fact in import,
                    #   and final Fact in store is closed.
                    new_facts[0].start = ante_fact.end
                if ante_fact.end > first_time:
                    # Fact from store starts before final_time,
                    #  and ends after first_time.
                    return True
                # else, first fact found before final_time
                #        ends before first_time.
                return check_all
            # If here, it's the ongoing (active) fact.
            ongoing = controller.facts.endless()
            controller.affirm(ante_fact == ongoing[0])
            if new_facts[0].start:
                ante_fact.end = new_facts[0].start
                new_facts.insert(0, ante_fact)
            else:
                # Squash store's ongoing (active) and import's first.
                ante_fact.squash(new_facts[0], DEFAULT_SQUASH_SEP)
                new_facts[0] = ante_fact

            return check_all

        return _fix_range_conflicts_easy()

    def barf_on_overlapping_facts_old(conflicts):
        if not conflicts:
            return

        for fact, resolved_edits in conflicts:
            for edited_fact, original in resolved_edits:
                echo_saved_fact_conflict(fact, edited_fact, original)
        msg = _(
            'One or more new Facts would conflict with existing Facts.'
            ' This Is Not Allowed'
        )
        barf_and_exit(msg)

    def echo_saved_fact_conflict(fact, edited_fact, original):
        echo_block_header(_('Saved Fact Datetime Conflict!'))
        click_echo()
        click_echo(_('  Fact being imported:'))
        click_echo(_('  ───────────────────'))
        click_echo(fact.friendly_diff(fact, truncate=True))
        click_echo()
        click_echo(_('  Impact on saved Fact:'))
        click_echo(_('  ────────────────────'))
        click_echo(original.friendly_diff(edited_fact))
        click_echo()

    # ***

    return _parse_input()

