<?php
/**
 * NMF Article model.
 *
 * @package flo-sync
 */

namespace NMFS\Sync\Items\Articles\Models;

use Exception;
use NMFS\Shared;
use NMFS\Sync\Items\Articles\Article_Assets;
use NMFS\Sync\Items\Articles\Article_Sync_Helpers;
use NMFS\Sync\Items\Articles\Articles;
use NMFS\Sync\Items\Articles\Stock\Stock;
use NMFS\Sync\Items\Groups\Groups;
use NMFS\Sync\Items\Groups\Sync_Groups as Group_Sync;
use Northmill_Online_Shared_Sync\Northmill_Online_Shared\Abstracts\Hub_API\Models\Hub_API_Properties;
use Northmill_Online_Shared_Sync\Northmill_Online_Shared\Modules\Loggers\Log_Level_Enum;
use Northmill_Online_Shared_Sync\Northmill_Online_Shared\Modules\Sync\Bookkeeping\Article_Account_Codes\Article_Account_Codes;
use WC_Data_Exception;
use WC_Product;
use WC_Product_Factory;
use WC_Product_Variation;

defined( 'ABSPATH' ) || exit;

/**
 * Model class.
 */
class Article_Model {

	/**
	 * Construct model.
	 *
	 * @param string          $publish_status          WC Publish status.
	 * @param string          $sku                     Article SKU (same in WC and Hub).
	 * @param float|null      $quantity                Article stock quantity.
	 * @param string          $title                   Article title.
	 * @param bool            $manages_stock           Whether article is stock managed.
	 * @param int             $display_order           Article display order number.
	 * @param string          $description_html        HTML of description.
	 * @param string          $description_raw         Raw text description.
	 * @param null|string     $hub_category_id         Category Hub ID.
	 * @param float           $price                   Article price.
	 * @param string          $article_account_code_id Hub Article account code ID.
	 * @param string|null     $barcode                 Barcode.
	 * @param array|null      $barcode_alternatives    Additional barcodes.
	 * @param null|array      $images                  Product images.
	 * @param bool            $order_ticket_dialogue   Whether order ticket dialogue is enabled.
	 * @param array           $order_ticket_text       Order ticket text options.
	 * @param string|null     $info_updated_time       When article was last updated in Hub.
	 * @param string|null     $quantity_updated_time   When stock was changed in Hub.
	 * @param string|false    $stored_hash             Article hash stored in WC.
	 * @param null|WC_Product $wc_product              WC product instance, if already existing.
	 * @param string          $hub_type                Article type in Hub.
	 * @param bool            $is_web_shop_article     Whether article is Web shop article in Hub.
	 * @param null|array      $hub_api_data            Data received from Hub, if model is constructed from API response. Mainly for use in hooks.
	 * @param string          $unit                    Unit.
	 */
	public function __construct(
		public string $publish_status,
		public string $sku,
		public float|null $quantity,
		public string $title,
		public bool $manages_stock,
		public int $display_order,
		public string $description_html,
		public string $description_raw,
		public null|string $hub_category_id,
		public float $price,
		public string $article_account_code_id,
		public null|string $barcode,
		public null|array $barcode_alternatives,
		public null|array $images,
		public bool $order_ticket_dialogue,
		public array $order_ticket_text,
		public string|null $info_updated_time,
		public string|null $quantity_updated_time,
		public string|false $stored_hash = false,
		public ?WC_Product $wc_product = null,
		public string $hub_type = Hub_API_Properties::ARTICLE_TYPE_GOODS,
		public bool $is_web_shop_article = true,
		public ?array $hub_api_data = null,
		public string $unit = 'St'
	) {}

	/**
	 * Transform product to data for API.
	 *
	 * @param bool $creating_new_article Whether we are creating a new article, and not updating an existing one.
	 *
	 * @return array
	 *
	 * @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found
	 */
	public function get_data_to_send_to_api( bool $creating_new_article ): array {

		$article = array(
			Hub_API_Properties::ARTICLE_NUMBER             => $this->sku,
			Hub_API_Properties::ARTICLE_UNIT               => $this->unit,
			Hub_API_Properties::ARTICLE_UPDATE_ON_EXISTING => true,
			Hub_API_Properties::ARTICLE_IS_WEB_SHOP_ARTICLE => $this->is_web_shop_article,
			Hub_API_Properties::ARTICLE_ACCOUNT_CODE_ID    => $this->article_account_code_id,
			Hub_API_Properties::ARTICLE_TYPE               => $this->hub_type,
			Hub_API_Properties::ARTICLE_IS_STOCK_ITEM      => $this->manages_stock,
		);

		if ( $creating_new_article || Article_Sync_Helpers::is_title_sync_active() ) {
			$article[ Hub_API_Properties::ARTICLE_NAME ] = $this->title;
		}

		if ( $creating_new_article || Article_Sync_Helpers::is_display_order_sync_active() ) {
			$article[ Hub_API_Properties::ARTICLE_DISPLAY_ORDER ] = $this->display_order;
		}

		if ( $creating_new_article || Article_Sync_Helpers::is_price_sync_active() ) {
			$article[ Hub_API_Properties::ARTICLE_RETAIL_PRICE ] = $this->price;
		}

		if ( Articles::PRODUCT_STATUS_PUBLISH === $this->publish_status ) {
			$article[ Hub_API_Properties::ARTICLE_IS_ACTIVE ] = true;
		}

		if ( $this->manages_stock && null !== $this->quantity ) {
			$article[ Hub_API_Properties::ARTICLE_QUANTITY ] = $this->quantity;
		}

		if ( ! $this->stored_hash ) {
			// Set new products as Accumulative.
			$article[ Hub_API_Properties::ARTICLE_ACCUMULATIVE ] = true;
		}

		return $article;
	}
	// phpcs:enable Generic.CodeAnalysis.UnusedFunctionParameter.Found

	/**
	 * Whether article has all required data for sending to Hub.
	 *
	 * @return bool
	 */
	public function is_full_product(): bool {
		if (
			empty( $this->sku ) ||
			empty( $this->article_account_code_id ) ||
			empty( $this->hub_type )
		) {
			return false;
		}

		return true;
	}

	/**
	 * Get values used to generate hash.
	 *
	 * @return array
	 */
	public function get_values_for_hash(): array {
		$values = array();

		$values[] = $this->sku;
		$values[] = Article_Sync_Helpers::is_title_sync_active() ? $this->title : '';
		$values[] = Article_Sync_Helpers::is_display_order_sync_active() ? $this->display_order : '';
		$values[] = $this->unit;
		$values[] = ( Articles::PRODUCT_STATUS_PUBLISH === $this->publish_status ) ? '1' : '0';
		$values[] = $this->manages_stock ? '1' : '0';
		$values[] = $this->article_account_code_id;
		$values[] = Article_Sync_Helpers::is_price_sync_active() ? $this->price : '';
		// Skipped recommended price skipped for now.
		$values[] = $this->hub_type;

		return $values;
	}

	/**
	 * Generate hash used to see if article has changed.
	 *
	 * @return false|string
	 */
	public function generate_article_hash(): false|string {
		$values          = $this->get_values_for_hash();
		$complied_string = implode( '', $values );

		return wp_hash( $complied_string, 'nonce' );
	}

	/**
	 * Trigger storing article in WC.
	 *
	 * @return int|null WC product ID, or null on failure.
	 */
	public function store_in_wc(): int|null {

		try {
			if ( empty( $this->wc_product ) ) {
				return $this->add_in_wc();
			} else {
				return $this->update_in_wc();
			}
		} catch ( Exception $e ) {
			Shared::instance()->logger->log(
				'sync',
				'Error when storing product',
				array(
					'sku'      => $this->sku,
					'error'    => $e->getMessage(),
					'updating' => ! empty( $this->wc_product ),
				),
				Log_Level_Enum::ERROR
			);
		}

		return null;
	}

	/**
	 * Create product in WooCommerce, from Hub.
	 *
	 * @return int New product id.
	 *
	 * @throws WC_Data_Exception On invalid data.
	 */
	private function add_in_wc(): int {
		$this->wc_product = new WC_Product();

		/**
		 * Title.
		 */
		$this->wc_product->set_name( $this->title );

		/**
		 * SKU.
		 */
		$this->wc_product->set_sku( $this->sku );

		/**
		 * Featured.
		 */
		$this->wc_product->set_featured( 'no' );

		/**
		 * Visibility
		 */
		$this->wc_product->set_catalog_visibility( 'visible' );

		/**
		 * Publish status.
		 */
		if (
			Article_Sync_Helpers::is_publish_status_sync_active() &&
			Hub_API_Properties::ARTICLE_TYPE_GOODS === $this->hub_type
		) {
			$this->wc_product->set_status( $this->publish_status );
		} else {
			$this->wc_product->set_status( Articles::PRODUCT_STATUS_DRAFT );
		}

		/**
		 * WC core article account code.
		 */
		$tax_class_slug = Article_Account_Codes::get_slug_by_id( $this->article_account_code_id );
		if ( Article_Sync_Helpers::is_tax_class_sync_active() ) {
			if ( ! empty( $tax_class_slug ) ) {
				$this->wc_product->set_tax_class( $tax_class_slug );
				$this->wc_product->set_tax_status( 'taxable' );
			} else {
				$this->wc_product->set_tax_status( 'none' );
				Shared::instance()->logger->log(
					'sync',
					'Tax class not found for account code',
					array(
						'account_code' => $this->article_account_code_id,
						'product'      => $this->wc_product->get_id(),
					),
					Log_Level_Enum::ERROR
				);
			}
		}

		$this->update_common_order_data_pre_save_in_wc();
		$product_id = $this->wc_product->save();

		/**
		 * Reload product
		 */
		$this->wc_product = wc_get_product( $product_id );

		/**
		 * WC Product type.
		 */
		if ( Hub_API_Properties::ARTICLE_TYPE_GOODS === $this->hub_type ) {
			wp_set_object_terms( $product_id, Articles::CORE_TAX_PROD_TYPE_VALUE_SIMPLE, Articles::CORE_TAX_PROD_TYPE );
		}

		/**
		 * Stock.
		 */
		if (
			Hub_API_Properties::ARTICLE_TYPE_GOODS === $this->hub_type &&
			$this->quantity_updated_time !== $this->info_updated_time
		) {
			Stock::get_and_update_article_stock_data( $this->sku );
		}

		$this->update_order_meta_post_save( true );

		/**
		 * Action triggered when an article has been created from Hub.
		 *
		 * @param int $product_id WC product id.
		 * @param array|null $hub_api_data Data from API response, if applicable.
		 * @param Article_Model $this Article model instance.
		 *
		 * @used Customize order
		 */
		do_action( 'northmill_sync_article_added', $product_id, $this->hub_api_data, $this );

		return $product_id;
	}

	/**
	 * Update product in WooCommerce, from Hub.
	 *
	 * @return int Product ID.
	 *
	 * @throws WC_Data_Exception On invalid data.
	 */
	private function update_in_wc(): int {
		/**
		 * Title.
		 */
		if ( Article_Sync_Helpers::is_title_sync_active() ) {
			$this->wc_product->set_name( $this->title );
		}

		/**
		 * Publish status.
		 */
		if (
			Article_Sync_Helpers::is_publish_status_sync_active() &&
			Hub_API_Properties::ARTICLE_TYPE_GOODS === $this->hub_type
		) {
			$old_status = $this->wc_product->get_status();
			$this->wc_product->set_status( $this->publish_status );

			if (
				Articles::PRODUCT_STATUS_PUBLISH === $this->publish_status &&
				Articles::PRODUCT_STATUS_PUBLISH !== $old_status &&
				$this->quantity_updated_time !== $this->info_updated_time
			) {
				/**
				 * Stock.
				 * Stock status might have changed when the article wasn't active.
				 * If updated times above are the same a separate stock sync will be triggered.
				 */
				Stock::get_and_update_article_stock_data( $this->sku );
			}
		}

		/**
		 * WC core article account code.
		 */
		$tax_class_slug = Article_Account_Codes::get_slug_by_id( $this->article_account_code_id );
		if (
			Article_Sync_Helpers::is_tax_class_sync_active() &&
			! empty( $tax_class_slug )
		) {
			$this->wc_product->set_tax_class( $tax_class_slug );
			$this->wc_product->set_tax_status( 'taxable' );
		}

		$this->update_common_order_data_pre_save_in_wc();
		$this->wc_product->save();
		$this->update_order_meta_post_save( false );

		/**
		 * WC Product type.
		 */
		$product_type = WC_Product_Factory::get_product_type( $this->wc_product->get_id() );

		if (
			Hub_API_Properties::ARTICLE_TYPE_GOODS === $this->hub_type &&
			! in_array( $product_type, array( 'simple', 'composite', 'variable', 'variation' ), true )
		) {
			wp_set_object_terms( $this->wc_product->get_id(), Articles::CORE_TAX_PROD_TYPE_VALUE_SIMPLE, Articles::CORE_TAX_PROD_TYPE );

			delete_post_meta( $this->wc_product->get_id(), Articles::CORE_META_KEY_DOWNLOADABLE, 'no' );
			delete_post_meta( $this->wc_product->get_id(), Articles::CORE_META_KEY_VIRTUAL, 'yes' );
			delete_post_meta( $this->wc_product->get_id(), Articles::CORE_META_KEY_MANAGE_STOCK, 'no' );
			delete_post_meta( $this->wc_product->get_id(), Articles::CORE_META_KEY_STOCKSTATUS, Articles::CORE_META_VALUE_IN_STOCK );

			Shared::instance()->logger->log(
				'sync',
				'Product type change to Simple',
				array(
					'product_id'    => $this->wc_product->get_id(),
					'previous_type' => $product_type,
				),
				Log_Level_Enum::NOTICE
			);
		}

		/**
		 * Action triggered when an article has been updated from Hub.
		 *
		 * @param int $product_id WC product id.
		 * @param array|null $hub_api_data Data from API response, if applicable.
		 * @param Article_Model $this Article model instance.
		 *
		 * @used Customize order
		 */
		do_action( 'northmill_sync_article_updated', $this->wc_product->get_id(), $this->hub_api_data, $this );

		return $this->wc_product->get_id();
	}

	/**
	 * Run product data updates shared between adding and updating articles.
	 *
	 * @return void
	 */
	private function update_common_order_data_pre_save_in_wc(): void {
		/**
		 * Display order.
		 */
		if ( Article_Sync_Helpers::is_display_order_sync_active() ) {
			$this->wc_product->set_menu_order( $this->display_order );
		}

		/**
		 * Description.
		 */
		if ( Article_Sync_Helpers::is_desc_sync_active() ) {
			$this->wc_product->set_description( $this->description_html );
		}

		/**
		 * Price.
		 */
		if ( Article_Sync_Helpers::is_price_sync_active() ) {
			$woocommerce_prices_include_tax = get_option( 'woocommerce_prices_include_tax' );
			$woocommerce_calc_taxes_enable  = get_option( 'woocommerce_calc_taxes' );

			$product_price = $this->price;
			if (
				Article_Sync_Helpers::is_tax_class_sync_active() &&
				'no' === $woocommerce_prices_include_tax &&
				'yes' === $woocommerce_calc_taxes_enable
			) {
				$tax_vat_rate  = Article_Account_Codes::get_vat_rate_by_id( $this->article_account_code_id );
				$tax_amount    = $this->price - ( $this->price / ( ( $tax_vat_rate / 100 ) + 1 ) );
				$product_price = $this->price - $tax_amount;
			}

			$this->wc_product->set_regular_price( $product_price );
		}
	}

	/**
	 * Run product metadata updates shared between adding and updating articles.
	 *
	 * @param bool $is_adding If we are currently adding a product (false means updating product).
	 *
	 * @return void
	 */
	private function update_order_meta_post_save( bool $is_adding ): void {

		$is_variation = $this->wc_product instanceof WC_Product_Variation;

		/**
		 * Barcodes
		 */
		// Remove all since we don't know which ones are new.
		if ( ! $is_adding && get_post_meta( $this->wc_product->get_id(), '_custom_product_barcodes_field' ) ) {
			delete_post_meta( $this->wc_product->get_id(), '_custom_product_barcodes_field' );
		}

		if ( ! empty( $this->barcode ) ) {
			add_post_meta( $this->wc_product->get_id(), '_custom_product_barcodes_field', $this->barcode );
		}

		if ( ! empty( $this->barcode_alternatives ) ) {
			foreach ( $this->barcode_alternatives as $barcode ) {
				if ( ! empty( $barcode['Barcode'] ) ) {
					$alt_bar_code = $barcode['Barcode'];
					add_post_meta( $this->wc_product->get_id(), '_custom_product_barcodes_field', $alt_bar_code );
				}
			}
		}

		/**
		 * Images
		 */
		if ( ! $is_variation ) {
			if ( Article_Sync_Helpers::is_image_sync_active() && ! empty( $this->images ) ) {
				Article_Assets::upload_images( $this->wc_product->get_id(), $this->images );
			}
		} else { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedElse
			/**
			 * TODO: get main image and add to thumbnail and rest of image save it main products
			 */
		}

		/**
		 * Order ticket text
		 */
		update_post_meta( $this->wc_product->get_id(), Articles::META_KEY_ORDER_TICKET_DIALOGUE, $this->order_ticket_dialogue );
		update_post_meta( $this->wc_product->get_id(), Articles::META_KEY_ORDER_TICKET_TEXT, $this->order_ticket_text );

		/**
		 * Product category
		 */
		self::update_product_group( $this->wc_product->get_id(), $this->hub_category_id );
	}

	/**
	 * Updates the product group to the Hub group if it exists.
	 * TODO: Update to handle variations?
	 *
	 * @param int    $product_id   WC Product ID.
	 * @param string $hub_group_id Hub Group ID.
	 */
	private static function update_product_group( int $product_id, string $hub_group_id ): void {
		if ( ! Groups::is_enabled() ) {
			return;
		}

		update_post_meta( $product_id, Articles::META_KEY_GROUP_ID, $hub_group_id );
		$terms = array();
		if ( ! empty( $hub_group_id ) ) {
			$terms = get_terms(
				array(
					'taxonomy'   => 'product_cat',
					'meta_query' => array(
						array(
							'key'   => Group_Sync::META_KEY_GROUP_ID,
							'value' => $hub_group_id,
						),
					),
					'fields'     => 'ids',
					'number'     => 1,
					'hide_empty' => false,
				)
			);
		}
		wp_set_object_terms( $product_id, $terms, 'product_cat' );
	}
}
