<?php


namespace CubiStore\Web\Service;

use CubiStore\Web\Model\App;
use DI\Container;
use Doctrine\ORM\EntityManager;
use RuntimeException;
use ZipArchive;

class FDroidRepoService
{
    /**
     * @var Container
     */
    private $container;

    /**
     * FDroidRepoService constructor.
     * @param Container $container
     */
    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    public function createIndexV1Array()
    {
        /** @var EntityManager $em */
        $em = $this->container->get(EntityManager::class);

        /** @var App[] $apps */
        $apps = $em
            ->getRepository(App::class)
            ->findBy([
                'status' => App::STATUS_APPROVED
            ]);

        $appObjects = [];
        $packageObjects = [];

        /** @var App $app */
        foreach ($apps as $app) {
            $appObjects[] = [
                "authorEmail" => "example@example.com",
                "authorName" => "example",
                "authorWebSite" => "https://example.org",
                "categories" => [
                    'example'
                ],
                "suggestedVersionCode" => $app->getReleases()->first()->getVersionCode(),
                "name" => $app->getLabel(),
                "added" => 0,
                "packageName" => $app->getName(),
                "lastUpdated" => 0,
                "license" => "no"
            ];

            $packages = [];

            foreach ($app->getReleases() as $release) {
                $packages[] = [
                    "added" => 0,
                    "apkName" => "/apk/" . $app->getName() . "/" . $release->getVersionCode() . ".apk",
                    "hash" => hash('sha256', $release->getApk()),
                    "hashType" => "sha256",
                    "minSdkVersion" => 4,
                    "packageName" => $app->getName(),
                    "sig" => "deadbeef",
                    "signer" => "deadbeef",
                    "size" => filesize($release->getApk()),
                    "targetSdkVersion" => 28,
                    "versionCode" => $release->getVersionCode(),
                    "versionName" => $release->getVersionName()
                ];
            }

            $packageObjects[$app->getName()] = $packages;
        }

        return [
            'repo' => [
                'timestamp' => time() * 1000,
                'version' => 21,
                'maxage' => 14,
                'name' => $this->container->get('app.name'),
                'description' => $this->container->get('app.description'),
                'address' => "https://" . $this->container->get('app.domain') . '/repo'
            ],
            'requests' => [
                'install' => [],
                'uninstall' => [],
            ],
            "apps" => $appObjects,
            "packages" => $packageObjects
        ];
    }

    /**
     * Create's a signed jar with a single file.
     *
     * A jar is a zip file.
     * Contains the following files:
     *
     * META-INF/MANIFEST.MF
     *   Contains manifest header
     *   Contains header (and hashes) for files
     *
     * META-INF/1.SF
     *   Contains hash of full manifest, manifest header and file header
     *
     * META-INF/1.RSA
     *   PKCS7 Signature of META-INF/1.SF
     *
     * @param string $file The filename of the file to add in the jar
     * @param string $contents The contents of the file to add in the jar
     * @return string
     */
    public function createSigned($file, $contents)
    {
        $zip = new ZipArchive();
        $zipPath = tempnam(sys_get_temp_dir(), 'zip');
        if ($zipPath === false) {
            throw new RuntimeException("Failed to create temp file");
        }

        $fileDigest = hash('sha256', $contents, true);
        $zip->open($zipPath, ZipArchive::CREATE);

        $fileHeader = 'Name: ' . $file . "\n";
        $fileHeader .= 'SHA-256-Digest: ' . base64_encode($fileDigest) . "\n\n";

        $fileHeaderDigest = hash('sha256', $fileHeader, true);

        $manifest = "Manifest-Version: 1.0\n";
        $manifest .= "Created-By: CubiStore\n\n";
        $manifestHeaderDigest = hash('sha256', $manifest, true);

        $manifest .= $fileHeader;
        $manifestDigest = hash('sha256', $manifest, true);

        $fileManifest = "Signature-Version: 1.0\n";
        $fileManifest .= "SHA-256-Digest-Manifest-Main-Attributes: " . base64_encode($manifestHeaderDigest) . "\n";
        $fileManifest .= "SHA-256-Digest-Manifest: " . base64_encode($manifestDigest) . "\n";
        $fileManifest .= "Created-By: CubiStore\n\n";
        $fileManifest .= "Name: " . $file . "\n";
        $fileManifest .= "SHA-256-Digest: " . base64_encode($fileHeaderDigest) . "\n\n";

        $zip->addFromString('META-INF/MANIFEST.MF', $manifest);
        $zip->addFromString('META-INF/1.SF', $fileManifest);

        $in = tempnam(sys_get_temp_dir(), 'repo');
        if ($in === false) {
            throw new RuntimeException("Failed to create temp file");
        }

        $out = tempnam(sys_get_temp_dir(), 'repo');
        if ($out === false) {
            throw new RuntimeException("Failed to create temp file");
        }

        file_put_contents($in, $fileManifest);


        $success = openssl_pkcs7_sign(
            $in,
            $out,
            'file://' . __DIR__ . '/../../certs/cert.pem',
            'file://' . __DIR__ . '/../../certs/key.pem',
            [],
            PKCS7_BINARY | PKCS7_NOATTR
        );

        if (!$success) {
            $error = '';
            while ($line = openssl_error_string()) {
                $error .= $line . "\n";
            }

            throw new RuntimeException($error);
        }

        unlink($in);
        $signed = file_get_contents($out);
        unlink($out);

        if ($signed === false) {
            throw new RuntimeException("Signature wasn't created");
        }

        $contentOffset = strpos($signed, "\n\n");
        if ($contentOffset === false) {
            throw new RuntimeException("Signature is not as expected");
        }

        $content = substr($signed, $contentOffset);
        $base64 = str_replace("\n", "", $content);
        $der = base64_decode($base64);

        if ($der === false) {
            throw new RuntimeException("Failed to decode DER from signature");
        }

        $zip->addFromString('META-INF/1.RSA', $der);
        $zip->addFromString($file, $contents);
        $zip->close();
        $zipContent = file_get_contents($zipPath);
        unlink($zipPath);

        if ($zipContent === false) {
            return new RuntimeException("Failed to read resulting jar");
        }

        return $zipContent;
    }
}
