import os
import re
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass

from typing import (
    Set, Iterator, List, Optional, Dict, Union, Any, Iterable, Tuple
)
from .graph import UniqueId
from .selector_methods import MethodName
from dbt.exceptions import RuntimeException, InvalidSelectorException


RAW_SELECTOR_PATTERN = re.compile(
    r'\A'
    r'(?P<childrens_parents>(\@))?'
    r'(?P<parents>((?P<parents_depth>(\d*))\+))?'
    r'((?P<method>([\w.]+)):)?(?P<value>(.*?))'
    r'(?P<children>(\+(?P<children_depth>(\d*))))?'
    r'\Z'
)
SELECTOR_METHOD_SEPARATOR = '.'


def _probably_path(value: str):
    """Decide if value is probably a path. Windows has two path separators, so
    we should check both sep ('\\') and altsep ('/') there.
    """
    if os.path.sep in value:
        return True
    elif os.path.altsep is not None and os.path.altsep in value:
        return True
    else:
        return False


def _match_to_int(match: Dict[str, str], key: str) -> Optional[int]:
    raw = match.get(key)
    # turn the empty string into None, too.
    if not raw:
        return None
    try:
        return int(raw)
    except ValueError as exc:
        raise RuntimeException(
            f'Invalid node spec - could not handle parent depth {raw}'
        ) from exc


SelectionSpec = Union[
    'SelectionCriteria',
    'SelectionIntersection',
    'SelectionDifference',
    'SelectionUnion',
]


@dataclass
class SelectionCriteria:
    raw: Any
    method: MethodName
    method_arguments: List[str]
    value: Any
    childrens_parents: bool
    parents: bool
    parents_depth: Optional[int]
    children: bool
    children_depth: Optional[int]
    greedy: bool = False
    greedy_warning: bool = False  # do not raise warning for yaml selectors

    def __post_init__(self):
        if self.children and self.childrens_parents:
            raise RuntimeException(
                f'Invalid node spec {self.raw} - "@" prefix and "+" suffix '
                'are incompatible'
            )

    @classmethod
    def default_method(cls, value: str) -> MethodName:
        if _probably_path(value):
            return MethodName.Path
        else:
            return MethodName.FQN

    @classmethod
    def parse_method(
        cls, groupdict: Dict[str, Any]
    ) -> Tuple[MethodName, List[str]]:
        raw_method = groupdict.get('method')
        if raw_method is None:
            return cls.default_method(groupdict['value']), []

        method_parts: List[str] = raw_method.split(SELECTOR_METHOD_SEPARATOR)
        try:
            method_name = MethodName(method_parts[0])
        except ValueError as exc:
            raise InvalidSelectorException(
                f"'{method_parts[0]}' is not a valid method name"
            ) from exc

        method_arguments: List[str] = method_parts[1:]

        return method_name, method_arguments

    @classmethod
    def selection_criteria_from_dict(
        cls, raw: Any, dct: Dict[str, Any], greedy: bool = False
    ) -> 'SelectionCriteria':
        if 'value' not in dct:
            raise RuntimeException(
                f'Invalid node spec "{raw}" - no search value!'
            )
        method_name, method_arguments = cls.parse_method(dct)

        parents_depth = _match_to_int(dct, 'parents_depth')
        children_depth = _match_to_int(dct, 'children_depth')
        return cls(
            raw=raw,
            method=method_name,
            method_arguments=method_arguments,
            value=dct['value'],
            childrens_parents=bool(dct.get('childrens_parents')),
            parents=bool(dct.get('parents')),
            parents_depth=parents_depth,
            children=bool(dct.get('children')),
            children_depth=children_depth,
            greedy=(greedy or bool(dct.get('greedy'))),
        )

    @classmethod
    def dict_from_single_spec(cls, raw: str):
        result = RAW_SELECTOR_PATTERN.match(raw)
        if result is None:
            return {'error': 'Invalid selector spec'}
        dct: Dict[str, Any] = result.groupdict()
        method_name, method_arguments = cls.parse_method(dct)
        meth_name = str(method_name)
        if method_arguments:
            meth_name = meth_name + '.' + '.'.join(method_arguments)
        dct['method'] = meth_name
        dct = {k: v for k, v in dct.items() if (v is not None and v != '')}
        if 'childrens_parents' in dct:
            dct['childrens_parents'] = bool(dct.get('childrens_parents'))
        if 'parents' in dct:
            dct['parents'] = bool(dct.get('parents'))
        if 'children' in dct:
            dct['children'] = bool(dct.get('children'))
        if 'greedy' in dct:
            dct['greedy'] = bool(dct.get('greedy'))
        return dct

    @classmethod
    def from_single_spec(cls, raw: str, greedy: bool = False) -> 'SelectionCriteria':
        result = RAW_SELECTOR_PATTERN.match(raw)
        if result is None:
            # bad spec!
            raise RuntimeException(f'Invalid selector spec "{raw}"')

        return cls.selection_criteria_from_dict(raw, result.groupdict(), greedy=greedy)


class BaseSelectionGroup(Iterable[SelectionSpec], metaclass=ABCMeta):
    def __init__(
        self,
        components: Iterable[SelectionSpec],
        expect_exists: bool = False,
        greedy_warning: bool = True,
        raw: Any = None,
    ):
        self.components: List[SelectionSpec] = list(components)
        self.expect_exists = expect_exists
        self.greedy_warning = greedy_warning
        self.raw = raw

    def __iter__(self) -> Iterator[SelectionSpec]:
        for component in self.components:
            yield component

    @abstractmethod
    def combine_selections(
        self,
        selections: List[Set[UniqueId]],
    ) -> Set[UniqueId]:
        raise NotImplementedError(
            '_combine_selections not implemented!'
        )

    def combined(self, selections: List[Set[UniqueId]]) -> Set[UniqueId]:
        if not selections:
            return set()

        return self.combine_selections(selections)


class SelectionIntersection(BaseSelectionGroup):
    def combine_selections(
        self,
        selections: List[Set[UniqueId]],
    ) -> Set[UniqueId]:
        return set.intersection(*selections)


class SelectionDifference(BaseSelectionGroup):
    def combine_selections(
        self,
        selections: List[Set[UniqueId]],
    ) -> Set[UniqueId]:
        return set.difference(*selections)


class SelectionUnion(BaseSelectionGroup):
    def combine_selections(
        self,
        selections: List[Set[UniqueId]],
    ) -> Set[UniqueId]:
        return set.union(*selections)
