from typing import List, Optional
from urllib.parse import urlparse

from zeus.exceptions import CommandError, HostError, InvalidPublicKey, UnknownRevision
from zeus.utils.functional import memoize
from zeus.utils import sentry, timezone

from .base import Vcs, RevisionResult, BufferParser

LOG_FORMAT = "%H\x01%an <%ae>\x01%at\x01%cn <%ce>\x01%ct\x01%P\x01%B\x02"

ORIGIN_PREFIX = "remotes/origin/"


class GitVcs(Vcs):
    binary_path = "git"

    def get_default_env(self) -> dict:
        return {"GIT_SSH": self.ssh_connect_path}

    def get_default_branch(self) -> str:
        return "master"

    def get_default_revision(self) -> str:
        return "master"

    @memoize
    def remote_url(self) -> str:
        username: Optional[str] = self.username
        if self.url.startswith("ssh:") and not self.username:
            username = "git"
        if username and self.url.startswith(("ssh:", "http:", "https:")):
            parsed = urlparse(self.url)
            url = "%s://%s@%s/%s" % (
                parsed.scheme,
                parsed.username or username,
                "{}{}".format(
                    parsed.hostname, ":%s" % (parsed.port,) if parsed.port else ""
                ),
                parsed.path.lstrip("/"),
            )
        else:
            url = self.url
        return url

    async def get_known_branches(self, commit_id=None) -> list:
        """ List all branches or those related to the commit for this repo.

        Either gets all the branches (if the commit_id is not specified) or then
        the branches related to the given commit reference.

        :param commit_id: A commit ID for fetching all related branches. If not
            specified, returns all branch names for this repository.
        :return: List of branches for the commit, or all branches for the repo.
        """
        results = []
        command_parameters = ["branch", "-a"]
        if commit_id:
            command_parameters.extend(["--contains", commit_id])
        output = await self.run(command_parameters)

        for result in output.splitlines():
            # HACK(dcramer): is there a better way around removing the prefix?
            result = result[2:].strip()
            if result.startswith(ORIGIN_PREFIX):
                result = result[len(ORIGIN_PREFIX) :]
            if result == "HEAD":
                continue

            results.append(result)
        return list(set(results))

    async def run(self, cmd, **kwargs) -> str:
        with sentry.Span("vcs.run-command", description=self.remote_url) as span:
            span.set_data("command", " ".join(cmd))
            span.set_data("backend", "git")

            cmd = [self.binary_path] + cmd
            try:
                output = await super().run(cmd, **kwargs)
                span.set_data("output_bytes", len(output))
                return output

            except CommandError as e:
                stderr = e.stderr.decode("utf-8") if e.stderr else ""
                if "unknown revision or path" in stderr:
                    # fatal: ambiguous argument '82f750e7a3b692e049b95ed66bf9149f56218733^..82f750e7a3b692e049b95ed66bf9149f56218733': unknown revision or path not in the working tree.\nUse '--' to separate paths from revisions, like this:\n'git <command> [<revision>...] -- [<file>...]'\n
                    raise UnknownRevision(
                        ref=stderr.split("fatal: ambiguous argument ")[-1].split(
                            "^", 1
                        )[0],
                        cmd=e.cmd,
                        retcode=e.retcode,
                        stdout=e.stdout,
                        stderr=e.stderr,
                    ) from e
                if "fatal: bad object" in stderr:
                    # bad object 5d953e751835a52472ca2e1906023435a71cb5e4\n
                    raise UnknownRevision(
                        ref=stderr.split("\n")[0].split("bad object ", 1)[-1],
                        cmd=e.cmd,
                        retcode=e.retcode,
                        stdout=e.stdout,
                        stderr=e.stderr,