<?php

namespace App\Classes\Protocol;

use Carbon\Carbon;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use League\Flysystem\UnreadableFileEncountered;

use App\Classes\Crypt;
use App\Classes\Node;
use App\Classes\Protocol as BaseProtocol;
use App\Classes\Sock\Exception\SocketException;
use App\Classes\Sock\SocketClient;
use App\Exceptions\{FileGrewException,InvalidFTNException};
use App\Models\{Address,Mailer};

final class Binkp extends BaseProtocol
{
	private const LOGKEY = 'PB-';

	/* CONSTS */

	public const PORT			= 24554;
	/** protocol text **/
	private const PROT			= 'binkp';
	/** version implemented */
	private const VERSION		= '1.1';
	/** block size - compressed files can only use a blocksize of 0x3fff */
	private const BLOCKSIZE		= 0x1000;
	/** session timeout secs */
	private const TIMEOUT_TIME	= 300;
	/** max block size */
	private const MAX_BLKSIZE	= 0x7fff;

	/* FEATURES */

	/** COMPRESS mode */
	public const F_COMP			= 1<<0;
	/** CHAT mode - not implemented */
	public const F_CHAT			= 1<<1;
	/** Crypt mode */
	public const F_CRYPT		= 1<<2;
	/** Multi-Batch mode */
	public const F_MULTIBATCH	= 1<<3;
	/** CRAM-MD5 mode */
	public const F_MD			= 1<<4;
	/** Force MD5 passwords */
	public const F_MDFORCE		= 1<<5;
	/** http://ftsc.org/docs/fsp-1027.001: No-dupes mode - negotiated, implies NR mode */
	public const F_NODUPE		= 1<<6;
	/** http://ftsc.org/docs/fsp-1027.001: Asymmetric ND mode */
	public const F_NODUPEA		= 1<<7;
	/** http://ftsc.org/docs/fsp-1028.001: Non-Reliable mode */
	public const F_NOREL		= 1<<8;
	/** Multi-Password mode - not implemented */
	public const F_MULTIPASS	= 1<<9;

	/* BINKP MESSAGES */

	/** No available data */
	private const BPM_NONE		= 99;
	/** Binary data */
	private const BPM_DATA		= 98;
	/** Site information */
	private const BPM_NUL		= 0;
	/** List of addresses */
	private const BPM_ADR		= 1;
	/** Session password */
	private const BPM_PWD		= 2;
	/** File information */
	private const BPM_FILE		= 3;
	/** Password was acknowledged (data ignored) */
	private const BPM_OK		= 4;
	/** End Of Batch (data ignored) */
	private const BPM_EOB		= 5;
	/** File received */
	private const BPM_GOTSKIP	= 6;
	/** Misc errors */
	private const BPM_ERR		= 7;
	/** All AKAs are busy */
	private const BPM_BSY		= 8;
	/** Get a file from offset */
	private const BPM_GET		= 9;
	/** Skip a file (RECEIVE LATER) */
	private const BPM_SKIP		= 10;
	/** Reserved for later */
	private const BPM_RESERVED	= 11;
	/** For chat */
	private const BPM_CHAT		= 12;
	/** Minimal message type value */
	private const BPM_MIN		= self::BPM_NUL;
	/** Maximal message type value */
	private const BPM_MAX		= self::BPM_CHAT;

	/* SESSION STATE */

	/** 0000 0001 - Are we in initialise mode */
	private const SE_INIT		= 1<<0;
	/** 0000 0010 - Have we sent our EOB */
	private const SE_SENTEOB	= 1<<1;
	/** 0000 0100 - Have we received EOB */
	private const SE_RECVEOB	= 1<<2;
	/** 0000 1000 - Delay sending M_EOB message until remote's one if remote is binkp/1.0 and pretends to have FREQ on us */
	private const SE_DELAYEOB	= 1<<3;
	/** 0001 0000 - Wait for GET before sending a file */
	private const SE_WAITGET	= 1<<4;
	/** 0010 0000 - We are waiting for a GOT from the remote */
	private const SE_WAITGOT	= 1<<5;
	/** 0100 0000 - Are we sending a file */
	private const SE_SENDFILE	= 1<<6;
	/** 1000 0000 - We have no more files to send */
	private const SE_NOFILES	= 1<<7;

	/* CLASS VARS */

	/** The MD5 challenge with the remote system */
	private string $md_challenge;
	private int $is_msg;
	/** Messages In Batch (MIB 3 :) */
	private int $mib;
	private int $rc;
	private int $error;

	private int $rx_size;
	private string $rx_buf = '';

	private ?Collection $mqueue;
	private string $tx_buf;
	private int $tx_ptr;
	private int $tx_left;

	private string $comp_mode = '';

	/**
	 * @var Crypt Inbound encryption
	 */
	private Crypt $crypt_in;
	/**
	 * @var Crypt Outbound encryption
	 */
	private Crypt $crypt_out;

	/**
	 * Incoming BINKP session
	 *
	 * @param SocketClient $client
	 * @return int|null
	 * @throws SocketException
	 */
	public function onConnect(SocketClient $client): ?int
	{
		// If our parent returns a PID, we've forked
		if (! parent::onConnect($client)) {
			Log::withContext(['pid'=>getmypid()]);

			$this->session(Mailer::where('name','BINKP')->singleOrFail(),$client,(new Address));
			$this->client->close();
			exit(0);
		}

		return NULL;
	}

	/**
	 * BINKD handshake
	 *
	 * @throws \Exception
	 */
	private function binkp_hs(): bool
	{
		Log::debug(sprintf('%s:+ Starting BINKP handshake',self::LOGKEY));

		if (! $this->originate && $this->down) {
			Log::info(sprintf('%s:! System down for maintenance',self::LOGKEY));

			$this->msgs(self::BPM_BSY,'RETRY 0600: Down for maintenance, back soon...');

			while ($this->tx_left || $this->mqueue->count())
				$this->binkp_send();

			return FALSE;
		}

		if (! $this->originate && $this->capGet(self::F_MD,self::O_WANT)) {
			$random_key = random_bytes(8);
			$this->md_challenge = md5($random_key,TRUE);
			$this->msgs(self::BPM_NUL,sprintf('OPT CRAM-MD5-%s',md5($random_key)));
		}

		$this->msgs(self::BPM_NUL,sprintf('SYS %s',$this->setup->system->name));
		$this->msgs(self::BPM_NUL,sprintf('ZYZ %s',$this->setup->system->sysop));
		$this->msgs(self::BPM_NUL,sprintf('LOC %s',$this->setup->system->location));
		$this->msgs(self::BPM_NUL,sprintf('NDL %d,TCP,BINKP',$this->client->speed));
		$this->msgs(self::BPM_NUL,sprintf('TIME %s',Carbon::now()->toRfc2822String()));
		$this->msgs(self::BPM_NUL,
			sprintf('VER %s-%s %s/%s',config('app.name'),$this->setup->version,self::PROT,self::VERSION));

		if ($this->originate) {
			$opt = $this->capGet(self::F_NOREL,self::O_WANT) ? ' NR' : '';
			$opt .= $this->capGet(self::F_NODUPE,self::O_WANT) ? ' ND' : '';
			$opt .= $this->capGet(self::F_NODUPEA,self::O_WANT) ? ' NDA': '';
			$opt .= $this->capGet(self::F_MULTIBATCH,self::O_WANT) ? ' MB' : '';
			$opt .= $this->capGet(self::F_CHAT,self::O_WANT) ? ' CHAT' : '';
			$opt .= $this->capGet(self::F_COMP,self::O_WANT) ? ' EXTCMD GZ' : '';
			$opt .= $this->capGet(self::F_COMP,self::O_WANT) && $this->capGet(self::F_COMP,self::O_EXT) ? ' BZ2' : '';
			$opt .= $this->capGet(self::F_CRYPT,self::O_WANT) ? ' CRYPT' : '';

			if (strlen($opt))
				$this->msgs(self::BPM_NUL,sprintf('OPT%s',$opt));
		}

		// If we are originating, we'll show the remote our address in the same network
		if ($this->originate) {
			$addresses = $this->our_addresses();

			$this->msgs(self::BPM_ADR,$addresses->pluck('ftn')->join(' '));
		}

		return TRUE;
	}

	/**
	 * @return int
	 */
	private function binkp_hsdone(): bool
	{
		Log::debug(sprintf('%s:+ BINKP handshake complete',self::LOGKEY));

		// If the remote doesnt provide a password, or in MD5 mode, then we cant use CRYPT
		if (! $this->optionGet(self::O_PWD) && (! $this->capGet(self::F_MD,self::O_WE))) {
			Log::notice(sprintf('%s:= CRYPT disabled, since we have no password or not MD5',self::LOGKEY));
			$this->capSet(self::F_CRYPT,self::O_NO);
		}

		if ($this->capGet(self::F_CRYPT,self::O_WE)) {
			$this->capSet(self::F_CRYPT,self::O_YES);

			Log::info(sprintf('%s:- CRYPT mode initialised',self::LOGKEY));

			if ($this->originate) {
				$this->crypt_out = new Crypt($this->node->password);
				$this->crypt_in = new Crypt('-'.$this->node->password);

			} else {
				$this->crypt_in = new Crypt($this->node->password);
				$this->crypt_out = new Crypt('-'.$this->node->password);
			}
		}

		// @todo Implement max incoming sessions and max incoming session for the same node

		// We have no mechanism to support chat
		if ($this->capGet(self::F_CHAT,self::O_THEY))
			Log::warning(sprintf('%s:/ The remote wants to chat, but we cant do chat',self::LOGKEY));

		/*
		if ($this->capGet(self::F_CHAT,self::O_WE))
			$this->capSet(self::F_CHAT,self::O_YES);
		*/

		// No dupes mode is preferred on BINKP 1.1
		if ($this->capGet(self::F_NODUPE,self::O_WE) || ($this->originate && $this->capGet(self::F_NOREL,self::O_WANT) && $this->node->get_versionint() > 101)) {
			Log::debug(sprintf('%s:/ NR mode enabled, because we are in NDA mode, or I want NDA and the remote is version [%d]',self::LOGKEY,$this->node->get_versionint()));
			$this->capSet(self::F_NOREL,self::O_YES);
		}

		if ((($this->node->get_versionint() > 100) && $this->capGet(self::F_MULTIBATCH,self::O_WANT)) || $this->capGet(self::F_MULTIBATCH,self::O_WE)) {
			Log::debug(sprintf('%s:/ MB mode enabled, because we agree to MB mode, or I want MB and the remote is version [%d]',self::LOGKEY,$this->node->get_versionint()));
			$this->capSet(self::F_MULTIBATCH,self::O_YES);
		}

		if (($this->node->get_versionint() > 100) && (! $this->capGet(self::F_MULTIBATCH,self::O_YES)))
			$this->sessionClear(self::SE_DELAYEOB);

		$this->mib = 0;
		$this->sessionClear(self::SE_INIT);

		Log::info(sprintf('%s:= Session: BINKP/%d.%d - NR:%d, ND:%d, NDA:%d, MD:%d, MB:%d, CR:%d, CO:%d, CH:%d',
			self::LOGKEY,
			$this->node->ver_major,
			$this->node->ver_minor,
			$this->capGet(self::F_NOREL,self::O_YES),
			$this->capGet(self::F_NODUPE,self::O_WE),
			$this->capGet(self::F_NODUPEA,self::O_WE),
			$this->capGet(self::F_MD,self::O_WE),
			$this->capGet(self::F_MULTIBATCH,self::O_YES),
			$this->capGet(self::F_CRYPT,self::O_YES),
			$this->capGet(self::F_COMP,self::O_WE),
			$this->capGet(self::F_CHAT,self::O_WE),
		));

		return TRUE;
	}

	private function binkp_init(): int
	{
		$this->sessionSet(self::SE_INIT);

		$this->is_msg = -1;
		$this->mib = 0;
		$this->error = 0;
		$this->mqueue = collect();

		$this->rx_size = -1;

		$this->tx_buf = '';
		$this->tx_left = 0;	// @todo can we replace this with strlen($tx_buf)?
		$this->tx_ptr = 0;	// @todo is this required?

		// Setup our default capabilities
		$this->md_challenge = '';

		// We cant do chat
		$this->capSet(self::F_CHAT,self::O_NO);

		// Compression
		if ($this->setup->optionGet(self::F_COMP,'binkp_options'))
			$this->capSet(self::F_COMP,self::O_WANT|self::O_EXT);

		// CRAM-MD5 session
		if ($this->setup->optionGet(self::F_MD,'binkp_options')) {
			$this->capSet(self::F_MD,self::O_WANT);

			if ($this->setup->optionGet(self::F_MDFORCE,'binkp_options'))
				$this->capSet(self::F_MD,self::O_NEED);
		}

		// Crypt Mode
		if ($this->setup->optionGet(self::F_CRYPT,'binkp_options'))
			$this->capSet(self::F_CRYPT,self::O_WANT);

		// Multibatch
		if ($this->setup->optionGet(self::F_MULTIBATCH,'binkp_options'))
			$this->capSet(self::F_MULTIBATCH,self::O_WANT);

		// Non reliable mode
		if ($this->setup->optionGet(self::F_NOREL,'binkp_options')) {
			$this->capSet(self::F_NOREL,self::O_WANT);

			// No dupes
			if ($this->setup->optionGet(self::F_NODUPE,'binkp_options')) {
				$this->capSet(self::F_NODUPE,self::O_WANT);

				// No dupes asymmetric
				if ($this->setup->optionGet(self::F_NODUPEA,'binkp_options'))
					$this->capSet(self::F_NODUPEA,self::O_WANT);
			}
		}

		return self::OK;
	}

	/**
	 * Receive data from the remote
	 *
	 * @throws \Exception
	 */
	private function binkp_recv(): bool
	{
		$blksz = $this->rx_size === -1 ? BinkpMessage::BLK_HDR_SIZE : $this->rx_size;
		Log::debug(sprintf('%s:+ BINKP receive, reading [%d] chars',self::LOGKEY,$blksz));

		if ($blksz !== 0) {
			try {
				Log::debug(sprintf('%s:- We need [%d] more chars, buffer currently has [%d] chars',self::LOGKEY,$blksz,strlen($this->rx_buf)));
				$rx_buf = $this->client->read(0,$blksz-strlen($this->rx_buf));
				Log::debug(sprintf('%s:- Got [%d] more chars for the read buffer',self::LOGKEY,strlen($rx_buf)));

			} catch (SocketException $e) {
				if ($e->getCode() === 11) {
					// @todo We maybe should count these and abort if there are too many?
					if (static::DEBUG)
						Log::debug(sprintf('%s:- Got a socket EAGAIN',self::LOGKEY));

					return TRUE;
				}

				$this->error = 1;

				Log::error(sprintf('%s:! Reading we got an EXCEPTION [%d-%s]',self::LOGKEY,$e->getCode(),$e->getMessage()));
				return FALSE;
			}

			if (strlen($rx_buf) === 0) {
				// @todo Check that this is correct.
				Log::debug(sprintf('%s:- Was the socket closed by the remote?',self::LOGKEY));
				$this->error = -2;

				return FALSE;
			}

			if ($this->capGet(self::F_CRYPT,self::O_YES)) {
				Log::debug(sprintf('%s:%% Decrypting data from remote.',self::LOGKEY));
				$this->rx_buf .= $this->crypt_in->decrypt($rx_buf);

			} else {
				$this->rx_buf .= $rx_buf;
			}
		}

		Log::debug(sprintf('%s:- Read buffer has [%d] chars to process.',self::LOGKEY,strlen($this->rx_buf)));

		/* Received complete block */
		if (strlen($this->rx_buf) === $blksz) {
			/* Header */
			if ($this->rx_size === -1 ) {
				$this->is_msg = ord(substr($this->rx_buf,0,1)) >> 7;
				// If compression is used, then this needs to be &0x3f, since the 2nd high bit is the compression flag
				// @todo Need to see what happens, if we are receiving a block higher than 0x3fff. Possible?
				$this->rx_size = ((ord(substr($this->rx_buf,0,1))&0x7f) << 8)+ord(substr($this->rx_buf,1,1));

				Log::debug(sprintf('%s:- BINKP receive HEADER, is_msg [%d], rx_size [%d]',self::LOGKEY,$this->is_msg,$this->rx_size));

				if ($this->rx_size === 0)
					goto ZeroLen;

				$rc = TRUE;

			/* Next block */
			} else {
				ZeroLen:
				if ($this->is_msg) {
					$this->mib++;

					/* Handle zero length block */
					if ($this->rx_size === 0 ) {
						Log::debug(sprintf('%s:- Received a ZERO length msg - dropped',self::LOGKEY));
						$this->rx_size = -1;
						$this->rx_buf = '';

						return TRUE;
					}

					if (static::DEBUG)
						Log::debug(sprintf('%s: - binkp_recv BUFFER [%d]',self::LOGKEY,strlen($this->rx_buf)));

					$msg = ord(substr($this->rx_buf,0,1));

					if ($msg > self::BPM_MAX) {
						Log::error(sprintf('%s:! Unknown message received [%d] (%d-%s)',self::LOGKEY,$msg,strlen($this->rx_buf),$this->rx_buf));
						$rc = TRUE;

					} else {
						// http://ftsc.org/docs/fts-1026.001 - frames may be NULL terminated
						$data = rtrim(substr($this->rx_buf,1),"\x00");

						switch ($msg) {
							case self::BPM_ADR:
								Log::debug(sprintf('%s:- ADR:Address [%s]',self::LOGKEY,$data));
								// @note It seems taurus may pad data with nulls at the end (esp BPM_ADR), so we should trim that.
								$rc = $this->M_adr(trim($data));
								break;

							case self::BPM_EOB:
								Log::debug(sprintf('%s:- EOB:We got an EOB message with [%d] chars in the buffer',self::LOGKEY,strlen($data)));

								if (strlen($data))
									Log::critical(sprintf('%s:! EOB but we have data?',self::LOGKEY),['data'=>$data]);

								$rc = $this->M_eob();
								break;

							case self::BPM_NUL:
								Log::debug(sprintf('%s:- NUL:Message [%s]',self::LOGKEY,$data));
								$rc = $this->M_nul($data);
								break;

							case self::BPM_PWD:
								Log::debug(sprintf('%s:- PWD:We got a password [%s]',self::LOGKEY,$data));
								$rc = $this->M_pwd(ltrim($data));
								break;

							case self::BPM_ERR:
								Log::debug(sprintf('%s:- ERR:We got an error [%s]',self::LOGKEY,$data));
								$rc = $this->M_err($data);
								break;

							case self::BPM_FILE:
								Log::debug(sprintf('%s:- FIL:We are receiving a file [%s]',self::LOGKEY,$data));
								$rc = $this->M_file($data);
								break;

							case self::BPM_GET:
								Log::debug(sprintf('%s:- GET:We are sending a file [%s]',self::LOGKEY,$data));
								$rc = $this->M_get($data);
								break;

							case self::BPM_GOTSKIP:
								Log::debug(sprintf('%s:- GOT:Remote received, or already has a file [%s]',self::LOGKEY,$data));
								$rc = $this->M_gotskip($data);
								break;

							case self::BPM_OK:
								Log::debug(sprintf('%s:- OK:Got an OK [%s]',self::LOGKEY,$data));
								$rc = $this->M_ok(ltrim($data));
								break;

							case self::BPM_CHAT:
								Log::debug(sprintf('%s:- CHT:Remote sent a message [%s]',self::LOGKEY,$data));
								$rc = $this->M_chat($data);
								break;

							default:
								Log::error(sprintf('%s:! BINKP command not implemented [%d]',self::LOGKEY,$msg));
								$rc = TRUE;
						}
					}

				} else {
					if ($this->recv->fd) {
						try {
							$this->recv->write($this->rx_buf);

						} catch (FileGrewException $e) {
							// Retry the file without compression
							Log::error(sprintf('%s:! %s',self::LOGKEY,$e->getMessage()));
							$this->msgs(self::BPM_GET,sprintf('%s %ld NZ',$this->recv->name_size_time,$this->recv->pos));

						} catch (\Exception $e) {
							Log::error(sprintf('%s:! %s',self::LOGKEY,$e->getMessage()));

							$this->recv->close();
							$this->msgs(self::BPM_SKIP,$this->recv->name_size_time);
						}

						$rc = TRUE;

						if ($this->recv->pos === $this->recv->recvsize) {
							Log::info(sprintf('%s:- Finished receiving file [%s] with size [%d]',self::LOGKEY,$this->recv->nameas,$this->recv->recvsize));

							$this->msgs(self::BPM_GOTSKIP,$this->recv->name_size_time);
							$this->recv->close();
						}

					} else {
						Log::critical(sprintf('%s:- Ignoring data block, we dont have a received FD open?', self::LOGKEY));
						$rc = TRUE;
					}
				}

				$this->rx_size = -1;
			}

			$this->rx_buf = '';

		} else {
			$rc = TRUE;
		}

		if (static::DEBUG)
			Log::debug(sprintf('%s:= binkp_recv [%d]',self::LOGKEY,$rc));

		return $rc;
	}

	/**
	 * @throws \Exception
	 */
	private function binkp_send(): int
	{
		Log::debug(sprintf('%s:+ BINKP send, TX buffer has [%d] chars (%d), and [%d] messages queued',self::LOGKEY,strlen($this->tx_buf),$this->tx_left,$this->mqueue->count()));

		if ($this->tx_left === 0 ) {			/* tx buffer is empty */
			$this->tx_ptr = 0;

			if ($this->mqueue->count()) {		/* there are unsent messages */
				while ($msg=$this->mqueue->shift()) {
					if ($msg instanceof BinkpMessage) {
						if (($msg->len+$this->tx_left) > self::MAX_BLKSIZE) {
							Log::alert(sprintf('%s:! MSG [%d] would overflow our buffer [%d]',self::LOGKEY,$msg->len,$this->tx_left));
							break;
						}

						Log::debug(sprintf('%s:- TX buffer empty, adding [%d] chars from the queue',self::LOGKEY,$msg->len));
							$this->tx_buf .= $msg->msg;
							$this->tx_left += $msg->len;

					} else {
						$this->tx_buf .= $msg;
						$this->tx_left += strlen($msg);
					}
				}

			} elseif ($this->sessionGet(self::SE_SENDFILE) && $this->send->fd && (! $this->sessionGet(self::SE_WAITGET))) {
				try {
					$buf = $this->send->read(self::BLOCKSIZE);

				} catch (UnreadableFileEncountered) {
					$this->send->close(FALSE,$this->node);
					$this->sessionClear(self::SE_SENDFILE);

				} catch (\Exception $e) {
					Log::error(sprintf('%s:! BINKP send unexpected ERROR [%s]',self::LOGKEY,$e->getMessage()));

					throw new \Exception($e->getMessage());
				}

				if ($buf) {
					$data = BinkpMessage::mkheader(strlen($buf));
					$data .= $buf;

					if ($this->capGet(self::F_CRYPT,self::O_YES)) {
						$enc = $this->crypt_out->encrypt($data);

						$this->tx_buf .= $enc;
						$this->tx_left = strlen($enc);

					} else {
						$this->tx_buf .= $data;
						$this->tx_left = strlen($buf)+BinkpMessage::BLK_HDR_SIZE;
					}
				}

				// @todo should this be less than BLOCKSIZE? Since a read could return a blocksize and it could be the end of the file?
				if ($this->send->pos === $this->send->size) {
					$this->sessionSet(self::SE_WAITGOT);
					$this->sessionClear(self::SE_SENDFILE);
				}
			}

		} else {
			try {
				Log::debug(sprintf('%s:- Sending [%d] chars to remote: tx_buf [%d], tx_ptr [%d]',self::LOGKEY,$this->tx_left,strlen($this->tx_buf),$this->tx_ptr));
				$rc = $this->client->send(substr($this->tx_buf,$this->tx_ptr,$this->tx_left),self::TIMEOUT_TIME);
				Log::debug(sprintf('%s:- Sent [%d] chars to remote',self::LOGKEY,$rc));

			} catch (\Exception $e) {
				if ($e->getCode() === 11) {
					Log::error(sprintf('%s:! Got a socket EAGAIN',self::LOGKEY));
					return 1;
				}

				Log::error(sprintf('%s:! Sending we got an EXCEPTION [%d-%s]',self::LOGKEY,$e->getCode(),$e->getMessage()));
				return 0;
			}

			$this->tx_ptr += $rc;
			$this->tx_left -= $rc;

			if (! $this->tx_left) {
				$this->tx_buf = '';
				$this->tx_ptr = 0;
			}
		}

		return 1;
	}

	private function file_parse(string $str): ?array
	{
		$name = $this->strsep($str,' ');
		$size = (int)$this->strsep($str,' ');
		$time = (int)$this->strsep($str,' ');
		$offs = (int)$this->strsep($str,' ');
		$flags = $this->strsep($str,' ');

		if ($name && $size && $time) {
			return [
				'file'=>['name'=>$name,'size'=>$size,'mtime'=>$time],
				'offs'=>$offs,
				'flags'=>$flags,
			];
		}

		return NULL;
	}

	/**
	 * Add a BINKP control message to the queue
	 *
	 * @param string $id
	 * @param string $msg_body
	 * @return void
	 */
	private function msgs(string $id,string $msg_body): void
	{
		Log::debug(sprintf('%s:+ Queueing message to remote [%d:%s]',self::LOGKEY,$id,$msg_body));

		$msg = new BinkpMessage($id,$msg_body);

		// If encryption is enabled, we need to queue the encrypted version of the message
		// @todo rework this so queue only has data, not objects
		if ($this->capGet(self::F_CRYPT,self::O_YES)) {
			$enc = $this->crypt_out->encrypt($msg->msg);
			$this->mqueue->push($enc);

		} else {
			$this->mqueue->push($msg);
		}

		$this->mib++;
	}

	/**
	 * @throws \Exception
	 */
	private function M_adr(string $buf): bool
	{
		$rc = 0;

		while ($rem_aka=$this->strsep($buf,' ')) {
			try {
				if (! ($o=Address::findFTN($rem_aka,TRUE))) {
					// @todo when we have multiple inactive records, this returns more than 1, so pluck the active record if there is one
					Log::alert(sprintf('%s:? AKA is UNKNOWN [%s]',self::LOGKEY,$rem_aka));

					$this->node->ftn_other = $rem_aka;
					continue;

				// If we only present limited AKAs dont validate password against akas outside of the domains we present
				} elseif (is_null(our_address($o))) {
					Log::alert(sprintf('%s:/ AKA domain [%s] is not in our domain(s) [%s] - ignoring',self::LOGKEY,$o->zone->domain->name,our_address()->pluck('zone.domain.name')->unique()->join(',')));

					$this->node->ftn_other = $rem_aka;
					continue;

				} elseif (! $o->active) {
					Log::alert(sprintf('%s:/ AKA is not active [%s] - ignoring',self::LOGKEY,$rem_aka));

					$this->node->ftn_other = $rem_aka;
					continue;

				} else {
					Log::info(sprintf('%s:- Got AKA [%s]',self::LOGKEY,$rem_aka));

					// We'll update this address status
					$o->validated = TRUE;
					$o->role &= ~(Address::NODE_HOLD|Address::NODE_DOWN);
					$o->save();
				}

			} catch (InvalidFTNException $e) {
				Log::error(sprintf('%s:! AKA is INVALID [%s] (%s) - ignoring',self::LOGKEY,$rem_aka,$e->getMessage()));

				continue;

			} catch (\Exception $e) {
				Log::error(sprintf('%s:! AKA is INVALID [%s] (%d:%s-%s)',self::LOGKEY,$rem_aka,$e->getLine(),$e->getFile(),$e->getMessage()));

				$this->msgs(self::BPM_ERR,sprintf('Bad address %s',$rem_aka));
				$this->rc = self::S_FAILURE;

				return FALSE;
			}

			// Check if the remote has our AKA
			if ($this->setup->system->addresses->pluck('ftn')->search($rem_aka) !== FALSE) {
				Log::error(sprintf('%s:! Remote\'s AKA is mine [%s]?',self::LOGKEY,$rem_aka));

				$this->msgs(self::BPM_ERR,sprintf('Sorry that is my AKA [%s], who are you?',$rem_aka));
				$this->rc = self::S_FAILURE;

				return FALSE;
			}

			// @todo lock nodes
			$this->node->ftn = $o;

			$rc = $this->node->aka_num;
		}

		if ($rc === 0) {
			Log::error(sprintf('%s:! All AKAs [%d] are busy',self::LOGKEY,$this->node->aka_num));

			$this->msgs( self::BPM_BSY,'All AKAs are busy, nothing to do :(');
			$this->rc = self::S_BUSY;

			return FALSE;
		}

		if ($this->originate) {
			if (! $this->node->originate_check()) {
				Log::error(sprintf('%s:! We didnt get who we called?',self::LOGKEY));

				$this->msgs( self::BPM_ERR,'Sorry, you are not who I expected');
				$this->rc = self::S_FAILURE;

				return 0;
			}

			/**
			 * http://ftsc.org/docs/fts-1026.001
			 * M_NUL "TRF netmail_bytes arcmail_bytes"
			 * traffic prognosis (in bytes) for the netmail
			 * (netmail_bytes) and arcmail + files (arcmail_bytes),
			 * both are decimal ASCII strings
			 */
			// @todo This is affectively redundant, because we are not determining our mail until later
			$this->msgs(self::BPM_NUL,sprintf('TRF %lu %lu',$this->send->mail_size,$this->send->files_size));

			if ($this->md_challenge) {
				Log::info(sprintf('%s:! Sending MD5 challenge',self::LOGKEY));
				$this->msgs(self::BPM_PWD,sprintf('CRAM-MD5-%s',$this->node->get_md5chal($this->md_challenge)));

			} elseif ($this->capGet(self::F_MD,self::O_NEED)) {
				Log::error(sprintf('%s:! Node wants plaintext, but we insist on MD5 challenges',self::LOGKEY));

				$this->msgs(self::BPM_ERR,'Can\'t use plaintext password');
				$this->rc = self::S_FAILURE;

				return 0;

			} else {
				Log::info(sprintf('%s:! Sending plain text password',self::LOGKEY));
				$this->msgs(self::BPM_PWD,$this->node->password ?: '');
			}
		}

		if (! $this->node->aka_num)
			$this->optionClear(self::O_PWD);
		else
			$this->optionSet(self::O_PWD);

		// If we are not the originator, we'll show our addresses in common.
		if (! $this->originate)
			$this->msgs(self::BPM_ADR,$this->our_addresses()->pluck('ftn')->join(' '));

		return TRUE;
	}

	private function M_chat(string $buf): bool
	{
		if ($this->capGet(self::F_CHAT,self::O_YES)) {
			Log::error(sprintf('%s:! We cannot do chat',self::LOGKEY));

		} else {
			Log::error(sprintf('%s:! We got a chat message, but chat is disabled (%s)',self::LOGKEY,strlen($buf)),['buf'=>$buf]);
		}

		return TRUE;
	}

	/**
	 * We received EOB from the remote.
	 *
	 * @throws \Exception
	 */
	private function M_eob(): bool
	{
		if ($this->recv->fd) {
			Log::info(sprintf('%s:= Closing receiving file.',self::LOGKEY));

			$this->recv->close();
		}

		$this->sessionSet(self::SE_RECVEOB);
		$this->sessionClear(self::SE_DELAYEOB);

		if (! $this->send->togo_count && $this->sessionGet(self::SE_NOFILES) && $this->capGet(self::F_MULTIBATCH,self::O_YES)) {
			$this->getFiles($this->node);

			if ($this->send->togo_count)
				$this->sessionClear(self::SE_NOFILES|self::SE_SENTEOB);
		}

		return TRUE;
	}

	/**
	 * @throws \Exception
	 */
	private function M_err(string $buf): bool
	{
		Log::error(sprintf('%s:! We got an error, there are [%d] chars in the buffer (%s)',self::LOGKEY,strlen($buf),$buf));

		$this->error_close();
		$this->rc = self::S_FAILURE;

		return TRUE;
	}

	/**
	 * @throws \Exception
	 */
	private function M_file(string $buf): bool
	{
		Log::info(sprintf('%s:+ About to receive a file [%s]',self::LOGKEY,$buf));

		if ($this->sessionGet(self::SE_SENTEOB) && $this->sessionGet(self::SE_RECVEOB))
			$this->sessionClear(self::SE_SENTEOB);

		$this->sessionClear(self::SE_RECVEOB);

		//if ($this->recv->fd)
		//	$this->recv->close();

		if (! ($file=$this->file_parse($buf))) {
			Log::error(sprintf('%s:! UNPARSABLE file info [%s]',self::LOGKEY,$buf));
			$this->msgs(self::BPM_ERR,sprintf('M_FILE: unparsable file info: "%s", what are you on?',$buf));

			if ($this->sessionGet(self::SE_SENDFILE))
				$this->send->close(FALSE,$this->node);

			$this->rc = self::S_FAILURE;

			return FALSE;
		}

		// In NR mode, when we got -1 for the file offsite, the reply to our get will confirm our requested offset.
		if ($this->recv->ready
			&& $this->recv->nameas
			&& (! strncasecmp(Arr::get($file,'file.name'),$this->recv->nameas,self::MAX_PATH))
			&& $this->recv->recvmtime === Arr::get($file,'file.mtime')
			&& $this->recv->recvsize === Arr::get($file,'file.size')
			&& $this->recv->pos === $file['offs'])
		{
			$this->recv->open($file['offs']<0,$file['flags']);

			return TRUE;
		}

		$this->recv->new($file['file'],$this->node->address,$this->force_queue);

		try {
			switch ($this->recv->open($file['offs']<0,$file['flags'])) {
				case self::FOP_ERROR:
					Log::error(sprintf('%s:! File ERROR',self::LOGKEY));

				case self::FOP_SUSPEND:
				case self::FOP_SKIP:
					Log::info(sprintf('%s:- File Skipped',self::LOGKEY));
					$this->msgs(self::BPM_SKIP,$this->recv->name_size_time);

					// Close the file, since we are skipping it.
					$this->recv->close();

					break;

				case self::FOP_GOT:
					Log::info(sprintf('%s:- File skipped, we already have it',self::LOGKEY));
					$this->msgs(self::BPM_GOTSKIP,$this->recv->name_size_time);

					// Close the file, since we already have it.
					$this->recv->close();

					break;

				case self::FOP_CONT:
					Log::debug(sprintf('%s:- Continuing file [%s] from (%ld)',self::LOGKEY,$this->recv->name,$file['offs']));

				case self::FOP_OK:
					Log::debug(sprintf('%s:- Getting file from offset [%ld]',self::LOGKEY,$file['offs']));

					if (((int)$file['offs'] === -1) && $this->capGet(self::F_NOREL,self::O_WANT)) {
						Log::debug(sprintf('%s:- Assuming the remote wants NR mode, since offset is [%d] and they didnt specify an OPT with it',self::LOGKEY,$file['offs']));
						$this->capSet(self::F_NOREL,self::O_YES);
					}

					if ($this->capGet(self::F_NOREL,self::O_YES))
						$this->msgs(self::BPM_GET,sprintf('%s %ld',$this->recv->name_size_time,($file['offs'] < 0) ? 0 : $file['offs']));

					break;
			}

		} catch (\Exception $e) {
			Log::error(sprintf('%s:! File Open ERROR [%s]',self::LOGKEY,$e->getMessage()));

			$this->msgs(self::BPM_SKIP,$this->recv->name_size_time);

			// Close the file, since we had an error opening it.
			if ($this->recv->fd)
				$this->recv->close();
		}

		return TRUE;
	}

	/**
	 * @throws \Exception
	 */
	private function M_get(string $buf): bool
	{
		Log::debug(sprintf('%s:+ Sending file [%s]',self::LOGKEY,$buf));

		if ($file=$this->file_parse($buf)) {
			if ($this->sessionGet(self::SE_SENDFILE)
				&& $this->send->nameas
				&& ! strncasecmp(Arr::get($file,'file.name'),$this->send->nameas,self::MAX_PATH)
				&& $this->send->mtime === Arr::get($file,'file.mtime')
				&& $this->send->size === Arr::get($file,'file.size'))
			{
				if (! $this->send->seek($file['offs'])) {
					Log::error(sprintf('%s:! Cannot send file from requested offset [%d]',self::LOGKEY,$file['offs']));

					$this->msgs(self::BPM_ERR,'Can\'t send file from requested offset');
					$this->send->close(FALSE,$this->node);
					$this->sessionClear(self::SE_SENDFILE);

				} else {
					$this->sessionClear(self::SE_WAITGET);
					Log::debug(sprintf('%s:Sending file [%s] as [%s]',self::LOGKEY,$this->send->name,$this->send->nameas));
					$this->msgs(self::BPM_FILE,sprintf('%s %lu %ld %lu %s',$this->send->nameas,$this->send->size,$this->send->mtime,$file['offs'],$file['flags']));
				}

			} else {
				Log::error(sprintf('%s:! Remote requested an unknown file [%s]',self::LOGKEY,$buf));
			}

		} else {
			Log::error(sprintf('%s:! UNPARSABLE file info [%s]',self::LOGKEY,$buf));
		}

		return TRUE;
	}

	/**
	 * M_GOT/M_SKIP commands
	 *
	 * @param string $buf
	 * @return bool
	 * @throws \Exception
	 */
	private function M_gotskip(string $buf): bool
	{
		Log::debug(sprintf('%s:+ Remote confirms receipt for file [%s]',self::LOGKEY,$buf));

		if ($file = $this->file_parse($buf)) {
			if ($this->send->nameas
				&& ! strncasecmp(Arr::get($file,'file.name'),$this->send->nameas,self::MAX_PATH)
				&& $this->send->mtime === Arr::get($file,'file.mtime')
				&& $this->send->size === Arr::get($file,'file.size'))
			{
				if ((! $this->sessionGet(self::SE_SENDFILE)) && (! $this->sessionGet(self::SE_WAITGOT))) {
					Log::error(sprintf('%s:! M_got[skip] for unknown file [%s]',self::LOGKEY,$buf));

				} else {
					Log::info(sprintf('%s:= Packet/File [%s], type [%d] sent.',self::LOGKEY,$this->send->nameas,$this->send->type));
					$this->sessionClear(self::SE_WAITGOT|self::SE_SENDFILE);

					$this->send->close(TRUE,$this->node);
				}

			} else {
				Log::error(sprintf('%s:! M_got[skip] not for our file? [%s]',self::LOGKEY,$buf));
			}

		} else {
			Log::error(sprintf('%s:! UNPARSABLE file info [%s]',self::LOGKEY,$buf));
		}

		return TRUE;
	}

	/**
	 * @throws \Exception
	 */
	private function M_nul(string $buf): bool
	{
		Log::info(sprintf('%s:+ M_NUL [%s]',self::LOGKEY,$buf));

		if (! strncmp($buf,'SYS ',4)) {
			$this->node->system = ltrim(substr($buf,4));

		} elseif (! strncmp($buf, 'ZYZ ',4)) {
			$this->node->sysop = ltrim(substr($buf,4));

		} elseif (! strncmp($buf,'LOC ',4)) {
			$this->node->location = ltrim(substr($buf,4));

		} elseif (! strncmp($buf,'NDL ',4)) {
			$data = ltrim(substr($buf,4));
			$comma = strpos($data,',');
			$spd = substr($data,0,$comma);

			if ($comma)
				$this->node->flags = substr($data,$comma+1);

			if ($spd >= 300) {
				$this->client->speed = $spd;

			} else {
				$comma = ltrim(substr($buf,4));
				$c = 0;
				while (($x=substr($comma,$c,1)) && is_numeric($x))
					$c++;

				$comma = substr($comma,0,$c);

				if (! $comma) {
					$this->client->speed = self::TCP_SPEED;

				} elseif (strtolower(substr($comma,$c+1,1)) === 'k') {
					$this->client->speed = $spd * 1024;

				} elseif (strtolower(substr($comma,$c+1,1)) === 'm') {
					$this->client->speed = $spd * 1024 * 1024;

				} else {
					$this->client->speed = self::TCP_SPEED;
				}
			}

		} elseif (! strncmp($buf,'TIME ',5)) {
			$this->node->node_time = ltrim(substr($buf,5));

		} elseif (! strncmp($buf,'VER ',4)) {
			$data = ltrim(substr($buf,4));
			$matches = [];
			preg_match('#^(.+)\s+\(?binkp/([0-9]+)\.([0-9]+)\)?$#',$data,$matches);

			if (count($matches) === 4) {
				$this->node->software = $matches[1];
				$this->node->ver_major = $matches[2];
				$this->node->ver_minor = $matches[3];
			} else {
				$this->node->software = 'Unknown';
				$this->node->ver_major = 0;
				$this->node->ver_minor = 0;
			}

		} elseif (! strncmp($buf,'TRF ',4)) {
			$data = ltrim(substr($buf,4));
			$matches = [];
			preg_match('/^([0-9]+)\s+([0-9]+)$/',$data,$matches);

			$this->node->netmail = isset($matches[1]) ?: 0;
			$this->node->files = isset($matches[2]) ?: 0;

			if ($this->node->netmail + $this->node->files)
				$this->sessionSet(self::SE_DELAYEOB);

		} elseif (! strncmp($buf,'FREQ',4)) {
			$this->sessionSet(self::SE_DELAYEOB);

		} elseif (! strncmp($buf,'PHN ',4)) {
			$this->node->phone = ltrim(substr($buf,4));

		} elseif (! strncmp($buf,'OPM ',4)) {
			$this->node->message = ltrim(substr($buf,4));

		} elseif (! strncmp($buf,'OPT ',4)) {
			$data = ltrim(substr($buf,4));

			while ($data && ($p = $this->strsep($data,' '))) {
				if (! strcmp($p,'MB')) {
					Log::info(sprintf('%s:- Remote wants MULTIBATCH mode',self::LOGKEY));
					$this->capSet(self::F_MULTIBATCH,self::O_THEY);

				} elseif (! strcmp($p,'ND')) {
					Log::info(sprintf('%s:- Remote wants NO DUPES mode',self::LOGKEY));
					$this->capSet(self::F_NOREL,self::O_THEY);
					$this->capSet(self::F_NODUPE,self::O_THEY);

				} elseif (! strcmp($p,'NDA')) {
					Log::info(sprintf('%s:- Remote wants NO DUPES ASYMMETRIC mode',self::LOGKEY));
					$this->capSet(self::F_NOREL, self::O_THEY);
					$this->capSet(self::F_NODUPE, self::O_THEY);
					$this->capSet(self::F_NODUPEA, self::O_THEY);

				} elseif (! strcmp($p,'NR')) {
					Log::info(sprintf('%s:- Remote wants NON RELIABLE MODE mode',self::LOGKEY));
					$this->capSet(self::F_NOREL,self::O_THEY);

				} elseif (! strcmp($p,'CHAT')) {
					Log::info(sprintf('%s:- Remote wants CHAT mode',self::LOGKEY));
					$this->capSet(self::F_CHAT,self::O_THEY);

				} elseif (! strcmp($p,'CRYPT')) {
					Log::info(sprintf('%s:- Remote wants CRYPT mode',self::LOGKEY));
					$this->capSet(self::F_CRYPT,self::O_THEY);

				} elseif (! strcmp($p,'GZ')) {
					Log::info(sprintf('%s:- Remote wants GZ compression',self::LOGKEY));
					$this->capSet(self::F_COMP,self::O_THEY);

				} elseif (! strcmp($p,'BZ2')) {
					Log::info(sprintf('%s:- Remote wants BZ2 compression',self::LOGKEY));
					$this->capSet(self::F_COMP,self::O_THEY|self::O_EXT);

				} elseif (! strncmp($p,'CRAM-MD5-',9) && $this->originate && $this->capGet(self::F_MD,self::O_WANT)) {
					if (strlen($hex=substr($p,9)) > 64 ) {
						Log::error(sprintf('%s:! The challenge string is TOO LONG [%d] (%s)',self::LOGKEY,strlen($hex),$p));

					} elseif (strlen($hex)%2) {
						Log::error(sprintf('%s:! The challenge string is an odd size [%d] (%s)',self::LOGKEY,strlen($hex),$hex));

					} else {
						Log::info(sprintf('%s:- Remote wants MD5 auth with [%s]',self::LOGKEY,$hex));
						$this->md_challenge = hex2bin($hex);

						if ($this->md_challenge)
							$this->capSet(self::F_MD,self::O_THEY);
					}

					if ($this->capGet(self::F_MD,self::O_WE))
						$this->capSet(self::F_MD,self::O_YES);

				} else {
					Log::warning(sprintf('%s:/ Ignoring UNSUPPORTED option [%s]',self::LOGKEY,$p));
				}
			}

		} else {
			Log::warning(sprintf('%s:/ M_nul Got UNKNOWN NUL [%s]',self::LOGKEY,$buf));
		}

		return TRUE;
	}

	/**
	 * Remote accepted our password
	 *
	 * @throws \Exception
	 */
	private function M_ok(string $buf): bool
	{
		Log::debug(sprintf('%s:+ M_ok [%s]',self::LOGKEY,$buf));

		if (! $this->originate) {
			Log::error(sprintf('%s:! UNEXPECTED M_OK [%s] from remote on incoming call',self::LOGKEY,$buf));

			$this->rc = self::S_FAILURE;
			return FALSE;
		}

		if ($this->optionGet(self::O_PWD) && $buf) {
			while (($t=$this->strsep($buf," \t")))
				if (strcmp($t,'non-secure') === 0) {
					Log::info(sprintf('%s:- NOT secure',self::LOGKEY));

					$this->capSet(self::F_CRYPT,self::O_NO);
					$this->optionClear(self::O_PWD);

					break;

				} else {
					Log::debug(sprintf('%s:? Got unknown string from M_ok [%s]',self::LOGKEY,$t));
				}
		}

		if ($this->optionGet(self::O_PWD))
			Log::info(sprintf('%s:- SECURE',self::LOGKEY));

		return $this->binkp_hsdone();
	}

	/**
	 * @todo It appears when we poll a node, we dont ask for passwords, but we still send echomail and files.
	 */
	private function M_pwd(string $buf): bool
	{
		$have_CRAM = !strncasecmp($buf,'CRAM-MD5-',9);
		$have_pwd = $this->optionGet(self::O_PWD);

		if ($this->originate) {
			Log::error(sprintf('%s:! Unexpected password [%s] from remote on OUTGOING call',self::LOGKEY,$buf));

			$this->rc = self::S_FAILURE;
			return FALSE;
		}

		if ($this->md_challenge) {
			if ($have_CRAM) {
				// Loop to match passwords
				$x = $this->node->auth(substr($buf,9),$this->md_challenge);
				$this->capSet(self::F_MD,self::O_THEY);

				Log::info(sprintf('%s:- We authed [%d] akas',self::LOGKEY,$x));

			} elseif ($this->capGet(self::F_MD,self::O_NEED)) {
				Log::error(sprintf('%s:! Remote doesnt support MD5, but we want it',self::LOGKEY));

				$this->msgs( self::BPM_ERR,'You must support MD5 auth to talk to me');
				$this->rc = self::S_FAILURE;

				return FALSE;
			}
		}

		if (! $this->md_challenge || (! $have_CRAM && (! $this->capGet(self::F_MD,self::O_NEED)))) {
			// Loop to match passwords
			$x = $this->node->auth($buf);

			Log::info(sprintf('%s:- We authed [%d] akas',self::LOGKEY,$x));
		}

		if ($have_pwd) {
			// If no passwords matched (ie: aka_authed is 0), but we know this system
			if ((! $this->node->aka_authed) && ($this->node->aka_remote->count())) {
				Log::error(sprintf('%s:! Bad password [%s]',self::LOGKEY,$buf));

				$this->optionSet(self::O_BAD);
				$this->rc = self::S_FAILURE;

				return FALSE;
			}

		} elseif (! $this->node->aka_authed) {
			Log::notice(sprintf('%s:= Remote proposed password for us [%s]',self::LOGKEY,$buf));
		}

		// We dont use crypt if we dont have an MD5 sessions
		if (! $have_pwd && (! $this->capGet(self::F_MD,self::O_YES))) {
			Log::notice(sprintf('%s:= CRYPT disabled, since we have no password or not MD5',self::LOGKEY));
			$this->capSet(self::F_CRYPT,self::O_NO);
		}

		$opt = '';

		if ($this->capGet(self::F_NOREL,self::O_WE) && $this->capGet(self::F_NODUPE,self::O_WE) && $this->capGet(self::F_NODUPEA,self::O_YES))
			$opt .= ' NDA';
		elseif ($this->capGet(self::F_NOREL,self::O_WE) && $this->capGet(self::F_NODUPE,self::O_WE))
			$opt .= ' ND';
		elseif ($this->capGet(self::F_NOREL,self::O_WE))
			$opt .= ' NR';

		$opt .= $this->capGet(self::F_MULTIBATCH,self::O_WE) ? ' MB' : '';
		$opt .= $this->capGet(self::F_CHAT,self::O_WE) ? ' CHAT' : '';

		if ($this->capGet(self::F_COMP,self::O_WE) && $this->capGet(self::F_COMP,self::O_EXT)) {
			$this->comp_mode = 'BZ2';
			$opt .= ' EXTCMD BZ2';
		} elseif ($this->capGet(self::F_COMP,self::O_WE)) {
			$this->comp_mode = 'GZ';
			$opt .= ' EXTCMD GZ';
		}

		$opt .= $this->capGet(self::F_CRYPT,self::O_WE) ? ' CRYPT' : '';

		if (strlen($opt))
			$this->msgs(self::BPM_NUL,sprintf('OPT%s',$opt));

		// @todo This is effectively redundant, because we are not getting files until later
		$this->msgs(self::BPM_NUL,sprintf('TRF %lu %lu',$this->send->mail_size,$this->send->files_size));

		if ($this->node->aka_authed) {
			$this->msgs(self::BPM_OK,sprintf('%ssecure',$have_pwd ? '' : 'non-'));

		} else {
			$this->msgs(self::OK,'non-secure');
		}

		return $this->binkp_hsdone();
	}

	protected function protocol_init(): int
	{
		// Not Used
		return 0;
	}

	/**
	 * Set up our BINKP session
	 *
	 * @param bool $force_queue
	 * @return int
	 * @throws \Exception
	 */
	protected function protocol_session(bool $force_queue=FALSE): int
	{
		if ($this->binkp_init() !== self::OK)
			return self::S_FAILURE;

		$this->force_queue = $force_queue;
		if (! $this->binkp_hs())
			return self::S_FAILURE;

		while (TRUE) {
			if ((! $this->sessionGet(self::SE_INIT))
				&& (! $this->sessionGet(self::SE_SENDFILE))
				&& (! $this->sessionGet(self::SE_SENTEOB))
				&& (! $this->sessionGet(self::SE_NOFILES))
				&& (! $this->send->fd))
			{
				if (! $this->send->togo_count)
					$this->getFiles($this->node);

				// Open our next file to send
				if ($this->send->togo_count && ! $this->send->fd) {
					Log::info(sprintf('%s:- Opening next file to send - we have [%d] left',self::LOGKEY,$this->send->togo_count));
					$this->send->open();
				}

				// We have an open file descriptor, set our mode to send
				if ($this->send->fd) {
					$this->sessionSet(self::SE_SENDFILE);

					// NR mode, we wait for an M_GET before sending
					if ($this->capGet(self::F_NOREL,self::O_YES)) {
						$this->sessionSet(self::SE_WAITGET);

						Log::debug(sprintf('%s:- NR mode, waiting for M_GET',self::LOGKEY));
					}

					$this->msgs(self::BPM_FILE,
						sprintf('%s %lu %lu %ld %s',
							$this->send->nameas,
							$this->send->size,
							$this->send->mtime,
							$this->sessionGet(self::SE_WAITGET) ? -1 : 0,
							/*$this->send->comp ?:*/ ''));

					$this->sessionClear(self::SE_SENTEOB);

				// We dont have anything to send
				} else {
					Log::info(sprintf('%s:- Nothing left to send in this batch',self::LOGKEY));
					// @todo We should look for more mail/files before thinking about sending an EOB
					// IE: When we are set to only send X messages, but we have > X to send, get the next batch.
					$this->sessionSet(self::SE_NOFILES);
				}
			}

			if ((! $this->sessionGet(self::SE_INIT))
				&& (! $this->sessionGet(self::SE_WAITGOT))
				&& (! $this->sessionGet(self::SE_SENTEOB))
				&& (! $this->sessionGet(self::SE_DELAYEOB))
				&& $this->sessionGet(self::SE_NOFILES))
			{
				Log::info(sprintf('%s:- Sending EOB',self::LOGKEY));
				$this->msgs(self::BPM_EOB,'');
				$this->sessionSet(self::SE_SENTEOB);
			}

			$this->rc = self::S_OK;

			if ($this->sessionGet(self::SE_SENTEOB) && $this->sessionGet(self::SE_RECVEOB)) {
				Log::info(sprintf('%s:- EOBs sent and received',self::LOGKEY),['m'=>$this->mib,'remote_version'=>$this->node->get_versionint()]);

				if ($this->mib < 3 || $this->node->get_versionint() <= 100) {
					break;
				}

				Log::info(sprintf('%s:- EOBs sent and received CLEARED',self::LOGKEY));

				$this->mib = 0;
				$this->sessionClear(self::SE_RECVEOB|self::SE_SENTEOB);
				$this->sessionSet(self::SE_DELAYEOB);
			}

			$wd = ($this->mqueue->count() || $this->tx_left || ($this->sessionGet(self::SE_SENDFILE) && $this->send->fd && ! $this->sessionGet(self::SE_WAITGET)));
			$rd = TRUE;

			try {
				Log::debug(sprintf('%s:- Checking if there more data (ttySelect), timeout [%d]',self::LOGKEY,self::TIMEOUT_TIME));

				// @todo we need to catch a timeout if there are no reads/writes
				$rc = $this->client->ttySelect($rd,$wd,self::TIMEOUT_TIME);

				Log::debug(sprintf('%s:- ttySelect returned [%d]',self::LOGKEY,$rc));

			} catch (\Exception) {
				$this->error_close();
				$this->error = -2;

				break;
			}

			$this->rc = ($this->originate ? (self::S_REDIAL|self::S_ADDTRY) : self::S_BUSY);

			if ($rc === 0) {
				$this->error_close();
				$this->error = -1;

				break;
			}

			if ($rd && ! $this->binkp_recv()) {
				Log::info(sprintf('%s:- BINKP finished reading',self::LOGKEY));

				break;
			}

			if (($this->mqueue->count() || $wd) && ! $this->binkp_send() && (! $this->send->togo_count)) {
				Log::info(sprintf('%s:- BINKP finished sending',self::LOGKEY));

				break;
			}
		}

		if ($this->error === -1)
			Log::error(sprintf('%s:! protocol_session TIMEOUT',self::LOGKEY));
		elseif ($this->error > 0)
			Log::error(sprintf('%s:! During our protocol session we got ERROR [%d]',self::LOGKEY,$this->error));

		while (! $this->error) {
			try {
				Log::debug(sprintf('%s:- BINKP reading [%d]',self::LOGKEY,self::MAX_BLKSIZE));
				$buf = $this->client->read(0,self::MAX_BLKSIZE);
				Log::debug(sprintf('%s:- BINKP got [%d] chars',self::LOGKEY,strlen($buf)));

			} catch (\Exception $e) {
				if ($e->getCode() !== 11) {
					Log::error(sprintf('%s:! Got an exception [%d] while reading (%s)',self::LOGKEY,$e->getCode(),$e->getMessage()));

					$this->error = 1;
				}

				break;
			}

			if (strlen($buf) === 0)
				break;

			Log::warning(sprintf('%s:- Purged [%d] bytes from input stream (%s) ',self::LOGKEY,strlen($buf),hex_dump($buf)));
		}

		Log::debug(sprintf('%s:- We have [%d] messages and [%d] data left to send',self::LOGKEY,$this->mqueue->count(),strlen($this->tx_left)));
		while (! $this->error && ($this->mqueue->count() || $this->tx_left) && $this->binkp_send()) {}

		return $this->rc;
	}

	public function getFiles(Node $node): void
	{
		// Add our mail to the queue if we have authenticated
		if ($node->aka_authed) {
			Log::info(sprintf('%s:- We have authed these AKAs [%s]',self::LOGKEY,$node->aka_remote_authed->pluck('ftn')->join(',')));

			foreach ($node->aka_remote_authed as $ao) {
				Log::debug(sprintf('%s:- Checking for any new mail and files to [%s]',self::LOGKEY,$ao->ftn));

				if (! $ao->validated) {
					Log::alert(sprintf('%s:! Address [%s] is not validated, so we wont bundle mail for it',self::LOGKEY,$ao->ftn));
					continue;
				}

				$this->send->mail($ao);
				$this->send->files($ao);
				$this->send->dynamic($ao);

				/*
				 * Add "dynamic files", eg: nodelist, nodelist segment, status reports.
				 * Dynamic files are built on the fly
				 * * query "dynamic" for items for the address
				 * * column 'method' identifies the method that will be called, with the $ao as the argument
				 * * a 'new Item' is added to the queue
				 * * when it its ready to be sent, the __tostring() is called that renders it
				 * * when sent, the dynamic table is updated with the sent_at
				 */
			}

			Log::info(sprintf('%s:- We have [%d] items to send to [%s]',self::LOGKEY,$this->send->togo_count,$ao->system->name));

		} else {
			// @todo We should only send netmail if unauthenticated - netmail that is direct to this node (no routing)
			Log::debug(sprintf('%s:- Not AUTHed so not looking for mail, but we know these akas [%s]',self::LOGKEY,$node->aka_remote->pluck('ftn')->join(',')));
		}
	}

	/**
	 * Return the string delimited by char and shorten the input to the remaining characters
	 *
	 * @param string $str
	 * @param string $char
	 * @return string
	 */
	private function strsep(string &$str,string $char): string
	{
		if ($x=strpos($str,$char)) {
			$return = substr($str,0,$x);
			$str = substr($str,$x+1);

		} else {
			$return = $str;
			$str = '';
		}

		return $return;
	}
}