<?php
/**
 * The WooCommerce events that can send an SMS.
 *
 * @package TextTango
 */

declare( strict_types=1 );

namespace TextTango\WooCommerce;

use TextTango\Api\PhoneNumber;
use TextTango\Support\Settings;

defined( 'ABSPATH' ) || exit;

/**
 * Registry of order events, their default templates, and who they go to (spec §6).
 *
 * Every event is off by default. A merchant connecting the plugin should never
 * discover it has already texted their customers.
 */
final class Events {

	/**
	 * Message goes to the customer on the order.
	 */
	public const AUDIENCE_CUSTOMER = 'customer';

	/**
	 * Message goes to the store's own staff numbers.
	 */
	public const AUDIENCE_ADMIN = 'admin';

	/**
	 * Fires when an order reaches a status.
	 */
	public const TRIGGER_STATUS = 'status';

	/**
	 * Fires when an order is created.
	 */
	public const TRIGGER_NEW_ORDER = 'new_order';

	/**
	 * Fires when a tracking number appears on an order.
	 */
	public const TRIGGER_TRACKING = 'tracking';

	/**
	 * Fires when somebody creates an account.
	 */
	public const TRIGGER_ACCOUNT = 'account';

	/**
	 * Fires when a product runs low or sells out.
	 */
	public const TRIGGER_STOCK = 'stock';

	/**
	 * Fires when somebody asks to reset their password.
	 */
	public const TRIGGER_PASSWORD_RESET = 'password_reset';

	/**
	 * Which merge tags an event's template can resolve.
	 *
	 * A template editor that offers `{order_number}` to a low-stock alert is
	 * offering a tag that can only ever resolve to nothing, and the merchant
	 * finds out from a message with a hole in it.
	 *
	 * @param string $event Event id.
	 */
	public static function tag_set( string $event ): string {
		$trigger = (string) ( self::get( $event )['trigger'] ?? self::TRIGGER_STATUS );

		if ( self::TRIGGER_ACCOUNT === $trigger ) {
			return 'account';
		}

		if ( self::TRIGGER_STOCK === $trigger ) {
			return 'stock';
		}

		if ( self::TRIGGER_PASSWORD_RESET === $trigger ) {
			return 'reset';
		}

		return 'order';
	}

	/**
	 * Every supported event.
	 *
	 * `trigger` says what makes it fire and `status` narrows that to one order
	 * status when the trigger is a status change. Everything else carries an
	 * empty status because nothing about an order status starts it.
	 *
	 * @return array<string, array{label: string, description: string, audience: string, status: string, trigger: string, template: string}>
	 */
	public static function all(): array {
		return array(
			'order_processing' => array(
				'label'       => __( 'Order processing', 'texttango' ),
				'description' => __( 'Payment received and the order is being prepared.', 'texttango' ),
				'audience'    => self::AUDIENCE_CUSTOMER,
				'status'      => 'processing',
				'trigger'     => self::TRIGGER_STATUS,
				'template'    => __( 'Hi {first_name}, we have received your order {order_number} ({order_total}) and are getting it ready. - {store_name}', 'texttango' ),
			),
			'order_completed'  => array(
				'label'       => __( 'Order completed', 'texttango' ),
				'description' => __( 'The order has been fulfilled.', 'texttango' ),
				'audience'    => self::AUDIENCE_CUSTOMER,
				'status'      => 'completed',
				'trigger'     => self::TRIGGER_STATUS,
				'template'    => __( 'Hi {first_name}, your order {order_number} is complete. Thank you for shopping with {store_name}.', 'texttango' ),
			),
			'order_on_hold'    => array(
				'label'       => __( 'Order on hold', 'texttango' ),
				'description' => __( 'The order is waiting on something, usually payment.', 'texttango' ),
				'audience'    => self::AUDIENCE_CUSTOMER,
				'status'      => 'on-hold',
				'trigger'     => self::TRIGGER_STATUS,
				'template'    => __( 'Hi {first_name}, order {order_number} is on hold while we confirm payment. - {store_name}', 'texttango' ),
			),
			'order_cancelled'  => array(
				'label'       => __( 'Order cancelled', 'texttango' ),
				'description' => __( 'The order was cancelled.', 'texttango' ),
				'audience'    => self::AUDIENCE_CUSTOMER,
				'status'      => 'cancelled',
				'trigger'     => self::TRIGGER_STATUS,
				'template'    => __( 'Hi {first_name}, order {order_number} has been cancelled. Contact us if this was not expected. - {store_name}', 'texttango' ),
			),
			'order_refunded'   => array(
				'label'       => __( 'Order refunded', 'texttango' ),
				'description' => __( 'The order was refunded in full.', 'texttango' ),
				'audience'    => self::AUDIENCE_CUSTOMER,
				'status'      => 'refunded',
				'trigger'     => self::TRIGGER_STATUS,
				'template'    => __( 'Hi {first_name}, your refund for order {order_number} has been processed. - {store_name}', 'texttango' ),
			),
			'order_failed'     => array(
				'label'       => __( 'Payment failed', 'texttango' ),
				'description' => __( 'Payment for the order did not go through.', 'texttango' ),
				'audience'    => self::AUDIENCE_CUSTOMER,
				'status'      => 'failed',
				'trigger'     => self::TRIGGER_STATUS,
				'template'    => __( 'Hi {first_name}, payment for order {order_number} did not go through. You can try again here: {order_url}', 'texttango' ),
			),
			'order_shipped'    => array(
				'label'       => __( 'Order shipped', 'texttango' ),
				'description' => __( 'A tracking number was added to the order.', 'texttango' ),
				'audience'    => self::AUDIENCE_CUSTOMER,
				'status'      => '',
				'trigger'     => self::TRIGGER_TRACKING,
				'template'    => __( 'Hi {first_name}, order {order_number} is on its way. Track it with {tracking_number}. - {store_name}', 'texttango' ),
			),
			'account_created'  => array(
				'label'       => __( 'Welcome (new account)', 'texttango' ),
				'description' => __( 'Somebody created an account and already has a number on file.', 'texttango' ),
				'audience'    => self::AUDIENCE_CUSTOMER,
				'status'      => '',
				'trigger'     => self::TRIGGER_ACCOUNT,
				'template'    => __( 'Welcome to {store_name}, {first_name}. Your account is ready: {site_url}', 'texttango' ),
			),
			'new_order_admin'  => array(
				'label'       => __( 'New order (staff alert)', 'texttango' ),
				'description' => __( 'Sent to your own numbers when an order comes in.', 'texttango' ),
				'audience'    => self::AUDIENCE_ADMIN,
				'status'      => '',
				'trigger'     => self::TRIGGER_NEW_ORDER,
				'template'    => __( 'New order {order_number} for {order_total} ({order_items} items) at {store_name}.', 'texttango' ),
			),
			'password_reset'   => array(
				'label'       => __( 'Password reset', 'texttango' ),
				'description' => __( 'The same reset link WordPress emails, texted as well. Only to a number the customer has verified.', 'texttango' ),
				'audience'    => self::AUDIENCE_CUSTOMER,
				'status'      => '',
				'trigger'     => self::TRIGGER_PASSWORD_RESET,
				'template'    => __( 'Reset your {store_name} password: {reset_link}', 'texttango' ),
			),
			'low_stock_admin'  => array(
				'label'       => __( 'Low stock (staff alert)', 'texttango' ),
				'description' => __( 'Sent to your own numbers when a product runs low or sells out.', 'texttango' ),
				'audience'    => self::AUDIENCE_ADMIN,
				'status'      => '',
				'trigger'     => self::TRIGGER_STOCK,
				'template'    => __( '{product_name} is down to {stock_quantity} at {store_name}. SKU {sku}.', 'texttango' ),
			),
		);
	}

	/**
	 * One event's definition.
	 *
	 * @param string $event Event id.
	 * @return array<string, string>|null
	 */
	public static function get( string $event ): ?array {
		return self::all()[ $event ] ?? null;
	}

	/**
	 * Events triggered by an order reaching a given status.
	 *
	 * @param string $status Status without the `wc-` prefix.
	 * @return string[] Event ids.
	 */
	public static function for_status( string $status ): array {
		$matches = array();

		foreach ( self::all() as $event => $definition ) {
			if ( self::TRIGGER_STATUS !== $definition['trigger'] ) {
				continue;
			}

			if ( '' !== $definition['status'] && $definition['status'] === $status ) {
				$matches[] = $event;
			}
		}

		return $matches;
	}

	/**
	 * Whether an event is switched on.
	 *
	 * @param string $event Event id.
	 */
	public static function is_enabled( string $event ): bool {
		$stored = self::settings()[ $event ] ?? array();

		return ! empty( $stored['enabled'] ) && null !== self::get( $event );
	}

	/**
	 * The template in use for an event: the merchant's, or the default.
	 *
	 * @param string $event Event id.
	 */
	public static function template( string $event ): string {
		$definition = self::get( $event );

		if ( null === $definition ) {
			return '';
		}

		$stored = self::settings()[ $event ] ?? array();
		$custom = trim( (string) ( $stored['template'] ?? '' ) );

		return '' !== $custom ? $custom : $definition['template'];
	}

	/**
	 * The template shipped with the plugin for an event.
	 *
	 * @param string $event Event id.
	 */
	public static function default_template( string $event ): string {
		return (string) ( self::get( $event )['template'] ?? '' );
	}

	/**
	 * Whether a merchant has edited an event's message.
	 *
	 * Nothing is stored while the message still matches the shipped default, so
	 * improvements to a default — a wording fix, or a character that was quietly
	 * doubling the segment count — reach every site that never customised it.
	 *
	 * @param string $event Event id.
	 */
	public static function is_customised( string $event ): bool {
		$stored = self::settings()[ $event ] ?? array();

		return '' !== trim( (string) ( $stored['template'] ?? '' ) );
	}

	/**
	 * Normalize a submitted template: blank when it still matches the default.
	 *
	 * @param string $event    Event id.
	 * @param string $template Template as submitted.
	 */
	public static function normalize_template( string $event, string $template ): string {
		$template = trim( $template );

		if ( '' === $template || trim( self::default_template( $event ) ) === $template ) {
			return '';
		}

		return $template;
	}

	/**
	 * Stored per-event configuration.
	 *
	 * @return array<string, array<string, mixed>>
	 */
	public static function settings(): array {
		$stored = Settings::get( 'notifications', array() );

		return is_array( $stored ) ? $stored : array();
	}

	/**
	 * Numbers staff alerts are sent to, ready to send to.
	 *
	 * Normalized here rather than by each caller, and — the point — **made
	 * distinct here**. One handset can be written several ways: `0548828183`,
	 * `+233548828183` and `+233 548 828 183` are the same phone and three
	 * different strings, and a merchant listing a number twice while tidying the
	 * box is an ordinary thing to do.
	 *
	 * Left to the callers, each spelling became its own message. Nothing caught
	 * it either: every caller puts the list index into the dedupe key so that
	 * three *different* staff numbers do not collide on one key and lose two
	 * messages — which means the one guard that would have noticed was
	 * deliberately told not to. The merchant gets the same alert twice and pays
	 * twice, and the only place it shows is the bill.
	 *
	 * @return string[] Distinct numbers in E.164, ready for {@see Sender}.
	 */
	public static function admin_recipients(): array {
		$raw = (string) Settings::get( 'admin_recipients', '' );

		$parts = preg_split( '/[,\n]+/', $raw );

		$numbers = array();

		foreach ( array_filter( array_map( 'trim', is_array( $parts ) ? $parts : array() ) ) as $entry ) {
			$number = PhoneNumber::normalize( $entry );

			// An unusable entry is dropped rather than surfaced: staff numbers are
			// configured once and the alert is not the place to report a typo.
			if ( ! is_wp_error( $number ) ) {
				$numbers[] = (string) $number;
			}
		}

		return array_values( array_unique( $numbers ) );
	}
}
