import os
import re
import tempfile
import urlparse

from djblets.util.filesystem import is_exe_in_path

from reviewboard.scmtools import sshutils
from reviewboard.scmtools.core import SCMTool, HEAD, PRE_CREATION
from reviewboard.scmtools.errors import SCMError, FileNotFoundError, \
                                        RepositoryNotFoundError
from reviewboard.diffviewer.parser import DiffParser, DiffParserError


sshutils.register_rbssh('CVS_RSH')


class CVSTool(SCMTool):
    name = "CVS"
    supports_authentication = True
    dependencies = {
        'executables': ['cvs'],
    }

    rev_re = re.compile(r'^.*?(\d+(\.\d+)+)\r?$')
    repopath_re = re.compile(r'^(?P<hostname>.*):(?P<port>\d+)?(?P<path>.*)')
    ext_cvsroot_re = re.compile(r':ext:([^@]+@)?(?P<hostname>[^:/]+)')

    def __init__(self, repository):
        super(CVSTool, self).__init__(repository)

        self.cvsroot, self.repopath = \
            self.build_cvsroot(self.repository.path,
                               self.repository.username,
                               self.repository.password)

        local_site_name = None

        if repository.local_site:
            local_site_name = repository.local_site.name

        self.client = CVSClient(self.cvsroot, self.repopath, local_site_name)

    def get_file(self, path, revision=HEAD):
        if not path:
            raise FileNotFoundError(path, revision)

        return self.client.cat_file(path, revision)

    def parse_diff_revision(self, file_str, revision_str, *args, **kwargs):
        if revision_str == "PRE-CREATION":
            return file_str, PRE_CREATION

        m = self.rev_re.match(revision_str)
        if not m:
            raise SCMError("Unable to parse diff revision header '%s'" %
                           revision_str)
        return file_str, m.group(1)

    def get_diffs_use_absolute_paths(self):
        return True

    def get_fields(self):
        return ['diff_path']

    def get_parser(self, data):
        return CVSDiffParser(data, self.repopath)

    @classmethod
    def build_cvsroot(cls, path, username, password):
        # NOTE: According to cvs, the following formats are valid.
        #
        #  :(gserver|kserver|pserver):[[user][:password]@]host[:[port]]/path
        #  [:(ext|server):][[user]@]host[:]/path
        #  :local:e:\path
        #  :fork:/path

        if not path.startswith(":"):
            # The user has a path or something. We'll want to parse out the
            # server name, port (if specified) and path and build a :pserver:
            # CVSROOT.
            m = cls.repopath_re.match(path)

            if m:
                path = m.group("path")
                cvsroot = ":pserver:"

                if username:
                    if password:
                        cvsroot += '%s:%s@' % (username,
                                               password)
                    else:
                        cvsroot += '%s@' % (username)

                cvsroot += "%s:%s%s" % (m.group("hostname"),
                                        m.group("port") or "",
                                        path)
                return cvsroot, path

        # We couldn't parse this as a hostname:port/path. Assume it's a local
        # path or a full CVSROOT and let CVS handle it.
        return path, path

    @classmethod
    def check_repository(cls, path, username=None, password=None,
                         local_site_name=None):
        """
        Performs checks on a repository to test its validity.

        This should check if a repository exists and can be connected to.
        This will also check if the repository requires an HTTPS certificate.

        The result is returned as an exception. The exception may contain
        extra information, such as a human-readable description of the problem.
        If the repository is valid and can be connected to, no exception
        will be thrown.
        """
        # CVS paths are a bit strange, so we can't actually use the
        # SSH checking in SCMTool.check_repository. Do our own.
        m = cls.ext_cvsroot_re.match(path)

        if m:
            sshutils.check_host(m.group('hostname'), username, password,
                                local_site_name)

        cvsroot, repopath = cls.build_cvsroot(path, username, password)
        client = CVSClient(cvsroot, repopath, local_site_name)

        try:
            client.cat_file('CVSROOT/modules', HEAD)
        except (SCMError, FileNotFoundError):
            raise RepositoryNotFoundError()

    @classmethod
    def parse_hostname(cls, path):
        """Parses a hostname from a repository path."""
        return urlparse.urlparse(path)[1] # netloc


class CVSDiffParser(DiffParser):
    """This class is able to parse diffs created with CVS. """

    regex_small = re.compile('^RCS file: (.+)$')

    def __init__(self, data, repo):
        DiffParser.__init__(self, data)
        self.regex_full = re.compile('^RCS file: %s/(.*),v$' % re.escape(repo))

    def parse_special_header(self, linenum, info):
        linenum = super(CVSDiffParser, self).parse_special_header(linenum, info)

        if 'index' not in info:
            # We didn't find an index, so the rest is probably bogus too.
            return linenum

        m = self.regex_full.match(self.lines[linenum])
        if not m:
            m = self.regex_small.match(self.lines[linenum])

        if m:
            info['filename'] = m.group(1)
            linenum += 1
        else:
            raise DiffParserError('Unable to find RCS line', linenum)

        while self.lines[linenum].startswith('retrieving '):
            linenum += 1

        if self.lines[linenum].startswith('diff '):
            linenum += 1

        return linenum

    def parse_diff_header(self, linenum, info):
        linenum = super(CVSDiffParser, self).parse_diff_header(linenum, info)

        if info.get('origFile') == '/dev/null':
            info['origFile'] = info['newFile']
            info['origInfo'] = 'PRE-CREATION'
        elif 'filename' in info:
            info['origFile'] = info['filename']

        if info.get('newFile') == '/dev/null':
            info['deleted'] = True

        return linenum


class CVSClient(object):
    def __init__(self, cvsroot, path, local_site_name):
        self.tempdir = ""
        self.currentdir = os.getcwd()
        self.cvsroot = cvsroot
        self.path = path
        self.local_site_name = local_site_name

        if not is_exe_in_path('cvs'):
            # This is technically not the right kind of error, but it's the
            # pattern we use with all the other tools.
            raise ImportError

    def cleanup(self):