<?php

namespace App\Modules\Network\Controllers;

// Laravel
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;

use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

use Symfony\Component\Process\Process;

use Illuminate\Support\Facades\Notification;

// Creatia Default
use App\Models\Organisation;
use App\Models\Suborganisation;
use App\Helpers\Utility\RouteList;
use App\Helpers\Security;
use App\Models\SecurityEvent;
use App\Modules\Network\Controllers\NetworkController;
use App\Notifications\SlackNotification;
use App\Helpers\Utility\GoliathBgp;
use App\Modules\Network\Commands\PrefixCommunities;
// Mitigation
use App\Modules\Network\Models\Goliath\GoliathAppliance;
use App\Modules\Network\Models\Goliath\Chain;
use App\Modules\Network\Models\Goliath\ChainElement;
use App\Modules\Network\Models\Goliath\ChainElementAction;
use App\Modules\Network\Models\Goliath\ChainElementOutcome;
use App\Modules\Network\Models\Goliath\GoliathActionType;
use App\Modules\Network\Models\Goliath\GoliathChainType;
use App\Modules\Network\Models\Goliath\GoliathMatchType;
use App\Modules\Network\Models\Goliath\GoliathMetro;
use App\Modules\Network\Models\Goliath\GoliathRegion;
use App\Modules\Network\Models\Goliath\GoliathSubregion;
use App\Modules\Network\Models\Goliath\PrefixGoliathFirewall;
use App\Modules\Network\Models\Goliath\PrefixGoliathFlag;
use App\Modules\Network\Models\Goliath\GoliathApplianceRollback;
use App\Modules\Network\Models\Goliath\GoliathApplianceTermination;
use App\Modules\Network\Models\Goliath\GoliathSnapshot;
use App\Modules\Network\Models\Goliath\GoliathAttack;
use App\Modules\Network\Models\Goliath\GoliathApplianceCapture;
use App\Modules\Network\Models\PrefixRedirect;

use App\Modules\Network\Helpers\ControllerCommons;
use App\Modules\Network\Models\PrefixCommunity;
use GoliathAttacks;
use Log;

class AgentGoliathController extends NetworkController
{
    public static function precompress_table() {
        return [
            ['key' => '"prefix"', 'sub' => '__a'],
            ['key' => '"default_pps_limit"', 'sub' => '__c'],
            ['key' => '"default_bitrate"', 'sub' => '_d'],
            ['key' => '"default_action"', 'sub' => '__e'],
            ['key' => '"default_next_uuid"', 'sub' => '__f'],
            ['key' => '"elements"', 'sub' => '__/'],
            ['key' => '"origin_uuid"', 'sub' => '__}'],
            ['key' => '"next_uuid"', 'sub' => '__:'],
            ['key' => '"pps_limit"', 'sub' => '__,'],
            ['key' => '"bitrate"', 'sub' => '__-'],
            ['key' => '"action"', 'sub' => '__{'],
            ['key' => '"position"', 'sub' => '__0'],
            ['key' => '"bpf"', 'sub' => '__1'],
            ['key' => '"origin_uuid"', 'sub' => '__2'],
            ['key' => '"flag_heuristics"', 'sub' => '__3'],
            ['key' => '"flag_heuristics_level"', 'sub' => '__g'],
            ['key' => '"flag_synproxy"', 'sub' => '__4'],
            ['key' => 'null', 'sub' => '__5'],
            ['key' => '"accept"', 'sub' => '__6'],
            ['key' => '"drop"', 'sub' => '__7'],
            ['key' => '"goto"', 'sub' => '__8'],
            ['key' => '"police"', 'sub' => '__9'],
            ['key' => '"uuid"', 'sub' => '__b'],
            ['key' => 'tcp', 'sub' => '::'],
            ['key' => 'udp', 'sub' => '--'],
            ['key' => 'port', 'sub' => '{]'],
            ['key' => 'and', 'sub' => '[}'],
            ['key' => 'not', 'sub' => ':]'],
            ['key' => 'dst', 'sub' => ':}'],
            ['key' => 'src', 'sub' => '[:'],
            ['key' => 'host', 'sub' => '{:'],
        ];
    }

    public static function unprecompress_snapshot_payload(string $_payload_string) {
        foreach(static::precompress_table() as $x) {
            $_payload_string = str_replace($x['sub'], $x['key'], $_payload_string);
        }

        return $_payload_string;
    }

    public static function precompress_snapshot_payload(string $_payload_string) {
        foreach(static::precompress_table() as $x) {
            $_payload_string = str_replace($x['key'], $x['sub'], $_payload_string);
        }

        return $_payload_string;
    }

    public function fetch_update(Request $_r, $_region_tag = null, $_subregion_tag = null, $_facility_tag = null) {
        $st = microtime(true);

        if($_region_tag === '') {
            $_region_tag = null;
        }

        if($_subregion_tag === '') {
            $_subregion_tag = null;
        }
        
        if($_facility_tag === '') {
            $_facility_tag = null;
        }

        $facility_id = $_facility_tag === null ? '-' : GoliathMetro::where('telemetry_identifier','=',$_facility_tag)->get()->first()->id;
        $subregion_id = $_subregion_tag === null ? '-' : GoliathSubregion::where('telemetry_identifier','=',$_subregion_tag)->get()->first()->id;
        $region_id =  $_region_tag === null ? '-' : GoliathRegion::where('telemetry_identifier','=',$_region_tag)->get()->first()->id;

        // Check if there is a termination order in place - If there is, serve something specific.

        // Check if there is a rollback order in place - We do it by precise regional identifiers incase a device is deregistered and re-registered.
        $rollback_in_place = GoliathApplianceRollback::leftJoin('goliath_snapshots', 'rollback_id', '=', 'goliath_snapshots.id')
            ->where('goliath_snapshots.region_id', '=', $region_id)
            ->where('goliath_snapshots.subregion_id', '=', $subregion_id)
            ->where('goliath_snapshots.metro_id', '=', $facility_id)
            ->whereNotNull('compressed')
            ->get();

        if($rollback_in_place->count() != 0) {
            $snapshot = $rollback_in_place->first();

            $uncompressed_snapshot = json_decode(static::unprecompress_snapshot_payload(gzinflate(base64_decode($snapshot->compressed))), true);

            return response()->json($uncompressed_snapshot, 200);
        }
        
        try {

            // Select relevant chains to this region, subregion, and metro as well as default pps, bps and action...
            $relevant_chains = Chain::leftJoin('prefix_goliath_firewall', 'chains.id', '=', 'prefix_goliath_firewall.chain_id')
                ->leftJoin('prefixes', 'prefix_goliath_firewall.prefix_id', '=', 'prefixes.id')
                ->leftJoin('chain_element_outcomes', 'chain_element_outcomes.chain_id', '=', 'chains.id')
                ->leftJoin('chain_element_actions', 'pass_action_id', '=', 'chain_element_actions.id')
                ->leftJoin('goliath_action_types', 'action_type_id', '=', 'goliath_action_types.id')
                ->leftJoin('chains as next_chain', 'pass_chain_id', '=', 'next_chain.id')
                ->whereNull('prefixes.deleted_at')
                ->whereNull('chain_element_outcomes.deleted_at')
                ->whereNull('chain_element_actions.deleted_at')
                ->whereNull('prefix_goliath_firewall.deleted_at')
                ->whereNull('chain_element_outcomes.element_id')
                ->where(function($query) use($region_id) { // Select region specifics
                    if($region_id !== null) {
                        $query->orWhere('prefix_goliath_firewall.region_id', '=', $region_id)->orWhereNull('prefix_goliath_firewall.region_id');
                    }
                    else {
                        $query->whereNull('prefix_goliath_firewall.region_id');
                    }
                })
                ->where(function($query) use($subregion_id) { // Select region specifics
                    if($subregion_id !== null) {
                        $query->orWhere('prefix_goliath_firewall.subregion_id', '=', $subregion_id)->orWhereNull('prefix_goliath_firewall.subregion_id');
                    }
                    else {
                        $query->whereNull('prefix_goliath_firewall.subregion_id');
                    }
                })
                ->where(function($query) use($facility_id) { // Select region specifics
                    if($facility_id !== null) {
                        $query->orWhere('prefix_goliath_firewall.metro_id', '=', $facility_id)->orWhereNull('prefix_goliath_firewall.metro_id');
                    }
                    else {
                        $query->whereNull('prefix_goliath_firewall.metro_id');
                    }
                })
                ->select(
                    DB::raw("CONCAT(prefixes.address, '/', prefixes.mask) as prefix"),
                    'chains.uuid',
                    'pps_limit as default_pps_limit',
                    'bitrate as default_bitrate',
                    'system_name as default_action',
                    'next_chain.uuid as default_next_uuid',
                    'chains.id',
                    'chains.name',
                    'region_id',
                    'subregion_id',
                    'metro_id'
                )
                ->get();

            // Need to clean out rules where there's something of higher order specificity for a given prefix...
            $specific_chains = [];

            foreach($relevant_chains->pluck('prefix') as $prefix) {
                $many_chains = $relevant_chains->where('prefix', '=', $prefix);

                // Select by order of specificity...
                $final_specific_chain = null;

                if($many_chains->count() !== 1) {
                    $metro_candidates = $many_chains->where('metro_id', '!=', null);
                    $subregion_candidates = $many_chains->where('subregion_id', '!=', null);
                    $region_candidates = $many_chains->where('region_id', '!=', null);

                    if( $metro_candidates->count() ) {
                        $final_specific_chain = $metro_candidates->first();
                    }
                    elseif( $subregion_candidates->count() ) {
                        $final_specific_chain = $subregion_candidates->first();
                    }
                    elseif( $region_candidates->count() ) {
                        $final_specific_chain = $region_candidates->first();
                    }
                }
                else {
                    $final_specific_chain = $many_chains->first();
                }

                if($final_specific_chain !== null) {
                    $specific_chains[] = $final_specific_chain;
                }
            }

            // Collect up the specifics...
            $specific_chains = collect($specific_chains)->merge(Chain::onlyTrashed()->leftJoin('chain_element_outcomes', 'chain_element_outcomes.chain_id', '=', 'chains.id')
                ->leftJoin('chain_element_actions', 'pass_action_id', '=', 'chain_element_actions.id')
                ->leftJoin('goliath_action_types', 'action_type_id', '=', 'goliath_action_types.id')
                ->leftJoin('chains as next_chain', 'pass_chain_id', '=', 'next_chain.id')
                ->whereNull('chain_element_outcomes.deleted_at')
                ->whereNull('chain_element_actions.deleted_at')
                ->whereNull('chain_element_outcomes.element_id')
                ->select(
                    'chains.uuid',
                    'pps_limit as default_pps_limit',
                    'bitrate as default_bitrate',
                    'src_mask as default_src_mask',
                    'system_name as default_action',
                    'next_chain.uuid as default_next_uuid',
                    'chains.id',
                    'chains.name',
                )
                ->get());

            // Chain elements in their totality
            $elements = ChainElement::leftJoin('chain_element_matches', 'chain_element_matches.id', '=', 'chain_elements.match_id')
                ->leftJoin('chain_element_outcomes', 'chain_element_outcomes.element_id', '=', 'chain_elements.id')
                ->leftJoin('chains as next_chain', 'pass_chain_id', '=', 'next_chain.id')
                ->leftJoin('chains as origin_chain', 'origin_chain.id', '=', 'chain_elements.chain_id')
                ->leftJoin('chain_element_actions', 'pass_action_id', '=', 'chain_element_actions.id')
                ->leftJoin('goliath_action_types', 'action_type_id', '=', 'goliath_action_types.id')
                ->select(
                    'origin_chain.uuid as origin_uuid',
                    'next_chain.uuid as next_uuid',
                    'pps_limit',
                    'bitrate',
                    'src_mask',
                    'system_name as action',
                    'position',
                    'bpf'
                )
                ->whereIn('chain_elements.chain_id', $specific_chains->pluck('id'))
                ->get();

            // Build a list of goto chains...
            $goto_chain_uuids = array_merge(
                $elements->where('next_uuid', '!=', null)->pluck('next_uuid')->toArray(),
                $specific_chains->where('default_next_uuid', '!=', null)->pluck('default_next_uuid')->toArray());

            $final_chains = $specific_chains
                ->reject(function ($chain) use($elements, $goto_chain_uuids) {
                    return $chain['prefix'] === '/' && array_search($chain['uuid'], $goto_chain_uuids) === false;
                })
                ->map(function ($chain) use($elements, $goto_chain_uuids) {
                    return [
                        'prefix' => $chain['prefix'] !== '/' ? $chain['prefix'] : false,
                        'uuid' => $chain['uuid'],
                        'default_pps_limit' => $chain['default_pps_limit'],
                        'default_bitrate' => $chain['default_bitrate'],
                        'default_src_mask' => $chain['default_src_mask'],
                        'default_action' => $chain['default_action'],
                        'default_next_uuid' => $chain['default_next_uuid'],
                        'elements' => array_values($elements->where('origin_uuid', '=', $chain['uuid'])->toArray())
                    ];
            })->values()->toArray();

            unset($goto_chain_uuids);
            unset($specific_chains);
            unset($elements);
            unset($relevant_chains);

            $relevant_flags = PrefixGoliathFlag::leftJoin('prefixes', 'prefix_goliath_flags.prefix_id', '=', 'prefixes.id')
                ->select(
                    DB::raw("CONCAT(prefixes.address, '/', prefixes.mask) as prefix"),
                    'flag_heuristics',
                    'flag_heuristics_level',
                    'flag_synproxy',
                    'flag_fivem'
                )
                ->where(function($query) use($region_id) { // Select region specifics
                    if($region_id !== null) {
                        $query->orWhere('prefix_goliath_flags.region_id', '=', $region_id)->orWhereNull('prefix_goliath_flags.region_id');
                    }
                    else {
                        $query->whereNull('prefix_goliath_flags.region_id');
                    }
                })
                ->where(function($query) use($subregion_id) { // Select region specifics
                    if($subregion_id !== null) {
                        $query->orWhere('prefix_goliath_flags.subregion_id', '=', $subregion_id)->orWhereNull('prefix_goliath_flags.subregion_id');
                    }
                    else {
                        $query->whereNull('prefix_goliath_flags.subregion_id');
                    }
                })
                ->where(function($query) use($facility_id) { // Select region specifics
                    if($facility_id !== null) {
                        $query->orWhere('prefix_goliath_flags.metro_id', '=', $facility_id)->orWhereNull('prefix_goliath_flags.metro_id');
                    }
                    else {
                        $query->whereNull('prefix_goliath_flags.metro_id');
                    }
                })
                ->whereNull('prefixes.deleted_at')
                ->get();
                        
            $final_flags = [];

            // Reserve a lookup table to check whether something exists in a chained supernet
            $chained_supernets = PrefixGoliathFirewall::leftJoin('supernets', 'prefix_id', '=', 'supernet_id')
                ->leftJoin('prefixes', 'subnet_id', '=', 'prefixes.id')
                ->whereNotNull('chain_id')
                ->select(DB::raw("CONCAT(prefixes.address, '/', prefixes.mask) as prefix"))
                ->get()
                ->keyBy('prefix');

            // Quickly format our relevant flags...
            $flag_lookup = [];

            foreach($relevant_flags as $flags) {

                if(isset($chained_supernets[$flags->prefix])) {
                    $flags['flag_heuristics'] = 1;
                    $flags['flag_heuristics_level'] = 3;
                }

                if($flags['flag_fivem'] == 1) {
                    $flags['flag_synproxy'] = 1;
                    $flags['flag_heuristics'] = 1;
                    $flags['flag_heuristics_level'] = 3;
                }

                if(isset($final_chains[$flags->prefix])) {
                    $flag_lookup[$flags->prefix][] = $flags;
                }
                else {
                    $flag_lookup[$flags->prefix] = [$flags];
                }
            }

            unset($relevant_flags);

            foreach($flag_lookup as $prefix => $values) {
                $many_flags = collect($values);

                // Select by order of specificity...
                $final_specific_flags = null;

                if($many_flags->count() !== 1) {
                    $metro_candidates = $many_flags->where('metro_id', '!=', null);
                    $subregion_candidates = $many_flags->where('subregion_id', '!=', null);
                    $region_candidates = $many_flags->where('region_id', '!=', null);

                    if( $metro_candidates->count() ) {
                        $final_specific_flags = $metro_candidates->first();
                    }
                    elseif( $subregion_candidates->count() ) {
                        $final_specific_flags = $subregion_candidates->first();
                    }
                    elseif( $region_candidates->count() ) {
                        $final_specific_flags = $region_candidates->first();
                    }
                }
                else {
                    $final_specific_flags = $many_flags->first();
                }

                if($final_specific_flags !== null) {
                    $final_flags[$final_specific_flags->prefix] = $final_specific_flags->toArray();
                }
            }

            unset($flag_lookup);

            $mercury_lookup = PrefixCommunity::leftJoin('prefixes', 'prefix_community.prefix_id', '=', 'prefixes.id')
                ->where('community', '=', '7578:90')
                ->whereNull('prefixes.deleted_at')
                ->select(DB::raw("CONCAT(prefixes.address, '/', prefixes.mask) as prefix"))
                ->get();

            foreach($mercury_lookup as $x) {
                $final_flags[$x->prefix]['flag_clamp_mss'] = 1;
            }

            unset($mercury_lookup);
            
            /* This is gross but helpful - We're going to cache a copy of this region specific information everytime that it changes */
            $payload = [
                'chains' => $final_chains,
                'flags' => $final_flags
            ];

            unset($final_chains);
            unset($final_flags);

            // This was stupid - Break it out in another way.
            // We need to look at an additional compression cycle. A simple substitution method would work, I.E Aa -> !, Ab -> @, Sa -> # - This will reduce the size slightly, and enhance compression.
            // Must be ASCII however to keep size low. Since this is base 64, and ASCII-ex is 1 byte, we could use quite a few characters here. We have > 127 characters to use.
            /*$json_for_compression = json_encode($payload);
            $checksum_for_compression = md5($json_for_compression);

            $last_snapshot = GoliathSnapshot::where([['region_id', '=', $region_id], ['subregion_id', '=', $subregion_id], ['metro_id', '=', $facility_id]])->orderByDesc('created_at')->limit(1)->get();
            
            // If the last values checksum
            if($last_snapshot->count() == 0 || $last_snapshot->first()->checksum != $checksum_for_compression) {
                // Perform a precompression step...
                
                $new_snapshot = new GoliathSnapshot([
                    'metro_id' => $facility_id,
                    'subregion_id' => $subregion_id,
                    'region_id' => $region_id,
                    'checksum' => $checksum_for_compression,
                    'compressed' => base64_encode(gzdeflate(static::precompress_snapshot_payload($json_for_compression), 1))
                ]);

                $new_snapshot->save();
            }*/

            // Attach all capture requests, then kill them.
            if($_r->input('appliance_id') !== null) {
                $payload['captures'] = GoliathApplianceCapture::where('appliance_id', '=', $_r->input('appliance_id'))->whereNull('issued_at')->select('id', 'prescrubbed', 'bpf')->get()->toArray();

                GoliathApplianceCapture::where('appliance_id', '=', $_r->input('appliance_id'))->update([
                    'issued_at' => Carbon::now()
                ]);
            }

            return response()->json($payload, 200);
        }
        catch(\Exception $ex) {
            return response()->json([], 500);
        }
    }

    public function register_appliance(Request $_r) {

        // Validate this request in the first place.
        $valid_request = !Validator::make( $_r->all(), [
            'name'                      => ['required', 'string'],
            
            'region_tag'                => ['nullable', 'exists:goliath_regions,telemetry_identifier'],
            'subregion_tag'             => ['nullable', 'exists:goliath_subregions,telemetry_identifier'],
            'facility_tag'              => ['nullable', 'exists:goliath_metro,telemetry_identifier'],

            'appliance_id'              => ['exists:goliath_appliances,id']
            ])->fails();

        if($valid_request) {
            $appliance_id = $_r->input('appliance_id', false);

            $metro = $_r->input('facility_tag') == null ? '-' : GoliathMetro::where('telemetry_identifier','=',$_r->input('facility_tag'))->get()->first();
            $subregion = $_r->input('subregion_tag') == null ? '-' : GoliathSubregion::where('telemetry_identifier','=',$_r->input('subregion_tag'))->get()->first();
            $region =  $_r->input('region_tag') == null ? '-' : GoliathRegion::where('telemetry_identifier','=',$_r->input('region_tag'))->get()->first();
            
            $appliance = $appliance_id ? GoliathAppliance::find($appliance_id) : new GoliathAppliance([
                'name' => $_r->input('name'),
                'organisation_id' => Security::bearer_token_organisation_id($_r),
                'registered_at' => Carbon::now()
            ]);

            $appliance->region_id = $region->id ?? null;
            $appliance->subregion_id = $subregion->id ?? null;
            $appliance->metro_id = $metro->id ?? null;

            $appliance->authorised = 0;
            $appliance->address = Security::real_ip();

            if($appliance_id) {
                $update = false;

                // Check if the region, subregion and metro have changed...
                if($appliance_id && ($appliance->region_id != $region->id || $appliance->subregion_id != $subregion->id || $appliance->metro_id != $metro->id)) {
                    Notification::route('slack', env('GOLIATH_SLACK_HOOK'))->notify(new SlackNotification("An appliance of ID: $appliance_id is re-registering with new location information."));
                    $update = true;
                }

                if($appliance->address != Security::real_ip()) {
                    Notification::route('slack', env('GOLIATH_SLACK_HOOK'))->notify(new SlackNotification("An appliance of ID: $appliance_id is re-registering with a new address: " . Security::real_ip()));
                    $update = true;
                }

                if($update) {
                    if($appliance->save()) {
                        Notification::route('slack', env('GOLIATH_SLACK_HOOK'))->notify(new SlackNotification(
                            "An appliance of ID: $appliance->id has successfully reregistered at facility: $metro->name, subregion: $subregion->name, region: $region->name. Uses: " . Security::real_ip() . ". Approval required to resume function."
                        ));
                    };
                }
                else {
                    // Save it anyway so that way we can keep track of reporting via the updated_at field.
                    $appliance->save();
                }
            }
            else {
                if($appliance->save()) {
                    Notification::route('slack', env('GOLIATH_SLACK_HOOK'))->notify(new SlackNotification(
                        "An appliance of ID: $appliance->id has successfully registered at metro: $metro->name, subregion: $subregion->name, region: $region->name. Uses: " . Security::real_ip() . ". Approval required to begin function."
                    ));
                }
            }

            return response()->json([
                'message' => 'success',
                'appliance_id' => $appliance->id
            ], 200);
        }

        return response()->json([
            'message' => 'failure'
        ], 400);
    }

    public function update_hash(Request $_r) {
        // Validate this request in the first place.
        $valid_request = !Validator::make( $_r->all(), [
            'hash'                  => ['required', 'string'],
            'goliath_commit'        => ['string'],
            'agent_commit'          => ['string'],
            'appliance_id'          => ['required', 'exists:goliath_appliances,id']
            ])->fails();

        if($valid_request) {
            $appliance = GoliathAppliance::find($_r->input('appliance_id', false));

            if($appliance->update([
                'hash' => $_r->input('hash'),
                'goliath_commit' => $_r->input('goliath_commit', 'None Provided'),
                'agent_commit' => $_r->input('agent_commit', 'None Provided')
            ])) {
                return response()->json([
                    'message' => 'success',
                ], 200);   
            }
        }

        Notification::route('slack', env('SLACK_HOOK'))->notify(new SlackNotification(
            "An appliance supplied an invalid hash update attempt. IP Address: " . Security::real_ip()
        ));

        return response()->json([
            'message' => 'failure'
        ], 400);
    }

    public function upload_pcap(Request $_r, $_appliance_id, $_capture_id) {        
        $_r->file('upload_pcap')->storeAs('pcaps', "APP-$_appliance_id-CAP-$_capture_id.pcap");

        // Associate our stored file...
        GoliathApplianceCapture::where('id', '=', $_capture_id)->update([
            'location' => "pcaps/APP-$_appliance_id-CAP-$_capture_id.pcap"
        ]);

        return response()->json(['message' => 'success'], 200);   
    }

    public function add_attack(Request $_r) {

        try {
            // Validate this request in the first place.
            $valid_request = !Validator::make( $_r->all(), [
                'appliance_id'                      => ['required', 'exists:goliath_appliances,id'],
                'attacks.*.description'             => ['required', 'string'],
                'attacks.*.type'                    => ['required', 'string'],
                'attacks.*.destination'             => ['required', 'ip'],
                'attacks.*.rounded_start_timestamp' => ['required', 'numeric'],
                'attacks.*.whole_start_timestamp'   => ['required', 'numeric'],
                'attacks.*.rounded_last_timestamp'  => ['required', 'numeric'],
                'attacks.*.whole_last_timestamp'    => ['required', 'numeric']
                ])->fails();

            if($valid_request) {
                $appliance = GoliathAppliance::find($_r->input('appliance_id', false));

                $to_insert = [];

                foreach($_r->input('attacks') as $attack) {
                    $lookup = GoliathAttack::where([
                            ['appliance_id', '=', $_r->input('appliance_id')],
                            ['description', '=', $attack['description']],
                            ['updated_at', '>=', Carbon::now()->subMinutes(2)]
                        ])
                        ->select(DB::raw("concat(appliance_id, description, destination) as x"))
                        ->get()
                        ->first() ?? false;

                    if(!$lookup) {
                        $to_insert[] = [
                            'created_at'            => Carbon::now(),
                            'updated_at'            => Carbon::now(),
                            'started_at'            => Carbon::createFromTimestamp($attack['whole_start_timestamp']),
                            'rounded_started_at'    => Carbon::createFromTimestamp($attack['rounded_start_timestamp']),
                            'last_at'               => Carbon::createFromTimestamp($attack['whole_last_timestamp']),
                            'rounded_last_at'       => Carbon::createFromTimestamp($attack['rounded_last_timestamp']),
                            'type'                  => $attack['type'],
                            'description'           => $attack['description'],
                            'destination'           => $attack['destination'],
                            'region_id'             => $appliance['region_id'],
                            'subregion_id'          => $appliance['subregion_id'],
                            'metro_id'              => $appliance['metro_id'],
                            'appliance_id'          => $_r->input('appliance_id')
                        ];
                    }
                    else {
                        GoliathAttack::where([
                                ['appliance_id', '=', $_r->input('appliance_id')],
                                ['description', '=', $attack['description']],
                                ['destination', '=', $attack['destination']],
                                ['started_at', '=', Carbon::createFromTimestamp($attack['whole_start_timestamp'])]
                            ])->update([
                                'last_at'               => Carbon::createFromTimestamp($attack['whole_last_timestamp']),
                                'rounded_last_at'       => Carbon::createFromTimestamp($attack['rounded_last_timestamp'])
                            ]);
                    }
                }

                DB::table('goliath_attacks')->insert($to_insert);

                if($appliance->save()) {
                    return response()->json([
                        'message' => 'success',
                    ], 200);   
                }
            }
        }
        catch(Exception $ex) {

            Notification::route('slack', env('SLACK_HOOK'))->notify(new SlackNotification(
                "An appliance tried to notify of an attack but an error occured in Creatia. Investigation required"
            ));

            return response()->json([
                'message' => 'failure'
            ], 500);
        }

        Notification::route('slack', env('SLACK_HOOK'))->notify(new SlackNotification(
            "An appliance tried to notify of an attack but the payload was malformed. Device IP: " . Security::real_ip()
        ));

        return response()->json([
            'message' => 'failure'
        ], 400);
    }

    public function notify_event(Request $_r) {

    }

    public function prefix_redirect(Request $_r) {
        try {
            $valid_request = !Validator::make( $_r->all(), [
                'prefix'    =>    ['required', 'regex:/^((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(\/((1([0-1][0-9]|2[0-8]))|([1-9][0-9])|([8-9])))?)|(((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])(\.)){3}(25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])(\/((3[0-2])|([1-2][0-9])|([8-9])))?)$/'],
                'action'    => ['required', Rule::in(['ban', 'unban'])],
                ])->fails();
    
            if($valid_request) {
                [$address, $mask] = explode('/', $_r->input('prefix'));
                $action = $_r->input('action');
    
                if($action === 'ban') {
                    PrefixRedirect::insert([
                        'created_at' => Carbon::now(),
                        'updated_at' => Carbon::now(),
                        'address'   => $address,
                        'mask'      => $mask
                    ]);
                }
                else {
                    PrefixRedirect::where([['address', '=', $address], ['mask', '=', $mask]])->delete();
                }

                GoliathBgp::emit_acl();
    
                return response()->json([
                    'message' => 'success'
                ], 200);
            
            }
    
            return response()->json([
                'message' => 'failure'
            ], 400);
        }
        catch(\Exception $ex) {
            \Log::debug($ex);
            return response()->json([
                'message' => 'error'
            ], 500);
        }

        return response()->json([
            'message' => 'unknown'
        ], 500);
    }
}
