"""
Some service code not falling into more specific categories.
"""

import collections.abc
import logging
import shlex
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any, List, Mapping

import stm32pio.core.log


def _get_version_from_scm() -> str:
    try:
        import setuptools_scm  # setuptools_scm is the dev-only dependency
    except ImportError:
        return "Portable (not-installed). See git tag"
    else:
        # Calculate the version at runtime retrieving it from the actual Git repo
        return setuptools_scm.get_version(root='../..', relative_to=__file__)


# TODO: refactor this after dropping 3.7
def get_version() -> str:
    """Retrieve the app version as string"""
    if sys.version_info >= (3, 8):
        import importlib.metadata
        try:
            # For modern Python use the package metadata (if we are installed). For this to be available the wheel build
            # should be done with setuptools_scm
            return importlib.metadata.version('stm32pio')
        except importlib.metadata.PackageNotFoundError:
            # stm32pio is not installed (i.e. running from sources)
            return _get_version_from_scm()
    else:
        try:
            # Version is stored at the stm32pio/core/version.py file auto-generated by setuptools_scm tool
            import stm32pio.core.version
        except ImportError:
            # Version file is not available, most likely we are not installed (i.e. running from sources)
            return _get_version_from_scm()
        else:
            # noinspection PyUnresolvedReferences
            return stm32pio.core.version.version


def cleanup_mapping(mapping: Mapping[str, Any]) -> dict:
    """Return a deep copy of the given mapping excluding None and empty string values"""
    cleaned = {}
    for key, value in mapping.items():
        if isinstance(value, collections.abc.Mapping):
            cleaned[key] = cleanup_mapping(value)
        elif value is not None and value != '':
            cleaned[key] = value
    return cleaned


def get_folder_contents(path: Path, pattern: str = '*', ignore_list: List[Path] = None) -> List[Path]:
    """
    Return all endpoints inside the given directory (recursively). If specified, paths from the ignore_list will be
    excluded. The resulting array is fully "unfolded" meaning every folder will be expanded, so both it and its children
    will be included into the list. Conversely, the ignore_list is treated in the opposite way so for every folder met
    both it and its children will be ignored completely.

    Note: this is a "naive", straightforward and non-efficient solution (probably, both for time and memory
    consumption). The algorithm behind it can (but not necessarily *should*) probably be improved.

    :param path: root directory
    :param pattern: optional glob-style pattern string to use. Default one will pass all
    :param ignore_list: optional list of paths to ignore (see the full description)
    :return: resulting list of paths
    """

    folder_contents = []
    ignore_list = sorted(ignore_list) if ignore_list is not None else []

    for child in sorted(path.rglob(pattern)):  # all files and folders, recursively
        # Here we check such cases:
        #   1) current child:        a/b/
        #      ignore list entry:    a/b/c/d.txt
        #
        #   2) current child:        a/b/c/d.txt
        #      ignore list entry:    a/b/
        is_root_of_another = next(
            (True for entry in ignore_list if (child in entry.parents) or (entry in child.parents)), False)

        if child not in ignore_list and not is_root_of_another:
            folder_contents.append(child)

    return folder_contents


def remove_folder(path: Path, logger: 'stm32pio.core.log.Logger'):
    """Remove specified directory (empty or not) logging a result"""
    try:
        shutil.rmtree(path)
    except FileNotFoundError:
        logger.debug(f"'{path.name}' folder doesn't exist already")
    except Exception as e:
        logger.error(f"Cannot remove '{path.name}' folder", exc_info=e)
    else:
        logger.debug(f"'{path.name}' folder has been removed")


def run_command(command: str, path: Path, logger: 'stm32pio.core.log.Logger') -> int:
    """
    Launch the command consisting of the given executable and some path as its argument:
        $  [editor] [folder]

    :param command: shell command (e.g. some executable in PATH or as a direct path)
    :param path: argument to pass to the executable
    :param logger: logging.Logger-compatible object
    :return: passes a return code of the launched command
    """

    sanitized_input = shlex.quote(command)
    executable_name = sanitized_input.split()[0]

    logger.info(f"starting '{executable_name}'...")
    try:
        with stm32pio.core.log.LogPipe(logger, logging.DEBUG) as log:
            # Works unstable on some Windows 7 systems, but correct on Win10...
            # result = subprocess.run([command, self.path], check=True)
            completed_process = subprocess.run(f'{sanitized_input} "{path}"', shell=True, check=True,
                                               stdout=log.pipe, stderr=log.pipe)
        logger.debug(completed_process.stdout, from_subprocess=True)

        return completed_process.returncode
    except subprocess.CalledProcessError as e:
        logger.error(f"failed to start '{executable_name}': {e.stdout}")
        return e.returncode


def extract_header_comment(text: str, comment_symbol: str = '#') -> str:
    """
    If text has 1 or more of its first consequent lines that starts with ``comment_symbol``, return them.

    :param text: string to analyze
    :param comment_symbol: symbol for line to be considered as a comment (e.g. # or //)
    :return: header comment
    """

    header_comment = ''
    if text.startswith(comment_symbol):
        for line in text.splitlines(keepends=True):
            if line.startswith(comment_symbol):
                header_comment += line
            else:
                break
    return header_comment
