<?php
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

/**
 * Webhook integration for WooCommerce Appointments.
 *
 * Registers custom webhook resources and topics, maps plugin lifecycle hooks
 * to WooCommerce webhook topics, and builds delivery payloads. This enables
 * external systems to receive real-time updates for appointments and
 * availability rules without polling the REST API.
 *
 * Usage Examples
 *
 * Admin Setup
 *
 * 	- Go to `WooCommerce > Settings > Advanced > Webhooks`
 * 	- Click `Add Webhook`
 * 	- Set `Name`: e.g., `Appointments Updates`
 * 	- Set `Status`: `Active`
 * 	- Set `Topic`: choose one of:
 * 		- `appointment.created`, `appointment.updated`, `appointment.deleted`, `appointment.trashed`
 * 		- `appointment.rescheduled`, `appointment.cancelled`
 * 		- `availability.created`, `availability.updated`, `availability.deleted`
 * 	- Set `Delivery URL`: your receiver endpoint, e.g., `https://yourapp.example/webhooks/woocommerce`
 * 	- Set `Secret`: shared secret used to sign payloads
 * 	- Save
 *
 * Programmatic: Create Webhook via REST (curl)
 *
 * 	curl -X POST "https://yourstore.example/wp-json/wc/v3/webhooks" \
 * 		-u ck_yourkey:cs_yoursecret \
 * 		-H "Content-Type: application/json" \
 * 		-d '{
 * 			"name": "Appointments Updated",
 * 			"topic": "appointment.updated",
 * 			"delivery_url": "https://yourapp.example/webhooks/woocommerce",
 * 			"secret": "your-shared-secret",
 * 			"status": "active"
 * 		}'
 *
 * Example Payload: `appointment.updated`
 *
 * 	{
 * 		"id": 12345,
 * 		"status": "confirmed",
 * 		"customer_status": "expected",
 * 		"product_id": 987,
 * 		"order_id": 456,
 * 		"order_item_id": 78910,
 * 		"customer_id": 321,
 * 		"qty": 1,
 * 		"cost": 0,
 * 		"all_day": false,
 * 		"start": 1732140000,
 * 		"end": 1732143600,
 * 		"timezone": "UTC",
 * 		"local_timezone": "America/New_York",
 * 		"staff_ids": [ 55 ],
 * 		"google_calendar_event_id": "evt_abc",
 * 		"google_calendar_staff_event_ids": [],
 * 		"date_created": "2025-11-21 15:05:00",
 * 		"date_modified": "2025-11-21 15:10:00"
 * 	}
 *
 * Signature Verification (Node.js / Express)
 *
 * 	// Header: X-WC-Webhook-Signature (base64 HMAC-SHA256 of raw body using your secret)
 * 	app.post('/webhooks/woocommerce', express.raw({ type: 'application/json' }), (req, res) => {
 * 		const signature = req.header('X-WC-Webhook-Signature') || '';
 * 		const secret = process.env.WC_WEBHOOK_SECRET;
 * 		const crypto = require('crypto');
 * 		const digest = crypto
 * 			.createHmac('sha256', secret)
 * 			.update(req.body)
 * 			.digest('base64');
 * 		if ( crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest)) ) {
 * 			// Process req.body JSON
 * 			return res.sendStatus(200);
 * 		}
 * 		return res.sendStatus(400);
 * 	});
 *
 * Signature Verification (PHP)
 *
 * 	$secret = getenv('WC_WEBHOOK_SECRET');
 * 	$raw = file_get_contents('php://input');
 * 	$signature = $_SERVER['HTTP_X_WC_WEBHOOK_SIGNATURE'] ?? '';
 * 	$digest = base64_encode( hash_hmac( 'sha256', $raw, $secret, true ) );
 * 	if ( hash_equals( $signature, $digest ) ) {
 * 		// Process json_decode( $raw, true )
 * 		http_response_code( 200 );
 * 		exit;
 * 	}
 * 	http_response_code( 400 );
 * 	exit;
 *
 * Notes
 *
 * 	- WooCommerce handles retries and delivery logging; see `WooCommerce > Status > Logs`.
 * 	- For delete events where the resource no longer exists, payload contains only `{ "id": <int> }`.
 * 	- Use HTTPS for `delivery_url` and store the secret securely.
 */
class WC_Appointments_Webhooks {

	/**
	 * Bootstraps all WooCommerce webhook filters.
	 *
	 * - Adds resources: `appointment`, `availability`
	 * - Adds topics: create/update/delete/reschedule/cancel for appointments,
	 *   and create/update/delete for availability
	 * - Maps topics to internal actions to trigger deliveries
	 * - Overrides payload builder to return structured JSON bodies
	 */
	public function __construct() {
		// Allow WooCommerce to recognize our resources.
		add_filter( 'woocommerce_valid_webhook_resources', [ $this, 'add_resources' ] );
		// Register new topics alongside WooCommerce core ones.
		add_filter( 'woocommerce_valid_webhook_topics', [ $this, 'add_topics' ] );
		// Map topics to plugin actions so Woo triggers deliveries correctly.
		add_filter( 'woocommerce_webhook_topic_hooks', [ $this, 'add_topic_hooks' ] );
		// Provide structured payloads for our resources.
		add_filter( 'woocommerce_webhook_payload', [ $this, 'build_payload' ], 10, 4 );
	}

	/**
	 * Add custom webhook resources.
	 *
	 * @param array $resources Existing resources.
	 * @return array Modified resources including `appointment` and `availability`.
	 */
	public function add_resources( $resources ): array {
		$resources[] = 'appointment';
		$resources[] = 'availability';
		return array_values( array_unique( $resources ) );
	}

	/**
	 * Add webhook topics for Appointments and Availability.
	 *
	 * @param array $topics Existing topics.
	 * @return array Topics with appointment/availability events.
	 */
	public function add_topics( $topics ): array {
		$new = [
			'appointment.created',
			'appointment.updated',
			'appointment.deleted',
			'appointment.trashed',
			'appointment.rescheduled',
			'appointment.cancelled',
			'availability.created',
			'availability.updated',
			'availability.deleted',
		];
		return array_values( array_unique( array_merge( $topics, $new ) ) );
	}

	/**
	 * Map webhook topics to plugin lifecycle actions.
	 *
	 * WooCommerce dispatches webhooks when actions listed here fire.
	 *
	 * @param array $topic_hooks Topic → actions mapping.
	 * @return array Mapping extended with our topics.
	 */
	public function add_topic_hooks( array $topic_hooks ): array {
		$topic_hooks['appointment.created'] = [ 'woocommerce_new_appointment' ];
		$topic_hooks['appointment.updated'] = [ 'woocommerce_update_appointment' ];
		$topic_hooks['appointment.deleted'] = [ 'woocommerce_delete_appointment' ];
		$topic_hooks['appointment.trashed'] = [ 'woocommerce_trash_appointment' ];
		$topic_hooks['appointment.rescheduled'] = [ 'woocommerce_appointments_rescheduled_appointment' ];
		$topic_hooks['appointment.cancelled'] = [ 'woocommerce_appointments_cancelled_appointment' ];
		$topic_hooks['availability.created'] = [ 'woocommerce_appointments_availability_created' ];
		$topic_hooks['availability.updated'] = [ 'woocommerce_appointments_availability_updated' ];
		$topic_hooks['availability.deleted'] = [ 'woocommerce_after_appointments_availability_object_delete' ];
		return $topic_hooks;
	}

	/**
	 * Build webhook payloads for our resources.
	 *
	 * @param mixed             $payload     Existing payload built by Woo.
	 * @param string            $resource    Resource slug (e.g., 'appointment').
	 * @param int|string        $resource_id Resource identifier.
	 * @param WC_Webhook|mixed  $webhook     Webhook instance.
	 * @return array JSON-serializable array for our resources.
	 */
	public function build_payload( $payload, $resource, $resource_id, $webhook ) {
		if ( 'appointment' === $resource ) {
			return $this->get_appointment_payload( (int) $resource_id );
		}
		if ( 'availability' === $resource ) {
			return $this->get_availability_payload( (int) $resource_id );
		}
		return $payload;
	}

	/**
	 * Appointment payload builder.
	 *
	 * Includes identity, status, linkage, scheduling, and cost details so consumers
	 * can reconcile state without additional API calls. Falls back to minimal body
	 * if the appointment was deleted and cannot be read.
	 *
	 * @param int $id Appointment ID.
	 * @return array Structured payload.
	 */
	protected function get_appointment_payload( $id ): array {
		$appointment = get_wc_appointment( $id );
		if ( ! $appointment || ! $appointment->get_id() ) {
			return [ 'id' => (int) $id ];
		}
		return [
			'id' => (int) $appointment->get_id(),
			'status' => (string) $appointment->get_status(),
			'customer_status' => (string) $appointment->get_customer_status(),
			'product_id' => (int) $appointment->get_product_id(),
			'order_id' => (int) $appointment->get_order_id(),
			'order_item_id' => (int) $appointment->get_order_item_id(),
			'customer_id' => (int) $appointment->get_customer_id(),
			'qty' => (int) $appointment->get_qty(),
			'cost' => (float) $appointment->get_cost(),
			'all_day' => (bool) $appointment->get_all_day(),
			'start' => (int) $appointment->get_start(),
			'end' => (int) $appointment->get_end(),
			'timezone' => (string) $appointment->get_timezone(),
			'local_timezone' => (string) $appointment->get_local_timezone(),
			'staff_ids' => array_map( 'intval', (array) $appointment->get_staff_ids() ),
			'google_calendar_event_id' => (string) $appointment->get_google_calendar_event_id(),
			'google_calendar_staff_event_ids' => (array) $appointment->get_google_calendar_staff_event_ids(),
			'date_created' => (string) $appointment->get_date_created( 'edit' ),
			'date_modified' => (string) $appointment->get_date_modified( 'edit' ),
		];
	}

	/**
	 * Availability payload builder.
	 *
	 * Mirrors the availability rule structure so listeners can re-index quickly.
	 * Falls back to minimal body when the rule was deleted.
	 *
	 * @param int $id Availability rule ID.
	 * @return array Structured payload.
	 */
	protected function get_availability_payload( $id ): array {
		$availability = get_wc_appointments_availability( $id );
		if ( ! $availability || ! $availability->get_id() ) {
			return [ 'id' => (int) $id ];
		}
		return [
			'id' => (int) $availability->get_id(),
			'title' => (string) $availability->get_title(),
			'kind' => (string) $availability->get_kind(),
			'kind_id' => (string) $availability->get_kind_id(),
			'event_id' => (string) $availability->get_event_id(),
			'range_type' => (string) $availability->get_range_type(),
			'from_date' => (string) $availability->get_from_date(),
			'to_date' => (string) $availability->get_to_date(),
			'from_range' => (string) $availability->get_from_range(),
			'to_range' => (string) $availability->get_to_range(),
			'appointable' => (string) $availability->get_appointable(),
			'priority' => (int) $availability->get_priority(),
			'qty' => (string) $availability->get_qty(),
			'ordering' => (int) $availability->get_ordering(),
			'rrule' => (string) $availability->get_rrule(),
			'date_created' => (string) $availability->get_date_created( 'edit' ),
			'date_modified' => (string) $availability->get_date_modified( 'edit' ),
		];
	}
}

/**
 * Initialize webhook registration after WooCommerce loads.
 */
add_action( 'plugins_loaded', function(): void {
	if ( class_exists( 'WooCommerce' ) ) {
		new WC_Appointments_Webhooks();
	}
}, 20 );