<?php

namespace App\Models;

use Carbon\Carbon;
use Clarkeash\Doorman\Facades\Doorman;
use Clarkeash\Doorman\Models\Invite;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Mail;
use Leenooks\Casts\LeenooksCarbon;
use Leenooks\Traits\ScopeActive;

use App\Casts\CollectionOrNull;
use App\Interfaces\IDs;
use App\Mail\{InvoiceEmail,InvoiceGeneratedAdmin};
use App\Traits\{PushNew,SiteID};

/**
 * Class Invoice
 * Invoices that belong to an Account
 *
 * Attributes for services:
 * + created_at         : Date the invoice was created
 * + due                : Balance due on an invoice
 * + due_at             : Date the invoice is due
 * + lid                : Local ID for invoice
 * + paid               : Total of payments received (excluding pending)
 * + paid_date          : Date the invoice was paid in full
 * + paid_pending       : Total of pending payments received
 * + sid                : System ID for invoice
 * + sub_total          : Invoice sub-total before taxes
 * + tax_total          : Invoices total of taxes
 * + total              : Invoice total
 *
 * @package App\Models
 */
class Invoice extends Model
{
	use PushNew,ScopeActive,SiteID;

	protected $casts = [
		'created_at' => 'datetime:Y-m-d',
		'due_at' => LeenooksCarbon::class,
		'reminders' => CollectionOrNull::class,
		'_paid_at' => 'datetime:Y-m-d',
	];

	public const BILL_WEEKLY = 0;
	public const BILL_MONTHLY = 1;
	public const BILL_QUARTERLY = 2;
	public const BILL_SEMI_YEARLY = 3;
	public const BILL_YEARLY = 4;
	public const BILL_TWOYEARS = 5;
	public const BILL_THREEYEARS = 6;
	public const BILL_FOURYEARS = 7;
	public const BILL_FIVEYEARS = 8;

	/* Our available billing periods */
	public const billing_periods = [
		self::BILL_WEEKLY => [
			'name' => 'Weekly',
			'interval' => 0.25,
		],
		self::BILL_MONTHLY => [
			'name' => 'Monthly',
			'interval' => 1,
		],
		self::BILL_QUARTERLY => [
			'name' => 'Quarterly',
			'interval' => 3,
		],
		self::BILL_SEMI_YEARLY => [
			'name' => 'Semi-Annually',
			'interval' => 6,
		],
		self::BILL_YEARLY => [
			'name' => 'Annually',
			'interval' => 12,
		],
		self::BILL_TWOYEARS => [
			'name' => 'Two years',
			'interval' => 24,
		],
		self::BILL_THREEYEARS => [
			'name' => 'Three Years',
			'interval' => 36,
		],
		self::BILL_FOURYEARS => [
			'name' => 'Four Years',
			'interval' => 48,
		],
		SELF::BILL_FIVEYEARS => [
			'name' => 'Five Years',
			'interval' => 60,
		],
	];

	// Our related items that need to be updated when we call pushNew()
	protected $pushable = ['items_active'];

	protected $with = [
		'items_active:id,start_at,stop_at,quantity,price_base,discount_amt,item_type,product_id,service_id,invoice_id',
		'items_active.taxes:id,invoice_item_id,amount,tax_id',
		'items_active.product:id',
		'items_active.product.translate:id,product_id,name_short,name_detail',
		'payment_items_active:id,amount,payment_id,invoice_id',
		'payment_items_active.payment:id,paid_at',
	];

	public function __get($key): mixed
	{
		return match ($key) {
			'lid' => sprintf('%06s',$this->id),
			'sid' => sprintf('%02s-%04s-%s',$this->site_id,$this->account_id,$this->lid),

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

	/* STATIC METHODS */

	public static function boot()
	{
		parent::boot();

		static::created(function($model) {
			// Send an email to an admin that the invoice was created
			$uo = User::where('email',config('osb.admin'))->sole();

			Mail::to($uo->email)
				->send(new InvoiceGeneratedAdmin($model));

			// @todo Queue an email to the user
		});
	}

	/**
	 * This works out what multiplier to use to change billing periods
	 *
	 * @param int $source
	 * @param int $target
	 * @return float
	 */
	public static function billing_change(int $source,int $target): float
	{
		return Arr::get(self::billing_periods,$target.'.interval')/Arr::get(self::billing_periods,$source.'.interval');
	}

	/**
	 * Return the name for the billing interval
	 *
	 * @param int $interval
	 * @return string
	 */
	public static function billing_name(int $interval): string
	{
		$interval = collect(self::billing_periods)->get($interval);

		return Arr::get($interval,'name','Unknown');
	}

	/**
	 * Return the number of months in the billing interval
	 *
	 * @param int $interval
	 * @return int
	 */
	public static function billing_period(int $interval): int
	{
		$interval = collect(self::billing_periods)->get($interval);

		return Arr::get($interval,'interval',0);
	}

	/**
	 * Given a contract in months, this will calculate the number of billing intervals required
	 *
	 * @param int $contract_term
	 * @param int $source
	 * @return int
	 */
	public static function billing_term(int $contract_term,int $source): int
	{
		return ceil(($contract_term ?: 1)/(Arr::get(self::billing_periods,$source.'.interval') ?: 1));
	}

	/**
	 * Work out the time period for a particular date and invoice period
	 *
	 * @param \Leenooks\Carbon $date
	 * @param int $interval
	 * @param bool $strict
	 * @return Collection
	 * @throws \Exception
	 */
	public static function invoice_period(Carbon $date,int $interval,bool $strict): Collection
	{
		$date_start = $date->clone();
		$date_end = $date->clone();

		switch ($interval) {
			case self::BILL_WEEKLY:
				$result = collect([
					'start' => $strict
						? $date_start->startOfWeek()
						: $date_start,
					'end'=> $strict
						? $date_end->endOfWeek()
						: $date_end->addWeek()->subDay()
				]);
				break;

			case self::BILL_MONTHLY:
				$result = collect([
					'start' => $strict
						? $date_start->startOfMonth()
						: $date_start,
					'end' => $strict
						? $date_end->endOfMonth()
						: $date_end->addMonth()->subDay()
				]);
				break;

			case self::BILL_QUARTERLY:
				$result = collect([
					'start' => $strict// The service charges
						? $date_start->startOfQuarter()
						: $date_start,
					'end' => $strict
						? $date_end->endOfQuarter()
						: $date_end->addQuarter()->subDay()
				]);
				break;

			case self::BILL_SEMI_YEARLY:
				$result = collect([
					'start' => $strict
						? $date_start->startOfHalf()
						: $date_start,
					'end' => $strict
						? $date_end->endOfHalf()
						: $date_end->addQuarters(2)->subDay()
				]);
				break;

			case self::BILL_YEARLY:
				$result = collect([
					'start' => $strict
						? $date_start->startOfYear()
						: $date_start,
					'end' => $strict
						? $date_end->endOfYear()
						: $date_end->addYear()->subDay()
				]);
				break;

			case self::BILL_TWOYEARS:
				if (! $strict) {
					$result = collect([
						'start' => $date_start,
						'end' => $date_end->addYears(2)->subDay(),
					]);

				} else {
					$data_end = $date_end->addYears(2)->subDay()->endOfYear();

					// Make sure we end on an even year
					if ($data_end->clone()->addDay()->year%2)
						$data_end = $data_end->subYear();

					$result = collect([
						'start' => $data_end->clone()->subYears(2)->addDay(),
						'end' => $data_end,
					]);
				}
				break;

			// NOTE: price_recur_strict ignored
			case self::BILL_THREEYEARS:
				$result = collect([
					'start' => $date_start,
					'end' => $date_end->addYears(3)->subDay(),
				]);
				break;

			// NOTE: price_recur_strict ignored
			case self::BILL_FOURYEARS:
				$result = collect([
					'start' => $date_start,
					'end' => $date_end->addYears(4)->subDay(),
				]);
				break;

			// NOTE: price_recur_strict ignored
			case self::BILL_FIVEYEARS:
				$result = collect([
					'start' => $date_start,
					'end' => $date_end->addYears(5)->subDay(),
				]);
				break;

			default:
				throw new \Exception('Unknown recur_schedule: '.$interval);
		}

		return $result;
	}

	/**
	 * @param \Leenooks\Carbon $start Start Date
	 * @param Carbon $end End Date
	 * @param Collection $period
	 * @return float
	 * @throws \Exception
	 */
	public static function invoice_quantity(Carbon $start,Carbon $end,Collection $period): float
	{
		if ($start->lessThan(Arr::get($period,'start')) || $end->greaterThan(Arr::get($period,'end')))
			throw new \Exception('Billing Period differ');

		$d = Arr::get($period,'start')->diffInDays(Arr::get($period,'end'));
		if (! $d)
			throw new \Exception('Start and End period dates cannot be the same');

		return round(($d-Arr::get($period,'start')->diffInDays($start)-$end->diffInDays(Arr::get($period,'end')))/$d,2);
	}

	/* RELATIONS */

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

	/**
	 * Items on this invoice belongs to
	 */
	public function items()
	{
		return $this->hasMany(InvoiceItem::class)
			->with(['taxes','product']);
	}

	/**
	 * Active items on this invoice belongs to
	 */
	public function items_active()
	{
		return $this->items()
			->where('active',TRUE);
	}

	/**
	 * Payments applied to this invoice
	 */
	public function payments()
	{
		return $this->hasManyThrough(Payment::class,PaymentItem::class,NULL,'id',NULL,'payment_id')
			->where('payments.active',TRUE);
	}

	/**
	 * Payment items attached to this invoice
	 */
	public function payment_items()
	{
		return $this->hasMany(PaymentItem::class);
	}

	public function payment_items_active()
	{
		return $this->payment_items()
			->where('payment_items.active',TRUE);
	}

	/**
	 * 3rd party provider details to this invoice (eg: accounting providers)
	 */
	public function providers()
	{
		return $this->belongsToMany(ProviderOauth::class,'invoice__provider')
			->where('invoice__provider.site_id',$this->site_id)
			->withPivot('ref','synctoken','created_at','updated_at');
	}

	/* SCOPES */

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

	/* ATTRIBUTES */

	/**
	 * Balance due on an invoice
	 * @return float
	 */
	public function getDueAttribute(): float
	{
		return round($this->getTotalAttribute()-$this->getPaidAttribute(),2);
	}

	/**
	 * Total of payments received for this invoice
	 * excluding pending payments
	 *
	 * @return float
	 */
	public function getPaidAttribute(): float
	{
		return $this->payment_items_active->sum('amount');
	}

	/**
	 * Get the date that the invoice was paid in full.
	 * We assume the last payment received pays it in full, if its fully paid.
	 *
	 * @return Carbon|null
	 */
	public function getPaidDateAttribute(): ?Carbon
	{
		// If the invoice still has a due balance, its not paid
		if ($this->getDueAttribute())
			return NULL;

		$o = $this
			->payments
			->filter(fn($item)=>(! $item->pending_status))
			->last();

		return $o?->paid_at;
	}

	/**
	 * Total of pending payments received for this invoice
	 *
	 * @return mixed
	 */
	public function getPaidPendingAttribute(): float
	{
		return $this->payment_items
			->filter(fn($item)=>$item->payment->pending_status)
			->sum('amount');
	}

	/**
	 * Get invoice subtotal before taxes
	 *
	 * @return float
	 */
	public function getSubTotalAttribute(): float
	{
		return $this->items_active->sum('sub_total');
	}

	/**
	 * Get the invoices taxes total
	 *
	 * @return float
	 */
	public function getTaxTotalAttribute(): float
	{
		return $this->items_active->sum('tax');
	}

	/**
	 * Invoice total due
	 *
	 * @return float
	 */
	public function getTotalAttribute(): float
	{
		return $this->getSubTotalAttribute()+$this->getTaxTotalAttribute();
	}

	/* METHODS */

	/**
	 * Return a download link for non-auth downloads
	 *
	 * @return string
	 */
	public function download_link(): string
	{
		// Re-use an existing code
		$io = Invite::where('for',$this->account->user->email)->first();

		$tokendate = ($x=Carbon::now()->addDays(21)) > ($y=$this->due_at->addDays(21)) ? $x : $y;

		// Extend the expire date
		if ($io && ($tokendate > $io->valid_until)) {
			$io->valid_until = $tokendate;
			$io->save();
		}

		$code = (! $io)
			? Doorman::generate()
				->for($this->account->user->email)
				->uses(0)
				->expiresOn($tokendate)
				->make()
				->first()
				->code
			: $io->code;

		return url('u/invoice',[$this->id,'email',$code]);
	}

	/**
	 * Return all the items on an invoice for a particular service and product
	 *
	 * @param Product $po
	 * @param Service $so
	 * @return Collection
	 */
	public function product_service_items(Product $po,Service $so): Collection
	{
		return $this
			->items_active
			->filter(fn($item)=>($item->product_id === $po->id) && ($item->service_id === $so->id))
			->sortBy('item_type');
	}

	/**
	 * @param string $key
	 * @return array
	 * @todo Ugly hack to update reminders
	 */
	public function reminders(string $key): array
	{
		$r = $this->reminders;
		if (! Arr::get($r,$key)) {
			$r[$key] = time();
		}

		return $r;
	}

	/**
	 * Automatically set our due_at at save time.
	 *
	 * @param array $options
	 * @return bool
	 * @todo Change this to a saving event
	 */
	public function save(array $options = [])
	{
		// Automatically set the date_due attribute for new records.
		if (! $this->exists AND ! $this->due_at) {
			$this->due_at = $this->items->min('start_at');

			// @todo This 7 days should be sysetm configurable
			if (($x=Carbon::now()->addDay(7)) > $this->due_at)
				$this->due_at = $x;
		}

		return parent::save($options);
	}

	/**
	 * Record the invoice being sent
	 *
	 * @return int
	 */
	public function send(): int
	{
		$result = Mail::to($this->account->user->email)
			->send(new InvoiceEmail($this));

		$this->print_status = TRUE;

		if ($this->reminders->has('sent'))
			$this->reminders->put('sent',collect($this->reminders->get('sent')));
		else
			$this->reminders->put('sent',collect());

		$this->reminders->get('sent')->push(Carbon::now());

		return $result;
	}

	/**
	 * Group the invoice items by product ID, returning the number of products and total
	 *
	 * @return Collection
	 */
	public function summary_products(): Collection
	{
		$return = collect();

		foreach ($this->items_active->groupBy('product_id') as $id => $o) {
			if (! $id) {
				$po = new Product;
				$po->translate = new ProductTranslate;
				$po->translate->name_detail = 'Miscellaneous';

			} else {
				$po = $o->first()->product;
			}

			$po->count = count($o->pluck('service_id')->unique());

			$return->push([
				'product' => $po,
				'services' => $o->pluck('service_id')->unique(),
				'sub_total' => $o->sum('sub_total'),
				'tax_total' => $o->sum('tax'),
				'total' => $o->sum('total'),
			]);
		}

		return $return->sortBy('product.name');
	}

	public function summary_other(): Collection
	{
		$result = collect();

		foreach ($this->items_active->whereNull('service_id') as $o) {
			dd($o);
			$result->push([
				'description' => 'Account Items',
				'sub_total' => $o->sub_total,
				'tax_total' => $o->tax,
				'total' => $o->total,
			]);
		}

		return $result;
	}
}