#!/usr/bin/env php
<?php

declare(strict_types=1);

/**
 * Usage: git close-pull-request -t <target>
 *
 * Run this from a branch which has an upstream remote branch, and an associated
 * pull request.
 *
 * The script will merge the branch into master, push master (which will
 * automatically close the pull request), and delete both the local and remote
 * branches.
 *
 * Based on a script by @christoomey. Translated into PHP.
 */

class ClosesPullRequests
{
    private $targetBranch;
    private $localBranch;
    private $remoteBranch;

    private const CI_ERROR = 'error';
    private const CI_PENDING = 'pending';
    private const CI_SUCCESS = 'success';

    private const RUN_TYPE_COMMAND = 'command';
    private const RUN_TYPE_QUERY = 'query';

    public function __construct()
    {
        $this->localBranch = $this->run(
            'git rev-parse --abbrev-ref HEAD',
            self::RUN_TYPE_QUERY
        );
        $this->targetBranch = $this->getTargetBranchFromArgs();

        $this->remoteBranch = $this->run(
            'git rev-parse --abbrev-ref --symbolic-full-name @{u}',
            self::RUN_TYPE_QUERY
        );
        $this->remoteBranch = str_replace('origin/', '', $this->remoteBranch);
    }

    public function __invoke(): void
    {
        $this->confirmCiStatusIsPassing();
        // TODO: Check that the current branch has a tracking branch.
        $this->fetchOrigin();
        $this->ensureFeatureBranchInSync();
        $this->ensureTargetBranchInSync();
        $this->checkoutTargetBranch();
        $this->mergeLocalBranch();
        $this->pushTargetBranch();
        $this->deleteRemoteBranch();
        $this->deleteLocalBranch();
    }

    private function getTargetBranchFromArgs(): string
    {
        if (!$targetBranchName = $this->getArg('t:', ['target:'])) {
            $this->dieWithMessage('Invalid target branch specified. Aborting.');
        }

        return $targetBranchName;
    }

    private function confirmCiStatusIsPassing(): void
    {
        echo 'Confirming ci-status on PR is green...' . PHP_EOL;

        // TODO: Check for failures, or skip if there is no CI.
        $errors = [
            self::CI_ERROR => 'Aborting: CI error',
            self::CI_PENDING => 'Aborting: CI pending',
        ];

        $ciStatus = $this->run('hub ci-status', self::RUN_TYPE_QUERY);

        switch ($ciStatus) {
            case self::CI_PENDING:
                $this->exitWithWarning($errors[$ciStatus]);

            case self::CI_ERROR:
                $this->dieWithMessage($errors[$ciStatus]);
        }
    }

    private function fetchOrigin(): void
    {
        print 'Fetching origin to confirm local and remote in sync...'
            . PHP_EOL;

        $this->run('git fetch origin', self::RUN_TYPE_COMMAND);
    }

    private function ensureTargetBranchInSync(): void
    {
        $this->ensureBranchInSyncWithUpstream(
            $this->targetBranch,
            $this->targetBranch
        );
    }

    private function ensureFeatureBranchInSync(): void
    {
        $this->ensureBranchInSyncWithUpstream(
            $this->localBranch,
            $this->remoteBranch
        );
    }

    private function ensureBranchInSyncWithUpstream(
        string $localBranch,
        string $remoteBranch
    ): void {
        echo sprintf(
            'Ensuring that %s is in sync with its upstream...',
            $localBranch
        ) . PHP_EOL;

        $localCommitTip = $this->tipCommitOfBranch($localBranch);
        $remoteCommitTip = $this->tipCommitOfBranch(sprintf(
            'origin/%s',
            $remoteBranch
        ));

        if ($localCommitTip != $remoteCommitTip) {
            $this->dieWithMessage(sprintf(
                'Branch %s was out of date, needs rebasing. Aborting.',
                $localBranch
            ));
        }
    }

    private function tipCommitOfBranch(string $branchName): string
    {
        return $this->run(
            sprintf('git rev-parse %s', $branchName),
            self::RUN_TYPE_QUERY
        );
    }

    private function checkoutTargetBranch(): void
    {
        echo sprintf('Checking out %s...' . PHP_EOL, $this->targetBranch);

        $this->run(
            sprintf('git checkout %s', $this->targetBranch),
            self::RUN_TYPE_COMMAND
        );
    }

    private function mergeLocalBranch(): void
    {
        echo sprintf(
            'Merging %s into %s...' . PHP_EOL,
            $this->localBranch,
            $this->targetBranch
        );

        $mergeCommand = sprintf('git merge --ff-only %s', $this->localBranch);
        if (!$this->run($mergeCommand, self::RUN_TYPE_COMMAND)) {
            // Switch back to the previous branch.
            $this->run('git checkout -', self::RUN_TYPE_COMMAND);

            $this->dieWithMessage(sprintf(
                'Branch %s is not fast-forwardable.',
                $this->localBranch
            ));
        }
    }

    public function pushTargetBranch(): void
    {
        print(sprintf('Pushing updated %s branch...', $this->targetBranch));

        $this->run(
            sprintf('git push origin %s', $this->targetBranch),
            self::RUN_TYPE_COMMAND
        );
    }

    public function deleteRemoteBranch(): void
    {
        echo 'Deleting remote branch...' . PHP_EOL;

        $this->run(
            sprintf('git push origin :%s', $this->remoteBranch),
            self::RUN_TYPE_COMMAND
        );
    }

    public function deleteLocalBranch(): void
    {
        echo 'Deleting local branch...' . PHP_EOL;

        $this->run(
            sprintf('git branch -d %s', $this->localBranch),
            self::RUN_TYPE_COMMAND
        );
    }

    private function getArg(string $shortOpts, array $longOpts = []): ?string
    {
        if (!$values = getopt($shortOpts, $longOpts)) {
            return NULL;
        }

        return current($values);
    }

    /**
     * Run the command.
     *
     * @return bool|string
     *   If the type is 'command', the method will return if there were any
     *   errors when running the command based on its return code.
     *
     *   If the type is 'query', then the output of the command will be returned
     *   as a string.
     */
    private function run(string $command, string $type)
    {
        switch ($type) {
            case self::RUN_TYPE_COMMAND:
                // Perform the command, hiding the original output and return
                // whether or not there were errors.
                @exec("$command", $output, $return);

                return $return == 0;

            case self::RUN_TYPE_QUERY:
                // Perform the command and return the output.
                return exec($command, $output);
        }
    }

    private function dieWithMessage(string $message): void
    {
        echo sprintf("\e[31m%s\e[0m", $message);

        exit(1);
    }

    private function exitWithWarning(string $message): void
    {
        echo sprintf("\e[33m%s\e[0m", $message);

        exit(2);
    }
}

(new ClosesPullRequests())->__invoke();