<?php declare(strict_types = 0);
/*
** Copyright (C) 2001-2026 Zabbix SIA
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see <https://www.gnu.org/licenses/>.
**/


namespace Zabbix\Widgets;

use CApiInputValidator,
	DB;

abstract class CWidgetField {

	public const DEFAULT_VIEW = null;

	public const FLAG_NOT_EMPTY = 0x02;
	public const FLAG_LABEL_ASTERISK = 0x04;
	public const FLAG_DISABLED = 0x08;

	public const FOREIGN_REFERENCE_KEY = '_reference';
	public const REFERENCE_DASHBOARD = 'DASHBOARD';

	protected string $name;
	protected ?string $label;
	protected ?string $label_prefix = null;

	protected ?int $save_type = null;

	protected $value;
	protected $default;

	protected array $values_captions = [];
	protected string $inaccessible_caption = '';

	protected int $max_length;

	protected ?string $action = null;

	protected int $flags = 0x00;

	private array $validation_rules = [];

	private $templateid = null;

	private bool $default_prevented = false;
	private bool $widget_accepted = false;
	private bool $dashboard_accepted = false;

	private string $in_type = '';

	/**
	 * @param string      $name   Field name in form.
	 * @param string|null $label  Label for the field in form.
	 */
	public function __construct(string $name, ?string $label = null) {
		$this->name = $name;
		$this->label = $label;
		$this->value = null;
		$this->setSaveType(ZBX_WIDGET_FIELD_TYPE_STR);
	}

	public function getName(): string {
		return $this->name;
	}

	public function getLabel(): ?string {
		return $this->label;
	}

	/**
	 * Prefix field label to enhance clarity in case of error messages. For example:
	 * Invalid parameter "<LABEL PREFIX>: <LABEL>": too many decimal places.
	 */
	public function prefixLabel(?string $prefix): static {
		$this->label_prefix = $prefix;

		return $this;
	}

	/**
	 * Get fully qualified label for displaying in error messages.
	 *
	 * @return string
	 */
	public function getErrorLabel(): string {
		$label = $this->label ?? $this->name;

		if ($this->label_prefix !== null) {
			$label = $this->label_prefix.': '.$label;
		}

		return $label;
	}

	/**
	 * Get field value. If no value is set, return the default value.
	 *
	 * @return mixed
	 */
	public function getValue() {
		return $this->value ?? $this->getDefault();
	}

	public function setValue($value): self {
		$this->value = $value;

		return $this;
	}

	public function getValuesCaptions(): array {
		return $this->values_captions;
	}

	public function setValuesCaptions(array $captions): self {
		$values = [];
		$this->toApi($values);

		$inaccessible = 0;
		foreach ($values as $value) {
			if (array_key_exists($value['type'], $captions)) {
				$this->values_captions[$value['value']] = array_key_exists($value['value'], $captions[$value['type']])
					? $captions[$value['type']][$value['value']]
					: [
						'id' => $value['value'],
						'name' => $this->inaccessible_caption.(++$inaccessible > 1 ? ' ('.$inaccessible.')' : ''),
						'inaccessible' => true
					];
			}
		}

		return $this;
	}

	public function getDefault() {
		return $this->default;
	}

	public function setDefault($value): self {
		$this->default = $value;

		return $this;
	}

	public function isDefaultPrevented(): bool {
		return $this->default_prevented;
	}

	/**
	 * Disable exact object selection, like item or host.
	 *
	 * @return $this
	 */
	public function preventDefault($default_prevented = true): self {
		$this->default_prevented = $default_prevented;

		return $this;
	}

	public function isWidgetAccepted(): bool {
		return $this->widget_accepted;
	}

	/**
	 * Allow selecting widget as reference.
	 *
	 * @return $this
	 */
	public function acceptWidget($widget_accepted = true): self {
		$this->widget_accepted = $widget_accepted;

		return $this;
	}

	public function isDashboardAccepted(): bool {
		return $this->dashboard_accepted;
	}

	/**
	 * Allow selecting dashboard as reference.
	 *
	 * @return $this
	 */
	public function acceptDashboard($dashboard_accepted = true): self {
		$this->dashboard_accepted = $dashboard_accepted;

		return $this;
	}

	public function setInType(string $in_type): self {
		$this->in_type = $in_type;

		return $this;
	}

	public function getInType(): string {
		return $this->in_type;
	}

	public function getAction(): ?string {
		return $this->action;
	}

	/**
	 * Set JS code that will be called on field change.
	 *
	 * @param string $action  JS function to call on field change.
	 */
	public function setAction(string $action): self {
		$this->action = $action;

		return $this;
	}

	public function getMaxLength(): int {
		return $this->max_length;
	}

	public function setMaxLength(int $max_length): self {
		$this->max_length = $max_length;

		$this->validation_rules['length'] = $this->max_length;

		return $this;
	}

	/**
	 * Get additional flags, which can be used in configuration form.
	 */
	public function getFlags(): int {
		return $this->flags;
	}

	/**
	 * Set additional flags, which can be used in configuration form.
	 */
	public function setFlags(int $flags): self {
		$this->flags = $flags;

		return $this;
	}

	/**
	 * @return int|string|null
	 */
	public function getTemplateId() {
		return $this->templateid;
	}

	public function setTemplateId($templateid): self {
		$this->templateid = $templateid;

		return $this;
	}

	public function isTemplateDashboard(): bool {
		return $this->templateid !== null;
	}

	/**
	 * @param bool $strict  Widget form submit validation?
	 *
	 * @return array  Errors.
	 */
	public function validate(bool $strict = false): array {
		$errors = [];

		$validation_rules = $this->getValidationRules($strict);
		$value = $this->getValue();
		$label = $this->getErrorLabel();

		if (CApiInputValidator::validate($validation_rules, $value, $label, $error)) {
			$this->setValue($value);
		}
		else {
			$this->setValue($this->getDefault());
			$errors[] = $error;
		}

		return $errors;
	}

	/**
	 * Prepares array entry for widget field, ready to be passed to CDashboard API functions.
	 * Reference is needed here to avoid array merging in CWidgetForm::fieldsToApi method. With large number of widget
	 * fields it causes significant performance decrease.
	 *
	 * @param array $widget_fields  reference to Array of widget fields.
	 */
	public function toApi(array &$widget_fields = []): void {
		$value = $this->getValue();

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

		if (is_array($value)) {
			$value = array_values($value);
			$default = $this->getDefault();

			if (!is_array($default) || $value !== array_values($default)) {
				foreach ($value as $index => $each_value) {
					$widget_fields[] = [
						'type' => $this->save_type,
						'name' => $this->name.'.'.$index,
						'value' => $each_value
					];
				}
			}
		}
		elseif ($value !== $this->getDefault()) {
			$widget_fields[] = [
				'type' => $this->save_type,
				'name' => $this->name,
				'value' => $value
			];
		}
	}

	protected function setSaveType($save_type): self {
		switch ($save_type) {
			case ZBX_WIDGET_FIELD_TYPE_INT32:
				$this->validation_rules = ['type' => API_INT32];
				break;

			case ZBX_WIDGET_FIELD_TYPE_STR:
				$this->max_length = DB::getFieldLength('widget_field', 'value_str');

				$this->validation_rules = [
					'type' => API_STRING_UTF8,
					'length' => $this->max_length
				];
				break;

			case ZBX_WIDGET_FIELD_TYPE_GROUP:
			case ZBX_WIDGET_FIELD_TYPE_HOST:
			case ZBX_WIDGET_FIELD_TYPE_ITEM:
			case ZBX_WIDGET_FIELD_TYPE_ITEM_PROTOTYPE:
			case ZBX_WIDGET_FIELD_TYPE_GRAPH:
			case ZBX_WIDGET_FIELD_TYPE_GRAPH_PROTOTYPE:
			case ZBX_WIDGET_FIELD_TYPE_MAP:
			case ZBX_WIDGET_FIELD_TYPE_SERVICE:
			case ZBX_WIDGET_FIELD_TYPE_SLA:
			case ZBX_WIDGET_FIELD_TYPE_USER:
			case ZBX_WIDGET_FIELD_TYPE_ACTION:
			case ZBX_WIDGET_FIELD_TYPE_MEDIA_TYPE:
				$this->validation_rules = ['type' => API_IDS];
				break;

			default:
				exit(_('Internal error.'));
		}

		$this->save_type = $save_type;

		return $this;
	}

	protected function getValidationRules(bool $strict = false): array {
		return $this->validation_rules;
	}

	protected function setValidationRules(array $validation_rules): self {
		$this->validation_rules = $validation_rules;

		return $this;
	}

	/**
	 * Set additional flags for validation rule array.
	 */
	protected static function setValidationRuleFlag(array &$validation_rule, int $flag): void {
		if (array_key_exists('flags', $validation_rule)) {
			$validation_rule['flags'] |= $flag;
		}
		else {
			$validation_rule['flags'] = $flag;
		}
	}

	/**
	 * Parse typed reference (a reference to a foreign data source).
	 *
	 * @param string $typed_reference
	 *
	 * @return array
	 */
	public static function parseTypedReference(string $typed_reference): array {
		$separator_index = strpos($typed_reference, '.');

		if ($separator_index === false) {
			return ['reference' => '', 'type' => ''];
		}

		return [
			'reference' => substr($typed_reference, 0, $separator_index),
			'type' => substr($typed_reference, $separator_index + 1)
		];
	}

	/**
	 * Create a typed reference (a reference to a foreign data source).
	 *
	 * @param string $reference
	 * @param string $type
	 *
	 * @return string
	 */
	public static function createTypedReference(string $reference, string $type = ''): string {
		return $type !== '' ? $reference.'.'.$type : $reference;
	}
}
