# Copyright (C) 2018 Jelmer Vernooij <jelmer@jelmer.uk>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

"""Propose command implementations."""

from io import StringIO

from ... import branch as _mod_branch
from ... import controldir, errors
from ... import forge as _mod_forge
from ... import log as _mod_log
from ... import missing as _mod_missing
from ... import msgeditor, urlutils
from ...commands import Command
from ...i18n import gettext
from ...option import ListOption, Option, RegistryOption
from ...trace import note, warning


def branch_name(branch):
    if branch.name:
        return branch.name
    return urlutils.basename(branch.user_url)


def _check_already_merged(branch, target):
    # TODO(jelmer): Check entire ancestry rather than just last revision?
    if branch.last_revision() == target.last_revision():
        raise errors.CommandError(gettext(
            'All local changes are already present in target.'))


class cmd_publish_derived(Command):
    __doc__ = """Publish a derived branch.

    Try to create a public copy of a local branch on a hosting site,
    derived from the specified base branch.

    Reasonable defaults are picked for owner name, branch name and project
    name, but they can also be overridden from the command-line.
    """

    takes_options = [
        'directory',
        Option('owner', help='Owner of the new remote branch.', type=str),
        Option('project', help='Project name for the new remote branch.',
               type=str),
        Option('name', help='Name of the new remote branch.', type=str),
        Option('no-allow-lossy',
               help='Allow fallback to lossy push, if necessary.'),
        Option('overwrite', help="Overwrite existing commits."),
        'revision',
        ]
    takes_args = ['submit_branch?']

    def run(self, submit_branch=None, owner=None, name=None, project=None,
            no_allow_lossy=False, overwrite=False, directory='.',
            revision=None):
        local_branch = _mod_branch.Branch.open_containing(directory)[0]
        self.add_cleanup(local_branch.lock_write().unlock)
        if submit_branch is None:
            submit_branch = local_branch.get_submit_branch()
            note(gettext('Using submit branch %s') % submit_branch)
        if submit_branch is None:
            submit_branch = local_branch.get_parent()
            note(gettext('Using parent branch %s') % submit_branch)
        submit_branch = _mod_branch.Branch.open(submit_branch)
        _check_already_merged(local_branch, submit_branch)
        if name is None:
            name = branch_name(local_branch)
        forge = _mod_forge.get_forge(submit_branch)
        if revision is None:
            stop_revision = None
        else:
            stop_revision = revision.as_revision_id(branch)
        remote_branch, public_url = forge.publish_derived(
            local_branch, submit_branch, name=name, project=project,
            owner=owner, allow_lossy=not no_allow_lossy,
            overwrite=overwrite, revision_id=stop_revision)
        local_branch.set_push_location(remote_branch.user_url)
        local_branch.set_public_branch(public_url)
        local_branch.set_submit_branch(submit_branch.user_url)
        note(gettext("Pushed to %s") % public_url)


def summarize_unmerged(local_branch, remote_branch, target,
                       prerequisite_branch=None):
    """Generate a text description of the unmerged revisions in branch.

    :param branch: The proposed branch
    :param target: Target branch
    :param prerequisite_branch: Optional prerequisite branch
    :return: A string
    """
    log_format = _mod_log.log_formatter_registry.get_default(local_branch)
    to_file = StringIO()
    lf = log_format(to_file=to_file, show_ids=False, show_timezone='original')
    if prerequisite_branch:
        local_extra = _mod_missing.find_unmerged(
            remote_branch, prerequisite_branch, restrict='local')[0]
    else:
        local_extra = _mod_missing.find_unmerged(
            remote_branch, target, restrict='local')[0]

    if remote_branch.supports_tags():
        rev_tag_dict = remote_branch.tags.get_reverse_tag_dict()
    else:
        rev_tag_dict = {}

    for revision in _mod_missing.iter_log_revisions(
            local_extra, local_branch.repository, False, rev_tag_dict):
        lf.log_revision(revision)
    return to_file.getvalue()


class cmd_propose_merge(Command):
    __doc__ = """Propose a branch for merging.

    This command creates a merge proposal for the local
    branch to the target branch. The format of the merge
    proposal depends on the submit branch.
    """

    takes_options = [
        'directory',
        RegistryOption(
            'forge',
            help='Use the forge.',
            lazy_registry=('breezy.forge', 'forges')),
        ListOption('reviewers', short_name='R', type=str,
                   help='Requested reviewers.'),
        Option('name', help='Name of the new remote branch.', type=str),
        Option('description', help='Description of the change.', type=str),
        Option('prerequisite', help='Prerequisite branch.', type=str),
        Option('wip', help='Mark merge request as work-in-progress'),
        Option('auto', help='Automatically merge when the CI passes'),
        Option(
            'commit-message',
            help='Set commit message for merge, if supported', type=str),
        ListOption('labels', short_name='l', type=str,
                   help='Labels to apply.'),
        Option('no-allow-lossy',
               help='Allow fallback to lossy push, if necessary.'),
        Option('allow-collaboration',
               help='Allow collaboration from target branch maintainer(s)'),
        Option('allow-empty',
               help='Do not prevent empty merge proposals.'),
        Option('overwrite', help="Overwrite existing commits."),
        Option('open', help='Open merge proposal in web browser'),
        Option('delete-source-after-merge', help='Delete source branch when proposal is merged'),
        'revision',
        ]
    takes_args = ['submit_branch?']

    aliases = ['propose']

    def run(self, submit_branch=None, directory='.', forge=None,
            reviewers=None, name=None, no_allow_lossy=False, description=None,
            labels=None, prerequisite=None, commit_message=None, wip=False,
            allow_collaboration=False, allow_empty=False, overwrite=False,
            open=False, auto=False, delete_source_after_merge=None, revision=None):
        tree, branch, relpath = (
            controldir.ControlDir.open_containing_tree_or_branch(directory))
        if submit_branch is None:
            submit_branch = branch.get_submit_branch()
        if submit_branch is None:
            submit_branch = branch.get_parent()
        if submit_branch is None:
            raise errors.CommandError(
                gettext("No target location specified or remembered"))
        target = _mod_branch.Branch.open(submit_branch)
        if not allow_empty:
            _check_already_merged(branch, target)
        if forge is None:
            forge = _mod_forge.get_forge(target)
        else:
            forge = forge.probe(target)
        if name is None:
            name = branch_name(branch)
        if revision is None:
            stop_revision = None
        else:
            stop_revision = revision.as_revision_id(branch)
        remote_branch, public_branch_url = forge.publish_derived(
            branch, target, name=name, allow_lossy=not no_allow_lossy,
            overwrite=overwrite, revision_id=stop_revision)
        branch.set_push_location(remote_branch.user_url)
        branch.set_submit_branch(target.user_url)
        note(gettext('Published branch to %s'),
             forge.get_web_url(remote_branch) or public_branch_url)
        if prerequisite is not None:
            prerequisite_branch = _mod_branch.Branch.open(prerequisite)
        else:
            prerequisite_branch = None
        proposal_builder = forge.get_proposer(remote_branch, target)
        if description is None:
            body = proposal_builder.get_initial_body()
            info = proposal_builder.get_infotext()
            info += "\n\n" + summarize_unmerged(
                branch, remote_branch, target, prerequisite_branch)
            description = msgeditor.edit_commit_message(
                info, start_message=body)
        try:
            proposal = proposal_builder.create_proposal(
                description=description, reviewers=reviewers,
                prerequisite_branch=prerequisite_branch, labels=labels,
                commit_message=commit_message,
                work_in_progress=wip, allow_collaboration=allow_collaboration,
                delete_source_after_merge=delete_source_after_merge)
        except _mod_forge.MergeProposalExists as e:
            note(gettext('There is already a branch merge proposal: %s'), e.url)
        else:
            note(gettext('Merge proposal created: %s') % proposal.url)
            if open:
                web_url = proposal.get_web_url()
                import webbrowser
                note(gettext('Opening %s in web browser'), web_url)
                webbrowser.open(web_url)
            if auto:
                proposal.merge(auto=True)


class cmd_find_merge_proposal(Command):
    __doc__ = """Find a merge proposal.

    """

    takes_options = ['directory']
    takes_args = ['submit_branch?']
    aliases = ['find-proposal']

    def run(self, directory='.', submit_branch=None):
        tree, branch, relpath = controldir.ControlDir.open_containing_tree_or_branch(
            directory)
        public_location = branch.get_public_branch()
        if public_location:
            branch = _mod_branch.Branch.open(public_location)
        if submit_branch is None:
            submit_branch = branch.get_submit_branch()
        if submit_branch is None:
            submit_branch = branch.get_parent()
        if submit_branch is None:
            raise errors.CommandError(
                gettext("No target location specified or remembered"))
        else:
            target = _mod_branch.Branch.open(submit_branch)
        forge = _mod_forge.get_forge(branch)
        for mp in forge.iter_proposals(branch, target):
            self.outf.write(gettext('Merge proposal: %s\n') % mp.url)


class cmd_my_merge_proposals(Command):
    __doc__ = """List all merge proposals owned by the logged-in user.

    """

    hidden = True

    takes_args = ['base_url?']
    takes_options = [
        'verbose',
        RegistryOption.from_kwargs(
            'status',
            title='Proposal Status',
            help='Only include proposals with specified status.',
            value_switches=True,
            enum_switch=True,
            all='All merge proposals',
            open='Open merge proposals',
            merged='Merged merge proposals',
            closed='Closed merge proposals'),
        RegistryOption(
            'forge',
            help='Use the forge.',
            lazy_registry=('breezy.forge', 'forges')),
        ]

    def run(self, status='open', verbose=False, forge=None, base_url=None):

        for instance in _mod_forge.iter_forge_instances(forge=forge):
            if base_url is not None and instance.base_url != base_url:
                continue
            try:
                for mp in instance.iter_my_proposals(status=status):
                    self.outf.write('%s\n' % mp.url)
                    if verbose:
                        source_branch_url = mp.get_source_branch_url()
                        if source_branch_url:
                            self.outf.write(
                                '(Merging %s into %s)\n' %
                                (source_branch_url,
                                 mp.get_target_branch_url()))
                        else:
                            self.outf.write(
                                '(Merging into %s)\n' %
                                mp.get_target_branch_url())
                        description = mp.get_description()
                        if description:
                            self.outf.writelines(
                                ['\t%s\n' % l
                                 for l in description.splitlines()])
                        self.outf.write('\n')
            except _mod_forge.ForgeLoginRequired as e:
                warning('Skipping %s, login required.', instance)


class cmd_land_merge_proposal(Command):
    __doc__ = """Land a merge proposal."""

    takes_args = ['url']
    takes_options = [
        Option('message', help='Commit message to use.', type=str)]

    def run(self, url, message=None):
        proposal = _mod_forge.get_proposal_by_url(url)
        proposal.merge(commit_message=message)


class cmd_web_open(Command):
    __doc__ = """Open a branch page in your web browser."""

    takes_options = [
        Option('dry-run',
               'Do not actually open the browser. Just say the URL we would '
               'use.'),
        ]
    takes_args = ['location?']

    def _possible_locations(self, location):
        """Yield possible external locations for the branch at 'location'."""
        yield location
        try:
            branch = _mod_branch.Branch.open_containing(location)[0]
        except errors.NotBranchError:
            return
        branch_url = branch.get_public_branch()
        if branch_url is not None:
            yield branch_url
        branch_url = branch.get_push_location()
        if branch_url is not None:
            yield branch_url

    def _get_web_url(self, location):
        for branch_url in self._possible_locations(location):
            try:
                branch = _mod_branch.Branch.open_containing(branch_url)[0]
            except errors.NotBranchError as e:
                mutter('Unable to open branch %s: %s',
                       branch_url, e)
                continue

            try:
                forge = _mod_forge.get_forge(branch)
            except _mod_forge.UnsupportedForge:
                continue

            return forge.get_web_url(branch)
        raise errors.CommandError(
            'Unable to get web URL for %s' % location)

    def run(self, location=None, dry_run=False):
        if location is None:
            location = '.'
        web_url = self._get_web_url(location)
        note(gettext('Opening %s in web browser') % web_url)
        if not dry_run:
            import webbrowser

            # otherwise brz.exe lacks this module
            webbrowser.open(web_url)


class cmd_forges(Command):
    __doc__ = """List all known hosting sites and user details."""

    hidden = True

    def run(self):
        for instance in _mod_forge.iter_forge_instances():
            current_user = instance.get_current_user()
            if current_user is not None:
                current_user_url = instance.get_user_url(current_user)
                if current_user_url is not None:
                    self.outf.write(
                        gettext('%s (%s) - user: %s (%s)\n') % (
                            instance.name, instance.base_url,
                            current_user, current_user_url))
                else:
                    self.outf.write(
                        gettext('%s (%s) - user: %s\n') % (
                            instance.name, instance.base_url,
                            current_user))
            else:
                self.outf.write(
                    gettext('%s (%s) - not logged in\n') % (
                        instance.name, instance.base_url))
