from __future__ import absolute_import, division, print_function, unicode_literals
from builtins import input

import argparse
import sys

from publisher.git_handling.setup import setup_users_machine
from publisher.utils import (
    get_procedure_code,
    tspublisher_up_to_date,
    get_latest_tspub_version,
    get_current_tspub_version,
)
from publisher.translations.mo import build_mo_files
from publisher.translations.pot import ready_for_translations
from publisher.translations.po import merge_procedure_translation_files
from publisher import procedure_content
from publisher.exceptions import ValidationError
from publisher.processing import procedure
from publisher.processing.data_sources.ftrack import (
    FtrackAccountError,
    get_release_distribution_group,
    get_review_distribution_group,
)
from publisher.settings import CHANNEL_TYPES
from publisher.exceptions import ChannelExistsError, ChannelMissingError
from publisher.utils import get_yes_no


class PublisherCLI():

    def __init__(self, args):
        # Check version is up to date
        if not tspublisher_up_to_date():
            print("######################################################")
            print(
                "Your version of the tspub is out of date.\n"
                "You are using version {}\n"
                "The latest version is {}".format(
                    get_current_tspub_version(), get_latest_tspub_version()
                )
            )
            print("######################################################")

            if not get_yes_no(
                "Do you wish to continue with this outdated version? [y/N]: "
            ):
                sys.exit()

        parser = argparse.ArgumentParser(
            description='Touch Surgery Publisher',
            usage='tspub <command> [<args>]'
        )
        subparsers = parser.add_subparsers(
            title='Available commands',
            description=(
                'These commands allow you to create and '
                'publish TS simulations'
            ),
            help='subparsers help'
        )
        cli_commands = [c for c in dir(self) if not c.startswith('_')]

        for command in cli_commands:
            sub_parser = subparsers.add_parser(
                command,
                help=getattr(self, command).__doc__
            )
            sub_parser.set_defaults(func=getattr(self, command))

        namespace = parser.parse_args(args[0:1])

        try:
            namespace.func(args[1:])
        except ValidationError as e:
            print('The following issues were encountered.')
            print(e.message)

    def setup(self, args):
        """ Setup your machine, ready to create and edit simulations
        """
        setup_users_machine()

    def procedures(self, args):
        """ List all procedures in the git repository, each being a separate branch
        """
        procedure_content.display_procedures()

    def phases(self, args):
        """ List all phases of the procedure and their commit histories.
        """
        phase_code = procedure_content.get_user_selected_phase()
        procedure_content.display_phase_history(phase_code)

    def reset(self, args):
        """ Reset all local changes to match the most recent commit
        """
        procedure_content.cl_reset_repo()

    def revert_phase(self, args):
        """ Revert to a previous commit state for a chosen commit and phase.
        """
        phase_code = procedure_content.get_user_selected_phase()
        procedure_content.display_phase_history_and_revert(phase_code)

    def workon(self, args):
        """ Move to the branch containing the specified procedure so that it can be worked on
        """
        parser = argparse.ArgumentParser(
            description='Move to the branch containing the specified procedure',
            usage='''tspub workon <procedure_code>'''
        )
        parser.add_argument(
            'procedure',
            help='The procedure code'
        )

        self._clean_working_directory_state()

        args = parser.parse_args(args)
        procedure_content.change_procedure(args.procedure)

    def save(self, args):
        """ Commit and push current changes to the repo
        """
        parser = argparse.ArgumentParser(
            description='Commit and push current changes to the repo',
            usage='''tspub save <commit_message>'''
        )
        parser.add_argument(
            'commit_message',
            help='Describe what changes have been made'
        )
        args = parser.parse_args(args)
        procedure_content.save_working_changes(args.commit_message)

    def create(self, args):
        """Create a new branch with the name of the procedure_code param
        """
        parser = argparse.ArgumentParser(
            description='Create new procedure with the specified procedure_code param',
            usage='''tspub create <procedure_code>'''
        )
        parser.add_argument('procedure_code', help='code of the procedure to be created')
        args = parser.parse_args(args)

        if args.procedure_code not in procedure_content.build_procedure_list():
            self._clean_working_directory_state()
            procedure_content.create_procedure_branch(args.procedure_code)
        else:
            raise Exception("The procedure {0} already exists and thus cannot be created".format(args.procedure_code))

        procedure.create_procedure(args.procedure_code)

        procedure_content.save_working_changes("Initial commit", initial=True, procedure_code=args.procedure_code)

    def update_procedure(self, args):
        """Update current working procedure. Update phases with build_phases param.
        """
        parser = argparse.ArgumentParser(
            description='Update current procedure.',
            usage='''tspub update_procedure'''
        )
        parser.add_argument('--build_phases', action='store_true', help='Build all phases as well as procedure')
        parser.add_argument('--graphics', action='store_true', help='Copy latest images (normal)')
        parser.add_argument('--pip_graphics', action='store_true', help='Copy latest pip images (normal)')

        args = self._get_build_args(parser, args)

        procedure_code = get_procedure_code()

        graphics = args.graphics
        if args.graphics:
            graphics = "latest"

        pip_graphics = args.pip_graphics
        if args.pip_graphics:
            pip_graphics = {}

        try:
            procedure.build_procedure(procedure_code,
                                      build_phases=args.build_phases,
                                      graphics=graphics, pip_graphics=pip_graphics,
                                      widget_graphics=args.widget_graphics,
                                      thumbnails=args.thumbnails,
                                      step_numbers=args.step_numbers,
                                      info_step=args.info_step)
        except FtrackAccountError:
            raise RuntimeError(
                "update_procedure requires access to Ftrack "
                "and we were unable to find any ftrack account details"
            )

    def update_phase(self, args):
        """Update the specified phase with the phase_code param.
        """
        parser = argparse.ArgumentParser(
            description='Update specified phase with the phase_code param.',
            usage='''tspub update_phase <phase_code>'''
        )
        parser.add_argument('phase_code', help='code of the procedure to be created')
        parser.add_argument('--graphics', help='Copy latest images, can be supplied with version number',
                            const="latest", nargs="?")
        parser.add_argument('--pip_graphics', help='Copy latest pip images, can be supplied with version number e.g.: '
                                                   '--pip_graphics="pipName 2,otherPipName 1"', const={},
                            nargs="?")
        parser.add_argument('--csv_version', help='Define csv version to build with', nargs="+")

        args = self._get_build_args(parser, args)

        pip_graphics = self.format_pip_graphics(args.pip_graphics)

        if args.csv_version:
            csv_version = args.csv_version[0]
        else:
            csv_version = None

        try:
            procedure.build_single_phase(
                args.phase_code,
                graphics=args.graphics,
                pip_graphics=pip_graphics,
                widget_graphics=args.widget_graphics,
                thumbnails=args.thumbnails,
                step_numbers=args.step_numbers,
                info_step=args.info_step,
                csv_version=csv_version,
            )
        except FtrackAccountError:
            raise RuntimeError(
                "update_phase requires access to Ftrack "
                "and we were unable to find any ftrack account details"
            )

    def publish(self, args):
        """Publish procedure with the specified distribution group. Defaults to TS-Testing.
        """
        parser = argparse.ArgumentParser(
            description='Publish procedure with the specified distribution group. Defaults to TS-Testing.',
            usage='''tspub publish'''
        )
        group = parser.add_mutually_exclusive_group()
        group.add_argument(
            '--distgroup',
            help='Distribution group to be published to e.g. --distgroup=distGroup1,distGroup2',
            required=False
        )
        group.add_argument(
            "--review",
            action="store_true",
            help="Publish to review distribution group(s) specified in ftrack",
        )
        group.add_argument(
            "--release",
            action="store_true",
            help="Publish to release distribution group(s) specified in ftrack",
        )
        parser.add_argument(
            '--number',
            help='The number of previous commits to be shown',
            required=False
        )
        args = parser.parse_args(args)

        if args.review:
            try:
                distgroup = get_review_distribution_group(get_procedure_code())
            except FtrackAccountError:
                raise RuntimeError(
                    "Unable to use --review shortcut "
                    "as you are not setup to use Ftrack."
                    "\nPlease use --distgroup= method"
                )
            else:
                if not distgroup:
                    raise ValueError("No review groups specified in Ftrack")
        elif args.release:
            try:
                distgroup = get_release_distribution_group(get_procedure_code())
            except FtrackAccountError:
                raise RuntimeError(
                    "Unable to use --release shortcut "
                    "as you are not setup to use Ftrack.\n"
                    "Please use --distgroup= method"
                )
            else:
                if not distgroup:
                    raise ValueError("No release groups specified in Ftrack")
        else:
            distgroup = args.distgroup

        self._validate_distgroups(distgroup)

        procedure_content.publish_with_display(distgroup, args.number)

    def translate(self, args):
        """Available for testing only, builds a messages.pot in the working directory for the active procedure."""
        if ready_for_translations():
            merge_procedure_translation_files()
            build_mo_files()

    def generate_review_doc(self, args):
        if procedure_content.has_unstaged_changes():
            raise Exception(
                "You have unsaved local changes, "
                "please save or clear these before generating review document"
            )
        parser = argparse.ArgumentParser()
        parser.add_argument(
            "--output_dir",
            help="Directory to output generated report to",
            required=False,
        )
        parser.add_argument(
            "--number",
            type=int,
            help="The number of previous commits to be shown",
            required=False,
        )
        args = parser.parse_args(args)
        procedure_content.generate_review_document_with_display(args.output_dir, args.number)

    def create_channel(self, args):
        parser = argparse.ArgumentParser()
        parser.add_argument('code')
        parser.add_argument('name')
        parser.add_argument('type', choices=CHANNEL_TYPES)
        group = parser.add_mutually_exclusive_group(required=True)
        group.add_argument('--eula_required', dest='eula', action='store_true')
        group.add_argument('--eula_not_required', dest='eula', action='store_false')

        args = parser.parse_args(args)
        if args.code in procedure_content.build_channel_list():
            raise ChannelExistsError("A channel with code {} already exists.")
        procedure.build_channel(vars(args))

    def update_channel(self, args):
        parser = argparse.ArgumentParser()
        parser.add_argument('code')
        parser.add_argument('--name')
        parser.add_argument('--type', choices=CHANNEL_TYPES)
        group = parser.add_mutually_exclusive_group(required=True)
        group.add_argument('--eula_required', dest='eula', action='store_true')
        group.add_argument('--eula_not_required', dest='eula', action='store_false')
        args = parser.parse_args(args)
        if args.code not in procedure_content.build_channel_list():
            raise ChannelMissingError("A channel with code {} does not exist.")

        updates = {}
        if args.name:
            updates['name'] = args.name
        if args.type:
            updates['type'] = args.type
        updates['eula'] = args.eula
        procedure.update_channel(args.code, updates)

    def save_channels(self, args):
        """ Commit and push current channel changes to the repo
        """
        parser = argparse.ArgumentParser(
            description='Commit and push current channel changes to the repo',
            usage='''tspub save_channels <commit_message>'''
        )
        parser.add_argument(
            'commit_message',
            help='Describe what changes have been made'
        )
        args = parser.parse_args(args)
        procedure_content.save_channel_changes(args.commit_message)

    @staticmethod
    def _get_build_args(parser, args):
        parser.add_argument('--widget_graphics', action='store_true', help='Copy latest widget images (normal)')
        parser.add_argument('--thumbnails', action='store_true', help="Pull latest thumbnails (both)")
        parser.add_argument('--step_numbers', action='store_true', help='Prepend step number in step content')
        parser.add_argument('--info_step', action='store_true', help='Add build information')

        args = parser.parse_args(args)
        return args

    @staticmethod
    def _clean_working_directory_state():
        if procedure_content.has_unstaged_changes():
            user_choice = PublisherCLI._get_user_decision()

            if user_choice == 'y':
                commit_message = PublisherCLI._get_commit_message()
                procedure_content.save_working_changes(commit_message)
            else:
                procedure_content.delete_unstaged_changes()

    @staticmethod
    def _get_commit_message():
        """ Return the commit message from the user
        """
        return input("Please enter a commit message")

    @staticmethod
    def _get_user_decision():
        """ Return the users y or n input
        """
        current_procedure = get_procedure_code()
        user_input = ''
        while user_input not in ['y', 'n']:
            print(
                "You have unsaved changes to {}. Would you save them now? "
                "If you do not save them they will be deleted".format(current_procedure)
            )
            print("> y or n:")
            sys.stdout.flush()
            user_input = input('')
        return user_input.lower()

    @staticmethod
    def _validate_distgroups(distgroups):
        if distgroups is None:
            return True

        groups = distgroups.split(",")

        if "Public" in groups and len(groups) != 1:
            raise ValueError(
                "Can not publish to Public and another distribution group "
                "at the same time"
            )
        elif "TS-Testing" in groups and len(groups) != 1:
            raise ValueError(
                "Can not publish to TS-Testing and another distribution group "
                "at the same time"
            )
        return True

    @staticmethod
    def format_pip_graphics(pip_versions):
        if pip_versions:
            pip_graphics = {}
            pips = pip_versions.split(",")
            for pip in pips:
                version_list = pip.split(" ")
                pip_graphics[version_list[0]] = version_list[1]
            return pip_graphics

        return pip_versions


def main():
    PublisherCLI(sys.argv[1:])
