<?php

namespace App\Classes\FTN;

use Carbon\Carbon;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use League\Flysystem\UnableToReadFile;

use App\Classes\FTN as FTNBase;
use App\Exceptions\{InvalidCRCException,
	InvalidPasswordException,
	NodeNotSubscribedException,
	NoWriteSecurityException};
use App\Exceptions\TIC\{NoFileAreaException,NotToMeException,SizeMismatchException};
use App\Models\{Address,File,Filearea,Setup};

/**
 * Class TIC
 * This class handles the TIC files that accompany file transfers
 *
 * @package App\Classes
 */
class Tic extends FTNBase
{
	private const LOGKEY = 'FT-';

	// Single value kludge items and whether they are required
	// http://ftsc.org/docs/fts-5006.001
	private array $_kludge = [
		'AREA' => TRUE,
		'areadesc' => FALSE,
		'ORIGIN' => TRUE,
		'FROM' => TRUE,
		'to' => FALSE,
		'FILE' => TRUE,			// 8.3 DOS format
		'lfile' => FALSE,		// alias fullname
		'fullname' => FALSE,
		'size' => FALSE,
		'date' => FALSE,		// File creation date
		'desc' => FALSE,		// One line description of file
		'ldesc' => FALSE,		// Can have multiple
		'created' => FALSE,
		'magic' => FALSE,
		'replaces' => FALSE,	// ? and * are wildcards, as per DOS
		'CRC' => TRUE,			// crc-32
		'PATH' => TRUE,			// can have multiple: [FTN] [unix timestamp] [datetime human readable] [signature]
		'SEENBY' => TRUE,
		'pw' => FALSE,			// Password
	];

	private File $file;
	private Address $to;					// Should be me

	public function __construct(File $file=NULL)
	{
		$this->file = $file ?: new File;

		$this->file->kludges = collect();
		$this->file->rogue_seenby = collect();
		$this->file->set_path = collect();
		$this->file->set_seenby = collect();
	}

	public function __get(string $key): mixed
	{
		switch ($key) {
			case 'file':
				return $this->{$key};

			case 'name':
				return $this->file->name;

			default:
				return parent::__get($key);
		}
	}

	/**
	 * Generate the TIC file
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function __toString(): string
	{
		if (! $this->to)
			throw new \Exception('No to address defined');

		$sysaddress = our_address($this->to);

		$result = collect();

		// Origin is the first address in our path
		$result->put('ORIGIN',$this->file->path->first()->ftn3d);
		$result->put('FROM',$sysaddress->ftn3d);
		$result->put('TO',$this->to->ftn3d);
		$result->put('FILE',$this->file->name);
		$result->put('SIZE',$this->file->size);
		if ($this->file->description)
			$result->put('DESC',$this->file->description);
		if ($this->file->replaces)
			$result->put('REPLACES',$this->file->replaces);
		$result->put('AREA',$this->file->filearea->name);
		$result->put('AREADESC',$this->file->filearea->description);
		if ($x=strtoupper($this->to->session('ticpass')))
			$result->put('PW',$x);
		$result->put('CRC',sprintf("%X",$this->file->crc));

		$out = '';
		foreach ($result as $key=>$value)
			$out .= sprintf("%s %s\r\n",$key,$value);

		foreach ($this->file->path as $o)
			$out .= sprintf("PATH %s %s %s\r\n",$o->ftn3d,$o->pivot->datetime,$o->pivot->extra);

		// Add ourself to the path:
		$out .= sprintf("PATH %s %s\r\n",$sysaddress->ftn3d,Carbon::now());

		foreach ($this->file->seenby as $o)
			$out .= sprintf("SEENBY %s\r\n",$o->ftn3d);

		$out .= sprintf("SEENBY %s\r\n",$sysaddress->ftn3d);

		return $out;
	}

	/**
	 * Does this TIC file bring us a nodelist
	 *
	 * @return bool
	 */
	public function isNodelist(): bool
	{
		Log::critical(sprintf('%s:D fo_nodelist_file_area [%d], fo_filearea_domain_filearea_id [%d], regex [%s] name [%s]',
			self::LOGKEY,
			$this->file->nodelist_filearea_id,
			$this->file->filearea->domain->filearea_id,
			str_replace(['.','?'],['\.','[0-9]'],'#^'.$this->file->filearea->domain->nodelist_filename.'$#i'),
			$this->file->name,
		));
		return (($this->file->nodelist_filearea_id === $this->file->filearea->domain->filearea_id)
			&& (preg_match(str_replace(['.','?'],['\.','[0-9]'],'#^'.$this->file->filearea->domain->nodelist_filename.'$#i'),$this->file->name)));
	}

	/**
	 * Load a TIC file from an existing filename
	 *
	 * @param string $filename Relative to filesystem
	 * @return File
	 * @throws FileNotFoundException
	 * @throws InvalidCRCException
	 * @throws InvalidPasswordException
	 * @throws NoFileAreaException
	 * @throws NoWriteSecurityException
	 * @throws NodeNotSubscribedException
	 * @throws NotToMeException
	 * @throws SizeMismatchException
	 */
	public function load(string $filename): File
	{
		Log::info(sprintf('%s:+ Processing TIC file [%s]',self::LOGKEY,$filename));
		$fs = Storage::disk(config('fido.local_disk'));
		$rel_path_name = sprintf('%s/%s',config('fido.dir'),$filename);

		if (! $fs->exists($rel_path_name))
			throw new FileNotFoundException(sprintf('File [%s] doesnt exist',$fs->path($rel_path_name)));

		if ((! is_readable($fs->path($rel_path_name))) || ! ($f = $fs->readStream($rel_path_name)))
			throw new UnableToReadFile(sprintf('File [%s] is not readable',$fs->path($rel_path_name)));

		/*
		 * Filenames are in the format X-Y-N.tic
		 * Where:
		 * - X is the nodes address that sent us the file
		 * - Y is the mtime of the TIC file from the sender
		 * - N is the sender's filename
		 */

		$aid = NULL;
		$mtime = NULL;
		$this->file->recv_tic = preg_replace('/\.[Tt][Ii][Cc]$/','',$filename);

		$m = [];
		if (preg_match(sprintf('/^%s\.[Tt][Ii][Cc]$/',Packet::regex),$filename,$m)) {
			$aid = $m[1];
			$mtime = $m[2];
			$this->file->recv_tic = $m[3];
		}

		$ldesc = '';

		while (! feof($f)) {
			$line = chop(fgets($f));
			$m = [];

			if (! $line)
				continue;

			preg_match('/([a-zA-Z]+)\ ?(.*)?/',$line,$m);

			if (in_array(strtolower(Arr::get($m,1,'-')),$this->_kludge)) {
				switch ($k=strtolower($m[1])) {
					case 'area':
						try {
							if ($fo=Filearea::where('name',strtoupper($m[2]))->firstOrFail())
								$this->file->filearea_id = $fo->id;

						} catch (ModelNotFoundException $e) {
							// Rethrow this as No File Area
							throw new NoFileAreaException($e->getMessage());
						}

						break;

					case 'from':
						if (($ao=Address::findFTN($m[2])) && ((! $aid) || ($ao->zone->domain_id === Address::findOrFail(hexdec($aid))->zone->domain_id)))
							$this->file->fftn_id = $ao->id;
						else
							throw new ModelNotFoundException(sprintf('FTN Address [%s] not found or sender mismatch',$m[2]));

						break;

					// The origin should be the first address in the path
					case 'origin':
					// Ignore
					case 'areadesc':
					case 'created':
						break;

					// This should be one of my addresses
					case 'to':
						$ftns = our_address()->pluck('ftn3d');

						if (! ($ftns->contains($m[2])))
							throw new NotToMeException(sprintf('FTN Address [%s] not found or not one of my addresses',$m[2]));

						break;

					case 'file':
						$this->file->name = $m[2];

						break;

					case 'pw':
						$pw = $m[2];

						break;

					case 'lfile':
					case 'fullname':
						$this->file->lname = $m[2];

						break;

					case 'desc':
					case 'magic':
					case 'replaces':
					case 'size':
						$this->file->{$k} = $m[2];

						break;

					case 'date':
						$this->file->datetime = Carbon::createFromTimestamp($m[2]);

						break;

					case 'ldesc':
						$ldesc .= ($ldesc ? "\r" : '').$m[2];

						break;

					case 'crc':
						$this->file->{$k} = hexdec($m[2]);

						break;

					case 'path':
						$this->file->set_path->push($m[2]);

						break;

					case 'seenby':
						$this->file->set_seenby->push($m[2]);

						break;
				}

			} else {
				$this->file->kludges->push($line);
			}
		}

		fclose($f);

		if ($ldesc)
			$this->file->ldesc = $ldesc;

		// @todo Check that origin is the first address in the path
		// @todo Make sure origin/from are in seenby
		// @todo Make sure origin/from are in the path

		/*
		 * Find our file and check the CRC
		 * If there is more than 1 file, select files that within 24hrs of the TIC file.
		 * If no files report file not found
		 * If there is more than 1 check each CRC to match the right one.
		 * If none match report, CRC error
		 */
		$found = FALSE;
		$crcOK = FALSE;
		foreach ($fs->files(config('fido.dir')) as $file) {
			if (abs($x=$fs->lastModified($rel_path_name)-$fs->lastModified($file)) > 86400) {
				Log::debug(sprintf('%s:/ Ignoring [%s] its mtime is outside of our scope [%d]',self::LOGKEY,$file,$x));

				continue;
			}

			// Our file should have the same prefix as the TIC file
			if (preg_match('#/'.($aid ? $aid.'-' : '').'.*'.$this->file->name.'$#',$file)) {
				$found = TRUE;

				if (sprintf('%08x',$this->file->crc) === ($y=$fs->checksum($file,['checksum_algo'=>'crc32b']))) {
					$crcOK = TRUE;

					break;
				}
			}
		}

		if (($found) && (! $crcOK))
			throw new InvalidCRCException(sprintf('TIC file CRC [%08x] doesnt match file [%s] (%s)',$this->file->crc,$fs->path($rel_path_name),$y));
		elseif (! $found)
			throw new FileNotFoundException(sprintf('File not found? [%s...%s] in [%s]',$aid,$this->file->name,$fs->path($rel_path_name)));

		// @todo Add notifications back to the system if the replaces line doesnt match
		if ($this->file->replaces && (! preg_match('/^'.$this->file->replaces.'$/',$this->file->name))) {
			Log::alert(sprintf('%s:! Regex [%s] doesnt match file name [%s]',self::LOGKEY,$this->file->replaces,$this->file->name));

			$this->file->replaces = NULL;
		}

		// @todo Add notification back to the system if no replaces line and the file already exists

		// Validate Size
		if ($this->file->size !== ($y=$fs->size($file)))
			throw new SizeMismatchException(sprintf('TIC file size [%d] doesnt match file [%s] (%d)',$this->file->size,$fs->path($rel_path_name),$y));

		// Validate Password
		if (strtoupper($pw) !== ($y=strtoupper($this->file->fftn->session('ticpass'))))
			throw new InvalidPasswordException(sprintf('TIC file PASSWORD [%s] doesnt match system [%s] (%s)',$pw,$this->file->fftn->ftn,$y));

		// Validate Sender is linked
		if ($this->file->fftn->fileareas->search(function($item) { return $item->id === $this->file->filearea_id; }) === FALSE)
			throw new NodeNotSubscribedException(sprintf('Node [%s] is not subscribed to [%s]',$this->file->fftn->ftn,$this->file->filearea->name));

		// Validate sender is permitted to write
		// @todo Send a notification
		if (! $this->file->filearea->can_write($this->file->fftn->security))
			throw new NoWriteSecurityException(sprintf('Node [%s] doesnt have enough security to write to [%s] (%d)',$this->file->fftn->ftn,$this->file->filearea->name,$this->file->fftn->security));

		// If the file create time is blank, we'll take the files
		if (! $this->file->datetime)
			$this->file->datetime = Carbon::createFromTimestamp($fs->lastModified($file));

		$this->file->src_file = $file;
		$this->file->recv_tic = $filename;

		return $this->file;
	}

	public function save(): bool
	{
		return $this->file->save();
	}

	public function to(Address $ao): self
	{
		$this->to = $ao;

		return $this;
	}
}