<?php

namespace CommerceBird\Admin\Actions\Ajax;

use CommerceBird\Admin\Connectors\Connector;
use CommerceBird\Admin\Traits\AjaxRequest;
use CommerceBird\Admin\Traits\LogWriter;
use CommerceBird\Admin\Traits\OptionStatus;
use CommerceBird\Admin\Traits\Singleton;

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

/**
 * Handles Exact Online AJAX endpoints for the CommerceBird admin.
 *
 * @since 1.0.0
 */
final class ExactOnlineAjax {
	use Singleton;
	use LogWriter;
	use AjaxRequest;
	use OptionStatus;

	private const PRODUCT_CRON_HOOK            = 'cmbird_eo_product_mapping_daily';
	private const PRODUCT_CRON_ENABLED_OPTION  = 'cmbird_eo_product_cron_enabled';
	private const PRODUCT_CRON_INTERVAL_OPTION = 'cmbird_eo_product_cron_interval';
	// Daily Action Scheduler job that runs on top of the user-configured cron
	// to do a full reset-sync (clears all item/price/stock cursors on the API
	// side and re-pulls everything), so per-warehouse stock totals stay
	// accurate when delta updates only touch some warehouses.
	private const PRODUCT_DAILY_RESET_HOOK = 'cmbird_eo_product_daily_reset';
	private const PRODUCT_CRON_INTERVALS   = array(
		'cmbird_every_10_minutes',
		'cmbird_every_30_minutes',
		'hourly',
	);

	private const FORMS   = array(
		'product'                => array(
			'importProducts',
		),
		'order'                  => array(),
		'customer'               => array(
			'importCustomers',
		),
		'webhooks'               => array(
			'enable_StockPosition',
			'enable_Item',
		),
		'sync_settings'          => array(
			'allowedInvoicesStores',
			'allowedOrdersStores',
			'allowedMandateStores',
			'errors_notifications_email',
		),
		'general_settings'       => array(
			'divisionId',
			'cost_center',
			'cost_unit',
			'email_layout',
			'invoice_style',
			'journal',
			'products_gla',
			'send_invoice',
			'shipping_cost_label',
			'shipping_gla',
			'warehouse',
		),
		'tax_mapping'            => array( 'taxMapping' ),
		'payment_status_mapping' => array(
			'paymentStatusMapping',
		),
	);
	private const ACTIONS = array(
		'save_woo_webhooks'                        => 'create_woo_webhooks',
		'save_sync_order_via_cron'                 => 'sync_order',
		'save_exact_online_connect'                => 'connect_exact_online',
		'get_exact_online_connect'                 => 'get_exact_connection',
		'save_exact_online_cost_center'            => 'cost_center_save',
		'save_exact_online_cost_unit'              => 'cost_unit_save',
		'save_exact_online_gl_account'             => 'gl_account_save',
		'save_exact_online_payment_status'         => 'get_payment_save',
		'map_exact_online_product'                 => 'product_map',
		'toggle_eo_stock'                          => 'toggle_stock',
		'get_eo_stock_setting'                     => 'get_stock_setting',
		'toggle_eo_webhook_publish'                => 'toggle_webhook_publish',
		'get_eo_webhook_publish_setting'           => 'get_webhook_publish_setting',
		'toggle_eo_product_cron'                   => 'toggle_product_cron',
		'get_eo_product_cron_setting'              => 'get_product_cron_setting',
		'get_exact_orphaned_products'              => 'get_orphaned_products',
		'delete_exact_orphaned_products'           => 'delete_orphaned_products',
		'map_exact_online_customer'                => 'customer_map',
		'map_exact_online_order'                   => 'order_map',
		'export_exact_online_order'                => 'order_export',
		'get_exact_online_webhooks'                => 'webhooks_get',
		'save_exact_online_webhooks'               => 'webhooks_save',
		'reset_exact_online_connect'               => 'reset_mapping',
		'get_exact_online_sync_settings'           => 'get_exact_sync_settings',
		'save_exact_online_sync_settings'          => 'save_exact_sync_settings',
		'get_exact_online_general_settings'        => 'get_exact_general_settings',
		'save_exact_online_general_settings'       => 'save_exact_general_settings',
		'get_exact_online_cost_center'             => 'cost_center_get',
		'get_exact_online_cost_unit'               => 'cost_unit_get',
		'get_exact_online_gl_accounts'             => 'gl_account_get',
		'get_exact_online_journals'                => 'journals_get',
		'get_exact_online_warehouses'              => 'warehouses_get',
		'get_exact_online_invoice_layouts'         => 'invoice_layouts_get',
		'get_exact_online_tax_mapping'             => 'get_exact_tax_mapping',
		'save_exact_online_tax_mapping'            => 'save_exact_tax_mapping',
		'get_exact_online_vat_codes'               => 'get_exact_vat_codes',
		'get_exact_online_payment_status_mapping'  => 'get_exact_payment_status_mapping',
		'save_exact_online_payment_status_mapping' => 'save_exact_payment_status_mapping',
		'get_exact_online_payment_conditions'      => 'get_exact_payment_conditions',
		'get_wc_payment_methods'                   => 'wc_payment_gateways',
	);
	private const OPTIONS = array(
		'connect'     => array(
			'exact_domain'       => 'commercebird-exact-online-domain',
			'woo_webhook_status' => 'commercebird-woo-webhook-status',
		),
		'cost_center' => 'commercebird-exact-online-cost-center',
		'cost_unit'   => 'commercebird-exact-online-cost-unit',
		'gl_accounts' => 'commercebird-exact-online-gl-accounts',
	);

	public function __construct() {
		$this->load_actions();
		add_action( 'cmbird_exact_online_sync_orders', array( $this, 'sync_via_cron' ) );
	}

	/**
	 * Register custom WP-Cron schedules used by Exact Online product mapping.
	 *
	 * @param array $schedules Existing schedules.
	 * @return array
	 */
	public function register_product_cron_schedules( array $schedules ): array {
		if ( ! isset( $schedules['cmbird_every_10_minutes'] ) ) {
			$schedules['cmbird_every_10_minutes'] = array(
				'interval' => 10 * MINUTE_IN_SECONDS, // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___ttl -- Required product sync interval by feature spec.
				'display'  => __( 'Once every 10 minutes', 'commercebird' ),
			);
		}

		if ( ! isset( $schedules['cmbird_every_30_minutes'] ) ) {
			$schedules['cmbird_every_30_minutes'] = array(
				'interval' => 30 * MINUTE_IN_SECONDS,
				'display'  => __( 'Once every 30 minutes', 'commercebird' ),
			);
		}

		return $schedules;
	}

	/**
	 * Get a valid product cron interval, falling back to hourly.
	 *
	 * @param string|null $requested_interval Requested interval slug.
	 * @return string
	 */
	private function get_product_cron_interval( ?string $requested_interval = null ): string {
		$interval = is_string( $requested_interval ) && '' !== $requested_interval
			? sanitize_key( $requested_interval )
			: (string) get_option( self::PRODUCT_CRON_INTERVAL_OPTION, 'hourly' );

		if ( ! in_array( $interval, self::PRODUCT_CRON_INTERVALS, true ) ) {
			return 'hourly';
		}

		return $interval;
	}

	/**
	 * Return the human-readable label for a product cron interval.
	 *
	 * @param string $interval Interval slug.
	 * @return string
	 */
	private function get_product_cron_interval_label( string $interval ): string {
		switch ( $interval ) {
			case 'cmbird_every_10_minutes':
				return __( 'once every 10 minutes', 'commercebird' );
			case 'cmbird_every_30_minutes':
				return __( 'once every 30 minutes', 'commercebird' );
			case 'hourly':
			default:
				return __( 'once every 1 hour', 'commercebird' );
		}
	}

	/**
	 * Schedule product mapping cron with the selected interval.
	 *
	 * @param string $interval Interval slug.
	 * @return void
	 */
	private function schedule_product_cron( string $interval ): void {
		wp_clear_scheduled_hook( self::PRODUCT_CRON_HOOK );
		wp_schedule_event( time(), $interval, self::PRODUCT_CRON_HOOK );

		// Schedule the daily full-reset on top of the user-configured cron.
		// Starts 24h from now so it doesn't immediately collide with the
		// freshly-scheduled delta run above.
		if ( function_exists( 'as_unschedule_all_actions' ) ) {
			as_unschedule_all_actions( self::PRODUCT_DAILY_RESET_HOOK );
		}
		if ( function_exists( 'as_schedule_recurring_action' ) ) {
			as_schedule_recurring_action(
				time() + DAY_IN_SECONDS,
				DAY_IN_SECONDS,
				self::PRODUCT_DAILY_RESET_HOOK
			);
		}
	}

	/**
	 * Cancel the daily reset Action Scheduler job, if scheduled.
	 *
	 * @return void
	 */
	private function unschedule_product_daily_reset(): void {
		if ( function_exists( 'as_unschedule_all_actions' ) ) {
			as_unschedule_all_actions( self::PRODUCT_DAILY_RESET_HOOK );
		}
	}

	/**
	 * Whether Exact Online cron work should be skipped for configured quiet hours.
	 * Uses the site's configured timezone from WordPress settings.
	 *
	 * @return bool
	 */
	private function is_exact_cron_quiet_hours(): bool {
		$current_hour = (int) wp_date( 'G', time(), wp_timezone() );

		return $current_hour >= 0 && $current_hour < 6;
	}

	/**
	 * Load actions for the class.
	 *
	 * @return void
	 */
	public function create_woo_webhooks() {
		$this->verify( array( 'token', 'override' ) );
		if ( ! isset( $this->data['token'] ) || empty( $this->data['token'] ) ) {
			$this->errors['message'] = __( 'Token is required', 'commercebird' );
		}

		// Check if webhook status has value in option then don't create webhook and response with status.
		$status = get_option( self::OPTIONS['connect']['woo_webhook_status'] );
		if ( empty( $this->data['override'] ) && isset( $status ) && ! empty( $status ) ) {
			$this->response['message'] = esc_html( $status );
		} else {
			$response = ( new Connector() )->create_woo_webbhook();
			if ( empty( $response ) ) {
				$this->errors['message'] = __( 'Unable to connect', 'commercebird' );
			} elseif ( 200 === $response['code'] ) {
					// Update the option with the webhook status.
					update_option( self::OPTIONS['connect']['woo_webhook_status'], $response['message'] );
					$this->response['message'] = __( 'Webhook created', 'commercebird' );
			} else {
				$this->errors['message'] = $response['message'];
			}
		}
		$this->serve();
	}


	/**
	 * Sync orders from Exact Online.
	 *
	 * @return void
	 */
	public function sync_order(): void {
		$this->verify( array( 'sync' ) );
		if ( $this->data['sync'] ) {
			if ( ! wp_next_scheduled( 'cmbird_exact_online_sync_orders' ) ) {
				wp_schedule_event( time(), 'hourly', 'cmbird_exact_online_sync_orders' );
			}
		} else {
			wp_clear_scheduled_hook( 'cmbird_exact_online_sync_orders' );
		}
		update_option( 'cmbird_exact_online_sync_orders', (bool) $this->data['sync'] );
		$this->response = array(
			'success' => true,
			'message' => __( 'Synced', 'commercebird' ),
		);
		$this->serve();
	}

	public function get_payment_save(): void {
		$this->verify();
		$this->get_payment_status();
	}

	/**
	 * Get Payment status from Exact Online. This is used to update the payment status of the order.
	 *
	 * @return void
	 */
	public function get_payment_status(): void {
		// $fd = fopen( __DIR__ . '/get_payment_status.log', 'a+' );

		$start_date = gmdate( 'Y-m-d H:i:s', strtotime( '-200 days' ) );
		$end_date   = gmdate( 'Y-m-d H:i:s', strtotime( '-5 days' ) );

		// Get all orders with the included statuses.
		$orders = wc_get_orders(
			array(
				'status'       => 'wc-on-hold',
				'limit'        => -1,
				'date_created' => $start_date . '...' . $end_date,
				'return'       => 'ids',
			)
		);
		if ( empty( $orders ) ) {
			$this->response = array(
				'success' => false,
				'message' => __( 'No Invoice orders found', 'commercebird' ),
			);
			$this->serve();
		}
		foreach ( $orders as $order_id ) {
			// send each order_id to wc action scheduler to process if not scheduled.
			if ( ! as_has_scheduled_action( 'cmbird_payment_status', array( $order_id ) ) ) {
				as_schedule_single_action(
					time(),
					'cmbird_payment_status',
					array(
						$order_id,
					)
				);
			} else {
				continue;
			}
		}
		// fclose( $fd );

		// Schedule the event to run weekly starting next week.
		if ( ! wp_next_scheduled( 'cmbird_eo_get_payment_statuses' ) ) {
			$seven_days_in_future = time() + 7 * DAY_IN_SECONDS;
			wp_schedule_event( $seven_days_in_future, 'weekly', 'cmbird_eo_get_payment_statuses' );
		}
		$this->response = array(
			'success' => true,
			'message' => __( 'Payment status updated', 'commercebird' ),
		);
		$this->serve();
	}

	private function process_orders( bool $reset_sync = false ): bool {
		$result = 0;
		$orders = ( new Connector() )->order( $reset_sync );
		if ( is_string( $orders ) ) {
			$this->response = array(
				'success' => false,
				'message' => $orders,
			);
			$this->serve();
			return false;
		}
		if ( empty( $orders['orders'] ) ) {
			return false;
		}
		$chunked = array_chunk( $orders['orders'], 20 );
		foreach ( $chunked as $chunked_order ) {
			$result = as_schedule_single_action(
				time(),
				'cmbird_sync_eo',
				array(
					'orders',
					wp_json_encode( $chunked_order ),
					false,
				)
			);
			if ( empty( $result ) ) {
				break;
			}
		}
		return $result > 0;
	}

	public function order_map(): void {
		$reset_sync = ! empty( $this->data['resetSync'] ) ? (bool) $this->data['resetSync'] : false;
		$result     = $this->process_orders( $reset_sync );

		$this->response['success'] = $result;
		$this->response['message'] = $result
			? __( 'Orders are being mapped in the background.', 'commercebird' )
			: __( 'No new orders since last sync.', 'commercebird' );
		$this->serve();
	}

	public function export_order( $start_date_raw, $end_date_raw ) {
		// $fd = fopen( __DIR__ . '/export_order.log', 'a+' );

		$start_date = gmdate( 'Y-m-d H:i:s', $start_date_raw );
		$end_date   = gmdate( 'Y-m-d H:i:s', $end_date_raw );
		// Define the order statuses to exclude.
		$exclude_statuses = array( 'wc-failed', 'wc-pending', 'wc-on-hold', 'wc-cancelled' );
		$posts_per_page   = 20;
		$paged            = 1;

		do {
			// Query to get orders.
			$args   = array(
				'date_created' => $start_date . '...' . $end_date,
				'status'       => array_diff( array_keys( wc_get_order_statuses() ), $exclude_statuses ),
				'limit'        => $posts_per_page,
				'paged'        => $paged,
				'orderby'      => 'date',
				'order'        => 'ASC',
				'return'       => 'ids',
			);
			$orders = wc_get_orders( $args );

			// Loop through orders and add customer note.
			foreach ( $orders as $order_id ) {
				$order = wc_get_order( $order_id );
				$order->set_status( $order->get_status() );
				$order->save();
			}

			// Increment the offset for the next batch.
			++$paged;
		} while ( ! empty( $orders ) );
		// fclose( $fd );
	}

	public function order_export() {
		$this->verify( self::FORMS['order'] );
		if ( empty( $this->data ) || empty( $this->data['range'] ) ) {
			$this->response['success'] = false;
			$this->response['message'] = __( 'Select dates', 'commercebird' );
			$this->serve();
		}
		// Set the date range to last 30 days.
		$start_date = strtotime( $this->data['range'][0] );
		$end_date   = strtotime( $this->data['range'][1] );
		$this->export_order( $start_date, $end_date );
		$this->response['success'] = true;
		$this->response['message'] = __( 'Exported', 'commercebird' );
		$this->serve();
	}

	public function product_map(): void {
		$this->verify( self::FORMS['product'] );
		// Manual map always does a full sync; cron handles incremental delta.
		$products = ( new Connector() )->products( true );
		if ( is_string( $products ) ) {
			$this->response = array(
				'success' => false,
				'message' => $products,
			);
			$this->serve();
		}
		if ( empty( $products['items'] ) ) {
			$this->response['message'] = __( 'No new or changed items since last sync.', 'commercebird' );
			$this->serve();
		}
		$chunked = array_chunk( $products['items'], 100 );
		foreach ( $chunked as $index => $chunked_products ) {
			$base_key      = wp_unique_id( 'cmbird_product_chunk_' );
			$transient_key = $base_key . '_' . $index;
			set_transient( $transient_key, $chunked_products, HOUR_IN_SECONDS );
			as_schedule_single_action(
				time(),
				'cmbird_process_product_chunk',
				array(
					array(
						'transient_key'   => $transient_key,
						'import_products' => (bool) $this->data['importProducts'],
					),
				)
			);
		}
		$this->response['message'] = __( 'Items are being mapped in the background. You can visit other tabs :).', 'commercebird' );
		$this->serve();
	}

	/**
	 * Toggle the enable_eo_stock option.
	 *
	 * @since 2.6.5
	 * @return void
	 */
	public function toggle_stock(): void {
		$this->verify();
		$enable = isset( $this->data['enable_eo_stock'] ) ? (bool) $this->data['enable_eo_stock'] : false;
		update_option( 'cmbird_enable_eo_stock', $enable );
		$this->response = array(
			'success' => true,
			'message' => __( 'Stock setting saved!', 'commercebird' ),
		);
		$this->serve();
	}

	/**
	 * Get the enable_eo_stock setting.
	 *
	 * @since 2.6.5
	 * @return void
	 */
	public function get_stock_setting(): void {
		$this->verify();
		$this->response = array(
			'success' => true,
			'enable'  => (bool) get_option( 'cmbird_enable_eo_stock', false ),
		);
		$this->serve();
	}

	/**
	 * Toggle whether new products created via Item webhook are published automatically.
	 *
	 * @return void
	 */
	public function toggle_webhook_publish(): void {
		$this->verify();
		$enable = isset( $this->data['enable_eo_webhook_publish'] ) ? (bool) $this->data['enable_eo_webhook_publish'] : false;
		update_option( 'cmbird_enable_eo_webhook_publish', $enable );
		$this->response = array(
			'success' => true,
			'message' => __( 'Publish setting saved!', 'commercebird' ),
		);
		$this->serve();
	}

	/**
	 * Get whether new products created via Item webhook are published automatically.
	 *
	 * @return void
	 */
	public function get_webhook_publish_setting(): void {
		$this->verify();
		$this->response = array(
			'success' => true,
			'enable'  => (bool) get_option( 'cmbird_enable_eo_webhook_publish', false ),
		);
		$this->serve();
	}

	/**
	 * Toggle the scheduled product mapping WP-Cron schedule.
	 *
	 * @since 2.8.2
	 * @return void
	 */
	public function toggle_product_cron(): void {
		$this->verify();
		$enable   = isset( $this->data['enable_eo_product_cron'] ) ? (bool) $this->data['enable_eo_product_cron'] : false;
		$interval = $this->get_product_cron_interval( $this->data['eo_product_cron_interval'] ?? null );

		update_option( self::PRODUCT_CRON_INTERVAL_OPTION, $interval );

		if ( $enable ) {
			$this->schedule_product_cron( $interval );
		} else {
			wp_clear_scheduled_hook( self::PRODUCT_CRON_HOOK );
			$this->unschedule_product_daily_reset();
		}

		update_option( self::PRODUCT_CRON_ENABLED_OPTION, $enable );

		$interval_label = $this->get_product_cron_interval_label( $interval );
		$this->response = array(
			'success'  => true,
			'enable'   => $enable,
			'interval' => $interval,
			'message'  => $enable
				? sprintf(
					/* translators: %s selected stock import interval label. */
					__( 'Scheduled stock import set to %s.', 'commercebird' ),
					esc_html( $interval_label )
				)
				: __( 'Scheduled stock import disabled.', 'commercebird' ),
		);
		$this->serve();
	}

	/**
	 * Return whether the product mapping cron is enabled.
	 *
	 * @since 2.8.2
	 * @return void
	 */
	public function get_product_cron_setting(): void {
		$this->verify();
		$interval       = $this->get_product_cron_interval();
		$this->response = array(
			'success'  => true,
			'enable'   => (bool) get_option( self::PRODUCT_CRON_ENABLED_OPTION, false ),
			'interval' => $interval,
		);
		$this->serve();
	}

	/**
	 * WP-Cron callback: runs scheduled product mapping (never import).
	 *
	 * Reuses the same Connector + Action Scheduler chunk logic as product_map(),
	 * but runs outside the AJAX context and always sets import_products = false.
	 *
	 * @since 2.8.2
	 * @return void
	 */
	public function product_map_cron(): void {
		if ( $this->is_exact_cron_quiet_hours() ) {
			return;
		}

		$products = ( new Connector() )->products();
		if ( is_string( $products ) || empty( $products['items'] ) ) {
			return;
		}
		$chunked = array_chunk( $products['items'], 100 );
		foreach ( $chunked as $index => $chunked_products ) {
			$base_key      = wp_unique_id( 'cmbird_product_chunk_' );
			$transient_key = $base_key . '_' . $index;
			set_transient( $transient_key, $chunked_products, HOUR_IN_SECONDS );
			as_schedule_single_action(
				time(),
				'cmbird_process_product_chunk',
				array(
					array(
						'transient_key'   => $transient_key,
						'import_products' => false,
					),
				)
			);
		}
	}

	/**
	 * Daily full-reset of the product sync. Runs on top of the user-configured
	 * delta cron (10min/30min/hourly) to backfill any stock totals that drifted
	 * because partial-warehouse updates only refreshed some StockPosition rows.
	 * Passes reset_sync=true so the API clears its item/price/stock cursors and
	 * returns every webshop item with summed FreeStock across all warehouses.
	 *
	 * @since 2.8.2
	 * @return void
	 */
	public function product_map_daily_reset(): void {
		if ( $this->is_exact_cron_quiet_hours() ) {
			return;
		}

		$products = ( new Connector() )->products( true );
		if ( is_string( $products ) || empty( $products['items'] ) ) {
			return;
		}
		$chunked = array_chunk( $products['items'], 100 );
		foreach ( $chunked as $index => $chunked_products ) {
			$base_key      = wp_unique_id( 'cmbird_product_chunk_' );
			$transient_key = $base_key . '_' . $index;
			set_transient( $transient_key, $chunked_products, HOUR_IN_SECONDS );
			as_schedule_single_action(
				time(),
				'cmbird_process_product_chunk',
				array(
					array(
						'transient_key'   => $transient_key,
						'import_products' => false,
					),
				)
			);
		}
	}

	/**
	 * Maps customers from Exact Online to WooCommerce.
	 *
	 * @return void
	 */
	public function customer_map(): void {
		$this->verify( self::FORMS['customer'] );
		// Manual map always does a full sync; cron handles incremental delta.
		$response = ( new Connector() )->customer( true );
		if ( is_string( $response ) ) {
			$this->response = array(
				'success' => false,
				'message' => $response,
			);
			$this->serve();
		}
		if ( empty( $response['customers'] ) ) {
			$this->response = array(
				'success' => false,
				'message' => __( 'No new or changed customers since last sync.', 'commercebird' ),
			);
			$this->serve();
		}
		$chunked = array_chunk( $response['customers'], 100 );
		foreach ( $chunked as $index => $chunked_customers ) {
			$transient_key = 'cmbird_customer_chunk_' . time() . '_' . $index;
			set_transient( $transient_key, $chunked_customers, HOUR_IN_SECONDS );
			as_schedule_single_action(
				time(),
				'cmbird_process_customer_chunk',
				array(
					array(
						'transient_key'    => $transient_key,
						'import_customers' => (bool) $this->data['importCustomers'],
					),
				)
			);
		}
		$this->response['message'] = __( 'Customers are being mapped in the background. You can visit other tabs :).', 'commercebird' );
		$this->serve();
	}

	/**
	 * Resets all product and customer mappings from Exact Online.
	 *
	 * @return void
	 */
	public function reset_mapping() {
		$this->verify();
		// Check if resetAll is true (sent from Vue).
		$reset_all = isset( $this->data['reset'] ) && 1 === $this->data['reset'];
		if ( ! $reset_all ) {
			delete_option( self::OPTIONS['connect']['token'] );
			$response = ( new Connector() )->reset_exact_connection();
			if ( is_string( $response ) || ( isset( $response['code'] ) && $response['code'] !== 200 ) ) {
				$this->errors['message'] = is_string( $response ) ? $response : $response['message'];
			} elseif ( isset( $response['code'] ) && 200 === $response['code'] ) {
				$this->response['message'] = __( 'Connection reset completed', 'commercebird' );
			} else {
				$this->errors['message'] = __( 'Unable to reset connection', 'commercebird' );
			}
			$this->serve();
		}
		global $wpdb;
		// Delete all transients related to products and customers.
		$transients = array( 'cmbird_product_chunk_', 'cmbird_customer_chunk_' );
		foreach ( $transients as $transient ) {
			$wpdb->query(
				$wpdb->prepare(
					"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
					$transient . '%'
				)
			);
		}
		// Delete all product and customer meta keys.
		$wpdb->query( "DELETE FROM {$wpdb->prefix}postmeta WHERE meta_key = 'eo_item_id'" );
		$wpdb->query( "DELETE FROM {$wpdb->prefix}usermeta WHERE meta_key IN ('eo_account_id', 'eo_contact_id', 'eo_gl_account')" );
		// return response.
		$this->response['message'] = __( 'Mapping reset completed', 'commercebird' );
		$this->serve();
	}

	public function cost_center_get_options() {
		return get_option( self::OPTIONS['cost_center'], array() );
	}

	public function cost_unit_get_options() {
		return get_option( self::OPTIONS['cost_unit'], array() );
	}

	public function gl_account_get_options() {
		return get_option( self::OPTIONS['gl_accounts'], array() );
	}


	public function cost_center_save() {
		$this->verify();
		$response = ( new Connector() )->cost_centers();
		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Cost centers not found', 'commercebird' );
		} else {
			$centers = array_map(
				function ( $item ) {
					return "{$item['Code']}-{$item['Description']}";
				},
				$response['data']
			);
			update_option( self::OPTIONS['cost_center'], $centers );
			// remove each meta if it does not exists in the cost centers.
			global $wpdb;
			if ( ! empty( $centers ) ) {
				// Sanitize and prepare the units for the SQL query.
				$placeholders = implode( ',', array_fill( 0, count( $centers ), '%s' ) );

				// Prepare the query string.
				$query = "DELETE FROM {$wpdb->prefix}wc_orders_meta
              			WHERE meta_key = 'costcenter'
              			AND meta_value NOT IN ($placeholders)";
				$wpdb->query( $wpdb->prepare( $query, ...$centers ) ); // phpcs:ignore.
				// also remove from postmeta table.
				$query2 = "DELETE FROM {$wpdb->prefix}postmeta
              			WHERE meta_key = 'costcenter'
              			AND meta_value NOT IN ($placeholders)";
				$wpdb->query( $wpdb->prepare( $query2, ...$centers ) ); // phpcs:ignore.
			} else {
				// If $units is empty, delete all meta keys with 'costunit'.
				$wpdb->query(
					"DELETE FROM {$wpdb->prefix}wc_orders_meta
         			WHERE meta_key = 'costcenter'"
				);
			}
			$this->response['message'] = __( 'Cost centers saved', 'commercebird' );
		}
		$this->serve();
	}

	public function cost_unit_save() {
		$this->verify();
		$response = ( new Connector() )->cost_units();
		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Cost units not found', 'commercebird' );
		} else {
			$units = array_map(
				function ( $item ) {
					return "{$item['Code']}-{$item['Description']}";
				},
				$response['data']
			);
			update_option( self::OPTIONS['cost_unit'], $units );
			// remove each meta if it does not exists in the units.
			global $wpdb;
			if ( ! empty( $units ) ) {
				// Sanitize and prepare the units for the SQL query.
				$placeholders = implode( ',', array_fill( 0, count( $units ), '%s' ) );

				// Prepare the query string.
				$query = "DELETE FROM {$wpdb->prefix}wc_orders_meta
              			WHERE meta_key = 'costunit'
              			AND meta_value NOT IN ($placeholders)";
				$wpdb->query( $wpdb->prepare( $query, ...$units ) ); // phpcs:ignore.
				// also remove from postmeta table.
				$query2 = "DELETE FROM {$wpdb->prefix}postmeta
              			WHERE meta_key = 'costunit'
              			AND meta_value NOT IN ($placeholders)";
				$wpdb->query( $wpdb->prepare( $query2, ...$units ) ); // phpcs:ignore.
			} else {
				// If $units is empty, delete all meta keys with 'costunit'.
				$wpdb->query(
					"DELETE FROM {$wpdb->prefix}wc_orders_meta
         			WHERE meta_key = 'costunit'"
				);
			}
			$this->response['message'] = __( 'Cost units saved', 'commercebird' );
		}
		$this->serve();
	}

	public function gl_account_save() {
		$this->verify();
		$response = ( new Connector() )->gl_accounts();
		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'GL accounts not found', 'commercebird' );
		} else {
			$accounts = array_map(
				function ( $item ) {
					return "{$item['ID']} : {$item['Description']}";
				},
				$response['data']
			);
			update_option( self::OPTIONS['gl_accounts'], $accounts );
			$this->response['message'] = __( 'GL accounts saved', 'commercebird' );
		}
		$this->serve();
	}

	public function sync_via_cron(): void {
		if ( $this->is_exact_cron_quiet_hours() ) {
			return;
		}

		$this->process_orders();

		$this->export_order( strtotime( '-1 day' ), strtotime( 'now' ) );
	}

	public function connect_exact_online() {
		$this->verify( array( 'exact_domain' ) );
		if ( ! isset( $this->data['exact_domain'] ) || empty( $this->data['exact_domain'] ) ) {
			$this->errors['message'] = __( 'Please select exact domain', 'commercebird' );
		} else {
			update_option( self::OPTIONS['connect']['exact_domain'], $this->data['exact_domain'] );
			$response = ( new Connector() )->connect_exact( $this->data['exact_domain'] );
			if ( empty( $response ) ) {
				$this->errors['message'] = __( 'Unable to connect', 'commercebird' );
			} elseif ( 200 === $response['code'] ) {
				$this->response['message']  = __( 'Account generared', 'commercebird' );
				$this->response['auth_url'] = $response['data']['auth_url'];
				$this->response['message']  = __( 'Saved', 'commercebird' );
			} else {
				$this->errors['message'] = $response['message'];
			}
		}

		$this->serve();
	}

	public function get_exact_connection() {
		$this->verify();
		$exact_domain = get_option( self::OPTIONS['connect']['exact_domain'], '' );
		$token        = SettingsAjax::instance()->get_token();
		if ( ! empty( $exact_domain ) && ! empty( $token ) ) {
			$response = ( new Connector() )->get_exact_divisions();
			if ( empty( $response ) ) {
				$this->errors['message'] = __( 'Unable to connect', 'commercebird' );
			} else {
				$this->response['message'] = $response['message'];
				if ( 200 === $response['code'] ) {
					$this->response['connection'] = $response['data'];
				}
			}
			$this->response['exact_domain'] = $exact_domain;
			$this->response['token']        = $token;
		}
		$this->serve();
	}


	/**
	 * @description Retrieves the general account settings.
	 **/
	public function get_exact_sync_settings() {
		$this->verify();
		$response = ( new Connector() )->get_account_settings();
		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Unable to connect', 'commercebird' );
		} else {
			$this->response['message'] = $response['message'];
			if ( 200 === $response['code'] ) {
				$this->response['settings'] = $response['data'];
			}
		}
		$this->serve();
	}

	/**
	 * @description Function to save the general account settings.
	 */
	public function save_exact_sync_settings() {
		$this->verify( self::FORMS['sync_settings'] );

		$settings = $this->data;

		if ( empty( $settings ) ) {
			$this->errors['message'] = __( 'Invalid sync settings payload.', 'commercebird' );
			$this->serve();
		}

		$response = ( new Connector() )->save_account_settings( $settings );

		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Unable to save settings.', 'commercebird' );
		} else {
			$this->response['message'] = $response['message'];
			if ( ! empty( $response['code'] ) && 200 === $response['code'] ) {
				$this->response['success']  = true;
				$this->response['settings'] = $response['data'] ?? array();
			}
		}

		$this->serve();
	}

	/**
	 * @description Retrieves mapped general settings for exact.
	 **/
	public function get_exact_general_settings() {
		$this->verify();
		$response = ( new Connector() )->exact_general_settings();
		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Unable to connect', 'commercebird' );
		} else {
			$this->response['message'] = $response['message'];
			if ( $response['code'] === 200 ) {
				$this->response['settings'] = $response['data'];
			}
		}
		$this->serve();
	}

	/**
	 * @description Function to save the mapped general settings.
	 */
	public function save_exact_general_settings() {
		$this->verify( self::FORMS['general_settings'] );

		$settings = $this->data;

		if ( empty( $settings ) ) {
			$this->errors['message'] = __( 'Invalid general settings payload.', 'commercebird' );
			$this->serve();
		}

		$response = ( new Connector() )->save_exact_general_settings( $settings );

		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Unable to save settings.', 'commercebird' );
		} else {
			$this->response['message'] = $response['message'];
			if ( ! empty( $response['code'] ) && $response['code'] === 200 ) {
				$this->response['success']  = true;
				$this->response['settings'] = $response['data'] ?? array();
			}
		}

		$this->serve();
	}

	/**
	 * @description Retrieves Journals list from exact.
	 **/
	public function journals_get() {
		$this->verify();
		$response = ( new Connector() )->journals();
		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Unable to connect', 'commercebird' );
		} else {
			$this->response['message'] = $response['message'];
			if ( $response['code'] === 200 ) {
				$this->response['settings'] = $response['data'];
			}
		}
		$this->serve();
	}

	/**
	 * @description Retrieves warehoused from exact.
	 **/
	public function warehouses_get() {
		$this->verify();
		$response = ( new Connector() )->exact_warehouses();
		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Unable to connect', 'commercebird' );
		} else {
			$this->response['message'] = $response['message'];
			if ( $response['code'] === 200 ) {
				$this->response['settings'] = $response['data'];
			}
		}
		$this->serve();
	}

	/**
	 * @description Retrieves invoice layouts from exact.
	 **/
	public function invoice_layouts_get() {
		$this->verify();
		$response = ( new Connector() )->invoice_layouts();
		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Unable to connect', 'commercebird' );
		} else {
			$this->response['message'] = $response['message'];
			if ( $response['code'] === 200 ) {
				$this->response['settings'] = $response['data'];
			}
		}
		$this->serve();
	}

	/**
	 * @description Retrieves invoice layouts from exact.
	 **/
	public function gl_account_get() {
		$this->verify();
		$response = ( new Connector() )->gl_accounts();
		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Unable to connect', 'commercebird' );
		} else {
			$this->response['message'] = $response['message'];
			if ( $response['code'] === 200 ) {
				$this->response['settings'] = $response['data'];
			}
		}
		$this->serve();
	}

	/**
	 * @description Retrieves cost centers for exact.
	 **/
	public function cost_center_get() {
		$this->verify();
		$response = ( new Connector() )->cost_centers();
		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Unable to connect', 'commercebird' );
		} else {
			$this->response['message'] = $response['message'];
			if ( $response['code'] === 200 ) {
				$this->response['settings'] = $response['data'];
			}
		}
		$this->serve();
	}

	/**
	 * @description Retrieves cost units for exact.
	 **/
	public function cost_unit_get() {
		$this->verify();
		$response = ( new Connector() )->cost_units();
		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Unable to connect', 'commercebird' );
		} else {
			$this->response['message'] = $response['message'];
			if ( $response['code'] === 200 ) {
				$this->response['settings'] = $response['data'];
			}
		}
		$this->serve();
	}

	/**
	 * Sets the webhooks settings and updates the status.
	 *
	 * @return void
	 */
	public function webhooks_save(): void {
		$this->verify( self::FORMS['webhooks'] );
		$form_data = $this->data;
		foreach ( $form_data as $topic => $is_active ) {
			$webhooks[] = array(
				'topic'  => $topic,
				'status' => $is_active ? 'active' : 'inactive',
			);
		}
		$response = ( new Connector() )->subscribe_exact_webhooks( $webhooks );
		// error_log( print_r( $response, true ) );
		$this->option_status_update( $this->data );
		$this->response = is_array( $response ) ? $response : array( 'message' => $response );
		$this->serve();
	}

	/**
	 * Retrieves the wehbhook settings.
	 *
	 * @return void
	 */
	public function webhooks_get(): void {
		$this->verify();
		$this->response = $this->option_status_get( self::FORMS['webhooks'] );
		$this->serve();
	}

	/**
	 * @description Retrieves exact vat codes.
	 **/
	public function get_exact_vat_codes() {
		$this->verify();
		$response = ( new Connector() )->get_exact_vat_codes();
		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Unable to connect', 'commercebird' );
		} else {
			$this->response['message'] = $response['message'];
			if ( $response['code'] === 200 ) {
				$this->response['settings'] = $response['data'];
			}
		}
		$this->serve();
	}

	/**
	 * @description Retrieves mapped tax settings.
	 **/
	public function get_exact_tax_mapping() {
		$this->verify();
		$response = ( new Connector() )->get_exact_tax_mapping();
		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Unable to connect', 'commercebird' );
		} else {
			$this->response['message'] = $response['message'];
			if ( $response['code'] === 200 ) {
				$this->response['taxMapping'] = $response['data'];
			}
		}
		$this->serve();
	}

	/**
	 * @description Function to save the mapped taxes.
	 */
	public function save_exact_tax_mapping() {
		$this->verify( self::FORMS['tax_mapping'] );
		$settings = $this->data['taxMapping'] ?? array();
		if ( empty( $settings ) ) {
			$this->errors['message'] = __( 'Invalid tax mapping payload.', 'commercebird' );
			$this->serve();
		}

		$response = ( new Connector() )->save_exact_tax_mapping( $settings );

		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Unable to save tax mapping.', 'commercebird' );
		} else {
			$this->response['message'] = $response['message'];
			if ( ! empty( $response['code'] ) && 200 === $response['code'] ) {
				$this->response['success']  = true;
				$this->response['settings'] = $response['data'] ?? array();
			}
		}

		$this->serve();
	}

	/**
	 * @description Retrieves exact payment conditions.
	 **/
	public function get_exact_payment_conditions() {
		$this->verify();
		$response = ( new Connector() )->get_exact_payment_conditions();
		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Unable to connect', 'commercebird' );
		} else {
			$this->response['message'] = $response['message'];
			if ( $response['code'] === 200 ) {
				$this->response['payment_conditions'] = $response['data'];
			}
		}
		$this->serve();
	}
	/**
	 * Retrieves mapped payment status mapping.
	 *
	 * @return void
	 */
	public function get_exact_payment_status_mapping() {
		$this->verify();
		$response = ( new Connector() )->get_exact_payment_status_mapping();
		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Unable to connect', 'commercebird' );
		} else {
			$this->response['message'] = $response['message'];
			if ( $response['code'] === 200 ) {
				$this->response['paymentStatusMapping'] = $response['data'];
			}
		}
		$this->serve();
	}

	/**
	 * @description Function to save the mapped taxes.
	 */
	public function save_exact_payment_status_mapping() {
		$this->verify( self::FORMS['payment_status_mapping'] );
		$settings = $this->data['paymentStatusMapping'] ?? array();
		if ( empty( $settings ) ) {
			$this->errors['message'] = __( 'Invalid payment status mapping payload.', 'commercebird' );
			$this->serve();
		}

		$response = ( new Connector() )->save_exact_payment_status_mapping( $settings );

		if ( empty( $response ) ) {
			$this->errors['message'] = __( 'Unable to save payment status mapping.', 'commercebird' );
		} else {
			$this->response['message'] = $response['message'];
			if ( ! empty( $response['code'] ) && 200 === $response['code'] ) {
				$this->response['success']              = true;
				$this->response['paymentStatusMapping'] = $response['data'] ?? array();
			}
		}

		$this->serve();
	}

	/**
	 * Retrieves available WooCommerce payment gateways.
	 *
	 * @return void
	 */
	public function wc_payment_gateways() {
		$gateways           = array();
		$available_gateways = WC()->payment_gateways->payment_gateways();
		foreach ( $available_gateways as $gateway_id => $gateway ) {
			if ( 'yes' !== $gateway->enabled ) {
				continue; // skip disabled gateways.
			}
			$gateways[] = array(
				'id'           => $gateway_id,
				'title'        => $gateway->get_title(),
				'description'  => $gateway->get_description(),
				'method_title' => $gateway->method_title,
				'enabled'      => $gateway->enabled,
				'supports'     => $gateway->supports,
			);
		}
		$this->response['success']  = true;
		$this->response['gateways'] = $gateways;
		$this->serve();
	}

	/**
	 * Get orphaned products (exist in WooCommerce but not in Exact Online).
	 *
	 * @return void
	 */
	public function get_orphaned_products() {
		global $wpdb;

		// Prevent stale results: if mapping chunks are still running the transient is incomplete.
		$pending_jobs = as_get_scheduled_actions(
			array(
				'hook'     => 'cmbird_process_product_chunk',
				'status'   => \ActionScheduler_Store::STATUS_PENDING,
				'per_page' => 1,
			),
			'ids'
		);
		if ( ! empty( $pending_jobs ) ) {
			$this->response['success']  = false;
			$this->response['products'] = array();
			$this->response['message']  = __( 'Product mapping is still in progress. Please wait until it completes before checking orphaned products.', 'commercebird' );
			$this->serve();
			return;
		}

		$synced_skus = get_option( 'cmbird_eo_synced_skus', false );
		if ( false === $synced_skus ) {
			$legacy_synced_skus = get_transient( 'cmbird_eo_synced_skus' );
			if ( ! empty( $legacy_synced_skus ) && is_array( $legacy_synced_skus ) ) {
				$synced_skus = array_values( array_unique( $legacy_synced_skus ) );
				update_option( 'cmbird_eo_synced_skus', $synced_skus, false );
				delete_transient( 'cmbird_eo_synced_skus' );
			}
		}

		if ( false === $synced_skus || empty( $synced_skus ) ) {
			$this->response['success']  = true;
			$this->response['products'] = array();
			$this->response['message']  = __( 'No sync data available. Please run product mapping first.', 'commercebird' );
			$this->serve();
			return;
		}

		// Create placeholders for the IN clause.
		$placeholders = implode( ',', array_fill( 0, count( $synced_skus ), '%s' ) );

		// GROUP BY p.ID prevents duplicate rows when a product has multiple eo_item_id meta entries.
		// Only return products/variations that do not have an Exact Online mapping.
		$results           = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT p.ID, p.post_title, pm_sku.meta_value as sku
				FROM {$wpdb->posts} p
				LEFT JOIN {$wpdb->postmeta} pm_eo ON p.ID = pm_eo.post_id AND pm_eo.meta_key = 'eo_item_id'
				LEFT JOIN {$wpdb->postmeta} pm_sku ON p.ID = pm_sku.post_id AND pm_sku.meta_key = '_sku'
				WHERE p.post_type IN ( 'product', 'product_variation' )
				AND p.post_status = 'publish'
				AND pm_eo.post_id IS NULL
				AND (pm_sku.meta_value NOT IN ({$placeholders}) OR pm_sku.meta_value IS NULL)
				GROUP BY p.ID",
				...$synced_skus
			),
			ARRAY_A
		);
		$orphaned_products = array();
		foreach ( $results as $row ) {
			$orphaned_products[] = array(
				'id'   => (int) $row['ID'],
				'name' => $row['post_title'],
				'sku'  => $row['sku'] ?? '',
			);
		}

		$this->response['success']  = true;
		$this->response['products'] = $orphaned_products;
		$this->response['count']    = count( $orphaned_products );
		$this->response['message']  = sprintf(
			/* translators: %d: number of orphaned products */
			_n(
				'Found %d product that exists in WooCommerce but not in Exact Online.',
				'Found %d products that exist in WooCommerce but not in Exact Online.',
				count( $orphaned_products ),
				'commercebird'
			),
			count( $orphaned_products )
		);
		$this->serve();
	}

	/**
	 * Delete orphaned products from WooCommerce.
	 *
	 * @return void
	 */
	public function delete_orphaned_products() {
		$this->verify( array( 'productIds' ) );

		if ( empty( $this->data['productIds'] ) || ! is_array( $this->data['productIds'] ) ) {
			$this->errors['message'] = __( 'No product IDs provided', 'commercebird' );
			$this->serve();
			return;
		}

		$product_ids = array_values(
			array_filter(
				array_unique( array_map( 'intval', $this->data['productIds'] ) )
			)
		);

		if ( empty( $product_ids ) ) {
			$this->errors['message'] = __( 'No valid product IDs provided', 'commercebird' );
			$this->serve();
			return;
		}

		// Persist the deletion queue so the latest request is available in wp_options.
		update_option( 'cmbird_exact_orphaned_product_ids', $product_ids, false );

		$deleted = array();
		$failed  = array();

		foreach ( $product_ids as $product_id ) {
			$result = wp_delete_post( $product_id, true );
			if ( false !== $result ) {
				wc_delete_product_transients( $product_id );
				$deleted[] = $product_id;
			} else {
				$failed[] = $product_id;
			}
		}

		if ( ! empty( $failed ) ) {
			update_option( 'cmbird_exact_orphaned_product_ids', $failed, false );
		} else {
			delete_option( 'cmbird_exact_orphaned_product_ids' );
		}

		// Clear synced SKUs cache after direct deletion.
		delete_option( 'cmbird_eo_synced_skus' );

		$this->response['success'] = empty( $failed );
		$this->response['deleted'] = count( $deleted );
		$this->response['failed']  = count( $failed );

		if ( empty( $failed ) ) {
			$this->response['message'] = sprintf(
				/* translators: %d: number of products */
				_n(
					'Deleted %d orphaned product.',
					'Deleted %d orphaned products.',
					count( $deleted ),
					'commercebird'
				),
				count( $deleted )
			);
		} else {
			$this->response['message'] = sprintf(
				/* translators: 1: deleted count, 2: failed count */
				__( 'Deleted %1$d orphaned products. Failed to delete %2$d products.', 'commercebird' ),
				count( $deleted ),
				count( $failed )
			);
			$this->response['failed_ids'] = $failed;
		}

		$this->serve();
	}
}
