<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\Collection as DatabaseCollection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Session;
use Laravel\Passport\HasApiTokens;
use Leenooks\Traits\ScopeActive;
use Leenooks\Traits\UserSwitch;

use App\Interfaces\IDs;
use App\Models\Scopes\SiteScope;
use App\Notifications\ResetPassword as ResetPasswordNotification;
use App\Traits\{QueryCacheableConfig,SiteID};

/**
 * Class User
 *
 * Attributes for users:
 * + role				: User's role
 */
class User extends Authenticatable implements IDs
{
	use HasFactory,HasApiTokens,Notifiable,UserSwitch,QueryCacheableConfig,SiteID,ScopeActive;

	private const CACHE_TIME = 3600;

	protected $dates = [
		'created_at',
		'updated_at',
		'last_access'
	];

	/**
	 * The attributes that are mass assignable.
	 *
	 * @var array
	 */
	protected $fillable = [
		'name',
		'email',
		'password',
	];

	/**
	 * The attributes that should be hidden for arrays.
	 *
	 * @var array
	 */
	protected $hidden = [
		'password',
		'remember_token',
	];

	/**
	 * Role hierarchy order
	 *
	 * @var array
	 */
	public const role_order = [
		'wholesaler',
		'reseller',
		'customer',
	];

	/* OVERRIDES */

	/**
	 * Users password reset email notification
	 *
	 * @param string $token
	 */
	public function sendPasswordResetNotification($token)
	{
		$this->notify((new ResetPasswordNotification($token))->onQueue('high'));
	}

	/* INTERFACES */

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

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

	/* RELATIONS */

	/**
	 * The accounts that this user manages
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasMany
	 * @note This cannot be loaded with "with"?
	 */
	public function accounts()
	{
		return $this->hasMany(Account::class)
			->orWhereIn('id',$this->rtm_accounts()->pluck('id'))
			->active();
	}

	/**
	 * This users invoices
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
	 * @deprecated Accounts have invoices, not users
	 */
	public function invoices()
	{
		return $this->hasManyThrough(Invoice::class,Account::class)
			->active();
	}

	/**
	 * This users language configuration
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
	 */
	public function language()
	{
		return $this->belongsTo(Language::class);
	}

	/**
	 * Return the routes to market account for this user
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasOneThrough
	 */
	public function rtm()
	{
		return $this->hasOneThrough(Rtm::class,Account::class);
	}

	/**
	 * The services this user has
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
	 * @deprecated Accounts have services, not users
	 */
	public function services()
	{
		return $this->hasManyThrough(Service::class,Account::class)
			->with(['product.type'])
			->active();
	}

	/**
	 * Supplier configuration for this user
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
	 * @deprecated Move to account->suppliers()
	 */
	public function suppliers()
	{
		return $this->belongsToMany(Supplier::class)
			->where('supplier_user.site_id',$this->site_id)
			->withPivot('id','created_at');
	}

	/* ATTRIBUTES */

	/**
	 * This is an alias method, as it is used by the framework
	 *
	 * @return string
	 */
	public function getNameAttribute(): string
	{
		return $this->full_name;
	}

	/**
	 * Logged in users full name
	 *
	 * @return string
	 */
	public function getFullNameAttribute(): string
	{
		return sprintf('%s %s',$this->firstname,$this->lastname);
	}

	/**
	 * Return my accounts, but only those accounts with the same group_id
	 *
	 * @note Users can only manage accounts with the same group ID, thereby ensuring they dont see different
	 *		pricing options - since prices can be controlled by groups
	 * @return Collection
	 */
	public function getMyAccountsAttribute(): Collection
	{
		$result = $this->accounts->where('user_id',$this->id);

		if (! $result->count())
			return $result;

		return $this->isReseller() ? $result : $result->groupBy('group.id')->first();
	}

	/**
	 * Return a friendly string of this persons role
	 * @return string
	 */
	public function getRoleAttribute(): string
	{
		return ucfirst($this->role());
	}

	public function getSurFirstNameAttribute()
	{
		return sprintf('%s, %s',$this->lastname,$this->firstname);
	}

	/* SCOPES */

	/**
	 * Search for a record
	 *
	 * @param		$query
	 * @param string $term
	 */
	public function scopeSearch($query,string $term)
	{
		// Build our where clause
		// First Name, Last name
		if (preg_match('/\ /',$term)) {
			[$fn,$ln] = explode(' ',$term,2);

			$query->where(function($query1) use ($fn,$ln,$term) {
				$query1->where(function($query2) use ($fn,$ln) {
					return $query2
						->where('firstname','like','%'.$fn.'%')
						->where('lastname','like','%'.$ln.'%');
				});
			});

		} elseif (is_numeric($term)) {
			$query->where('users.id','like','%'.$term.'%');

		} elseif (preg_match('/\@/',$term)) {
			$query->where('email','like','%'.$term.'%');

		} else {
			$query
				->Where('firstname','like','%'.$term.'%')
				->orWhere('lastname','like','%'.$term.'%');
		}

		return $query;
	}

	/* METHODS */

	/**
	 * Show this user's clients with service movements
	 *
	 * A service movement, is an active service where the status is not ACTIVE
	 *
	 * @return DatabaseCollection
	 */
	public function client_service_movements(): DatabaseCollection
	{
		return Service::active()
			->serviceUserAuthorised($this)
			->where('order_status','!=','ACTIVE')
			->with(['account','product'])
			->get();
	}

	/**
	 * Determine if the user is an admin of the user with $id
	 *
	 * @param User|null $user
	 * @return bool
	 */
	public function isAdmin(User $user=NULL): bool
	{
		return $user->exists AND $this->isReseller() AND $this->accounts->pluck('user_id')->contains($user->id);
	}

	/**
	 * Determine if the logged in user is a reseller or wholesaler
	 *
	 * @return bool
	 */
	public function isReseller(): bool
	{
		return in_array($this->role(),['wholesaler','reseller']);
	}

	/**
	 * Determine if the logged in user is a wholesaler
	 *
	 * @return bool
	 */
	public function isWholesaler(): bool
	{
		return in_array($this->role(),['wholesaler']);
	}

	/**
	 * Get all the items for the next invoice
	 *
	 * @param bool $future
	 * @return DatabaseCollection
	 */
	public function next_invoice_items(bool $future=FALSE): DatabaseCollection
	{
		$result = new DatabaseCollection;
		$this->services->load(['invoice_items.taxes']);

		foreach ($this->services as $o) {
			if ($future) {
				if ($o->invoice_next->subDays(config('app.invoice_inadvance'))->isPast())
					continue;

			} else {
				if ($o->invoice_next->subDays(config('app.invoice_inadvance'))->isFuture())
					continue;
			}

			foreach ($o->next_invoice_items($future) as $oo)
				$result->push($oo);
		}

		$result->load([
			'product.translate',
			'service.type',
		]);

		return $result;
	}

	/**
	 * Return an SQL query that will return a list of invoices
	 *
	 * @return \Illuminate\Database\Query\Builder
	 */
	private function query_invoice_items()
	{
		return DB::table('invoice_items')
			->select([
				'invoice_id',
				DB::raw('invoice_items.id AS invoice_item_id'),
				DB::raw('IFNULL(invoice_items.discount_amt,0) AS discount'),
				DB::raw('ROUND(CAST(quantity*price_base AS decimal(8,2)),2) AS base'),
				DB::raw('ROUND(invoice_item_taxes.amount,2) AS tax'),

			])
			->leftjoin('invoice_item_taxes',['invoice_item_taxes.invoice_item_id'=>'invoice_items.id'])
			->where('active',TRUE);
	}

	/**
	 * Return an SQL query that will return payment summaries by invoices.
	 *
	 * @return \Illuminate\Database\Query\Builder
	 */
	private function query_payment_items()
	{
		return DB::table('payment_items')
			->select([
				'payment_id',
				'invoice_id',
				DB::raw('SUM(amount) AS allocate'),
			])
			->where('amount','>',0)
			->groupBy(['invoice_id','payment_id']);
	}

	/**
	 * Return an SQL query that will summarise invoices with payments
	 *
	 * @return \Illuminate\Database\Query\Builder
	 * @todo change this to just return outstanding invoices as a collection.
	 */
	public function query_invoice_summary()
	{
		$invoices = (new Invoice)
			->select([
				'invoice_id',
				DB::raw('SUM(discount) AS discount'),
				DB::raw('SUM(base) AS base'),
				DB::raw('SUM(tax) AS tax'),
				DB::raw('ROUND(SUM(base)+SUM(tax)-SUM(discount),2) AS total'),
				DB::raw('false AS payments'),
				DB::raw('false AS payment_fees'),
			])
			->from($this->query_invoice_items(),'II')
			->join('invoices',['invoices.id'=>'II.invoice_id'])
			->whereIN('account_id',$this->accounts->pluck('id'))
			->where('invoices.active',TRUE)
			->groupBy(['invoice_id']);

		$payments = (new Payment)
			->select([
				'invoice_id',
				DB::raw('false AS discount'),
				DB::raw('false AS base'),
				DB::raw('false AS tax'),
				DB::raw('false AS total'),
				DB::raw('SUM(allocate) AS payments'),
				DB::raw('SUM(fees_amt) AS payment_fees'),
			])
			->from($this->query_payment_items(),'PI')
			->join('payments',['payments.id'=>'PI.payment_id'])
			->whereIN('account_id',$this->accounts->pluck('id'))
			//->where('payments.active',TRUE) // @todo To implement
			->groupBy(['invoice_id']);

		$summary = (new Invoice)
			->withoutGlobalScope(SiteScope::class)
			->select([
				'invoice_id',
				DB::raw('SUM(discount) AS discount'),
				DB::raw('SUM(base) AS invoice_base'),
				DB::raw('SUM(tax) AS invoice_tax'),
				DB::raw('SUM(total) AS invoice_total'),
				DB::raw('SUM(payments) AS payments'),
				DB::raw('SUM(payment_fees) AS payment_fees'),
			])
			->from($invoices->unionAll($payments),'invoices')
			->groupBy(['invoice_id']);

		return (new Invoice)
			->select([
				'account_id',
				'id',
				'due_at',
				'created_at',
				'discount',
				'invoice_base',
				'invoice_tax',
				'invoice_total',
				'payments',
				'payment_fees',
				DB::raw('ROUND(invoice_total-payments,2) AS balance'),
			])
			->join('invoices',['invoices.id'=>'invoice_id'])
			->with(['items.taxes'])
			->from($summary,'summary');
	}

	/**
	 * Determine what the logged in user's role is
	 * + Wholesaler - aka Super User
	 * + Reseller - services accounts on behalf of their customers
	 * + Customer - end user customer
	 *
	 * @return string
	 */
	public function role()
	{
		// Cache our role for this session
		$cache_key = sprintf('%s:%s:%s',$this->id,__METHOD__,Session::getId());

		return Cache::remember($cache_key,self::CACHE_TIME,function() {
			// Get the RTM for our accounts
			$rtms = Rtm::whereIn('account_id',$this->accounts->pluck('id'))->get();

			// If I have no parent, I am the wholesaler
			if ($rtms->whereNull('parent_id')->count())
				return 'wholesaler';

			// If I exist in the RTM table, I'm a reseller
			else if ($rtms->count())
				return 'reseller';

			// Otherwise a client
			else
				return 'customer';
		});
	}

	/**
	 * Return the accounts that this user can manage
	 * This method is a helper to User::accounts() - use $user->accounts instead
	 *
	 * @return Collection
	 */
	private function rtm_accounts(): Collection
	{
		return Account::whereIn('rtm_id',$this->rtm_list()->pluck('id'))
			->get();
	}

	/**
	 * Return the RTM hierarchy that this user can manage
	 *
	 * @param Rtm|null $rtm
	 * @return Collection
	 */
	public function rtm_list(Rtm $rtm=NULL): Collection
	{
		// If this user doesnt manage any accounts
		if (! $this->exists || ! $this->rtm)
			return collect();

		$list = collect();

		// Add this RTM to the list
		if (! $rtm) {
			$list->push($this->rtm);
			$children = $this->rtm->children;

		} else {
			$list->push($rtm);
			$children =$rtm->children;
		}

		// Capture any children
		foreach ($children as $child)
			$list->push($this->rtm_list($child));

		return $rtm ? $list : $list->flatten()->unique(function($item) { return $item->id; });
	}
}