import os
import re
import subprocess
import sys
import urlparse

from wptrunner.update.sync import LoadManifest
from wptrunner.update.tree import get_unique_name
from wptrunner.update.base import Step, StepRunner, exit_clean, exit_unclean

from .tree import Commit, GitTree, Patch
import github
from .github import GitHub


def rewrite_patch(patch, strip_dir):
    """Take a Patch and convert to a different repository by stripping a prefix from the
    file paths. Also rewrite the message to remove the bug number and reviewer, but add
    a bugzilla link in the summary.

    :param patch: the Patch to convert
    :param strip_dir: the path prefix to remove
    """

    if not strip_dir.startswith("/"):
        strip_dir = "/%s"% strip_dir

    new_diff = []
    line_starts = ["diff ", "+++ ", "--- "]
    for line in patch.diff.split("\n"):
        for start in line_starts:
            if line.startswith(start):
                new_diff.append(line.replace(strip_dir, "").encode("utf8"))
                break
        else:
            new_diff.append(line)

    new_diff = "\n".join(new_diff)

    assert new_diff != patch

    return Patch(patch.author, patch.email, rewrite_message(patch), new_diff)

def rewrite_message(patch):
    rest = patch.message.body

    if patch.message.bug is not None:
        return "\n".join([patch.message.summary,
                          patch.message.body,
                          "",
                          "Upstreamed from https://bugzilla.mozilla.org/show_bug.cgi?id=%s" %
                          patch.message.bug])

    return "\n".join([patch.message.full_summary, rest])


class SyncToUpstream(Step):
    """Sync local changes to upstream"""

    def create(self, state):
        if not state.kwargs["upstream"]:
            return

        if not isinstance(state.local_tree, GitTree):
            self.logger.error("Cannot sync with upstream from a non-Git checkout.")
            return exit_clean

        try:
            import requests
        except ImportError:
            self.logger.error("Upstream sync requires the requests module to be installed")
            return exit_clean

        if not state.sync_tree:
            os.makedirs(state.sync["path"])
            state.sync_tree = GitTree(root=state.sync["path"])

        kwargs = state.kwargs
        with state.push(["local_tree", "sync_tree", "tests_path", "metadata_path",
                         "sync"]):
            state.token = kwargs["token"]
            runner = SyncToUpstreamRunner(self.logger, state)
            runner.run()


class CheckoutBranch(Step):
    """Create a branch in the sync tree pointing at the last upstream sync commit
    and check it out"""

    provides = ["branch"]

    def create(self, state):
        self.logger.info("Updating sync tree from %s" % state.sync["remote_url"])
        state.branch = state.sync_tree.unique_branch_name(
            "outbound_update_%s" % state.test_manifest.rev)
        state.sync_tree.update(state.sync["remote_url"],
                               state.sync["branch"],
                               state.branch)
        state.sync_tree.checkout(state.test_manifest.rev, state.branch, force=True)


class GetLastSyncCommit(Step):
    """Find the gecko commit at which we last performed a sync with upstream."""

    provides = ["last_sync_path", "last_sync_commit"]

    def create(self, state):
        self.logger.info("Looking for last sync commit")
        state.last_sync_path = os.path.join(state.metadata_path, "mozilla-sync")
        with open(state.last_sync_path) as f:
            last_sync_sha1 = f.read().strip()

        state.last_sync_commit = Commit(state.local_tree, last_sync_sha1)

        if not state.local_tree.contains_commit(state.last_sync_commit):
            self.logger.error("Could not find last sync commit %s" % last_sync_sha1)
            return exit_clean

        self.logger.info("Last sync to web-platform-tests happened in %s" % state.last_sync_commit.sha1)


class GetBaseCommit(Step):
    """Find the latest upstream commit on the branch that we are syncing with"""

    provides = ["base_commit"]

    def create(self, state):
        state.base_commit = state.sync_tree.get_remote_sha1(state.sync["remote_url"],
                                                            state.sync["branch"])
        self.logger.debug("New base commit is %s" % state.base_commit.sha1)


class LoadCommits(Step):
    """Get a list of commits in the gecko tree that need to be upstreamed"""

    provides = ["source_commits"]

    def create(self, state):
        state.source_commits = state.local_tree.log(state.last_sync_commit,
                                                    state.tests_path)

        update_regexp = re.compile("Bug \d+ - Update web-platform-tests to revision [0-9a-f]{40}")

        for i, commit in enumerate(state.source_commits[:]):
            if update_regexp.match(commit.message.text):
                # This is a previous update commit so ignore it
                state.sourc
e_commits.remove(commit)
                continue

            if commit.message.backouts:
                #TODO: Add support for collapsing backouts

                raise NotImplementedError("Need to get the Git->Hg commits for backouts and remove the backed out patch")

            if not commit.message.bug:
                self.logger.error("Commit %i (%s) doesn't have an associated bug number." %
                             (i + 1, commit.sha1))
                return exit_unclean

        self.logger.debug("Source commits: %s" % state.source_commits)

class SelectCommits(Step):
    """Provide a UI to select which commits to upstream"""

    def create(self, state):
        if not state.source_commits:
            return

        while True:
            commits = state.source_commits[:]
            for i, commit in enumerate(commits):
                print "%i:\t%s" % (i, commit.message.summary)

            remove = raw_input("Provide a space-separated list of any commits numbers to remove from the list to upstream:\n").strip()
            remove_idx = set()
            invalid = False
            for item in remove.split(" "):
                try:
                    item = int(item)
                except:
                    invalid = True
                    break
                if item < 0 or item >= len(commits):
                    invalid = True
                    break
                remove_idx.add(item)

            if invalid:
                continue

            keep_commits = [(i,cmt) for i,cmt in enumerate(commits) if i not in remove_idx]
            #TODO: consider printed removed commits
            print "Selected the following commits to keep:"
            for i, commit in keep_commits:
                print "%i:\t%s" % (i, commit.message.summary)
            confirm = raw_input("Keep the above commits? y/n\n").strip().lower()

            if confirm == "y":
                state.source_commits = [item[1] for item in keep_commits]
                break

class MovePatches(Step):
    """Convert gecko commits into patches against upstream and commit these to the sync tree."""

    provides = ["commits_loaded"]

    def create(self, state):
        state.commits_loaded = 0

        strip_path = os.path.relpath(state.tests_path,
                                     state.local_tree.root)
        self.logger.debug("Stripping patch %s" % strip_path)

        for commit in state.source_commits[state.commits_loaded:]:
            i = state.commits_loaded + 1
            self.logger.info("Moving commit %i: %s" % (i, commit.message.full_summary))
            patch = commit.export_patch(state.tests_path)
            stripped_patch = rewrite_patch(patch, strip_path)
            try:
                state.sync_tree.import_patch(stripped_patch)
            except:
                print patch.diff
                raise
            state.commits_loaded = i

class RebaseCommits(Step):
    """Rebase commits from the current branch on top of the upstream destination branch.

    This step is particularly likely to fail if the rebase generates merge conflicts.
    In t