<?php

namespace Stevebauman\Location\Drivers;

use Exception;
use GeoIp2\Database\Reader;
use GeoIp2\Model\City;
use GeoIp2\Model\Country;
use GeoIp2\WebService\Client;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Fluent;
use Illuminate\Support\Str;
use PharData;
use PharFileInfo;
use RecursiveIteratorIterator;
use RuntimeException;
use Stevebauman\Location\Position;
use Stevebauman\Location\Request;

class MaxMind extends Driver implements Updatable
{
    /**
     * Update the MaxMind database.
     */
    public function update(Command $command): void
    {
        @mkdir(
            $root = Str::of($this->getDatabasePath())->dirname()
        );

        $storage = Storage::build([
            'driver' => 'local',
            'root' => $root,
        ]);

        $tarFilePath = $storage->path(
            $tarFileName = 'maxmind.tar.gz'
        );

        $response = Http::withOptions(['sink' => $tarFilePath])->get(
            $this->getDatabaseUrl()
        );

        throw_if(
            $response->failed(),
            new RuntimeException('Failed to download MaxMind database. Response: '.$response->body())
        );

        $archive = new PharData($tarFilePath);

        $file = $this->discoverDatabaseFile($archive);

        $directory = Str::of($file->getPath())->basename();

        $relativePath = implode('/', [$directory, $file->getFilename()]);

        $archive->extractTo($storage->path('/'), $relativePath, true);

        file_put_contents(
            $this->getDatabasePath(),
            fopen($storage->path($relativePath), 'r')
        );

        $storage->delete($tarFileName);
        $storage->deleteDirectory($directory);
    }

    /**
     * Attempt to discover the database file inside the archive.
     *
     * @throws Exception
     */
    protected function discoverDatabaseFile(PharData $archive): PharFileInfo
    {
        foreach (new RecursiveIteratorIterator($archive) as $file) {
            if (pathinfo($file, PATHINFO_EXTENSION) === 'mmdb') {
                return $file;
            }
        }

        throw new Exception('Unable to locate database file inside of MaxMind archive.');
    }

    /**
     * {@inheritdoc}
     */
    protected function hydrate(Position $position, Fluent $location): Position
    {
        $position->countryName = $location->country;
        $position->countryCode = $location->country_code;
        $position->isoCode = $location->country_code;
        $position->regionCode = $location->regionCode;
        $position->regionName = $location->regionName;
        $position->cityName = $location->city;
        $position->postalCode = $location->postal;
        $position->metroCode = $location->metro_code;
        $position->timezone = $location->timezone;
        $position->latitude = $location->latitude;
        $position->longitude = $location->longitude;

        return $position;
    }

    /**
     * {@inheritdoc}
     */
    protected function process(Request $request): Fluent|false
    {
        return rescue(function () use ($request) {
            $location = $this->fetchLocation($request->getIp());

            if ($location instanceof City) {
                return new Fluent([
                    'country' => $location->country->name,
                    'country_code' => $location->country->isoCode,
                    'city' => $location->city->name,
                    'regionCode' => $location->mostSpecificSubdivision->isoCode,
                    'regionName' => $location->mostSpecificSubdivision->name,
                    'postal' => $location->postal->code,
                    'timezone' => $location->location->timeZone,
                    'latitude' => (string) $location->location->latitude,
                    'longitude' => (string) $location->location->longitude,
                    'metro_code' => (string) $location->location->metroCode,
                ]);
            }

            return new Fluent([
                'country' => $location->country->name,
                'country_code' => $location->country->isoCode,
            ]);
        }, false, false);
    }

    /**
     * Attempt to fetch the location model from Maxmind.
     *
     * @throws \Exception
     */
    protected function fetchLocation(string $ip): City|Country
    {
        $maxmind = $this->isWebServiceEnabled()
            ? $this->newClient($this->getUserId(), $this->getLicenseKey(), $this->getOptions())
            : $this->newReader($this->getDatabasePath());

        if ($this->isWebServiceEnabled() || $this->getLocationType() === 'city') {
            return $maxmind->city($ip);
        }

        return $maxmind->country($ip);
    }

    /**
     * Get a new MaxMind web service client.
     */
    protected function newClient(string $userId, string $licenseKey, array $options = []): Client
    {
        return new Client($userId, $licenseKey, $options);
    }

    /**
     * Get a new MaxMind reader client.
     */
    protected function newReader(string $path): Reader
    {
        return new Reader($path);
    }

    /**
     * Returns true / false if the MaxMind web service is enabled.
     */
    protected function isWebServiceEnabled(): bool
    {
        return (bool) config('location.maxmind.web.enabled', false);
    }

    /**
     * Returns the configured MaxMind web user ID.
     */
    protected function getUserId(): string
    {
        return config('location.maxmind.web.user_id');
    }

    /**
     * Returns the configured MaxMind web license key.
     */
    protected function getLicenseKey(): string
    {
        return config('location.maxmind.license_key', config('location.maxmind.web.license_key'));
    }

    /**
     * Returns the configured MaxMind web option array.
     */
    protected function getOptions(): array
    {
        return config('location.maxmind.web.options', []);
    }

    /**
     * Returns the MaxMind database file path.
     */
    protected function getDatabasePath(): string
    {
        return config('location.maxmind.local.path', database_path('maxmind/GeoLite2-City.mmdb'));
    }

    /**
     * Get the database URL to download.
     */
    protected function getDatabaseUrl(): string
    {
        return config(
            'location.maxmind.local.url',
            sprintf('https://download.maxmind.com/app/geoip_download_by_token?edition_id=GeoLite2-City&license_key=%s&suffix=tar.gz', $this->getLicenseKey()),
        );
    }

    /**
     * Returns the MaxMind location type.
     */
    protected function getLocationType(): string
    {
        return config('location.maxmind.local.type', 'city');
    }
}
