<?php

namespace App\Http\Controllers;

use Carbon\Carbon;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\QueryException;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\ViewErrorBag;

use App\Classes\FTN\Message;
use App\Http\Requests\{AddressAdd,AddressMerge,AreafixRequest,SystemEchoareaRequest,SystemRegisterRequest,SystemSessionRequest};
use App\Jobs\AddressPoll;
use App\Models\{Address,Echoarea,Echomail,Filearea,Netmail,Setup,System,Zone};
use App\Notifications\Netmails\AddressLink;

class SystemController extends Controller
{
	private const LOGKEY = 'CSC';

	/**
	 * Add or edit a node
	 */
	public function add_edit(SystemRegisterRequest $request, System $o)
	{
		if ($request->validated()) {
			foreach (['name','location','phone','address','port','active','method','pkt_msgs','pkt_type'] as $key)
				$o->{$key} = $request->validated($key);

			// Sometimes items
			foreach (['sysop','hold','notes','zt_id','heartbeat'] as $key)
				if ($request->has($key))
					$o->{$key} = $request->validated($key);

			switch ($request->validated('pollmode')) {
				case 1: $o->pollmode = FALSE; break;
				case 2: $o->pollmode = TRUE; break;
				default: $o->pollmode = NULL;
			}

			$o->active = (! is_null($x=$request->validated('active'))) && $x;
			$o->autohold = FALSE;
			$o->save();

			$mailers = collect($request->post('mailer_details'))
				->filter(function($item) { return $item['port']; })
				->transform(function($item) { $item['active'] = Arr::get($item,'active',FALSE); return $item; });

			$o->mailers()->sync($mailers);
			if ($request->validated('users')) {
				if (array_filter($request->validated('users'),function($item) { return $item; }))
					$o->users()->sync($request->validated('users'));
				else
					$o->users()->detach();
			}

			return redirect()
				->to('system/addedit/'.$o->id)
				->with('saved',TRUE);
		}

		$o->load(['addresses.zone.domain','addresses.system','sessions.domain','sessions.systems']);

		return view('system.addedit')
				->with('action',$o->exists ? 'update_nn' : 'create')
				->with('o',$o);
	}

	/**
	 * Add an address to a system
	 *
	 * @param AddressAdd $request
	 * @param System $o
	 * @return \Illuminate\Http\RedirectResponse
	 * @throws \Illuminate\Auth\Access\AuthorizationException
	 */
	public function address_add(AddressAdd $request,System $o)
	{
		$oo = Address::findOrNew($request->validated('submit'));
		$oo->zone_id = $request->validated('zone_id');
		$oo->security = $request->validated('security');
		$oo->active = TRUE;

		switch ($request->validated('action')) {
			case 'region':
				$oo->region_id = $request->validated('region_id_new');
				$oo->host_id = $request->validated('region_id_new');
				$oo->node_id = 0;
				$oo->point_id = 0;

				break;

			case 'host':
				$oo->region_id = $request->validated('region_id');
				$oo->host_id = $request->validated('host_id_new');
				$oo->node_id = 0;
				$oo->point_id = 0;

				break;

			case 'update':
			case 'node':
				$oo->region_id = $request->validated('region_id');
				$oo->host_id = $request->validated('host_id');
				$oo->node_id = $request->validated('node_id');
				$oo->point_id = $request->validated('point_id');
				$oo->hub_id = $request->validated('hub_id') ? $request->validated('hub_id') : NULL;

				break;

			default:
				return redirect()->back()->withErrors(['action'=>'Unknown action: '.$request->action]);
		}

		$o->addresses()->save($oo);

		return redirect()->to(sprintf('system/addedit/%d',$o->id));
	}

	/**
	 * Delete address assigned to a host
	 *
	 * @param Address $o
	 * @return \Illuminate\Http\RedirectResponse
	 * @throws \Illuminate\Auth\Access\AuthorizationException
	 */
	public function address_del(Address $o)
	{
		// @todo This should be admin of the zone
		$this->authorize('admin',$o);
		session()->flash('accordion','address');

		$sid = $o->system_id;
		$o->active = FALSE;
		$o->save();
		$o->delete();

		return redirect()->to(sprintf('system/addedit/%d',$sid));
	}

	/**
	 * Demote an address NC -> node
	 *
	 * @param Address $o
	 * @return \Illuminate\Http\RedirectResponse
	 * @throws \Illuminate\Auth\Access\AuthorizationException
	 */
	public function address_dem(Address $o)
	{
		// @todo This should be admin of the zone
		$this->authorize('admin',$o);
		session()->flash('accordion','address');

		// Make sure that no other system has this address active.
		if ($o->role_id === Address::NODE_NN)
			return redirect()->back()->withErrors(['address'=>sprintf('%s cannot be demoted any more',$o->ftn3D)]);

		$off = $o->role_id;
		$o->role &= ~$off;
		$o->role |= ($off << 1);

		$o->save();

		return redirect()->to(sprintf('system/addedit/%d',$o->system_id));
	}

	public function address_merge(AddressMerge $request,int $id)
	{
		if ($request->validated()) {
			DB::beginTransaction();

			// Find all the echomails that have both seenby's
			$x = Echomail::select(['id'])
				->join('echomail_seenby',['echomail_seenby.echomail_id'=>'echomails.id'])
				->whereIn('address_id',[$request->src])
				->distinct()
				->with(['seenby'=>function($query) use ($request) {
					return $query
						->select('id')
						->whereIn('address_id',[$request->src,$request->dst]);
				}])
				->get()
				->filter(function($item) use ($request) {
					return $item->seenby->contains($request->dst) && $item->seenby->contains($request->src);
				});

			// If the Echomail has both source and dest, delete the src
			if ($x->count())
				DB::table('echomail_seenby')
					->where('address_id',$request->src)
					->whereIn('echomail_id',$x->pluck('id'))
					->delete();

			// Find all echomail seenbys
			$x = DB::update('update echomail_seenby set address_id=? where address_id=?',[$request->dst,$request->src]);

			// Find all echomail paths
			$x = DB::update('update echomail_path set address_id=? where address_id=?',[$request->dst,$request->src]);

			// Find all echomails
			$x = DB::update('update echomails set fftn_id=? where fftn_id=?',[$request->dst,$request->src]);

			// Find all netmails
			$x = DB::update('update netmails set fftn_id=? where fftn_id=?',[$request->dst,$request->src]);

			// Find all netmails
			$x = DB::update('update netmails set tftn_id=? where tftn_id=?',[$request->dst,$request->src]);

			// Find all nodelist
			$x = DB::update('update address_nodelist set address_id=? where address_id=?',[$request->dst,$request->src]);

			// Find all file seenbys
			$x = DB::update('update file_seenby set address_id=? where address_id=?',[$request->dst,$request->src]);

			// Find all files
			$x = DB::update('update files set fftn_id=? where fftn_id=?',[$request->dst,$request->src]);

			$src = Address::withTrashed()->findOrFail($request->src);

			// Resubscribe echoareas
			try {
				$x = DB::update('update address_echoarea set address_id=? where address_id=?',[$request->dst,$request->src]);

			} catch (QueryException $e) {
				DB::rollback();

				return back()->withInput()->withErrors('error',sprintf('You may need to remove %s:%s (%d) from echoareas',$src->ftn,$src->system->name,$src->id));
			}

			// Resubscribe fileareas
			try {
				$x = DB::update('update address_filearea set address_id=? where address_id=?',[$request->dst,$request->src]);

			} catch (QueryException $e) {
				DB::rollback();

				return back()->withInput()->withErrors('error',sprintf('You may need to remove %s:%s (%d) from fileareas',$src->ftn,$src->system->name,$src->id));
			}

			if ($src->forceDelete()) {
				DB::commit();
				return redirect()->to('address/merge/'.$request->dst);

			} else {
				return back()->withInput()->withErrors('error',sprintf('Address [%s] didnt delete?',$src->ftn));
				DB::rollBack();
			}
		}
		$o = Address::withTrashed()
			->findOrFail($id);

		$oo = Address::withTrashed()
			->where('zone_id',$o->zone_id)
			->where('host_id',$o->host_id)
			->where('node_id',$o->node_id)
			->where('point_id',$o->point_id)
			->get();

		if ($o->zone->domain->flatten)
			$oo = $oo->merge(Address::withTrashed()
				->whereIn('zone_id',$o->zone->domain->zones->pluck('id'))
				->where('host_id',$o->host_id)
				->where('node_id',$o->node_id)
				->where('point_id',$o->point_id)
				->get()
			);

		return view('system/address-merge')
			->with('o',$o)
			->with('oo',$oo);
	}

	/**
	 * Move address to another system
	 *
	 * @param Request $request
	 * @param System $so
	 * @param Address $o
	 * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
	 * @throws \Illuminate\Auth\Access\AuthorizationException
	 */
	public function address_mov(Request $request,System $so,Address $o)
	{
		session()->flash('accordion','address');

		// Quick check that this address belongs to this system
		if ($so->addresses->search(function($item) use ($o) { return $item->id === $o->id; }) === FALSE)
			abort(404);

		if ($request->post()) {
			$this->authorize('admin',$o);

			$validated = $request->validate([
				'system_id' => 'required|exists:systems,id',
				'remove' => 'nullable|boolean',
				'remsess' => 'nullable|boolean|exclude_if:remove,1',
			]);

			$o->system_id = $validated['system_id'];
			$o->save();

			if (Arr::get($validated,'remove')) {
				$so->sessions()->detach($o->zone);
				$so->logs()->delete();
				$so->mailers()->detach();
				$so->users()->detach();
				$so->delete();

			} elseif (Arr::get($validated,'remsess')) {
				$so->sessions()->detach($o->zone);
			}

			return redirect()->to('system/addedit/'.$validated['system_id']);
		}

		return view('system.moveaddr')
			->with('o',$o);
	}

	/**
	 * Promote an address node -> NC
	 *
	 * @param Address $o
	 * @return \Illuminate\Http\RedirectResponse
	 * @throws \Illuminate\Auth\Access\AuthorizationException
	 */
	public function address_pro(Address $o)
	{
		// @todo This should be admin of the zone
		$this->authorize('admin',$o);
		session()->flash('accordion','address');

		// Make sure that no other system has this address active.
		if ($o->role_id === Address::NODE_NC)
			return redirect()->back()->withErrors(['address'=>sprintf('%s cannot be promoted any more',$o->ftn3D)]);

		$off = $o->role_id;
		$o->role &= ~$off;
		$o->role |= ($off >> 1);

		$o->save();

		return redirect()->to(sprintf('system/addedit/%d',$o->system_id));
	}

	/**
	 * Recover a deleted address
	 *
	 * @param int $id
	 * @return \Illuminate\Http\RedirectResponse
	 * @throws \Illuminate\Auth\Access\AuthorizationException
	 */
	public function address_pur(int $id)
	{
		$o = Address::onlyTrashed()->findOrFail($id);

		// @todo This should be admin of the zone
		$this->authorize('admin',$o);
		session()->flash('accordion','address');

		$o->forceDelete();

		return redirect()->to(sprintf('system/addedit/%d',$o->system_id));
	}

	/**
	 * Recover a deleted address
	 *
	 * @param int $id
	 * @return \Illuminate\Http\RedirectResponse
	 * @throws \Illuminate\Auth\Access\AuthorizationException
	 */
	public function address_rec(int $id)
	{
		$o = Address::onlyTrashed()->findOrFail($id);

		// @todo This should be admin of the zone
		$this->authorize('admin',$o);
		session()->flash('accordion','address');

		$o->restore();

		return redirect()->to(sprintf('system/addedit/%d',$o->system_id));
	}

	/**
	 * Suspend address assigned to a host
	 *
	 * @param Address $o
	 * @return \Illuminate\Http\RedirectResponse
	 * @throws \Illuminate\Auth\Access\AuthorizationException
	 */
	public function address_sus(Address $o)
	{
		// @todo This should be admin of the zone
		$this->authorize('admin',$o);
		session()->flash('accordion','address');

		// Make sure that no other system has this address active.
		if (! $o->active && ($x=Address::where([
				'zone_id'=>$o->zone_id,
				'host_id'=>$o->host_id,
				'node_id'=>$o->node_id,
				'point_id'=>$o->point_id,
				'active'=>TRUE,
			])->single())) {

			return redirect()->back()->withErrors(['address'=>sprintf('%s is already active on system [<a href="%s">%s</a>]',$o->ftn,url('system/addedit',$x->system_id),$x->system->name)]);
		}

		$o->active = (! $o->active);
		$o->save();

		return redirect()->to(sprintf('system/addedit/%d',$o->system_id));
	}

	public function api_address(Request $request,System $o): Collection
	{
		return Address::select(['addresses.id','addresses.zone_id','region_id','host_id','node_id','point_id'])
			->leftjoin('zones',['zones.id'=>'addresses.zone_id'])
			->where('addresses.system_id',$o->id)
			->where('zones.domain_id',$request->domain_id)
			->withTrashed()
			->FTNorder()
			->get()
			->map(function($item) { return ['id'=>(string)$item->id,'value'=>$item->ftn4d]; });
	}

	public function api_address_get(Address $o)
	{
		return $o;
	}

	/**
	 * Identify all the addresses from systems that are not owned by a user
	 *
	 * @param Request $request
	 * @return Collection
	 */
	public function api_address_orphan(Request $request): Collection
	{
		$result = collect();

		list($zone_id,$host_id,$node_id,$point_id,$domain) = sscanf($request->query('term'),'%d:%d/%d.%d@%s');

		# Look for Systems
		foreach (Address::select(['addresses.id','systems.name',DB::raw('systems.id AS system_id'),'zones.zone_id','region_id','host_id','node_id','point_id','addresses.zone_id'])
			 ->join('zones',['zones.id'=>'addresses.zone_id'])
			 ->rightjoin('systems',['systems.id'=>'addresses.system_id'])
			 ->when($zone_id || $host_id || $node_id,function($query) use ($zone_id,$host_id,$node_id) {
				 return $query
					 ->when($zone_id,function($q,$zone_id) { return $q->where('zones.zone_id',$zone_id); })
					 ->where(function($q) use ($host_id) {
						 return $q
							 ->when($host_id,function($q,$host_id) { return $q->where('region_id',$host_id); })
							 ->when($host_id,function($q,$host_id) { return $q->orWhere('host_id',$host_id); });
					 })
					 ->when($node_id,function($q,$node_id) { return $q->where('node_id',$node_id); });
			 })
			 ->orWhere('systems.name','ilike','%'.$request->query('term').'%')
			 ->orderBy('systems.name')
			 ->get() as $o)
		{
			$result->push(['id'=>$o->id,'name'=>sprintf('%s (%s)',$o->ftn3d,$o->name),'category'=>'Systems']);
		}

		return $result;
	}

	public function api_address_validated_toggle(Request $request,string $state): array
	{
		$o = Address::findOrFail($request->id);
		$o->validated = $state === 'off' ? FALSE : TRUE;
		$o->save();

		Log::debug(sprintf('%s:- Address Validated set to [%s]',self::LOGKEY,$o->autohold ? 'ON' : 'OFF'));

		return ['validated'=>$o->validated];
	}

	public function api_autohold_toggle(Request $request,string $state): array
	{
		$o = System::findOrFail($request->id);

		if ($request->user()->can('update_nn',$o)) {
			$o->autohold = !($state === 'off');
			$o->save();

			Log::debug(sprintf('%s:- Autohold set to [%s]',self::LOGKEY,$o->autohold ? 'ON' : 'OFF'));

		} else {
			abort(403);
		}

		return ['autohold'=>$o->autohold];
	}

	public function areafix(AreafixRequest $request,System $o,Zone $zo)
	{
		if ($request->post()) {
			$no = new Netmail;
			foreach ($request->safe() as $item => $value)
				$no->{$item} = $value;

			$no->from = auth::user()->name;
			$no->msg .= "\r";

			$no->datetime = Carbon::now();
			$no->tzoffset = $no->datetime->utcOffset();
			$no->flags = (Message::FLAG_LOCAL|Message::FLAG_PRIVATE|Message::FLAG_CRASH);
			$no->cost = 0;

			$no->set_tearline = sprintf('%s (%04X)',Setup::PRODUCT_NAME,Setup::PRODUCT_ID);
			$no->save();

			Log::info(sprintf('%s:= Areafix to [%s], scheduling a poll',self::LOGKEY,$no->tftn->ftn));
			AddressPoll::dispatch($no->tftn);

			return redirect()->back()->with('success','Areafix/Filefix sent');
		}

		return view('system.areafix')
			->with('zo',$zo)
			->with('ao',$o->match($zo)->first())
			->with('o',$o)
			->with('setup',Setup::findOrFail(config('app.id')));
	}

	/**
	 * Update the systems echoareas
	 *
	 * @param SystemEchoareaRequest $request
	 * @param System $o
	 * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
	 */
	public function echoareas(SystemEchoareaRequest $request,System $o)
	{
		$ao = $o->addresses->firstWhere('id',$request->address_id);

		if (($request->method() === 'POST') && $request->validated()) {
			$ao->echoareas()->syncWithPivotValues($request->validated('id',[]),['subscribed'=>Carbon::now()]);

			return redirect()->back()->with('success','Echoareas updated');
		}

		$eo = Echoarea::active()
			->where('domain_id',$ao->zone->domain_id)
			->where(function($query) use ($ao) {
				return $query
					->whereRaw(sprintf('(security&7) <= %d',$ao->security))			// write
					->orWhereRaw(sprintf('((security>>3)&7) <= %d',$ao->security));	// read
			})
			->orderBy('name')
			->get();

		return view('system.widget.echoarea')
			->with('o',$o)
			->with('ao',$ao)
			->with('echoareas',$eo);
	}

	/**
	 * Update the systems fileareas
	 *
	 * @param Request $request
	 * @param System $o
	 * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
	 */
	public function fileareas(Request $request,System $o)
	{
		$ao = $o->addresses->firstWhere('id',$request->address_id);

		if (($request->method() === 'POST') && $request->post()) {
			session()->flash('accordion','filearea');

			// Ensure we have session details for this address.
			if (! $ao->pass_session)
				return redirect()->back()->withErrors('System doesnt belong to this network');

			$ao->fileareas()->syncWithPivotValues($request->get('id',[]),['subscribed'=>Carbon::now()]);

			return redirect()->back()->with('success','Fileareas updated');
		}

		// @todo Allow a NC/RC/ZC to override
		$fo = Filearea::active()
			->where('domain_id',$ao->zone->domain_id)
			->where(function($query) use ($ao) {
				return $query
					->whereRaw(sprintf('(security&7) <= %d',$ao->security))			// write
					->orWhereRaw(sprintf('((security>>3)&7) <= %d',$ao->security));	// read
			})
			->orderBy('name')
			->get();

		return view('system.widget.filearea')
			->with('o',$o)
			->with('ao',$ao)
			->with('fileareas',$fo);
	}

	/**
	 * Register a system, or link to an existing system
	 */
	public function register(SystemRegisterRequest $request)
	{
		// Step 1, show the user a form to select an existing defined system
		if ($request->isMethod('GET'))
			return view('user.system.register');

		if ($request->action === 'register' && $request->name && is_numeric($request->name))
			return view('user.system.widget.register_confirm')
				->with('o',System::findOrFail($request->name));

		$o = System::findOrNew(is_numeric($request->system_id) ? $request->system_id : NULL);

		// If the system exists, and we are 'register', we'll start the address claim process
		if ($o->exists && $request->action === 'Link') {
			$validate = Setup::findOrFail(config('app.id'))->system->inMyZones($o->addresses);

			// If we have addresses, we'll trigger the routed netmail
			if ($validate->count()) {
				Notification::route('netmail',$x=$validate->first())->notify(new AddressLink(Auth::user()));
				AddressPoll::dispatch($x)->delay(15);
			}

			return view('user.system.widget.register_send')
				->with('validate',$validate)
				->with('o',$o);
		}

		// If the system doesnt exist, we'll create it
		if (! $o->exist) {
			$o->sysop = Auth::user()->name;

			foreach (['name','zt_id','location','phone','method','address','port'] as $item)
				if ($request->{$item})
					$o->{$item} = $request->{$item};

			$o->active = TRUE;
		}

		if ($request->post('submit')) {
			Auth::user()->systems()->save($o);

			// @todo if the system already exists and part of one of our networks, we'll need to send the registration email to confirm the address.
			// @todo mark the system (or addresses) as "pending" at this stage until it is confirmed
			return redirect()->to(url('system/addedit',$o->id));
		}

		// Re-flash our previously input data
		if ($request->old)
			session()->flashInput($request->old);

		return view('system.widget.system')
			->with('action',$request->action)
			->with('o',$o)
			->with('errors',new ViewErrorBag);
	}

	/**
	 * Add Session details
	 *
	 * @param SystemSessionRequest $request
	 * @param System $o
	 * @return \Illuminate\Http\RedirectResponse
	 * @throws \Illuminate\Auth\Access\AuthorizationException
	 */
	public function session_add(SystemSessionRequest $request,System $o)
	{
		$zo = Zone::findOrFail($request->zone_id);

		/*
		// @todo Disabling this, it needs improvement. If the new node is the ZC it becomes the default for the zone (and therefore remove all defaults from other addresses in the same zone), otherwise default should be false
		// If this session is for the ZC, it now becomes the default.
		if ($o->match($zo,Address::NODE_ZC)->count()) {
			SystemZone::where('default',TRUE)->update(['default'=>FALSE]);
			$validate['default'] = TRUE;
		}
		*/

		$o->sessions()->attach($zo,$request->validated());

		return redirect()->to(sprintf('system/addedit/%d',$o->id));
	}

	/**
	 * Delete address assigned to a host
	 *
	 * @param System $o
	 * @param Zone $zo
	 * @return RedirectResponse
	 * @throws AuthorizationException
	 */
	public function session_del(System $o,Zone $zo)
	{
		$this->authorize('update_nn',$o);
		session()->flash('accordion','session');

		$o->sessions()->detach($zo);

		return redirect()->to(sprintf('system/addedit/%d',$o->id));
	}
	// @todo Can this be consolidated with system_register()

	public function system_link(Request $request)
	{
		if (! $request->system_id)
			return redirect('user/system/register');

		$so = System::findOrFail($request->system_id);

		$ca = NULL;
		$la = NULL;
		foreach (our_address() as $ao) {
			if (($ca=$so->match($ao->zone))->count())
				break;
		}

		if ($ca->count() && $la=$ca->pop()) {
			Notification::route('netmail',$la)->notify(new AddressLink(Auth::user()));
			AddressPoll::dispatch($la)->delay(15);
		}

		return view('user.system.register_send')
			->with('la',$la)
			->with('o',$so);
	}

	public function view(System $o)
	{
		$o->load(['addresses']);

		return view('system.view')
			->with('o',$o);
	}
}