<?php

namespace Limb_Chatbot\Includes\Repositories;

use Limb_Chatbot\Includes\Data_Objects\Chatbot;
use Limb_Chatbot\Includes\Data_Objects\Setting;
use Limb_Chatbot\Includes\Exceptions\Error_Codes;
use Limb_Chatbot\Includes\Exceptions\Exception;
use Limb_Chatbot\Includes\Services\Helper;
use Limb_Chatbot\Includes\Services\Preview_State;


/**
 * Repository for managing settings storage, retrieval, updates, and deletion.
 *
 * Handles special cases such as preview settings and triggers actions on updates.
 *
 * @since 1.0.0
 */
class Setting_Repository {

	/**
	 * Retrieves settings filtered by key or group.
	 *
	 * @since 1.0.0
	 *
	 * @param array $params Optional. Array of parameters including:
	 *                      - 'key'   => string|array Setting key(s) to filter.
	 *                      - 'group' => string      Prefix group of keys.
	 *
	 * @return Setting[] Array of Setting objects.
	 */
	public function get_items( $params = [] ) {
		$where['option_name'] = ! empty( $params['key'] ) && is_array( $params['key'] ) ? array_map( function ( $item ) {
			return Setting::sanitize_key( $item );
		}, $params['key'] ) : ( ! empty( $params['key'] ) ? Setting::sanitize_key( $params['key'] ) : null );
		if ( empty( $where['option_name'] ) && ! empty( Setting::sanitize_key( $params['group'] ) ) ) {
			$where['option_nameLIKE'] = Setting::sanitize_key( $params['group'] ) . '%';
		}
		if ( is_null( $where['option_name'] ) ) {
			unset( $where['option_name'] );
		}

		return Setting::where( $where )->get();
	}

	/**
	 * Updates a single setting value by key.
	 *
	 * Triggers a hook after update.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key  Setting key.
	 * @param array  $data Array containing 'value' key with new value.
	 *
	 * @throws Exception When update fails.
	 *
	 * @return Setting Updated Setting object.
	 */
	public function update( $key, $data ) {
		$setting = Setting::update( [ 'key' => $key ], [ 'value' => $data['value'] ] );
		if ( ! is_a( $setting, Setting::class ) ) {
			throw new Exception( Error_Codes::TECHNICAL_ERROR, __( 'Failed to create setting', 'limb-chatbot' ) );
		}
		do_action( "{$setting->get_key()}_updated", $setting );

		return $setting;
	}

	/**
	 * Creates multiple settings in batch. Alias for batch_update.
	 *
	 * @since 1.0.0
	 *
	 * @param array    $data    Array of key-value pairs or array of arrays.
	 * @param bool|null $preview Optional flag for preview mode.
	 *
	 * @return Setting[] Array of Setting objects.
	 */
	public function batch_create( array $data, ?bool $preview = false ) {
		return $this->batch_update( $data, $preview );
	}

	/**
	 * Batch updates or creates multiple settings.
	 *
	 * Triggers relevant hooks and handles preview flag logic.
	 *
	 * @since 1.0.0
	 *
	 * @param array    $data    Array of settings to update or create.
	 * @param bool|null $preview Optional flag to control preview mode.
	 *
	 * @return Setting[] Array of updated or created Setting objects.
	 */
	public function batch_update( array $data, ?bool $preview = false ) {
		if ( $preview ) {
			// Merge into single preview state blob instead of creating individual .preview settings.
			$normalized   = $this->normalize_array( $data );
			$preview      = Preview_State::load();
			$parameters   = $preview->get_parameters();
			$widgets_data = $preview->get_widgets();
			$widgets_override = false;

			foreach ( $normalized as $item ) {
				if ( ! isset( $item['key'] ) ) {
					continue;
				}
				$key   = $item['key'];
				$value = $item['value'] ?? null;

				// Existing admin preview UI sends keys with ".preview" postfix.
				// Strip it so the runtime lookup uses the base key.
				if ( str_ends_with( $key, Setting::PREVIEW_POSTFIX ) ) {
					$key = substr( $key, 0, - strlen( Setting::PREVIEW_POSTFIX ) );
				}

				// Special-case widgets payload key: store entire array under widgets.
				if ( 'lbaic.utilities.chatbot.widgets' === $key ) {
					if ( is_array( $value ) ) {
						$widgets_data = $value;
					}
					$widgets_override = true;
					continue;
				}

				// Only accept scalar/array values, matching what Setting would.
				$parameters[ $key ] = $value;
			}

			$preview = $preview
				->with_merged_parameters( $parameters )
				->with_widgets( $widgets_data )
				->with_widgets_override( $widgets_override );
			$preview->save();

			// Do not remove existing preview settings here; caller controls that.

			return array();
		}

		foreach ( $this->normalize_array( $data ) as $item ) {
			$setting        = Setting::find( $item['key'] );
			$setting        = ! is_null( $setting->get_value() ) ? Setting::update( [ 'key' => $item['key'] ], [ 'value' => $item['value'] ] ) : Setting::create( $item );
			$chatbot_prefix = Setting::SETTING_PREFIX . Chatbot::SETTING_PREFIX;
			if ( str_starts_with( $setting->get_key(), $chatbot_prefix ) ) {
				$parameter = str_replace( $chatbot_prefix, '', $setting->get_key() );
				if ( str_ends_with( $parameter, Setting::PREVIEW_POSTFIX ) ) {
					$parameter = str_replace( Setting::PREVIEW_POSTFIX, '', $parameter );
					do_action( 'lbaic_parameter_' . $parameter . '_preview_updated', $item );
				} else {
					do_action( 'lbaic_parameter_' . $parameter . '_updated', $item );
				}
			}
			do_action( "{$item['key']}_updated", $setting );
			$settings[] = $setting;
		}
		if ( ! $preview ) {
			$this->remove_preview_settings();
			Preview_State::clear();
		}

		return $settings ?? [];
	}

	/**
	 * Creates a new setting.
	 *
	 * @since 1.0.0
	 *
	 * @param array $params Associative array of setting parameters.
	 *
	 * @throws Exception When creation fails.
	 *
	 * @return Setting Newly created Setting object.
	 */
	public function create( array $params ) {
		$setting = Setting::create( $params );
		if ( ! is_a( $setting, Setting::class ) ) {
			throw new Exception( Error_Codes::TECHNICAL_ERROR, __( 'Failed to create setting', 'limb-chatbot' ) );
		}
		do_action( "{$setting->get_key()}_updated", $setting );

		return $setting;
	}

	/**
	 * Deletes settings or entries within array-based setting values.
	 *
	 * If 'uuid' is provided in filter params, deletes only the matching entry inside the array value.
	 * Also deletes the associated uploaded file if it exists.
	 *
	 * @since 1.0.0
	 *
	 * @param array      $params        Parameters containing 'key' and optionally others.
	 * @param array|null $filter_params Optional filters such as 'uuid'.
	 *
	 * @return bool True on success, false otherwise.
	 */
	public function delete( array $params, ?array $filter_params = [] ): bool {
		if ( $uuid = $filter_params['uuid'] ?? null ) {
			if ( $value = Setting::find( $params['key'] )->get_value() ) {
				if ( is_array( $value ) ) {
					$matched_item = null;
					$value        = array_filter( $value, function ( $item ) use ( $uuid, &$matched_item ) {
						if ( $item['uuid'] !== $uuid ) {
							return true;
						}
						$matched_item = $item;

						return false;
					} );
					$value        = array_values( $value );
					if ( $matched_item ) {
						$file_path = Helper::get_wp_uploaded_file_dir( $matched_item['file_path'] );
						if ( file_exists( $file_path ) ) {
							wp_delete_file( $file_path );
						}
					}
					if ( ! empty( $value ) ) {
						return Setting::update( [ 'key' => $params['key'] ], [ 'value' => $value ] ) instanceof Setting;
					} else {
						return Setting::delete( [ 'key' => $params['key'] ] );
					}
				}
			}
		} else {
			return Setting::delete( [ 'key' => $params['key'] ] );
		}

		return false;
	}

	/**
	 * Deletes all preview settings if suffix param is set.
	 *
	 * @since 1.0.0
	 *
	 * @param array $params Optional params with 'suffix' key.
	 *
	 * @return bool True on success.
	 */
	public function delete_all( $params = [] ) {
		if ( isset( $params['suffix'] ) && $params['suffix'] === 'preview' ) {
			$this->remove_preview_settings();
			Preview_State::clear();

			return true;
		}

		return true;
	}

	/**
	 * Removes all settings with keys ending in '.preview'.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True on success.
	 */
	public function remove_preview_settings(): bool {
		// TODO make more flexible ::delete method needed to call only ::delete and that's it. Start supporting double LIKE params support
		$settings = Setting::where( [ 'option_nameLIKE' => '%.preview' ] )->get();
		foreach ( $settings as $setting ) {
			if ( str_starts_with( $setting->get_key(), Setting::SETTING_PREFIX ) ) {
				$deleted = Setting::delete( [ 'key' => $setting->get_key() ] );
			}
		}

		return $deleted ?? true;
	}

	/**
	 * Normalizes an associative array of key-value pairs into an array of ['key' => ..., 'value' => ...] arrays.
	 *
	 * @since 1.0.0
	 *
	 * @param array $metas The input array (associative or indexed).
	 *
	 * @return array Normalized array ready for batch processing.
	 */
	public function normalize_array( array $metas ): array {
		if ( Helper::is_assoc( $metas ) ) {
			return array_map( fn( $key, $value ) => [ 'key' => $key, 'value' => $value ], array_keys( $metas ), $metas );
		}

		return $metas;
	}
}