# Copyright © 2012-2023 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html

import argparse
import re
import textwrap

from jrnl.commands import postconfig_decrypt
from jrnl.commands import postconfig_encrypt
from jrnl.commands import postconfig_import
from jrnl.commands import postconfig_list
from jrnl.commands import preconfig_diagnostic
from jrnl.commands import preconfig_version
from jrnl.output import deprecated_cmd
from jrnl.plugins import EXPORT_FORMATS
from jrnl.plugins import IMPORT_FORMATS
from jrnl.plugins import util


class WrappingFormatter(argparse.RawTextHelpFormatter):
    """Used in help screen"""

    def _split_lines(self, text: str, width: int) -> list[str]:
        text = text.split("\n\n")
        text = map(lambda t: self._whitespace_matcher.sub(" ", t).strip(), text)
        text = map(lambda t: textwrap.wrap(t, width=56), text)
        text = [item for sublist in text for item in sublist]
        return text


class IgnoreNoneAppendAction(argparse._AppendAction):
    """
    Pass -not without a following string and avoid appending
    a None value to the excluded list
    """

    def __call__(self, parser, namespace, values, option_string=None):
        if values is not None:
            super().__call__(parser, namespace, values, option_string)


def parse_not_arg(
    args: list[str], parsed_args: argparse.Namespace, parser: argparse.ArgumentParser
) -> argparse.Namespace:
    """
    It's possible to use -not as a precursor to -starred and -tagged
    to reverse their behaviour, however this requires some extra logic
    to parse, and to ensure we still do not allow passing an empty -not
    """

    parsed_args.exclude_starred = False
    parsed_args.exclude_tagged = False

    if "-not-starred" in "".join(args):
        parsed_args.starred = False
        parsed_args.exclude_starred = True
    if "-not-tagged" in "".join(args):
        parsed_args.tagged = False
        parsed_args.exclude_tagged = True
    if "-not" in args and not any(
        [parsed_args.exclude_starred, parsed_args.exclude_tagged, parsed_args.excluded]
    ):
        parser.error("argument -not: expected 1 argument")

    return parsed_args


def parse_args(args: list[str] = []) -> argparse.Namespace:
    """
    Argument parsing that is doable before the config is available.
    Everything else goes into "text" for later parsing.
    """
    parser = argparse.ArgumentParser(
        formatter_class=WrappingFormatter,
        add_help=False,
        description="Collect your thoughts and notes without leaving the command line",
        epilog=textwrap.dedent(
            """
        We gratefully thank all contributors!
        Come see the whole list of code and financial contributors at https://github.com/jrnl-org/jrnl
        And special thanks to Bad Lip Reading for the Yoda joke in the Writing section above :)"""  # noqa: E501
        ),
    )

    optional = parser.add_argument_group("Optional Arguments")
    optional.add_argument(
        "--debug",
        dest="debug",
        action="store_true",
        help="Print information useful for troubleshooting",
    )

    standalone = parser.add_argument_group(
        "Standalone Commands",
        "These commands will exit after they complete. You may only run one at a time.",
    )
    standalone.add_argument("--help", action="help", help="Show this help message")
    standalone.add_argument("-h", action="help", help=argparse.SUPPRESS)
    standalone.add_argument(
        "--version",
        action="store_const",
        const=preconfig_version,
        dest="preconfig_cmd",
        help="Print version information",
    )
    standalone.add_argument(
        "-v",
        action="store_const",
        const=preconfig_version,
        dest="preconfig_cmd",
        help=argparse.SUPPRESS,
    )
    standalone.add_argument(
        "--diagnostic",
        action="store_const",
        const=preconfig_diagnostic,
        dest="preconfig_cmd",
        help=argparse.SUPPRESS,
    )
    standalone.add_argument(
        "--list",
        action="store_const",
        const=postconfig_list,
        dest="postconfig_cmd",
        help="""
        List all configured journals.

        Optional parameters:

        --format [json or yaml]
        """,
    )
    standalone.add_argument(
        "--ls",
        action="store_const",
        const=postconfig_list,
        dest="postconfig_cmd",
        help=argparse.SUPPRESS,
    )
    standalone.add_argument(
        "-ls",
        action="store_const",
        const=lambda **kwargs: deprecated_cmd(
            "-ls", "--list or --ls", callback=postconfig_list, **kwargs
        ),
        dest="postconfig_cmd",
        help=argparse.SUPPRESS,
    )
    standalone.add_argument(
        "--encrypt",
        help="Encrypt selected journal with a password",
        action="store_const",
        metavar="TYPE",
        const=postconfig_encrypt,
        dest="postconfig_cmd",
    )
    standalone.add_argument(
        "--decrypt",
        help="Decrypt selected journal and store it in plain text",
        action="store_const",
        metavar="TYPE",
        const=postconfig_decrypt,
        dest="postconfig_cmd",
    )
    standalone.add_argument(
        "--import",
        action="store_const",
        metavar="TYPE",
        const=postconfig_import,
        dest="postconfig_cmd",
        help=f"""
        Import entries from another journal.

        Optional parameters:

        --file FILENAME (default: uses stdin)

        --format [{util.oxford_list(IMPORT_FORMATS)}] (default: jrnl)
        """,
    )
    standalone.add_argument(
        "--file",
        metavar="FILENAME",
        dest="filename",
        help=argparse.SUPPRESS,
        default=None,
    )
    standalone.add_argument("-i", dest="filename", help=argparse.SUPPRESS)

    compose_msg = """
    To add a new entry into your journal, simply write it on the command line:

        jrnl yesterday: I was walking and I found this big log.

    The date and the following colon ("yesterday:") are optional. If you leave
    them out, "now" will be used:

        jrnl Then I rolled the log over.

    Also, you can mark extra special entries ("star" them) with an asterisk:

        jrnl *And underneath was a tiny little stick.

    Please note that asterisks might be a special character in your shell, so you
    might have to escape them. When in doubt about escaping, put quotes around
    your entire entry:

        jrnl "saturday at 2am: *Then I was like 'That log had a child!'" """

    composing = parser.add_argument_group(
        "Writing", textwrap.dedent(compose_msg).strip()
    )
    composing.add_argument("text", metavar="", nargs="*")
    composing.add_argument(
        "--template",
        dest="template",
        help="Path to template file. Can be a local path, absolute path, or a path "
        "relative to $XDG_DATA_HOME/jrnl/templates/",
    )

    read_msg = (
        "To find entries from your journal, use any combination of the below filters."
    )
    reading = parser.add_argument_group("Searching", textwrap.dedent(read_msg))
    reading.add_argument(
        "-on", dest="on_date", metavar="DATE", help="Show entries on this date"
    )
    reading.add_argument(
        "-today-in-history",
        dest="today_in_history",
        action="store_true",
        help="Show entries of today over the years",
    )
    reading.add_argument(
        "-month",
        dest="month",
        metavar="DATE",
        help="Show entries on this month of any year",
    )
    reading.add_argument(
        "-day",
        dest="day",
        metavar="DATE",
        help="Show entries on this day of any month",
    )
    reading.add_argument(
        "-year",
        dest="year",
        metavar="DATE",
        help="Show entries of a specific year",
    )
    reading.add_argument(
        "-from",
        dest="start_date",
        metavar="DATE",
        help="Show entries after, or on, this date",
    )
    reading.add_argument(
        "-to",
        dest="end_date",
        metavar="DATE",
        help="Show entries before, or on, this date (alias: -until)",
    )
    reading.add_argument("-until", dest="end_date", help=argparse.SUPPRESS)
    reading.add_argument(
        "-contains",
        dest="contains",
        action="append",
        metavar="TEXT",
        help="Show entries containing specific text (put quotes around text with "
        "spaces)",
    )
    reading.add_argument(
        "-and",
        dest="strict",
        action="store_true",
        help='Show only entries that match all conditions, like saying "x AND y" '
        "(default: OR)",
    )
    reading.add_argument(
        "-starred",
        dest="starred",
        action="store_true",
        help="Show only starred entries (marked with *)",
    )
    reading.add_argument(
        "-tagged",
        dest="tagged",
        action="store_true",
        help="Show only entries that have at least one tag",
    )
    reading.add_argument(
        "-n",
        dest="limit",
        default=None,
        metavar="NUMBER",
        help="Show a maximum of NUMBER entries (note: '-n 3' and '-3' have the same "
        "effect)",
        nargs="?",
        type=int,
    )
    reading.add_argument(
        "-not",
        dest="excluded",
        nargs="?",
        default=[],
        metavar="TAG/FLAG",
        action=IgnoreNoneAppendAction,
        help=(
            "If passed a string, will exclude entries with that tag. "
            "Can be also used before -starred or -tagged flags, to exclude "
            "starred or tagged entries respectively."
        ),
    )

    search_options_msg = (
        "    "  # Preserves indentation
        """
        These help you do various tasks with the selected entries from your search.
        If used on their own (with no search), they will act on your entire journal"""
    )
    exporting = parser.add_argument_group(
        "Searching Options", textwrap.dedent(search_options_msg)
    )
    exporting.add_argument(
        "--edit",
        dest="edit",
        help="Opens the selected entries in your configured editor",
        action="store_true",
    )
    exporting.add_argument(
        "--delete",
        dest="delete",
        action="store_true",
        help="Interactively deletes selected entries",
    )
    exporting.add_argument(
        "--change-time",
        dest="change_time",
        nargs="?",
        metavar="DATE",
        const="now",
        help="Change timestamp for selected entries (default: now)",
    )
    exporting.add_argument(
        "--format",
        metavar="TYPE",
        dest="export",
        choices=EXPORT_FORMATS,
        help=f"""
        Display selected entries in an alternate format.

        TYPE can be: {util.oxford_list(EXPORT_FORMATS)}.

        Optional parameters:

        --file FILENAME Write output to file instead of stdout
        """,
        default=False,
    )
    exporting.add_argument(
        "--export",
        metavar="TYPE",
        dest="export",
        choices=EXPORT_FORMATS,
        help=argparse.SUPPRESS,
    )
    exporting.add_argument(
        "--tags",
        dest="tags",
        action="store_true",
        help="Alias for '--format tags'. Returns a list of all tags and number of "
        "occurrences",
    )
    exporting.add_argument(
        "--short",
        dest="short",
        action="store_true",
        help="Show only titles or line containing the search tags",
    )
    exporting.add_argument(
        "-s",
        dest="short",
        action="store_true",
        help=argparse.SUPPRESS,
    )
    exporting.add_argument(
        "-o",
        dest="filename",
        help=argparse.SUPPRESS,
    )

    config_overrides = parser.add_argument_group(
        "Config file override",
        textwrap.dedent("Apply a one-off override of the config file option"),
    )
    config_overrides.add_argument(
        "--config-override",
        dest="config_override",
        action="append",
        type=str,
        nargs=2,
        default=[],
        metavar="CONFIG_KV_PAIR",
        help="""
        Override configured key-value pair with CONFIG_KV_PAIR for this command invocation only.

        Examples: \n
        \t - Use a different editor for this jrnl entry, call: \n
            \t jrnl --config-override editor "nano" \n
        \t - Override color selections\n
           \t jrnl --config-override colors.body blue --config-override colors.title green
        """,  # noqa: E501
    )
    config_overrides.add_argument(
        "--co",
        dest="config_override",
        action="append",
        type=str,
        nargs=2,
        default=[],
        help=argparse.SUPPRESS,
    )

    alternate_config = parser.add_argument_group(
        "Specifies alternate config to be used",
        textwrap.dedent("Applies alternate config for current session"),
    )

    alternate_config.add_argument(
        "--config-file",
        dest="config_file_path",
        type=str,
        default="",
        help="""
        Overrides default (created when first installed) config file for this command only.

        Examples: \n
        \t - Use a work config file for this jrnl entry, call: \n
            \t jrnl --config-file /home/user1/work_config.yaml
        \t - Use a personal config file stored on a thumb drive: \n
            \t jrnl --config-file /media/user1/my-thumb-drive/personal_config.yaml
        """,  # noqa: E501
    )

    alternate_config.add_argument(
        "--cf", dest="config_file_path", type=str, default="", help=argparse.SUPPRESS
    )

    # Handle '-123' as a shortcut for '-n 123'
    num = re.compile(r"^-(\d+)$")
    args = [num.sub(r"-n \1", arg) for arg in args]
    parsed_args = parser.parse_intermixed_args(args)
    parsed_args = parse_not_arg(args, parsed_args, parser)

    return parsed_args
