# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# src/pdftl/cli/help.py

"""Help for CLI interface"""

import importlib.metadata
import itertools
import logging
import sys
import textwrap
from datetime import date

# Third-party imports
import pikepdf

# Local application imports
from pdftl.cli.console import console
from pdftl.cli.help_data import (
    SPECIAL_HELP_TOPICS_MAP,
    SYNOPSIS_TEMPLATE,
    VERSION_TEMPLATE,
)
from pdftl.cli.whoami import HOMEPAGE, PACKAGE, WHOAMI
from pdftl.core.cli_data import CLI_DATA
from pdftl.core.registry import registry
from pdftl.utils.string import before_space


def get_synopsis():
    """Returns the main synopsis text for the application."""
    special_help_topics = " | ".join([x[0] for x in SPECIAL_HELP_TOPICS_MAP.keys()])
    ret = SYNOPSIS_TEMPLATE.strip().format(
        whoami=WHOAMI, special_help_topics=special_help_topics
    )
    return ret


def get_project_version():
    """
    Gets the project version from installed package metadata or the
    setuptools_scm generated _version.py file.
    """
    # 1. Try standard package metadata (Best for pip-installed users)
    try:
        return importlib.metadata.version("pdftl")
    except importlib.metadata.PackageNotFoundError:
        pass

    # 2. Fallback: Read the file generated by setuptools_scm
    # This works for local dev/editable installs where metadata might fail
    try:
        # Since help.py is in pdftl.cli, we import from the package root
        # pylint: disable=import-outside-toplevel
        from pdftl._version import version

        return version
    except ImportError:
        # _version.py might not exist if setuptools_scm hasn't run yet
        return "unknown-dev-version"


def print_version(dest=None):
    """Prints detailed version information for the package and its core dependencies."""

    dependencies = "\n".join(
        [
            f"  - {name} {version}"
            for name, version in [
                ("python", sys.version.split()[0]),
                ("pikepdf", pikepdf.__version__),
                ("libqpdf", pikepdf.__libqpdf_version__),
            ]
        ]
    )

    start_year = 2025
    current_year = date.today().year
    output = VERSION_TEMPLATE.strip().format(
        whoami=WHOAMI,
        package=PACKAGE,
        project_version=get_project_version(),
        years=(
            str(start_year)
            if current_year <= start_year
            else f"{start_year}-{current_year}"
        ),
        homepage=HOMEPAGE,
        dependencies=dependencies,
    )
    if dest is None:
        console.print(output)
    else:
        print(output, file=dest)


def _format_long_desc_string(desc):
    """Formats a multi-line string with indentation for detailed descriptions."""
    wrapped_lines = [
        textwrap.fill(line, width=77, initial_indent="  ", subsequent_indent="  ")
        for line in desc.strip().split("\n")
    ]
    return "Details:\n\n" + "\n".join(wrapped_lines)


def _format_example_header_string(input_string):
    separator = "\n  # "
    return separator + separator.join(input_string.strip().split("\n"))


def _format_example_topic_heading(topic, count):
    return _format_example_header_string(
        "Example" + (" " + str(count) if count > 1 else "") + f" for '{topic}'"
    )


def _format_example_command(cmd):
    return f"\n  {WHOAMI} {cmd.strip()}"


def _format_examples_block(examples, show_topics=False):
    output = ""
    per_example_topic_count = 1
    topic = None
    for ex in examples:
        if show_topics and ex.get("topic"):
            per_example_topic_count = (
                per_example_topic_count + 1 if ex["topic"] == topic else 1
            )
            topic = ex["topic"]
            output += _format_example_topic_heading(topic, per_example_topic_count)

        assert "desc" in ex and "cmd" in ex
        output += _format_example_header_string(ex["desc"])
        output += _format_example_command(ex["cmd"])
        output += "\n"
    return "Examples:\n" + output.rstrip()  # remove trailing newline


def _print_topic_help(hprint, topic_data, topic_name):
    """Prints the detailed help for a specific command or topic."""
    hprint(f"{WHOAMI}: help for '{topic_name}'")
    if desc := topic_data.get("desc", None):
        hprint(f"\n  {desc.strip()}")
    if usage := topic_data.get("usage", None):
        hprint(f"\nUsage:\n\n  {WHOAMI} {usage.strip()}")
    if examples := topic_data.get("examples", None):
        hprint("\n" + _format_examples_block(examples))
    if long_desc := topic_data.get("long_desc", None):
        hprint("\n" + _format_long_desc_string(long_desc))
    if tags := topic_data.get("tags", None):
        hprint(f"\nTags: {', '.join(tags)}")


def print_main_help(hprint):
    """Prints the main, default help screen."""
    hprint(f"{WHOAMI} - PDF Tackle {get_project_version()}")
    hprint("\n  A wannabe CLI compatible clone/extension of pdftk\n")
    hprint("Usage:\n")
    for line in get_synopsis().splitlines():
        hprint(f"  {line}")

    hprint("\nOperations:\n")
    all_ops_and_opts = list(registry.operations) + list(registry.options)
    col_width = 3 + max(len(x) for x in all_ops_and_opts)

    def print_in_cols(col1, col2):
        indented_col2 = textwrap.fill(col2, width=80, subsequent_indent=" " * col_width)
        hprint(f"  {col1:<{col_width - 3}} {indented_col2.lstrip()}")

    for operation, info in sorted(registry.operations.items()):
        print_in_cols(operation, info["desc"])

    hprint("\nOptions for PDF output:\n")
    for opt, info in sorted(registry.options.items()):
        print_in_cols(opt, info["desc"])
    hprint()


def _print_output_options_help(hprint):
    """Prints detailed help for all output options."""
    hprint("Options for PDF output:\n")
    for opt, info in sorted(registry["options"].items()):
        hprint(opt)
        hprint()
        hprint(f"  {info['desc']}")
        hprint()
        if "long_desc" in info:
            hprint(_format_long_desc_string(info["long_desc"]))
            hprint()
        if examples := info.get("examples", None):
            hprint("  " + _format_examples_block(examples).replace("\n", "\n  "))
            hprint()


def _print_examples_help(hprint):
    """Prints all examples and not much else"""
    hprint(_format_examples_block(_discover_examples(), show_topics=True))


def _discover_examples():
    """Find all known CLI examples."""
    all_examples = []
    for topic, topic_data in itertools.chain(
        registry.operations.items(),
        CLI_DATA["extra help topics"].items(),
        registry["options"].items(),
    ):
        new_examples = topic_data.get("examples", [])
        for example in new_examples:
            example["topic"] = topic
        all_examples.extend(new_examples)
    return all_examples


def _print_help_dispatch_table():
    """Return a dispatch table for print_help"""
    dispatch_table = {
        before_space(op): (
            lambda hprint, op_info=info, op_name=op: _print_topic_help(
                hprint, op_info, op_name
            )
        )
        for op, info in itertools.chain(
            registry.operations.items(), registry.options.items()
        )
    }
    dispatch_table.update(
        {
            topic: (
                lambda hprint, t_data=data: _print_topic_help(
                    hprint, t_data, t_data["title"]
                )
            )
            for topic, data in CLI_DATA["extra help topics"].items()
        }
    )
    dispatch_table["output_options"] = _print_output_options_help
    dispatch_table["examples"] = _print_examples_help
    return dispatch_table


def print_help(command=None, dest=None):
    """
    Displays help information for the tool, a specific command, or a topic.

    Args:
        command (str, optional): The command or topic to get help for.
                                 If None, the main help screen is shown.
        dest (file object, optional): The destination to write help text to.
                                      Defaults to sys.stderr.
    """

    def hprint(*args):
        if dest is None:
            print(*args, file=sys.stderr)
        else:
            console.print(*[x.replace("[", "\\[") for x in args])

    safe_command = command.lower() if command else None

    if safe_command is None:
        print_main_help(hprint)
    elif safe_command in (dispatch_table := _print_help_dispatch_table()):
        dispatch_table[safe_command](hprint)
    elif safe_command == "all":
        # Iterates through all topics
        all_topics = [
            None,
            "input",
            *sorted(registry.operations),
            "filter_mode",
            "output_options",
            "pipeline",
        ]
        for i, topic in enumerate(all_topics):
            if i > 0:
                hprint("\n" + "-" * 79 + "\n")
            print_help(topic, dest=dest)
    else:
        logging.warning(
            "Unknown help topic '%s' requested, showing default help\n", command
        )
        print_main_help(hprint)


def find_special_topic_command(topic):
    """Searches for a command within a predefined set of special topics."""
    if topic is None:
        return None
    for keywords, command in SPECIAL_HELP_TOPICS_MAP.items():
        if topic in keywords:
            return command
    return None


def find_operator_topic_command(help_topics):
    """Searches for a command that matches a known operator."""
    return next(
        (topic for topic in help_topics if topic.lower() in registry.operations), None
    )


def find_option_topic_command(help_topics):
    """Searches for a command that matches a known option."""
    known_options = [before_space(opt) for opt in registry.options]
    return next((topic for topic in help_topics if topic in known_options), None)
