<?php

namespace Limb_Chatbot\Includes\Services;

use Limb_Chatbot\Includes\Data_Objects\Chatbot;
use Limb_Chatbot\Includes\Data_Objects\Chatbot_User;
use Limb_Chatbot\Includes\Data_Objects\Limit;
use Limb_Chatbot\Includes\Exceptions\Error_Codes;
use Limb_Chatbot\Includes\Exceptions\Exception;
use Limb_Chatbot\Includes\Factories\Limit_Factory;
use Limb_Chatbot\Includes\Interfaces\Chatbot_Parameter_Parser_Interface;

/**
 * Parses, validates, sanitizes, and prepares chatbot usage limits.
 *
 * @since 1.0.0
 */
class Chatbot_Limits_Parser implements Chatbot_Parameter_Parser_Interface {

	/**
	 * Current chatbot user, if available.
	 *
	 * @var Chatbot_User|null
	 * @since 1.0.0
	 */
	private ?Chatbot_User $chatbot_user;

	/**
	 * Chatbot_Limits_Parser constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		$this->chatbot_user = User_Manager::instance()->get_current_user();
	}

	/**
	 * Parses raw limit input data into a collection of Limit objects.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $value Raw input limits data.
	 * @param Chatbot $chatbot Chatbot instance.
	 * @return Collection|null Collection of parsed limits or null.
	 */
	public function parse( $value, Chatbot $chatbot ): ?Collection {
		if ( is_array( $value ) ) {
			$limits = new Collection();
			foreach ( $value as $type => $limit_group ) {
				if ( ! is_array( $limit_group ) ) {
					continue;
				}
				foreach ( $limit_group as $limit ) {
					if ( ( $object = ( new Limit_Factory() )->make( $type ) ) && class_exists( $object ) ) {
						$object = $object::make( $this->prepare_data( $type, $limit, $chatbot ) );
						$limits->push_item( $object );
					}
				}
			}
		}

		return $limits ?? null;
	}

	/**
	 * Validates the parsed limits structure and data.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $limits Limits data to validate.
	 * @return bool True if valid, otherwise throws Exception.
	 * @throws Exception When limits data is invalid.
	 */
	public function validate( $limits ): bool {
		$allowed_types = [
			Limit::TYPE_USER,
			Limit::TYPE_INPUT,
			Limit::TYPE_GUEST,
			Limit::TYPE_OUTPUT,
			Limit::TYPE_GENERAL
		];
		foreach ( $limits as $type => $limit_group ) {
			if ( ! in_array( $type, $allowed_types, true ) ) {
				throw new Exception( Error_Codes::VALIDATION_INVALID_VALUE, __( 'Invalid limit type', 'limb-chatbot' ) );
			}
			if ( ! is_array( $limit_group ) ) {
				continue;
			}
			foreach ( $limit_group as $limit ) {
				if ( ! is_array( $limit ) ) {
					throw new Exception( Error_Codes::VALIDATION_INVALID_VALUE, __( 'Invalid limit', 'limb-chatbot' ) );
				}
				if ( ! $this->is_valid_limit( $limit ) ) {
					throw new Exception( Error_Codes::VALIDATION_INVALID_VALUE, __( 'Invalid limit', 'limb-chatbot' ) );
				}
			}
		}

		return true;
	}

	/**
	 * Checks if a single limit entry is valid.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $limit Limit data.
	 * @return bool True if valid, false otherwise.
	 */
	private function is_valid_limit( $limit ): bool {
		return isset( $limit['value'] ) && is_numeric( $limit['value'] )
		       && isset( $limit['unit'] )
		       && in_array( $limit['unit'], [
				Limit::UNIT_TOKEN,
				Limit::UNIT_COST,
				Limit::UNIT_NEW_CHAT,
				Limit::UNIT_MESSAGE_PER_CHAT
			] )
		       && isset( $limit['period'] )
		       && is_numeric( $limit['period'] )
		       && ( ( isset( $limit['status'] ) && in_array( $limit['status'], [ Limit::STATUS_ENABLED, Limit::STATUS_DISABLED ] ) ) || ! isset( $limit['status'] ) );
	}

	/**
	 * Sanitizes input limits array by trimming and cleaning keys and values.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $limits Limits data to sanitize.
	 * @return array Sanitized limits array.
	 */
	public function sanitize( $limits ): array {
		$sanitized = [];
		foreach ( (array) $limits as $type => $limit_group ) {
			$type = is_string( $type ) ? trim( strip_tags( $type ) ) : '';
			if ( ! is_array( $limit_group ) ) {
				continue;
			}
			$sanitized_group = [];
			foreach ( $limit_group as $limit ) {
				if ( ! is_array( $limit ) ) {
					continue;
				}
				$clean_limit = [];
				foreach ( $limit as $key => $value ) {
					$key = sanitize_text_field( $key );
					if ( is_scalar( $value ) ) {
						// TODO below float casting removed, kept poor sanitization
						$clean_limit[ $key ] = sanitize_text_field( $value );
					}
				}
				if ( ! empty( $clean_limit ) ) {
					$sanitized_group[] = $clean_limit;
				}
			}
			if ( ! empty( $sanitized_group ) ) {
				$sanitized[ $type ] = $sanitized_group;
			}
		}

		return $sanitized;
	}

	/**
	 * Prepares limit data by adding chatbot and user info if applicable.
	 *
	 * @since 1.0.0
	 *
	 * @param string $type Limit type.
	 * @param array $limit Limit data.
	 * @param Chatbot $chatbot Chatbot instance.
	 * @return array Prepared data array.
	 */
	private function prepare_data( $type, $limit, $chatbot ): array {
		$data = array_merge( $limit, [ 'chatbot' => $chatbot ] );
		if ( in_array( $type, array( Limit::TYPE_GUEST, Limit::TYPE_USER ) ) ) {
			$data = array_merge( $data, [ 'chatbot_user' => $this->chatbot_user ] );
		}

		return $data;
	}
}
