from dataclasses import dataclass
import re
from typing import List

from packaging import version as packaging_version

from dbt.exceptions import VersionsNotCompatibleException
import dbt.utils

from dbt.dataclass_schema import dbtClassMixin, StrEnum
from typing import Optional


class Matchers(StrEnum):
    GREATER_THAN = '>'
    GREATER_THAN_OR_EQUAL = '>='
    LESS_THAN = '<'
    LESS_THAN_OR_EQUAL = '<='
    EXACT = '='


@dataclass
class VersionSpecification(dbtClassMixin):
    major: Optional[str] = None
    minor: Optional[str] = None
    patch: Optional[str] = None
    prerelease: Optional[str] = None
    build: Optional[str] = None
    matcher: Matchers = Matchers.EXACT


_MATCHERS = r"(?P<matcher>\>=|\>|\<|\<=|=)?"
_NUM_NO_LEADING_ZEROS = r"(0|[1-9][0-9]*)"
_ALPHA = r"[0-9A-Za-z-]*"
_ALPHA_NO_LEADING_ZEROS = r"(0|[1-9A-Za-z-][0-9A-Za-z-]*)"

_BASE_VERSION_REGEX = r"""
(?P<major>{num_no_leading_zeros})\.
(?P<minor>{num_no_leading_zeros})\.
(?P<patch>{num_no_leading_zeros})
""".format(num_no_leading_zeros=_NUM_NO_LEADING_ZEROS)

_VERSION_EXTRA_REGEX = r"""
(\-?
  (?P<prerelease>
    {alpha_no_leading_zeros}(\.{alpha_no_leading_zeros})*))?
(\+
  (?P<build>
    {alpha}(\.{alpha})*))?
""".format(
    alpha_no_leading_zeros=_ALPHA_NO_LEADING_ZEROS,
    alpha=_ALPHA)


_VERSION_REGEX_PAT_STR = r"""
^
{matchers}
{base_version_regex}
{version_extra_regex}
$
""".format(
    matchers=_MATCHERS,
    base_version_regex=_BASE_VERSION_REGEX,
    version_extra_regex=_VERSION_EXTRA_REGEX)

_VERSION_REGEX = re.compile(_VERSION_REGEX_PAT_STR, re.VERBOSE)


@dataclass
class VersionSpecifier(VersionSpecification):
    def to_version_string(self, skip_matcher=False):
        prerelease = ''
        build = ''
        matcher = ''

        if self.prerelease:
            prerelease = '-' + self.prerelease

        if self.build:
            build = '+' + self.build

        if not skip_matcher:
            matcher = self.matcher
        return '{}{}.{}.{}{}{}'.format(
            matcher,
            self.major,
            self.minor,
            self.patch,
            prerelease,
            build)

    @classmethod
    def from_version_string(cls, version_string):
        match = _VERSION_REGEX.match(version_string)

        if not match:
            raise dbt.exceptions.SemverException(
                'Could not parse version "{}"'.format(version_string))

        matched = {k: v for k, v in match.groupdict().items() if v is not None}

        return cls.from_dict(matched)

    def __str__(self):
        return self.to_version_string()

    def to_range(self):
        range_start: VersionSpecifier = UnboundedVersionSpecifier()
        range_end: VersionSpecifier = UnboundedVersionSpecifier()

        if self.matcher == Matchers.EXACT:
            range_start = self
            range_end = self

        elif self.matcher in [Matchers.GREATER_THAN,
                              Matchers.GREATER_THAN_OR_EQUAL]:
            range_start = self

        elif self.matcher in [Matchers.LESS_THAN,
                              Matchers.LESS_THAN_OR_EQUAL]:
            range_end = self

        return VersionRange(
            start=range_start,
            end=range_end)

    def compare(self, other):
        if self.is_unbounded or other.is_unbounded:
            return 0

        for key in ['major', 'minor', 'patch', 'prerelease']:
            (a, b) = (getattr(self, key), getattr(other, key))
            if key == 'prerelease':
                if a is None and b is None:
                    continue
                if a is None:
                    if self.matcher == Matchers.LESS_THAN:
                        # If 'a' is not a pre-release but 'b' is, and b must be
                        # less than a, return -1 to prevent installations of
                        # pre-releases with greater base version than a
                        # maximum specified non-pre-release version.
                        return -1
                    # Otherwise, stable releases are considered greater than
                    # pre-release
                    return 1
                if b is None:
                    return -1
            if packaging_version.parse(a) > packaging_version.parse(b):
                return 1
            elif packaging_version.parse(a) < packaging_version.parse(b):
                return -1

        equal = ((self.matcher == Matchers.GREATER_THAN_OR_EQUAL and
                  other.matcher == Matchers.LESS_THAN_OR_EQUAL) or
                 (self.matcher == Matchers.LESS_THAN_OR_EQUAL and
                  other.matcher == Matchers.GREATER_THAN_OR_EQUAL))
        if equal:
            return 0

        lt = ((self.matcher == Matchers.LESS_THAN and
               other.matcher == Matchers.LESS_THAN_OR_EQUAL) or
              (other.matcher == Matchers.GREATER_THAN and
               self.matcher == Matchers.GREATER_THAN_OR_EQUAL) or
              (self.is_upper_bound and other.is_lower_bound))
        if lt:
            return -1

        gt = ((other.matcher == Matchers.LESS_THAN and
               self.matcher == Matchers.LESS_THAN_OR_EQUAL) or
              (self.matcher == Matchers.GREATER_THAN and
               other.matcher == Matchers.GREATER_THAN_OR_EQUAL) or
              (self.is_lower_bound and other.is_upper_bound))
        if gt:
            return 1

        return 0

    def __lt__(self, other):
        return self.compare(other) == -1

    def __gt__(self, other):
        return self.compare(other) == 1

    def __eq___(self, other):
        return self.compare(other) == 0

    def __cmp___(self, other):
        return self.compare(other)

    @property
    def is_unbounded(self):
        return False

    @property
    def is_lower_bound(self):
        return self.matcher in [Matchers.GREATER_THAN,
                                Matchers.GREATER_THAN_OR_EQUAL]

    @property
    def is_upper_bound(self):
        return self.matcher in [Matchers.LESS_THAN,
                                Matchers.LESS_THAN_OR_EQUAL]

    @property
    def is_exact(self):
        return self.matcher == Matchers.EXACT


@dataclass
class VersionRange:
    start: VersionSpecifier
    end: VersionSpecifier

    def _try_combine_exact(self, a, b):
        if a.compare(b) == 0:
            return a
        else:
            raise VersionsNotCompatibleException()

    def _try_combine_lower_bound_with_exact(self, lower, exact):
        comparison = lower.compare(exact)

        if (comparison < 0 or
            (comparison == 0 and
             lower.matcher == Matchers.GREATER_THAN_OR_EQUAL)):
            return exact

        raise VersionsNotCompatibleException()

    def _try_combine_lower_bound(self, a, b):
        if b.is_unbounded:
            return a
        elif a.is_unbounded:
            return b

        if not (a.is_exact or b.is_exact):
            comparison = (a.compare(b) < 0)

            if comparison:
                return b
            else:
                return a

        elif a.is_exact:
            return self._try_combine_lower_bound_with_exact(b, a)

        elif b.is_exact:
            return self._try_combine_lower_bound_with_exact(a, b)

    def _try_combine_upper_bound_with_exact(self, upper, exact):
        comparison = upper.compare(exact)

        if (comparison > 0 or
            (comparison == 0 and
             upper.matcher == Matchers.LESS_THAN_OR_EQUAL)):
            return exact

        raise VersionsNotCompatibleException()

    def _try_combine_upper_bound(self, a, b):
        if b.is_unbounded:
            return a
        elif a.is_unbounded:
            return b

        if not (a.is_exact or b.is_exact):
            comparison = (a.compare(b) > 0)

            if comparison:
                return b
            else:
                return a

        elif a.is_exact:
            return self._try_combine_upper_bound_with_exact(b, a)

        elif b.is_exact:
            return self._try_combine_upper_bound_with_exact(a, b)

    def reduce(self, other):
        start = None

        if(self.start.is_exact and other.start.is_exact):
            start = end = self._try_combine_exact(self.start, other.start)

        else:
            start = self._try_combine_lower_bound(self.start, other.start)
            end = self._try_combine_upper_bound(self.end, other.end)

        if start.compare(end) > 0:
            raise VersionsNotCompatibleException()

        return VersionRange(start=start, end=end)

    def __str__(self):
        result = []

        if self.start.is_unbounded and self.end.is_unbounded:
            return 'ANY'

        if not self.start.is_unbounded:
            result.append(self.start.to_version_string())

        if not self.end.is_unbounded:
            result.append(self.end.to_version_string())

        return ', '.join(result)

    def to_version_string_pair(self):
        to_return = []

        if not self.start.is_unbounded:
            to_return.append(self.start.to_version_string())

        if not self.end.is_unbounded:
            to_return.append(self.end.to_version_string())

        return to_return


class UnboundedVersionSpecifier(VersionSpecifier):
    def __init__(self, *args, **kwargs):
        super().__init__(
            matcher=Matchers.EXACT,
            major=None,
            minor=None,
            patch=None,
            prerelease=None,
            build=None
        )

    def __str__(self):
        return "*"

    @property
    def is_unbounded(self):
        return True

    @property
    def is_lower_bound(self):
        return False

    @property
    def is_upper_bound(self):
        return False

    @property
    def is_exact(self):
        return False


def reduce_versions(*args):
    version_specifiers = []

    for version in args:
        if isinstance(version, UnboundedVersionSpecifier) or version is None:
            continue

        elif isinstance(version, VersionSpecifier):
            version_specifiers.append(version)

        elif isinstance(version, VersionRange):
            if not isinstance(version.start, UnboundedVersionSpecifier):
                version_specifiers.append(version.start)

            if not isinstance(version.end, UnboundedVersionSpecifier):
                version_specifiers.append(version.end)

        else:
            version_specifiers.append(
                VersionSpecifier.from_version_string(version))

    for version_specifier in version_specifiers:
        if not isinstance(version_specifier, VersionSpecifier):
            raise Exception(version_specifier)

    if not version_specifiers:
        return VersionRange(start=UnboundedVersionSpecifier(),
                            end=UnboundedVersionSpecifier())

    try:
        to_return = version_specifiers.pop().to_range()

        for version_specifier in version_specifiers:
            to_return = to_return.reduce(version_specifier.to_range())
    except VersionsNotCompatibleException:
        raise VersionsNotCompatibleException(
            'Could not find a satisfactory version from options: {}'
            .format([str(a) for a in args]))

    return to_return


def versions_compatible(*args):
    if len(args) == 1:
        return True

    try:
        reduce_versions(*args)
        return True
    except VersionsNotCompatibleException:
        return False


def find_possible_versions(requested_range, available_versions):
    possible_versions = []

    for version_string in available_versions:
        version = VersionSpecifier.from_version_string(version_string)

        if(versions_compatible(version,
                               requested_range.start,
                               requested_range.end)):
            possible_versions.append(version)

    sorted_versions = sorted(possible_versions, reverse=True)
    return [v.to_version_string(skip_matcher=True) for v in sorted_versions]


def resolve_to_specific_version(requested_range, available_versions):
    max_version = None
    max_version_string = None

    for version_string in available_versions:
        version = VersionSpecifier.from_version_string(version_string)

        if(versions_compatible(version,
           requested_range.start, requested_range.end) and
           (max_version is None or max_version.compare(version) < 0)):
            max_version = version
            max_version_string = version_string

    return max_version_string


def filter_installable(
        versions: List[str],
        install_prerelease: bool
) -> List[str]:
    installable = []
    installable_dict = {}
    for version_string in versions:
        version = VersionSpecifier.from_version_string(version_string)
        if install_prerelease or not version.prerelease:
            installable.append(version)
            installable_dict[str(version)] = version_string
    sorted_installable = sorted(installable)
    sorted_installable_original_versions = [
        str(installable_dict.get(str(version))) for version in sorted_installable
    ]
    return sorted_installable_original_versions
