<?php

namespace SureCart\Models;

use SureCart\Models\Traits\HasDates;
use SureCart\Models\Traits\HasImageSizes;
use SureCart\Models\Traits\HasPurchases;
use SureCart\Models\Traits\HasCommissionStructure;
use SureCart\Support\Contracts\GalleryItem;
use SureCart\Support\Contracts\PageModel;
use SureCart\Support\Currency;
use SureCart\Support\TimeDate;

/**
 * Product model
 */
class Product extends Model implements PageModel {
	use HasImageSizes;
	use HasPurchases;
	use HasCommissionStructure;
	use HasDates;

	/**
	 * These always need to be fetched during create/update in order to sync with post model.
	 *
	 * @var array
	 */
	protected $sync_expands = array( 'prices', 'product_medias', 'product_media.media', 'variants', 'variant_options', 'product_collections', 'featured_product_media' );

	/**
	 * Rest API endpoint
	 *
	 * @var string
	 */
	protected $endpoint = 'products';

	/**
	 * Object name
	 *
	 * @var string
	 */
	protected $object_name = 'product';

	/**
	 * Is this cachable?
	 *
	 * @var boolean
	 */
	protected $cachable = true;

	/**
	 * Clear cache when products are updated.
	 *
	 * @var string
	 */
	protected $cache_key = 'products';

	/**
	 * Create a new model
	 *
	 * @param array $attributes Attributes to create.
	 *
	 * @return $this|false
	 */
	protected function create( $attributes = array() ) {
		// create the model.
		$created = parent::create( $attributes );
		if ( is_wp_error( $created ) ) {
			return $created;
		}

		// sync with the post.
		$this->sync();

		// return.
		return $this;
	}

	/**
	 * Update a model
	 *
	 * @param array $attributes Attributes to update.
	 *
	 * @return $this|false
	 */
	protected function update( $attributes = array() ) {
		// update the model.
		$updated = parent::update( $attributes );
		if ( is_wp_error( $updated ) ) {
			return $updated;
		}

		// sync with the post.
		$this->sync();

		// return.
		return $this;
	}

	/**
	 * Immediately sync with a post.
	 *
	 * @param string $id The id of the product to sync.
	 *
	 * @return \WP_Post|\WP_Error|self
	 */
	protected function sync( $id = '' ) {
		// set the id.
		if ( ! empty( $id ) ) {
			$this->id = $id;
		}

		// we need an id.
		if ( empty( $this->id ) ) {
			return new \WP_Error( 'missing_id', __( 'Missing ID', 'surecart' ) );
		}

		// if there are no syncable expands, let's fetch them.
		$this->with( $this->sync_expands )->where( array( 'cached' => false ) )->find( $this->id );

		// sync the product.
		$synced = \SureCart::sync()->product()->sync( $this );

		// on success, cancel any queued syncs.
		if ( is_wp_error( $synced ) ) {
			return $synced;
		}

		return $this;
	}

	/**
	 * Delete the synced post.
	 *
	 * @param string $id The id of the model to delete.
	 * @return \SureCart\Models\Product
	 */
	protected function deleteSynced( $id = '' ) {
		$id = ! empty( $id ) ? $id : $this->id;
		\SureCart::sync()
			->product()
			->delete( $id );

		return $this;
	}

	/**
	 * Queue a sync process with a post.
	 *
	 * @param boolean $show_notice Whether to show a notice.
	 *
	 * @return \SureCart\Background\QueueService
	 */
	protected function queueSync( $show_notice = false ) {
		\SureCart::sync()
			->product()
			->withNotice( $show_notice )
			->queue( $this );

		return $this;
	}

	/**
	 * Maybe queue a sync job if updated_at is different
	 * than the product post updated_at.
	 *
	 * @return \SureCart\Background\QueueService|false Whether the sync was queued.
	 */
	protected function maybeQueueSync() {
		// already in sync.
		if ( $this->synced ) {
			return false;
		}

		return $this->queueSync();
	}

	/**
	 * Update a model
	 *
	 * @param string $id The id of the model to delete.
	 * @return $this|false
	 */
	protected function delete( $id = '' ) {
		// delete the model.
		$deleted = parent::delete( $id );

		// check for errors.
		if ( is_wp_error( $deleted ) ) {
			return $deleted;
		}

		// delete the post.
		$this->deleteSynced( $id );

		// return.
		return $this;
	}

	/**
	 * Get the attached post.
	 *
	 * @return int|false
	 */
	public function getPostAttribute() {
		return \SureCart::sync()->product()->post()->findByModelId( $this->id );
	}

	/**
	 * Get the is synced attribute.
	 *
	 * @return bool
	 */
	protected function getSyncedAttribute() {
		// we don't have a post.
		if ( empty( $this->post ) ) {
			return false;
		}

		// the post is trashed.
		if ( 'trash' === $this->post->post_status ) {
			return false;
		}

		// this doesn't have updated at.
		if ( empty( $this->updated_at ) ) {
			return false;
		}

		// get the product and decode it.
		$product = get_post_meta( $this->post->ID, 'product', true );
		$product = is_string( $product ) ? json_decode( get_post_meta( $this->post->ID, 'product', true ) ) : $product;
		$product = (object) $product;
		if ( empty( $product ) || ! isset( $product->updated_at ) ) {
			return false;
		}

		// sync if updated at is different.
		return $this->updated_at === $product->updated_at;
	}

	/**
	 * Check if model has syncable expands as properties
	 *
	 * @return bool
	 */
	protected function getHasSyncableExpandsAttribute() {
		foreach ( $this->sync_expands as $expand ) {
			// if expand contains a ., let's ignore it for now.
			if ( false !== strpos( $expand, '.' ) ) {
				return true;
			}
			if ( ! isset( $this->$expand ) ) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Get the sync expands.
	 *
	 * @return array
	 */
	protected function getSyncExpands() {
		return $this->sync_expands;
	}

	/**
	 * Maybe queue a sync job if updated_at is different
	 * than the product post updated_at.
	 *
	 * @param string $value The updated_at value.
	 *
	 * @return void
	 */
	public function setUpdatedAtAttribute( $value ) {
		$this->attributes['updated_at'] = apply_filters( "surecart/$this->object_name/attributes/updated_at", $value, $this );
		$this->maybeQueueSync();
	}

	/**
	 * Image srcset.
	 *
	 * @return string
	 */
	public function getImageSrcsetAttribute() {
		if ( empty( $this->attributes['image_url'] ) ) {
			return '';
		}
		return $this->imageSrcSet( $this->attributes['image_url'] );
	}

	/**
	 * Get the image url for a specific size.
	 *
	 * @param integer $size The size.
	 *
	 * @return string
	 */
	public function getImageUrl( $size = 0 ) {
		if ( empty( $this->attributes['image_url'] ) ) {
			return '';
		}
		return $size ? $this->imageUrl( $this->attributes['image_url'], $size ) : $this->attributes['image_url'];
	}

	/**
	 * Set the prices attribute.
	 *
	 * @param  object $value Array of price objects.
	 * @return void
	 */
	public function setPricesAttribute( $value ) {
		$this->setCollection( 'prices', $value, Price::class );
	}

	/**
	 * Set the product collections attribute
	 *
	 * @param object $value Product collections.
	 * @return void
	 */
	public function setProductCollectionsAttribute( $value ) {
		$this->setCollection( 'product_collections', $value, ProductCollection::class );
	}

	/**
	 * Set the variants attribute.
	 *
	 * @param  object $value Array of price objects.
	 * @return void
	 */
	public function setVariantsAttribute( $value ) {
		$this->setCollection( 'variants', $value, Variant::class );
	}

	/**
	 * Set the variants attribute.
	 *
	 * @param  object $value Array of price objects.
	 * @return void
	 */
	public function setVariantOptionsAttribute( $value ) {
		$this->setCollection( 'variant_options', $value, VariantOption::class );
	}

	/**
	 * Set the featured product media attribute.
	 *
	 * @param  string $value Product properties.
	 * @return void
	 */
	public function setFeaturedProductMediaAttribute( $value ) {
		$this->setRelation( 'featured_product_media', $value, ProductMedia::class );
	}

	/**
	 * Set the product media attribute
	 *
	 * @param  string $value ProductMedia properties.
	 * @return void
	 */
	public function setProductMediasAttribute( $value ) {
		$this->setCollection( 'product_medias', $value, ProductMedia::class );
	}

	/**
	 * Buy link model
	 *
	 * @return \SureCart\Models\BuyLink
	 */
	public function buyLink() {
		return new BuyLink( $this );
	}

	/**
	 * Checkout Permalink.
	 *
	 * @return string
	 */
	public function getCheckoutPermalinkAttribute() {
		return $this->buyLink()->url();
	}

	/**
	 * Get the product permalink.
	 *
	 * @return string
	 */
	public function getPermalinkAttribute(): string {
		return ! empty( $this->post ) ? get_the_permalink( $this->post->ID ) : '';
	}

	/**
	 * Is the post published?
	 *
	 * @return string
	 */
	public function getIsPublishedAttribute(): bool {
		return ! empty( $this->post ) && 'publish' === $this->post->post_status;
	}

	/**
	 * Get the page title.
	 *
	 * @return string
	 */
	public function getPageTitleAttribute(): string {
		return $this->metadata->page_title ?? $this->name ?? '';
	}

	/**
	 * Get the meta description.
	 *
	 * @return string
	 */
	public function getMetaDescriptionAttribute(): string {
		return $this->metadata->meta_description ?? $this->description ?? '';
	}

	/**
	 * Get the product in stock attribute.
	 *
	 * @param Product $product The product.
	 *
	 * @return bool
	 */
	public function getInStockAttribute(): bool {
		if ( ! $this->stock_enabled ) {
			return true;
		}

		if ( $this->allow_out_of_stock_purchases ) {
			return true;
		}

		return $this->available_stock > 0;
	}

	/**
	 * Return attached active prices.
	 *
	 * @return array
	 */
	public function getActivePricesAttribute() {
		$active_prices = array_values(
			array_filter(
				$this->prices->data ?? array(),
				function ( $price ) {
					return ! $price->archived;
				}
			)
		);

		usort(
			$active_prices,
			function ( $a, $b ) {
				if ( $a->position == $b->position ) {
					return 0;
				}
				return ( $a->position < $b->position ) ? -1 : 1;
			}
		);

		return $active_prices;
	}

	/**
	 * Get the has multiple prices attribute.
	 *
	 * @return boolean
	 */
	public function getHasMultiplePricesAttribute() {
		return count( $this->active_prices ) > 1;
	}

	/**
	 * Return attached active prices.
	 */
	public function activeAdHocPrices() {
		return array_filter(
			$this->active_prices ?? array(),
			function ( $price ) {
				return $price->ad_hoc;
			}
		);
	}

	/**
	 * Get the featured image attribute.
	 *
	 * @return \SureCart\Support\Contracts\GalleryItem|null;
	 */
	public function getFeaturedImageAttribute() {
		$gallery = array_values( $this->gallery ?? array() );

		if ( ! empty( $gallery ) ) {
			return $gallery[0] ?? null;
		}
		if ( empty( $this->featured_product_media ) ) {
			return null;
		}
		if ( ! is_a( $this->featured_product_media, \SureCart\Models\ProductMedia::class ) ) {
			return null;
		}
		return new GalleryItemProductMedia( $this->featured_product_media );
	}

	/**
	 * Returns the product media image attributes.
	 *
	 * @return \SureCart\Support\Contracts\GalleryItem|null;
	 */
	public function getFeaturedMediaAttribute() {
		return $this->featured_product_image;
	}

	/**
	 * Get the product template id.
	 *
	 * @return string
	 */
	public function getTemplateIdAttribute(): string {
		if ( ! empty( $this->metadata->wp_template_id ) ) {
			// we have a php file, switch to default.
			if ( wp_is_block_theme() && false !== strpos( $this->metadata->wp_template_id, '.php' ) ) {
				return 'single-sc_product';
			}

			// this is acceptable.
			return $this->metadata->wp_template_id;
		}

		return '';
	}

	/**
	 * Get with sorted prices.
	 *
	 * @return this
	 */
	public function withSortedPrices() {
		if ( empty( $this->prices->data ) ) {
			return $this;
		}

		$filtered = clone $this;

		// Sort prices by position.
		usort(
			$filtered->prices->data,
			function ( $a, $b ) {
				return $a->position - $b->position;
			}
		);

		return $filtered;
	}

	/**
	 * Get product with acgive and sorted prices.
	 *
	 * @return this
	 */
	public function withActivePrices() {
		if ( empty( $this->prices->data ) ) {
			return $this;
		}

		$filtered = clone $this;

		// Filter out archived prices.
		$filtered->prices->data = array_values(
			array_filter(
				$filtered->prices->data ?? array(),
				function ( $price ) {
					return ! $price->archived;
				}
			)
		);

		return $filtered;
	}

	/**
	 * Get the trial text attribute.
	 *
	 * @return string
	 */
	public function getTrialTextAttribute() {
		return $this->initial_price ? $this->initial_price->trial_text ?? '' : '';
	}

	/**
	 * Get the billing interval attribute.
	 *
	 * @return string
	 */
	public function getBillingIntervalTextAttribute() {
		return $this->initial_price ? $this->initial_price->interval_text ?? '' : '';
	}

	/**
	 * Get the setup fee attribute
	 *
	 * @return string
	 */
	public function getSetupFeeTextAttribute() {
		return $this->initial_price ? $this->initial_price->setup_fee_text ?? '' : '';
	}

	/**
	 * Is the product or any variants in stock.
	 *
	 * @return int
	 */
	public function getHasUnlimitedStockAttribute() {
		if ( empty( $this->stock_enabled ) ) {
			return true;
		}
		return $this->allow_out_of_stock_purchases;
	}

	/**
	 * Get the first variant with stock.
	 *
	 * @return \SureCart\Models\Variant;
	 */
	public function getFirstVariantWithStockAttribute() {
		return $this->in_stock_variants[0] ?? null;
	}

	/**
	 * Get the in stock variants.
	 *
	 * @return array
	 */
	public function getInStockVariantsAttribute() {
		if ( ! $this->has_unlimited_stock && ! empty( $this->variants->data ) ) {
			return array_map(
				function ( $variant ) {
					return $variant->available_stock > 0;
				},
				$this->variants->data,
			);
		}
		return $this->variants->data ?? null;
	}

	/**
	 * Get the initial price.
	 *
	 * @return string
	 */
	public function getInitialPriceAttribute() {
		$prices        = $this->active_prices ?? array();
		$initial_price = $prices[0] ?? null;
		return $initial_price;
	}

	/**
	 * Get the initial variant.
	 *
	 * @return string
	 */
	public function getInitialVariantAttribute() {
		$initial_variant = $this->first_variant_with_stock;
		if ( ! empty( $initial_variant ) ) {
			return $initial_variant;
		}
		return $this->variants->data[0] ?? null;
	}

	/**
	 * Get the initial amount.
	 *
	 * @return string
	 */
	public function getInitialAmountAttribute() {
		$initial_price = $this->initial_price;
		if ( count( $this->active_prices ) > 1 ) {
			return $this->initial_price->amount;
		}

		$initial_variant = $this->initial_variant;
		if ( ! empty( $initial_variant->amount ) ) {
			return $initial_variant->amount;
		}

		return $initial_price->amount ?? null;
	}

	/**
	 * Get the scratch amount.
	 *
	 * @return string
	 */
	public function getScratchAmountAttribute() {
		$prices        = $this->active_prices ?? array();
		$initial_price = $prices[0] ?? null;
		return $initial_price->scratch_amount ?? null;
	}

	/**
	 * Get the scratch display amount.
	 *
	 * @return string
	 */
	public function getScratchDisplayAmountAttribute() {
		if ( empty( $this->initial_price->scratch_amount ) ) {
			return '';
		}
		return Currency::format( $this->initial_price->scratch_amount, $this->initial_price->currency );
	}

	/**
	 * Get the product template
	 *
	 * @return \WP_Template
	 */
	public function getTemplateAttribute() {
		return null;// get_block_template( $this->getTemplateIdAttribute() );
	}

	/**
	 * Get the product template id.
	 *
	 * @return string
	 */
	public function getTemplatePartIdAttribute(): string {
		if ( ! empty( $this->metadata->wp_template_part_id ) ) {
			return $this->metadata->wp_template_part_id;
		}
		return 'surecart/surecart//product-info';
	}

	/**
	 * Get the gallery ids attribute.
	 *
	 * @return array
	 */
	public function getGalleryIdsAttribute() {
		// fallback.
		if ( empty( $this->metadata->gallery_ids ) ) {
			return array_values(
				array_filter(
					array_map(
						function ( $media ) {
							return $media->id;
						},
						$this->product_medias->data ?? array()
					)
				),
			);
		}

		// gallery.
		return json_decode( $this->metadata->gallery_ids ?? '' );
	}

	/**
	 * Set the gallery ids attribute.
	 * This needs to be converted to JSON for the platform.
	 *
	 * @param array $value The gallery array.
	 * @return void
	 */
	public function setGalleryIdsAttribute( $value ) {
		$this->attributes['metadata']              = (object) ( $this->attributes['metadata'] ?? [] );
		$this->attributes['metadata']->gallery_ids = is_string( $value ) ? $value : wp_json_encode( $value );
	}

	/**
	 * Get the gallery attribute.
	 *
	 * Map the post gallery array to GalleryItem objects.
	 *
	 * @return GalleryItem[]
	 */
	public function getGalleryAttribute() {
		$cached = $this->getCachedAttribute( 'gallery' );
		if ( null !== $cached ) {
			return $cached;
		}

		$gallery = array_values(
			array_filter(
				array_map(
					function ( $id ) {
						// this is an attachment id.
						if ( is_int( $id ) ) {
							return new GalleryItemAttachment( $id );
						}

						// get the product media item that matches the id.
						$item = array_filter(
							$this->getAttribute( 'product_medias' )->data ?? array(),
							function ( $item ) use ( $id ) {
								return $item->id === $id;
							}
						);

						// get the first item.
						$item = array_shift( $item );
						if ( ! empty( $item ) ) {
							return new GalleryItemProductMedia( $item );
						}

						return null;
					},
					$this->gallery_ids
				),
				function ( $item ) {
					// it must have a src at least.
					return ! empty( $item ) && ! empty( $item->attributes()->src );
				}
			)
		);

		$this->setAttributeCache( 'gallery', $gallery );

		return $gallery;
	}

	/**
	 * Get the price display amount.
	 *
	 * @return array
	 */
	public function getDisplayAmountAttribute() {
		$prices = $this->active_prices ?? array();

		// only if we have one price.
		if ( count( $prices ) === 1 ) {
			$initial_variant = $this->first_variant_with_stock;
			if ( ! empty( $initial_variant->amount ) ) {
				return Currency::format( $initial_variant->amount, $initial_variant->currency );
			}
		}

		// we don't have an initial price.
		if ( empty( $this->initial_price ) ) {
			return '';
		}

		// return the formatted amount.
		return Currency::format( $this->initial_price->amount, $this->initial_price->currency );
	}

	/**
	 * Get Price Range Display Amount.
	 *
	 * @return string
	 */
	public function getRangeDisplayAmountAttribute() {
		// there are no metrics.
		if ( ! $this->metrics || empty( $this->metrics->min_price_amount ) || empty( $this->metrics->max_price_amount ) ) {
			return '';
		}

		// the min and max are the same.
		if ( $this->metrics->min_price_amount === $this->metrics->max_price_amount ) {
			return Currency::format( $this->metrics->min_price_amount, $this->metrics->currency );
		}

		// return the range.
		return sprintf(
		// translators: %1$1s is the min price, %2$2s is the max price.
			__(
				'%1$1s - %2$2s',
				'surecart',
			),
			Currency::format( $this->metrics->min_price_amount, $this->metrics->currency ),
			Currency::format( $this->metrics->max_price_amount, $this->metrics->currency )
		);
	}

	/**
	 * Is the product on sale?
	 *
	 * @return array
	 */
	public function getIsOnSaleAttribute() {
		return $this->initial_price->is_on_sale ?? false;
	}

	/**
	 * Get the image used in line items.
	 *
	 * @return object
	 */
	public function getLineItemImageAttribute() {
		return is_a( $this->featured_image, GalleryItem::class ) ? $this->featured_image->attributes( 'thumbnail' ) : (object) array();
	}

	/**
	 * Get the image used in line items.
	 *
	 * @return object
	 */
	public function getPreviewImageAttribute() {
		return is_a( $this->featured_image, GalleryItem::class ) ? $this->featured_image->attributes( 'medium_large' ) : (object) [];
	}

	/**
	 * Get the product page initial state
	 *
	 * @param array $args Array of arguments.
	 *
	 * @return array
	 */
	public function getInitialPageState( $args = [] ) {
		$form = \SureCart::forms()->getDefault();

		return wp_parse_args(
			$args,
			[
				'formId'          => $form->ID,
				'mode'            => \SureCart\Models\Form::getMode( $form->ID ),
				'product'         => $this,
				'prices'          => $this->active_prices,
				'selectedPrice'   => ( $this->active_prices ?? [] )[0] ?? null,
				'checkoutUrl'     => \SureCart::pages()->url( 'checkout' ),
				'variant_options' => $this->variant_options->data ?? [],
				'variants'        => $this->variants->data ?? [],
				'selectedVariant' => $this->first_variant_with_stock ?? null,
				'isProductPage'   => ! empty( get_query_var( 'surecart_current_product' )->id ),
			]
		);
	}

	/**
	 * Get the cataloged at date time attribute.
	 *
	 * @return string
	 */
	public function getCatalogedAtDateTimeAttribute() {
		return ! empty( $this->cataloged_at ) ? TimeDate::formatDateAndTime( $this->cataloged_at ) : '';
	}
}
