import operator
import re

from .vendor import attr, delegator

from .environments import PIPENV_INSTALL_TIMEOUT


@attr.s
class Version(object):

    major = attr.ib()
    minor = attr.ib()
    patch = attr.ib()

    def __str__(self):
        parts = [self.major, self.minor]
        if self.patch is not None:
            parts.append(self.patch)
        return '.'.join(str(p) for p in parts)

    @classmethod
    def parse(cls, name):
        """Parse an X.Y.Z or X.Y string into a version tuple.
        """
        match = re.match(r'^(\d+)\.(\d+)(?:\.(\d+))?$', name)
        if not match:
            raise ValueError('invalid version name {0!r}'.format(name))
        major = int(match.group(1))
        minor = int(match.group(2))
        patch = match.group(3)
        if patch is not None:
            patch = int(patch)
        return cls(major, minor, patch)

    @property
    def cmpkey(self):
        """Make the version a comparable tuple.

        Some old Python versions does not have a patch part, e.g. 2.7.0 is
        named "2.7" in pyenv. Fix that, otherwise `None` will fail to compare
        with int.
        """
        return (self.major, self.minor, self.patch or 0)

    def matches_minor(self, other):
        """Check whether this version matches the other in (major, minor).
        """
        return (self.major, self.minor) == (other.major, other.minor)


class PyenvError(RuntimeError):
    def __init__(self, desc, c):
        super(PyenvError, self).__init__(desc)
        self.out = c.out
        self.err = c.err


class Runner(object):

    def __init__(self, pyenv):
        self._cmd = pyenv

    def _pyenv(self, *args, **kwargs):
        timeout = kwargs.pop('timeout', delegator.TIMEOUT)
        if kwargs:
            k = list(kwargs.keys())[0]
            raise TypeError('unexpected keyword argument {0!r}'.format(k))
        args = (self._cmd,) + tuple(args)
        c = delegator.run(args, block=False, timeout=timeout)
        c.block()
        if c.return_code != 0:
            raise PyenvError('faild to run {0}'.format(args), c)
        return c

    def iter_installable_versions(self):
        """Iterate through CPython versions available for Pipenv to install.
        """
        for name in self._pyenv('install', '--list').out.splitlines():
            try:
                version = Version.parse(name.strip())
            except ValueError:
                continue
            yield version

    def find_version_to_install(self, name):
        """Find a version in pyenv from the version supplied.

        A ValueError is raised if a matching version cannot be found.
        """
        version = Version.parse(name)
        if version.patch is not None:
            return name
        try:
            best_match = max((
                inst_version
                for inst_version in self.iter_installable_versions()
                if inst_version.matches_minor(version)
            ), key=operator.attrgetter('cmpkey'))
        except ValueError:
            raise ValueError(
                'no installable version found for {0!r}'.format(name),
            )
        return best_match

    def install(self, version):
        """Install the given version with pyenv.

        The version must be a ``Version`` instance representing a version
        found in pyenv.

        A ValueError is raised if the given version does not have a match in
        pyenv. A PyenvError is raised if the pyenv command fails.
        """
        c = self._pyenv(
            'install', '-s', str(version),
            timeout=PIPENV_INSTALL_TIMEOUT,
        )
        return c