<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\MainBundle\Phonenumber;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\ServerException;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberType;
use libphonenumber\PhoneNumberUtil;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;

final class PhonenumberHelper implements PhoneNumberHelperInterface
{
    public const FORMAT_URI = 'https://lookups.twilio.com/v1/PhoneNumbers/%s';

    public const LOOKUP_URI = 'https://lookups.twilio.com/v1/PhoneNumbers/%s';

    private readonly array $config;

    private bool $isConfigured = false;

    private readonly PhoneNumberUtil $phoneNumberUtil;

    private Client $twilioClient;

    public function __construct(
        private readonly CacheItemPoolInterface $cachePool,
        ParameterBagInterface $parameterBag,
        private readonly LoggerInterface $logger,
    ) {
        $this->config = $config = $parameterBag->get('chill_main.phone_helper');

        if (
            \array_key_exists('twilio_sid', $config)
            && !empty($config['twilio_sid'])
            && \strlen((string) $config['twilio_sid']) > 2
            && \array_key_exists('twilio_secret', $config)
            && !empty($config['twilio_secret'])
            && \strlen((string) $config['twilio_secret']) > 2
        ) {
            $this->twilioClient = new Client([
                'auth' => [$config['twilio_sid'], $config['twilio_secret']],
            ]);
            $this->isConfigured = true;
        }

        $this->phoneNumberUtil = PhoneNumberUtil::getInstance();
    }

    /**
     * @param string $phoneNumber A national phone number starting with +
     *
     * @throws NumberParseException
     */
    public function format(?PhoneNumber $phoneNumber = null): string
    {
        if (null === $phoneNumber) {
            return '';
        }

        return $this->phoneNumberUtil
            ->formatOutOfCountryCallingNumber($phoneNumber, $this->config['default_carrier_code']);
    }

    /**
     * @throws NumberParseException
     */
    public function parse(string $phoneNumber): PhoneNumber
    {
        $sanitizedPhoneNumber = $phoneNumber;

        if (str_starts_with($sanitizedPhoneNumber, '00')) {
            $sanitizedPhoneNumber = '+'.substr($sanitizedPhoneNumber, 2, null);
        }

        if (!str_starts_with($sanitizedPhoneNumber, '+') && !str_starts_with($sanitizedPhoneNumber, '0')) {
            $sanitizedPhoneNumber = '+'.$sanitizedPhoneNumber;
        }

        return $this->phoneNumberUtil->parse($sanitizedPhoneNumber, $this->config['default_carrier_code']);
    }

    /**
     * Get type (mobile, landline, ...) for phone number.
     */
    public function getType(PhoneNumber $phonenumber): string
    {
        return match ($this->phoneNumberUtil->getNumberType($phonenumber)) {
            PhoneNumberType::MOBILE => 'mobile',
            PhoneNumberType::FIXED_LINE, PhoneNumberType::VOIP => 'landline',
            default => 'landline',
        };
    }

    /**
     * Return true if the validation is configured and available.
     */
    public function isPhonenumberValidationConfigured(): bool
    {
        return $this->isConfigured;
    }

    /**
     * Return true if the phonenumber is a landline or voip phone.  Return always true
     * if the validation is not configured.
     *
     * @param string|PhoneNumber $phonenumber
     */
    public function isValidPhonenumberAny($phonenumber): bool
    {
        if (false === $this->isConfigured) {
            return true;
        }
        $validation = $this->performTwilioLookup($phonenumber);

        if (null === $validation) {
            return false;
        }

        return \in_array($validation, ['landline', 'voip', 'mobile'], true);
    }

    /**
     * Return true if the phonenumber is a landline or voip phone.  Return always true
     * if the validation is not configured.
     *
     * @param string|PhoneNumber $phonenumber
     */
    public function isValidPhonenumberLandOrVoip($phonenumber): bool
    {
        if (false === $this->isConfigured) {
            return true;
        }

        $validation = $this->performTwilioLookup($phonenumber);

        if (null === $validation) {
            return true;
        }

        return \in_array($validation, ['landline', 'voip'], true);
    }

    /**
     * REturn true if the phonenumber is a mobile phone. Return always true
     * if the validation is not configured.
     *
     * @param string|PhoneNumber $phonenumber
     */
    public function isValidPhonenumberMobile($phonenumber): bool
    {
        if (false === $this->isConfigured) {
            return true;
        }

        $validation = $this->performTwilioLookup($phonenumber);

        if (null === $validation) {
            return true;
        }

        return 'mobile' === $validation;
    }

    private function performTwilioLookup($phonenumber)
    {
        if (false === $this->isConfigured) {
            return null;
        }

        if ($phonenumber instanceof PhoneNumber) {
            $phonenumber = (string) $phonenumber;
        }

        // filter only number
        $filtered = \preg_replace('/[^0-9]/', '', (string) $phonenumber);

        $item = $this->cachePool->getItem('pnum_'.$filtered);

        if ($item->isHit()) {
            return $item->get();
        }

        try {
            $response = $this->twilioClient->get(sprintf(self::LOOKUP_URI, '+'.$filtered), [
                'http_errors' => true,
                'query' => [
                    'Type' => 'carrier',
                ],
            ]);
        } catch (ClientException) {
            return 'invalid';
        } catch (ServerException $e) {
            $response = $e->getResponse();
            $this->logger->error('[phonenumber helper] Could not perform validation '
                .'due to server error', [
                    'message' => $response->getBody()->getContents(),
                    'status_code' => $response->getStatusCode(),
                    'phonenumber' => $phonenumber,
                ]);

            return null;
        } catch (ConnectException $e) {
            $this->logger->error('[phonenumber helper] Could not format number '
                .'due to connect error', [
                    'message' => $e->getMessage(),
                    'phonenumber' => $phonenumber,
                ]);

            return null;
        }

        $validation = \json_decode($response->getBody()->getContents(), null, 512, JSON_THROW_ON_ERROR)->carrier->type;

        $item
            ->set($validation)
            // expires after 12h
            ->expiresAfter(3600 * 12);

        $this->cachePool->save($item);

        return $validation;
    }
}
