<?php

namespace App\Models;

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\DB;
use Laravel\Passport\HasApiTokens;

use Leenooks\Carbon;
use Leenooks\Traits\UserSwitch;
use Spinen\QuickBooks\HasQuickBooksToken;

use App\Notifications\ResetPassword as ResetPasswordNotification;
use App\Traits\SiteID;

class User extends Authenticatable
{
	use HasApiTokens,Notifiable,UserSwitch,HasQuickBooksToken,SiteID;

	protected $appends = [
		'active_display',
		'services_count_html',
		'surfirstname',
		'switch_url',
		'user_id_url',
	];

	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',
	];

	protected $visible = [
		'active_display',
		'id',
		'level',
		'services_count_html',
		'switch_url',
		'surfirstname',
		'user_id_url',
	];

	protected $with = ['accounts'];

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

	/* RELATIONS */

	/**
	 * The accounts that this user manages
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasMany
	 */
	public function accounts()
	{
		return $this->hasMany(Account::class);
	}

	/**
	 * The agents that this users manages
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasMany
	 */
	public function agents() {
		return $this->hasMany(static::class,'parent_id','id')->with('agents');
	}

	/**
	 * The clients that this user has
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasMany
	 */
	public function clients() {
		return $this
			->hasMany(static::class,'parent_id','id')
			->with('clients');
	}

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

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

	/**
	 * The payments this user has made
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
	 */
	public function payments()
	{
		return $this->hasManyThrough(Payment::class,Account::class);
	}

	/**
	 * THe services this user has
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
	 */
	public function services()
	{
		return $this->hasManyThrough(Service::class,Account::class)
			->active();
	}

	/**
	 * The site this user is configured to access
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
	 */
	public function site()
	{
		return $this->belongsTo(Site::class);
	}

	/**
	 * This users supplier/reseller
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
	 */
	protected function supplier()
	{
		return $this->belongsTo(static::class,'parent_id','id');
	}

	/**
	 * Who this user supplies to
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasMany
	 */
	protected function suppliers() {
		return $this->hasMany(static::class,'parent_id','id');
	}

	/* ATTRIBUTES */

	public function getActiveDisplayAttribute($value)
	{
		return sprintf('<span class="btn-sm btn-block btn-%s text-center">%s</span>',$this->active ? 'primary' : 'danger',$this->active ? 'Active' : 'Inactive');
	}

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

	/**
	 * A list of all invoices currently unpaid
	 *
	 * @return mixed
	 */
	public function getInvoicesDueAttribute()
	{
		return $this->invoices
			->where('active',TRUE)
			->sortBy('id')
			->transform(function ($item) { if ($item->due > 0) return $item; })
			->reverse()
			->filter();
	}

	/**
	 * Return a Carbon Date if it has a value.
	 *
	 * @param $value
	 * @return Carbon
	 * @throws \Exception
	 * @todo This attribute is not in the schema
	 */
	public function getLastAccessAttribute($value)
	{
		if (! is_null($value))
			return new Carbon($value);
	}

	/**
	 * @deprecated Use static::getFullNameAttribute()
	 * @return mixed
	 */
	public function getNameAttribute()
	{
		return $this->full_name;
	}

	/**
	 * Return a list of the payments that the user has made
	 *
	 * @return mixed
	 * @todo Merge this with payments()
	 */
	public function getPaymentHistoryAttribute()
	{
		return $this->payments
			->sortBy('payment_date')
			->reverse();
	}

	public function getServicesCountHtmlAttribute()
	{
		return sprintf('%s <small>/%s</small>',$this->services->where('active',TRUE)->count(),$this->services->count());
	}

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

	public function getSwitchUrlAttribute()
	{
		return sprintf('<a href="/a/switch/start/%s"><i class="fas fa-external-link-alt"></i></a>',$this->id);
	}

	public function getUserIdAttribute()
	{
		return sprintf('%02s-%04s',$this->site_id,$this->id);
	}

	public function getUserIdUrlAttribute()
	{
		return sprintf('<a href="/u/account/view/%s">%s</a>',$this->id,$this->user_id);
	}

	/* METHODS */

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

	/* SCOPES */

	// @todo use trait
	public function scopeActive()
	{
		return $this->where('active',TRUE);
	}

	/**
	 * Search for a record
	 *
	 * @param		$query
	 * @param string $term
	 * @return
	 */
	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('id','like','%'.$term.'%');

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

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

		return $query;
	}

	/* GENERAL METHODS */

	/**
	 * Determine if the user is an admin of the user with $id
	 *
	 * @param $id
	 * @return bool
	 */
	public function isAdmin($id): bool
	{
		return $id AND $this->isReseller() AND in_array($id,$this->all_accounts()->pluck('user_id')->toArray());
	}

	/**
	 * Get a list of accounts for the clients of this user
	 *
	 * @return DatabaseCollection
	 */
	public function all_accounts(): DatabaseCollection
	{
		$result = new DatabaseCollection();
		$clients = $this->all_clients();

		foreach ($clients->pluck('accounts') as $accounts) {
			foreach ($accounts as $o) {
				if (! $o->active)
					continue;

				$result->push($o);
			}
		}

		// Include my accounts
		foreach ($this->accounts as $o) {
			if (! $o->active)
				continue;

			$result->push($o);
		}

		$result->load('user.accounts');

		return $result;
	}

	/**
	 * Get a list of clients that this user is responsible for.
	 *
	 * @param int $level
	 * @return Collection
	 */
	public function all_clients($level=0,DatabaseCollection $clients=NULL): DatabaseCollection
	{
		$result = is_null($clients) ? $this->clients : $clients;

		$result
			->filter(function($item) { return $item->active; })
			->transform(function($item) use ($level) { $item->level = $level; return $item; });

		foreach ($result->pluck('clients') as $clients) {
			foreach ($this->all_clients($level+1,$clients) as $o) {
				if (! $o->active)
					continue;

				$result->push($o);
			}
		}

		return $result;
	}

	public function all_client_service_inactive()
	{
		$s = Service::InActive();
		$aa = $this->all_accounts()->pluck('id')->unique()->toArray();

		return $s->get()->filter(function($item) use ($aa) {
			return in_array($item->account_id,$aa);
		});
	}

	/**
	 * List of all this users agents, recursively
	 *
	 * @param int $level
	 * @return Collection
	 */
	public function all_agents($level=0)
	{
		$result = collect();

		foreach ($this->agents as $o) {
			if (! $o->active OR ! $o->agents->count())
				continue;

			$o->level = $level;

			$result->push($o);

			// Include agents of agents
			$result->push($o->all_agents($level+1));
		}

		return $result->flatten();
	}

	/**
	 * 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()
			->authorised($this)
			->where('order_status','!=','ACTIVE')
			->with(['account','product'])
			->get();
	}

	/**
	 * 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;

		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.description',
			'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('ab_invoice_item')
			->select([
				'invoice_id',
				DB::raw('ab_invoice_item.id AS invoice_item_id'),
				DB::raw('IFNULL(ab_invoice_item.discount_amt,0) AS discount'),
				DB::raw('ROUND(CAST(quantity*price_base AS decimal(8,2)),2) AS base'),
				DB::raw('ROUND(ab_invoice_item_tax.amount,2) AS tax'),

			])
			->leftjoin('ab_invoice_item_tax',['ab_invoice_item_tax.invoice_item_id'=>'ab_invoice_item.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(alloc_amt) AS allocate'),
			])
			->where('alloc_amt','>',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('ab_invoice',['ab_invoice.id'=>'II.invoice_id'])
			->whereIN('account_id',$this->all_accounts()->pluck('id')->unique()->toArray())
			->where('ab_invoice.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->all_accounts()->pluck('id')->unique()->toArray())
			//->where('payments.active',TRUE) // @todo To implement
			->groupBy(['invoice_id']);

		$summary = (new Invoice)
			->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_date',
				'date_orig',
				'discount',
				'invoice_base',
				'invoice_tax',
				'invoice_total',
				'payments',
				'payment_fees',
				DB::raw('ROUND(invoice_total-payments,2) AS balance'),
			])
			->join('ab_invoice',['ab_invoice.id'=>'invoice_id'])
			->from($summary,'summary');
	}

	public function query_payment_summary()
	{
		$payment = (new Payment)
			->select([
				DB::raw('payment_id AS id'),
				DB::raw('SUM(allocate) AS allocate'),

			])
			->from($this->query_payment_items(),'PI')
			//->where('payments.active',TRUE) // @todo To implement
			->groupBy(['payment_id']);

		return (new Payment)
			->select([
				DB::raw('payments.id AS id'),
				'date_orig',
				'payment_date',
				'total_amt',
				//'fees_amt',
				DB::raw('total_amt-allocate AS balance'),
			])
			->rightJoin('payments',['payments.id'=>'summary.id'])
			//->where('payments.active',TRUE) // @todo To implement
			->whereIN('account_id',$this->all_accounts()->pluck('id')->unique()->toArray())
			->from($payment,'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()
	{
		// If I have agents and no parent, I am the wholesaler
		if (is_null($this->parent_id) AND ($this->all_agents()->count() OR $this->all_clients()->count()))
			return 'wholesaler';

		// If I have agents and a parent, I am a reseller
		elseif ($this->parent_id AND ($this->all_agents()->count() OR $this->all_clients()->count()))
			return 'reseller';

		// If I have no agents and a parent, I am a customer
		elseif (! $this->all_agents()->count() AND ! $this->all_clients()->count())
			return 'customer';
	}
}