<?php
/**
 * REST API: shared CRUD for flat campaign lists.
 *
 * @package PRECO
 */

namespace PRECO\Includes\Admin\Rest;

use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

defined( 'ABSPATH' ) || exit;

/**
 * Base controller for campaign-style records (Popup Upsell, Order Bump).
 *
 * These features store their records as a flat list of independent entries
 * (unlike `preco_recommendations`, which is one big page->locations->lists
 * tree loaded/saved wholesale). Each record supports its own Edit/Duplicate/
 * Delete actions from an admin listing table, so this controller exposes
 * real per-record CRUD routes instead of a single bulk GET/POST. Subclasses
 * only supply naming: option key, route base, payload/response keys.
 */
abstract class CampaignRest {
	/**
	 * REST namespace.
	 */
	const PRECO_REST_NAMESPACE = 'preco/v1';

	/**
	 * Option name where the records are stored.
	 *
	 * @return string
	 */
	abstract protected function option_name();

	/**
	 * Route base, e.g. 'popup-upsells'.
	 *
	 * @return string
	 */
	abstract protected function rest_base();

	/**
	 * Request/response key for a single record, e.g. 'popup'.
	 *
	 * @return string
	 */
	abstract protected function item_key();

	/**
	 * Response key for the record list, e.g. 'popups'.
	 *
	 * @return string
	 */
	abstract protected function collection_key();

	/**
	 * Action fired after any write, e.g. 'preco_popup_upsell_saved'.
	 *
	 * @return string
	 */
	abstract protected function saved_action();

	/**
	 * Human label used in error messages, e.g. 'Popup'.
	 *
	 * @return string
	 */
	abstract protected function item_label();

	/**
	 * Constructor.
	 */
	public function __construct() {
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
	}

	/**
	 * Register routes.
	 *
	 * @return void
	 */
	public function register_routes() {
		register_rest_route(
			self::PRECO_REST_NAMESPACE,
			'/' . $this->rest_base(),
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'permission_check' ),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'permission_check' ),
					'args'                => array(
						$this->item_key() => array(
							'required' => true,
						),
					),
				),
			)
		);

		register_rest_route(
			self::PRECO_REST_NAMESPACE,
			'/' . $this->rest_base() . '/(?P<id>\d+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_item' ),
					'permission_callback' => array( $this, 'permission_check' ),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'update_item' ),
					'permission_callback' => array( $this, 'permission_check' ),
					'args'                => array(
						$this->item_key() => array(
							'required' => true,
						),
					),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_item' ),
					'permission_callback' => array( $this, 'permission_check' ),
				),
			)
		);

		register_rest_route(
			self::PRECO_REST_NAMESPACE,
			'/' . $this->rest_base() . '/(?P<id>\d+)/duplicate',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'duplicate_item' ),
				'permission_callback' => array( $this, 'permission_check' ),
			)
		);
	}

	/**
	 * Permission check.
	 *
	 * @return bool
	 */
	public function permission_check() {
		return current_user_can( 'manage_woocommerce' ) || current_user_can( 'manage_options' );
	}

	/**
	 * Verify the request nonce.
	 *
	 * @param WP_REST_Request $request Request.
	 * @return bool
	 */
	private function verify_nonce( WP_REST_Request $request ) {
		return (bool) wp_verify_nonce( $request->get_header( 'X-WP-Nonce' ), 'wp_rest' );
	}

	/**
	 * List all records.
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response
	 */
	public function get_items( WP_REST_Request $request ) {
		if ( ! $this->verify_nonce( $request ) ) {
			return new WP_REST_Response( array( 'message' => 'Invalid nonce.' ), 403 );
		}

		return new WP_REST_Response(
			array( $this->collection_key() => $this->get_all() ),
			200
		);
	}

	/**
	 * Get a single record.
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response
	 */
	public function get_item( WP_REST_Request $request ) {
		if ( ! $this->verify_nonce( $request ) ) {
			return new WP_REST_Response( array( 'message' => 'Invalid nonce.' ), 403 );
		}

		$id    = (int) $request->get_param( 'id' );
		$items = $this->get_all();
		$found = $this->find( $items, $id );

		if ( null === $found ) {
			return new WP_REST_Response( array( 'message' => $this->item_label() . ' not found.' ), 404 );
		}

		return new WP_REST_Response( array( $this->item_key() => $found ), 200 );
	}

	/**
	 * Create a record.
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response
	 */
	public function create_item( WP_REST_Request $request ) {
		if ( ! $this->verify_nonce( $request ) ) {
			return new WP_REST_Response( array( 'message' => 'Invalid nonce.' ), 403 );
		}

		$payload = $request->get_param( $this->item_key() );
		if ( ! is_array( $payload ) ) {
			return new WP_REST_Response( array( 'message' => 'Invalid ' . strtolower( $this->item_label() ) . ' payload.' ), 400 );
		}

		$items = $this->get_all();

		$item              = $this->sanitize_recursive( $payload );
		$item['id']        = $this->next_id( $items );
		$item['createdAt'] = current_time( 'c' );
		$item['updatedAt'] = current_time( 'c' );

		$items[] = $item;
		$this->save_all( $items );

		return new WP_REST_Response( array( $this->item_key() => $item ), 201 );
	}

	/**
	 * Update a record (full-record overwrite).
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response
	 */
	public function update_item( WP_REST_Request $request ) {
		if ( ! $this->verify_nonce( $request ) ) {
			return new WP_REST_Response( array( 'message' => 'Invalid nonce.' ), 403 );
		}

		$id      = (int) $request->get_param( 'id' );
		$payload = $request->get_param( $this->item_key() );
		if ( ! is_array( $payload ) ) {
			return new WP_REST_Response( array( 'message' => 'Invalid ' . strtolower( $this->item_label() ) . ' payload.' ), 400 );
		}

		$items = $this->get_all();
		$index = $this->find_index( $items, $id );

		if ( null === $index ) {
			return new WP_REST_Response( array( 'message' => $this->item_label() . ' not found.' ), 404 );
		}

		$updated              = $this->sanitize_recursive( $payload );
		$updated['id']        = $id;
		$updated['createdAt'] = isset( $items[ $index ]['createdAt'] ) ? $items[ $index ]['createdAt'] : current_time( 'c' );
		$updated['updatedAt'] = current_time( 'c' );

		$items[ $index ] = $updated;
		$this->save_all( $items );

		return new WP_REST_Response( array( $this->item_key() => $updated ), 200 );
	}

	/**
	 * Delete a record.
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response
	 */
	public function delete_item( WP_REST_Request $request ) {
		if ( ! $this->verify_nonce( $request ) ) {
			return new WP_REST_Response( array( 'message' => 'Invalid nonce.' ), 403 );
		}

		$id    = (int) $request->get_param( 'id' );
		$items = $this->get_all();
		$index = $this->find_index( $items, $id );

		if ( null === $index ) {
			return new WP_REST_Response( array( 'message' => $this->item_label() . ' not found.' ), 404 );
		}

		array_splice( $items, $index, 1 );
		$this->save_all( $items );

		return new WP_REST_Response( array( 'deleted' => true ), 200 );
	}

	/**
	 * Duplicate a record.
	 *
	 * @param WP_REST_Request $request Request.
	 * @return WP_REST_Response
	 */
	public function duplicate_item( WP_REST_Request $request ) {
		if ( ! $this->verify_nonce( $request ) ) {
			return new WP_REST_Response( array( 'message' => 'Invalid nonce.' ), 403 );
		}

		$id     = (int) $request->get_param( 'id' );
		$items  = $this->get_all();
		$source = $this->find( $items, $id );

		if ( null === $source ) {
			return new WP_REST_Response( array( 'message' => $this->item_label() . ' not found.' ), 404 );
		}

		$copy         = $source;
		$copy['id']   = $this->next_id( $items );
		$copy['name'] = ( isset( $source['name'] ) ? (string) $source['name'] : '' ) . ' (Copy)';
		// Duplicates start disabled so they don't silently begin firing on the storefront.
		$copy['enabled']   = false;
		$copy['createdAt'] = current_time( 'c' );
		$copy['updatedAt'] = current_time( 'c' );

		$items[] = $copy;
		$this->save_all( $items );

		return new WP_REST_Response( array( $this->item_key() => $copy ), 201 );
	}

	/**
	 * Read the full list from storage.
	 *
	 * @return array
	 */
	private function get_all() {
		$items = get_option( $this->option_name(), array() );
		return is_array( $items ) ? array_values( $items ) : array();
	}

	/**
	 * Persist the full list and fire the saved action.
	 *
	 * @param array $items Records.
	 * @return void
	 */
	private function save_all( $items ) {
		update_option( $this->option_name(), $items, false );
		do_action( $this->saved_action() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Action name is dynamic based on subclass.
	}

	/**
	 * Find a record by id.
	 *
	 * @param array $items Records.
	 * @param int   $id Id.
	 * @return array|null
	 */
	private function find( $items, $id ) {
		$index = $this->find_index( $items, $id );
		return null === $index ? null : $items[ $index ];
	}

	/**
	 * Find the array index of a record by id.
	 *
	 * @param array $items Records.
	 * @param int   $id Id.
	 * @return int|null
	 */
	private function find_index( $items, $id ) {
		foreach ( $items as $index => $item ) {
			if ( isset( $item['id'] ) && (int) $item['id'] === (int) $id ) {
				return $index;
			}
		}
		return null;
	}

	/**
	 * Compute the next auto-increment id.
	 *
	 * Starts at 1000 when empty (matching the plugin's example ids) and is
	 * otherwise max(existing ids) + 1 — no separate counter option needed,
	 * and self-healing if a row is ever deleted out of order.
	 *
	 * @param array $items Existing records.
	 * @return int
	 */
	private function next_id( $items ) {
		$max = 0;
		foreach ( $items as $item ) {
			$max = max( $max, isset( $item['id'] ) ? (int) $item['id'] : 0 );
		}
		return $max > 0 ? $max + 1 : 1000;
	}

	/**
	 * Recursively sanitize payload.
	 *
	 * Mirrors RecommendationsRest::sanitize_recursive() — preserves
	 * camelCase keys (needed by the admin app) and array structure.
	 *
	 * @param mixed $value Value.
	 * @param int   $depth Depth.
	 * @return mixed
	 */
	private function sanitize_recursive( $value, $depth = 0 ) {
		if ( $depth > 25 ) {
			return null;
		}

		if ( is_bool( $value ) || is_null( $value ) ) {
			return $value;
		}

		if ( is_int( $value ) || is_float( $value ) ) {
			return $value;
		}

		if ( is_string( $value ) ) {
			return sanitize_text_field( $value );
		}

		if ( is_array( $value ) ) {
			$sanitized = array();
			foreach ( $value as $k => $v ) {
				$key = $k;
				if ( is_string( $k ) && ctype_digit( $k ) ) {
					$key = (int) $k;
				}
				$sanitized[ $key ] = $this->sanitize_recursive( $v, $depth + 1 );
			}
			return $sanitized;
		}

		return null;
	}
}
