import copy
import re

from buildbot.plugins import steps, util
from buildbot.process import buildstep
from buildbot.status.results import SUCCESS
from twisted.internet import defer
import yaml

import environments as envs


SERVO_REPO = "https://github.com/servo/servo"


class ServoFactory(util.BuildFactory):
    """\
    Build factory which checks out the servo repo as the first build step.
    """

    def __init__(self, build_steps):
        """\
        Takes a list of Buildbot steps.
        Prefer using DynamicServoFactory to using this class directly.
        """
        all_steps = [
            steps.Git(repourl=SERVO_REPO,
                      mode="full", method="fresh", retryFetch=True),
        ] + build_steps
        # util.BuildFactory is an old-style class so we cannot use super()
        # but must hardcode the superclass here
        util.BuildFactory.__init__(self, all_steps)


class StepsYAMLParsingStep(buildstep.ShellMixin, buildstep.BuildStep):
    """\
    Step which reads the YAML steps configuration in the main servo repo
    and dynamically adds test steps.
    """

    haltOnFailure = True
    flunkOnFailure = True
    workdir = None

    def __init__(self, builder_name, environment, yaml_path, **kwargs):
        kwargs = self.setupShellMixin(kwargs)
        buildstep.BuildStep.__init__(self, **kwargs)
        self.builder_name = builder_name
        self.environment = environment
        self.yaml_path = yaml_path

    def setDefaultWorkdir(self, workdir):
        buildstep.BuildStep.setDefaultWorkdir(self, workdir)
        self.workdir = workdir

    @defer.inlineCallbacks
    def run(self):
        self.is_windows = re.match('windows.*', self.builder_name) is not None
        try:
            cmd = yield self.makeRemoteShellCommand(
                command=["cat", "./{}".format(self.yaml_path)],
                collectStdout=True
            )
            yield self.runCommand(cmd)

            result = cmd.results()
            if result != util.SUCCESS:
                raise Exception("Command failed with return code: {}" .format(
                    str(cmd.rc)
                ))
            else:
                builder_steps = yaml.safe_load(cmd.stdout)
                commands = builder_steps[self.builder_name]
                dynamic_steps = [self.make_step(command)
                                 for command in commands]
        except Exception as e:  # Bad step configuration, fail build
            # Capture the exception and re-raise with a friendly message
            raise Exception("Bad step configuration for {}: {}".format(
                self.builder_name,
                str(e)
            ))

        pkill_step = [self.make_pkill_step("servo")]

        for step in pkill_step + dynamic_steps:
            self.add_step(step)

        defer.returnValue(result)

    def add_step(self, step):
        """\
        Adds a new step to this build, making sure to maintain internal
        Buildbot invariants.
        Semi-polyfill for addStepsAfterLastStep from Buildbot 9.
        """
        step.setBuild(self.build)
        step.setBuildSlave(self.build.slavebuilder.slave)
        step.setDefaultWorkdir(self.workdir)
        self.build.steps.append(step)

        step_status = self.build.build_status.addStepWithName(step.name)
        step.setStepStatus(step_status)

    def make_step(self, command):
        step_kwargs = {}
        step_env = copy.deepcopy(self.environment)

        command = command.split(' ')

        # Add `bash -l` before every command on Windows builders
        bash_args = ["bash", "-l"] if self.is_windows else []
        step_kwargs['command'] = bash_args + command
        if self.is_windows:
            step_env += envs.Environment({
                # Set home directory, to avoid adding `cd` command every time
                'HOME': r'C:\buildbot\slave\{}\build'.format(
                    self.builder_name
                ),
            })

        step_desc = []
        step_class = steps.ShellCommand
        args = iter(command)
        for arg in args:
            # Change Step class to capture warnings as needed
            # (steps.Compile and steps.Test catch warnings)
            if arg == './mach':
                mach_arg = next(args)
                step_desc = [mach_arg]
                if re.match('build(-.*)?', mach_arg):
                    step_class = steps.Compile
                elif re.match('package', mach_arg):
                    step_class = steps.Compile
                elif re.match('test-.*', mach_arg):
                    step_class = steps.Test

            # Capture any logfiles
            elif re.match('--log-.*', arg):
                logfile = next(args)
                if 'logfiles' not in step_kwargs:
                    step_kwargs['logfiles'] = {}
                step_kwargs['logfiles'][logfile] = logfile

            # Provide environment variables for s3cmd
            elif arg == './etc/ci/upload_nightly.sh':
                step_kwargs['logEnviron'] = False
                step_env += envs.upload_nightly
                if self.is_windows:
                    # s3cmd on Windows only works within msys
                    step_env['MSYSTEM'] = 'MSYS'
                    step_env['PATH'] = ';'.join([
                        r'C:\msys64\usr\bin',
                        r'C:\Windows\system32',
                        r'C:\Windows',
                        r'C:\Windows\System32\Wbem',
                        r'C:\Windows\System32\WindowsPowerShell\v1.0',
                        r'C:\Program Files\Amazon\cfn-bootstrap',
                    ])

            else:
                step_desc += [arg]

        if step_class != steps.ShellCommand:
            step_kwargs['description'] = "running"
            step_kwargs['descriptionDone'] = "ran"
            step_kwargs['descriptionSuffix'] = " ".join(step_desc)

        step_kwargs['env'] = step_env
        return step_class(**step_kwargs)

    def make_pkill_step(self, target):
        if self.is_windows:
            pkill_command = ["powershell", "kill", "-n", target]
        else:
            pkill_command = ["pkill", "-x", target]

        return steps.ShellCommand(
            command=pkill_command,
            decodeRC={0: SUCCESS, 1: SUCCESS}
        )


class DynamicServoFactory(ServoFactory):
    """\
    Smart factory which takes a list of shell commands
    from a YAML file located in the main servo/servo repository
    and creates the appropriate Buildbot Steps.
    Uses heuristics to infer Step type, if there are any logfiles, etc.
    """

    def __init__(self, builder_name, environment):

        # util.BuildFactory is an old-style class so we cannot use super()
        # but must hardcode the superclass here
        ServoFactory.__init__(self, [
            StepsYAMLParsingStep(builder_name, environment,
                                 "etc/ci/buildbot_steps.yml")
        ])


doc = ServoFactory([
    # This is not dynamic because a) we need to pass the logEnviron kwarg
    # and b) changes to the documentation build are already encapsulated
    # in the upload_docs.sh script; any further changes should go through
    # the saltfs repo to avoid leaking the token.
    steps.ShellCommand(command=["etc/ci/upload_docs.sh"],
                       env=envs.doc,
                       # important not to leak token
                       logEnviron=False),
])

@@ -1735,16 +1735,80 @@
    try:%0A
+            show_cmd = %22cat%22 if not self.is_windows else %22type%22%0A
         
@@ -1877,21 +1877,24 @@
 ommand=%5B
-%22cat%22
+show_cmd
 , %22./%7B%7D%22

Fix steps reading on Windows