<?php

namespace App\Classes;

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

use App\Classes\File\{Receive,Send};
use App\Classes\Protocol\EMSI;
use App\Classes\Sock\Exception\SocketException;
use App\Classes\Sock\SocketClient;
use App\Models\{Address,Mailer,Setup,System,SystemLog};

// @todo after receiving a mail packet/file, dont acknowledge it until we can validate that we can read it properly.

abstract class Protocol
{
	// Enable extra debugging
	protected const DEBUG = FALSE;

	private const LOGKEY = 'P--';

	/* CONSTS */

	// Return constants
	protected const OK				= 0;
	protected const TIMEOUT			= -2;
	protected const RCDO			= -3;
	protected const ERROR			= -5;

	protected const MAX_PATH		= 1024;

	/* O_ options - [First 9 bits are protocol P_* (Interfaces/ZModem)] */

	/** 0000 0000 0000 0000 0010 0000 0000 BOTH - No file requests accepted by this system */
	protected const O_NRQ			= 1<<9;
	/** 0000 0000 0000 0000 0100 0000 0000 BOTH - Hold file requests (not processed at this time). */
	protected const O_HRQ			= 1<<10;
	/** 0000 0000 0000 0000 1000 0000 0000 - Filename conversion, transmitted files must be 8.3 */
	protected const O_FNC			= 1<<11;
	/** 0000 0000 0000 0001 0000 0000 0000 - Supports other forms of compressed mail */
	protected const O_XMA			= 1<<12;
	/** 0000 0000 0000 0010 0000 0000 0000 BOTH - Hold ALL files (Answering System) */
	protected const O_HAT			= 1<<13;
	/** 0000 0000 0000 0100 0000 0000 0000 BOTH - Hold Mail traffic */
	protected const O_HXT			= 1<<14;
	/** 0000 0000 0000 1000 0000 0000 0000 - No files pickup desired (Calling System) */
	protected const O_NPU			= 1<<15;
	/** 0000 0000 0001 0000 0000 0000 0000 - Pickup files for primary address only */
	protected const O_PUP			= 1<<16;
	/** 0000 0000 0010 0000 0000 0000 0000 EMSI - Pickup files for all presented addresses */
	protected const O_PUA			= 1<<17;
	/** 0000 0000 0100 0000 0000 0000 0000 BINK - Node needs to be password validated */
	protected const O_PWD			= 1<<18;
	/** 0000 0000 1000 0000 0000 0000 0000 BOTH - Node invalid password presented */
	protected const O_BAD			= 1<<19;
	/** 0000 0001 0000 0000 0000 0000 0000 EMSI - Use RH1 for Hydra (files-after-freqs) */
	protected const O_RH1			= 1<<20;
	/** 0000 0010 0000 0000 0000 0000 0000 BOTH - Node is nodelisted */
	protected const O_LST			= 1<<21;
	/** 0000 0100 0000 0000 0000 0000 0000 BOTH - Inbound session */
	protected const O_INB			= 1<<22;
	/** 0000 1000 0000 0000 0000 0000 0000 BOTH - TCP session */
	protected const O_TCP			= 1<<23;
	/** 0001 0000 0000 0000 0000 0000 0000 EMSI - Remote understands EMSI-II */
	protected const O_EII			= 1<<24;

	/* Negotiation Options */

	/** 00 0000 I/They dont want a capability? */
	protected const O_NO			= 0;
	/** 00 0001 - I want a capability, but can be persuaded */
	protected const O_WANT			= 1<<0;
	/** 00 0010 - They want a capability and we want it too */
	protected const O_WE			= 1<<1;
	/** 00 0100 - They want a capability */
	protected const O_THEY			= 1<<2;
	/** 00 1000 - I want a capability, and wont compromise */
	protected const O_NEED			= 1<<3;
	/** 01 0000 - Extra options set */
	protected const O_EXT			= 1<<4;
	/** 10 0000 - We agree on a capability and we are set to use it */
	protected const O_YES			= 1<<5;

	// Session Status
	public const S_OK				= 0;
	protected const S_NODIAL		= 1;
	protected const S_REDIAL		= 2;
	protected const S_BUSY			= 3;
	protected const S_FAILURE		= 4;
	public const S_MASK				= 7;
	protected const S_HOLDR			= 8;
	protected const S_HOLDX			= 16;
	protected const S_HOLDA			= 32;
	protected const S_ADDTRY		= 64;
	protected const S_ANYHOLD		= (self::S_HOLDR|self::S_HOLDX|self::S_HOLDA);

	// File transfer status

	public const FOP_OK			= 0;
	public const FOP_CONT		= 1;
	public const FOP_SKIP		= 2;
	public const FOP_ERROR		= 3;
	public const FOP_SUSPEND		= 4;
	public const FOP_GOT			= 5;

	public const TCP_SPEED			= 115200;

	protected SocketClient $client;									/* Our socket details */
	protected ?Setup $setup;										/* Our setup */
	protected Node $node;											/* The node we are communicating with */
	/** The list of files we are sending */
	protected Send $send;
	/** The list of files we are receiving */
	protected Receive $recv;

	/** @var int The active options for a session */
	private int $options;
	/** @var int Tracks the session state */
	private int $session;
	/** @var array Our negotiated capability for a protocol session */
	protected array $capability; // @todo make private
	/** @var bool Are we originating a connection */
	protected bool $originate;
	/** Our comms details */

	protected bool $down = FALSE;

	private array $comms;

	protected bool $force_queue = FALSE;

	abstract protected function protocol_init(): int;

	abstract protected function protocol_session(bool $force_queue=FALSE): int;

	public function __construct(Setup $o=NULL)
	{
		if ($o && ! $o->system->akas->count())
			throw new \Exception('We dont have any ACTIVE FTN addresses assigned');

		$this->setup = $o;
	}

	/**
	 * @throws \Exception
	 */
	public function __get($key)
	{
		switch ($key) {
			case 'ls_SkipGuard':									/* double-skip protection on/off */
			case 'rxOptions':										/* Options from ZRINIT header */
				return $this->comms[$key] ?? 0;

			case 'ls_rxAttnStr':
				return $this->comms[$key] ?? '';

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

	/**
	 * @throws \Exception
	 */
	public function __set($key,$value)
	{
		switch ($key) {
			case 'ls_rxAttnStr':
			case 'ls_SkipGuard':
			case 'rxOptions':
				$this->comms[$key] = $value;
				break;

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

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

	/* Capabilities are what we negotitate with the remote and are valid for the session */

	/**
	 * Clear a capability bit
	 *
	 * @param int $cap (F_*)
	 * @param int $val (O_*)
	 * @return void
	 */
	public function capClear(int $cap,int $val): void
	{
		if (! array_key_exists($cap,$this->capability))
			$this->capability[$cap] = 0;

		$this->capability[$cap] &= ~$val;
	}

	/**
	 * Get a session bit (SE_*)
	 *
	 * @param int $cap (F_*)
	 * @param int $val (O_*)
	 * @return bool
	 */
	protected function capGet(int $cap,int $val): bool
	{
		if (! array_key_exists($cap,$this->capability))
			$this->capability[$cap] = 0;

		if ($val === self::O_WE)
			return $this->capGet($cap,self::O_WANT) && $this->capGet($cap,self::O_THEY);

		return $this->capability[$cap] & $val;
	}

	/**
	 * Set a session bit (SE_*)
	 *
	 * @param int $cap (F_*)
	 * @param int $val (O_*)
	 */
	protected function capSet(int $cap,int $val): void
	{
		if (! array_key_exists($cap,$this->capability) || $val === 0)
			$this->capability[$cap] = 0;

		$this->capability[$cap] |= $val;
	}

	/**
	 * We got an error, close anything we are have open
	 *
	 * @throws \Exception
	 */
	protected function error_close(): void
	{
		if ($this->send->fd)
			$this->send->close(FALSE,$this->node);

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

	/**
	 * Incoming Protocol session
	 *
	 * @param SocketClient $client
	 * @return int|null
	 * @throws SocketException
	 */
	public function onConnect(SocketClient $client): ?int
	{
		$pid = pcntl_fork();

		if ($pid === -1)
			throw new SocketException(SocketException::CANT_ACCEPT,'Could not fork process');

		if ($pid)
			Log::info(sprintf('%s:+ New connection from [%s], thread [%d] created',self::LOGKEY,$client->address_remote,$pid));

		// Parent return ready for next connection
		return $pid;
	}

	/* O_* determine what features processing is availabile */

	/**
	 * Clear an option bit (O_*)
	 *
	 * @param int $key
	 * @return void
	 */
	protected function optionClear(int $key): void
	{
		$this->options &= ~$key;
	}

	/**
	 * Get an option bit (O_*)
	 *
	 * @param int $key
	 * @return int
	 */
	protected function optionGet(int $key): int
	{
		return ($this->options & $key);
	}

	/**
	 * Set an option bit (O_*)
	 *
	 * @param int $key
	 * @return void
	 */
	protected function optionSet(int $key): void
	{
		$this->options |= $key;
	}

	/**
	 * Our addresses to send to the remote
	 *
	 * @return Collection
	 */
	protected function our_addresses(): Collection
	{
		if ($this->setup->optionGet(Setup::O_HIDEAKA,'options_options')) {
			$addresses = collect();

			foreach (($this->originate ? $this->node->aka_remote_authed : $this->node->aka_remote) as $ao)
				$addresses = $addresses->merge(our_address($ao->zone->domain));

			$addresses = $addresses->unique();

			Log::debug(sprintf('%s:- Presenting limited AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(',')));

		} else {
			$addresses = $this->setup->system->akas;

			Log::debug(sprintf('%s:- Presenting ALL our AKAs [%s]',self::LOGKEY,$addresses->pluck('ftn')->join(',')));
		}

		return $addresses;
	}

	/**
	 * Initialise our Session
	 *
	 * @param Mailer $mo
	 * @param SocketClient $client
	 * @param Address|null $o
	 * @return int
	 * @throws \Exception
	 */
	public function session(Mailer $mo,SocketClient $client,Address $o=NULL): int
	{
		if ($o->exists)
			Log::withContext(['ftn'=>$o->ftn]);

		// This sessions options
		$this->options = 0;
		$this->session = 0;
		$this->capability = [];

		// Our files that we are sending/receive
		$this->send = new Send;
		$this->recv = new Receive;

		if ($o) {
			// The node we are communicating with
			$this->node = new Node;

			$this->originate = $o->exists;

			// If we are connecting to a node
			if ($o->exists) {
				Log::debug(sprintf('%s:+ Originating a connection to [%s]',self::LOGKEY,$o->ftn));
				$this->node->originate($o);

			} else {
				$this->optionSet(self::O_INB);
			}
		}

		// We are an IP node
		$this->optionSet(self::O_TCP);
		$this->client = $client;
		// @todo This appears to be a bug in laravel? Need to call app()->isDownForMaintenance() twice?
		app()->isDownForMaintenance();
		$this->down = app()->isDownForMaintenance();

		switch ($mo->name) {
			case 'EMSI':
				Log::debug(sprintf('%s:- Starting EMSI',self::LOGKEY));

				$rc = $this->protocol_init();
				if ($rc < 0) {
					Log::error(sprintf('%s:! Unable to start EMSI [%d]',self::LOGKEY,$rc));

					return self::S_FAILURE;
				}

				$rc = $this->protocol_session($this->originate);

				break;

			case 'BINKP':
				Log::debug(sprintf('%s:- Starting BINKP',self::LOGKEY));

				$rc = $this->protocol_session($this->originate);

				break;

			default:
				Log::error(sprintf('%s:! Unsupported session type [%d]',self::LOGKEY,$mo->id));

				return self::S_FAILURE;
		}

		// @todo Unlock outbounds

		// @todo These flags determine when we connect to the remote.
		// If the remote indicated that they dont support file requests (NRQ) or temporarily hold them (HRQ)
		if (($this->node->optionGet(self::O_NRQ) && (! $this->setup->optionGet(EMSI::F_IGNORE_NRQ,'emsi_options'))) || $this->node->optionGet(self::O_HRQ))
			$rc |= self::S_HOLDR;

		if ($this->optionGet(self::O_HXT))
			$rc |= self::S_HOLDX;

		if ($this->optionGet(self::O_HAT))
			$rc |= self::S_HOLDA;

		Log::info(sprintf('%s:= Total: %s - %d:%02d:%02d online, (%d) %lu%s sent, (%d) %lu%s received - %s',
			self::LOGKEY,
			$this->node->address ? $this->node->address->ftn : 'Unknown',
			$this->node->session_time/3600,
			$this->node->session_time%3600/60,
			$this->node->session_time%60,
			$this->send->total_sent,$this->send->total_sent_bytes,'b',
			$this->recv->total_recv,$this->recv->total_recv_bytes,'b',
			(($rc & self::S_MASK) === self::S_OK) ? 'Successful' : 'Failed',
		));

		// Add unknown FTNs to the DB
		$so = ($this->node->aka_remote_authed->count())
			? $this->node->aka_remote_authed->first()->system
			: System::createUnknownSystem();

		if ($so && $so->exists) {
			foreach ($this->node->aka_other as $aka)
				// @todo For disabled zones, we shouldnt refuse to record the address
				if ((! Address::findFTN($aka)) && ($oo=Address::createFTN($aka,$so))) {
					$oo->validated = TRUE;
					$oo->save();
				}

			// Log session in DB
			$slo = new SystemLog;
			$slo->items_sent = $this->send->total_sent;
			$slo->items_sent_size = $this->send->total_sent_bytes;
			$slo->items_recv = $this->recv->total_recv;
			$slo->items_recv_size = $this->recv->total_recv_bytes;
			$slo->mailer_id = $mo->id;
			$slo->sessiontime = $this->node->session_time;
			$slo->result = ($rc & self::S_MASK);
			$slo->originate = $this->originate;

			$so->logs()->save($slo);

			// If we are autohold, then remove that
			if ($so->autohold) {
				$so->autohold = FALSE;
				$so->save();
			}
		}

		// @todo Optional after session execution event
		// if ($this->node->start_time && $this->setup->cfg('CFG_AFTERSESSION')) {}

		// @todo Optional after session includes mail event
		// if ($this->node->start_time && $this->setup->cfg('CFG_AFTERMAIL')) {}

		return $rc;
	}

	/* SE_* flags determine our session processing status, at any point in time */

	/**
	 * Clear a session bit (SE_*)
	 *
	 * @param int $key
	 */
	protected function sessionClear(int $key): void
	{
		$this->session &= ~$key;
	}

	/**
	 * Get a session bit (SE_*)
	 *
	 * @param int $key
	 * @return bool
	 */
	protected function sessionGet(int $key): bool
	{
		return ($this->session & $key);
	}

	/**
	 * Set a session bit (SE_*)
	 *
	 * @param int $key
	 */
	protected function sessionSet(int $key): void
	{
		$this->session |= $key;
	}
}