<?php

namespace App\Models;

use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

use App\Classes\FTN\Packet;
use App\Http\Controllers\DomainController;
use App\Traits\ScopeActive;

class Address extends Model
{
	use ScopeActive,SoftDeletes;

	/* SCOPES */

	public function scopeFTNOrder($query)
	{
		return $query
			->orderBy('region_id')
			->orderBy('host_id')
			->orderBy('node_id')
			->orderBy('point_id');
	}

	/* RELATIONS */

	/**
	 * Find children dependent on this record
	 */
	public function children()
	{
		// We have no session data for this address, by definition it has no children
		if (! $this->session('sespass'))
			return $this->hasMany(self::class,'id','void');

		if (! $this->session('default')) {
			switch ($this->role) {
				case DomainController::NODE_ZC:
					$children = self::select('addresses.*')
						->where('zone_id',$this->zone_id);

					break;

				case DomainController::NODE_RC:
					$children = self::select('addresses.*')
						->where('zone_id',$this->zone_id)
						->where('region_id',$this->region_id);

					break;

				case DomainController::NODE_NC:
					$children = self::select('addresses.*')
						->where('zone_id',$this->zone_id)
						->where('region_id',$this->region_id)
						->where('host_id',$this->host_id);

					break;

				case DomainController::NODE_HC:
					// Identify our children.
					$children = self::select('addresses.*')
						->where('hub_id',$this->id);

					break;

				case DomainController::NODE_ACTIVE:
				case DomainController::NODE_POINT:
					// Nodes dont have children, but must return a relationship instance
					return $this->hasOne(self::class,NULL,'void');

				default:
					throw new Exception('Unknown role: '.serialize($this->role));
			}

		} else {
			$children = self::select('addresses.*')
				->where('zone_id',$this->zone_id);
		}

		// Remove any children that we have session details for (SAME AS HC)
		$sessions = self::select('hubnodes.*')
			->join('system_zone',['system_zone.system_id'=>'addresses.system_id','system_zone.zone_id'=>'addresses.zone_id'])
			->join('addresses as hubnodes',['hubnodes.zone_id'=>'addresses.zone_id','hubnodes.id'=>'addresses.id'])
			->where('addresses.zone_id',$this->zone_id)
			->where('addresses.system_id','<>',$this->system_id)
			->whereIN('addresses.system_id',$children->get()->pluck('system_id'));

		// For each of the session, identify their children
		$session_kids = collect();
		foreach ($sessions->get() as $so)
			$session_kids = $session_kids->merge(($x=$so->children) ? $x->pluck('id') : []);

		// ZC's receive all mail, except for defined nodes, and defined hubs/hosts/rcs
		return $this->hasMany(self::class,'zone_id','zone_id')
			->whereIn('id',$children->get()->pluck('id')->toArray())
			->whereNotIn('id',$sessions->get()->pluck('id')->toArray())
			->whereNotIn('id',$session_kids->toArray())
			->where('system_id','<>',$this->system_id)
			->select('addresses.*')
			->orderBy('region_id')
			->orderBy('host_id')
			->orderBy('node_id')
			->orderBy('point_id')
			->with(['zone.domain']);
	}

	/**
	 * Who we send this systems mail to.
	 *
	 * @return Address|null
	 * @throws Exception
	 */
	public function parent(): ?Address
	{
		// If we are have session password, then we dont have a parent
		if ($this->session('sespass'))
			return $this;

		switch ($this->role) {
			// ZCs dont have parents, but we may have a default
			case DomainController::NODE_ZC:
				if (($x=$this->zone->systems->where('pivot.default',TRUE))->count())
					return $x->first()->match($this->zone)->first();
				else
					return NULL;

			// RC
			case DomainController::NODE_RC:
				$parent = self::where('zone_id',$this->zone_id)
					->where('region_id',0)
					->where('host_id',0)
					->where('node_id',0)
					->single();

				break;

			// Hosts
			case DomainController::NODE_NC:
				// See if we have a RC
				$parent = self::where('zone_id',$this->zone_id)
					->where('region_id',$this->region_id)
					->where('host_id',0)
					->where('node_id',0)
					->single();

				if (! $parent) {
					// See if we have a RC
					$parent = self::where('zone_id',$this->zone_id)
						->where('region_id',0)
						->where('host_id',0)
						->where('node_id',0)
						->single();
				}

				break;

			// Hubs
			case DomainController::NODE_HC:
			// Normal Nodes
			case DomainController::NODE_ACTIVE:
				// If we are a child of a hub, then check our hub
				$parent = (($this->hub_id)
					? self::where('id',$this->hub_id)
					: self::where('zone_id',$this->zone_id)
					->where('region_id',$this->region_id)
					->where('host_id',$this->host_id)
					->where('node_id',0))
					->single();

				break;

			case DomainController::NODE_POINT:
				// @todo Points - if the boss is defined, we should return it.
				return NULL;

			default:
				throw new Exception('Unknown role: '.serialize($this->role));
		}

		return $parent?->parent();
	}

	public function system()
	{
		return $this->belongsTo(System::class);
	}

	public function zone()
	{
		return $this->belongsTo(Zone::class);
	}

	/* ATTRIBUTES */

	/**
	 * Render the node name in full 5D
	 *
	 * @return string
	 */
	public function getFTNAttribute(): string
	{
		return sprintf('%s@%s',$this->getFTN4DAttribute(),$this->zone->domain->name);
	}

	public function getFTN3DAttribute(): string
	{
		return sprintf('%d:%d/%d',$this->zone->zone_id,$this->host_id ?: $this->region_id,$this->node_id);
	}

	public function getFTN4DAttribute(): string
	{
		return sprintf('%s.%d',$this->getFTN3DAttribute(),$this->point_id);
	}

	public function getRoleNameAttribute(): string
	{
		switch ($this->role) {
			case DomainController::NODE_ACTIVE:
				return 'NODE';
			case DomainController::NODE_ZC:
				return 'ZC';
			case DomainController::NODE_RC:
				return 'RC';
			case DomainController::NODE_NC:
				return 'NC';
			case DomainController::NODE_HC:
				return 'HUB';
			case DomainController::NODE_POINT:
				return 'POINT';
			default:
				return '?';
		}
	}

	/* METHODS */

	/**
	 * Find a record in the DB for a node string, eg: 10:1/1.0
	 *
	 * @param string $ftn
	 * @return Address|null
	 * @throws Exception
	 */
	public static function findFTN(string $ftn): ?self
	{
		$ftn = self::parseFTN($ftn);

		// Are we looking for a region address
		if (($ftn['f'] === 0) && $ftn['p'] === 0) {
			$o = (new self)->active()
				->select('addresses.*')
				->where('zones.zone_id',$ftn['z'])
				->where(function($q) use ($ftn) {
					return $q
						->where(function($q) use ($ftn) {
							return $q->where('region_id',$ftn['n'])
								->where('host_id',0);
						});
				})
				->where('node_id',$ftn['f'])
				->where('point_id',$ftn['p'])
				->join('zones',['zones.id'=>'addresses.zone_id'])
				->join('domains',['domains.id'=>'zones.domain_id'])
				->where('zones.active',TRUE)
				->where('domains.active',TRUE)
				->where('addresses.active',TRUE)
				->when($ftn['d'],function($query,$domain) {
					$query->where('domains.name',$domain);
				})
				->when((! $ftn['d']),function($query) {
					$query->where('domains.default',TRUE);
				})
				->single();

			if ($o && $o->system->active)
				return $o;
		}

		$o = (new self)->active()
			->select('addresses.*')
			->where('zones.zone_id',$ftn['z'])
			->where('host_id',$ftn['n'])
			->where('node_id',$ftn['f'])
			->where('point_id',$ftn['p'])
			->join('zones',['zones.id'=>'addresses.zone_id'])
			->join('domains',['domains.id'=>'zones.domain_id'])
			->where('zones.active',TRUE)
			->where('domains.active',TRUE)
			->where('addresses.active',TRUE)
			->when($ftn['d'],function($query,$domain) {
				$query->where('domains.name',$domain);
			})
			->when((! $ftn['d']),function($query) {
				$query->where('domains.default',TRUE);
			})
			->single();

		return ($o && $o->system->active) ? $o : NULL;
	}

	/**
	 * Get echomail for this node
	 *
	 * @return Packet|null
	 */
	public function getEchomail(): ?Packet
	{
		if (($x=Echomail::select('*') //where('tftn_id',$this->id)
			->where(function($q) {
				return $q->whereNull('sent')
					->orWhere('sent',FALSE);
			}))
			->count())
		{
			return $this->getPacket($x->get());
		}

		return NULL;
	}

	/**
	 * Get netmail for this node (including it's children)
	 *
	 * @return Packet|null
	 */
	public function getNetmail(): ?Packet
	{
		if (($x=Netmail::whereIn('tftn_id',(($x=$this->children) ? $x->pluck('id') : collect())->push($this->id))
			->where(function($q) {
				return $q->whereNull('sent')
					->orWhere('sent',FALSE);
			}))
			->count())
		{
			return $this->getPacket($x->get());
		}

		return NULL;
	}

	/**
	 * Return a packet of mail
	 *
	 * @param Collection $c
	 * @return Packet
	 */
	private function getPacket(Collection $c): Packet
	{
		$o = new Packet($this);

		foreach ($c as $oo) {
			$o->addMail($oo->packet($this));

			$oo->packet = $o->name;
			$oo->sent = TRUE;
			$oo->sent_at = Carbon::now();
			$oo->save();
		}

		return $o;
	}

	/**
	 * Parse a string and split it out as an FTN array
	 *
	 * @param string $ftn
	 * @return array
	 * @throws Exception
	 */
	public static function parseFTN(string $ftn): array
	{
		// http://ftsc.org/docs/frl-1028.002
		if (! preg_match('#^([0-9]+):([0-9]+)/([0-9]+)(.([0-9]+))?(@([a-z0-9\-_~]{0,8}))?$#',strtolower($ftn),$matches))
			throw new Exception('Invalid FTN: '.$ftn);

		// Check our numbers are correct.
		foreach ([1,2,3] as $i) {
			if ((! is_numeric($matches[$i])) || ($matches[$i] > DomainController::NUMBER_MAX))
				throw new Exception('Invalid FTN: '.$ftn);
		}

		if (isset($matches[5]) AND ((! is_numeric($matches[$i])) || ($matches[5] > DomainController::NUMBER_MAX)))
			throw new Exception('Invalid FTN: '.$ftn);

		return [
			'z'=>(int)$matches[1],
			'n'=>(int)$matches[2],
			'f'=>(int)$matches[3],
			'p'=>isset($matches[5]) && $matches[5] ? (int)$matches[5] : 0,
			'd'=>$matches[7] ?? NULL
		];
	}

	/**
	 * Retrieve the address session details/passwords
	 *
	 * @param string $type
	 * @return string|null
	 */
	public function session(string $type): ?string
	{
		return ($x=$this->system->sessions->where('id',$this->zone_id)->first()) ? $x->pivot->{$type} : NULL;
	}
}