<?php

namespace idoit\Module\Forms\Model\Validators;

use idoit\Component\Property\Property;
use isys_application;
use isys_cmdb_dao_category;
use isys_cmdb_dao_category_g_custom_fields;
use isys_component_template_language_manager;
use isys_exception_database;

class GenericValidator implements ValidatorInterface
{
    /**
     * @var isys_cmdb_dao_category
     */
    private $categoryDao;

    /**
     * @var isys_component_template_language_manager
     */
    private $language;

    /**
     * @var string
     */
    private $tablePrepend = '';

    /**
     * @return GenericValidator
     */
    public static function factory()
    {
        return (new self())
          ->setLanguage(isys_application::instance()->container->get('language'));
    }

    /**
     * @param string $categoryConst
     *
     * @return bool
     */
    public function isApplicable(string $categoryConst): bool
    {
        return true;
    }

    /**
     * @param array                  $formData
     * @param isys_cmdb_dao_category $categoryDao
     *
     * @return array|null
     */
    public function validate(array $formData, isys_cmdb_dao_category $categoryDao): ?array
    {
        $validationIssues = [];
        $properties = $categoryDao->get_properties(C__PROPERTY__WITH__VALIDATION);

        if (is_array($formData)) {
            foreach ($properties as $key => $property) {
                $property = $properties[$key];
                $value = $formData[$key] ?: null;

                if (is_array($property)) {
                    $property = Property::createInstanceFromArray($property);
                }

                if (!$this->isValidatable($property)) {
                    continue;
                }

                $propertyChecks = $property->getCheck();
                // Mandatory field always have to be checked
                if ($propertyChecks->isMandatory() && ($issue = $this->validateValueSet($value, $property))) {
                    $validationIssues[$key] = $issue;
                    continue;
                }

                // Value is empty, but it's not a mandatory field.
                if (!isset($formData[$key]) || trim($value . '') === '') {
                    continue;
                }

                if ($issue = $this->validateUniqueChecks($value, $property, $properties, $categoryDao)) {
                    $validationIssues[$key] = $issue;
                    continue;
                }

                if ($propertyChecks->getValidationType() && ($issue = $this->validateFilterChecks($value, $property))) {
                    $validationIssues[$key] = $issue;
                }
            }
        }

        if (empty($validationIssues)) {
            $validationIssues = null;
        }

        return $validationIssues;
    }

    /**
     * @param isys_component_template_language_manager $language
     *
     * @return GenericValidator
     */
    public function setLanguage(isys_component_template_language_manager $language): GenericValidator
    {
        $this->language = $language;
        return $this;
    }

    /**
     * @param string|null $tablePrepend
     *
     * @return GenericValidator
     */
    public function setTablePrepend(?string $tablePrepend): GenericValidator
    {
        $this->tablePrepend = $tablePrepend;
        return $this;
    }

    /**
     * @param mixed $value
     * @param Property $property
     *
     * @return string|null
     */
    private function validateValueSet($value, Property $property)
    {
        // Check, if we got an empty string.
        if (trim($value . '') === '') {
            return $this->language->get('LC__UNIVERSAL__MANDATORY_FIELD_IS_EMPTY');
        }

        // Now to check for Dialog fields.
        if ($value == -1 && $property->getUi()->getType() == C__PROPERTY__UI__TYPE__DIALOG) {
            return $this->language->get('LC__UNIVERSAL__MANDATORY_FIELD_IS_EMPTY');
        }

        // Now to check for Dialog+ and Object-Browser fields.
        if ($property->getUi()->getType() == C__PROPERTY__UI__TYPE__POPUP &&
            ($value == -1 || $value == 'NULL' || $value == '0' || (is_array($value) && count($value) === 0))) {
            return $this->language->get('LC__UNIVERSAL__MANDATORY_FIELD_IS_EMPTY');
        }

        return null;
    }

    /**
     * @param                        $value
     * @param Property               $property
     *
     * @return string|null
     */
    private function validateFilterChecks($value, Property $property): ?string
    {
        $propertyChecks = $property->getCheck();
        $filter = null;

        if (is_numeric($propertyChecks->getValidationType()) && $propertyChecks->getValidationType() > 0) {
            $filter = $propertyChecks->getValidationType();
        } else {
            if (defined($propertyChecks->getValidationType())) {
                $filter = constant($propertyChecks->getValidationType());
            } else {
                if ($propertyChecks->getValidationType() == 'VALIDATE_BY_TEXTFIELD') {
                    // This case requires special treatment, because "filter_var" can not handle it!
                    $stringsToCheck = explode("\n", $propertyChecks->getValidationOptions()['value']);

                    if (!in_array($value, $stringsToCheck)) {
                        return $this->language->get('LC__SETTINGS__CMDB__VALIDATION__BY_TEXTFIELD_ERROR');
                    }
                }
            }
        }

        if ($filter === null) {
            return null;
        }

        if (is_string($filter) && defined($filter)) {
            $filter = constant($filter);
        }

        if ($propertyChecks->getValidationOptions()) {
            $options = $propertyChecks->getValidationOptions();
        } else {
            $options = null;
        }

        // Check, if the regular expression has delimiter.
        if (isset($options['options']['regexp'])
          && substr($options['options']['regexp'], 0, 1) != substr($options['options']['regexp'], -1, 1)
        ) {
            $options['options']['regexp'] = '~' . $options['options']['regexp'] . '~';
        }

        if (isset($filter)) {
            if ($filter == FILTER_VALIDATE_FLOAT) {
                // ID-2717 If we want to validate floats, always replace the comma with a dot. This will also happen before saving.
                $value = str_replace(',', '.', $value);
            }
            
            if ($filter == FILTER_VALIDATE_INT && !is_numeric($value)) {
                return $this->language->get('LC__SETTINGS__CMDB__VALIDATION_MESSAGE__NEEDS_TO_BE_INTEGER');
            } elseif ($filter != FILTER_VALIDATE_INT && filter_var($value, $filter, $options) === false) {
                switch ($filter) {
                    case FILTER_VALIDATE_FLOAT:
                        $validationErrorMessage = 'LC__SETTINGS__CMDB__VALIDATION_MESSAGE__NEEDS_TO_BE_FLOAT';
                        break;

                    case FILTER_VALIDATE_REGEXP:
                        $validationErrorMessage = $this->language->get(
                            'LC__SETTINGS__CMDB__VALIDATION_MESSAGE__NEEDS_TO_BE_REGEX',
                            $options['options']['regexp']
                        );
                        break;

                    case FILTER_VALIDATE_EMAIL:
                        $validationErrorMessage = 'LC__SETTINGS__CMDB__VALIDATION_MESSAGE__NEEDS_TO_BE_EMAIL';
                        break;

                    case FILTER_VALIDATE_URL:
                        $validationErrorMessage = 'LC__SETTINGS__CMDB__VALIDATION_MESSAGE__NEEDS_TO_BE_URL';
                        break;

                    default:
                        $validationErrorMessage = 'LC__UNIVERSAL__FIELD_VALUE_IS_INVALID';
                        break;
                }

                return $this->language->get($validationErrorMessage);
            }
        }
        return null;
    }

    /**
     * @param mixed $value
     * @param Property $property
     * @param Property[] $properties
     * @param isys_cmdb_dao_category $categoryDao
     *
     * @return string|null
     */
    private function validateUniqueChecks($value, Property $property, array $properties, isys_cmdb_dao_category $categoryDao): ?string
    {
        $tablePrepend = $this->tablePrepend;
        $objectTypeId = $categoryDao->get_object_type_id();
        $categoryTable = $categoryDao->get_table();
        $result = false;
        $entryId = null;
        $dataField = $property->getData()->getFieldAlias() ?: $property->getData()->getField();
        $propertyChecks = $property->getCheck();
        $isCustomCategory = $categoryDao instanceof isys_cmdb_dao_category_g_custom_fields;

        if ($isCustomCategory) {
            $dataField = 'isys_catg_custom_fields_list__field_content';
            $entryId = 0;
        }

        try {
            $validationErrorMessage = 'LC__SETTINGS__CMDB__VALIDATION_MESSAGE__UNIQUE_GLOBAL';

            if ($propertyChecks->getUniqueGlobal()) {
                $validationErrorMessage = 'LC__SETTINGS__CMDB__VALIDATION_MESSAGE__UNIQUE_GLOBAL';
                $result = $categoryDao->get_data(
                    $entryId,
                    null,
                    'AND BINARY ' . $tablePrepend . $dataField . ' = ' . $categoryDao->convert_sql_text($value),
                    null,
                    C__RECORD_STATUS__NORMAL
                );
            } elseif ($propertyChecks->getUniqueObjectType() && $objectTypeId > 0) {
                $validationErrorMessage = 'LC__SETTINGS__CMDB__VALIDATION_MESSAGE__UNIQUE_OBJTYPE';
                $result = $categoryDao->get_data(
                    $entryId,
                    null,
                    'AND isys_obj__isys_obj_type__id = ' . $categoryDao->convert_sql_id($objectTypeId) . ' AND BINARY ' . $tablePrepend . $dataField . ' = ' . $categoryDao->convert_sql_text($value),
                    null,
                    C__RECORD_STATUS__NORMAL
                );
            }

            if ($result !== false && is_countable($result) && count($result) > 0) {
                $objects = [];

                while ($dataset = $result->get_row()) {
                    if (isset($dataset['isys_obj__status']) && $dataset['isys_obj__status'] != C__RECORD_STATUS__NORMAL) {
                        continue;
                    }

                    // @see AOF-54 Removed condition to always record 'unique' errors.
                    $objects[] = $this->language->get($dataset['isys_obj_type__title']) . ' > ' . $dataset['isys_obj__title'];

                    if (isset($dataset[$categoryTable . '__id'])) {
                        // @see  ID-8333  Custom categories use their own 'data id' instead of the table entry ID.
                        $targetField = $isCustomCategory
                            ? 'isys_catg_custom_fields_list__data__id'
                            : $categoryTable . '__id';

                        if ($categoryDao->get_list_id() > 0 && $dataset[$targetField] == $categoryDao->get_list_id()) {
                            array_pop($objects);
                        }
                    }
                }

                $objects = array_unique($objects);

                if ($objectCount = count($objects)) {
                    if ($objectCount > isys_cmdb_dao_category::UNIQUE_VALIDATION_OBJECT_COUNT) {
                        $objects = array_slice($objects, 0, isys_cmdb_dao_category::UNIQUE_VALIDATION_OBJECT_COUNT);
                        $objects[] = $this->language->get(
                            'LC__SETTINGS__CMDB__VALIDATION_MESSAGE__UNIQUE_AND_MORE',
                            ($objectCount - isys_cmdb_dao_category::UNIQUE_VALIDATION_OBJECT_COUNT)
                        );
                    }

                    return $this->language->get($validationErrorMessage) . implode(', ', $objects);
                }
            }
        } catch (isys_exception_database $e) {
            return $e->getMessage();
        }
        return null;
    }

    /**
     * @param Property $property
     *
     * @return bool
     */
    private function isValidatable(Property $property)
    {
        $propertyChecks = $property->getCheck();
        if ((($propertyChecks->getValidationType() === null || $propertyChecks->getValidationType() === false)
          && $propertyChecks->isMandatory() === null
          && $propertyChecks->getUniqueObject() === null
          && $propertyChecks->getUniqueObjectType() === null
          && $propertyChecks->getUniqueGlobal() === null)
          || $property->getProvides()->isVirtual()
        ) {
            return false;
        }

        return true;
    }
}
