# 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 logging
import sys

logger = logging.getLogger(__name__)

from pdftl.cli.console import get_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.registry import register_help_topic, registry
from pdftl.core.types import HelpExample
from pdftl.utils.string import before_space

TAG_PREFIX = "tag:"


def get_synopsis():
    """Returns the main synopsis text for the application."""
    # Note: Using Markdown syntax here provides better output when rendered by rich
    special_help_topics = " | ".join([f"{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.
    """
    import importlib.metadata

    # 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 get_optional_dependencies_status():
    import importlib.metadata

    from packaging.requirements import Requirement

    try:
        raw_reqs = importlib.metadata.requires("pdftl") or []
    except importlib.metadata.PackageNotFoundError:
        return []

    optional_pkgs = set()

    # We will ignore dependencies that ONLY exist for these extras
    IGNORED_GROUPS = {"dev", "docs", "test", "dev-all"}

    for req_str in raw_reqs:
        req = Requirement(req_str)

        # 1. Must be optional
        if not req.marker:
            continue

        # 2. Ignore self-references
        if req.name == "pdftl":
            continue

        # 3. FILTER: Check if this requirement belongs to a "ignored" extra.
        # The marker string usually looks like: 'extra == "dev"'
        marker_str = str(req.marker)

        # If the marker explicitly restricts this to a banned group, skip it.
        # (This simplistic string check is effective for standard metadata)
        is_dev_tool = any(
            f"extra == '{g}'" in marker_str or f'extra == "{g}"' in marker_str
            for g in IGNORED_GROUPS
        )

        if is_dev_tool:
            continue

        optional_pkgs.add(req.name)

    # Check status of each identified package
    results = []
    for pkg in sorted(optional_pkgs):
        try:
            ver = importlib.metadata.version(pkg)
            results.append((pkg, ver))
        except importlib.metadata.PackageNotFoundError:
            results.append((pkg, None))

    return results


def print_version(dest=None):
    """Prints detailed version information for the package, core, and optional deps."""
    from datetime import date

    import pikepdf

    # --- 1. 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,
    )

    # Plain text append for optional deps
    opts = get_optional_dependencies_status()
    if opts:
        output += "\n\nOptional dependencies:\n"
        for name, ver in opts:
            status = ver if ver else "(not found)"
            output += f"  - {name}: {status}\n"

    output = output.rstrip()

    # --- 2. Print Output ---
    console = get_console()

    # If writing to file/string, strip rich formatting
    if dest is not None and dest is not sys.stdout and dest is not sys.stderr:
        print(output, file=dest)
        return

    # If writing to terminal, use Rich
    console.print(output)


def _format_examples_block(examples, show_topics=False):
    """
    Formats the examples into a rich-ready Markdown block.
    We convert the structured data into a Markdown code/list structure.
    """
    output = "## Examples\n\n"
    per_example_topic_count = 1
    topic = None

    for ex in examples:
        # Add topic heading if necessary
        if show_topics and ex.get("topic"):
            if ex.topic == topic:
                per_example_topic_count += 1
            else:
                per_example_topic_count = 1
            topic = ex.topic

            # Use Markdown heading or strong text for topics
            heading_text = (
                "Example"
                + (f" {per_example_topic_count}" if per_example_topic_count > 1 else "")
                + f" for '`{topic}`'"
            )
            output += f"### {heading_text}\n"

        assert "desc" in ex and "cmd" in ex

        # Use a quote block for description and a fenced code block for command
        output += f"> {ex.get('desc','')}\n"
        output += f"```\n{WHOAMI} {ex['cmd'].strip()}\n```\n\n"

    return output.rstrip()


def _usage_as_markdown(x: str) -> str:
    return " ".join(map(lambda y: f"`{y}`", x.strip().split()))


def _print_topic_help(hprint, topic_data, topic_name):
    """Prints the detailed help for a specific command or topic."""
    safe_topic_name = (
        topic_name if any(topic_name in x for x in SPECIAL_HELP_TOPICS_MAP) else f"`{topic_name}`"
    )

    hprint(f"# {WHOAMI}: help for {safe_topic_name}")

    if desc := topic_data.get("desc", None):
        hprint(f"\n{desc.strip()}")

    if usage := topic_data.get("usage", None):
        usage_string = f"## Usage\n> {WHOAMI} {_usage_as_markdown(usage)}"
        hprint(usage_string)

    if long_desc := topic_data.get("long_desc", None):
        cleaned_desc = long_desc.strip()
        hprint("\n## Details\n" + cleaned_desc)

    if examples := topic_data.get("examples", None):
        example_markdown = _format_examples_block(examples)
        hprint(example_markdown)

    if tags := topic_data.get("tags", None):
        hprint(_format_tags(tags))

    if caller := topic_data.get("caller", None):
        hprint(f"\n*Source: {caller}*")

    hprint(f"\n*Type: {type(topic_data).__name__}*")


def _format_tags(tags):
    return f"\n\n**Tags**: {', '.join(tags)}"


def print_main_help(dest=None, raw=False):
    """
    Prints the main help screen using either the Fast Path (Rich)
    or the Doc Path (Raw Markdown).
    """
    if raw:
        # --- Strategy 1: Raw Markdown (For docs/grep) ---
        # Uses built-in print, outputs Markdown syntax
        target = dest if dest is not None else sys.stdout

        def print_title(main, sub):
            print(f"# **{main}**\n_{sub}_", file=target)

        def print_usage(text):
            print(f"## Usage\n\n```\n{text.strip()}\n```", file=target)

        def print_table(title, data):
            # Reuses the original markdown table logic
            out = f"|{title}||\n|-|-|\n"
            for op, info in sorted(data.items()):
                out += f"|`{op}`|{info.get('desc', '')}|\n"
            print(out, file=target)

    else:
        # --- Strategy 2: Fast Rich (Restored Visuals) ---
        import re

        from rich import box  # Low cost import for styling
        from rich.console import Console
        from rich.markup import escape
        from rich.padding import Padding
        from rich.panel import Panel
        from rich.table import Table
        from rich.text import Text

        # Setup console
        if dest is None or dest is sys.stdout or dest is sys.stderr:
            console = get_console()
        else:
            console = Console(file=dest, width=80, force_terminal=False)

        def print_title(main, sub):
            # 1. Restore the Heavy Box around the title
            console.print(
                Panel(
                    Text(main, justify="left", style="bold"),
                    box=box.HEAVY,
                    style="white",
                    expand=True,
                )
            )
            # 2. Subtitle
            console.print(Text(sub, style="dim"))  # 'dim' often looks cleaner than italic
            console.print()

        def print_usage(text):
            console.print(Text("Usage", style="bold underline"))
            usage_raw = f"\n{text.strip()}\n"
            usage_styled = Text(usage_raw)

            # # next style is probably orthodox (c.f. man pages)
            # # but it looks vile, so let's omit it
            # usage_styled.highlight_regex(r"<[^>]+>", "underline")

            usage_styled.highlight_regex(
                r"((\b(pdftl|help|all|sign|filter|pages|example|output)|--version)\b|---|[^<]input[^>])",
                "bold cyan",
            )
            usage_styled.highlight_regex(r"\[|\]", "dim")

            # Indent the synopsis slightly for readability
            console.print(Padding(usage_styled, (0, 0, 1, 2)))

        def print_table(title, data):
            # 3. Restore the "Markdown Table" look
            # box.HEAVY_HEAD puts a thick line (━) under the header, matching old output
            # show_edge=False removes the outer side borders
            t = Table(box=box.SIMPLE_HEAVY, show_edge=False, pad_edge=False, show_lines=False)

            # The header becomes the section title ("Operations", "Options")
            t.add_column(f" {title}", style="bold cyan", no_wrap=True)
            t.add_column("")  # Empty string or 'Description' for 2nd col

            for op, info in sorted(data.items()):
                description_string = info.get("desc", "")
                safe_desc = escape(description_string)
                formatted_desc = re.sub(r"`(.*?)`", r"[cyan]\1[/cyan]", safe_desc)
                # Add a leading space to operation name for breathing room
                t.add_row(f" {op}", formatted_desc)

            console.print(Padding(t, (0, 2, 0, 2)))
            console.print()

    # We define the layout and content ONCE here.

    print_title(
        f"{WHOAMI} - PDF tackle {get_project_version()}",
        "A wannabe CLI compatible clone/extension of pdftk",
    )

    print_usage(get_synopsis())

    print_table("Operations", registry.operations)
    print_table("Options", registry.options)


def _print_output_options_help(hprint):
    """Prints detailed help for all output options."""
    hprint("# pdftl: Options for PDF output\n")
    for opt, info in sorted(registry.options.items()):
        safe_opt = opt
        hprint(f"\n## `{safe_opt}`")
        hprint(f"\n> {info.get('desc','')}\n")
        if hasattr(info, "long_desc"):
            cleaned_desc = info.long_desc.strip()
            if cleaned_desc:
                hprint("\n## Details\n")
                hprint(cleaned_desc)
        if hasattr(info, "examples"):
            example_markdown = _format_examples_block(info.examples)
            hprint(example_markdown)
        if hasattr(info, "tags"):
            hprint(_format_tags(info.tags))


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


def _discover_examples():
    """Find all known CLI examples."""
    all_examples = []
    import itertools

    for topic, topic_data in itertools.chain(
        registry.operations.items(),
        registry.options.items(),
        registry.help_topics.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"""
    import itertools

    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 itertools.chain(
                registry.help_topics.items(),
            )
        }
    )
    dispatch_table["output_options"] = _print_output_options_help
    dispatch_table["examples"] = _print_examples_help
    return dispatch_table


def _load_help_markdown():
    from rich import box
    from rich.console import Console, ConsoleOptions, RenderResult
    from rich.markdown import Heading, Markdown
    from rich.panel import Panel
    from rich.text import Text

    class LeftJustifiedHeading(Heading):
        def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
            text = self.text
            text.justify = "left"
            if self.tag == "h1":
                # Draw a border around h1s
                yield Panel(
                    text,
                    box=box.HEAVY,
                    style="markdown.h1.border",
                )
            else:
                # Styled text for h2 and beyond
                if self.tag == "h2":
                    yield Text("")
                yield text

    class HelpMarkdown(Markdown):
        elements = {
            **Markdown.elements,
            "heading_open": LeftJustifiedHeading,
        }

        def __init__(self, markup, *args, **kwargs):
            self.source = markup
            super().__init__(markup, *args, **kwargs)

        def __str__(self):
            return self.source

    return HelpMarkdown


def _load_hprint(dest, raw):

    def hprint(x):
        HelpMarkdown = _load_help_markdown()
        use_rich_console = not raw and (dest is None or dest is sys.stdout or dest is sys.stderr)
        if use_rich_console:
            get_console().print(HelpMarkdown(x))
        elif not raw:
            from rich.console import Console

            # Rendered output for files/pipes (fixes missing table format in files)
            console = get_console()
            width = console.width if console.width else 80
            file_console = Console(file=dest, width=width, force_terminal=False)
            file_console.print(HelpMarkdown(x))
        else:
            # Raw output (Markdown source)
            target = dest if dest is not None else sys.stdout
            print(x, file=target)

    return hprint


def print_help(command=None, dest=None, raw=False):
    """
    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.stdout.
        raw (bool, optional): If True, output raw markdown/text instead of rendered Rich output.
    """

    hprint = _load_hprint(dest, raw)

    safe_command = command.lower() if command else None

    if safe_command is None:
        print_main_help(dest=dest, raw=raw)
    elif safe_command.startswith(TAG_PREFIX):
        import itertools

        tag = safe_command[len(TAG_PREFIX) :]
        taggable_topics = itertools.chain(
            registry.operations.items(),
            registry.options.items(),
            registry.help_topics.items(),
        )
        tagged_topics = [
            before_space(k) for k, t in taggable_topics if hasattr(t, "tags") and tag in t.tags
        ]
        _print_multiple_topics(tagged_topics, hprint, dest, raw)

    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",
            "signing",
            "page_specs",
            "pipeline",
            "help",
        ]
        _print_multiple_topics(all_topics, hprint, dest, raw)
    else:
        logger.warning("Unknown help topic '%s' requested, showing default help\n", command)
        print_main_help(hprint)


def _print_multiple_topics(topics, hprint, dest, raw):
    for i, topic in enumerate(topics):
        if i > 0:
            hprint("\n---\n")
        print_help(topic, dest=dest, raw=raw)


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)


@register_help_topic(
    "help",
    title="pdftl help",
    desc="Get help",
    examples=[
        HelpExample(
            desc=(
                "Get all help. This is nice if you set `FORCE_COLORS=1` and pipe "
                "the output to `less -R`, with the complete command "
                "`FORCE_COLORS=1 pdftl help all | less -R`."
            ),
            cmd="help all",
        ),
        HelpExample(desc=("Get help topics tagged with `encryption`"), cmd="help tag:encryption"),
        HelpExample(
            desc=("Gather all examples from the various help topics"), cmd="help examples"
        ),
    ],
)
def _help_help_topic():
    """If a `help` argument is given, the remaining arguments are
    scanned for a keyword. This can be `tag:<tagname>`, or one of the
    operation names, or an option name, or a special help topic, or an
    alias. If a match is found, the help is printed. Tags are printed
    at the end of any help topics which have tags.

    By default, colors are used if printing directly to the
    terminal, and usually not in other situations (e.g., if
    the output is redirected). If the environment variable
    `FORCE_COLORS` is set, then colors should appear in all
    cases.

    The special help topic `all` is particularly interesting.

    """
