<?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.
	 *
	 * Expects the new structure where each type maps to:
	 *   { "units": [ ... ], "errors": { "rate_limit": "...", "input_limit": "..." } }
	 *
	 * @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;
				}

				$units  = isset( $limit_group['units'] ) && is_array( $limit_group['units'] ) ? $limit_group['units'] : array();
				$errors = isset( $limit_group['errors'] ) && is_array( $limit_group['errors'] ) ? $limit_group['errors'] : array();

				foreach ( $units as $limit ) {
					if ( ( $object = ( new Limit_Factory() )->make( $type ) ) && class_exists( $object ) ) {
						$error_message = $this->resolve_error_message( $errors, (int) ( $limit['unit'] ?? 0 ) );
						$object        = $object::make( $this->prepare_data( $type, $limit, $chatbot, $error_message ) );
						$limits->push_item( $object );
					}
				}
			}
		}

		return $limits ?? null;
	}

	/**
	 * Validates the parsed limits structure and data.
	 *
	 * Expects each limit group to contain a "units" array and an optional "errors" object.
	 *
	 * @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 {
		$wp_role_keys  = array_map( 'sanitize_key', array_keys( wp_roles()->roles ) );
		$allowed_types = array_merge(
			[
				Limit::TYPE_INPUT,
				Limit::TYPE_GUEST,
				Limit::TYPE_GENERAL,
			],
			$wp_role_keys
		);
		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;
			}

			// Validate "units" sub-key.
			if ( ! isset( $limit_group['units'] ) || ! is_array( $limit_group['units'] ) ) {
				throw new Exception( Error_Codes::VALIDATION_INVALID_VALUE, __( 'Invalid limit group structure: missing units', 'limb-chatbot' ) );
			}

			foreach ( $limit_group['units'] 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' ) );
				}
			}

			// Validate optional "errors" sub-key.
			if ( isset( $limit_group['errors'] ) ) {
				if ( ! $this->is_valid_errors( $limit_group['errors'] ) ) {
					throw new Exception( Error_Codes::VALIDATION_INVALID_VALUE, __( 'Invalid limit errors', '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( (int) $limit['unit'], [
				Limit::UNIT_NEW_CHAT,
				Limit::UNIT_WORD,
				Limit::UNIT_MESSAGE_PER_DAY,
			], true )
		       && isset( $limit['period'] )
		       && is_numeric( $limit['period'] )
		       && ( ( isset( $limit['status'] ) && in_array( (int) $limit['status'], [ Limit::STATUS_ENABLED, Limit::STATUS_DISABLED ], true ) ) || ! isset( $limit['status'] ) );
	}

	/**
	 * Sanitizes input limits array by trimming and cleaning keys and values.
	 *
	 * Handles the new structure: { "units": [ ... ], "errors": { ... } }
	 *
	 * @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_entry = [];

			// Sanitize "units" sub-key.
			$units           = isset( $limit_group['units'] ) && is_array( $limit_group['units'] ) ? $limit_group['units'] : array();
			$sanitized_units = [];
			$integer_keys    = array( 'unit', 'value', 'period', 'status' );
			foreach ( $units as $limit ) {
				if ( ! is_array( $limit ) ) {
					continue;
				}
				$clean_limit = [];
				foreach ( $limit as $key => $value ) {
					$key = sanitize_text_field( $key );
					if ( is_scalar( $value ) ) {
						$clean_limit[ $key ] = in_array( $key, $integer_keys, true ) && is_numeric( $value )
							? (int) $value
							: sanitize_text_field( $value );
					}
				}
				if ( ! empty( $clean_limit ) ) {
					$sanitized_units[] = $clean_limit;
				}
			}
			$sanitized_entry['units'] = $sanitized_units;

			// Sanitize "errors" sub-key.
			$errors           = isset( $limit_group['errors'] ) && is_array( $limit_group['errors'] ) ? $limit_group['errors'] : array();
			$sanitized_errors = [];
			$allowed_keys     = [ Limit::ERROR_KEY_RATE_LIMIT, Limit::ERROR_KEY_INPUT_LIMIT ];
			foreach ( $allowed_keys as $error_key ) {
				if ( isset( $errors[ $error_key ] ) && is_string( $errors[ $error_key ] ) ) {
					$sanitized_errors[ $error_key ] = sanitize_text_field( $errors[ $error_key ] );
				}
			}
			$sanitized_entry['errors'] = $sanitized_errors;

			if ( ! empty( $sanitized_units ) ) {
				$sanitized[ $type ] = $sanitized_entry;
			}
		}

		return $sanitized;
	}

	/**
	 * Validates the "errors" object within a limit group.
	 *
	 * Only "rate_limit" and "input_limit" string keys are allowed.
	 *
	 * @since 1.2.0
	 *
	 * @param mixed $errors Errors data to validate.
	 * @return bool True if valid.
	 */
	private function is_valid_errors( $errors ): bool {
		if ( ! is_array( $errors ) ) {
			return false;
		}

		$allowed_keys = [ Limit::ERROR_KEY_RATE_LIMIT, Limit::ERROR_KEY_INPUT_LIMIT ];

		foreach ( $errors as $key => $value ) {
			if ( ! in_array( $key, $allowed_keys, true ) ) {
				return false;
			}
			if ( ! is_string( $value ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Resolves the error message for a specific unit from the group-level errors.
	 *
	 * @since 1.2.0
	 *
	 * @param array $errors Group-level errors array.
	 * @param int   $unit   The unit constant.
	 * @return string Resolved error message or empty string.
	 */
	private function resolve_error_message( array $errors, int $unit ): string {
		$error_key = Limit::resolve_error_key_for_unit( $unit );

		return isset( $errors[ $error_key ] ) && is_string( $errors[ $error_key ] ) ? $errors[ $error_key ] : '';
	}

	/**
	 * Prepares limit data by adding chatbot, user info, and resolved error message.
	 *
	 * @since 1.0.0
	 *
	 * @param string $type          Limit type.
	 * @param array  $limit         Limit data.
	 * @param Chatbot $chatbot      Chatbot instance.
	 * @param string $error_message Resolved error message for this unit.
	 * @return array Prepared data array.
	 */
	private function prepare_data( $type, $limit, $chatbot, string $error_message = '' ): array {
		$data      = array_merge( $limit, [
			'chatbot'       => $chatbot,
			'error_message' => $error_message,
		] );
		$role_keys = array_map( 'sanitize_key', array_keys( wp_roles()->roles ) );

		if ( in_array( $type, [ Limit::TYPE_GUEST, Limit::TYPE_USER ], true )
		     || in_array( $type, $role_keys, true ) ) {
			$data = array_merge( $data, [ 'chatbot_user' => $this->chatbot_user ] );
		}
		if ( in_array( $type, $role_keys, true ) ) {
			$data = array_merge( $data, [ 'role' => $type ] );
		}

		return $data;
	}
}
