<?php

namespace onespace\core\validators;

use Closure;
use Exception;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberType;
use libphonenumber\PhoneNumberUtil;
use Yii;
use yii\helpers\Inflector;
use yii\validators\Validator;

/**
 * @psalm-api
 * 
 * Phone validator class that validates phone numbers for given
 * country and formats.
 * Country codes and attributes value should be ISO 3166-1 alpha-2 codes
 * @property ?string $countryAttribute The country code attribute of model
 * @property ?string $country The country is fixed
 * @property ?PhoneNumberType|PhoneNumberType[] $type The type of number it should be - can be left empty, or an array
 * @property bool $strict If country is not set or selected adds error
 * @property PhoneNumberFormat $format If phone number is valid formats value with provided `$formatter`
 * @property ?Closure $formatter A method to complete final formatting of the string. Should accept a string, and must return a string
 *          libphonenumber/PhoneNumberFormat const (default to INTERNATIONAL)
 */
class PhoneValidator extends Validator {
    /** @var bool */
    public $strict = true;
    /** @var ?string */
    public $countryAttribute;
    /** @var ?string */
    public $country;
    /** @var ?PhoneNumberType|PhoneNumberType[] */
    public $type = null;
    /** @var ?Closure */
    public $formatter;
    /** @var PhoneNumberFormat */
    public $format = PhoneNumberFormat::INTERNATIONAL;
    /** @var bool */
    public $includePlus = false;

    /**
     * @param \yii\base\Model $model the data model to be validated
     * @param string $attribute the name of the attribute to be validated.
     * @return bool|null
     */
    #[\Override]
    public function validateAttribute($model, $attribute) {
        // if countryAttribute is set
        if (isset($this->countryAttribute)) {
            $countryAttribute = $this->countryAttribute;
            $country = $model->$countryAttribute;
        }

        // if country is fixed
        if (!isset($country) && isset($this->country)) {
            $country = $this->country;
        }

        // if none select from our models with best effort
        if (!isset($country) && isset($model->country_code)) {
            $country = $model->country_code;
        }

        if (!isset($country) && isset($model->country)) {
            $country = $model->country;
        }


        // if none and strict
        if (!isset($country) && $this->strict) {
            $this->addError($model, $attribute, Yii::t('udokmeci.phone.validator', 'For phone validation country required'));
            return false;
        }

        if (!isset($country)) {
            return true;
        }

        $phoneUtil = PhoneNumberUtil::getInstance();
        try {
            // add a plus if needed
            $number = $model->$attribute;
            if ($number[0] != '0' && $number[0] != '+') {
                $number = '+' . $number;
            }
            $numberProto = $phoneUtil->parse($number, $country);
            if ($phoneUtil->isValidNumber($numberProto)) {
                if (isset($this->type)) {
                    $type = $phoneUtil->getNumberType($numberProto);
                    if (is_array($this->type)) {
                        if (!in_array($type, $this->type)) {
                            $this->addError($model, $attribute, $model->getAttributeLabel($attribute) . ' must be a ' . Inflector::sentence(self::getTypeStrings($this->type), ' or ') . ' number');
                            return false;
                        }
                    } elseif ($type != $this->type) {
                        $this->addError($model, $attribute, $model->getAttributeLabel($attribute) . ' must be a ' . self::getTypeString($this->type) . ' number');
                        return false;
                    }
                }
                $model->$attribute = $phoneUtil->format($numberProto, $this->format);
                if (!$this->includePlus) {
                    $model->$attribute = str_replace('+', '', $model->$attribute);
                }
                if ($this->formatter != null) {
                    $model->$attribute = $this->formatter->call($this, $model->$attribute);
                }
                return true;
            } else {
                $this->addError($model, $attribute, $model->getAttributeLabel($attribute) . ' does not seem to be a valid phone number');
                return false;
            }
        } catch (NumberParseException) {
            $this->addError($model, $attribute, 'Unexpected Phone Number Format');
        } catch (Exception) {
            $this->addError($model, $attribute, 'Unexpected Phone Number Format or Country Code');
        }
    }

    public static function getTypeString(PhoneNumberType $value): string {
        if (!key_exists($value->value, static::TYPE_STRINGS)) {
            $value = PhoneNumberType::UNKNOWN;
        }
        return static::TYPE_STRINGS[$value->value];
    }

    /**
     * 
     * 
     * 
     * @psalm-return list<string>
     */
    public static function getTypeStrings(array $values): array {
        $labels = [];
        foreach ($values as $value) {
            $labels[] = self::getTypeString($value);
        }
        return $labels;
    }

    /**
     * @var array<int, string>
     */
    const array TYPE_STRINGS = [
        PhoneNumberType::FIXED_LINE->value => 'Landline',
        PhoneNumberType::MOBILE->value => 'Mobile',
        PhoneNumberType::FIXED_LINE_OR_MOBILE->value => 'Landline/Mobile',
        PhoneNumberType::TOLL_FREE->value => 'Toll Free',
        PhoneNumberType::PREMIUM_RATE->value => 'Premium Rate',
        PhoneNumberType::SHARED_COST->value => 'Shared Cost',
        PhoneNumberType::VOIP->value => 'VOIP',
        PhoneNumberType::PERSONAL_NUMBER->value => 'Personal Number',
        PhoneNumberType::PAGER->value => 'Pager',
        PhoneNumberType::UAN->value => 'UAN',
        PhoneNumberType::EMERGENCY->value => 'Emergency',
        PhoneNumberType::VOICEMAIL->value => 'Voicemail',
        PhoneNumberType::SHORT_CODE->value => 'Short Code',
        PhoneNumberType::STANDARD_RATE->value => 'Standard Rate',
        PhoneNumberType::UNKNOWN->value => 'Unknown',
    ];
}
