<?php

namespace App\Classes\FTN;

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\Models\Address;
use App\Rules\TwoByteInteger;
use App\Traits\GetNode;

/**
 * Class Message
 * NOTE: FTN Echomail Messages are ZONE agnostic.
 *
 * @package App\Classes
 */
class Message extends FTNBase
{
	//use GetNode;

	// Single value kludge items
	private array $_kludge = [
		'chrs' => 'CHRS: ',
		'charset' => 'CHARSET: ',
		'codepage' => 'CODEPAGE: ',
		'msgid' => 'MSGID: ',
		'pid' => 'PID: ',
		'replyid' => 'REPLY: ',
		'tid' => 'TID: ',
		'tzutc' => 'TZUTC: ',
	];

	// Flags for messages
	public const FLAG_PRIVATE       = 1<<0;
	public const FLAG_CRASH         = 1<<1;
	public const FLAG_RECD          = 1<<2;
	public const FLAG_SENT          = 1<<3;
	public const FLAG_FILEATTACH    = 1<<4;
	public const FLAG_INTRANSIT     = 1<<5;
	public const FLAG_ORPHAN        = 1<<6;
	public const FLAG_KILLSENT      = 1<<7;
	public const FLAG_LOCAL         = 1<<8;
	public const FLAG_HOLD          = 1<<9;
	public const FLAG_UNUSED_10     = 1<<10;
	public const FLAG_FREQ          = 1<<11;
	public const FLAG_RETRECEIPT    = 1<<12;
	public const FLAG_ISRETRECEIPT  = 1<<13;
	public const FLAG_AUDITREQ      = 1<<14;
	public const FLAG_FILEUPDATEREQ = 1<<15;
	public const FLAG_ECHOMAIL		= 1<<16;

	// FTS-0001.016 Message header 32 bytes node, net, flags, cost, date
	private const HEADER_LEN = 0x20;	// Length of message header
	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
		'date'	=> [0x0c,'A20',20]		// Message Date			FTS-0001.016 Date: upto 20 chars null terminated
	];

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

	private ?ValidatorResult $errors = NULL;	// Packet validation
	private array $header;						// Message Header
	private Collection $kludge;					// Hold kludge items
	private string $user_from;					// User message is From
	private string $user_to;					// User message is To
	private string $subject;					// Message subject
	private string $message;					// The actual message content
	private string $origin;						// FTS-0004.001
	private ?string $echoarea = NULL;			// FTS-0004.001
	private array $zone;						// Zone the message belongs to. (src/dst - for netmail)
	private array $point;						// Point the message belongs to (Netmail)
	private array $netmail;						// Netmail details

	private Collection $path;					// FTS-0004.001 The message PATH lines
	private Collection $seenby;					// FTS-0004.001 The message SEEN-BY lines
	private Collection $via;					// The path the message has gone using Via lines (Netmail)
	private Collection $_other;					// Temporarily hold attributes we dont process yet.
	private Collection $unknown;				// Temporarily hold attributes we have no logic for.

	public function __construct(string $msg)
	{
		$this->kludge = collect();
		$this->path = collect();
		$this->seenby = collect();
		$this->via = collect();
		$this->_other = collect();
		$this->unknown = collect();
		$this->zone = [];
		$this->point = [];

		$this->header = unpack($this->unpackheader(self::header),substr($msg,0,self::HEADER_LEN));

		$ptr = 0;
		// To User
		$this->user_to = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE);
		$ptr += strlen($this->user_to)+1;

		// From User
		$this->user_from = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE);
		$ptr += strlen($this->user_from)+1;

		// Subject
		$this->subject = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE);
		$ptr += strlen($this->subject)+1;

		// Check if this is an Echomail
		if (! strncmp(substr($msg,self::HEADER_LEN+$ptr),'AREA:',5)) {
			$this->echoarea = substr($msg,self::HEADER_LEN+$ptr+5,strpos($msg,"\r",self::HEADER_LEN+$ptr+5)-(self::HEADER_LEN+$ptr+5));
			$ptr += strlen($this->echoarea)+5+1;
		}

		$this->parseMessage(substr($msg,self::HEADER_LEN+$ptr));

		if (($x=$this->validate()->getMessageBag())->count())
			Log::debug('Message fails validation',['result'=>$x]);
	}

	public function __get($key)
	{
		switch ($key) {
			// From Addresses
			case 'fz': return Arr::get($this->zone,'src',0);
			case 'fn': return Arr::get($this->header,'onet');
			case 'ff': return Arr::get($this->header,'onode');
			case 'fp': return Arr::get($this->point,'src');

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

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

			case 'date':
				return sprintf('%s (%s)',Arr::get($this->header,$key),$this->kludge->get('tzutc'));

			case 'flags':
			case 'cost': return Arr::get($this->header,$key);

			case 'msgid': return $this->kludge->get('msgid');

			case 'message':
			case 'subject':
			case 'user_to':
			case 'user_from':
			case 'kludge':
			case 'path':
			case 'seenby':
			case 'errors':
			case 'echoarea':
				return $this->{$key};

			/*
			case 'tearline':
				return '--- FTNHub';
			*/

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

	/**
	 * Export an FTN message, ready for sending.
	 *
	 * @return string
	 * @todo To rework
	 */
	public function __toString(): string
	{
		// if (f->net == 65535) { /* Point packet - Get Net from auxNet */
		$return = '';

		$return .= pack(join('',collect(self::header)->pluck(1)->toArray()),
			$this->ff,
			$this->tf,
			$this->fn,
			$this->tn,
			$this->flags,
			$this->cost
		);

		// @todo use pack for this.
		$return .= $this->date->format('d M y  H:i:s')."\00";
		$return .= $this->to."\00";
		$return .= $this->from."\00";
		$return .= $this->subject."\00";

		if ($this->type == 'echomail')
			$return .= "AREA:".$this->echoarea."\r";

		// Add some kludges
		$return .= "\01MSGID ".$this->_fqfa." 1"."\r";

		foreach ($this->_kludge as $k=>$v) {
			if ($x=$this->kludge->get($k))
				$return .= chr(1).$v.$x."\r";
		}

		$return .= $this->message."\r";
		$return .= $this->tearline."\r";
		$return .= $this->origin."\r";

		switch ($this->type)
		{
			case 'echomail':
				break;

			case 'netmail':
				foreach ($this->via as $k=>$v)
					$return .= "\01Via: ".$v."\r";

				// @todo Set product name/version as var
				$return .= sprintf('%sVia: %s @%s.UTC %s %i.%i',
					chr(1),
					'10:0/0',
					now('UTC')->format('Ymd.His'),
					'FTNHub',
					1,1)."\r";

				break;
		}

		$return .= "\00";

		return $return;
	}

	/**
	 * Return an array of flag descriptions
	 *
	 * @return array
	 *
	 * http://ftsc.org/docs/fsc-0001.000
	 *       AttributeWord   bit       meaning
                      ---       --------------------
                        0  +    Private
                        1  + s  Crash
                        2       Recd
                        3       Sent
                        4  +    FileAttached
                        5       InTransit
                        6       Orphan
                        7       KillSent
                        8       Local
                        9    s  HoldForPickup
                       10  +    unused
                       11    s  FileRequest
                       12  + s  ReturnReceiptRequest
                       13  + s  IsReturnReceipt
                       14  + s  AuditRequest
                       15    s  FileUpdateReq

		s - this bit is supported by SEAdog only
		+ - this bit is not zeroed before packeting
	 */
	/*
	public function flags(int $flags): array
	{
		return [
			'private' => $this->isFlagSet($flags,self::FLAG_PRIVATE),
			'crash' => $this->isFlagSet($flags,self::FLAG_CRASH),
			'recd' => $this->isFlagSet($flags,self::FLAG_RECD),
			'sent' => $this->isFlagSet($flags,self::FLAG_SENT),
			'killsent' => $this->isFlagSet($flags,self::FLAG_KILLSENT),
			'local' => $this->isFlagSet($flags,self::FLAG_LOCAL),
		];
	}

	private function isFlagSet($value,$flag): bool
	{
		return (($value & $flag) == $flag);
	}
	*/

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

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

		// Split out the <SOH> lines
		$result = collect(explode("\01",$message))->filter();

		$this->message = '';

		foreach ($result as $v) {
			// Search for \r - if that is the end of the line, then its a kludge
			$x = strpos($v,"\r");
			$t = '';

			// If there are more characters, then put the kludge back into the result, so that we process it.
			if ($x != strlen($v)-1) {
				/**
				 * Anything after the origin line is also kludge data.
				 */
				if ($y = strpos($v,"\r * Origin: ")) {
					$this->message .= substr($v,$x+1,$y-$x-1);
					$this->parseOrigin(substr($v,$y));

					// If this is netmail, the FQFA will have been set by the INTL line, we can skip the rest of this
					$matches = [];

					// Capture the fully qualified 4D name from the Origin Line - it tells us the ZONE.
					preg_match('/^.*\((.*)\)$/',$this->origin,$matches);

					// Double check we have an address in the origin line
					if (! Arr::get($matches,1))
						throw new InvalidPacketException(sprintf('No address in Origin?',$matches));

					// Double check, our src and origin match
					$ftn = Address::parseFTN($matches[1]);

					// We'll double check our FTN
					if (($ftn['n'] !== $this->fn) || ($ftn['f'] !== $this->ff)) {
						Log::error(sprintf('FTN [%s] doesnt match message header',$matches[1]),['ftn'=>$ftn]);
					}

					$this->zone['src'] = $ftn['z'];
					$this->point['src'] = $ftn['p'];

				// The message is the rest?
				} elseif (strlen($v) > $x+1) {
					$this->message .= substr($v,$x+1);
				}

				$v = substr($v,0,$x+1);
			}

			foreach ($this->_kludge as $a => $b) {
				if ($t = $this->kludge($b,$v)) {
					$this->kludge->put($a,$t);
					break;
				}
			}

			// There is more text.
			if ($t)
				continue;

		    // From point: <SOH>"FMPT <point number><CR>
			if ($t = $this->kludge('FMPT ',$v))
				$this->point['src'] = $t;

			/*
			 * 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>
			 */
			elseif ($t = $this->kludge('INTL ',$v)) {
				$this->netmail['intl'] = $t;

				// INTL kludge is in Netmail, so we'll do some validation:
				list($this->netmail['dst'],$this->netmail['src']) = explode(' ',$t);

				$src = Address::parseFTN($this->netmail['src']);
				if (($src['n'] !== $this->fn) || ($src['f'] !== $this->ff)) {
					Log::error(sprintf('INTL src address [%s] doesnt match packet',$this->netmail['src']));
				} else {
					// We'll set our source zone
					$this->zone['src'] = $src['z'];
				}

				$dst = Address::parseFTN($this->netmail['dst']);
				if (($dst['n'] !== $this->tn) || ($dst['f'] !== $this->tf)) {
					Log::error(sprintf('INTL dst address [%s] doesnt match packet',$this->netmail['dst']));
				} else {
					// We'll set our source zone
					$this->zone['dst'] = $dst['z'];
				}
			}

			elseif ($t = $this->kludge('PATH: ',$v))
				$this->path->push($t);

			// To Point: <SOH>TOPT <point number><CR>
			elseif ($t = $this->kludge('TOPT ',$v))
				$this->point['dst'] = $t;

			// <SOH>Via <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone] <Program Name> <Version> [Serial Number]<CR>
			elseif ($t = $this->kludge('Via ',$v))
				$this->via->push($t);

			// We got a kludge line we dont know about
			else
				$this->unknown->push(chop($v,"\r"));
		}
	}

	/**
	 * Process the data after the ORIGIN
	 * There may be kludge lines after the origin - notably SEEN-BY
	 *
	 * @param string $message
	 */
	private function parseOrigin(string $message)
	{
		// Split out each line
		$result = collect(explode("\r",$message))->filter();

		foreach ($result as $v) {
			foreach ($this->_kludge as $a => $b) {
				if ($t = $this->kludge($b,$v)) {
					$this->kludge->put($a,$t);
					break;
				}
			}

			if ($t = $this->kludge('SEEN-BY: ', $v))
				$this->seenby->push($t);

			elseif ($t = $this->kludge('PATH: ', $v))
				$this->path->push($t);

			elseif ($t = $this->kludge(' \* Origin: ',$v))
				$this->origin = $t;

			// We got unknown Kludge lines in the origin
			else
				$this->unknown->push($v);
		}
	}

	/**
	 * Validate details about this message
	 *
	 * @return \Illuminate\Contracts\Validation\Validator
	 */
	private function validate(): ValidatorResult
	{
		// Check lengths
		$validator = Validator::make([
			'user_from' => $this->user_from,
			'user_to' => $this->user_to,
			'subject' => $this->subject,
			'onode'	=> $this->fn,
			'dnode'	=> $this->ff,
			'onet' => $this->tn,
			'dnet' => $this->tf,
			'flags'	=> $this->flags,
			'cost' => $this->cost,
			'echoarea' => $this->echoarea,
		],[
			'user_from' => 'required|min:1|max:'.self::USER_FROM_LEN,
			'user_to' => 'required|min:1|max:'.self::USER_TO_LEN,
			'subject' => 'required|max:'.self::SUBJECT_LEN,
			'onode' => ['required',new TwoByteInteger],
			'dnode' => ['required',new TwoByteInteger],
			'onet' => ['required',new TwoByteInteger],
			'dnet' => ['required',new TwoByteInteger],
			'flags' => 'required|numeric',
			'cost' => 'required|numeric',
			'echoarea' => 'nullable|max:'.self::AREATAG_LEN,
		]);

		if ($validator->fails())
			$this->errors = $validator;

		return $validator;
	}
}