<?php

namespace App\Models;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Leenooks\Traits\ScopeActive;

use App\Interfaces\IDs;

/**
 * Class Account
 * Service Accounts
 *
 * Attributes for accounts:
 * + lid				: Local ID for account
 * + sid				: System ID for account
 * + name				: Account Name
 * + taxes				: Taxes Applicable to this account
 */
class Account extends Model implements IDs
{
	use HasFactory,ScopeActive;

	/* STATIC */

	/**
	 * A list of invoices that are in credit for all accounts
	 *
	 * @param Collection|NULL $invoices
	 * @return Collection
	 */
	public static function InvoicesCredit(Collection $invoices=NULL): Collection
	{
		return (new self)
			->invoiceSummaryCredit($invoices,TRUE)
			->get();
	}

	/**
	 * A list of invoices that are due for all accounts
	 *
	 * @param Collection|NULL $invoices
	 * @return Collection
	 */
	public static function InvoicesDue(Collection $invoices=NULL): Collection
	{
		return (new self)
			->invoiceSummaryDue($invoices,TRUE)
			->get();
	}

	/* INTERFACES */

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

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

	/* RELATIONS */

	/**
	 * Charges assigned to this account
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasMany
	 */
	public function charges()
	{
		return $this->hasMany(Charge::class);
	}

	/**
	 * Country this account belongs to
	 */
	public function country()
	{
		return $this->belongsTo(Country::class);
	}

	/**
	 * Group this account is assigned to
	 * Groups are used for pricing control
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasOneThrough
	 */
	public function group()
	{
		return $this->hasOneThrough(Group::class,AccountGroup::class,'account_id','id','id','group_id');
	}

	/**
	 * Invoices created for this account
	 */
	public function invoices()
	{
		return $this->hasMany(Invoice::class)
			->with(['items.taxes','payment_items.payment']);
	}

	/**
	 * Relation to only return active invoices
	 */
	public function invoices_active()
	{
		return $this->invoices()
			->with('active',TRUE);;
	}

	/**
	 * Payments received and assigned to this account
	 */
	public function payments()
	{
		return $this->hasMany(Payment::class)
			->with(['items']);
	}

	/**
	 * Relation to only return active payments
	 */
	public function payments_active()
	{
		return $this->payments()
			->with('active',TRUE);
	}

	/**
	 * Return the link to a provider's info for this account
	 */
	public function providers()
	{
		return $this->belongsToMany(ProviderOauth::class,'account__provider')
			->withPivot('ref','synctoken','created_at','updated_at');
	}

	/**
	 * Services assigned to this account
	 */
	public function services()
	{
		return $this->hasMany(Service::class)
			->with(['product.translate','product.type.supplied']);
	}

	/**
	 * Relation to only return active services
	 */
	public function services_active()
	{
		return $this->services()
			->ServiceActive();
	}

	/**
	 * Supplier configuration for this account
	 */
	public function suppliers()
	{
		return $this->belongsToMany(Supplier::class)
			->withPivot('supplier_ref','created_at');
	}

	/**
	 * Taxes applicable for this account
	 */
	public function taxes()
	{
		return $this->hasMany(Tax::class,'country_id','country_id')
			->select(['id','zone','rate','country_id']);
	}

	/**
	 * User that owns this account
	 */
	public function user()
	{
		return $this->belongsTo(User::class);
	}

	/* SCOPES */

	/**
	 * Search for a record
	 *
	 * @param        $query
	 * @param string $term
	 * @return mixed
	 */
	public function scopeSearch($query,string $term)
	{
		// Build our where clause
		if (is_numeric($term)) {
			$query->where('id','like','%'.$term.'%');

		} else {
			$query->where('company','ilike','%'.$term.'%')
				->orWhere('address1','ilike','%'.$term.'%')
				->orWhere('address2','ilike','%'.$term.'%')
				->orWhere('city','ilike','%'.$term.'%');
		}

		return $query;
	}

	/* ATTRIBUTES */

	/**
	 * Get the address for the account
	 *
	 * @return Collection
	 */
	public function getAddressAttribute(): Collection
	{
		return collect([
				'address1' => $this->address1,
				'address2' => $this->address2,
				'location' => sprintf('%s %s  %s',
					$this->city.(($this->state || $this->zip) ? ',' : ''),
					$this->state,
					$this->zip)
			])
			->filter();
	}

	/**
	 * Return the account name
	 *
	 * @return string
	 */
	public function getNameAttribute(): string
	{
		return $this->company ?: ($this->user_id ? $this->user->getNameSurFirstAttribute() : 'LID:'.$this->id);
	}

	/**
	 * Return the type of account this is - if it has a company name, then its a business account.
	 *
	 * @return string
	 */
	public function getTypeAttribute(): string
	{
		return $this->company ? 'Business' : 'Private';
	}

	/* METHODS */

	public function invoice_next(Carbon $date=NULL): Collection
	{
		$svs = $this
			->services_active
			->filter(fn($item)=>$item->isBilled() && $item->invoice_next)
			->sortBy(fn($item)=>(string)$item->invoice_next);

		// Collect all the invoice items for our active services
		$nextdate = $date ?: $svs
			->first()
			?->invoice_next
			->clone();

		// Add any additional items that will be invoiced 30 days
		$nextitemsdate = max($nextdate,Carbon::now())
			->clone()
			->addMonth()
			->subDay()
			->endOfday();

		$items = $svs
			->filter(fn($item)=>$item->invoice_next->lessThan($nextitemsdate))
			->sortBy(fn($item)=>$item->invoice_next.$item->name)
			->map(fn($item)=>$item->next_invoice_items($nextitemsdate))
			->flatten();

		// Add any account charges (charges with no active service)
		foreach ($this->charges->filter(function($item) { return $item->unprocessed && ((! $this->service_id) || (! $item->service->isBilled())); }) as $oo) {
			$ii = new InvoiceItem;

			$ii->active = TRUE;
			$ii->service_id = $oo->service_id;
			$ii->product_id = $this->product_id;
			$ii->quantity = $oo->quantity;
			$ii->item_type = $oo->type;
			$ii->price_base = $oo->amount;
			$ii->start_at = $oo->start_at;
			$ii->stop_at = $oo->stop_at;
			$ii->module_id = 30; // @todo This shouldnt be hard coded
			$ii->module_ref = $oo->id;
			$ii->site_id = 1; // @todo

			$ii->addTaxes($this->country->taxes);
			$items->push($ii);
		}

		return $items;
	}

	/**
	 * List of invoices (summary) for this account
	 *
	 * @param Collection|NULL $invoices
	 * @param bool $all
	 * @return Builder
	 */
	public function invoiceSummary(Collection $invoices=NULL,bool $all=FALSE): Builder
	{
		return (new Invoice)
			->select([
				'invoices.account_id',
				'invoices.id as id',
				DB::raw('SUM(item) AS _item'),
				DB::raw('SUM(tax) AS _tax'),
				DB::raw('SUM(payments) AS _payment'),
				DB::raw('SUM(discount)+COALESCE(invoices.discount_amt,0) AS _discount'),
				DB::raw('SUM(item_total) AS _item_total'),
				DB::raw('SUM(payment_fees) AS _payment_fee'),
				DB::raw('ROUND(CAST(SUM(item_total)-COALESCE(invoices.discount_amt,0) AS NUMERIC),2) AS _total'),
				DB::raw('ROUND(CAST(SUM(item_total)-COALESCE(invoices.discount_amt,0)-SUM(payments) AS NUMERIC),2) AS _balance'),
				'invoices.due_at',
				'invoices.created_at',
			])
			->from(
				(new Payment)
					->select([
						'invoice_id',
						DB::raw('0 as item'),
						DB::raw('0 as tax'),
						DB::raw('0 as discount'),
						DB::raw('0 as item_total'),
						DB::raw('SUM(amount) AS payments'),
						DB::raw('SUM(fees_amt) AS payment_fees'),
					])
					->leftjoin('payment_items',['payment_items.payment_id'=>'payments.id'])
					->where('payments.active',TRUE)
					->where('payment_items.active',TRUE)
					->groupBy(['payment_items.invoice_id'])
			->union(
				(new InvoiceItem)
					->select([
						'invoice_id',
						DB::raw('ROUND(CAST(SUM(quantity*price_base) AS NUMERIC),2) AS item'),
						DB::raw('ROUND(CAST(SUM(COALESCE(amount,0)) AS NUMERIC),2) AS tax'),
						DB::raw('ROUND(CAST(SUM(COALESCE(invoice_items.discount_amt,0)) AS NUMERIC),2) AS discount'),
						DB::raw('ROUND(CAST(SUM(ROUND(CAST(quantity*(price_base-COALESCE(invoice_items.discount_amt,0)) AS NUMERIC),2))+SUM(ROUND(CAST(COALESCE(amount,0) AS NUMERIC),2)) AS NUMERIC),2) AS item_total'),
						DB::raw('0 as payments'),
						DB::raw('0 as payment_fees'),
					])
					->leftjoin('invoice_item_taxes',['invoice_item_taxes.invoice_item_id'=>'invoice_items.id'])
					->rightjoin('invoices',['invoices.id'=>'invoice_items.invoice_id'])
					->where('invoice_items.active',TRUE)
					->where(fn($query)=>$query->where('invoice_item_taxes.active',TRUE)->orWhereNull('invoice_item_taxes.active'))
					->where('invoices.active',TRUE)
					->groupBy(['invoice_items.invoice_id']),
			),'p')
			->join('invoices',['invoices.id'=>'invoice_id'])
			->when(($all === FALSE),fn($query)=>$query->where('invoices.account_id',$this->id))
			->orderBy('due_at')
			->groupBy(['invoices.account_id','invoices.id','invoices.created_at','invoices.due_at','invoices.discount_amt'])
			->with(['account']);
	}

	public function invoiceSummaryDue(Collection $invoices=NULL,bool $all=FALSE): Builder
	{
		return $this->invoiceSummary($invoices,$all)
			->havingRaw('ROUND(CAST(SUM(item_total)-COALESCE(invoices.discount_amt,0)-SUM(payments) AS NUMERIC),2) > 0');
	}

	public function invoiceSummaryCredit(Collection $invoices=NULL,bool $all=FALSE): Builder
	{
		return $this->invoiceSummary($invoices,$all)
			->havingRaw('ROUND(CAST(SUM(item_total)-COALESCE(invoices.discount_amt,0)-SUM(payments) AS NUMERIC),2) < 0');
	}

	public function invoiceSummaryPast(Collection $invoices=NULL,bool $all=FALSE): Builder
	{
		return $this->invoiceSummary($invoices,$all)
			->join('payment_items',['payment_items.invoice_id'=>'invoices.id'])
			->join('payments',['payments.id'=>'payment_items.payment_id'])
			->addSelect(DB::raw('max(paid_at) as _paid_at'))
			->havingRaw('ROUND(CAST(SUM(item_total)-COALESCE(invoices.discount_amt,0)-SUM(payments) AS NUMERIC),2) <= 0');
	}

	/**
	 * Return the taxed value of a value
	 *
	 * @param float $value
	 * @return float
	 */
	public function taxed(float $value): float
	{
		return Tax::calc($value,$this->taxes);
	}
}