<?php

namespace Mautic\CoreBundle\IpLookup;

use GuzzleHttp\RequestOptions;
use Mautic\CoreBundle\Form\Type\IpLookupDownloadDataStoreButtonType;

abstract class AbstractLocalDataLookup extends AbstractLookup implements IpLookupFormInterface
{
    /**
     * @const TAR_CACHE_FOLDER
     */
    public const TAR_CACHE_FOLDER = 'unpack';

    /**
     * @const TAR_TEMP_FILE
     */
    public const TAR_TEMP_FILE = 'temp.tar.gz';

    /**
     * Path to the local data store.
     *
     * @return string
     */
    abstract public function getLocalDataStoreFilepath();

    /**
     * Return the URL to manually download.
     *
     * @return string
     */
    abstract public function getRemoteDateStoreDownloadUrl();

    /**
     * @return string
     */
    public function getConfigFormService()
    {
        return IpLookupDownloadDataStoreButtonType::class;
    }

    /**
     * @return array
     */
    public function getConfigFormThemes()
    {
        return [];
    }

    /**
     * Download remote data store.
     *
     * Used by the mautic:iplookup:update_data command and form fetch button (if applicable) to update local IP data stores
     *
     * @return bool
     */
    public function downloadRemoteDataStore()
    {
        $package   = $this->getRemoteDateStoreDownloadUrl();

        if (empty($package)) {
            $this->logger->error('Failed to fetch remote IP data: Invalid or inactive MaxMind license key');

            return false;
        }
        try {
            $data = $this->client->get($package, [
                RequestOptions::ALLOW_REDIRECTS => true,
            ]);
        } catch (\Exception $exception) {
            $this->logger->error('Failed to fetch remote IP data: '.$exception->getMessage());
        }

        $tempTarget        = $this->cacheDir.'/'.basename($package);
        $tempExt           = strtolower(pathinfo($package, PATHINFO_EXTENSION));
        $localTarget       = $this->getLocalDataStoreFilepath();
        $localTargetExt    = strtolower(pathinfo($localTarget, PATHINFO_EXTENSION));

        try {
            $success = false;

            switch (true) {
                case $localTargetExt === $tempExt:
                    $success = (bool) file_put_contents($localTarget, $data->getBody());

                    break;

                case $this->endsWith($package, 'tar.gz'):
                    /**
                     * If tar.gz it loops whole folder structure and copy the file which has the same basename as
                     * desired localTarget.
                     */
                    $tempTargetFolder = $this->cacheDir.'/'.self::TAR_CACHE_FOLDER;
                    $temporaryPhar    = $tempTargetFolder.'/'.self::TAR_TEMP_FILE;
                    if (!is_dir($tempTargetFolder)) {
                        // dir doesn't exist, make it
                        mkdir($tempTargetFolder);
                    }
                    file_put_contents($temporaryPhar, $data->getBody());
                    $pharData = new \PharData($temporaryPhar);
                    foreach (new \RecursiveIteratorIterator($pharData) as $file) {
                        /** @var \PharFileInfo $file */
                        if ($file->getBasename() === basename($localTarget)) {
                            $success = copy($file->getPathname(), $localTarget);
                        }
                    }
                    @unlink($temporaryPhar);

                    break;

                case 'gz' == $tempExt:
                    $memLimit = $this->sizeInByte(ini_get('memory_limit'));
                    $freeMem  = $memLimit - memory_get_peak_usage();
                    // check whether there is enough memory to handle large iplookp DB
                    // or will throw iplookup exception
                    if (function_exists('gzdecode') && strlen($data->getBody()) < ($freeMem / 3)) {
                        $success = (bool) file_put_contents($localTarget, gzdecode($data->getBody()));
                    } elseif (function_exists('gzopen')) {
                        if (file_put_contents($tempTarget, $data->getBody())) {
                            $bufferSize = 4096; // read 4kb at a time
                            $file       = gzopen($tempTarget, 'rb');
                            $outFile    = fopen($localTarget, 'wb');
                            while (!gzeof($file)) {
                                fwrite($outFile, gzread($file, $bufferSize));
                            }
                            fclose($outFile);
                            gzclose($file);
                            @unlink($tempTarget);
                            $success = true;
                        }
                    }

                    break;

                case 'zip' == $tempExt:
                    file_put_contents($tempTarget, $data->getBody());

                    $zipper = new \ZipArchive();

                    $zipper->open($tempTarget);
                    $success = $zipper->extractTo($localTarget);
                    $zipper->close();
                    @unlink($tempTarget);
                    break;
            }
        } catch (\Exception $exception) {
            error_log($exception);

            $success = false;
        }

        return $success;
    }

    /**
     * Get the common directory for data.
     *
     * @return string|null
     */
    protected function getDataDir()
    {
        if (null !== $this->cacheDir) {
            if (!file_exists($this->cacheDir)) {
                mkdir($this->cacheDir);
            }

            $dataDir = $this->cacheDir.'/../ip_data';

            if (!file_exists($dataDir)) {
                mkdir($dataDir);
            }

            return $dataDir;
        }

        return null;
    }

    protected function sizeInByte($size)
    {
        $data = (int) substr($size, 0, -1);
        switch (strtoupper(substr($size, -1))) {
            case 'K':
                return $data * 1024;
            case 'M':
                return $data * 1024 * 1024;
            case 'G':
                return $data * 1024 * 1024 * 1024;
        }
    }

    /**
     * Get if the string ends with.
     *
     * @param string $haystack
     * @param string $needle
     */
    private function endsWith($haystack, $needle): bool
    {
        return str_ends_with($haystack, $needle);
    }
}
