"""CLI to install/uninstall bash history improvements into .bashrc."""

import argparse
import os
import re
import shutil
import sys

BEGIN_MARKER = "# >>> bash-history-done-right >>>"
END_MARKER = "# <<< bash-history-done-right <<<"

SNIPPET_CORE = """\
HISTSIZE=9000
HISTFILESIZE=$HISTSIZE
HISTCONTROL=ignorespace:ignoredups
_bash_history_sync() {
    builtin history -a
    HISTFILESIZE=$HISTSIZE
}
history() {
    _bash_history_sync
    builtin history "$@"
}
PROMPT_COMMAND=_bash_history_sync"""

SNIPPET_ARROW_SEARCH = """\

if [[ "$-" =~ "i" ]] # Don't do this on non-interactive shells
then
    # MATLAB-style up-arrow history search: type a prefix then press up
    bind '"\\e[A":history-search-backward'
    bind '"\\e[B":history-search-forward'
fi"""

SNIPPET_ETERNAL = """\

# Keep a second history file forever
PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND ; }"'echo $$ $USER \\
           "$(history 1)" >> ~/.bash_eternal_history'"""

# Patterns for the default history lines that should be removed
DEFAULT_HISTORY_PATTERNS = [
    re.compile(r"^\s*HISTCONTROL="),
    re.compile(r"^\s*shopt\s+-s\s+histappend"),
    re.compile(r"^\s*HISTSIZE="),
    re.compile(r"^\s*HISTFILESIZE="),
]


def get_bashrc_path():
    return os.path.join(os.path.expanduser("~"), ".bashrc")


def build_snippet(arrow_search=True, eternal_history=True):
    parts = [BEGIN_MARKER, "", SNIPPET_CORE]
    if arrow_search:
        parts.append(SNIPPET_ARROW_SEARCH)
    if eternal_history:
        parts.append(SNIPPET_ETERNAL)
    parts.extend(["", END_MARKER, ""])
    return "\n".join(parts)


def remove_default_history_lines(content):
    """Remove common default bash history lines (HISTCONTROL, shopt -s histappend, etc.)."""
    lines = content.splitlines(keepends=True)
    filtered = []
    for line in lines:
        stripped = line.strip()
        # Skip blank lines or comments? No, only skip matching config lines.
        if stripped.startswith("#"):
            filtered.append(line)
            continue
        if any(pat.match(stripped) for pat in DEFAULT_HISTORY_PATTERNS):
            continue
        filtered.append(line)
    return "".join(filtered)


def remove_existing_block(content):
    """Remove an existing marker-wrapped block."""
    pattern = re.compile(
        re.escape(BEGIN_MARKER) + r".*?" + re.escape(END_MARKER) + r"\n?",
        re.DOTALL,
    )
    return pattern.sub("", content)


# Regexes that match the config lines we add, with or without markers.
# Order matters: multi-line patterns before single-line ones.
_CONFIG_PATTERNS = [
    # marker lines
    re.compile(r"^" + re.escape(BEGIN_MARKER) + r"\n?", re.MULTILINE),
    re.compile(r"^" + re.escape(END_MARKER) + r"\n?", re.MULTILINE),
    # _bash_history_sync function (multi-line)
    re.compile(r"^_bash_history_sync\(\)\s*\{.*?\n\}\n?", re.DOTALL | re.MULTILINE),
    # history() override (multi-line)
    re.compile(r"^history\(\)\s*\{.*?\n\}\n?", re.DOTALL | re.MULTILINE),
    # arrow-key bind block (multi-line if/fi)
    re.compile(
        r'^if \[\[ "\$-" =~ "i" \]\].*?^fi\n?',
        re.DOTALL | re.MULTILINE,
    ),
    # eternal history (two lines: comment + PROMPT_COMMAND with backslash continuation)
    re.compile(r"^# Keep a second history file forever\n", re.MULTILINE),
    re.compile(r'^PROMPT_COMMAND="\$\{PROMPT_COMMAND:.*bash_eternal_history.*\n', re.MULTILINE),
    # single config lines
    re.compile(r"^HISTSIZE=9000\n?", re.MULTILINE),
    re.compile(r"^HISTFILESIZE=\$HISTSIZE\n?", re.MULTILINE),
    re.compile(r"^HISTCONTROL=ignorespace:ignoredups\n?", re.MULTILINE),
    re.compile(r"^PROMPT_COMMAND=_bash_history_sync\n?", re.MULTILINE),
    # comment line from arrow search block
    re.compile(r"^\s*# MATLAB-style up-arrow.*\n?", re.MULTILINE),
]


def remove_config_lines(content):
    """Remove all config lines/blocks we manage, whether or not markers are present."""
    for pat in _CONFIG_PATTERNS:
        content = pat.sub("", content)
    return content


DEFAULT_HISTORY_LINES = """\
HISTCONTROL=ignoreboth
shopt -s histappend
HISTSIZE=1000
HISTFILESIZE=2000
"""


def restore_default_history_lines(content):
    """Re-add the standard bash history lines if they are not already present."""
    lines_to_add = []
    for line in DEFAULT_HISTORY_LINES.strip().splitlines():
        if line not in content:
            lines_to_add.append(line)
    if lines_to_add:
        content = content.rstrip("\n") + "\n\n" + "\n".join(lines_to_add) + "\n"
    return content


def is_installed(content):
    """Check whether the config is already present by looking for its distinctive lines."""
    return "_bash_history_sync" in content


def install(bashrc_path, arrow_search=True, eternal_history=True, quiet=False):
    if not os.path.isfile(bashrc_path):
        print(f"Error: {bashrc_path} not found.", file=sys.stderr)
        return 1

    content = open(bashrc_path).read()

    # Remove any previous config (marker-wrapped or manually pasted)
    content = remove_config_lines(content)

    # Back up
    backup_path = bashrc_path + ".bak"
    shutil.copy2(bashrc_path, backup_path)
    if not quiet:
        print(f"Backed up {bashrc_path} to {backup_path}")

    # Remove default history lines
    content = remove_default_history_lines(content)

    # Strip trailing whitespace from file, then append our block
    content = content.rstrip("\n") + "\n\n"
    content += build_snippet(arrow_search=arrow_search, eternal_history=eternal_history)

    with open(bashrc_path, "w") as f:
        f.write(content)

    if not quiet:
        print(f"Installed bash history improvements to {bashrc_path}")
        print("Restart your shell or run: source ~/.bashrc")
    return 0


def uninstall(bashrc_path, quiet=False):
    if not os.path.isfile(bashrc_path):
        print(f"Error: {bashrc_path} not found.", file=sys.stderr)
        return 1

    content = open(bashrc_path).read()

    if not is_installed(content):
        if not quiet:
            print("Nothing to uninstall.")
        return 0

    # Back up
    backup_path = bashrc_path + ".bak"
    shutil.copy2(bashrc_path, backup_path)
    if not quiet:
        print(f"Backed up {bashrc_path} to {backup_path}")

    content = remove_config_lines(content)

    # Restore default history lines if not already present
    content = restore_default_history_lines(content)

    with open(bashrc_path, "w") as f:
        f.write(content)

    if not quiet:
        print(f"Removed bash history improvements from {bashrc_path}")
    return 0


def main():
    parser = argparse.ArgumentParser(
        prog="bash-history-done-right",
        description="Install bash history improvements from "
        "https://abarry.org/bash-history-finally-done-right/",
    )
    sub = parser.add_subparsers(dest="command")

    install_parser = sub.add_parser("install", help="Install into ~/.bashrc")
    install_parser.add_argument(
        "--no-arrow-search",
        action="store_true",
        help="Skip the up-arrow prefix search feature",
    )
    install_parser.add_argument(
        "--no-eternal-history",
        action="store_true",
        help="Skip the eternal history file feature",
    )
    install_parser.add_argument(
        "--bashrc",
        default=get_bashrc_path(),
        help="Path to .bashrc (default: ~/.bashrc)",
    )
    install_parser.add_argument("-q", "--quiet", action="store_true")

    uninstall_parser = sub.add_parser(
        "uninstall", help="Remove from ~/.bashrc"
    )
    uninstall_parser.add_argument(
        "--bashrc",
        default=get_bashrc_path(),
        help="Path to .bashrc (default: ~/.bashrc)",
    )
    uninstall_parser.add_argument("-q", "--quiet", action="store_true")

    args = parser.parse_args()

    if args.command == "install":
        return install(
            args.bashrc,
            arrow_search=not args.no_arrow_search,
            eternal_history=not args.no_eternal_history,
            quiet=args.quiet,
        )
    elif args.command == "uninstall":
        return uninstall(args.bashrc, quiet=args.quiet)
    else:
        parser.print_help()
        return 1


if __name__ == "__main__":
    sys.exit(main())
