"""Shell completion."""

# The code in this file was partially copied from:
#    https://github.com/fastapi/typer/blob/6f2ec1d00ec76aa1d2b8743121dacdd48f4acbaa/typer/_completion_shared.py
#
# Which has this MIT License:
#
#    The MIT License (MIT)
#
#    Copyright (c) 2019 Sebastián Ramírez
#
#    Permission is hereby granted, free of charge, to any person obtaining a copy
#    of this software and associated documentation files (the "Software"), to deal
#    in the Software without restriction, including without limitation the rights
#    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#    copies of the Software, and to permit persons to whom the Software is
#    furnished to do so, subject to the following conditions:
#
#    The above copyright notice and this permission notice shall be included in
#    all copies or substantial portions of the Software.
#
#    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#    THE SOFTWARE.

import sys
from pathlib import Path
from subprocess import TimeoutExpired, run

import shellingham

from . import console, console_trace, err_console
from .cache import save_cached_subcommand_info
from .find_subcommands import SubCommand, find_subcommands
from .options import OPTIONS_WITH_ARGS, OPTIONS_WITH_ARGS__HELP, OPTIONS_WITHOUT_ARGS, OPTIONS_WITHOUT_ARGS__HELP


def _bash_completion_script() -> str:
    """Hack together a bash completion script that calls subcommands directly to complete them."""

    subcommand_completions = ""
    no_subcommand_compreply = ["pubkey", "auth", "project", "experiment", "clear-cache", "completion"]
    no_subcommand_compreply.extend(OPTIONS_WITHOUT_ARGS)
    no_subcommand_compreply.extend(OPTIONS_WITH_ARGS)

    subcommand_completions += (
        "\n\n"
        "    # Completions for subcommand 'completion'\n"
        """    if [[ -n "$SUBCOMMAND" ]] && ( [ "$SUBCOMMAND" = 'completion' ] )
    then
        if [[ 2 -eq "$COMP_CWORD" ]]
        then
           local IFS=$' \\t\\n'
           COMPREPLY=($(compgen -W 'install show --help' -- "${COMP_WORDS[-1]}"))
        else
           COMPREPLY=()
        fi
        return 0
    fi
    """
    )

    subcommands = find_subcommands()
    for subcommand in subcommands:
        aliases_match = ""
        for alias in subcommand.visible_aliases + subcommand.hidden_aliases:
            aliases_match += f" || [ \"$SUBCOMMAND\" = '{alias}' ]"

        no_subcommand_compreply.extend(subcommand.visible_aliases)

        if not subcommand.is_core:
            no_subcommand_compreply.append(subcommand.name)
        if subcommand.tab_completion_supported and not subcommand.is_core:
            name = subcommand.name

            # this assumes the completions are registered with: complete -F...
            subcommand_completions += (
                "\n\n"
                f"    # Completions for subcommand {name}\n"
                f"""    if [[ -n "$SUBCOMMAND" ]] && ( [ "$SUBCOMMAND" = '{name}' ]{aliases_match} )"""
                """
    then
        # Use the completion function already registered
        completion_func=$(complete -p slices-"""
                + name
                + """ | awk '{for(i=1;i<=NF;i++) if($i=="-F") print $(i+1)}')
        "$completion_func" '"""
                + str(subcommand.executable)
                + """'
        return 0
    fi
            """
            )
        if not subcommand.tab_completion_supported:
            name = subcommand.name
            subcommand_completions += (
                "\n\n"
                f"    # Completions for subcommand {name}\n"
                f"""    if [[ -n "$SUBCOMMAND" ]] && ( [ "$SUBCOMMAND" = '{name}' ]{aliases_match} )"""
                """
    then
        # No completion support for this subcommand
        COMPREPLY=()
        return 0
    fi
            """
            )

    return (
        """_slices_completion() {
    local IFS=$'
'
    # Find the subcommand
    SUBCOMMAND=""
    SUBCOMMAND_INDEX="-1"
    for index in "${!COMP_WORDS[@]}"; do
        if [[ "$index" -eq 0 ]]; then
            continue
        fi
        word="${COMP_WORDS[index]}"
        if [[ ! "$word" =~ ^- ]]; then
            SUBCOMMAND="$word"
            SUBCOMMAND_INDEX="$index"
            break
        fi
    done

    if [[ -z "$SUBCOMMAND" ]] || [[ "$SUBCOMMAND_INDEX" -eq "$COMP_CWORD" ]]
    then
        local IFS=$' \\t\\n'
        COMPREPLY=($(compgen -W '"""
        + (" ".join(no_subcommand_compreply))
        + """' -- "${COMP_WORDS[-1]}"))
        return 0
    fi
    """
        + subcommand_completions
        + """

    # For all other cases, use completions for slices-core (these handle the main slices CLI completions as well.)
    COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \
                   COMP_CWORD=$COMP_CWORD \
                   _SLICES_CORE_COMPLETE=complete_bash $1-core ) )
    return 0
}

complete -o default -F _slices_completion slices"""
    )


def _zsh_completion_script() -> str:
    """Hack together a zsh completion script that calls subcommands directly to complete them."""

    subcommand_completions = ""
    no_subcommand_compreply = {
        "pubkey": "Public key",
        "auth": "Authentication",
        "project": "Projects",
        "experiment": "Experiments",
        "clear-cache": "Remove all cache files.",
        "completion": "Install or show shell completion scripts.",
    }
    no_subcommand_compreply.update(OPTIONS_WITHOUT_ARGS__HELP)
    no_subcommand_compreply.update(OPTIONS_WITH_ARGS__HELP)

    subcommand_completions += (
        "\n\n"
        "    # Completions for subcommand 'completion'\n"
        """    if [[ -n "$subcommand" ]] && ( [ "$subcommand" = 'completion' ] ); then
        subcommands=(
            'install:Install Completion scripts'
            'show:Show Completion scripts'
        )
        _describe 'command' subcommands
        return $?
    fi
    """
    )

    subcommands = find_subcommands()
    for subcommand in subcommands:
        aliases_match = ""
        for alias in subcommand.visible_aliases + subcommand.hidden_aliases:
            aliases_match += f" || [ \"$subcommand\" = '{alias}' ]"

        for alias in subcommand.visible_aliases:
            no_subcommand_compreply[alias] = subcommand.short_help + " (alias)"

        if not subcommand.is_core:
            no_subcommand_compreply[subcommand.name] = subcommand.short_help
        if subcommand.tab_completion_supported and not subcommand.is_core:
            name = subcommand.name

            # this assumes the completions are registered with: complete -F...
            subcommand_completions += (
                "\n\n"
                f"    # Completions for subcommand {name}\n"
                f"""    if [[ -n "$subcommand" ]] && ( [ "$subcommand" = '{name}' ]{aliases_match} ); then"""
                """
        # Use the completion file registered in zsh dir
        _dispatch slices-"""
                + name
                + """bi slices-"""
                + name
                + """
        return $?
    fi
            """
            )
        if not subcommand.tab_completion_supported:
            name = subcommand.name
            subcommand_completions += (
                "\n\n"
                f"    # Completions for subcommand {name}\n"
                f"""    if [[ -n "$subcommand" ]] && ( [ "$subcommand" = '{name}' ]{aliases_match} ); then"""
                """
        # No completion support for this subcommand
        return 0
    fi
            """
            )

    return (
        """#compdef slices

_slices_completion() {
    # Set up Zsh completion system variables
    local -a subcommands
    local curcontext="$curcontext"
    typeset -A opt_args

    local subcommand=""
    local subcommand_index="-1"
    for i in {2..$CURRENT}; do
        local word="${words[i]}"
        # If the word doesn't start with a '-', it's likely the subcommand
        if [[ ! "$word" =~ ^- ]]; then
            subcommand="$word"
            subcommand_index="$i"
            break
        fi
    done

    if [[ -z "$subcommand" ]] || [[ "$subcommand_index" -eq "$CURRENT" ]]; then
        subcommands=(
        """
        + ("\n".join([f"'{k}:{v}'" for k, v in no_subcommand_compreply.items()]))
        + """
        )
        _describe 'command' subcommands
        return 0
    fi

    """
        + subcommand_completions
        + """

    _dispatch slices-core slices-core

    return 0
}

compdef _slices_completion slices
"""
    )


def install_bash(*, prog_name: str) -> Path:
    """Install the bash completion script and source it in ~/.bashrc."""

    # Ref: https://github.com/scop/bash-completion#faq
    # It seems bash-completion is the official completion system for bash:
    # Ref: https://www.gnu.org/software/bash/manual/html_node/A-Programmable-Completion-Example.html
    # But installing in the locations from the docs doesn't seem to have effect
    completion_path = Path.home() / ".bash_completions" / f"{prog_name}.sh"
    rc_path = Path.home() / ".bashrc"
    rc_path.parent.mkdir(parents=True, exist_ok=True)
    rc_content = ""
    if rc_path.is_file():
        rc_content = rc_path.read_text()
    completion_init_lines = [f"source '{completion_path}'"]
    for line in completion_init_lines:
        if line not in rc_content:  # pragma: no cover
            rc_content += f"\n{line}"
    rc_content += "\n"
    rc_path.write_text(rc_content)
    # Install completion
    completion_path.parent.mkdir(parents=True, exist_ok=True)
    script_content = _bash_completion_script()
    completion_path.write_text(script_content)
    return completion_path


def install_zsh(*, prog_name: str) -> Path:
    """Install the zsh completion script."""

    # Setup Zsh and load ~/.zfunc
    zshrc_path = Path.home() / ".zshrc"
    zshrc_path.parent.mkdir(parents=True, exist_ok=True)
    zshrc_content = ""
    if zshrc_path.is_file():
        zshrc_content = zshrc_path.read_text()
    completion_line = "fpath+=~/.zfunc; autoload -Uz compinit; compinit"
    if completion_line not in zshrc_content:
        zshrc_content += f"\n{completion_line}\n"
    style_line = "zstyle ':completion:*' menu select"
    # TODO: consider setting the style only for the current program
    # style_line = f"zstyle ':completion:*:*:{prog_name}:*' menu select"
    # Install zstyle completion config only if the user doesn't have a customization
    if "zstyle" not in zshrc_content:
        zshrc_content += f"\n{style_line}\n"
    zshrc_content = f"{zshrc_content.strip()}\n"
    zshrc_path.write_text(zshrc_content)
    # Install completion under ~/.zfunc/
    path_obj = Path.home() / f".zfunc/_{prog_name}"
    path_obj.parent.mkdir(parents=True, exist_ok=True)
    script_content = _zsh_completion_script()
    path_obj.write_text(script_content)
    return path_obj


def _fish_completion_script():
    """Generate the fish completion script."""

    console_trace.print("[bright_black]Generating fish completion script...")

    raise NotImplementedError("Not yet implemented for fish")


def install_fish(*, prog_name: str) -> Path:
    """Install the fish completion script."""
    path_obj = Path.home() / f".config/fish/completions/{prog_name}.fish"
    parent_dir: Path = path_obj.parent
    parent_dir.mkdir(parents=True, exist_ok=True)
    script_content = _fish_completion_script()
    path_obj.write_text(f"{script_content}\n")
    return path_obj


def install(
    *,
    prog_name: str,
    shell: str | None = None,
    complete_var: str | None = None,
) -> tuple[str, Path]:
    """Install the completion script for the given shell."""

    if complete_var is None:
        complete_var = "_{}_COMPLETE".format(prog_name.replace("-", "_").upper())
    if shell is None and shellingham is not None:
        shell, _ = shellingham.detect_shell()
        console_trace.print(f"[bright_black]Detected shell: {shell}")
    if shell == "bash":
        installed_path = install_bash(prog_name=prog_name)
        return shell, installed_path
    if shell == "zsh":
        installed_path = install_zsh(prog_name=prog_name)
        return shell, installed_path
    if shell == "fish":
        installed_path = install_fish(prog_name=prog_name)
        return shell, installed_path
    # No other shells supported at the moment
    err_console.print(f"[red] Shell {shell} is not supported.")
    sys.exit(1)


def install_subcommand_completions(subcommand: SubCommand):
    """Install the completion for a subcommand."""
    try:
        if isinstance(subcommand.tab_completion_install_option, str):
            args = [str(subcommand.executable), str(subcommand.tab_completion_install_option)]
        elif isinstance(subcommand.tab_completion_install_option, list):
            args = [str(subcommand.executable), *subcommand.tab_completion_install_option]
        else:
            raise TypeError(
                f"Invalid type for tab_completion_install_option: {type(subcommand.tab_completion_install_option)}"
            )

        install_completion_res = run(  # noqa: S603
            args, check=False, timeout=15
        )
        if install_completion_res.returncode != 0:
            err_console.print(f"[red]Failed to install completions for {subcommand.name}")
    except TimeoutExpired:
        err_console.print(f"[red]Failed to install completions for {subcommand.name}")


def install_completion():
    """Install the slices cli completion script."""
    # First, install completions for all subcommands that support it
    subcommands = find_subcommands()
    core_done = False
    for subcommand in subcommands:
        if subcommand.tab_completion_supported and subcommand.tab_completion_install_option:
            if subcommand.is_core:
                if core_done:
                    continue
                core_done = True
                console.print("[blue]Installing completion for 'slices-core'...")
                save_cached_subcommand_info(subcommand.executable, "core", "completion_installed", "true")
            else:
                console.print(f"[blue]Installing completion for 'slices-{subcommand.name}'...")
                save_cached_subcommand_info(subcommand.executable, subcommand.name, "completion_installed", "true")
            install_subcommand_completions(subcommand)
            console.print()
        if subcommand.tab_completion_supported and not subcommand.tab_completion_install_option:
            console.print(f"[red]Can not automatically install completions for 'slices-{subcommand.name}'.")
            console.print("[red bold]You need to install these manually.")
            console.print()

    # Finally, install the completion script for the slices executable itself
    console.print("[blue]Installing completion for 'slices'...")
    shell, path = install(prog_name="slices")
    console.print(f"[green]{shell} completion installed in {path}")
    console.print("Completion will take effect once you restart the terminal")
    sys.exit(0)


def print_completion_script():
    """Show the completion script for the given shell."""
    shell, _ = shellingham.detect_shell()
    console_trace.print(f"[bright_black]Detected shell: {shell}")
    if shell == "bash":
        print(_bash_completion_script())
        return
    if shell == "zsh":
        print(_zsh_completion_script())
        return
    if shell == "fish":
        print(_fish_completion_script())
        return
    # No other shells supported at the moment
    err_console.print(f"[red] Shell {shell} is not supported.")
    sys.exit(1)
