<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;

use App\Classes\FTN\Packet;
use App\Jobs\AddressPoll;

class System extends Model
{
	use HasFactory;

	public const default = 'Discovered System';

	protected $casts = [
		'last_session' => 'datetime:Y-m-d H:i:s'
	];

	/* STATIC */

	public static function createUnknownSystem(): self
	{
		self::unguard();
		$so = self::firstOrCreate([
			'name' => self::default,
			'sysop' => 'Unknown',
			'location' => 'Unknown',
			'active' => TRUE,
		]);
		self::reguard();

		return $so;
	}

	/* SCOPES */

	/**
	 * Only query active records
	 * @todo test false action
	 */
	public function scopeActive($query)
	{
		$uo = Auth::user();

		return $query
			->when($uo && ! $uo->isAdmin(),function($query) use ($uo) {
				return $query->whereIn('id',$uo->systems->pluck('id'))
					->orWhere($this->getTable().'.active',TRUE);
			},function($query) { $query->where($this->getTable().'.active',TRUE); })
			->orderBy('name');
	}

	/* RELATIONS */

	public function addresses()
	{
		return $this->hasMany(Address::class)
			->withTrashed()
			->FTNorder();
	}

	public function akas()
	{
		return $this->hasMany(Address::class)
			->select('addresses.*')
			->join('zones',['zones.id'=>'addresses.zone_id'])
			->join('domains',['domains.id'=>'zones.domain_id'])
			->where('addresses.active',TRUE)
			->where('zones.active',TRUE)
			->where('domains.active',TRUE)
			->orderBy('domains.name')
			->with(['zone.domain'])
			->FTNorder()
			->orderBy('role','ASC');
	}

	public function mailers()
	{
		return $this->belongsToMany(Mailer::class)
			->withPivot(['last_poll','port','attempts','active']);
	}

	public function mailer_preferred()
	{
		return $this->mailers()
			->preferred();
	}

	public function logs()
	{
		return $this->hasMany(SystemLog::class);
	}

	public function logs_recent()
	{
		return $this->hasMany(SystemLog::class)
			->orderby('created_at','DESC')
			->limit(10);
	}

	/**
	 * Session Passwords for system
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
	 */
	public function sessions()
	{
		return $this->belongsToMany(Zone::class)
			->select(['id','zones.zone_id','domain_id','active'])
			->withPivot(['sespass','pktpass','ticpass','fixpass','zt_ipv4','zt_ipv6','default'])
			->dontCache();
	}

	/**
	 * If this system is configured as this host
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasOne
	 */
	public function setup()
	{
		return $this->hasOne(Setup::class);
	}

	public function users()
	{
		return $this->belongsToMany(User::class);
	}

	/**
	 * This system is the ZC for the following zones
	 */
	public function zcs()
	{
		return $this->hasMany(Zone::class);
	}

	/**
	 * Zones a system has addresses for
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
	 */
	public function zones()
	{
		return $this->hasManyThrough(Zone::class,Address::class,'system_id','id','id','zone_id');
	}

	/* ATTRIBUTES */

	public function getAccessMethodAttribute(): string
	{
		switch ($this->method) {
			case 23: return sprintf('telnet://%s:%s',$this->address,$this->port);
			case 22: return sprintf('ssh://%s:%s',$this->address,$this->port);
			case 513: return sprintf('rlogin://%s:%s',$this->address,$this->port);
			default:
				return $this->method ? sprintf('%s:%s',$this->address,$this->port) : 'No access method available.';
		}
	}

	public function getAccessMailerAttribute(): string
	{
		switch (($x=$this->mailer_preferred()->first())?->name) {
			case 'BINKP': return sprintf('binkp://%s:%s',$this->address,$x->pivot->port);
			case 'EMSI': return sprintf('emsi://%s:%s',$this->address,$x->pivot->port);
			default:
				return $x?->name ? sprintf('%s:%s',$this->address,$x->pivot->port) : 'No mailer available.';
		}
	}

	public function getIsOwnedAttribute(): bool
	{
		return $this->users->count();
	}

	public function getPktMsgsAttribute(?int $val): int
	{
		return $val ?: Setup::findOrFail(config('app.id'))->msgs_pkt;
	}

	/* METHODS */

	public function echoareas()
	{
		return Echoarea::select('echoareas.*')
			->join('address_echoarea',['address_echoarea.echoarea_id'=>'echoareas.id'])
			->join('addresses',['addresses.id'=>'address_echoarea.address_id'])
			->where('addresses.system_id',$this->id);
	}

	public function fileareas()
	{
		return Filearea::select('fileareas.*')
			->join('address_filearea',['address_filearea.filearea_id'=>'fileareas.id'])
			->join('addresses',['addresses.id'=>'address_filearea.address_id'])
			->where('addresses.system_id',$this->id);
	}

	/**
	 * Return the system name, or role name for the zone
	 *
	 * @param Address $o
	 * @return string
	 */
	public function full_name(Address $o): string
	{
		switch ($o->role_id) {
			case Address::NODE_ZC;
				return sprintf('ZC-%s-%05d',$o->zone->domain->name,$o->zone->zone_id);

			case Address::NODE_RC;
				return sprintf('RC-%s-%05d',$o->zone->domain->name,$o->region_id);

			case Address::NODE_NC;
				return sprintf('NC-%s-%05d',$o->zone->domain->name,$o->host_id);

			case Address::NODE_HC;
			case Address::NODE_NN;
			default:
				return $this->name;
		}
	}

	/**
	 * Return the system's address in the same zone
	 * This function can filter based on the address type needed.
	 *
	 * @param Zone $o
	 * @param int $type
	 * @return Collection
	 */
	public function match(Zone $o,int $type=(Address::NODE_NC|Address::NODE_HC|Address::NODE_NN|Address::NODE_POINT)): Collection
	{
		$akas = $this->akas
			->where(function($item) use($o) {
				return ($item->zone_id === $o->id) || ($item->zone->domain_id === $o->domain_id);
			});

		return ($akas->count() > 1)
			? $akas->filter(function($item) use ($type) { return $item->role_id & $type;})
			: $akas;
	}

	/**
	 * Parse the addresses and return which ones are in my zones
	 *
	 * @param \Illuminate\Database\Eloquent\Collection $addresses
	 * @param int $type
	 * @return Collection
	 */
	public function inMyZones(Collection $addresses,int $type=(Address::NODE_HC|Address::NODE_NN|Address::NODE_POINT)): Collection
	{
		$myzones = $this->addresses->pluck('zone_id')->unique();

		return $addresses->filter(function($item) use ($myzones,$type) {
			return ($item->role & $type) && ($myzones->search($item->zone_id) !== FALSE);
		});
	}

	/**
	 * Return the packet that this system uses
	 *
	 * @param Address $ao
	 * @param string|null $password
	 * @return Packet
	 */
	public function packet(Address $ao,string $password=NULL): Packet
	{
		if ($ao->system_id !== $this->id)
			throw new \Exception('Packet for [%s] is not for system [%d]',$ao->ftn,$this->id);

		return
			(new (collect(Packet::PACKET_TYPES)
				->get($this->pkt_type ?: config('fido.packet_default'))))
			->for($ao)
			->password($password);
	}

	public function poll(): ?Job
	{
		return Job::where('queue',AddressPoll::QUEUE)
			->get()
			->where(function($item) {
				return $this->akas->pluck('id')->search($item->command->address->id) !== FALSE; })
			->last();
	}

	/**
	 * Return other addresses that are no collected here, but are on the same network as us.
	 *
	 * @return \Illuminate\Database\Eloquent\Collection
	 * @throws \Exception
	 */
	public function uncommon(): Collection
	{
		$our = our_address();

		return $this->akas->filter(fn($item)=>($item->parent() && (! $our->contains($item->parent()))));
	}
}