<?php
/**
 * Stable JSON AJAX boundary for the auto-reply policy editor.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Admin;

use SUBIN\AutoReplies\AutoReplyConditionEngine;
use SUBIN\AutoReplies\AutoReplyPolicyService;

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

if ( ! class_exists( AutoReplyConditionEngine::class ) ) {
	require_once dirname( __DIR__ ) . '/AutoReplies/AutoReplyConditionEngine.php';
}

/**
 * Authenticates admin requests and delegates all policy work to the service.
 */
class AutoReplyAjaxController {
	const NONCE_ACTION = 'subin_auto_reply_api';
	// JSON may expand each input byte to a six-byte \u00xx sequence.
	const MAX_JSON_STRING_EXPANSION = 6;
	const MAX_FIXED_RESOURCE_BYTES = 262144;
	const MAX_RESOURCE_OVERHEAD_BYTES = ( AutoReplyConditionEngine::MAX_DOCUMENT_BYTES * self::MAX_JSON_STRING_EXPANSION ) + self::MAX_FIXED_RESOURCE_BYTES;
	const MAX_PAYLOAD_BYTES = ( 1048576 * self::MAX_JSON_STRING_EXPANSION ) + self::MAX_RESOURCE_OVERHEAD_BYTES;

	const ACTION_INITIAL_STATE = 'subin_auto_reply_initial_state';
	const ACTION_LIST_DESIGNS  = 'subin_auto_reply_list_designs';
	const ACTION_LOAD_DESIGN   = 'subin_auto_reply_load_design';
	const ACTION_SAVE          = 'subin_auto_reply_save_rule';
	const ACTION_PREVIEW       = 'subin_auto_reply_preview';
	const ACTION_SIMULATE      = 'subin_auto_reply_simulate';
	const ACTION_TEST_SEND     = 'subin_auto_reply_test_send';

	/** @var AutoReplyPolicyService */
	private $service;

	/**
	 * Constructor.
	 *
	 * @param AutoReplyPolicyService $service Service.
	 */
	public function __construct( AutoReplyPolicyService $service ) {
		$this->service = $service;
	}

	/** Register authenticated WordPress AJAX hooks. @return void */
	public function register() {
		add_action( 'wp_ajax_' . self::ACTION_INITIAL_STATE, array( $this, 'ajax_initial_state' ) );
		add_action( 'wp_ajax_' . self::ACTION_LIST_DESIGNS, array( $this, 'ajax_list_designs' ) );
		add_action( 'wp_ajax_' . self::ACTION_LOAD_DESIGN, array( $this, 'ajax_load_design' ) );
		add_action( 'wp_ajax_' . self::ACTION_SAVE, array( $this, 'ajax_save' ) );
		add_action( 'wp_ajax_' . self::ACTION_PREVIEW, array( $this, 'ajax_preview' ) );
		add_action( 'wp_ajax_' . self::ACTION_SIMULATE, array( $this, 'ajax_simulate' ) );
		add_action( 'wp_ajax_' . self::ACTION_TEST_SEND, array( $this, 'ajax_test_send' ) );
	}

	/** Load initial editor state. @return void */
	public function ajax_initial_state() {
		$this->dispatch(
			function () {
				return $this->service->initial_state(
					absint( $this->post_scalar( 'rule_id' ) ),
					sanitize_key( $this->post_scalar( 'provider' ) ),
					sanitize_text_field( $this->post_scalar( 'form_id' ) )
				);
			}
		);
	}

	/** List saved designs with bounded search pagination. @return void */
	public function ajax_list_designs() {
		$this->dispatch(
			function () {
				return $this->service->list_saved_designs(
					sanitize_text_field( $this->post_scalar( 'search' ) ),
					absint( $this->post_scalar( 'page' ) ),
					absint( $this->post_scalar( 'per_page' ) )
				);
			}
		);
	}

	/** Load one selected starter or saved design. @return void */
	public function ajax_load_design() {
		$this->dispatch(
			function () {
				return $this->service->load_design(
					sanitize_key( $this->post_scalar( 'design_type' ) ),
					absint( $this->post_scalar( 'design_id' ) ),
					sanitize_key( $this->post_scalar( 'design_key' ) )
				);
			}
		);
	}

	/** Save or publish one rule. @return void */
	public function ajax_save() {
		$this->dispatch(
			function () {
				$payload = $this->payload();
				if ( is_wp_error( $payload ) ) {
					return $payload;
				}
				$rule = is_array( $payload['rule'] ?? null ) ? $payload['rule'] : null;
				if ( null === $rule ) {
					return $this->request_error( 'missing_rule', __( 'Auto-reply rule data is missing.', 'submission-inbox' ), 400 );
				}
				return $this->service->save( $rule, $payload['intent'] ?? '' );
			}
		);
	}

	/** Render a safe preview. @return void */
	public function ajax_preview() {
		$this->dispatch(
			function () {
				$payload = $this->payload();
				if ( is_wp_error( $payload ) ) {
					return $payload;
				}
				$rule = is_array( $payload['rule'] ?? null ) ? $payload['rule'] : null;
				if ( null === $rule ) {
					return $this->request_error( 'missing_rule', __( 'Auto-reply rule data is missing.', 'submission-inbox' ), 400 );
				}
				$sample = is_array( $payload['sample'] ?? null ) ? $payload['sample'] : array();
				return $this->service->preview( $rule, $sample );
			}
		);
	}

	/** Run a side-effect-free condition simulation. @return void */
	public function ajax_simulate() {
		$this->dispatch(
			function () {
				$payload = $this->payload();
				if ( is_wp_error( $payload ) ) {
					return $payload;
				}
				$rule = is_array( $payload['rule'] ?? null ) ? $payload['rule'] : null;
				if ( null === $rule ) {
					return $this->request_error( 'missing_rule', __( 'Auto-reply rule data is missing.', 'submission-inbox' ), 400 );
				}
				$sample = is_array( $payload['sample'] ?? null ) ? $payload['sample'] : array();
				return $this->service->simulate( $rule, $sample );
			}
		);
	}

	/** Send a rate-limited manual test. @return void */
	public function ajax_test_send() {
		$this->dispatch(
			function () {
				$payload = $this->payload();
				if ( is_wp_error( $payload ) ) {
					return $payload;
				}
				$rule = is_array( $payload['rule'] ?? null ) ? $payload['rule'] : null;
				if ( null === $rule ) {
					return $this->request_error( 'missing_rule', __( 'Auto-reply rule data is missing.', 'submission-inbox' ), 400 );
				}
				$sample = is_array( $payload['sample'] ?? null ) ? $payload['sample'] : array();
				return $this->service->send_test( $rule, $sample, $payload['to'] ?? '' );
			}
		);
	}

	/**
	 * Authenticate, catch recoverable failures, and emit one stable JSON object.
	 *
	 * @param callable $callback Endpoint callback.
	 * @return void
	 */
	private function dispatch( $callback ) {
		if ( ! current_user_can( \SUBIN_AdminMenu::CAPABILITY ) ) {
			$this->send_error( $this->request_error( 'forbidden', __( 'You do not have permission to manage auto-replies.', 'submission-inbox' ), 403 ) );
		}

		$nonce = sanitize_text_field( $this->post_scalar( 'nonce' ) );
		if ( '' === $nonce || ! wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) {
			$this->send_error( $this->request_error( 'invalid_nonce', __( 'Auto-reply request could not be verified. Reload the page and try again.', 'submission-inbox' ), 403 ) );
		}

		try {
			$result = call_user_func( $callback );
		} catch ( \Throwable $throwable ) {
			unset( $throwable );
			$result = $this->request_error( 'server_error', __( 'Auto-reply request could not be completed.', 'submission-inbox' ), 500 );
		}

		if ( is_wp_error( $result ) ) {
			$this->send_error( $result );
		}

		$data = is_array( $result ) ? $result : array();
		if ( empty( $data['code'] ) ) {
			$data['code'] = 'ok';
		}
		if ( ! array_key_exists( 'message', $data ) ) {
			$data['message'] = '';
		}
		wp_send_json_success( $data, 200 );
	}

	/**
	 * Decode a bounded JSON request body carried in the payload field.
	 *
	 * @return array<string,mixed>|\WP_Error
	 */
	private function payload() {
		$raw = isset( $_POST['payload'] ) ? wp_unslash( $_POST['payload'] ) : '';
		if ( ! is_string( $raw ) || '' === trim( $raw ) ) {
			return $this->request_error( 'missing_payload', __( 'Auto-reply request data is missing.', 'submission-inbox' ), 400 );
		}
		if ( strlen( $raw ) > self::MAX_PAYLOAD_BYTES ) {
			return $this->request_error( 'payload_too_large', __( 'Auto-reply request data is too large.', 'submission-inbox' ), 413 );
		}

		$payload = json_decode( $raw, true, 32 );
		if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $payload ) ) {
			return $this->request_error( 'invalid_json', __( 'Auto-reply request data is not valid JSON.', 'submission-inbox' ), 400 );
		}

		return $payload;
	}

	/** Read a scalar POST value without allowing arrays into string APIs. @param string $key Key. @return string */
	private function post_scalar( $key ) {
		$value = isset( $_POST[ $key ] ) ? wp_unslash( $_POST[ $key ] ) : '';
		return is_scalar( $value ) ? (string) $value : '';
	}

	/** Send a normalized WP_Error response. @param \WP_Error $error Error. @return void */
	private function send_error( \WP_Error $error ) {
		$data   = (array) $error->get_error_data();
		$status = max( 400, absint( $data['status'] ?? 500 ) );
		unset( $data['status'] );
		wp_send_json_error(
			array_merge(
				array(
					'code'    => $error->get_error_code(),
					'message' => $error->get_error_message(),
				),
				$data
			),
			$status
		);
	}

	/** @param string $code Code. @param string $message Message. @param int $status Status. @return \WP_Error */
	private function request_error( $code, $message, $status ) {
		return new \WP_Error( sanitize_key( $code ), $message, array( 'status' => absint( $status ) ) );
	}
}
