<?php
declare( strict_types=1 );

namespace FiboFilters\Indexer\Filters;

use FiboFilters\Filters\FilterSources;
use FiboFilters\Filters\StoreFilters;
use FiboFilters\Index\Descriptors;
use FiboFilters\Index\Documents;
use FiboFilters\Index\Sources;
use FiboFilters\Index\Values;
use FiboFilters\Indexer\AbstractDocument;
use FiboFilters\Integrations\Multilingual;
use FiboFilters\Struct\QueryContext;
use FiboFilters\Util\Cache;
use FiboFilters\Util\DbUtil;
use FiboFilters\Util\FilterUtil;
use FiboFilters\WPDBException;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// @TODO możliwośc załadowania danymi, które już są zorganizowane. Wtedy nie wywoływałaby sie metoda organizeDataToIndex().

/**
 * Class Document
 */
class Document extends AbstractDocument {

	/** @var Cache */
	private $cache;

	/** @var FilterSources */
	private $filter_sources;

	/** @var StoreFilters */
	private $store_filters;

	/** @var Multilingual */
	protected $multilingual;

	/** @var Descriptors */
	private Descriptors $descriptors;

	/** @var Documents */
	private Documents $documents;

	/** @var Sources */
	private Sources $sources;

	/** @var Values */
	private Values $values;

	public function __construct(
		array $data,
		array $config,
		Multilingual $multilingual,
		DbUtil $db_util,
		Cache $cache,
		StoreFilters $store_filters,
		FilterSources $filter_sources,
		Descriptors $descriptors,
		Documents $documents,
		Sources $sources,
		Values $values
	) {
		parent::__construct( $data, $db_util, $multilingual, $config );

		$this->cache          = $cache;
		$this->store_filters  = $store_filters;
		$this->filter_sources = $filter_sources;
		$this->descriptors    = $descriptors;
		$this->documents      = $documents;
		$this->sources        = $sources;
		$this->values         = $values;
	}

	/**
	 * Save values
	 *
	 * @param array $excluded_filter_types Filter types that whose values should not be indexed.
	 *
	 * @return array[]
	 *
	 * @throws WPDBException DB exception.
	 */
	public function save_valuelist( array $excluded_filter_types = [] ): array {
		$values = [];
		$ctx    = new QueryContext( $this->get_language(), $this->get_index_role(), true );

		foreach ( $this->data_to_index as $source_name => $segment ) {
			$no_save = false;

			// Don't index values that should be ignored due to they only play a supporting role.
			if ( empty( $segment ) || $source_name === 'product_type' || $source_name === 'parent_id' ) {
				continue;
			}

			// Don't index values from the excluded types e.g. range.
			$filter_type = $this->store_filters->get_filter_type_by_source_name( $source_name );
			if ( in_array( $filter_type, $excluded_filter_types, true ) ) {
				$no_save = true;
			}

			// If the source is used for ordering, it should be indexed, but not saved in table with values.
			$sources_for_ordering = $this->filter_sources->get_sources_for_ordering();
			if ( $filter_type === false && in_array( $source_name, array_keys( $sources_for_ordering ), true ) ) {
				$filter_type = $sources_for_ordering[ $source_name ]['type'];
				$no_save     = true;
			}
			if ( $filter_type === false && $source_name === 'order_by_featured' ) {
				$filter_type = 'range';
				$no_save     = true;
			}
			if ( $filter_type === false && $source_name === 'order_by_stock_status' ) {
				$filter_type = 'range';
				$no_save     = true;
			}
			// Support for multiple currencies.
			if ( $filter_type === false && preg_match( '/^price_[A-Z]{3}$/', $source_name ) ) {
				$filter_type = 'range';
			}
			if ( $filter_type === false && $source_name === 'cf__stock_status' && apply_filters( 'fibofilters/indexer/filters_source_query/exclude_out_of_stock_variations_while_filtering', false ) ) {
				$filter_type = '';
			}

			$source_id = $this->sources->get_source_id_or_create( $source_name, $ctx );

			$values[ $source_id ] = [];

			foreach ( $segment as $value ) {
				if ( $no_save ) {
					$values[ $source_id ][] = [
						'value_id' => 0,
						'value'    => $value['value'],
					];
					continue;
				}
				// Limit label length to prevent SQL error (max. 200 chars in column: "label").
				if ( mb_strlen( $value['label'] ) > 200 ) {
					$value['label'] = mb_substr( $value['label'], 0, 200 );
				}
				// Limit value length to prevent SQL error (max. 200 chars in column: "value").
				if ( mb_strlen( $value['value'] ) > 200 ) {
					$value['value'] = mb_substr( $value['value'], 0, 200 );
				}

				$data = [
					'value'        => $value['value'],
					'max_value'    => $value['max_value'] ?? '',
					'label'        => $value['label'],
					'label_url'    => ! empty( $value['label_url'] ) ? $value['label_url'] : $this->get_url_label( $value['label'], $value['value'], $filter_type ),
					'parent_value' => $value['parent_value'] ?? '0',
					'source_id'    => $source_id,
					'order'        => $value['order'] ?? 0,
				];

				$value_id = $this->values->get_value_id_or_create( $data, $ctx );

				$value_item = [
					'value_id' => $value_id,
					'value'    => $value['value'],
				];

				// We add the value "max_value" so that it can then be stored in the descriptors.
				if ( $filter_type === 'range' && ! empty( $data['max_value'] ) && $data['value'] !== $data['max_value'] ) {
					$value_item['max_value'] = $data['max_value'];
				}

				$values[ $source_id ][] = $value_item;
			}
		}

		return $values;
	}

	/**
	 * Set final format of the special URL label depends on filter type.
	 *
	 * @param string $label Value label.
	 * @param string $value Value.
	 * @param string $filter_type Type of the filter.
	 *
	 * @return string
	 */
	private function get_url_label( string $label, string $value, string $filter_type ): string {
		switch ( $filter_type ) {
			case 'range':
			case 'search':
				$url_label = '';
				break;
			case 'toggle':
				$url_label = FilterUtil::is_filter_true_value( $value ) ? '1' : '0';
				break;
			default:
				$url_label = FilterUtil::format_url_label( $label );
				break;
		}

		return $url_label;
	}

	/**
	 * Save documents
	 *
	 * @param array $values Doclist values.
	 *
	 * @throws WPDBException DB exception.
	 */
	public function save_doclist( array $values ): void {
		$ctx = new QueryContext( $this->get_language(), $this->get_index_role(), true );

		foreach ( $values as $values_for_source ) {
			foreach ( $values_for_source as $value ) {
				// Skip values that are not saved in the valuelist table.
				if ( $value['value_id'] === 0 ) {
					continue;
				}

				$data = [
					'value_id'          => $value['value_id'],
					'post_id'           => $this->get_id(),
					'post_or_parent_id' => empty( $this->get_parent_id() ) ? $this->get_id() : $this->get_parent_id(),
				];
				$this->documents->save_document( $data, $ctx );
			}
		}
	}

	/**
	 * Save product descriptors
	 *
	 * @param array  $values Document values.
	 * @param string $updated_date Date of update.
	 *
	 * @throws WPDBException DB exception.
	 */
	public function save_descriptors( array $values, string $updated_date = '' ): void {
		$descriptor = [];
		$ctx        = new QueryContext( $this->get_language(), $this->get_index_role(), true );

		foreach ( $values as $source_id => $values_for_source ) {
			$source_name          = $this->sources->get_source_name_by_id( $source_id, $ctx );
			$filter_type          = $this->store_filters->get_filter_type_by_source_name( $source_name );
			$sources_for_ordering = $this->filter_sources->get_sources_for_ordering();
			if ( $filter_type === false && in_array( $source_name, array_keys( $sources_for_ordering ), true ) ) {
				$filter_type = $sources_for_ordering[ $source_name ]['type'];
			}
			if ( $filter_type === false && $source_name === 'order_by_featured' ) {
				$filter_type = 'range';
			}
			if ( $filter_type === false && $source_name === 'order_by_stock_status' ) {
				$filter_type = 'range';
			}
			// Support for multiple currencies.
			if ( $filter_type === false && preg_match( '/^price_[A-Z]{3}$/', $source_name ) ) {
				$filter_type = 'range';
			}

			/**
			 * Special case for range filters with multiple values.
			 *
			 * In the "valuelist" table we have stored two numbers in the "value" and "max_value" columns,
			 * and here we need to split them into two separate values again so that the filtering works
			 * correctly on the frontend.
			 */
			if (
				$filter_type === 'range' &&
				count( $values_for_source ) === 1 &&
				! empty( $values_for_source[0]['max_value'] ) &&
				$values_for_source[0]['value'] !== $values_for_source[0]['max_value']
			) {
				$values_for_source = [
					[
						'value_id' => $values_for_source[0]['value_id'],
						'value'    => $values_for_source[0]['value'],
					],
					[
						'value_id' => $values_for_source[0]['value_id'],
						'value'    => $values_for_source[0]['max_value'],
					],
				];
			}

			$descriptor[ $source_id ] = [];

			foreach ( $values_for_source as $value ) {
				if ( $filter_type === 'range' ) {
					$descriptor[ $source_id ][] = floatval( str_replace( ',', '.', $value['value'] ) );
				} elseif ( $filter_type === 'search' ) {
					$descriptor[ $source_id ][] = $value['value'];
				} else {
					$descriptor[ $source_id ][] = intval( $value['value_id'] );
				}
			}

			if (
				$source_name === 'cf__stock_status' &&
				$this->get_post_type() === 'product_variation' &&
				isset( $values_for_source[0]['value'] ) &&
				$values_for_source[0]['value'] === 'outofstock' &&
				apply_filters( 'fibofilters/indexer/filters_source_query/exclude_out_of_stock_variations_while_filtering', false )
			) {
				// This is a special designation of a variation with out-of-stock status.
				$descriptor[-1] = [];
			}
		}

		$this->descriptors->save_descriptor(
			[
				'post_id'           => $this->get_id(),
				'post_or_parent_id' => $this->get_parent_id(),
				'descriptor'        => wp_json_encode( $descriptor ),
				'deleted'           => false,
				'updated_date'      => $updated_date,
			],
			$ctx
		);
	}

	/**
	 * Remove empty values.
	 */
	private function remove_empty_values(): void {
		$struct = [];

		if ( empty( $this->data_to_index ) ) {
			return;
		}

		foreach ( $this->data_to_index as $col_name => $values ) {
			if ( ! empty( $values ) ) {
				$struct[ $col_name ] = $values;
			}
		}

		$this->data_to_index = $struct;
	}

	/**
	 * @param string $updated_date Date of update.
	 *
	 * @throws WPDBException DB exception.
	 */
	public function delete( string $updated_date = '' ): void {
		$ctx = new QueryContext( $this->get_language(), $this->get_index_role(), true );

		$this->documents->delete_document( $this->get_id(), $ctx );

		if ( ! empty( $updated_date ) ) {
			// Soft-delete.
			$this->descriptors->delete_descriptor(
				[
					'post_id'      => $this->get_id(),
					'updated_date' => $updated_date,
				],
				$ctx
			);
		} else {
			// Hard-delete.
			$this->descriptors->delete_descriptor( [ 'post_id' => $this->get_id() ], $ctx );
		}

		$value_ids = $this->documents->get_all_value_ids( false, $ctx );
		if ( ! empty( $value_ids ) ) {
			$this->values->delete_orphaned_values( $value_ids, $ctx );
		}
	}

	public function prepare_data_to_index(): void {
		$this->remove_empty_values();
	}

	/**
	 * @param string $updated_date Date of update.
	 *
	 * @throws WPDBException DB exception.
	 */
	protected function save_data_to_index( string $updated_date = '' ): void {
		if ( $this->multilingual->is_multilingual() && ! $this->multilingual->is_language_indexable( $this->get_language() ) ) {
			return;
		}
		$values = $this->save_valuelist();
		$this->save_doclist( $values );
		$this->save_descriptors( $values, $updated_date );
	}
}
