<?php
/**
 * Helper utilities for Estimated Delivery for WooCommerce.
 * All limits and defaults use filters so add-ons/custom code can override.
 *
 * @package EstimatedDeliveryDateWC
 */

declare( strict_types=1 );

defined( 'ABSPATH' ) || exit;

/**
 * Class EDFW_Helper
 *
 * @since 1.0.0
 */
final class EDFW_Helper {

	/**
	 * Singleton instance.
	 *
	 * @since 1.0.0
	 * @var EDFW_Helper|null
	 */
	private static ?EDFW_Helper $instance = null;

	/**
	 * Get the singleton instance.
	 *
	 * @since 1.0.0
	 * @return EDFW_Helper
	 */
	public static function instance(): EDFW_Helper {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * All methods are static; the singleton only exists for the loader.
	 *
	 * @since 1.0.0
	 */
	private function __construct() {}

	/**
	 * Base rule structure: a list of conditions, a Min/Max window and an optional
	 * scheduled start date. Add-ons can add further keys (e.g. order_value_days).
	 *
	 * @since 1.0.0
	 * @return array<string, mixed>
	 */
	public static function base_rule(): array {
		$rule = array(
			'conditions' => array(),
			'min'        => '2',
			'max'        => '5',
			'scheduled'  => '',
		);
		/**
		 * Filter the shape of a delivery rule.
		 * Add-ons add keys here so their data survives alongside min, max and conditions.
		 *
		 * @since 1.0.0
		 * @param array $rule Base rule: conditions, min, max, scheduled.
		 */
		return (array) apply_filters( 'edfw_base_rule_args', $rule );
	}

	/**
	 * Default settings. Add-ons can add defaults for extra options.
	 *
	 * @since 1.0.0
	 * @return array<string, mixed>
	 */
	public static function default_settings(): array {
		$defaults = array(
			'position_archive'    => 'above_add_to_cart',
			'position_single'     => '31',
			'cart_item'           => 'no',
			'cart_overall'        => 'yes',
			'cart_overall_format' => 'latest',
			'order_item'          => 'yes',
			'date_format'         => 'M j, Y',
			'date_format_custom'  => 'M j, Y',
			'text'                => '',
			'text_min'            => '',
			'text_max'            => '',
			'text_cart_item'      => '',
			'text_cart_overall'   => '',
			'skipped_dates'       => array(),
			'reload_dates'        => 'no',
			'show_archive'        => 'yes',
			'show_single'         => 'yes',
			'show_checkout'       => 'yes',
			// Global estimate + schedule edited by the React "General" tab.
			'enabled'             => 'yes',
			// Min/Max now live in the "Default" delivery rule; kept here only as a
			// fallback/seed value for the default rule and preview.
			'min_days'            => '2',
			'max_days'            => '5',
			'cutoff_time'         => '14:00',
			'working_days'        => array( 1, 2, 3, 4, 5 ),
			'countdown_enabled'   => 'yes',
			// Display tab.
			'position'            => 'after_price',
			'message_template'    => 'Estimated delivery: {start} – {end}',
			'icon'                => 'truck',
			'accent_color'        => '#7f54b3',
			// Advanced tab.
			'caching'             => 'yes',
			'uninstall_cleanup'   => 'no',
		);
		/**
		 * Filter the plugin's default settings.
		 *
		 * @since 1.0.0
		 * @param array $defaults Default settings, keyed by setting name.
		 */
		return (array) apply_filters( 'edfw_default_settings', $defaults );
	}

	/**
	 * Archive display positions. Add-ons can add more.
	 *
	 * @since 1.0.0
	 * @return array<string, string>
	 */
	public static function archive_positions(): array {
		$positions = array(
			'under_title'       => __( 'Under title', 'wpactpro-estimated-delivery-date' ),
			'under_rating'      => __( 'Under rating', 'wpactpro-estimated-delivery-date' ),
			'under_price'       => __( 'Under price', 'wpactpro-estimated-delivery-date' ),
			'above_add_to_cart' => __( 'Above add to cart', 'wpactpro-estimated-delivery-date' ),
			'under_add_to_cart' => __( 'Under add to cart', 'wpactpro-estimated-delivery-date' ),
			'none'              => __( 'None (hide)', 'wpactpro-estimated-delivery-date' ),
		);
		/**
		 * Filter the shop/archive display positions offered in the admin.
		 *
		 * @since 1.0.0
		 * @param array $positions Position slug => label.
		 */
		return (array) apply_filters( 'edfw_archive_positions', $positions );
	}

	/**
	 * Single product display positions. Add-ons can add more.
	 *
	 * @since 1.0.0
	 * @return array<string, string>
	 */
	public static function single_positions(): array {
		$positions = array(
			'6'  => __( 'Under title', 'wpactpro-estimated-delivery-date' ),
			'11' => __( 'Under price & rating', 'wpactpro-estimated-delivery-date' ),
			'21' => __( 'Under excerpt', 'wpactpro-estimated-delivery-date' ),
			'31' => __( 'Under add to cart', 'wpactpro-estimated-delivery-date' ),
			'41' => __( 'Under meta', 'wpactpro-estimated-delivery-date' ),
			'0'  => __( 'None (hide)', 'wpactpro-estimated-delivery-date' ),
		);
		/**
		 * Filter the single-product display positions offered in the admin.
		 *
		 * @since 1.0.0
		 * @param array $positions Hook priority => label.
		 */
		return (array) apply_filters( 'edfw_single_positions', $positions );
	}

	/**
	 * Display locations (where to show estimate). Add-ons can add thank_you, mini_cart, account, etc.
	 *
	 * @since 1.0.0
	 * @return array<string, string>
	 */
	public static function display_locations(): array {
		$locations = array(
			'single'   => __( 'Product single', 'wpactpro-estimated-delivery-date' ),
			'archive'  => __( 'Product archive / shop', 'wpactpro-estimated-delivery-date' ),
			'cart'     => __( 'Cart', 'wpactpro-estimated-delivery-date' ),
			'checkout' => __( 'Checkout', 'wpactpro-estimated-delivery-date' ),
			'emails'   => __( 'Order emails', 'wpactpro-estimated-delivery-date' ),
		);
		/**
		 * Filter the storefront locations an estimate can appear in.
		 *
		 * @since 1.0.0
		 * @param array $locations Location slug => label.
		 */
		return (array) apply_filters( 'edfw_display_locations', $locations );
	}

	/**
	 * Cart overall format options. Add-ons can add more.
	 *
	 * @since 1.0.0
	 * @return array<string, string>
	 */
	public static function cart_overall_formats(): array {
		$formats = array(
			'latest'          => __( 'Latest date', 'wpactpro-estimated-delivery-date' ),
			'earliest'        => __( 'Earliest date', 'wpactpro-estimated-delivery-date' ),
			'earliest_latest' => __( 'Earliest – Latest', 'wpactpro-estimated-delivery-date' ),
		);
		/**
		 * Filter the formats offered for the cart's overall estimate.
		 *
		 * @since 1.0.0
		 * @param array $formats Format slug => label.
		 */
		return (array) apply_filters( 'edfw_cart_overall_formats', $formats );
	}

	/**
	 * Get shipping zone (matched, geo, or default).
	 *
	 * @since 1.0.0
	 * @param int $default_zone_id Default zone ID.
	 * @return WC_Shipping_Zone|null
	 */
	public static function get_shipping_zone( int $default_zone_id = 0 ) {
		$zone = self::get_matched_zone();
		if ( null === $zone ) {
			$zone = self::get_geo_zone();
		}
		if ( null === $zone ) {
			$zone = self::get_default_zone( $default_zone_id );
		}
		/**
		 * Filter the shipping zone used to evaluate zone conditions.
		 *
		 * @since 1.0.0
		 * @param WC_Shipping_Zone|null $zone Resolved zone, or null when unknown.
		 * @param int $default_zone_id Zone id to fall back to.
		 */
		return apply_filters( 'edfw_shipping_zone', $zone, $default_zone_id );
	}

	/**
	 * Get zone from cart package.
	 *
	 * @since 1.0.0
	 * @return WC_Shipping_Zone|null
	 */
	public static function get_matched_zone() {
		if ( ! isset( WC()->cart ) ) {
			return null;
		}
		$packages = WC()->cart->get_shipping_packages();
		if ( empty( $packages ) ) {
			return null;
		}
		$zone = wc_get_shipping_zone( reset( $packages ) );
		if ( ! is_object( $zone ) ) {
			return null;
		}
		$methods = $zone->get_shipping_methods( true );
		return ! empty( $methods ) ? $zone : null;
	}

	/**
	 * Get zone from IP geolocation.
	 *
	 * @since 1.0.0
	 * @return WC_Shipping_Zone|null
	 */
	public static function get_geo_zone() {
		$geo  = new WC_Geolocation();
		$ip   = $geo->get_ip_address();
		$loc  = $geo->geolocate_ip( $ip );
		$dest = array(
			'destination' => array(
				'country'  => $loc['country'],
				'state'    => $loc['state'],
				'postcode' => '',
			),
		);
		$zone = WC_Shipping_Zones::get_zone_matching_package( $dest );
		if ( ! is_object( $zone ) ) {
			return null;
		}
		$methods = $zone->get_shipping_methods( true );
		return ! empty( $methods ) ? $zone : null;
	}

	/**
	 * Get default zone by ID.
	 *
	 * @since 1.0.0
	 * @param int $zone_id Zone ID.
	 * @return WC_Shipping_Zone|null
	 */
	public static function get_default_zone( int $zone_id = 0 ) {
		$zone = WC_Shipping_Zones::get_zone( $zone_id );
		if ( ! is_object( $zone ) ) {
			return null;
		}
		$methods = $zone->get_shipping_methods( true );
		return ! empty( $methods ) ? $zone : null;
	}

	/**
	 * Get selected shipping method instance ID from session.
	 *
	 * @since 1.0.0
	 * @param bool $get_name Return method id (e.g. flat_rate) instead of instance ID.
	 * @return int|string|null
	 */
	public static function get_selected_method( bool $get_name = false ) {
		$chosen = array();
		if ( isset( WC()->session ) ) {
			$chosen = WC()->session->get( 'chosen_shipping_methods', array() );
		}
		if ( empty( $chosen[0] ) || false === $chosen[0] ) {
			return null;
		}
		$parts = explode( ':', $chosen[0] );
		if ( $get_name ) {
			return $parts[0] ?? null;
		}
		return isset( $parts[1] ) ? (int) $parts[1] : null;
	}

	/**
	 * Sanitize array recursively for options/rules.
	 *
	 * @since 1.0.0
	 * @param array $arr Input array.
	 * @return array
	 */
	public static function sanitize_array( array $arr ): array {
		foreach ( $arr as $k => $v ) {
			if ( is_array( $v ) ) {
				$arr[ $k ] = self::sanitize_array( $v );
			} else {
				$arr[ $k ] = sanitize_text_field( $v );
			}
		}
		return $arr;
	}

	/**
	 * Generate unique key for rules/dates.
	 *
	 * @since 1.0.0
	 * @return string
	 */
	public static function generate_key(): string {
		/**
		 * Filter the alphabet used to generate rule keys.
		 *
		 * @since 1.0.0
		 * @param string $chars Characters to pick from.
		 */
		$chars = apply_filters( 'edfw_key_characters', 'abcdefghijklmnopqrstuvwxyz0123456789' );
		/**
		 * Filter the length of generated rule keys.
		 *
		 * @since 1.0.0
		 * @param int $len Number of characters.
		 */
		$len = (int) apply_filters( 'edfw_key_length', 6 );
		$key = '';
		$max = strlen( $chars ) - 1;
		for ( $i = 0; $i < $len; $i++ ) {
			$key .= $chars[ random_int( 0, $max ) ];
		}
		/**
		 * Filter a freshly generated rule key.
		 *
		 * @since 1.0.0
		 * @param string $key The generated key.
		 */
		return apply_filters( 'edfw_generate_key', $key );
	}

	/**
	 * Weekday labels keyed by PHP weekday int (0 = Sunday).
	 *
	 * @since 1.0.0
	 * @return array<int, string>
	 */
	public static function weekday_labels(): array {
		return array(
			0 => __( 'Sunday', 'wpactpro-estimated-delivery-date' ),
			1 => __( 'Monday', 'wpactpro-estimated-delivery-date' ),
			2 => __( 'Tuesday', 'wpactpro-estimated-delivery-date' ),
			3 => __( 'Wednesday', 'wpactpro-estimated-delivery-date' ),
			4 => __( 'Thursday', 'wpactpro-estimated-delivery-date' ),
			5 => __( 'Friday', 'wpactpro-estimated-delivery-date' ),
			6 => __( 'Saturday', 'wpactpro-estimated-delivery-date' ),
		);
	}

	/**
	 * Extra settings sections contributed by add-ons, rendered inside this plugin's
	 * own settings screen.
	 *
	 * An add-on should not need a second admin page, and a store owner should not have
	 * to hunt across two screens to configure one feature. Returning a schema here lets
	 * the React app render the fields itself, so an add-on needs no JavaScript at all.
	 *
	 * A section is:
	 *
	 *   id          Unique slug.
	 *   tab         Which tab to render on: general|display|rules|holidays|advanced.
	 *   title       Card heading.
	 *   description Optional paragraph under the heading.
	 *   fields      List of fields, each:
	 *                 key         Setting key. Prefix it with your own plugin prefix.
	 *                 type        toggle|number|text|select
	 *                 label       Field label.
	 *                 description Optional help text.
	 *                 options     [ { value, label } ] — required for select.
	 *                 min         Optional minimum for number.
	 *
	 * Values are read from the settings payload, so an add-on must also merge its saved
	 * values in via `edfw_get_settings`, and persist them on `edfw_after_save_settings`.
	 *
	 * @since 1.0.0
	 * @return array<int, array<string, mixed>>
	 */
	public static function settings_schema(): array {
		/**
		 * Filter the extra settings sections rendered in the admin app.
		 *
		 * @since 1.0.0
		 * @param array $sections Sections contributed by add-ons.
		 */
		return (array) apply_filters( 'edfw_settings_schema', array() );
	}

	/**
	 * Feature map shared with the admin UI (condition types, display locations).
	 *
	 * `ruleConditions` is a slug => definition map that the rule builder renders
	 * directly, rather than a list the UI has to already know about. Each definition is:
	 *
	 *   label       Translated name shown in the type dropdown.
	 *   placeholder Translated "Any …" text shown when nothing is selected.
	 *   source      Where the value list comes from:
	 *                 'search'  paginated REST lookup (large sets: products, terms)
	 *                 'zones'   shipping zones already in the bootstrap payload
	 *                 'methods' shipping methods already in the bootstrap payload
	 *                 'options' a fixed list supplied as `options` below
	 *   options     [ { value, label }, … ] — required when source is 'options'.
	 *
	 * Add-ons register new condition types by adding an entry here and answering the
	 * matching `edfw_condition_in_{type}` filter. Nothing in the React app needs to
	 * change for a new type to appear.
	 *
	 * @since 1.0.0
	 * @return array<string, mixed>
	 */
	public static function capabilities(): array {
		/**
		 * Filter the feature map handed to the admin app.
		 * Add-ons register new rule condition types here; the React rule builder renders
		 * whatever this returns, so no JavaScript change is needed.
		 *
		 * @since 1.0.0
		 * @param array $capabilities ruleConditions and displayLocations.
		 */
		return (array) apply_filters(
			'edfw_capabilities',
			array(
				'ruleConditions'   => array(
					'product'        => array(
						'label'       => __( 'Product', 'wpactpro-estimated-delivery-date' ),
						'placeholder' => __( 'Any product', 'wpactpro-estimated-delivery-date' ),
						'source'      => 'search',
					),
					'product_cat'    => array(
						'label'       => __( 'Product category', 'wpactpro-estimated-delivery-date' ),
						'placeholder' => __( 'Any category', 'wpactpro-estimated-delivery-date' ),
						'source'      => 'search',
					),
					'shipping_class' => array(
						'label'       => __( 'Shipping class', 'wpactpro-estimated-delivery-date' ),
						'placeholder' => __( 'Any class', 'wpactpro-estimated-delivery-date' ),
						'source'      => 'search',
					),
					'zone'           => array(
						'label'       => __( 'Shipping zone', 'wpactpro-estimated-delivery-date' ),
						'placeholder' => __( 'Any zone', 'wpactpro-estimated-delivery-date' ),
						'source'      => 'zones',
					),
					'method'         => array(
						'label'       => __( 'Shipping method', 'wpactpro-estimated-delivery-date' ),
						'placeholder' => __( 'Any method', 'wpactpro-estimated-delivery-date' ),
						'source'      => 'methods',
					),
				),
				'displayLocations' => array( 'single', 'archive', 'cart', 'checkout', 'emails' ),
			)
		);
	}
}
