<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Flex;

use Composer\Util\HttpDownloader;
use Composer\Util\RemoteFilesystem;

class GithubApi
{
    /** @var HttpDownloader|RemoteFilesystem */
    private $downloader;

    public function __construct($downloader)
    {
        $this->downloader = $downloader;
    }

    /**
     * Attempts to find data about when the recipe was installed.
     *
     * Returns an array containing:
     *      commit: The git sha of the last commit of the recipe
     *      date: The date of the commit
     *      new_commits: An array of commit sha's in this recipe's directory+version since the commit
     *                   The key is the sha & the value is the date
     */
    public function findRecipeCommitDataFromTreeRef(string $package, string $repo, string $branch, string $version, string $lockRef): ?array
    {
        $repositoryName = $this->getRepositoryName($repo);
        if (!$repositoryName) {
            return null;
        }

        $recipePath = sprintf('%s/%s', $package, $version);
        $commitsData = $this->requestGitHubApi(sprintf(
            'https://api.github.com/repos/%s/commits?path=%s&sha=%s',
            $repositoryName,
            $recipePath,
            $branch
        ));

        $commitShas = [];
        foreach ($commitsData as $commitData) {
            $commitShas[$commitData['sha']] = $commitData['commit']['committer']['date'];
            // go back the commits one-by-one
            $treeUrl = $commitData['commit']['tree']['url'].'?recursive=true';

            // fetch the full tree, then look for the tree for the package path
            $treeData = $this->requestGitHubApi($treeUrl);
            foreach ($treeData['tree'] as $treeItem) {
                if ($treeItem['path'] !== $recipePath) {
                    continue;
                }

                if ($treeItem['sha'] === $lockRef) {
                    // remove *this* commit from the new commits list
                    array_pop($commitShas);

                    return [
                        // shorten for brevity
                        'commit' => substr($commitData['sha'], 0, 7),
                        'date' => $commitData['commit']['committer']['date'],
                        'new_commits' => $commitShas,
                    ];
                }
            }
        }

        return null;
    }

    public function getVersionsOfRecipe(string $repo, string $branch, string $recipePath): ?array
    {
        $repositoryName = $this->getRepositoryName($repo);
        if (!$repositoryName) {
            return null;
        }

        $url = sprintf(
            'https://api.github.com/repos/%s/contents/%s?ref=%s',
            $repositoryName,
            $recipePath,
            $branch
        );
        $contents = $this->requestGitHubApi($url);
        $versions = [];
        foreach ($contents as $fileData) {
            if ('dir' !== $fileData['type']) {
                continue;
            }

            $versions[] = $fileData['name'];
        }

        return $versions;
    }

    public function getCommitDataForPath(string $repo, string $path, string $branch): array
    {
        $repositoryName = $this->getRepositoryName($repo);
        if (!$repositoryName) {
            return [];
        }

        $commitsData = $this->requestGitHubApi(sprintf(
            'https://api.github.com/repos/%s/commits?path=%s&sha=%s',
            $repositoryName,
            $path,
            $branch
        ));

        $data = [];
        foreach ($commitsData as $commitData) {
            $data[$commitData['sha']] = $commitData['commit']['committer']['date'];
        }

        return $data;
    }

    public function getPullRequestForCommit(string $commit, string $repo): ?array
    {
        $data = $this->requestGitHubApi('https://api.github.com/search/issues?q='.$commit.'+is:pull-request');

        if (0 === \count($data['items'])) {
            return null;
        }

        $repositoryName = $this->getRepositoryName($repo);
        if (!$repositoryName) {
            return null;
        }

        $bestItem = null;
        foreach ($data['items'] as $item) {
            // make sure the PR referenced isn't from a different repository
            if (false === strpos($item['html_url'], sprintf('%s/pull', $repositoryName))) {
                continue;
            }

            if (null === $bestItem) {
                $bestItem = $item;

                continue;
            }

            // find the first PR to reference - avoids rare cases where an invalid
            // PR that references *many* commits is first
            // e.g. https://api.github.com/search/issues?q=a1a70353f64f405cfbacfc4ce860af623442d6e5
            if ($item['number'] < $bestItem['number']) {
                $bestItem = $item;
            }
        }

        if (!$bestItem) {
            return null;
        }

        return [
            'number' => $bestItem['number'],
            'url' => $bestItem['html_url'],
            'title' => $bestItem['title'],
        ];
    }

    private function requestGitHubApi(string $path)
    {
        $contents = $this->downloader->get($path)->getBody();

        return json_decode($contents, true);
    }

    /**
     * Converts the "repo" stored in symfony.lock to a repository name.
     *
     * For example: "github.com/symfony/recipes" => "symfony/recipes"
     */
    private function getRepositoryName(string $repo): ?string
    {
        // only supports public repository placement
        if (0 !== strpos($repo, 'github.com')) {
            return null;
        }

        $parts = explode('/', $repo);
        if (3 !== \count($parts)) {
            return null;
        }

        return implode('/', [$parts[1], $parts[2]]);
    }
}
