"""A plugin for pylint to identify imports and functions which should not be used."""
from __future__ import (absolute_import, division, print_function)

__metaclass__ = type

import astroid

from pylint.checkers import BaseChecker
from pylint.interfaces import IAstroidChecker


class BlacklistEntry:
    """Defines a import blacklist entry."""
    def __init__(self, alternative, modules_only=False, names=None, ignore_paths=None):
        """
        :type alternative: str
        :type modules_only: bool
        :type names: tuple[str] | None
        :type ignore_paths: tuple[str] | None
        """
        self.alternative = alternative
        self.modules_only = modules_only
        self.names = set(names) if names else None
        self.ignore_paths = ignore_paths

    def applies_to(self, path, name=None):
        """
        :type path: str
        :type name: str | None
        :rtype: bool
        """
        if self.names:
            if not name:
                return False

            if name not in self.names:
                return False

        if self.ignore_paths and any(path.endswith(ignore_path) for ignore_path in self.ignore_paths):
            return False

        if self.modules_only:
            return is_module_path(path)

        return True


def is_module_path(path):
    """
    :type path: str
    :rtype: bool
    """
    return '/lib/ansible/modules/' in path or '/lib/ansible/module_utils/' in path


class AnsibleBlacklistChecker(BaseChecker):
    """Checker for blacklisted imports and functions."""
    __implements__ = (IAstroidChecker,)

    name = 'blacklist'

    BAD_IMPORT = 'ansible-bad-import'
    BAD_IMPORT_FROM = 'ansible-bad-import-from'
    BAD_FUNCTION = 'ansible-bad-function'
    BAD_MODULE_IMPORT = 'ansible-bad-module-import'

    msgs = dict(
        E5101=('Import %s instead of %s',
               BAD_IMPORT,
               'Identifies imports which should not be used.'),
        E5102=('Import %s from %s instead of %s',
               BAD_IMPORT_FROM,
               'Identifies imports which should not be used.'),
        E5103=('Call %s instead of %s',
               BAD_FUNCTION,
               'Identifies functions which should not be used.'),
        E5104=('Import external package or ansible.module_utils not %s',
               BAD_MODULE_IMPORT,
               'Identifies imports which should not be used.'),
    )

    blacklist_imports = dict(
        # Additional imports that we may want to start checking:
        # boto=BlacklistEntry('boto3', modules_only=True),
        # requests=BlacklistEntry('ansible.module_utils.urls', modules_only=True),
        # urllib=BlacklistEntry('ansible.module_utils.urls', modules_only=True),

        # see https://docs.python.org/2/library/urllib2.html
        urllib2=BlacklistEntry('ansible.module_utils.urls',
                               ignore_paths=(
                                   '/lib/ansible/module_utils/urls.py',
                               )),

        # see https://docs.python.org/3.7/library/collections.abc.html
        collections=BlacklistEntry('ansible.module_utils.common._collections_compat',
                                   ignore_paths=(
                                       '/lib/ansible/module_utils/common/_collections_compat.py',
                                   ),
                                   names=(
                                       'MappingView',
                                       'ItemsView',
                                       'KeysView',
                                       'ValuesView',
                                       'Mapping', 'MutableMapping',
                                       'Sequence', 'MutableSequence',
                                       'Set', 'MutableSet',
                                       'Container',
                                       'Hashable',
                                       'Sized',
                                       'Callable',
                                       'Iterable',
                                       'Iterator',
                                   )),
    )

    blacklist_functions = {
        # see https://docs.python.org/2/library/tempfile.html#tempfile.mktemp
        'tempfile.mktemp': BlacklistEntry('tempfile.mkstemp'),

        'sys.exit': BlacklistEntry('exit_json or fail_json',
                                   ignore_paths=(
                                       '/lib/ansible/module_utils/basic.py',
                                       '/lib/ansible/modules/utilities/logic/async_wrapper.py',
                                       '/lib/ansible/module_utils/common/removed.py',
                                   ),
                                   modules_only=True),
    }

    def visit_import(self, node):
        """
        :type node: astroid.node_classes.Import
        """
        for name in node.names:
            self._check_import(node, name[0])

    def visit_importfrom(self, node):
        """
        :type node: astroid.node_classes.ImportFrom
        """
        self._check_importfrom(node, node.modname, node.names)

    def visit_attribute(self, node):
        """
        :type node: astroid.node_classes.Attribute
        """
        last_child = node.last_child()

        # this is faster than using type inference and will catch the most common cases
        if not isinstance(last_child, astroid.node_classes.Name):
            return

        module = last_child.name

        entry = self.blacklist_imports.get(module)

        if entry and entry.names:
            if entry.applies_to(self.linter.current_file, node.attrname):
                self.add_message(self.BAD_IMPORT_FROM, args=(node.attrname, entry.alternative, module), node=node)

    def visit_call(self, node):
        """
        :type node: astroid.node_classes.Call
        """
        try:
            for i in node.func.inferred():
                func = None

                if isinstance(i, astroid.scoped_nodes.FunctionDef) and isinstance(i.parent, astroid.scoped_nodes.Module):
                    func = '%s.%s' % (i.parent.name, i.name)

                if not func:
                    continue

                entry = self.blacklist_functions.get(func)

                if entry and entry.applies_to(self.linter.current_file):
                    self.add_message(self.BAD_FUNCTION, args=(entry.alternative, func), node=node)
        except astroid.exceptions.InferenceError:
            pass

    def _check_import(self, node, modname):
        """
        :type node: astroid.node_classes.Import
        :type modname: str
        """
        self._check_module_import(node, modname)

        entry = self.blacklist_imports.get(modname)

        if not entry:
            return

        if entry.applies_to(self.linter.current_file):
            self.add_message(self.BAD_IMPORT, args=(entry.alternative, modname), node=node)

    def _check_importfrom(self, node, modname, names):
        """
        :type node: astroid.node_classes.ImportFrom
        :type modname: str
        :type names:  list[str[
        """
        self._check_module_import(node, modname)

        entry = self.blacklist_imports.get(modname)

        if not entry:
            return

        for name in names:
            if entry.applies_to(self.linter.current_file, name[0]):
                self.add_message(self.BAD_IMPORT_FROM, args=(name[0], entry.alternative, modname), node=node)

    def _check_module_import(self, node, modname):
        """
        :type node: astroid.node_classes.Import | astroid.node_classes.ImportFrom
        :type modname: str
        """
        if not is_module_path(self.linter.current_file):
            return

        if modname == 'ansible.module_utils' or modname.startswith('ansible.module_utils.'):
            return

        if modname == 'ansible' or modname.startswith('ansible.'):
            self.add_message(self.BAD_MODULE_IMPORT, args=(modname,), node=node)


def register(linter):
    """required method to auto register this checker """
    linter.register_checker(AnsibleBlacklistChecker(linter))
