"""
Utilities for python packages.
"""
from __future__ import annotations
import importlib
import re
import subprocess
import sys
import time
from pathlib import Path
from typing import Optional

import requests
from loguru import logger

from packg.iotools import sort_file_paths_with_dirs_separated
from packg.misc import format_exception
from packg.strings import create_nested_abbreviations
from packg.testing.import_from_source import recurse_modules


def run_package(main_file, run_dir="cli", recursive=True, run_dir_only=True, abbreviations=True):
    """
    todo remove this. it is too slow and ineffective. solve the problem in bash instead.

    Create a command line interface for a package given a directory of scripts.

    Check the run directory, list scripts, create shortcuts, run if given shortcut.

    Args:
        main_file: __file__ attribute of __main__.py e.g.
            /path/to/lmbtools/python/lmbtools/__main__.py
        run_dir: submodule with run scripts
        recursive: if True, recursively search for scripts
        run_dir_only: if True, only enable running scripts in the run_dir
        abbreviations: if True, create abbreviations for the scripts
    """
    main_file = Path(main_file)
    print(f"main_file: {main_file}")
    package_dir = main_file.parent
    package_name = package_dir.name
    package_scripts_dir = package_dir / run_dir
    glob_str = "**/*.py" if recursive else "*.py"

    package_scripts = [
        f.relative_to(package_scripts_dir)
        for f in package_scripts_dir.glob(glob_str)
        if not f.name.startswith("__")
    ]
    sorted_scripts = sort_file_paths_with_dirs_separated(package_scripts, dirs_first=False)
    package_scripts = [f.as_posix()[:-3].replace("/", ".") for f in sorted_scripts]

    abbrevs = None
    if abbreviations:
        abbrevs = create_nested_abbreviations(package_scripts)

    args = sys.argv[1:]
    if len(args) > 0:
        abbrev_arg = args[0]
        if abbrevs is not None and abbrev_arg in abbrevs:
            script = abbrevs[abbrev_arg]
            target = f"{package_name}.{run_dir}.{script}"
            run_script(target, args)
            return

        target = f"{package_name}.{abbrev_arg}"
        if run_dir_only:
            target = f"{package_name}.{run_dir}.{abbrev_arg}"
        try:
            importlib.import_module(target)
            found = True
        except ModuleNotFoundError as e:
            # must figure out whether the error is due to the module not existing
            # or due to some import failing.
            if f"'{target}'" in str(e):
                # No module named (the module we are trying to run) -> module not found
                found = False
            else:
                # No module named (some other module) -> module found, but has import error
                # run it to trigger the error
                found = True
        if found:
            run_script(target, args)
            return
        print(f"Error, script not found: {abbrev_arg} and {target}")

    # no arguments, error, show help
    if abbreviations:
        print(f"Usage option 1: {package_name} shortcut [args]")
        print(f"Usage option 2: {package_name} path.to.module [args]")
        print()
        print(f"shortcut   script")
        for abbrev, f in abbrevs.items():
            if run_dir_only:
                print(f"{abbrev:10s} {f}")
            else:
                print(f"{abbrev:10s} {run_dir}.{f}")
        return

    # show help but without abbreviations
    print(f"Usage: {package_name} path.to.module [args]")
    print()
    print(f"Available scripts:")
    for f in package_scripts:
        if run_dir_only:
            print(f"{f}")
        else:
            print(f"{run_dir}.{f}")


def run_script(target, args):
    cmd = ["python", "-m", target, *args[1:]]
    print(f"$ {' '.join(cmd)}")

    subprocess.run(cmd)

    # # run by importing main() function
    # print(f"Running {target}")
    # sys.argv = [sys.argv[0]] + args[1:]
    # module = importlib.import_module(target)
    # main = getattr(module, "main")
    # main()


def _recurse_modules_remove_root_package(package: str, packages_only: bool = False):
    for candidate in recurse_modules(package, packages_only=packages_only):
        if candidate == package:
            continue
        candidate = candidate.removeprefix(f"{package}.")
        if candidate == "__main__":
            continue
        yield candidate


def get_modules_for_autocomplete(package: str, run_dir: Optional[str] = None):
    packages_only = set(_recurse_modules_remove_root_package(package, packages_only=True))
    all_modules = set(_recurse_modules_remove_root_package(package))
    output_modules = []

    for m in sorted(all_modules):
        if m in packages_only:
            mlog = f"{package}.{m}"
            if f"{m}.__main__" not in all_modules:
                logger.debug(f"SKIP package: {mlog}")
                continue
            logger.debug(f"ADD package (has __main__): {mlog}")
        if m.endswith("__main__"):
            logger.debug(f"SKIP __main__ file: {mlog}")
            continue
        output_modules.append(m)
    output_modules.sort()

    if run_dir is not None:
        new_output_modules = []
        for output_module in output_modules:
            if output_module.startswith(run_dir):
                new_output_modules.append(output_module)
        output_modules = new_output_modules
    return output_modules


def create_bash_autocomplete_script(
    package: str, function_name: str = None, command_name: str = None, run_dir: Optional[str] = None
) -> str:
    """
    Create a bash script for autocompletion of a python package.

    Workflow where this is useful:
    - src/packg/__main__.py calls run_package, so `python -m packg run.script` has the same effect
    as `python -m packg.run.script`
    - pyproject.toml defines script [project.scripts] packg = "packg.__main__:main"
    so now `packg run.script` has the same effect.
    - this function now can be used to create src/packg/autocomplete.sh which, when sourced,
    provides autocompletion for the `packg` command.

    Args:
        package: package to create the autocomplete for
        function_name: default _{package}
        command_name: default {package}
        run_dir: only create autocompletion for this directory

    Returns:

    """
    output_modules = get_modules_for_autocomplete(package, run_dir=run_dir)
    if function_name is None:
        function_name = f"_{package}"
    if command_name is None:
        command_name = package
    ob, cb = "{", "}"
    autocomplete_script = f"""
{function_name}() {ob}
    local cur prev opts
    _init_completion || return
    # complete first argument with script
    if [ $COMP_CWORD -eq 1 ]; then
        opts="{' '.join(output_modules)}"
        COMPREPLY=( $( compgen -W "${ob}opts{cb}" -- "${ob}cur{cb}") )
        return 0
    fi
    # # otherwise complete with filesystem
    # COMPREPLY=( $(compgen -f -- "${ob}cur{cb}") )
    _filedir
    return 0
{cb}

complete -F {function_name} {command_name}
"""
    return autocomplete_script


def create_new_bash_autocomplete_script(
    packages: list[str],
    command_name: str = "py",
    function_name: Optional[str] = None,
    run_dir: Optional[str] = None,
) -> str:
    """
    New version: allinone e.g. alias py and then complete for all installed packages.

    Args:
        packages: packages to create the autocomplete for
        function_name: default _{package}
        command_name: default {package}
        run_dir: only create autocompletion for this directory

    Returns:

    """
    all_output_modules = []
    for package in packages:
        output_modules = get_modules_for_autocomplete(package, run_dir=run_dir)
        output_modules = [f"{package}.{m}" for m in output_modules]
        logger.debug(f"package={package} output_modules={output_modules}")
        if len(output_modules) == 0:
            logger.warning(f"Nothing found for package {package} in {run_dir}, is it installed?")
        all_output_modules.extend(output_modules)
    if function_name is None:
        function_name = f"_{command_name}"

    ob, cb = "{", "}"
    autocomplete_script = f"""
{function_name}() {ob}
    local cur prev opts
    _init_completion || return
    # complete second argument with script, iff first argument is -m
    if [[ $COMP_CWORD -eq 2 && $prev == "-m" ]]; then
        opts="{' '.join(all_output_modules)}"
        COMPREPLY=( $( compgen -W "${ob}opts{cb}" -- "${ob}cur{cb}") )
        return 0
    fi
    # # otherwise complete with filesystem
    # COMPREPLY=( $(compgen -f -- "${ob}cur{cb}") )
    _filedir
    return 0
{cb}

complete -F {function_name} {command_name}
"""
    return autocomplete_script


FILEDIR_AUTOCOMPLETE = r"""
_filedir()  # source: ubuntu 2004 /usr/share/bash-completion/bash_completion
{
    local IFS=$'\n'
    _tilde "$cur" || return
    local -a toks
    local reset
    if [[ "$1" == -d ]]; then
        reset=$(shopt -po noglob); set -o noglob
        toks=( $(compgen -d -- "$cur") )
        IFS=' '; $reset; IFS=$'\n'
    else
        local quoted
        _quote_readline_by_ref "$cur" quoted
        # Munge xspec to contain uppercase version too
        # http://thread.gmane.org/gmane.comp.shells.bash.bugs/15294/focus=15306
        local xspec=${1:+"!*.@($1|${1^^})"} plusdirs=()
        # Use plusdirs to get dir completions if we have a xspec; if we don't,
        # there's no need, dirs come along with other completions. Don't use
        # plusdirs quite yet if fallback is in use though, in order to not ruin
        # the fallback condition with the "plus" dirs.
        local opts=( -f -X "$xspec" )
        [[ $xspec ]] && plusdirs=(-o plusdirs)
        [[ ${COMP_FILEDIR_FALLBACK-} ]] || opts+=( "${plusdirs[@]}" )
        reset=$(shopt -po noglob); set -o noglob
        toks+=( $(compgen "${opts[@]}" -- $quoted) )
        IFS=' '; $reset; IFS=$'\n'
        # Try without filter if it failed to produce anything and configured to
        [[ -n ${COMP_FILEDIR_FALLBACK:-} && -n "$1" && ${#toks[@]} -lt 1 ]] && {
            reset=$(shopt -po noglob); set -o noglob
            toks+=( $(compgen -f "${plusdirs[@]}" -- $quoted) )
            IFS=' '; $reset; IFS=$'\n'
        }
    fi
    if [[ ${#toks[@]} -ne 0 ]]; then
        # 2>/dev/null for direct invocation, e.g. in the _filedir unit test
        compopt -o filenames 2>/dev/null
        COMPREPLY+=( "${toks[@]}" )
    fi
} # _filedir()

"""


def _get_raw_shields_io_output(package: str):
    # todo wrap this request code into a separate function in packg.web
    url = f"https://img.shields.io/pypi/v/{package}"
    logger.info(f"Finding pypi version via {url}")
    counter = 0
    while True:
        try:
            response = requests.get(url, timeout=10)
            break
        except Exception as e:
            logger.warning(f"Error requesting {url}: {format_exception(e)}")
        counter += 1
        time.sleep(1)
    return response.text


_RE_TEXT = re.compile(r"<text.*?>.*?</text>")
_RE_TEXT_VERSION = re.compile(r"<text.*?>v([0-9]+.[0-9]+.[0-9]+)</text>")


def find_pypi_package_version(package: str) -> Optional[str]:
    """
    Find the latest version of a package on pypi.

    pip search was removed. An alternative is to query the shields.io service and parse the
    output svg.
    https://img.shields.io/pypi/v/numpy

    Args:
        package: package name

    Returns:
        version string

    """
    raw = _get_raw_shields_io_output(package)
    # find all occurrences of <text>...</text>
    matches = _RE_TEXT.findall(raw)
    # the last one is the displayed version
    version = matches[-1]
    # find the version number
    match = _RE_TEXT_VERSION.search(version)
    if match:
        version = match.group(1)
        return version
    logger.error(f"Could not find version for {package} on shields.io")
    return None


def main():
    print(_get_raw_shields_io_output("numpy"))


if __name__ == "__main__":
    main()
