import logging
import os

import runez
from runez import short

from pickley import abort, PICKLEY

LOG = logging.getLogger(__name__)

WRAPPER_MARK = "# Wrapper generated by https://pypi.org/project/pickley/"

GENERIC_WRAPPER = """
#!/bin/bash

%s

if [[ -x {pickley} ]]; then
    {hook}nohup {pickley} auto-upgrade {name}{bg}
fi
if [[ -x {source} ]]; then
    {hook}exec {source} "$@"
else
    echo "{source} is not available anymore"
    echo ""
    echo "Please reinstall with:"
    echo "{pickley} install -f {name}"
    exit 1
fi
""" % WRAPPER_MARK

# Specific wrapper for pickley itself (avoid calling ourselves back recursively for auto-upgrade)
PICKLEY_WRAPPER = """
#!/bin/bash

%s

if [[ -x {source} ]]; then
    if [[ "$*" != *"auto-"* ]]; then
        {hook}nohup {source} auto-upgrade {name}{bg}
    fi
    {hook}exec {source} "$@"
else
    echo "{source} is not available anymore"
    echo ""
    echo "Please reinstall with:"
    url=`curl -s https://pypi.org/pypi/pickley/json | grep -Eo '"download_url":"([^"]+)"' | cut -d'"' -f4`
    echo curl -sLo {pickley} $url
    exit 1
fi
""" % WRAPPER_MARK


class DeliveryMethod(object):
    """
    Various implementation of delivering the actual executables
    """

    action = "Delivered"
    short_name = "deliver"
    ping = True  # Touch .ping file when done?

    @classmethod
    def delivery_method_by_name(cls, name):
        """
        Args:
            name (str): Name of delivery method

        Returns:
            (DeliveryMethod): Associated delivery method
        """
        if name == "wrap":
            return DeliveryMethodWrap()

        if name == "symlink":
            return DeliveryMethodSymlink()

        return abort("Unknown delivery method '%s'" % runez.red(name))

    def install(self, pspec, venv, entry_points):
        """
        Args:
            pspec (pickley.PackageSpec): Package spec this installation is for
            venv (pickley.package.PythonVenv): Virtual env where executables reside (DOT_META/<package>/...)
            entry_points (dict | list): Full path of executable to deliver (<base>/<entry_point>)
        """
        if not pspec.is_clear_for_installation():
            auto_uninstall(pspec.exe_path(pspec.dashed))

        try:
            prev_manifest = pspec.get_manifest()
            for name in entry_points:
                src = venv.bin_path(name)
                dest = pspec.exe_path(name)
                if runez.DRYRUN:
                    print("Would %s %s -> %s" % (self.short_name, short(dest), short(src)))
                    continue

                if not os.path.exists(src):
                    abort("Can't %s %s -> %s: source does not exist" % (self.short_name, short(dest), runez.red(short(src))))

                LOG.debug("%s %s -> %s" % (self.action, short(dest), short(src)))
                self._install(pspec, dest, src)

            manifest = pspec.save_manifest(entry_points)
            if not runez.DRYRUN and prev_manifest and prev_manifest.entrypoints:
                for old_ep in prev_manifest.entrypoints:
                    if old_ep and old_ep not in entry_points:
                        # Remove old entry points that are not in new manifest any more
                        runez.delete(pspec.exe_path(old_ep))

            if self.ping:
                # Touch the .ping file since this is a fresh install (no need to check for upgrades right away)
                runez.touch(pspec.ping_path)

            return manifest

        except Exception as e:
            abort("Failed to %s %s: %s" % (self.short_name, short(pspec), runez.red(e)))

    def _install(self, pspec, target, source):
        raise NotImplementedError("%s is not implemented" % self.__class__.__name__)


class DeliveryMethodSymlink(DeliveryMethod):
    """
    Deliver via symlink
    """

    action = "Symlinked"
    short_name = "symlink"

    def _install(self, pspec, target, source):
        runez.delete(target, logger=False)
        if os.path.isabs(source) and os.path.isabs(target):
            parent = runez.parent_folder(target)
            if runez.parent_folder(source).startswith(parent):
                # Use relative path if source is under target
                source = os.path.relpath(source, parent)

        os.symlink(source, target)


class DeliveryMethodWrap(DeliveryMethod):
    """
    Deliver via a small wrap that ensures target executable is up-to-date
    """

    action = "Wrapped"
    short_name = "wrap"

    # Can be set in tests to make wrapper a no-op
    hook = ""
    bg = " &> /dev/null &"

    def _install(self, pspec, target, source):
        pickley = pspec.cfg.base.full_path(PICKLEY)
        if pspec.dashed == PICKLEY:
            wrapper = PICKLEY_WRAPPER

        else:
            wrapper = GENERIC_WRAPPER
            if not os.path.exists(pickley):
                # We're running from development venv
                pickley = pspec.cfg.program_path

        contents = wrapper.lstrip().format(
            hook=self.hook,
            bg=self.bg,
            name=runez.quoted(pspec.dashed, adapter=None),
            pickley=runez.quoted(pickley, adapter=None),
            source=runez.quoted(source, adapter=None),
        )
        runez.delete(target, logger=False)
        runez.write(target, contents, logger=False)
        runez.make_executable(target, logger=False)


def auto_uninstall(target):
    """
    Args:
        target (str): Path to executable to auto-uninstall if needed

    Returns:
        Aborts if uninstallation was not possible
    """
    brew, name = find_brew_name(target)
    if brew and name:
        result = runez.run(brew, "uninstall", "-f", name, fatal=False, logger=LOG.info)
        if result.succeeded:
            LOG.info("Auto-uninstalled brew formula '%s'" % name)
            return  #

        command = "%s uninstall %s" % (brew, name)
        abort("'%s' failed, please check" % runez.bold(command))

    abort("Can't automatically uninstall %s" % runez.short(target))


def find_brew_name(target):
    """
    Args:
        target (str): Path to executable file

    Returns:
        (str, str): Name of brew formula, if target was installed with brew
    """
    if os.path.islink(target):
        path = os.path.realpath(target)
        folder = runez.parent_folder(target)
        cellar = os.path.join(runez.parent_folder(folder), "Cellar")
        if path.startswith(cellar):
            brew = os.path.join(folder, "brew")
            name, _, _ = path[len(cellar) + 1:].partition("/")
            return brew, name

    return None, None
