<?php

namespace App\Models;

use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\File;
use Intuit\Exceptions\NotTokenException;
use Intuit\Traits\ProviderTokenTrait;
use Leenooks\Traits\ScopeActive;

use App\Casts\CollectionOrNull;
use App\Interfaces\{IDs,ProductItem};
use App\Traits\ProviderRef;

/**
 * Class Product
 * Products that are available to sale, and appear on invoices.
 *
 * Products have one Type (Product/*), made of an Offering (Supplier/*) from a Supplier.
 * Conversely, Suppliers provide Offerings (Supplier/*) which belong to a Type (Product/*) of a Product.
 *
 * So each product attribute has:
 * + supplied				: Supplier product provided for this offering (Supplier/*)
 * + type					: Returns the underlying product object, representing the type of product (Product/*)
 *
 * Attributes for products:
 * + lid                    : Local ID for product (part number)
 * + sid                    : System ID for product (part number)
 *
 * + base_charge			: Default billing amount
 * + base_cost				: Cost for this service
 *
 * + billing_interval		: Its the max of what we define, or what the supplier bills us at
 *
 * + category				: Type of product supplied
 * + category_lc			: Category name in lower case
 * + category_name			: Type of product supplied (Friendly Name for display, not for internal logic)
 *
 * + contract_term			: Contract term for this product
 *
 * + description			: Product description (description.description_full => description_full)
 * + has_usage				: Does this product instrument usage
 *
 * + min_cost				: Minimum cost for this product
 *
 * + name					: Details of our product (description.description_short => name_detail)
 * + pid					: Product ID for our Product (description.name => name_short)
 *
 * + setup_cost				: Charge by supplier to setup this product
 *
 * + supplied				: Suppliers product supplied
 * + supplier				: Supplier for this offering
 *
 * Attributes for product types (type - Product/*)
 * + name					: Short Name for our Product
 * + name_long				: Long Name for our Product
 * + description			: Description of offering (Broadband=speed)
 *
 * Attributes for supplier's offerings (type->supplied - Supplier/*)
 * + name					: Short Name for suppliers offering
 * + name_long				: Long Name for suppliers offering
 * + description			: Description of offering (Broadband=speed)
 *
 * Product Pricing self::pricing is an array of:
 * [
 *   timeperiod => [
 *                    show => true|false (show this time period to the user for ordering)
 *                    group => [ pricing/setup ]
 *                 ]
 * ]
 */
class Product extends Model
{
	use HasFactory,ScopeActive,ProviderRef,ProviderTokenTrait;

	protected $casts = [
		'pricing' => CollectionOrNull::class,
	];

	public function __get($key): mixed
	{
		return match ($key) {
			'base_cost' => round($this->supplied->base_cost,2)*Invoice::billing_change($this->type->billing_interval,$this->billing_interval) ?: 0,

			'billing_interval' => max($this->price_recur_default,$this->type->billing_interval),
			'billing_interval_name' => Invoice::billing_name($this->billing_interval),

			'category' => $this->supplied->category,
			'category_lc' => strtolower($this->category),
			'category_name' => $this->supplied->category_name,

			'contract_term' => $this->type->contract_term,

			'description' => $this->translate->description,

			'has_usage' => $this->type->hasUsage(),

			'min_cost' => $this->supplied->min_cost,

			'name' => $this->translate->name_detail,

			'lid' => sprintf('%04s',$this->id),
			'pid' => $this->translate->name_short,
			'sid' => sprintf('%02s-%s',$this->site_id,$this->lid),

			'setup_cost' => $this->supplied->setup_cost ?: 0,

			'supplied' => $this->type->supplied,
			'supplier' => $this->supplied->supplier,

			default => parent::__get($key),
		};
	}

	/* STATIC */

	/**
	 * Return a list of available product types
	 *
	 * @return Collection
	 */
	public static function availableTypes(): Collection
	{
		$models = collect(File::allFiles(app_path()))
			->map(function ($item) {
				$path = $item->getRelativePathName();
				$class = sprintf('%s%s',
					Container::getInstance()->getNamespace(),
					strtr(substr($path, 0, strrpos($path, '.')), '/', '\\'));

				return $class;
			})
			->filter(function ($class) {
				$valid = FALSE;

				if (class_exists($class)) {
					$reflection = new \ReflectionClass($class);
					$valid = $reflection->isSubclassOf(ProductItem::class) && (! $reflection->isAbstract());
				}

				return $valid;
			});

		return $models->values();
	}

	/* RELATIONS */

	public function providers()
	{
		return $this->belongsToMany(ProviderOauth::class,'product__provider')
			->where('product__provider.site_id',$this->site_id)
			->withPivot('ref');
	}

	/**
	 * Which services are configured with this product
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasMany
	 */
	public function services()
	{
		return $this->hasMany(Service::class);
	}

	/**
	 * Get the product name in the users language, and if the user isnt logged in, the sites language
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasOne
	 */
	public function translate()
	{
		return $this->hasOne(ProductTranslate::class)
			->where('language_id',(Auth::user() && Auth::user()->language_id)
					? Auth::user()->language_id
					: config('osb.language_id'));
	}

	/**
	 * Return a child model with details of the service
	 * This will return a product/* model.
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\MorphTo
	 */
	public function type()
	{
		return $this->morphTo(null,'model','model_id');
	}

	/* METHODS */

	/**
	 * Get a charge value from the pricing array
	 *
	 * This will traverse the groups to the parent group to find an appropriate charge, if one is not found,
	 * it will extrapolate a charge from the parent group, from another time period.
	 *
	 * @param string $type
	 * @param int|NULL $timeperiod
	 * @param Group|NULL $go
	 * @return float
	 */
	private function _charge(string $type,?int $timeperiod=NULL,?Group $go=NULL): float
	{
		// We'll cache this for performance
		static $default = NULL;

		// All public users
		if (is_null($default))
			$default = Group::whereNull('parent_id')->firstOrFail();

		if (! $go)
			$go = $default;

		if (is_null($timeperiod))
			$timeperiod = $this->billing_interval;

		// If the price doesnt exist for $go->id, use $go->id = 0 which is all users.
		if (! $price=$this->charge($timeperiod,$go,$type)) {
			$alt_tp = $timeperiod;

			// Traverse up our timeperiods if the default group to see if there is another time period that has the value
			while (is_null($price=$this->charge($alt_tp,$default,$type)) && ($alt_tp >= 0))
				$alt_tp--;

			// If we havent got a price, we'll extrapolate one, except for setup charges
			if (! is_null($price) && ($alt_tp !== $timeperiod) && ($type !== 'setup'))
				$price = $price*Invoice::billing_change($alt_tp,$timeperiod);
		}

		return round($price,2);
	}

	public function accounting(): Collection
	{
		$so = ProviderOauth::where('name',self::provider)
			->sole();

		if (! ($to=$so->token(Auth::user())))
			throw new NotTokenException(sprintf('Unknown Tokens for [%s]',Auth::user()->email));

		return $to
			->API()
			->getItems()
			->pluck('pid','Id')
			->map(fn($item,$value)=>['id'=>$value,'value'=>$item])
			->values();
	}

	/**
	 * The amount we invoice each time period for this service
	 *
	 * @param int|NULL $timeperiod
	 * @param Group|NULL $go
	 * @return float
	 */
	public function base_charge(?int $timeperiod=NULL,?Group $go=NULL): float
	{
		return $this->_charge('base',$timeperiod,$go);
	}

	/**
	 * Return the charge from the pricing table for the specific time period and group
	 *
	 * @param int $timeperiod
	 * @param Group $go
	 * @param string $type
	 * @return float|null
	 */
	public function charge(int $timeperiod,Group $go,string $type): ?float
	{
		return Arr::get($this->pricing,sprintf('%d.%d.%s',$timeperiod,$go->id,$type));
	}

	/**
	 * Do we have a charge for specific group/period
	 *
	 * @param int $timeperiod
	 * @return bool
	 */
	public function charge_available(int $timeperiod): bool
	{
		return Arr::get($this->pricing,sprintf('%d.show',$timeperiod),FALSE);
	}

	/**
	 * Get the minimum charge for this product
	 *
	 * @param int|null $timeperiod
	 * @param Group|null $go
	 * @return float
	 */
	public function min_charge(?int $timeperiod=NULL,?Group $go=NULL): float
	{
		return $this->setup_charge($timeperiod,$go)
			+ $this->base_charge($timeperiod,$go)*Invoice::billing_change($this->billing_interval,$this->type->billing_interval)*$this->contract_term;
	}

	/**
	 * When receiving an order, validate that we have all the required information for the product type
	 *
	 * @param Request $request
	 * @return Model|null
	 */
	public function orderValidation(Request $request): ?Model
	{
		return $this->type->orderValidation($request);
	}

	/**
	 * The charge to setup this service
	 *
	 * @param int|null $timeperiod
	 * @param Group|null $go
	 * @return float
	 */
	public function setup_charge(?int $timeperiod=NULL,?Group $go=NULL): float
	{
		return $this->_charge('setup',$timeperiod,$go);
	}
}