<?php

namespace App\Classes\FTN;

use Carbon\Carbon;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use League\Flysystem\UnableToWriteFile;

use App\Classes\FTN as FTNBase;
use App\Models\{Address,File,Filearea,Setup};
use App\Traits\EncodeUTF8;

/**
 * Class TIC
 * Used create the structure of TIC files
 *
 * @package App\Classes
 */
class Tic extends FTNBase
{
	use EncodeUTF8;

	private const LOGKEY = 'FT-';

	private const cast_utf8 = [
	];

	// 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 $fo;
	private Filearea $area;
	private Collection $values;

	private Address $origin;				// Should be first address in Path
	private Address $from;					// Should be last address in Path
	private Address $to;					// Should be me

	public function __construct()
	{
		$this->fo = new File;

		$this->fo->kludges = collect();
		$this->fo->set_path = collect();
		$this->fo->set_seenby = collect();
		$this->fo->rogue_path = collect();
		$this->fo->rogue_seenby = collect();

		$this->values = collect();
	}

	/**
	 * Does this TIC file bring us a nodelist
	 *
	 * @return bool
	 */
	public function isNodelist(): bool
	{
		return (($this->fo->nodelist_filearea_id === $this->fo->filearea->domain->filearea_id)
			&& (preg_match(str_replace(['.','?'],['\.','.'],'#^'.$this->fo->filearea->domain->nodelist_filename.'$#i'),$this->fo->name)));
	}

	/**
	 * Generate a TIC file for an address
	 *
	 * @param Address $ao
	 * @param File $fo
	 * @return string
	 */
	public function generate(Address $ao,File $fo): string
	{
		$sysaddress = Setup::findOrFail(config('app.id'))->system->match($ao->zone)->first();

		$result = collect();

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

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

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

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

		return $out;
	}

	/**
	 * Load a TIC file from an existing filename
	 *
	 * @param string $filename
	 * @return void
	 * @throws FileNotFoundException
	 */
	public function load(string $filename): void
	{
		Log::info(sprintf('%s:Processing TIC file [%s]',self::LOGKEY,$filename));

		list($hex,$name) = explode('-',$filename);
		$hex = basename($hex);

		if (! file_exists($filename))
			throw new FileNotFoundException(sprintf('%s:File [%s] doesnt exist',self::LOGKEY,realpath($filename)));

		if (! is_readable($filename))
			throw new UnableToWriteFile(sprintf('%s:File [%s] is not readable',self::LOGKEY,realpath($filename)));

		$f = fopen($filename,'rb');
		if (! $f) {
			Log::error(sprintf('%s:! Unable to open file [%s] for writing',self::LOGKEY,$filename));
			return;
		}

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

			if (! $line)
				continue;

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

			if (in_array(strtolower($matches[1]),$this->_kludge)) {
				switch ($k=strtolower($matches[1])) {
					case 'area':
						$this->{$k} = Filearea::singleOrNew(['name'=>strtoupper($matches[2])]);
						break;

					case 'origin':
					case 'from':
					case 'to':
						$this->{$k} = Address::findFTN($matches[2]);

						// @todo If $this->{$k} is null, we have discovered the system and it should be created
						break;

					case 'file':
						if (! Storage::disk('local')->exists($x=sprintf('%s/%s-%s',config('app.fido'),$hex,$matches[2])))
							throw new FileNotFoundException(sprintf('File not found? [%s]',$x));

						$this->fo->name = $matches[2];
						$this->fo->fullname = $x;
						break;

					case 'areadesc':
						$areadesc = $matches[2];
						break;

					case 'created':
						// ignored
						break;

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

					case 'lfile':
						$this->fo->lname = $matches[2];

					case 'desc':
					case 'magic':
					case 'replaces':
					case 'size':
						$this->fo->{$k} = $matches[2];
						break;

					case 'fullname':
						$this->fo->lfile = $matches[2];
						break;

					case 'date':
						$this->fo->datetime = Carbon::create($matches[2]);
						break;

					case 'ldesc':
						$this->fo->{$k} .= $matches[2];
						break;

					case 'crc':
						$this->fo->{$k} = hexdec($matches[2]);
						break;

					case 'path':
						$x = [];
						preg_match(sprintf('#^[Pp]ath (%s)\ ?([0-9]+)\ ?(.*)$#',Address::ftn_regex),$line,$x);
						$ao = Address::findFTN($x[1]);

						if (! $ao) {
							$this->fo->rogue_path->push($matches[2]);
						} else {
							$this->fo->set_path->push(['address'=>$ao,'datetime'=>Carbon::createFromTimestamp($x[8]),'extra'=>$x[9]]);
						}

						break;

					case 'seenby':
						$ao = Address::findFTN($matches[2]);

						if (! $ao) {
							$this->fo->rogue_seenby->push($matches[2]);
						} else {
							$this->fo->set_seenby->push($ao->id);
						}

						break;
				}

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

		fclose($f);

		$f = fopen($x=Storage::disk('local')->path($this->fo->fullname),'rb');
		$stat = fstat($f);
		fclose($f);

		// Validate Size
		if ($this->fo->size !== ($y=$stat['size']))
			throw new \Exception(sprintf('TIC file size [%d] doesnt match file [%s] (%d)',$this->fo->size,$this->fo->fullname,$y));

		// Validate CRC
		if (sprintf('%08x',$this->fo->crc) !== ($y=hash_file('crc32b',$x)))
			throw new \Exception(sprintf('TIC file CRC [%08x] doesnt match file [%s] (%s)',$this->fo->crc,$this->fo->fullname,$y));

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

		// Validate Sender is linked (and permitted to send)
		if ($this->from->fileareas->search(function($item) { return $item->id === $this->area->id; }) === FALSE)
			throw new \Exception(sprintf('Node [%s] is not subscribed to [%s]',$this->from->ftn,$this->area->name));

		// If the filearea is to be autocreated, create it
		if (! $this->area->exists) {
			$this->area->description = $areadesc;
			$this->area->active = TRUE;
			$this->area->public = FALSE;
			$this->area->notes = 'Autocreated';
			$this->area->domain_id = $this->from->zone->domain_id;
			$this->area->save();
		}

		$this->fo->filearea_id = $this->area->id;
		$this->fo->fftn_id = $this->origin->id;

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

		$this->fo->save();
	}
}