import __main__
import os
import re
import sys
import json
import errno
import random
import string
import urllib
import shutil
import logging
from typing import Dict, Any

log = logging.getLogger(__name__)

def get_main_source_path():
    """Get the absolute path to the program entrypoint source."""
    return os.path.abspath(__main__.__file__)

def main_source_lives_in_cwd():
    """Check if the main executable is located at the root of CWD."""
    return os.path.dirname(get_main_source_path()) == os.getcwd()

def random_string(size=5, chars=string.ascii_lowercase + string.digits):
    """Generate random string."""
    return "".join(random.choice(chars) for _ in range(size))

def abs_working_dir(path):
    """Get absolute path to parent dir."""
    abs_path = os.path.abspath(path)
    if os.path.isfile(path):
        abs_path = os.path.dirname(abs_path)
    return abs_path

def rm_r(path, ignore_missing=True, silent=False):
    """Remove a file or directory.

    Similar to rm -r. If the path does not exist and ignore_missing is False,
    OSError is raised, otherwise it is ignored.
    If silent is True, nothing is raised.
    """
    def onerror(function, path, excinfo):
        # Function to handle ENOENT in shutil.rmtree()
        e = excinfo[1]
        if (ignore_missing and isinstance(e, OSError)
                and e.errno == errno.ENOENT):
            return
        raise e

    log.info("Removing path `%s'", path)

    try:
        if os.path.isfile(path) or os.path.islink(path):
            os.remove(path)
        elif os.path.isdir(path):
            shutil.rmtree(path, onerror=onerror)
        elif os.path.exists(path) and not silent:
            raise RuntimeError("Failed to remove path `%s': Path exists but is"
                               " not a file nor a directory" % path)
        else:
            # The path does not exists, raise the appropriate exception and let
            # the exception handler handle it (i.e., check ignore_missing etc.)
            raise OSError(errno.ENOENT, "No such file or directory", path)
    except OSError as e:
        if silent:
            log.debug("Path `%s' does not exist, skipping removing it", path)
            return
        if (not ignore_missing) or (e.errno != errno.ENOENT):
            log.error("Failed to remove path `%s' (errno: %s): %s",
                      path, e.errno, e)
            raise

def remove_ansi_color_sequences(text):
    """Remove ANSI color sequences from text."""
    ansi_color_escape = re.compile(r'\x1B\[[0-9;]*m')
    return ansi_color_escape.sub('', text)

def comment_magic_commands(code):
    """Comment the magic commands in a code block."""
    magic_pattern = re.compile(r'^(\s*%%?.*)$', re.MULTILINE)
    return re.sub(magic_pattern, r'#\1', code.strip())

def encode_url_component(component: str):
    """Encode a value so it can safely be used as a URL component."""
    return urllib.parse.quote(component, safe="")

def sanitize_k8s_name(name):
    """Sanitize a string to conform to Kubernetes naming conventions."""
    name = re.sub("-+", "-", re.sub("[^-0-9a-z]+", "-", name.lower()))
    return name.lstrip("-").rstrip("-")

def is_ipython() -> bool:
    """Returns whether the code is running in a ipython kernel."""
    try:
        import IPython
        ipy = IPython.get_ipython()
        if ipy is None:
            return False
    except ImportError:
        return False
    return True

def read_json_from_file(path: str) -> Dict:
    """Read a file that contains a JSON object and return it as dictionary."""
    try:
        return json.loads(open(path, 'r').read())
    except json.JSONDecodeError:
        log.exception("Failed to parse json file %s", path)
        raise

def ensure_or_create_dir(filepath: str):
    """Ensure the dir of a file exists and isdir, or create it.

    Raises:
        RuntimeError: if dirname of filepath exists and is not a directory
    """
    dirname = os.path.dirname(filepath)
    if not os.path.exists(dirname):
        os.makedirs(dirname)
    elif not os.path.isdir(dirname):
        raise RuntimeError("'%s' is not a directory" % dirname)

def clean_dir(path: str):
    """If path exists, remove and then create empty dir."""
    if os.path.exists(path):
        shutil.rmtree(path)
    os.makedirs(path)

def shorten_long_string(obj: Any, chars: int = 75):
    """Shorten the string representation of the input object."""
    str_input = str(obj)
    return str_input[:chars] + " ..... " + str_input[len(str_input) - chars:]

def dedent(text: str):
    """Remove longest common prefix consisting of whitespaces.

    Args:
        text: Multiline string
    """
    matches = re.findall(r"(?m)^\s+", text)
    if len(matches) < len(text.splitlines()):
        return text
    return re.sub(r"(?m)^.{%d}" % min(map(len, matches)), "", text)
