# This file exists within 'dob':
#
#   https://github.com/hotoffthehamster/dob
#
# Copyright © 2018-2020 Landon Bouma,  2015-2016 Eric Goller.  All rights reserved.
#
# 'dob' 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.
#
# 'dob' 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.
#
# You can find the GNU General Public License reprinted in the file titled 'LICENSE',
# or visit <http://www.gnu.org/licenses/>.

from gettext import gettext as _

import sys
from inflector import English, Inflector

from nark.managers.query_terms import QueryTerms

from dob_bright.reports.render_results import render_results
from dob_bright.termio import (
    click_echo,
    dob_in_user_exit,
    dob_in_user_warning,
    highlight_value
)

from ..clickux.cmd_options_search import cmd_options_output_format_facts_only
from ..clickux.query_assist import error_exit_no_results

__all__ = (
    'list_facts',
)


def list_facts(
    controller,
    # - Save for controller, all parameters are CLI --options.
    # - The named keyword arguments listed first are used for output
    #   formatting and not for the query.
    # - Args: Select columns to output.
    show_usage=False,
    show_duration=False,
    hide_description=False,
    column=None,
    # - Args: Output formatter processor writer.
    output_format='csv',
    # - Args: Format- and Row-specific arguments.
    table_type='texttable',  # Applies when output_format == 'table'.
    max_width=-1,
    row_limit=None,
    factoid_rule='',
    # - Args: Output constraints.
    output_path=None,
    # - Args: Cell-specific arguments.
    spark_total=None,
    spark_width=None,
    spark_secs=None,
    # - Args: Total reporting arguments.
    show_totals=False,
    hide_totals=False,
    # - Developer controls.
    re_sort=False,
    # - Any unnamed arguments are used as search terms in the query.
    *args,
    # - All remaining keyword arguments correspond to nark.QueryTerms.
    **kwargs
):
    """
    Finds and lists facts according to specified filtering, sorting and display options.

    Writes to stdout, or to the file specified by ``output_path``.

    Arguments:
        Most of the arguments are documented elsewhere.

    Returns:
        None: If success.
    """
    format_restricted = output_format in cmd_options_output_format_facts_only()

    def _list_facts():
        # MAYBE: (lb): Verify QueryTerms output format options now, before searching.
        # - When I refactored query_terms, I moved validation later, so, e.g., you
        #   could specify an incorrect output_format, but it won't get caught until
        #   after find_facts returns. Which means the code does unnecessary processing,
        #   and the user has to wait a little longer until they're told they're wrong.
        qt = prepare_query_terms(*args, **kwargs)
        results = find_facts(controller, query_terms=qt)
        if not results:
            error_exit_no_results(_('facts'))
        n_total = len(results)
        n_written = display_results(results, qt, output_path)
        report_report_written(controller, output_path, n_total, n_written)

    # ***

    def prepare_query_terms(*args, **kwargs):
        qt = QueryTerms(*args, **kwargs)
        must_grouping_allowed(qt)
        qt.include_stats = should_include_stats(qt)
        qt.sort_cols = decide_sort_cols(qt)
        return qt

    def must_grouping_allowed(qt):
        if not qt.is_grouped or not format_restricted:
            return

        dob_in_user_exit(_(
            'ERROR: Specified format type does not support grouping results.'
        ))

    # Note that the two complementary commands, dob-list and dob-usage,
    # have complementary options, --show-usage and --hide-usage options,
    # and --show-duration and --hide-duration, that dictate if we need
    # the query to include the aggregate columns or not.
    def should_include_stats(qt):
        # If preparing a Factoid export, the FactoidWriter
        # expects a list of items, and not prepared tuples.
        if format_restricted:
            return False

        # If a group-by is specified, the report will show the aggregated names,
        # such as showing 'Activities' instead of 'Activity'. Include the extra
        # aggregate columns.
        if qt.is_grouped:
            return True

        # Not grouping results, so each result is a Fact, but check if the user
        # wants the 'Duration'. (It's an either-or with the SQL code, either you
        # retrieve just item columns, or you retrieve item columns and all extra
        # columns, whether or not you want them all.)
        # - The 'duration' is a simple julianday() - julianday() value, which
        #   the tabulate_results.prepare_duration may or may not use (it might
        #   call fact.delta instead) so really adding stats just to get the
        #   'Duration' is about as unnecessary as this comment.
        # - Ignore `show_usage` -- 'uses' obviously 1 because not qt.is_grouped.
        # - Check for 'sparkline' output, which is calculated from 'duration'.
        #   - Again, we could just do this during post-processing in tabulate_results.
        if (
            show_duration
            and (
                not column
                or 'duration' in column
                or 'sparkline' in column
            )
        ):
            return True

        return False

    def decide_sort_cols(qt):
        if qt.sort_cols is not None:
            return qt.sort_cols

        # If request includes the 'duration' column, might as well use it.
        if not qt.include_stats or not show_usage:
            # No 'duration' column, or user not asking to see it.
            return ('start',)  # The get_all default.
        return ('time',)  # Aka, sort-by 'duration'.

    # ***

    def find_facts(controller, **kwargs):
        """
        Search for one or more facts, given a set of search criteria and sort options.

        Args:
            query_terms: A QueryTerms object that defines the query.

            **kwargs: Alternatively, pass any or all of the QueryTerms attributes.

        Returns:
            A list of matching Facts, or matching (Fact, *statistics) tuples,
            depending on the QueryTerms.
        """
        try:
            return controller.facts.get_all(**kwargs)
        except Exception as err:
            # - NotImplementedError happens if db.engine != 'sqlite', because
            #   get_all uses SQLite-specific aggregate functions.
            # - ParserInvalidDatetimeException happens on bad since or until.
            dob_in_user_exit(str(err))

    # ***

    def display_results(results, qt, output_path):
        row_limit = suss_row_limit(qt)
        n_written = render_results(
            controller,
            results,
            query_terms=qt,
            show_usage=show_usage,
            show_duration=show_duration,
            hide_description=hide_description,
            custom_columns=column,
            output_format=output_format,
            table_type=table_type,
            max_width=max_width,
            row_limit=row_limit,
            factoid_rule=factoid_rule,
            output_path=output_path,
            spark_total=spark_total,
            spark_width=spark_width,
            spark_secs=spark_secs,
            show_totals=show_totals,
            hide_totals=hide_totals,
            re_sort=re_sort,
        )
        return n_written

    def suss_row_limit(qt):
        # Limit the number of rows dumped, unless user specified --limit,
        # or if not dumping to the terminal.
        _row_limit = row_limit
        # Note: Not caring about qt.limit here, as the query limit is
        # a separate concern from the row limit.
        if _row_limit is None and not output_path and sys.stdout.isatty():
            _row_limit = controller.config['term.row_limit']
        return _row_limit

    # ***

    def report_report_written(controller, output_path, n_total, n_written):
        if (
            not output_path
            or not sys.stdout.isatty()
            # (lb): I don't quite like this coupling, but it works:
            #       - Don't click_echo if carousel_active.
            or controller.ctx.command.name == 'edit'
        ):
            # If writ to stdout or pager, skip count and path report.
            return
        # Otherwise, path was formed from, e.g., "export.{format}", so display actual.

        if n_written < n_total:
            echo_warn_if_truncated(controller, n_written, n_total)

        click_echo(_(
            "Wrote {n_written} {facts} to {output_path}"
        ).format(
            n_written=highlight_value(n_written),
            facts=Inflector(English).conditional_plural(n_written, _('Fact')),
            output_path=highlight_value(output_path),
        ))

    def echo_warn_if_truncated(controller, n_results, n_rows):
        if n_results <= n_rows:
            return

        dob_in_user_warning(_(
            'Showed only {} of {} results. Use `-C term.row_limit=0` to see all results.'
        ).format(format(n_results, ','), format(n_rows, ',')))

    # ***

    _list_facts()

