from typing import Iterable, List

import jinja2

from dbt.clients import jinja
from dbt.contracts.graph.unparsed import UnparsedMacro
from dbt.contracts.graph.parsed import ParsedMacro
from dbt.exceptions import CompilationException
from dbt.logger import GLOBAL_LOGGER as logger
from dbt.node_types import NodeType
from dbt.parser.base import BaseParser
from dbt.parser.search import FileBlock, FilesystemSearcher
from dbt.utils import MACRO_PREFIX


class MacroParser(BaseParser[ParsedMacro]):
    def get_paths(self):
        return FilesystemSearcher(
            project=self.project,
            relative_dirs=self.project.macro_paths,
            extension='.sql',
        )

    @property
    def resource_type(self) -> NodeType:
        return NodeType.Macro

    @classmethod
    def get_compiled_path(cls, block: FileBlock):
        return block.path.relative_path

    def parse_macro(
        self, block: jinja.BlockTag, base_node: UnparsedMacro, name: str
    ) -> ParsedMacro:
        unique_id = self.generate_unique_id(name)

        return ParsedMacro(
            path=base_node.path,
            macro_sql=block.full_block,
            original_file_path=base_node.original_file_path,
            package_name=base_node.package_name,
            root_path=base_node.root_path,
            resource_type=base_node.resource_type,
            name=name,
            unique_id=unique_id,
        )

    def parse_unparsed_macros(
        self, base_node: UnparsedMacro
    ) -> Iterable[ParsedMacro]:
        try:
            blocks: List[jinja.BlockTag] = [
                t for t in
                jinja.extract_toplevel_blocks(
                    base_node.raw_sql,
                    allowed_blocks={'macro', 'materialization'},
                    collect_raw_data=False,
                )
                if isinstance(t, jinja.BlockTag)
            ]
        except CompilationException as exc:
            exc.add_node(base_node)
            raise

        for block in blocks:
            try:
                ast = jinja.parse(block.full_block)
            except CompilationException as e:
                e.add_node(base_node)
                raise

            macro_nodes = list(ast.find_all(jinja2.nodes.Macro))

            if len(macro_nodes) != 1:
                # things have gone disastrously wrong, we thought we only
                # parsed one block!
                raise CompilationException(
                    f'Found multiple macros in {block.full_block}, expected 1',
                    node=base_node
                )

            macro_name = macro_nodes[0].name

            if not macro_name.startswith(MACRO_PREFIX):
                continue

            name: str = macro_name.replace(MACRO_PREFIX, '')
            node = self.parse_macro(block, base_node, name)
            yield node

    def parse_file(self, block: FileBlock):
        # mark the file as seen, even if there are no macros in it
        self.results.get_file(block.file)
        source_file = block.file

        original_file_path = source_file.path.original_file_path

        logger.debug("Parsing {}".format(original_file_path))

        # this is really only used for error messages
        base_node = UnparsedMacro(
            path=original_file_path,
            original_file_path=original_file_path,
            package_name=self.project.project_name,
            raw_sql=source_file.contents,
            root_path=self.project.project_root,
            resource_type=NodeType.Macro,
        )

        for node in self.parse_unparsed_macros(base_node):
            self.results.add_macro(block.file, node)
