<?php

namespace App\Classes\FTN;

use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Validator as ValidatorResult;

use App\Classes\FTN as FTNBase;
use App\Exceptions\InvalidPacketException;
use App\Models\{Address,Domain,Echomail,Netmail,Setup,Zone};
use App\Rules\{TwoByteInteger,TwoByteIntegerWithZero};
use App\Traits\ObjectIssetFix;

/**
 * Class Message
 * Represents the structure of a message in a packet
 *
 * @note FTN packed echomail messages are ZONE agnostic.
 * @package App\Classes
 */
class Message extends FTNBase
{
	use ObjectIssetFix;

	private const LOGKEY = 'FM-';

	// Kludges handled here
	private const kludges = [
		'TZUTC:'=>'tzutc',
	];

	// Flags for messages
	/** @var int Private message */
	public const FLAG_PRIVATE		= 1<<0;
	/** @var int Crash priority message (Crash + Hold = Direct) */
	public const FLAG_CRASH			= 1<<1;
	/** @var int Read by addressee */
	public const FLAG_RECD			= 1<<2;
	/** @var int Message has been sent */
	public const FLAG_SENT			= 1<<3;
	/** @var int File attached (filename in subject) */
	public const FLAG_FILEATTACH	= 1<<4;
	/** @var int Message in transit to another destination */
	public const FLAG_INTRANSIT 	= 1<<5;
	/** @var int Unknown destination - node not in nodelist */
	public const FLAG_ORPHAN		= 1<<6;
	/** @var int Kill after mailing */
	public const FLAG_KILLSENT 		= 1<<7;
	/** @var int Message originated here */
	public const FLAG_LOCAL			= 1<<8;
	/** @var int Hold message here to be collected (Crash + Hold = Direct) */
	public const FLAG_HOLD 			= 1<<9;
	/** @var int Reserved for future use by FTS-0001 */
	public const FLAG_UNUSED_10		= 1<<10;
	/** @var int Requesting a file (filename in subject) */
	public const FLAG_FREQ			= 1<<11;
	/** @var int Return Receipt requested */
	public const FLAG_RETRECEIPT	= 1<<12; // (RRQ)
	/** @var int Return Receipt message in response to an RRQ */
	public const FLAG_ISRETRECEIPT	= 1<<13;
	/** @var int Request audit trail */
	public const FLAG_AUDITREQ		= 1<<14; // (ARQ)
	/** @var int Requesting a file update (filename in subject) */
	public const FLAG_FILEUPDATEREQ = 1<<15; // (URQ)
	/**	@var int Echomail has been scanned out */
	public const FLAG_ECHOMAIL		= 1<<16;
	/** @var int Use packet password on the subject line for this message */
	public const FLAG_PKTPASSWD		= 1<<17;

	// FTS-0001.016 Message header 32 bytes node, net, flags, cost, date
	private const HEADER = [			// Struct of message header
		'onode'		=> [0x00,'v',2],		// Originating Node
		'dnode'		=> [0x02,'v',2],		// Destination Node
		'onet'		=> [0x04,'v',2],		// Originating Net
		'dnet'		=> [0x06,'v',2],		// Destination Net
		'flags'		=> [0x08,'v',2],		// Message Flags
		'cost'		=> [0x0a,'v',2],		// Send Cost
		'datetime'	=> [0x0c,'a20',20]		// Message Date			FTS-0001.016 Date: upto 20 chars null terminated
	];

	public const USER_FROM_LEN	= 36;	// FTS-0001.016 From Name: upto 36 chars null terminated
	public const USER_TO_LEN	= 36;	// FTS-0001.016 To Name: upto 36 chars null terminated
	public const SUBJECT_LEN	= 71;	// FTS-0001.016 Subject: upto 72 chars null terminated
	public const AREATAG_LEN	= 35;	//

	private array $header;						// Message Header
	private int $tzutc = 0;						// TZUTC that needs to be converted to be used by Carbon @see self::kludges
	private Echomail|Netmail $mo;				// The object storing this packet message
	private Address $us;						// Our address for this message

	/** @deprecated Not sure why this is needed? */
	public bool $packed = FALSE;				// Has the message been packed successfully

	// Convert characters into printable chars
	// https://int10h.org/oldschool-pc-fonts/readme/#437_charset
	private const CP437 = [
		0x01 => 0x263a, 0x02 => 0x263b, 0x03 => 0x2665, 0x04 => 0x2666,
		0x05 => 0x2663, 0x06 => 0x2660, 0x07 => 0x2022, 0x08 => 0x25d8,
		0x09 => 0x25cb, 0x0a => 0x2509, 0x0b => 0x2642, 0x0c => 0x2640,
		0x0d => 0x266a, 0x0e => 0x266b, 0x0f => 0x263c,
		0x10 => 0x25ba, 0x11 => 0x25ca, 0x12 => 0x2195, 0x13 => 0x203c,
		0x14 => 0x00b6, 0x15 => 0x00a7, 0x16 => 0x25ac, 0x17 => 0x21a8,
		0x18 => 0x2191, 0x19 => 0x2193, 0x1a => 0x2192, 0x1b => 0x2190,
		0x1c => 0x221f, 0x1d => 0x2194, 0x1e => 0x25bc, 0x1f => 0x25bc,
		0x7f => 0x2302,
		0x80 => 0x00c7, 0x81 => 0x00fc, 0x82 => 0x00e9, 0x83 => 0x00e2,
		0x84 => 0x00e4, 0x85 => 0x00e0, 0x86 => 0x00e5, 0x87 => 0x00e7,
		0x88 => 0x00ea, 0x89 => 0x00eb, 0x8a => 0x00e8, 0x8b => 0x00ef,
		0x8c => 0x00ee, 0x8d => 0x00ec, 0x8e => 0x00c4, 0x8f => 0x00c5,
		0x90 => 0x00c9, 0x91 => 0x00e6, 0x92 => 0x00c6, 0x93 => 0x00f4,
		0x94 => 0x00f6, 0x95 => 0x00f2, 0x96 => 0x00fb, 0x97 => 0x00f9,
		0x98 => 0x00ff, 0x99 => 0x00d6, 0x9a => 0x00dc, 0x9b => 0x00a2,
		0x9c => 0x00a3, 0x9d => 0x00a5, 0x9e => 0x20a7, 0x9f => 0x0192,
		0xa0 => 0x00e1, 0xa1 => 0x00ed, 0xa2 => 0x00f3, 0xa3 => 0x00fa,
		0xa4 => 0x00f1, 0xa5 => 0x00d1, 0xa6 => 0x00aa, 0xa7 => 0x00ba,
		0xa8 => 0x00bf, 0xa9 => 0x2310, 0xaa => 0x00ac, 0xab => 0x00bd,
		0xac => 0x00bc, 0xad => 0x00a1, 0xae => 0x00ab, 0xaf => 0x00bb,
		0xb0 => 0x2591, 0xb1 => 0x2592, 0xb2 => 0x2593, 0xb3 => 0x2502,
		0xb4 => 0x2524, 0xb5 => 0x2561, 0xb6 => 0x2562, 0xb7 => 0x2556,
		0xb8 => 0x2555, 0xb9 => 0x2563, 0xba => 0x2551, 0xbb => 0x2557,
		0xbc => 0x255d, 0xbd => 0x255c, 0xbe => 0x255b, 0xbf => 0x2510,
		0xc0 => 0x2514, 0xc1 => 0x2534, 0xc2 => 0x252c, 0xc3 => 0x251c,
		0xc4 => 0x2500, 0xc5 => 0x253c, 0xc6 => 0x255e, 0xc7 => 0x255f,
		0xc8 => 0x255a, 0xc9 => 0x2554, 0xca => 0x2569, 0xcb => 0x2566,
		0xcc => 0x2560, 0xcd => 0x2550, 0xce => 0x256c, 0xcf => 0x2567,
		0xd0 => 0x2568, 0xd1 => 0x2564, 0xd2 => 0x2565, 0xd3 => 0x2559,
		0xd4 => 0x2558, 0xd5 => 0x2552, 0xd6 => 0x2553, 0xd7 => 0x256b,
		0xd8 => 0x256a, 0xd9 => 0x2518, 0xda => 0x250c, 0xdb => 0x2588,
		0xdc => 0x2584, 0xdd => 0x258c, 0xde => 0x2590, 0xdf => 0x2580,
		0xe0 => 0x03b1, 0xe1 => 0x00df, 0xe2 => 0x0393, 0xe3 => 0x03c0,
		0xe4 => 0x03a3, 0xe5 => 0x03c3, 0xe6 => 0x00b5, 0xe7 => 0x03c4,
		0xe8 => 0x03a6, 0xe9 => 0x0398, 0xea => 0x03a9, 0xeb => 0x03b4,
		0xec => 0x221e, 0xed => 0x03c6, 0xee => 0x03b5, 0xef => 0x2229,
		0xf0 => 0x2261, 0xf1 => 0x00b1, 0xf2 => 0x2265, 0xf3 => 0x2264,
		0xf4 => 0x2320, 0xf5 => 0x2321, 0xf6 => 0x00f7, 0xf7 => 0x2248,
		0xf8 => 0x00b0, 0xf9 => 0x2219, 0xfa => 0x00b7, 0xfb => 0x221a,
		0xfc => 0x207f, 0xfd => 0x00b2, 0xfe => 0x25a0, 0xff => 0x00a0,
	];

	public static function header_len(): int
	{
		return collect(static::HEADER)->sum(function($item) { return Arr::get($item,2); });
	}

	/**
	 * Pack a message for rendering in a packet
	 *
	 * @param Echomail|Netmail $o
	 * @return self
	 * @throws \Exception
	 */
	public static function packMessage(Echomail|Netmail $o): self
	{
		$oo = new self($o->fftn->zone);
		$oo->mo = $o;
		$oo->us = our_address($o->tftn);

		return $oo;
	}

	/**
	 * Parse a message from a packet
	 *
	 *  Each message has 3 discreet structures
	 *  + Pre kludge lines, each line starting with <soh> and completing with <cr>
	 *  + Message content, ending with
	 *    - tagline starting with ... (optional)
	 *    - tearline starting with --- (optional)
	 *    - origin line starting with ' * Origin ' (optional for netmail)
	 *  + Post kludge lines, which may or may not start with <soh>. There should be an <soh> if there wasnt an origin line.
	 *    - [<soh>]Via ... (netmail)
	 *    - SEEN-BY: ... (echomail)
	 *    - PATH: ... (echomail)
	 *
	 * @param string $msg
	 * @param Zone $zone
	 * @return Echomail|Netmail
	 * @throws \Exception
	 */
	public static function parseMessage(string $msg,Zone $zone): Echomail|Netmail
	{
		Log::info(sprintf('%s:= Processing message [%d] bytes from zone [%d]',self::LOGKEY,strlen($msg),$zone->zone_id));

		$header_len = self::header_len();
		$o = new self($zone);

		try {
			$o->header = unpack(self::unpackheader(self::HEADER),substr($msg,0,$header_len));

		} catch (\Exception $e) {
			Log::error(sprintf('%s:! Error bad packet header',self::LOGKEY),['e'=>$e->getMessage(),'header'=>substr($msg,0,$header_len)]);

			throw new InvalidPacketException($e->getMessage());
		}

		$ptr = 0;

		// To User
		$o->header['user_to'] = strstr(substr($msg,$header_len+$ptr),"\x00",TRUE);
		$ptr += strlen($o->header['user_to'])+1;

		// From User
		$o->header['user_from'] = strstr(substr($msg,$header_len+$ptr),"\x00",TRUE);
		$ptr += strlen($o->header['user_from'])+1;

		// Subject
		$o->header['subject'] = strstr(substr($msg,$header_len+$ptr),"\x00",TRUE);
		$ptr += strlen($o->header['subject'])+1;

		// Check if this is an Echomail
		if (! strncmp(substr($msg,$header_len+$ptr),'AREA:',5)) {
			$o->header['echoarea'] = strtoupper(substr($msg,$header_len+$ptr+5,strpos($msg,"\r",$header_len+$ptr+5)-($header_len+$ptr+5)));
			$ptr += strlen($o->header['echoarea'])+5+1;

			$eao = $zone->domain->echoareas->where('name',$o->header['echoarea'])->pop();

			$o->mo = new Echomail;

			if ($eao)
				$o->mo->echoarea_id = $eao->id;
			else
				$o->mo->set_echoarea = $o->header['echoarea'];

		} else {
			$o->mo = new Netmail;
		}

		$o->mo = $o->unpackMessage(substr($msg,$header_len+$ptr),$o->mo);

		$o->mo->to = $o->header['user_to'];
		$o->mo->from = $o->header['user_from'];
		$o->mo->subject = $o->header['subject'];

		$o->mo->datetime = $o->datetime;
		$o->mo->tzoffset = $o->datetime->utcOffset();
		$o->mo->flags = $o->header['flags'];
		$o->mo->cost = $o->header['cost'];

		if ($o->fftn)
			$o->mo->fftn_id = $o->fftn->id;
		else
			$o->mo->set_fftn = $o->fftn_t;

		switch (get_class($o->mo)) {
			case Echomail::class:
				// Echomails dont have a to address
				break;

			case Netmail::class:
				if ($o->tftn)
					$o->mo->tftn_id = $o->tftn->id;
				else
					$o->mo->set_tftn = $o->tftn_t;
				break;

			default:
				throw new InvalidPacketException('Unknown message class: '.get_class($o->mo));
		}

		$o->validate();

		return $o->mo;
	}

	/**
	 * Translate the string into something printable via the web
	 *
	 * @param string $string
	 * @param array $skip
	 * @return string
	 */
	public static function tr(string $string,array $skip=[0x0a,0x0d]): string
	{
		$tr = [];

		foreach (self::CP437 as $k=>$v) {
			if (in_array($k,$skip))
				continue;

			$tr[chr($k)] = '&#'.$v;
		}

		return strtr($string,$tr);
	}

	/**
	 * Packets have no concept of a zone, so we add it for any calculations that could use it
	 * This is taken from the sender
	 *
	 * @param Zone $zone
	 */
	public function __construct(Zone $zone)
	{
		$this->zone = $zone;
	}

	public function __get($key)
	{
		// @todo Do we need all these key values?
		//Log::debug(sprintf('%s:/ Requesting key for Message::class [%s]',self::LOGKEY,$key));

		switch ($key) {
			// From Addresses
			case 'fz': return (int)Arr::get($this->src,'z');
			case 'fn': return (int)($x=$this->src) ? Arr::get($x,'n') : Arr::get($this->header,'onet');
			case 'ff': return (int)($x=$this->src) ? Arr::get($x,'f') : Arr::get($this->header,'onode');
			case 'fp': return (int)$this->mo->kludges->get('FMPT:') ?: Arr::get($this->src,'p',Arr::get($this->header,'opoint',0));
			case 'fd': return Arr::get($this->src,'d');

			case 'fzone':
				// Use the zone if this class was called with it.
				if ($this->zone && ($this->fz === $this->zone->zone_id))
					return $this->zone;

				if ($this->fdomain) {
					if (($x=$this->fdomain->zones->search(function($item) { return $item->zone_id === $this->fz; })) !== FALSE)
						return $this->fdomain->zones->get($x);
				}

				// No domain, so we'll use the default zone
				return Zone::where('zone_id',$this->fz)
					->where('default',TRUE)
					->single();
			case 'fdomain':
				// We'll use the zone's domain if this method class was called with a zone
				if ($this->zone && (($this->zone->domain->name === Arr::get($this->src,'d')) || ! Arr::get($this->src,'d')))
					return $this->zone->domain;

				// If we get the domain from the packet, we'll find it
				if ($x=Arr::get($this->src,'d')) {
					return Domain::where('name',$x)->single();
				}

				return NULL;

			// To Addresses
			// Echomail doesnt have a zone, so we'll use the source zone
			case 'tz': return (int)Arr::get($this->isEchomail() ? $this->src : $this->dst,'z');
			case 'tn': return (int)Arr::get($this->header,'dnet');
			case 'tf': return (int)Arr::get($this->header,'dnode');
			case 'tp': return (int)$this->mo->kludges->get('TOPT:') ?: Arr::get($this->header,'dpoint',0);

			case 'tzone':
				// Use the zone if this class was called with it.
				if ($this->zone && ($this->tz === $this->zone->zone_id))
					return $this->zone;

				if ($this->tdomain) {
					if (($x=$this->tdomain->zones->search(function($item) { return $item->zone_id === $this->tz; })) !== FALSE)
						return $this->tdomain->zones->get($x);
				}

				// No domain, so we'll use the default zone
				return Zone::where('zone_id',$this->tz)
					->where('default',TRUE)
					->single();

			case 'tdomain':
				// We'll use the zone's domain if this method class was called with a zone
				if ($this->zone && (($this->zone->domain->name === Arr::get($this->dst,'d')) || ! Arr::get($this->dst,'d')))
					return $this->zone->domain;

				// If we get the domain from the packet, we'll find it
				if ($x=Arr::get($this->dst,'d')) {
					return Domain::where('name',$x)->single();
				}

				// Otherwise we'll assume the same as the source domain
				return $this->fdomain ?: NULL;

			case 'fftn_t':
			case 'fftn':
			case 'tftn_t':
			case 'tftn':
				return parent::__get($key);

			// For 5D we need to include the domain
			/* @deprecated - is this required? */
			case 'fboss':
				return sprintf('%d:%d/%d',$this->fz,$this->fn,$this->ff).(($x=$this->fdomain) ? '@'.$x->name : '');
			case 'tboss':
				return sprintf('%d:%d/%d',$this->tz,$this->tn,$this->tf).(($x=$this->tdomain) ? '@'.$x->name : '');
			case 'fboss_o':
				return Address::findFTN($this->fboss);
			case 'tboss_o':
				return Address::findFTN($this->tboss);

			// Convert our message (header[datetime]) with our TZUTC into a Carbon date
			case 'datetime':
				try {
					if (str_contains($x=rtrim(Arr::get($this->header,$key),"\x00"),"\x00"))
						throw new \Exception('Date contains null values.');

					return Carbon::createFromFormat('d M y  H:i:s O',
						sprintf('%s %s%04d',$x,($this->tzutc < 0) ? '-' : '+',abs($this->tzutc)));

				} catch (InvalidFormatException|\Exception $e) {
					Log::error(sprintf('%s:! Date doesnt parse [%s] (%s)',self::LOGKEY,$e->getMessage(),Arr::get($this->header,$key)));
					throw new \Exception(sprintf('%s (%s)',$e->getMessage(),hex_dump(Arr::get($this->header,$key))));
				}

			// The source of the message
			case 'src':
				// If this is a netmail, then our 4D details are in the netmail
				if ($this->mo instanceof Netmail) {
					/*
					 * The INTL control paragraph shall be used to give information about
					 * the zone numbers of the original sender and the ultimate addressee
					 * of a message.
					 *
					 * <SOH>"INTL "<destination address>" "<origin address><CR>
					 */
					if ($this->mo->kludges->has('INTL')) {
						// INTL kludge is in Netmail, so we'll do some validation:
						list($dst,$src) = explode(' ',$this->mo->kludges->get('INTL'));

						$src = Address::parseFTN($src);

						// @todo move to validation
						if (($src['n'] !== Arr::get($this->header,'onet')) || ($src['f'] !== Arr::get($this->header,'onode'))) {
							Log::error(sprintf('%s:! INTL src address [%s] doesnt match packet',self::LOGKEY,$src),['src'=>$src,'fn'=>Arr::get($this->header,'onet'),'ff'=>Arr::get($this->header,'onode')]);
						}

						return $src;
					}

				} elseif ($this->mo instanceof Echomail) {
					// Work out our zone/point
					// http://ftsc.org/docs/fsc-0068.001
					// MSGID should be the basis of the source, if it cannot be obtained from the origin
					// If the message was gated, we'll use the gateid
					$m = [];
					if ($this->mo->origin && preg_match('#\(([0-9]+:[0-9]+/[0-9]+)?\.?([0-9]+)?@?([A-Za-z-_~]+)?\)$#',$this->mo->origin,$m)) {
						return Address::parseFTN($m[1].((isset($m[2]) && $m[2] != '') ? '.'.$m[2] : '').(isset($m[3]) ? '@'.$m[3] : ''));

					} elseif (($this->mo->msgid || $this->mo->gateid) && preg_match('#([0-9]+:[0-9]+/[0-9]+)?\.?([0-9]+)?(\.[0-9]+)?@?([A-Za-z-_~]+)?\ +#',$this->mo->gateid ?: $this->mo->msgid,$m)) {
						try {
							return Address::parseFTN($m[1].((isset($m[2]) && $m[2] != '') ? '.'.$m[2] : '').(isset($m[4]) ? '@'.$m[4] : ''));
						} catch (\Exception $e) {
							Log::error(sprintf('%s:! MSGID [%s] address is invalid [%s]',self::LOGKEY,$this->mo->msgid,$e->getMessage()));
						}

					// Otherwise get it from our zone object and packet header
					} elseif ($this->zone) {
						return Address::parseFTN(sprintf('%d:%d/%d.%d@%s',$this->zone->zone_id,$this->fn,$this->ff,$this->fp,$this->zone->domain->name));
					}

				} else {
					throw new InvalidPacketException('Dont know what type of packet this is');
				}

				break;

			case 'dst':
				// If this is a netmail, then our 4D details are in the netmail
				if ($this->mo instanceof Netmail) {
					/*
					 * The INTL control paragraph shall be used to give information about
					 * the zone numbers of the original sender and the ultimate addressee
					 * of a message.
					 *
					 * <SOH>"INTL "<destination address>" "<origin address><CR>
					 */
					if ($this->mo->kludges->has('INTL')) {
						// INTL kludge is in Netmail, so we'll do some validation:
						list($dst,$src) = explode(' ',$this->mo->kludges->get('INTL'));

						$dst = Address::parseFTN($dst);

						// @todo move to validation
						if (($dst['n'] !== $this->tn) || ($dst['f'] !== $this->tf)) {
							Log::error(sprintf('%s:! INTL dst address [%s] doesnt match packet',self::LOGKEY,$dst),['dst'=>$dst,'tn'=>$this->tn,'tf'=>$this->tf]);
						}

						return $dst;
					}

				} elseif ($this->mo instanceof Echomail) {
					// Echomail doesnt have DST addresses

				} else {
					throw new InvalidPacketException('Dont know what type of packet this is');
				}

				break;

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

	/**
	 * When we serialise this object, we'll need to utf8_encode some values
	 *
	 * @return array
	 */
	public function __serialize(): array
	{
		return $this->encode();
	}

	/**
	 * Export an FTN message, ready for sending.
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function __toString(): string
	{
		$s = Setup::findOrFail(config('app.id'));

		$return = pack(collect(self::HEADER)->pluck(1)->join(''),
			$this->mo->fftn->node_id,	// Originating Node
			$this->mo->tftn->node_id,			// Destination Node
			$this->mo->fftn->host_id,			// Originating Net
			$this->mo->tftn->host_id,			// Destination Net
			$this->mo->flags&~(self::FLAG_INTRANSIT|self::FLAG_LOCAL),	// Turn off our local/intransit bits
			$this->mo->cost,
			$this->mo->date->format('d M y  H:i:s'),
		);

		$return .= $this->mo->to."\00";
		$return .= $this->mo->from."\00";
		$return .= $this->mo->subject."\00";

		if (($this->mo instanceof Netmail) && $this->mo->isFlagSet(self::FLAG_LOCAL)) {
			// If there isnt an INTL kludge, we'll add it
			if (! $this->mo->kludges->has('INTL'))
				$this->mo->kludges->put('INTL',sprintf('%s %s',$this->mo->tftn->ftn3d,$this->mo->fftn->ftn3d));

			if ((! $this->mo->kludges->has('FMPT')) && $this->mo->fftn->point_id)
				$this->mo->kludges->put('FMPT',$this->mo->fftn->point_id);

			if ((! $this->mo->kludges->has('TOPT')) && $this->mo->tftn->point_id)
				$this->mo->kludges->put('TOPT',$this->mo->tftn->point_id);
		}

		$this->mo->kludges->put($this->mo->isFlagSet(self::FLAG_LOCAL) ? 'PID:' : 'TID:',sprintf('%s %s',Setup::PRODUCT_NAME_SHORT,$s->version));
		$this->mo->kludges->put('DBID:',$this->mo->id);

		if ($this->mo instanceof Echomail)
			$return .= sprintf("AREA:%s\r",strtoupper($this->mo->echoarea->name));

		// Add some kludges
		$return .= sprintf("\01TZUTC: %s\r",str_replace('+','',$this->mo->date->getOffsetString('')));

		if ($this->mo->msgid)
			$return .= sprintf("\01MSGID: %s\r",$this->mo->msgid);

		if ($this->mo->replyid)
			$return .= sprintf("\01REPLY: %s\r",$this->mo->replyid);

		foreach ($this->mo->kludges as $k=>$v)
			$return .= sprintf("\01%s %s\r",$k,$v);

		$return .= $this->mo->content."\r";

		if ($this->mo instanceof Netmail) {
			foreach ($this->mo->path as $ao)
				$return .= sprintf("\x01Via %s\r",$this->mo->via($ao));

			// Add our address
			$return .= sprintf("\x01Via %s @%s.UTC %s %s\r",
				$this->us->ftn3d,
				Carbon::now()->format('Ymd.His'),
				Setup::PRODUCT_NAME_SHORT,$s->version);

		} else {
			// FTS-0004.001/FSC-0068.001 The message SEEN-BY lines
			// FTS-0004.001/FSC-0068.001 The message PATH lines

			// @todo This unique() function here shouldnt be required, but is while system generated messages are storing path/seenby
			$path = $this->mo->path->push($this->us)->unique('ftn')->filter(fn($item)=>($item->point_id === 0));

			// Create our rogue seenby objects
			$seenby = $this->mo->seenby;

			if ($this->mo->rogue_seenby->count()) {
				$do = $this->mo->echoarea->domain;

				foreach ($this->mo->rogue_seenby as $item)
					$seenby->push(Address::newFTN(sprintf('%s@%s',$item,$do->name)));
			}

			$seenby = $seenby
				->push($this->us)
				->filter(fn($item)=>($item->point_id === 0))
				->unique('ftn')
				->sortBy(function($item) { return sprintf('%05d%05d',$item->host_id,$item->node_id);});

			$return .= $this->aka_trim($seenby,'SEEN-BY:')."\r";
			$return .= "\x01".$this->aka_trim($path,'PATH:')."\r";
		}

		$return .= "\00";

		return $return;
	}

	/**
	 * When we unserialize, we'll restore (utf8_decode) some values
	 *
	 * @param array $values
	 */
	public function __unserialize(array $values): void
	{
		$this->decode($values);
	}

	/**
	 * Reduce our PATH/SEEN-BY for messages as per FSC-0068
	 *
	 * @param Collection $path
	 * @param string $prefix
	 * @param int $len
	 * @param string $delim
	 * @return string
	 */
	private function aka_trim(Collection $path,string $prefix,int $len=79,string $delim="\r"): string
	{
		$cur = NULL;
		$result = $prefix;
		$c = strlen($prefix);

		foreach ($path as $ao) {
			[$host,$node] = explode('/',$ao->ftn2d);

			if (($c+strlen(' '.$host)) > $len) {
				$result .= ($x=$delim.$prefix);
				$c = strlen($x);
				$cur = NULL;
			}

			if ($host !== $cur) {
				$cur = $host;
				$result .= ($x=' '.$ao->ftn2d);

			} else {
				$result .= ($x=' '.$node);
			}

			$c += strlen($x);
		}

		return $result;
	}

	private function isEchomail(): bool
	{
		return $this->mo instanceof Echomail;
	}

	/**
	 * If this message doesnt have an AREATAG, then its a netmail.
	 *
	 * @return bool
	 */
	public function isNetmail(): bool
	{
		return $this->mo instanceof Netmail;
	}

	/**
	 * Extract information out of the message text.
	 *
	 * @param string $message
	 * @param Echomail|Netmail $o
	 * @return Echomail|Netmail
	 * @throws InvalidPacketException
	 */
	public function unpackMessage(string $message,Echomail|Netmail $o): Echomail|Netmail
	{
		// Remove DOS \n\r
		$message = preg_replace("/\n\r/","\r",$message);
		$message = preg_replace("/\r\n/","\r",$message);

		// First find our kludge lines
		$ptr_start = 0;

		while (substr($message,$ptr_start,1) === "\x01") {
			$ptr_end = strpos($message,"\r",$ptr_start);

			$m = [];
			$kludge = ($x=substr($message,$ptr_start+1,$ptr_end-$ptr_start-1));
			preg_match('/^([^\s]+:?)+\s+(.*)$/',$kludge,$m);

			$ptr_start = $ptr_end+1;

			if (! $m) {
				Log::alert(sprintf('%s:! Invalid Kluge Line [%s]',self::LOGKEY,$x));
				continue;
			}

			// Catch any kludges we need to process here
			if (array_key_exists($m[1],self::kludges))
				$this->{self::kludges[$m[1]]} = $m[2];
			else
				$o->kludges = [$m[1],$m[2]];
		}

		// Next our message content ends with '\r * Origin: ... \r' or <soh>...
		// FTS-0004.001
		if ($ptr_end=strrpos($message,"\r * Origin: ",$ptr_start)) {
			// Find the <cr>
			$ptr_end = strpos($message,"\r",$ptr_end+1);

			// If there is no ptr_end, then this is not an origin
			if (! $ptr_end)
				throw new InvalidPacketException('Couldnt find the end of the origin');

		} elseif (! $ptr_end=strpos($message,"\r\x01",$ptr_start)) {
			throw new InvalidPacketException('Couldnt parse the end of the content');
		}

		$remaining = substr($message,$ptr_end+1);

		// At this point, the remaining part of the message should start with \x01, PATH or SEEN-BY
		if ((substr($remaining,0,9) !== 'SEEN-BY: ') && (substr($remaining,0,5) !== 'PATH:') && ($x=strpos($remaining,"\x01")) !== 0) {
			if ($x)
				$ptr_end += $x;
			else
				$ptr_end += strlen($remaining);
		}

		// Process the message content
		if ($content=substr($message,$ptr_start,$ptr_end-$ptr_start)) {
			$o->msg_src = $content;
			$o->msg_crc = md5($content);
			$ptr_content_start = 0;

			// See if we have a tagline
			if ($ptr_content_end=strrpos($content,"\r... ",$ptr_content_start)) {
				$o->msg = substr($content,$ptr_content_start,$ptr_content_end+1);

				$ptr_content_start = $ptr_content_end+5;
				$ptr_content_end = strpos($content,"\r",$ptr_content_start);

				// If there is no terminating "\r", then that's it
				if (! $ptr_content_end) {
					$o->set_tagline = substr($content,$ptr_content_start);
					$ptr_content_start = strlen($content);

				} else {
					$o->set_tagline = substr($content,$ptr_content_start,$ptr_content_end-$ptr_content_start);
					$ptr_content_start = $ptr_content_end;
				}
			}

			// See if we have a tearline
			if ($ptr_content_end=strrpos($content,"\r--- ",$ptr_content_start)) {
				if (! $ptr_content_start)
					$o->msg = substr($content,$ptr_content_start,$ptr_content_end+1);

				$ptr_content_start = $ptr_content_end+5;
				$ptr_content_end = strpos($content,"\r",$ptr_content_start);

				// If there is no terminating "\r", then that's it
				if (! $ptr_content_end) {
					$o->set_tearline = substr($content,$ptr_content_start);
					$ptr_content_start = strlen($content);

				} else {
					$o->set_tearline = substr($content,$ptr_content_start,$ptr_content_end-$ptr_content_start);
					$ptr_content_start = $ptr_content_end;
				}
			}

			// See if we have an origin
			if ($ptr_content_end=strrpos($content,"\r * Origin: ",$ptr_content_start)) {
				if (! $ptr_content_start)
					$o->msg = substr($content,$ptr_content_start,$ptr_content_end);

				$ptr_content_start = $ptr_content_end+12;

				$ptr_content_end = strpos($content,"\r",$ptr_content_start);

				// If there is no terminating "\r", then that's it
				if (! $ptr_content_end) {
					$o->set_origin = substr($content,$ptr_content_start);
					$ptr_content_start = strlen($content);

				} else {
					$o->set_origin = substr($content,$ptr_content_start,$ptr_content_end-$ptr_content_start);
					$ptr_content_start = $ptr_content_end+1;
				}
			}

			// If there wasnt any tagline/tearline/origin, then the whole content is the message
			if (! $ptr_content_start) {
				$o->msg = $content;
				$ptr_content_start = $ptr_end-$ptr_start;
			}

			// Trim any right \r from the message
			$o->msg = rtrim($o->msg,"\r");

			// Quick validation that we are done
			if ($ptr_content_start !== strlen($content))
				throw new InvalidPacketException('There is more data in the message content?');
		}

		$ptr_start = $ptr_end+1;

		// Finally work out control kludges
		foreach (collect(explode("\r",substr($message,$ptr_start)))->filter() as $line) {
			// If the line starts with <soh> ignore it
			if (substr($line,0,1) === "\x01")
				$line = ltrim($line,"\x01");

			$m = [];
			preg_match('/^([^\s]+:?)+\s+(.*)$/',$line,$m);
			$o->kludges = [$m[1],$m[2]];
		}

		return $o;
	}

	/**
	 * Validate details about this message
	 *
	 * @return \Illuminate\Contracts\Validation\Validator
	 */
	public function validate(): ValidatorResult
	{
		// Check lengths
		$validator = Validator::make(
			array_merge($this->mo->toArray(),[
				'echoarea' => $this->isEchomail() ? ($this->mo->set->get('set_echoarea') ?: $this->mo->echoarea->name ) : NULL,
				'onode'	=> $this->ff,
				'dnode'	=> $this->tf,
				'onet' => $this->fn,
				'dnet' => $this->tn,
				'ozone' => $this->fz,
				'dzone' => $this->tz,
			])
			,[
				'from' => 'required|min:1|max:'.self::USER_FROM_LEN,
				'to' => 'required|min:1|max:'.self::USER_TO_LEN,
				'subject' => 'required|max:'.self::SUBJECT_LEN,
				'datetime' => 'required|date', // |after:30 days ago',	// @todo within "x" days
				'tzoffset' => 'present|numeric',
				'flags' => 'required|numeric',
				'cost' => 'required|numeric',
				'msgid' => 'sometimes|min:1',
				'replyid' => 'sometimes|min:1',
				'msg' => 'required|min:1',		// @todo max message length?
				'msg_crc' => 'required|size:32',
				'local' => 'sometimes|boolean',
				'fftn_id' => 'required|exists:App\Models\Address,id',
				'tftn_id' => $this->isNetmail() ? 'required|exists:App\Models\Address,id' : 'prohibited',
				'rogue_path' => $this->isEchomail() ? 'sometimes|array' : 'prohibited',
				'rogue_seenby' => $this->isEchomail() ? 'sometimes|array' : 'prohibited',
				'kludges' => 'present|array',	// @todo add in required KEYS like INTL for netmails

				'onode' => ['required',new TwoByteIntegerWithZero],
				'dnode' => ['required',new TwoByteIntegerWithZero],
				'onet' => ['required',new TwoByteInteger],
				'dnet' => ['required',new TwoByteInteger],
				'ozone' => ['required',new TwoByteInteger],
				'dzone' => ['required',new TwoByteInteger],

				'echoarea_id' => $this->isEchomail() ? 'sometimes|exists:App\Models\Echoarea,id' : 'prohibited',
				'echoarea' => $this->isEchomail() ? 'required|max:'.self::AREATAG_LEN : 'prohibited',
			]
		);

		$validator->after(function($validator) {
			if ($this->zone->domain->flatten) {
				if (! $this->zone->domain->zones->pluck('zone_id')->contains($this->fz))
					$validator->errors()->add('invalid-zone',sprintf('Message zone [%d] doesnt match any zone in domain for packet zone [%d].',$this->fz,$this->zone->zone_id));

			} else {
				if ($this->zone->zone_id !== $this->fz)
					$validator->errors()->add('invalid-zone',sprintf('Message zone [%d] doesnt match packet zone [%d].',$this->fz,$this->zone->zone_id));
			}

			if (! $this->fftn)
				$validator->errors()->add('from',sprintf('Undefined Node [%s] sent message.',$this->fftn_t));
			if ($this->isNetmail() && (! $this->tftn))
				$validator->errors()->add('to',sprintf('Undefined Node [%s] for destination.',$this->tftn_t));
		});

		$this->mo->errors = $validator->errors();

		if ($validator->fails())
			Log::debug(sprintf('%s:! Message fails validation (%s@%s->%s@%s)',self::LOGKEY,$this->mo->from,$this->fftn_t,$this->mo->to,$this->tftn_t),['result'=>$validator->errors()]);

		return $validator;
	}
}