<?php

namespace App\Models;

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

use App\Interfaces\{IDs,ProductItem};
use App\Traits\{ProductDetails,SiteID};

/**
 * 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.
 *
 * Attributes for products:
 * + lid                    : Local ID for product (part number)
 * + sid                    : System ID for product (part number)
 * + category				: Type of product supplied
 * + category_name			: Type of product supplied (Friendly Name for display, not for internal logic)
 * + supplied				: Supplier product provided for this offering
 * + supplier				: Supplier for this offering
 * + name					: Brief Name for our product		// @todo we should change this to be consistent with service
 * + name_short				: Product ID for our Product
 * + name_long				: Long Name for our product
 * + billing_interval		: Default Billing Interval
 * + billing_interval_string: Default Billing Interval in human-readable form
 * + setup_charge			: Charge to setup this product
 * + setup_charge_taxable	: Charge to setup this product including taxes
 * + base_charge			: Default billing amount
 * + base_charge_taxable	: Default billing amount including taxes
 * + min_charge				: Minimum cost taking into account billing interval and setup costs
 * + min_charge_taxable		: Minimum cost taking into account billing interval and setup costs including taxes
 * + type					: Returns the underlying product object, representing the type of product
 *
 * 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 ]
 *                 ]
 * ]
 *
 * @todo doesnt appear that price_type is used - but could be used to have different offering types billed differently
 * @package App\Models
 */
class Product extends Model implements IDs
{
	use HasFactory,SiteID,ProductDetails,ScopeActive;

	protected $casts = [
		'pricing'=>'collection',
	];

	protected $with = ['description'];

	/* RELATIONS */

	/**
	 * 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 description()
	{
		return $this->hasOne(ProductTranslate::class)
			->where('language_id',(Auth::user() && Auth::user()->language_id) ? Auth::user()->language_id : config('site')->language_id);
	}

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

	/**
	 * 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');
	}

	/* INTERFACES */

	public function getLIDAttribute(): string
	{
		return sprintf('%04s',$this->id);
	}

	public function getSIDAttribute(): string
	{
		return sprintf('%02s-%s',$this->site_id,$this->getLIDattribute());
	}

	/* ATTRIBUTES */

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

	/**
	 * The amount we invoice each time period for this service, including taxes
	 *
	 * @param int|null $timeperiod
	 * @param Group|null $go
	 * @param Collection|NULL $taxes
	 * @return float
	 */
	public function getBaseChargeTaxableAttribute(int $timeperiod=NULL,Group $go=NULL,Collection $taxes=NULL): float
	{
		return Tax::tax_calc($this->getBaseChargeAttribute($timeperiod,$go),$taxes ?: config('site')->taxes);
	}

	/**
	 * The base cost of this product at the appropriate billing interval
	 *
	 * @return float
	 */
	public function getBaseCostAttribute(): float
	{
		return round($this->getSuppliedAttribute()->base_cost*Invoice::billing_change($this->getSuppliedAttribute()->getBillingIntervalAttribute(),$this->getBillingIntervalAttribute()) ?: 0,2);
	}

	/**
	 * The base cost of this product at the appropriate billing interval including taxes
	 *
	 * @param Collection|NULL $taxes
	 * @return float
	 */
	public function getBaseCostTaxableAttribute(Collection $taxes=NULL): float
	{
		return Tax::tax_calc($this->getBaseCostAttribute(),$taxes ?: config('site')->taxes);;
	}

	/**
	 * Our default billing interval
	 * Its the max of what we define, or what the supplier bills us at
	 *
	 * @return int
	 */
	public function getBillingIntervalAttribute(): int
	{
		return max($this->price_recur_default,$this->getSuppliedAttribute()->getBillingIntervalAttribute());
	}

	/**
	 * Return the type of service is provided. eg: Broadband, Phone.
	 *
	 * @return string
	 */
	public function getCategoryAttribute(): string
	{
		return $this->supplied->getCategoryAttribute();
	}

	/**
	 * This will return the category of the product (eg: domain, hosting, etc) which is the basis for all
	 * other logic of these types.
	 *
	 * @return string
	 */
	public function getCategoryNameAttribute(): string
	{
		return $this->supplied->getCategoryNameAttribute();
	}

	/**
	 * How long must this product be purchased for as a service.
	 *
	 * @return int
	 */
	public function getContractTermAttribute(): int
	{
		return $this->type->getContractTermAttribute();
	}

	/**
	 * Get the minimum cost of this product
	 *
	 * @param int|null $timeperiod
	 * @param Group|null $go
	 * @return float
	 */
	public function getMinChargeAttribute(int $timeperiod=NULL,Group $go=NULL): float
	{
		return $this->getSetupChargeAttribute($timeperiod,$go)+$this->getBaseChargeAttribute($timeperiod,$go)*Invoice::billing_term($this->getContractTermAttribute(),$this->getBillingIntervalAttribute());
	}

	/**
	 * Get the minimum cost of this product with taxes
	 *
	 * @param int|null $timeperiod
	 * @param Group|null $go
	 * @param Collection|NULL $taxes
	 * @return float
	 */
	public function getMinChargeTaxableAttribute(int $timeperiod=NULL,Group $go=NULL,Collection $taxes=NULL): float
	{
		return Tax::tax_calc($this->getMinChargeAttribute($timeperiod,$go),$taxes ?: config('site')->taxes);
	}

	/**
	 * Our products short descriptive name
	 *
	 * @return string
	 */
	public function getNameAttribute(): string
	{
		return $this->description ? $this->description->description_short : 'Unknown PRODUCT';
	}

	/**
	 * Our products PID
	 *
	 * @return string
	 */
	public function getNameShortAttribute(): string
	{
		return $this->description ? $this->description->name : 'Unknown PID';
	}

	/**
	 * This product full description
	 *
	 * @return string
	 */
	public function getNameLongAttribute(): string
	{
		return $this->description->description_full;
	}

	/**
	 * Suppliers
	 *
	 * @return Model
	 */
	public function getSupplierAttribute(): ?Model
	{
		return $this->getSuppliedAttribute() ? $this->getSuppliedAttribute()->supplier_detail->supplier : NULL;
	}

	/**
	 * Suppliers product
	 *
	 * @return Model
	 */
	public function getSuppliedAttribute(): ?Model
	{
		return $this->type && $this->type->supplied ? $this->type->supplied : NULL;
	}

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

	/**
	 * The charge to setup this service including taxes
	 *
	 * @param int|null $timeperiod
	 * @param Group|null $go
	 * @param Collection|null $taxes
	 * @return float
	 */
	public function getSetupChargeTaxableAttribute(int $timeperiod=NULL,Group $go=NULL,Collection $taxes=NULL): float
	{
		return Tax::tax_calc($this->getSetupChargeAttribute($timeperiod,$go),$taxes ?: config('site')->taxes);
	}

	/**
	 * The charge to setup this service
	 *
	 * @return float
	 */
	public function getSetupCostAttribute(): float
	{
		return $this->getSuppliedAttribute()->setup_cost ?: 0;
	}

	/**
	 * The charge to setup this service
	 *
	 * @param Collection|null $taxes
	 * @return float
	 */
	public function getSetupCostTaxableAttribute(Collection $taxes=NULL): float
	{
		return Tax::tax_calc($this->getSetupCostAttribute(),$taxes ?: config('site')->taxes);;
	}

	/* METHODS */

	/**
	 * Return a list of available product types
	 *
	 * @return Collection
	 */
	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();
	}

	/**
	 * Get a charge value from the pricing array
	 *
	 * @param string $type
	 * @param int|NULL $timeperiod
	 * @param Group|NULL $go
	 * @return float
	 */
	private function getCharge(string $type,int $timeperiod=NULL,Group $go=NULL): float
	{
		static $default = NULL;
		if (! $go) {
			if (is_null($default))
				$default = Group::whereNull('parent_id')->firstOrFail();	// All public users

			$go = $default;
		}

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

		// If the price doesnt exist for $go->id, use $go->id = 0 which is all users.
		if (! $price=Arr::get($this->pricing,sprintf('%d.%d.%s',$timeperiod,$go->id,$type))) {
			$alt_tp = $timeperiod;

			while (is_null($price=Arr::get($this->pricing,sprintf('%d.%d.%s',$alt_tp,0,$type))) && ($alt_tp >= 0)) {
				$alt_tp--;
			}

			if (! is_null($price) && $alt_tp !== $timeperiod) {
				$price = $price*Invoice::billing_change($alt_tp,$timeperiod);
			}
		}

		// @todo - if price doesnt exist for the time period, reduce down to timeperiod 1 and multiply appropriately.
		if (is_null($price)) {
			Log::error(sprintf('Price is still null for [%d] timeperiod [%d] group [%d]',$this->id,$timeperiod,$go->id));

			$price = 0;
		}

		return round($price,2);
	}

	/**
	 * Return if this product captures usage data
	 *
	 * @return bool
	 */
	public function hasUsage(): bool
	{
		return $this->type && $this->type->hasUsage();
	}

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