<?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;


/**
 * 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 ) {
		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();
		}

		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' ) {
			return $this->remove_preview_settings();
		}

		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;
	}
}