<?php

namespace App\Classes;

use Carbon\Carbon;
use Exception;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;

use App\Models\Address;

/**
 * Object representing the node we are communicating with
 *
 * @property int aka_authed
 * @property int aka_num
 * @property string ftn
 * @property string password
 * @property Carbon node_time
 * @property int session_time
 * @property int ver_major
 * @property int ver_minor
 */
class Node
{
	private const LOGKEY = 'N--';

	// Remote Version
	private int $version_major = 0;
	private int $version_minor = 0;

	private Carbon $start_time;									// The time our connection started
	// @todo Change this to Carbon
	private string $node_time;									// Current node's time
	private string $node_timezone;								// Current node's time zone

	private Collection $ftns;									// The FTNs of the remote system
	private Collection $ftns_authed;							// The FTNs we have validated
	private Collection $ftns_other;								// Other FTN addresses presented
	private bool $authed;										// Have we authenticated the remote.
	private Address $originate;									// When we originate a call, this is who we are after

	private int $options;										// This nodes capabilities/options

	public function __construct()
	{
		$this->options = 0;
		$this->authed = FALSE;
		$this->start_time = Carbon::now();
		$this->ftns = collect();
		$this->ftns_authed = collect();
		$this->ftns_other = collect();

		// @todo This should be configured in the DB for each FTN system
		$this->node_timezone = 'Australia/Melbourne';
	}

	/**
	 * @throws Exception
	 */
	public function __get($key)
	{
		switch ($key) {
			case 'address':
				return ($x=$this->ftns_authed)->count() ? $x->first() : $this->ftns->first();

			// Number of AKAs the remote has
			case 'aka_num':
				return $this->ftns->count();

			// The authenticated remote addresses
			case 'aka_remote_authed':
				return $this->ftns_authed;

			case 'aka_remote':
				return $this->ftns;

			// Have we authenticated the remote
			case 'aka_authed':
				return $this->authed;

			// FTNs that we dont know about
			case 'aka_other':
				return $this->ftns_other;

			// The nodes password
			case 'password':
				// If we are originating a session, we'll use that password.
				if (isset($this->originate))
					return $this->originate->pass_session;

				// If we have already authed, we'll use that password.
				if ($this->ftns_authed->count())
					return $this->ftns_authed->first()->pass_session;
				else
					return ($this->ftns->count() && ($x=$this->ftns->first()->pass_session)) ? $x : '-';

			// Return how long our session has been connected
			case 'session_time':
				return sprintf("%d",$this->start_time->diffInSeconds(Carbon::now()));

			case 'system':
			case 'sysop':
			case 'location':
			case 'phone':
			case 'flags':
			case 'message':
			case 'files':
			case 'node_time':
			case 'node_timezone':
			case 'netmail':
			// The current session speed
			case 'speed':
			// The time our session started.
			case 'start_time':
			case 'software':
				return $this->{$key};
			// Client version
			case 'ver_major':
				return $this->version_major;
			case 'ver_minor':
				return $this->version_minor;

			default:
				throw new Exception('Unknown key: '.$key);
		}
	}

	/**
	 * @throws Exception
	 */
	public function __set($key,$value)
	{
		switch ($key) {
			case 'ftn':
				if ((! is_object($value)) || (! $value instanceof Address))
					throw new Exception('Not an Address object: '.(is_object($value) ? get_class($value) : serialize($value)));

				// Ignore any duplicate FTNs that we get
				if ($this->ftns->search(function($item) use ($value) { return $item->id === $value->id; }) !== FALSE) {
					Log::debug(sprintf('%s:- Ignoring Duplicate FTN [%s]',self::LOGKEY,$value->ftn));
					break;
				}

				$this->ftns->push($value);

				break;

			case 'ftn_other':
				// Ignore any duplicate FTNs that we get
				if ($this->ftns_other->search($value) !== FALSE) {
					Log::debug(sprintf('%s:- Ignoring Duplicate FTN [%s]',self::LOGKEY,$value));
					break;
				}

				$this->ftns_other->push($value);

				break;

			case 'system':
			case 'sysop':
			case 'location':
			case 'phone':
			case 'flags':
			case 'message':
			case 'files':
			case 'netmail':
			case 'software':
			case 'speed':
			case 'start_time':
			case 'node_time':
			case 'node_timezone':

			case 'ver_major':
			case 'ver_minor':
				$this->{$key} = $value;
				break;

			default:
				throw new Exception('Unknown variable: '.$key);
		}
	}

	/**
	 * Authenticate the AKAs that the node provided
	 *
	 * @param string $password
	 * @param string $challenge
	 * @return int
	 * @throws Exception
	 */
	public function auth(string $password,string $challenge=''): int
	{
		Log::debug(sprintf('%s:+ auth [%s]',self::LOGKEY,$password));
		$so = NULL;

		// Make sure we havent been here already
		if ($this->ftns_authed->count())
			throw new Exception('Already authed');

		foreach ($this->ftns as $o) {
			Log::debug(sprintf('%s:- Attempting to authenticate [%s] with [%s]',self::LOGKEY,$o->ftn,$o->pass_session));

			if (! $sespass=$o->pass_session)
				continue;

			// If we have challenge, then we are doing MD5
			$exp_pwd = $challenge ? $this->md5_challenge($sespass,$challenge) : $sespass;

			if ($exp_pwd === $password) {
				if (! $so)
					$so = $o->system;

				// Make sure we are the same system
				if ($so->id !== $o->system->id) {
					Log::alert(sprintf('%s:! AKA [%s] is from a different system?',self::LOGKEY,$o->ftn),['so'=>$so->name]);

					continue;
				}

				$this->ftns_authed->push($o);
				$this->authed = TRUE;
			}
		}

		if ($this->authed) {
			$o->system->last_session = Carbon::now();
			$o->system->save();
		}

		Log::debug(sprintf('%s:= auth, we authed [%d] addresses',self::LOGKEY,$this->ftns_authed->count()));
		return $this->ftns_authed->count();
	}

	/**
	 * When we originate a connection and need to send our MD5 Challenge response
	 *
	 * @param string $challenge
	 * @return string
	 * @throws Exception
	 */
	public function get_md5chal(string $challenge): string
	{
		return $this->md5_challenge($this->password,$challenge);
	}

	/**
	 * Return the remotes BINKP version as a int
	 *
	 * @return int
	 */
	public function get_versionint(): int
	{
		return $this->ver_major*100+$this->ver_minor;
	}

	/**
	 * Calculate the response to an MD5 challenge, using the nodes password
	 *
	 * @param $pwd
	 * @param $challenge
	 * @return string
	 */
	private function md5_challenge($pwd,$challenge): string
	{
		$x = $pwd.str_repeat(chr(0x00),64-strlen($pwd));

		return md5($this->str_xor($x,0x5c).md5($this->str_xor($x,0x36).$challenge,true));
	}

	/**
	 * When we originate a call to a node, we need to store the node we are connecting with in the ftns_authed, so
	 * authentication proceeds when we send our M_pwd
	 *
	 * @param Address $o
	 */
	public function originate(Address $o): void
	{
		$this->originate = $o;
		$this->ftns_authed = $o->system->match($o->zone,Address::NODE_ALL);
	}

	/**
	 * Check that our received FTNs match who we called
	 *
	 * @return bool
	 */
	public function originate_check(): bool
	{
		// If we have already authed, we wont do it again
		if ($this->authed)
			return TRUE;

		Log::debug(sprintf('%s:- Making sure we called [%s] from [%s]',self::LOGKEY,$this->originate->ftn,$this->ftns->pluck('ftn')->join(',')));

		$this->authed = $this->ftns->pluck('ftn')->contains($this->originate->ftn);

		return $this->authed;
	}

	public function optionClear(int $key): void
	{
		$this->options &= ~$key;
	}

	public function optionGet(int $key): int
	{
		return ($this->options & $key);
	}

	public function optionSet(int $key): void
	{
		$this->options |= $key;
	}

	private function str_xor(string $string,int $val): string
	{
		$result = '';

		for ($i=0;$i<strlen($string);$i++)
			$result .= chr(ord($string[$i]) ^ $val);

		return $result;
	}
}