<?php

namespace App\Http\Controllers\Goliath;

// 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;

// Creatia
use App\Helpers\ChartHandler;
use App\Helpers\ClickhouseHandler;
use App\Helpers\Utility\GoliathAttacks;
use App\Http\Controllers\Network\NetworkController;
use App\Models\Asns;
use App\Models\Prefixes;
use App\Models\PrefixCommunity;

use App\Models\Goliath\GoliathAppliance;
use App\Models\Goliath\GoliathAttack;
use App\Models\Goliath\PrefixGoliathFirewall;
use App\Models\Goliath\PrefixGoliathFlag;
use App\Models\Goliath\GoliathAsnAttributes;
use App\Models\Goliath\PrefixGoliathInline;
use App\Models\Goliath\GoliathHistory;
use App\Models\Goliath\GoliathOrganisationSpecial;
use App\Models\Goliath\GoliathRegion;
use App\Models\Goliath\GoliathRuleset;
use App\Models\Goliath\GoliathSubregion;
use App\Models\Goliath\PrefixGoliathLoose;
use App\Models\Goliath\RulesetType;
use App\Models\Organisation;
use App\Models\Supernets;
use App\Notifications\SlackNotification;
use App\Rules\AdminAnyAsnIdentity;
use App\Rules\AdminAnyPrefixIdentity;
use App\Rules\AdminAnyRulesetIdentity;
use App\Rules\AdminGameFilters;
use App\Rules\AdminHeuristicsLevel;
use App\Rules\AdminRulesetType;
use App\Rules\ApplianceIdentity;
use App\Rules\Bpf;
use App\Rules\ExclusiveWith;
use App\Rules\Fragments\Regionality;
use App\Rules\Interval;
use App\Rules\RegionIdentity;
use App\Rules\RulesetActionType;
use App\Rules\RulesetClauseOperator;
use App\Rules\RulesetClauseType;
use App\Rules\SafeName;
use App\Rules\SrcLimit;
use App\Rules\SubregionIdentity;
use App\Rules\SynChallenge;
use App\Rules\SynCookieOsGate;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Redis;

class AdminGoliathController extends NetworkController
{
    public function fetch_appliance_list(Request $_r) {


        return $this->paged_table($_r, function() {

            return GoliathAppliance::leftJoin('goliath_regions', 'region_id', '=', 'goliath_regions.id')
                ->leftJoin('goliath_subregions', 'subregion_id', '=', 'goliath_subregions.id')
                ->leftJoin('goliath_metro', 'metro_id', '=', 'goliath_metro.id')
                ->select(
                    'goliath_regions.name as region_name',
                    'goliath_subregions.name as subregion_name',
                    'goliath_metro.name as metro_name',
                    'goliath_regions.telemetry_identifier as region_identifier',
                    'goliath_subregions.telemetry_identifier as subregion_identifier',
                    'goliath_metro.telemetry_identifier as metro_identifier',
                    'goliath_appliances.name as name',
                    'goliath_appliances.created_at as registered_at',
                    'goliath_appliances.updated_at as seen_at',
                    'hash',
                    'goliath_commit',
                    'agent_commit',
                    'address',
                    'goliath_appliances.id'
                );

        }, [
            'description'       => [ 
                'db' => DB::raw("CONCAT(goliath_regions.name, ' ', goliath_subregions.name, ' ', goliath_metro.name, ' ', goliath_regions.telemetry_identifier, ' ', goliath_subregions.telemetry_identifier, ' ', goliath_metro.telemetry_identifier, ' ', address)"),
                'searchable' => true
            ],
            'name'              => [ 'db' => 'goliath_appliances.name', 'searchable' => true ],
            'registered_at'     => [ 'db' => 'registered_at', 'searchable' => false ],
            'seen_at'           => [ 'db' => 'seen_at', 'searchable' => false ],
            'hash'              => [ 'db' => 'hash', 'searchable' => true ],
            'goliath_commit'    => [ 'db' => 'goliath_commit', 'searchable' => true ],
            'agent_commit'      => [ 'db' => 'agent_commit', 'searchable' => true ],
        ], 'id');

    }

    public function fetch_appliances_options(Request $_r) {
        // Get unit health
        $appliances = GoliathAppliance::leftJoin('goliath_subregions', 'goliath_subregions.id', '=', 'goliath_appliances.subregion_id')
            ->leftJoin('goliath_metro', 'goliath_metro.id', '=', 'goliath_appliances.metro_id')
            ->select('goliath_appliances.id', DB::raw("CONCAT(goliath_subregions.telemetry_identifier, '-', goliath_metro.telemetry_identifier, '-gol') as name"))
            ->orderBy('goliath_appliances.id')
            ->get()
            ->toArray();

        $facility_index = [];
        foreach($appliances as $k => $appliance) {
            if(!isset($facility_index[$appliance['name']])) {
                $facility_index[$appliance['name']] = 1;
            }

            $appliances[$k]['name'] = $appliances[$k]['name'] .  $facility_index[$appliance['name']];

            ++$facility_index[$appliance['name']];
        }

        $appliances_count = count($appliances);

        $payload = [
            'appliances' => $appliances,
            'appliance_count' => $appliances_count,
            'appliance_online' => $appliances_count

        ];

        return response()->json($payload, 200);
    }

    // Called by the entrypoint functions get_prefixes and get_chain_prefixes since this reusable function can be called with either $_network_id XOR $_chain_id
    public function helper_prefix_list(Request $_r, int|null $_network_id = null, int|null $_chain_id = null) {

        return $this->paged_table($_r, function() use($_network_id, $_chain_id) {
            $query = Prefixes::select(
                'prefixes.id',
                'asn',
                'peer',
                'owner',
                'prefixes.name',
                'organisations.name as organisation',
                'prefixes.organisation_id',
                DB::raw("CONCAT(address, '/', mask) as network"),
                DB::raw("(SELECT COUNT(*) FROM prefix_goliath_firewall WHERE prefix_id = prefixes.id AND ruleset_id IS NOT NULL AND prefix_goliath_firewall.deleted_at IS NULL) as ruleset_count"),
                DB::raw('(SELECT COUNT(*) FROM supernets INNER JOIN prefixes subprefixes ON subnet_id = subprefixes.id WHERE supernet_id = prefixes.id AND subprefixes.deleted_at IS NULL) AS "subnets"'),
                DB::raw('(SELECT string_agg(community, \',\') from prefix_community where prefix_id = prefixes.id and deleted_at is null) as "community"'),
                'created_source'
            )
            ->leftJoin('asns', 'asn_id', '=', 'asns.id')
            ->leftJoin('organisations', 'prefixes.organisation_id', '=', 'organisations.id')
            ->leftJoin('prefix_asn_peers', 'prefix_asn_peers.prefix_id', '=', 'prefixes.id')
            ->whereNull('prefixes.deleted_at')
            ->whereRaw('family(address) = 4');

        if($_network_id !== null) {
            $query = $query->leftJoin('supernets', 'prefixes.id', '=', 'supernets.subnet_id')->where('supernets.supernet_id', '=', $_network_id);
        }

        if($_chain_id !== null) {           
            $query = $query->leftJoin('prefix_goliath_firewall', 'prefix_goliath_firewall.prefix_id', '=', 'prefixes.id')
                ->whereNull('prefix_goliath_firewall.deleted_at')
                ->where('prefix_goliath_firewall.ruleset_id', '=', $_chain_id);
        }

        return $query;
        }, [ 
            'network'       => [ 'db' => DB::raw("CONCAT(address, '/', mask)"), 'searchable' => true ],
            'organisation'  => [ 'db' => 'organisations.name', 'searchable' => true ],
            'asn'           => [ 'db' => 'asn', 'searchable' => true ],
            'name'          => [ 'db' => DB::raw("COALESCE(prefixes.name, ' ')"), 'searchable' => true ]
        ], DB::raw("CONCAT(address, '/', mask)"), function($data) {
            $flag_lookup = [];
            $parent_inline_lookup = [];

            $unfiltered_flags = PrefixGoliathFlag::leftJoin('goliath_metro', 'goliath_metro.id', '=', 'prefix_goliath_flags.metro_id')
                ->leftJoin('prefixes', 'prefixes.id', '=', 'prefix_goliath_flags.prefix_id')
                ->leftJoin('goliath_regions', 'goliath_regions.id', '=', 'prefix_goliath_flags.region_id')
                ->leftJoin('goliath_subregions', 'goliath_subregions.id', '=', 'prefix_goliath_flags.subregion_id')
                ->whereIn('prefix_id', $data->pluck('id'))
                ->whereNull('prefixes.deleted_at')
                ->select(
                    'prefix_id',
                    'flag_synproxy',
                    'flag_heuristics',
                    'flag_heuristics_level',
                    'flag_filters',
                    'flag_block_udp',
                    'flag_block_tcp',
                    'flag_block_fragments',
                    'flag_syn_cookies',
                    'flag_drop_tcp_out_of_state',
                    'flag_app_filter_strict',
                    'flag_syn_cookie_data_promote',
                    'flag_syn_cookie_os_gate',
                    'flag_syn_challenge',
                    'flag_syn_drop_first',
                    'flag_src_limit_pps',
                    'flag_src_limit_mbps',
                    'flag_drop_unexpected_protocols',
                    'flag_drop_non_core_protocols',
                    'flag_drop_udp_to_tcp_ports',
                    'flag_udp_established_lane',
                    'flag_special',
                    'goliath_regions.telemetry_identifier as region',
                    'goliath_subregions.telemetry_identifier as subregion',
                    'goliath_metro.telemetry_identifier as datacentre'
                )
                ->get();

            $inline_lookup = PrefixGoliathInline::leftJoin('prefixes', 'prefixes.id', '=', 'prefix_goliath_inline.prefix_id')
                ->whereIn('prefix_id', $data->pluck('id'))
                ->whereNull('prefixes.deleted_at')
                ->select('prefix_id', 'forced')
                ->get()
                ->keyBy('prefix_id');

            $parent_inlines = PrefixGoliathInline::leftJoin('supernets', 'supernet_id', '=', 'prefix_goliath_inline.prefix_id')
                    ->leftJoin('prefix_goliath_flags', 'prefix_goliath_flags.prefix_id', '=', 'prefix_goliath_inline.prefix_id')
                    ->leftJoin('prefixes', 'supernet_id', '=', 'prefixes.id')
                    ->whereIn('subnet_id', $data->pluck('id'))
                    ->where(function($x) {
                        $x->where('forced', '=', 1)
                            ->orWhere('flag_synproxy', '=', 1)
                            ->orWhere(DB::raw("(SELECT COUNT(*) FROM prefix_goliath_firewall WHERE prefix_goliath_firewall.prefix_id = supernet_id AND ruleset_id IS NOT NULL AND prefix_goliath_firewall.deleted_at IS NULL)"), '>', 0);
                    })
                    ->where('still_announced', '=', 1)
                    ->whereNull('prefixes.deleted_at')
                    ->select('subnet_id', DB::raw("CONCAT(address, '/', mask) as network"))
                    ->distinct()
                    ->get();

            $loose_lookup = PrefixGoliathLoose::whereIn('prefix_id', $data->pluck('id'))
                ->select('prefix_id', 'loose')
                ->get()
                ->keyBy('prefix_id');

            foreach($unfiltered_flags as $x) {
                if(!isset($flag_lookup[$x->prefix_id])) {
                    $flag_lookup["$x->prefix_id"] = [[
                        'region' => $x->region,
                        'subregion' => $x->subregion,
                        'datacentre' => $x->datacentre,
                        'synproxy' => $x->flag_synproxy,
                        'filters' => $x->flag_filters,
                        'block_udp' => boolval($x->flag_block_udp),
                        'block_tcp' => boolval($x->flag_block_tcp),
                        'block_fragments' => boolval($x->flag_block_fragments),
                        'syn_cookies' => boolval($x->flag_syn_cookies),
                        'drop_tcp_out_of_state' => boolval($x->flag_drop_tcp_out_of_state),
                        'app_filter_strict' => boolval($x->flag_app_filter_strict),
                        'syn_cookie_data_promote' => boolval($x->flag_syn_cookie_data_promote),
                        'syn_cookie_os_gate' => $x->flag_syn_cookie_os_gate,
                        'syn_challenge' => $x->flag_syn_challenge,
                        'syn_drop_first' => boolval($x->flag_syn_drop_first),
                        'src_limit_pps' => $x->flag_src_limit_pps,
                        'src_limit_mbps' => $x->flag_src_limit_mbps,
                        'drop_unexpected_protocols' => boolval($x->flag_drop_unexpected_protocols),
                        'drop_non_core_protocols' => boolval($x->flag_drop_non_core_protocols),
                        'drop_udp_to_tcp_ports' => boolval($x->flag_drop_udp_to_tcp_ports),
                        'udp_established_lane' => boolval($x->flag_udp_established_lane),
                        'heuristics' => $x->flag_heuristics == 0 ? 0 : $x->flag_heuristics_level,
                        'special' => $x->flag_special === 1 ? true : false
                    ]];
                }
                else {
                    $flag_lookup["$x->prefix_id"][] = [
                        'region' => $x->region,
                        'subregion' => $x->subregion,
                        'datacentre' => $x->datacentre,
                        'synproxy' => $x->flag_synproxy,
                        'filters' => $x->flag_filters,
                        'block_udp' => boolval($x->flag_block_udp),
                        'block_tcp' => boolval($x->flag_block_tcp),
                        'block_fragments' => boolval($x->flag_block_fragments),
                        'syn_cookies' => boolval($x->flag_syn_cookies),
                        'drop_tcp_out_of_state' => boolval($x->flag_drop_tcp_out_of_state),
                        'app_filter_strict' => boolval($x->flag_app_filter_strict),
                        'syn_cookie_data_promote' => boolval($x->flag_syn_cookie_data_promote),
                        'syn_cookie_os_gate' => $x->flag_syn_cookie_os_gate,
                        'syn_challenge' => $x->flag_syn_challenge,
                        'syn_drop_first' => boolval($x->flag_syn_drop_first),
                        'src_limit_pps' => $x->flag_src_limit_pps,
                        'src_limit_mbps' => $x->flag_src_limit_mbps,
                        'drop_unexpected_protocols' => boolval($x->flag_drop_unexpected_protocols),
                        'drop_non_core_protocols' => boolval($x->flag_drop_non_core_protocols),
                        'drop_udp_to_tcp_ports' => boolval($x->flag_drop_udp_to_tcp_ports),
                        'udp_established_lane' => boolval($x->flag_udp_established_lane),
                        'heuristics' => $x->flag_heuristics == 0 ? 0 : $x->flag_heuristics_level,
                        'special' => $x->flag_special === 1 ? true : false
                    ];
                }
            }

            foreach($parent_inlines as $x) {
                if(!isset($parent_inline_lookup[$x->subnet_id])) {
                    $parent_inline_lookup["$x->subnet_id"] = [[ $x->network ]];
                }
                else {
                    $parent_inline_lookup["$x->subnet_id"][] = $x->network;
                }
            }

            $data->transform(function($row) use($flag_lookup, $inline_lookup, $loose_lookup, $parent_inline_lookup) {
                $row['flags'] = $flag_lookup[$row->id] ?? [];
                $row['inline'] = isset($inline_lookup[$row->id]) ? $inline_lookup[$row->id]->forced : 0;
                $row['community'] = array_reverse(explode(',', $row['community']));
                $row['parent_inline'] = isset($parent_inline_lookup[$row->id]) ? $parent_inline_lookup[$row->id] : [];
                $row['loose'] = isset($loose_lookup[$row->id]) && $loose_lookup[$row->id]->loose == 1;

                if($row['community'][0] === "") {
                    $row['community'] = [];
                }
                
                return $row;
            });

            return $data;
        });
    }

    public function get_prefix_list(Request $_r, int|null $_network_id = null) {
        return static::helper_prefix_list($_r, $_network_id, null);
    }

    public function get_ruleset_prefix_list(Request $_r, int|null $_chain_id = null) {
        return static::helper_prefix_list($_r, null, $_chain_id);
    }

    public function get_rulesets(Request $_r, int|null $_prefix_id = null) {

        return $this->paged_table($_r, function() use($_prefix_id) {

            $to_select = [
                'goliath_rulesets.id',
                'goliath_rulesets.name',
                'type',
                'rules',
                'goliath_rulesets.organisation_id',
                'organisations.name as organisation'
            ];

            if($_prefix_id !== null) {
                
                $query = GoliathRuleset::leftJoin('prefix_goliath_firewall', 'goliath_rulesets.id', '=', 'prefix_goliath_firewall.ruleset_id')
                    ->leftJoin('prefixes', 'prefix_goliath_firewall.prefix_id', '=', 'prefixes.id')
                    ->leftJoin('goliath_regions', 'goliath_regions.id', '=', 'region_id')
                    ->leftJoin('goliath_subregions', 'goliath_subregions.id', '=', 'prefix_goliath_firewall.subregion_id')
                    ->leftJoin('organisations', 'goliath_rulesets.organisation_id', '=', 'organisations.id')
                    ->whereNull('prefix_goliath_firewall.deleted_at')
                    ->whereNull('prefixes.deleted_at')
                    ->where('prefix_id', '=', $_prefix_id);

                return $query->select(...[
                    ...$to_select,
                    'goliath_regions.name as region',
                    'prefix_goliath_firewall.region_id',
                    'goliath_subregions.name as subregion',
                    'prefix_goliath_firewall.subregion_id',
                    DB::raw("CONCAT(prefixes.address, '/', prefixes.mask) as prefix"),
                ]);
            }
            else {
                return GoliathRuleset::leftJoin('organisations', 'goliath_rulesets.organisation_id', '=', 'organisations.id')
                    ->select($to_select)
                    ->addSelect([
                        'prefix_count' => PrefixGoliathFirewall::leftJoin('prefixes', 'prefixes.id', '=', 'prefix_id')
                            ->whereRaw('prefix_goliath_firewall.ruleset_id = goliath_rulesets.id')
                            ->whereNull([
                                'prefix_goliath_firewall.deleted_at',
                                'prefixes.deleted_at'
                            ])
                            ->selectRaw('count(*)')
                    ]);
            }
        }, [
            'name'                 => [ 'db' => 'goliath_rulesets.name', 'searchable' => true ],
            'type'                 => [ 'db' => 'goliath_rulesets.type', 'searchable' => true ],
            'organisation'         => [ 'db' => 'organisations.name', 'searchable' => true ],
            'prefix_count'         => [ 'db' => 'prefix_count', 'searchable' => false ]
        ], 'goliath_rulesets.created_at', function($data) {

            foreach($data->toArray() as $row => $datum) {
                $elements = [];

                // Process rules into a friendlier format
                foreach(json_decode($datum['rules']) as $i => $rule) {
                    $elements[$i] = [
                        'pass_chain_id'     => $rule->gotoNext ?? null,
                        'pps_limit'     => $rule->ppsLimit  ?? null,
                        'bps_limit'     => $rule->bpsLimit  ?? null,
                        'action'   => $rule->matchAction,
                        'position'     => $i,
                        'bpf'      => $rule->bpfRule ?? null,
                        'x'            => $rule->x ?? null,
                        'y'            => $rule->y ?? null
                    ];
                }

                $last = array_key_last($elements);

                $data[$row]['default_pps_limit'] = $elements[$last]['pps_limit'];
                $data[$row]['default_bitrate'] = $elements[$last]['bps_limit'];
                $data[$row]['default_action'] = $elements[$last]['action'];
                $data[$row]['default_goto'] = $elements[$last]['pass_chain_id'];

                unset($elements[$last]);

                $data[$row]['elements'] = $elements;
            }

            return $data;
        });

        return response()->json(['detail' => __('api.409_NOT_PERMITTED')], 409);
    }

    public function get_ruleset(Request $_r, int $_chain_id) {
        $valid_request = !Validator::make( $_r->all(), [ 'prefix_id' => [ new AdminAnyPrefixIdentity ] ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }
        
        // Build our query function
        $chain_query = GoliathRuleset::where('goliath_rulesets.id', '=', $_chain_id)
            ->select(
                'id',
                'name',
                'type as chain_type',
                'rules',
                'unencoded_rules',
                'organisation_id'
            );

            if($_r->input('prefix_id')) {
                $chain_query->leftJoin('prefix_goliath_firewall', 'goliath_rulesets.id', '=', 'prefix_goliath_firewall.ruleset_id')
                    ->whereNull('prefix_goliath_firewall.deleted_at')
                    ->where('prefix_id', '=', $_r->input('prefix_id'))
                    ->select(
                        'goliath_rulesets.id',
                        'name',
                        'type as chain_type',
                        'rules',
                        'unencoded_rules',
                        'organisation_id',
                        'region_id',
                        'subregion_id',  
                    );
            }

            $chain = $chain_query->first()->toArray();

        // If there's no chain, end this...
        if(count($chain) == 0) {
            return response()->json(['detail' => __('api.409_NOT_PERMITTED')], 409);
        }

        $chain['elements'] = [];

        foreach(json_decode($chain['rules']) as $i => $rule) {

            $element = [
                'name'              => $rule->name ?? 'Rule ' . ($i + 1),
                'goto_chain_id'     => $rule->gotoNext ?? null,
                'pps'               => $rule->ppsLimit ?? null,
                'bps'               => $rule->bpsLimit ?? null,
                'action'            => $rule->matchAction ?? null,
                'position'          => $i,
                'bpf'               => $rule->bpfRule ?? null,
                'x'                 => $rule->x ?? null,
                'y'                 => $rule->y ?? null
            ];

            $chain['elements'][] = $element;
        }

        $last = array_key_last($chain['elements']);

        $chain = [
            ...$chain,
            'unencoded_rules'   => json_decode($chain['unencoded_rules']),
            'default_pps'       => $chain['elements'][$last]['pps'],
            'default_bps'       => $chain['elements'][$last]['bps'],
            'default_action'    => $chain['elements'][$last]['action'],
            'default_goto'      => $chain['elements'][$last]['goto_chain_id'],
            'default_x'         => $chain['elements'][$last]['x'],
            'default_y'         => $chain['elements'][$last]['y']
        ];

        unset($chain['elements'][$last]);

        $chain['can_edit'] = $chain['organisation_id'] == Auth::user()->organisation_id;

        unset($chain['organisation_id']);
        unset($chain['rules']);

        return response()->json($chain, 200);
    }

    public function get_ruleset_options(Request $_r) {

        // Load standard options for everything
        $chains = GoliathRuleset::select(
            'id',
            DB::raw(
                '(CASE WHEN organisation_id = 1 AND type = \'Universal\' THEN \'Universal\' ELSE \'Reusable\' END) AS type'
            ),
            'name'
        )->get();

        $regions = GoliathRegion::join('goliath_appliances', 'goliath_regions.id', '=', 'goliath_appliances.region_id')->select('goliath_regions.id', 'goliath_regions.name')->groupBy('goliath_regions.id')->get()->toArray();
        $subregions = GoliathSubregion::join('goliath_appliances', 'goliath_subregions.id', '=', 'goliath_appliances.subregion_id')->select('goliath_subregions.region_id', 'goliath_subregions.id', 'goliath_subregions.name')->groupBy('goliath_subregions.id')->get()->toArray();

        // Load additional options
        $additional_options = [];

        if($_r->input('prefix_id', false) && ( 
            $associated_chains = PrefixGoliathFirewall::leftJoin('goliath_rulesets', 'goliath_rulesets.id', '=', 'prefix_goliath_firewall.ruleset_id')
                ->leftJoin('goliath_regions', 'goliath_regions.id', '=', 'prefix_goliath_firewall.region_id')
                ->leftJoin('goliath_subregions', 'goliath_subregions.id', '=', 'prefix_goliath_firewall.subregion_id')
                ->select( 'goliath_subregions.region_id', 'goliath_subregions.id as subregion_id', 'goliath_subregions.name as subregion_name', 'goliath_regions.name as region_name', 'goliath_rulesets.name as ruleset_name', 'ruleset_id')
                ->where('prefix_id', '=', $_r->input('prefix_id'))->whereNotNull('ruleset_id')->get() )
        ) {

            // Get the associated chains
            $completed_regions = [];

            foreach($associated_chains as $chain) {
                $completed_regions[] = [
                    'region_id'         => $chain['region_id'],
                    'subregion_id'      => $chain['subregion_id'],
                    'region_name'       => $chain['region_name'],
                    'subregion_name'    => $chain['subregion_name'],
                    'ruleset_id'        => $chain['ruleset_id'],
                    'ruleset_name'      => $chain['ruleset_name']
                ];
            }

            $additional_options = [
                'region_chains' => $completed_regions,
                'global_set' => boolval(PrefixGoliathFirewall::where('prefix_id', '=', $_r->input('prefix_id'))->whereNotNull('ruleset_id')->whereNull('region_id')->whereNull('subregion_id')->count())
            ];
        }

        return response()->json([ 
            'chains' => array_values($chains->toArray()),
            'regions' => array_values($regions),
            'subregions' => array_values($subregions),
            ...$additional_options
        ], 200);
    }

    public function get_ruleset_prefixes(Request $_r, int $_chain_id) {

        $valid_request = !Validator::make( [ 'chain_id' => $_chain_id ], [ 'chain_id' => [ new AdminAnyRulesetIdentity ] ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.409_NOT_PERMITTED')], 409);
        }

        $prefixes = PrefixGoliathFirewall::leftJoin('prefixes', 'prefix_goliath_firewall.prefix_id', '=', 'prefixes.id')
            ->where('ruleset_id', '=', $_chain_id)
            ->whereNull('prefixes.deleted_at')
            ->select(
                'prefixes.id',
                DB::raw("CONCAT(prefixes.address, '/', prefixes.mask) as prefix")
            )
            ->get()
            ->toArray();

        return response()->json($prefixes ? $prefixes : [], 200);

    }

    public function put_ruleset(Request $_r, int|null $_chain_id = null) {

        // Validate this request in the first place.
        $valid_request = Validator::make( array_merge([
            'chain_id' => $_chain_id
        ], $_r->all()), [

            'prefix_id'                 => [ new AdminAnyPrefixIdentity ],
            'chain_id'                  => [ 'nullable', new AdminAnyRulesetIdentity],

            'name'                      => ['required', 'string', 'max:255', SafeName::make(SafeName::CHARSET_NO_HASH)],
            'type'                      => ['required', new AdminRulesetType],

            'default_action'            => ['required', new RulesetActionType],
            'default_pps'               => ['required_if:default_action,police', 'numeric', 'min:0'],
            'default_bps'               => ['required_if:default_action,police', 'numeric', 'min:0'],
            'default_goto'              => ['required_if:default_action,goto', 'numeric', 'min:0', new AdminAnyRulesetIdentity],

            'default_x'                 => ['required', 'numeric'],
            'default_y'                 => ['required', 'numeric'],

            'elements.*.bpf'            => ['required', new Bpf],
            'elements.*.action'         => ['required', new RulesetActionType],
            'elements.*.pps'            => ['required_if:elements.*.action,police', 'numeric', 'min:0'],
            'elements.*.bps'            => ['required_if:elements.*.action,police', 'numeric', 'min:0'],
            'elements.*.goto_chain_id'  => ['required_if:elements.*.action,goto', 'numeric', 'min:0', new AdminAnyRulesetIdentity],
            'elements.*.name'           => ['nullable', 'string', 'max:255', SafeName::make(SafeName::CHARSET_SIMPLE)],
            'elements.*.x'              => ['required', 'numeric'],
            'elements.*.y'              => ['required', 'numeric'],

            'unencoded.*.type'          => ['nullable', new RulesetActionType],
            'unencoded.*.name'          => ['exclude_without:unencoded.*.type', 'string'],
            'unencoded.*.bps'           => ['required_if:unencoded.*.type,police', 'numeric', 'min:0'],
            'unencoded.*.pps'           => ['required_if:unencoded.*.type,police', 'numeric', 'min:0'],
            'unencoded.*.goto'          => ['required_if:unencoded.*.type,goto', 'numeric', 'min:0'],
            'unencoded.*.x'             => ['exclude_without:unencoded.*.type', 'numeric', 'min:0'],
            'unencoded.*.y'             => ['exclude_without:unencoded.*.type', 'numeric', 'min:0'],

            'unencoded.clauses.*.operator'      => ['exclude_without:unencoded.*.type', new RulesetClauseOperator],
            'unencoded.clauses.*.type'          => ['exclude_without:unencoded.*.type', new RulesetClauseType],
            'unencoded.clauses.*.negation'      => ['exclude_without:unencoded.*.type', 'boolean'],
            'unencoded.clauses.*.value.*'       => ['exclude_without:unencoded.*.type', 'string'],
            'unencoded.clauses.*.value.bpf'     => ['exclude_without:unencoded.*.type', new Bpf],

            ...Regionality::nullablePair()

        ]);

        if($valid_request->fails()) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        // Create our new rules array
        $rules = [];

        foreach($_r->input('elements') as $i => $element) {
            $rules[] = [
                'name'      => $element['name'] ?? 'Rule ' . ($i + 1),
                'x'         => $element['x'],
                'y'         => $element['y'],
                'bpf'       => $element['bpf'],
                'action'    => $element['action'],
                'gotoId'    => $element['goto_chain_id'] ?? null,
                'bpsLimit'  => $element['bps'] ?? null,
                'ppsLimit'  => $element['pps'] ?? null,
                'srcMask'   => $element['src_mask'] ?? null,
                'dstMask'   => $element['dst_mask'] ?? null
            ];
        }

        $default_rule = [
            'x'         => $_r->input('default_x'),
            'y'         => $_r->input('default_y'),
            'action'    => $_r->input('default_action'),
            'gotoId'    => $_r->input('default_goto', null),
            'bpsLimit'  => $_r->input('default_bps', null),
            'ppsLimit'  => $_r->input('default_pps', null),
            'srcMask'   => $_r->input('default_src_mask', null),
            'dstMask'   => $_r->input('default_dst_mask', null)
        ];

        /**
         * We have a few cases here we need to account for.
         * 
         * 1. It's a new ruleset
         *      Just make a new one
         * 2. It's an existing ruleset we own that we are updating
         *      Just update the old one and create a historical object
         * 3. It's an existing ruleset we don't own that we are forking
         *      Treat it as a new one
         */

        $ruleset_object = null;

        $update_rules = function($object, $rules, $default_rule) {
            foreach($rules as $rule) {
                $object->addRule(
                    $rule['name'],
                    $rule['x'],
                    $rule['y'],
                    $rule['bpf'],
                    $rule['action'],
                    $rule['gotoId'],
                    $rule['bpsLimit'],
                    $rule['ppsLimit'],
                    $rule['srcMask'],
                    $rule['dstMask']
                );
            }

            $object->addDefault(
                $default_rule['x'],
                $default_rule['y'],
                $default_rule['action'],
                $default_rule['gotoId'],
                $default_rule['bpsLimit'],
                $default_rule['ppsLimit'],
                $default_rule['srcMask'],
                $default_rule['dstMask']
            );

            return $object;
        };

        if($_chain_id === null) { // New
            $ruleset_object = new GoliathRuleset([
                'name'              => $_r->input('name'),
                'type'              => $_r->input('type', RulesetType::Reusable->value),
                'organisation_id'   => Auth::user()->organisation_id,
                'unencoded_rules'         => $_r->input('unencoded')
            ]);
        }
        else {
            // Retrieve existing one
            $existing = GoliathRuleset::find($_chain_id);

            $ruleset_object                     = $existing;
            $ruleset_object->name               = $_r->input('name');
            $ruleset_object->type               = $_r->input('type', RulesetType::Reusable->value);
            $ruleset_object->unencoded_rules    = $_r->input('unencoded');
        }

        $ruleset_object = $update_rules($ruleset_object, $rules, $default_rule);

        if($ruleset_object->save()) {

            // Attach to a provided prefix if it's provided
            if($_r->input('prefix_id', false)) {

                $this->put_prefix_ruleset(new Request([
                    'chain_id' => $ruleset_object->id,
                    'make_duplicate' => false,
                    'region_id' => $_r->input('region_id'),
                    'subregion_id' => $_r->input('subregion_id')
                ]), $_r->input('prefix_id'));

            }

            return response()->json([
                'chain_id' => $ruleset_object->id
            ], 200);
        }

        return response()->json([], 500);
    }
    
    public function delete_ruleset(Request $_r, int $_chain_id) {

        // Validate the request
        $valid_request = !Validator::make( array_merge([
            'chain_id'              => $_chain_id
        ], $_r->all()), [
            'chain_id'              => [ new AdminAnyRulesetIdentity ],
            'reassign_chain_id'     => [ 'nullable', new AdminAnyRulesetIdentity ]
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.409_NOT_PERMITTED')], 409);
        }

        // We reserve this so we can determine what rulesets we need to do any inline calculations around
        $to_emit = [];

        // If we're reassigning, we can safely assume all globals will be reassinged. If we're not, we need to purge any prefixes that are having their global removed.
        if($_r->input('reassign_chain_id') == null) {

            // No reassignment, purge regional rulesets on attached prefixes first, then remove the global. We can reuse the to to_emit prefix identification list.
            $to_emit = PrefixGoliathFirewall::where('ruleset_id', '=', $_chain_id)->get()->pluck('prefix_id');

            PrefixGoliathFirewall::whereIn('prefix_id', $to_emit->toArray())->whereNotNull('region_id')->delete();
        }

        // This will either be set to nothing on no reassignment, or set to what the reassignment value is.
        PrefixGoliathFirewall::where('ruleset_id', '=', $_chain_id)->update([ 'ruleset_id' => $_r->input('reassign_chain_id') ]);

        // Remove the ruleset.
        GoliathRuleset::where('id', '=', $_chain_id)->delete();

        return response()->json(['detail' => __('api.200_GENERIC')], 200);
    }

    public function delete_prefix_ruleset(Request $_r, int $_prefix_id) {

        // Validate this request in the first place.
        $valid_request = !Validator::make( array_merge([
            'prefix_id'         => $_prefix_id
        ], $_r->all()), [
            'prefix_id'         => [ new AdminAnyPrefixIdentity ],
            ...Regionality::nullablePair()
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.409_NOT_PERMITTED')], 409);
        }

        // Delete the individual ruleset associations
        $deletion_query = PrefixGoliathFirewall::where('prefix_id', '=', $_prefix_id); 

        if($_r->input('region_id', false)) {
            $deletion_query->where('region_id', '=', $_r->input('region_id'));
        }
        else {
            $deletion_query->whereNull('region_id');
        }

        if($_r->input('subregion_id', false)) {
            $deletion_query->where('subregion_id', '=', $_r->input('subregion_id'));
        }
        else {
            $deletion_query->whereNull('subregion_id');
        }

        $deletion_query->get()->each->delete();

        // Delete all rulesets if you remove global
        if($_r->input('region_id', null) === null && $_r->input('subregion_id', null) === null) {

            PrefixGoliathFirewall::where('prefix_id', '=', $_prefix_id)->get()->each->delete();
        }

        return response()->json(['detail' => __('api.200_GENERIC')], 200);
    }

    public function put_prefix_ruleset(Request $_r, int $_prefix_id) {

        // Validate this request in the first place.       
        $valid_request = Validator::make( array_merge([
            'prefix_id' => $_prefix_id
        ], $_r->all()), [

            'prefix_id'                 => [ 'required', new AdminAnyPrefixIdentity ],
            'chain_id'                  => [ 'required', new AdminAnyRulesetIdentity ],
            
            'make_duplicate'            => ['required', 'boolean'],

            ...Regionality::nullablePair()

        ]);

        if($valid_request->fails()) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        // Complex validation where we need to make sure that a global ruleset has been set.
        if(($_r->input('region_id') !== null || $_r->input('subregion_id') !== null) && PrefixGoliathFirewall::whereNull('region_id')->whereNull('subregion_id')->whereNull('metro_id')->where('prefix_id', '=', $_prefix_id)->count() === 0) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }
        
        $_chain_id = $_r->input('chain_id');
        
        try {
            $existing_ruleset = GoliathRuleset::find($_chain_id);

            if($_r->input('make_duplicate', false)) {

                // Copy the existing chain
                $new_ruleset = new GoliathRuleset([
                    'name'              => "$existing_ruleset->name (Copy)",
                    'type'              => $existing_ruleset->type,
                    'organisation_id'   => Auth::user()->organisation_id,
                    'rules'             => $existing_ruleset->rules, true
                ]);

                $new_ruleset->save();

                $_chain_id = $new_ruleset->id;   
            }

            // Associate chain with prefix
            $prefix_firewall = PrefixGoliathFirewall::where('prefix_id', '=', $_prefix_id)->whereNull('metro_id');

            if($_r->input('region_id', null) !== null) {
                $prefix_firewall = $prefix_firewall->where('region_id', '=', $_r->input('region_id'));
            }
            else {
                $prefix_firewall->whereNull('region_id');
            }

            if($_r->input('subregion_id', null) !== null) {
                $prefix_firewall = $prefix_firewall->where('subregion_id', '=', $_r->input('subregion_id'));
            }
            else {
                $prefix_firewall->whereNull('subregion_id');
            }

            $prefix_firewall = $prefix_firewall->first() ?? new PrefixGoliathFirewall([
                'region_id' => $_r->input('region_id', null),
                'subregion_id' => $_r->input('subregion_id', null),
                'metro_id' => null,
                'prefix_id' => $_prefix_id
            ]);

            $prefix_firewall->ruleset_id = $_chain_id;

            $prefix_firewall->save();

            // Flags only need to be a one shot - A regional chain will always be accompanied by a global one, afterall.
            $existing_flags = PrefixGoliathFlag::where([
                ['prefix_id', '=', $_prefix_id]
            ])->first();

            if($existing_flags === null) {
                (new PrefixGoliathFlag([
                    'flag_heuristics'           => 1,
                    'flag_heuristics_level'     => 2,
                    'region_id'                 => null,
                    'subregion_id'              => null,
                    'metro_id'                  => null,
                    'prefix_id'                 => $_prefix_id
                ]))->save();
            }

            return response()->json(['detail' => __('api.200_GENERIC')], 200);
        }
        catch(\Exception $ex) {
            \Log::debug($ex);
        }

        return response()->json([], 500);
    }

    public function get_flags(Request $_r, int $_prefix_id) {
        // Validate this request in the first place.
        $valid_request = !Validator::make( array_merge([
            'prefix_id' => $_prefix_id,
        ], $_r->all()), [
            'prefix_id'     => [ 'required', new AdminAnyPrefixIdentity ],
            ...Regionality::full()
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        $flags = PrefixGoliathFlag::where([
                ['prefix_id', '=', $_prefix_id],
                ['region_id', '=', $_r->input('region_id')],
                ['subregion_id', '=', $_r->input('subregion_id')],
                ['metro_id', '=', $_r->input('metro_id')]
            ])->first();

        $inline = PrefixGoliathInline::where('prefix_id', '=', $_prefix_id)->first();

        $parent_inline = PrefixGoliathInline::leftJoin('supernets', 'supernet_id', '=', 'prefix_goliath_inline.prefix_id')
            ->leftJoin('prefix_goliath_flags', 'prefix_goliath_flags.prefix_id', '=', 'prefix_goliath_inline.prefix_id')
            ->leftJoin('prefixes', 'supernet_id', '=', 'prefixes.id')
            ->whereNull('prefixes.deleted_at')
            ->where([['subnet_id', '=', $_prefix_id], ['still_announced', '=', 1]])
            ->where(function ($q) {
                $q->where('flag_synproxy', '=', 1)->orWhere('forced', '=', 1);
            })
            ->count();

        $parent_level_2 = PrefixGoliathFirewall::leftJoin('supernets', 'supernet_id', '=', 'prefix_goliath_firewall.prefix_id')
            ->leftJoin('prefixes', 'supernet_id', '=', 'prefixes.id')
            ->whereNotNull('prefix_goliath_firewall.ruleset_id')
            ->whereNull('prefixes.deleted_at')
            ->where('still_announced', '=', 1)
            ->where('subnet_id', '=', $_prefix_id)
            ->count();

        $parent_mercury = PrefixCommunity::leftJoin('supernets', 'supernet_id', '=', 'prefix_community.prefix_id')
            ->leftJoin('prefixes', 'supernet_id', '=', 'prefixes.id')
            ->where([['subnet_id', '=', $_prefix_id], ['community', '=', '7578:91']])
            ->whereNull('prefixes.deleted_at')
            ->where('still_announced', '=', 1)
            ->count();

        $loose = PrefixGoliathLoose::where('prefix_id', '=', $_prefix_id)->first();

        return response()->json([
            'parent_inline' => $parent_inline === 0 ? false : true,
            'parent_level_2' => $parent_level_2 === 0 ? false : true,
            'parent_mercury' => $parent_mercury === 0 ? false : true,
            'loose' => boolval($loose->loose ?? false),
            'level_2' => PrefixGoliathFirewall::where('prefix_id', '=', $_prefix_id)->whereNotNull('ruleset_id')->count() === 0 ? false : true,
            'mercury' => PrefixCommunity::where([['prefix_id', '=', $_prefix_id], ['community', '=', '7578:91']])->count() === 0 ? false : true,
            'inline' => boolval($inline->forced ?? false),
            'synproxy' => boolval($flags->flag_synproxy ?? false),
            'heuristics' => boolval($flags->flag_heuristics ?? false),
            'heuristics_level' => ($flags->flag_heuristics ?? false) ? $flags->flag_heuristics_level ?? 3 : null,
            'heuristics_lock' => PrefixGoliathFirewall::where('prefix_id', '=', $_prefix_id)->whereNotNull('ruleset_id')->count() === 0 ? false : true,
            'filters' => $flags === null ? null : json_decode($flags->flag_filters),
            'block_tcp' => boolval($flags->flag_block_tcp ?? null),
            'block_udp' => boolval($flags->flag_block_udp ?? null),
            'block_fragments' => boolval($flags->flag_block_fragments ?? null),
            'syn_cookies' => boolval($flags->flag_syn_cookies ?? false),
            'drop_tcp_out_of_state' => boolval($flags->flag_drop_tcp_out_of_state ?? false),
            'app_filter_strict' => boolval($flags->flag_app_filter_strict ?? false),
            'syn_cookie_data_promote' => boolval($flags->flag_syn_cookie_data_promote ?? false),
            'syn_cookie_os_gate' => $flags->flag_syn_cookie_os_gate ?? null,
            'syn_challenge' => $flags->flag_syn_challenge ?? null,
            'syn_drop_first' => boolval($flags->flag_syn_drop_first ?? false),
            'src_limit_pps' => $flags->flag_src_limit_pps ?? null,
            'src_limit_mbps' => $flags->flag_src_limit_mbps ?? null,
            'drop_unexpected_protocols' => boolval($flags->flag_drop_unexpected_protocols ?? false),
            'drop_non_core_protocols' => boolval($flags->flag_drop_non_core_protocols ?? false),
            'drop_udp_to_tcp_ports' => boolval($flags->flag_drop_udp_to_tcp_ports ?? false),
            'udp_established_lane' => boolval($flags->flag_udp_established_lane ?? false)
        ], 200);
    }

    public function put_flags(Request $_r, int $_prefix_id) {

        $valid_request = !Validator::make( array_merge([
            'prefix_id' => $_prefix_id,
        ], $_r->all()), [
            'prefix_id'         => ['required', new AdminAnyPrefixIdentity],
            'inline'            => ['required', 'boolean'],
            'synproxy'          => ['required', 'boolean'],
            'loose'             => ['required', 'boolean'],
            'heuristics'        => ['required_if:heuristics,true', new AdminHeuristicsLevel],
            ...Regionality::full(),
            'filters'                   => ['nullable', new AdminGameFilters],
            'block_tcp'                 => ['nullable', 'boolean'],
            'block_udp'                 => ['nullable', 'boolean'],
            'block_fragments'           => ['nullable', 'boolean'],
            'syn_cookies'               => ['nullable', 'boolean'],
            'drop_tcp_out_of_state'     => ['nullable', 'boolean'],
            'app_filter_strict'         => ['nullable', 'boolean'],
            'syn_cookie_data_promote'   => ['nullable', 'boolean'],
            'syn_cookie_os_gate'        => ['nullable', new SynCookieOsGate],
            'syn_challenge'             => ['nullable', new SynChallenge],
            'syn_drop_first'            => ['nullable', 'boolean'],
            'src_limit_pps'             => ['nullable', new SrcLimit('pps'), new ExclusiveWith('src_limit_mbps')],
            'src_limit_mbps'            => ['nullable', new SrcLimit('mbps')],
            'drop_unexpected_protocols' => ['nullable', 'boolean'],
            'drop_non_core_protocols'   => ['nullable', 'boolean', new ExclusiveWith('drop_unexpected_protocols')],
            'drop_udp_to_tcp_ports'     => ['nullable', 'boolean'],
            'udp_established_lane'      => ['nullable', 'boolean']
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.409_NOT_PERMITTED')], 409);
        }

        $flags = PrefixGoliathFlag::firstOrNew([
            'prefix_id' => $_prefix_id,
            'region_id' => $_r->input('region_id', null),
            'subregion_id' => $_r->input('subregion_id', null),
            'metro_id' => $_r->input('metro_id', null)
        ]);

        $flags->flag_synproxy = $_r->input('synproxy');
        $flags->flag_block_fragments = $_r->input('block_fragments');
        $flags->flag_syn_cookies = $_r->input('syn_cookies');
        $flags->flag_drop_tcp_out_of_state = $_r->input('drop_tcp_out_of_state');
        $flags->flag_syn_cookie_data_promote = $_r->input('syn_cookie_data_promote');
        $flags->flag_syn_cookie_os_gate = $_r->input('syn_cookie_os_gate') ?: null;
        $flags->flag_syn_challenge = $_r->input('syn_challenge') ?: null;
        $flags->flag_syn_drop_first = $_r->input('syn_drop_first');
        $flags->flag_src_limit_pps = $_r->input('src_limit_pps');
        $flags->flag_src_limit_mbps = $_r->input('src_limit_mbps');
        $flags->flag_drop_unexpected_protocols = $_r->input('drop_unexpected_protocols');
        $flags->flag_drop_non_core_protocols = $_r->input('drop_non_core_protocols');
        $flags->flag_drop_udp_to_tcp_ports = $_r->input('drop_udp_to_tcp_ports');
        $flags->flag_udp_established_lane = $_r->input('udp_established_lane');
        $flags->flag_heuristics = boolval($_r->input('heuristics'));
        $flags->flag_heuristics_level = $_r->input('heuristics');

        $flags->setFilters( $_r->input('filters') );

        if($_r->input('filters')) {
            $flags->flag_block_udp = $_r->input('block_udp') ?? false;
            $flags->flag_block_tcp = $_r->input('block_tcp') ?? false;
            $flags->flag_app_filter_strict = $_r->input('app_filter_strict') ?? false;
        }
        else {
            $flags->flag_block_udp = false;
            $flags->flag_block_tcp = false;
            $flags->flag_app_filter_strict = false;
        }

        $flags->save();

        $inline = PrefixGoliathInline::firstOrNew([
            'prefix_id' => $_prefix_id
        ]);

        $inline->forced = $_r->input('inline', 0) ? 1 : 0;

        if($_r->input('filters')) {
            $inline->forced = 1;
        }
        else {
            $inline->forced = $_r->input('inline', 0) ? 1 : 0;
        }

        $inline->save();

        if($_r->input('loose', false)) {

            $loose = PrefixGoliathLoose::firstOrNew([
                'prefix_id' => $_prefix_id
            ]);

            $loose->loose = 1;

            $loose->save();
            
        }
        else {
            PrefixGoliathLoose::where('prefix_id', '=', $_prefix_id)->delete();
        }

        return response()->json(['detail' => __('api.200_GENERIC')], 200);
    }

    public function post_subnet(Request $_r, int $_network_id) {

        return NetworkController::post_prefix($_r, $_network_id, ['input' => [
            'synproxy'              => $_r->input('synproxy'),
            'filters'               => $_r->input('filters'),
            'loose'                 => $_r->input('loose'),
            'heuristics'            => $_r->input('heuristics'),
            'inline'                => $_r->input('inline'),
            'block_tcp'             => $_r->input('block_tcp'),
            'block_udp'             => $_r->input('block_udp'),
            'block_fragments'       => $_r->input('block_fragments'),
            'syn_cookies'           => $_r->input('syn_cookies'),
            'drop_tcp_out_of_state' => $_r->input('drop_tcp_out_of_state'),
            'app_filter_strict'     => $_r->input('app_filter_strict'),
            'syn_cookie_data_promote'   => $_r->input('syn_cookie_data_promote'),
            'syn_cookie_os_gate'        => $_r->input('syn_cookie_os_gate'),
            'syn_challenge'             => $_r->input('syn_challenge'),
            'syn_drop_first'            => $_r->input('syn_drop_first'),
            'src_limit_pps'             => $_r->input('src_limit_pps'),
            'src_limit_mbps'            => $_r->input('src_limit_mbps'),
            'drop_unexpected_protocols' => $_r->input('drop_unexpected_protocols'),
            'drop_non_core_protocols'   => $_r->input('drop_non_core_protocols'),
            'drop_udp_to_tcp_ports'     => $_r->input('drop_udp_to_tcp_ports'),
            'udp_established_lane'      => $_r->input('udp_established_lane')
        ], 'rule' => [
            'synproxy'              => ['required', 'boolean'],
            'filters'               => ['nullable', new AdminGameFilters],
            'loose'                 => ['required', 'boolean'],
            'inline'                => ['required', 'boolean'],
            'heuristics'            => ['required', new AdminHeuristicsLevel],
            'block_tcp'             => ['nullable', 'boolean'],
            'block_udp'             => ['nullable', 'boolean'],
            'block_fragments'       => ['nullable', 'boolean'],
            'syn_cookies'           => ['nullable', 'boolean'],
            'drop_tcp_out_of_state' => ['nullable', 'boolean'],
            'app_filter_strict'     => ['nullable', 'boolean'],
            'syn_cookie_data_promote'   => ['nullable', 'boolean'],
            'syn_cookie_os_gate'        => ['nullable', new SynCookieOsGate],
            'syn_challenge'             => ['nullable', new SynChallenge],
            'syn_drop_first'            => ['nullable', 'boolean'],
            'src_limit_pps'             => ['nullable', new SrcLimit('pps'), new ExclusiveWith('src_limit_mbps')],
            'src_limit_mbps'            => ['nullable', new SrcLimit('mbps')],
            'drop_unexpected_protocols' => ['nullable', 'boolean'],
            'drop_non_core_protocols'   => ['nullable', 'boolean', new ExclusiveWith('drop_unexpected_protocols')],
            'drop_udp_to_tcp_ports'     => ['nullable', 'boolean'],
            'udp_established_lane'      => ['nullable', 'boolean']
        ] ] , function($new, $parent, $request) use($_network_id) {

            $this->put_flags(new Request([
                'inline'                => $request->input('inline', 0),
                'synproxy'              => $request->input('synproxy'),
                'filters'               => $request->input('filters'),
                'heuristics'            => $request->input('heuristics'),
                'loose'                 => $request->input('loose'),
                'block_tcp'             => $request->input('block_tcp'),
                'block_udp'             => $request->input('block_udp'),
                'block_fragments'       => $request->input('block_fragments'),
                'syn_cookies'           => $request->input('syn_cookies'),
                'drop_tcp_out_of_state' => $request->input('drop_tcp_out_of_state'),
                'app_filter_strict'     => $request->input('app_filter_strict'),
                'syn_cookie_data_promote'   => $request->input('syn_cookie_data_promote'),
                'syn_cookie_os_gate'        => $request->input('syn_cookie_os_gate'),
                'syn_challenge'             => $request->input('syn_challenge'),
                'syn_drop_first'            => $request->input('syn_drop_first'),
                'src_limit_pps'             => $request->input('src_limit_pps'),
                'src_limit_mbps'            => $request->input('src_limit_mbps'),
                'drop_unexpected_protocols' => $request->input('drop_unexpected_protocols'),
                'drop_non_core_protocols'   => $request->input('drop_non_core_protocols'),
                'drop_udp_to_tcp_ports'     => $request->input('drop_udp_to_tcp_ports'),
                'udp_established_lane'      => $request->input('udp_established_lane')
            ]), $new->id);

            GoliathAttacks::calculate($new->id, "$new->address/$new->mask");

        }, true);

    }

    public function get_appliance_options(Request $_r) {
        return response()->json([
            'appliances' => GoliathAppliance::leftJoin('goliath_subregions', 'goliath_subregions.id', '=', 'goliath_appliances.subregion_id')
                ->leftJoin('goliath_metro', 'goliath_metro.id', '=', 'goliath_appliances.metro_id')
                ->select(
                    'goliath_appliances.id',
                    'goliath_appliances.name',
                    DB::raw("CONCAT(goliath_subregions.telemetry_identifier, '-', goliath_metro.telemetry_identifier, '-gol') as identifier")
                )
                ->get()
                ->toArray()
        ], 200);
    }

    public function get_defaults(Request $_r, int $_asn_id) {
        $defaults = GoliathAsnAttributes::where('asn_id', '=', $_asn_id)->first();
        
        return response()->json($defaults === null ? [
            'synproxy' => false,
            'heuristics' => 3,
            'inline' => false,
            'chain' => false,
            'loose' => true
        ] : [
            'synproxy' => boolval($defaults->default_synproxy),
            'heuristics' => $defaults->default_heuristics,
            'inline' => boolval($defaults->default_inline),
            'chain' => $defaults->default_chain,
            'loose' => $defaults->default_loose
        ], 200);
    }

    public function put_defaults(Request $_r, int $_asn_id) {

        // Validate this request in the first place.
        $valid_request = !Validator::make( array_merge(['asn_id' => $_asn_id], $_r->all()), [
            'asn_id' => ['required', new AdminAnyAsnIdentity],
            'synproxy' => ['required', 'boolean'],
            'heuristics' => ['required', new AdminHeuristicsLevel],
            'inline' => ['required', 'boolean'],
            'override' => ['required', 'boolean'],
            'loose' => ['required', 'boolean']
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        $defaults =  GoliathAsnAttributes::firstOrNew(['asn_id' => $_asn_id]);

        $defaults->default_synproxy = $_r->input('synproxy') ? 1 : 0;
        $defaults->default_chain = null;
        $defaults->default_inline = $_r->input('inline') ? 1 : 0;
        $defaults->default_heuristics = $_r->input('heuristics');
        $defaults->default_loose = $_r->input('loose') ? 1 : 0;

        if($_r->input('override', false)) {

            $defaults->setOverwrite(true);

            $prefix_ids = Prefixes::select('id')->where('asn_id', '=', $_asn_id)->get()->pluck('id');

            // Find all of the level 2 prefixes - These must be handled differently.
            $level_2_prefixes = PrefixGoliathFirewall::leftJoin('prefixes', 'prefix_id', '=', 'prefixes.id')->where('asn_id', '=', $_asn_id)->whereNotNull('ruleset_id')->get()->pluck('prefix_id');
            
            // Handle setting default for level 1 prefixes
            PrefixGoliathFlag::whereIn('prefix_id', $prefix_ids)
                ->whereNotIn('prefix_id', $level_2_prefixes)
                ->update([
                    'flag_synproxy' => $_r->input('synproxy'),
                    'flag_heuristics' => $_r->input('heuristics') ? 1 : 0,
                    'flag_heuristics_level' => $_r->input('heuristics')
                ]);

            // Handle setting default for level 2 prefixes
            PrefixGoliathFlag::whereIn('prefix_id', $prefix_ids)
                ->whereIn('prefix_id', $level_2_prefixes)
                ->update([
                    'flag_synproxy' => $_r->input('synproxy'),
                    'flag_heuristics' => 1,
                    'flag_heuristics_level' => $_r->input('heuristics')
                ]);

            PrefixGoliathInline::whereIn('prefix_id', $prefix_ids)
                ->update([
                    'forced' => $_r->input('inline') ? 1 : 0
                ]);

            // Handle OAH
            $existing_oah = PrefixGoliathLoose::whereIn('prefix_id', $prefix_ids)
                ->whereNull('deleted_at')
                ->pluck('prefix_id')
                ->toArray();

            // Update existing records in batch
            if (!empty($existing_oah)) {
                PrefixGoliathLoose::whereIn('prefix_id', $existing_oah)
                    ->whereNull('deleted_at')
                    ->update(['loose' => $_r->input('loose') ? 1 : 0]);
            }

            // Find prefix_ids that need to be created
            $to_create_oah = array_diff($prefix_ids->toArray(), $existing_oah);

            // Bulk insert new records
            if (!empty($to_create_oah)) {
                $insert_data = [];
                foreach($to_create_oah as $prefix_id) {
                    $insert_data[] = [
                        'prefix_id' => $prefix_id,
                        'loose' => $_r->input('loose') ? 1 : 0,
                        'subregion_info' => null,
                        'created_at' => now(),
                        'updated_at' => now()
                    ];
                }
                PrefixGoliathLoose::insert($insert_data);
            }

        }

        if($defaults->save()) {
            return response()->json(['detail' => __('api.200_GENERIC')], 200);
        }

        return response()->json([], 500);
    }

    public function get_recent_changes(Request $_r) {

        return $this->paged_table($_r, function() {

            return GoliathHistory::leftJoin('goliath_history_visibility', 'goliath_history_visibility.history_id', '=', 'goliath_history.id')
                ->join('users', 'users.id', '=', 'goliath_history.user_id')
                ->join('organisations as org_resource', 'goliath_history.organisation_id', '=', 'org_resource.id')
                ->join('organisations as org_user', 'users.organisation_id', '=', 'org_user.id')
                ->where('goliath_history.created_at', '>', Carbon::parse('2025-09-09 05:00:00'))
                ->select(
                    'goliath_history.id',
                    'goliath_history.created_at',
                    DB::raw('org_resource.name as identifier'),
                    DB::raw('CONCAT(users.name, \' (\', org_user.name, \')\') as user'),
                    'type',
                    'history'
                );

        }, [ 
            'user'              => [ 'db' => 'users.name', 'searchable' => true ],
            'type'              => [ 'db' => 'type', 'searchable' => true ],
            'identifier'        => [ 'db' => DB::raw("CONCAT(CASE WHEN type = 'Flags' THEN history -> 'Prefix' WHEN type = 'Inline' THEN history -> 'Prefix' WHEN type = 'Firewall' THEN history -> 'Prefix' WHEN type = 'Default' THEN history -> 'Asn' WHEN type = 'Ruleset' THEN history -> 'Ruleset' WHEN type = 'RulesetFull' THEN history -> 'Ruleset' END, ' (', org_resource ,')')"), 'searchable' => true ]
        ], 'goliath_history.created_at', function($data) {

            $data->transform(function($row) {

                $history = json_decode($row->history, true);

                switch($row['type']) {
                    case 'Flags':
                        try {
                            $row->history = [
                                'heuristics'          => $history['Heuristics'],
                                'synproxy'            => $history['Synproxy'],
                                'dropTCPOutOfState'   => $history['DropTCPOutOfState'] ?? null,
                                'appFilterStrict'     => $history['AppFilterStrict'] ?? null,
                                'synCookieDataPromote'   => $history['SynCookieDataPromote'] ?? null,
                                'synCookieOsGate'        => $history['SynCookieOsGate'] ?? null,
                                'synChallenge'           => $history['SynChallenge'] ?? null,
                                'synDropFirst'           => $history['SynDropFirst'] ?? null,
                                'srcLimit'               => $history['SrcLimit'] ?? null,
                                'dropUnexpectedProtocols' => $history['DropUnexpectedProtocols'] ?? null,
                                'dropNonCoreProtocols'   => $history['DropNonCoreProtocols'] ?? null,
                                'dropUDPToTCPPorts'      => $history['DropUDPToTCPPorts'] ?? null,
                                'udpEstablishedLane'     => $history['UdpEstablishedLane'] ?? null
                            ];
                        }
                        catch(\Exception $ex) {
                            $row->history = ['error' => true];
                        }

                        $row->identifier = $history['Prefix'] . " ($row->identifier)";
                        break;
                    case 'Inline':
                        try {
                            $row->history = $history['Inline'];
                        }
                        catch(\Exception $ex) {
                            $row->history = ['error' => true];
                        }

                        $row->identifier = $history['Prefix'] . " ($row->identifier)";
                        break;
                    case 'Firewall':
                        try {
                            $row->history = [ 'ruleset' => $history['Ruleset'], 'region' => $history['Regionality'] ];
                        }
                        catch(\Exception $ex) {
                            $row->history = ['error' => true];
                        }

                        $row->identifier = $history['Prefix'] . " ($row->identifier)";
                        break;
                    case 'Default':
                        try {
                            $row->history = [
                                'heuristics'    => $history['Heuristics'],
                                'synproxy'      => $history['Synproxy'],
                                'inline'        => $history['Inline'],
                                'overwrite'     => $history['Overwrite']
                            ];
                        }
                        catch(\Exception $ex) {
                            $row->history = ['error' => true];
                        }

                        $row->identifier = 'AS' . $history['Asn'] . " ($row->identifier)";
                        break;
                    case 'Ruleset':
                        try {
                            $row->history = [
                                'rules' => [
                                    'old' => $history['Rules']['Old'],
                                    'new' => $history['Rules']['New']
                                ]
                            ];
                        }
                        catch(\Exception $ex) {
                            $row->history = ['error' => true];
                        }

                        $row->identifier = $history['Ruleset'] . " ($row->identifier)";
                        
                        break;
                    case 'RulesetFull':
                        try {
                            $row->history = [
                                'rules' => [
                                    'old' => $history['Rules']['Old'],
                                    'new' => $history['Rules']['New'],
                                    'prefixes' => $history['Prefixes']['List']
                                ]
                            ];
                        }
                        catch(\Exception $ex) {
                            $row->history = ['error' => true];
                        }

                        $row->type = 'Ruleset';
                        $row->identifier = $history['Ruleset'] . " ($row->identifier)";

                        break;
                }

                return $row->toArray();
            });

            return $data;
        });

    }

    public function get_attack_summary(Request $_r) {
        // Get count of ongoing attacks
        $ongoing_count = GoliathAttack::where('updated_at', '>', Carbon::now()->subMinutes(2))->count();

        // Get count blocked last 24 hours
        $last_day_blocked = GoliathAttack::where('updated_at', '>', Carbon::now()->subHours(24))->count();

        // Get count blocked last week
        $last_week_blocked = GoliathAttack::where('updated_at', '>', Carbon::now()->subHours(168))->count();

        return response()->json([
            'ongoing' => $ongoing_count,
            'blocked_daily' => $last_day_blocked,
            'blocked_weekly' => $last_week_blocked
        ], 200);
    }

    public function get_attack_logs(Request $_r, int|null $_prefix_id = null) {
        $flag_status = $_r->input('status', false);

        return $this->paged_table($_r, function() use($_prefix_id, $flag_status) {
            $q = GoliathAttack::leftJoin('goliath_metro', 'goliath_attacks.metro_id', '=', 'goliath_metro.id')
            ->leftJoin('goliath_subregions', 'goliath_attacks.subregion_id', '=', 'goliath_subregions.id')
            ->leftJoin('goliath_regions', 'goliath_attacks.region_id', '=', 'goliath_regions.id')
            ->where('goliath_attacks.updated_at', '>', Carbon::now()->subDays(60))
            ->select(
                'started_at as start',
                'last_at as end',
                'type',
                'description', 
                'destination',
                'goliath_metro.telemetry_identifier as datacentre',
                'goliath_subregions.telemetry_identifier as subregion',
                'goliath_regions.telemetry_identifier as region'
            );

            if($_prefix_id !== null) {
                $q = $q->leftJoin('goliath_attack_prefixes as gap', 'gap.attack_id', '=', 'goliath_attacks.id')
                    ->where('gap.prefix_id', '=', $_prefix_id);
            }

            if($flag_status !== false && strtoupper($flag_status) === 'ONGOING') {
                $q = $q->where('goliath_attacks.updated_at', '>', Carbon::now()->subMinutes(2));
            }

            return $q;
        }, [ 
            'start'         => [ 'db' => 'started_at', 'searchable' => true ],
            'end'           => [ 'db' => 'last_at', 'searchable' => true ],
            'type'          => [ 'db' => 'type', 'searchable' => true ],
            'description'   => [ 'db' => 'description', 'searchable' => true ],
            'destination'   => [ 'db' => 'destination', 'searchable' => true ],
            'datacentre'    => [ 'db' => 'goliath_metro.telemetry_identifier', 'searchable' => true ],
            'subregion'     => [ 'db' => 'goliath_subregions.telemetry_identifier', 'searchable' => true ],
            'region'        => [ 'db' => 'goliath_regions.telemetry_identifier', 'searchable' => true ]
        ], 'created_at', null);

    }

    public function get_usage_chart(Request $_r) {

        // For whoever reads this in the future - We're being careful here because we want to very strictly sanitise data that could end up being passed to clickhouse. While we sanitise, we've rolled our own sanity. We need to be cautious.
        $valid_request = !Validator::make($_r->only('time', 'type'), [
                'time' => ['required', 'integer', Interval::make(Interval::USAGE_CHART)],
                'type' => ['required', Rule::in( 'pps', 'bps' )]
            ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        // Build our clickhouse handler
        $chh = new ClickhouseHandler(
            'default',
            null,
            null,
            null,
            null,
            'http://10.100.18.32:9000/'
        );

        $asns = Asns::get()->pluck('asn');

        if($_r->input('type') === 'pps') {
            return ChartHandler::goliathDropped($chh, $asns, null, false, true, $_r->input('time'))[0];
        }
        else {
            return ChartHandler::goliathDropped($chh, $asns, null, true, false, $_r->input('time'))[1];
        }

        return response()->json([], 500);
    }

    public function get_special(Request $_r, int $_organisation_id) {
        
        $special = GoliathOrganisationSpecial::where('organisation_id', '=', $_organisation_id)->first();

        return response()->json($special === null ? [
            'special' => false
        ] : [
            'special' => boolval($special->special)
        ], 200);
    }

    public function put_special(Request $_r, int $_organisation_id) {

        $valid_request = !Validator::make( ['special' => $_r->input('special')], ['special' => ['required', 'boolean' ]] )->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        $defaults =  GoliathOrganisationSpecial::firstOrNew(['organisation_id' => $_organisation_id]);

        $defaults->special = $_r->input('special') ? 1 : 0;

        if($_r->input('override', false)) {

            $organisation = Organisation::find($_organisation_id)->name;
            
            Notification::route('slack', config('creatia.webhook.goliath'))->notify(new SlackNotification("Upcoming special update for \"$organisation\" will overwrite existing preferences."));

            $prefix_ids = Prefixes::getOrganisationPrefixes($_organisation_id)->pluck('id');

            PrefixGoliathFlag::whereIn('prefix_id', $prefix_ids)
                ->update([
                    'flag_special' => $_r->input('special'),
                ]);
        }

        if($defaults->save()) {
            return response()->json(['detail' => __('api.200_GENERIC')], 200);
        }

        return response()->json([], 500);
    }

    public function get_special_prefix(Request $_r, int $_prefix_id) {
        // Validate this request in the first place.
        $valid_request = !Validator::make( array_merge([
            'prefix_id' => $_prefix_id,
        ], $_r->all()), [
            'prefix_id'     => [ 'required', new AdminAnyPrefixIdentity ],
            ...Regionality::full()
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        }

        $flags = PrefixGoliathFlag::where([
            ['prefix_id', '=', $_prefix_id],
            ['region_id', '=', $_r->input('region_id')],
            ['subregion_id', '=', $_r->input('subregion_id')],
            ['metro_id', '=', $_r->input('metro_id')]
        ])->first();

        return response()->json([ 'special' => $flags === null ? false : ($flags->flag_special === 0 || $flags->flag_special === null ? false : true) ], 200);
    }

    public function put_special_prefix(Request $_r, int $_prefix_id) {

        $valid_request = !Validator::make( array_merge([
            'prefix_id' => $_prefix_id,
        ], $_r->all()), [
            'prefix_id'         => ['required', new AdminAnyPrefixIdentity],
            'special'           => ['required', 'boolean'],
            ...Regionality::full()
        ])->fails();

        if(!$valid_request) {
            return response()->json(['detail' => __('api.409_NOT_PERMITTED')], 409);
        }

        $flags = PrefixGoliathFlag::firstOrNew([
            'prefix_id' => $_prefix_id,
            'region_id' => $_r->input('region_id'),
            'subregion_id' => $_r->input('subregion_id'),
            'metro_id' => $_r->input('metro_id')
        ]);

        $flags->flag_special = $_r->input('special');

        $flags->save();

        return response()->json(['detail' => __('api.200_GENERIC')], 200);
    }

    public function fetch_appliance_configuration(Request $_r, int $_appliance_id) {
        $valid_request = !Validator::make( [
            'appliance_id'  => $_appliance_id
            ], [
            'appliance_id'  => [new ApplianceIdentity]
            ])->fails();

        $identifier = GoliathAppliance::leftJoin('goliath_subregions', 'subregion_id', 'goliath_subregions.id')->select(
            'telemetry_identifier'
        )->where('goliath_appliances.id', '=', $_appliance_id)->first()->telemetry_identifier;

        $configuration_flags = Redis::get("goliath-flags.$identifier");
        $configuration_chains = Redis::get("goliath-chains.$identifier");

        $count_h0 = substr_count($configuration_flags, '"heuristicsLevel":0');
        $count_h1 = substr_count($configuration_flags, '"heuristicsLevel":1');
        $count_h2 = substr_count($configuration_flags, '"heuristicsLevel":2');
        $count_h3 = substr_count($configuration_flags, '"heuristicsLevel":3');
        $count_h4 = substr_count($configuration_flags, '"heuristicsLevel":4');
        $count_h5 = substr_count($configuration_flags, '"heuristicsLevel":5');
        $count_syn_on = substr_count($configuration_flags, '"synproxy":true');
        $count_syn_off = substr_count($configuration_flags, '"synproxy":false');

        $count_fivem = substr_count($configuration_flags, '"fivem"');
        $count_rust = substr_count($configuration_flags, '"rust"');
        $count_source1 = substr_count($configuration_flags, '"source1"');
        $count_source2 = substr_count($configuration_flags, '"source2"');
        $count_dayz = substr_count($configuration_flags, '"dayz"');
        $count_scpsl = substr_count($configuration_flags, '"scpsl"');
        $count_arksa = substr_count($configuration_flags, '"arksa"');
        $count_arkse = substr_count($configuration_flags, '"arkse"');
        $count_reforger = substr_count($configuration_flags, '"reforger"');
        $count_a2s = substr_count($configuration_flags, '"a2s"');
        $count_hll = substr_count($configuration_flags, '"hll"');
        $count_squad = substr_count($configuration_flags, '"squad"');
        $count_bedrock = substr_count($configuration_flags, '"bedrock"');

        $count_chain = substr_count($configuration_flags, '"chainID":');
        $count_poe = substr_count($configuration_flags, '"poe":');
        $count_silk = substr_count($configuration_flags, '"silkroad":');

        $summary = [
            'H0' => $count_h0, 'H1' => $count_h1, 'H2' => $count_h2, 'H3' => $count_h3, 'H4' => $count_h4, 'H5' => $count_h5,
            'SYN-ON' => $count_syn_on, 'SYN-OFF' => $count_syn_off,
            'FIVEM-FILTER' => $count_fivem,
            'RUST-FILTER' => $count_rust,
            'SOURCE1-FILTER' => $count_source1,
            'SOURCE2-FILTER' => $count_source2,
            'DAYZ-FILTER' => $count_dayz,
            'SCPSL-FILTER' => $count_scpsl,
            'ARKSA-FILTER' => $count_arksa,
            'ARKSE-FILTER' => $count_arkse,
            'REFORGER-FILTER' => $count_reforger,
            'A2S-FILTER' => $count_a2s,
            'HLL-FILTER' => $count_hll,
            'SQUAD-FILTER' => $count_squad,
            'BEDROCK-FILTER' => $count_bedrock,
            'CHAIN' => $count_chain,
            'SPECIAL-POE' => $count_poe,
            'SPECIAL-SILKROAD' => $count_silk
        ];

        if($valid_request) {
            return response()->json([
                'flags' => json_decode($configuration_flags, true),
                'chains' => json_decode($configuration_chains, true),
                'summary' => $summary
            ], 200);
        }

        return response()->json([], 500);
    }

    public function bulk_update_flags(Request $_r, int $_prefix_id) {
        
        $valid_request = !Validator::make( array_merge([
            'prefix_id' => $_prefix_id,
        ], $_r->all()), [
            'prefix_id'             => [ 'required', new AdminAnyPrefixIdentity ],
            'inline'                => [ 'nullable', Rule::in([null, true, false])],
            'synproxy'              => [ 'nullable', Rule::in([null, true, false])],
            'filters'               => [ 'nullable', new AdminGameFilters ],
            'loose'                 => [ 'nullable', Rule::in([null, true, false])],
            'heuristics_level'      => [ 'nullable', new AdminHeuristicsLevel],
            'block_tcp'             => [ 'nullable', Rule::in([null, true, false])],
            'block_udp'             => [ 'nullable', Rule::in([null, true, false])],
            'block_fragments'       => [ 'nullable', Rule::in([null, true, false])],
            'syn_cookies'           => [ 'nullable', Rule::in([null, true, false])],
            'drop_tcp_out_of_state' => [ 'nullable', Rule::in([null, true, false])],
            'app_filter_strict'     => [ 'nullable', Rule::in([null, true, false])]
        ])->fails();

        if(!$valid_request) {

            return response()->json(['detail' => __('api.409_NOT_PERMITTED')], 409);
        }

        // Retrieve all child prefixes, then iterate over them with a unique request.
        
        Db::beginTransaction();

        $any_fail = false;

        foreach([ $_prefix_id, ...Supernets::where('supernet_id', '=', $_prefix_id)->distinct('subnet_id')->get()->pluck('subnet_id') ] as $x) {

            // Check the prefix still exists...
            if(Prefixes::find($_prefix_id)) {
                $flags = $this->get_flags(new Request([]), $x)->getData(true);

                $filters = $_r->has('filters') ?
                        ( $_r->input('filters') === null ? null : $_r->input('filters') ) : $flags['filters'];

                $any_fail = $this->put_flags(new Request([
                    'inline'                => $_r->input('inline') ?? $flags['inline'],
                    'synproxy'              => $_r->input('synproxy') ?? $flags['synproxy'],
                    'filters'               => $filters,
                    'loose'                 => $_r->input('loose') ?? $flags['loose'],
                    'heuristics'            => $_r->input('heuristics_level') ?? $flags['heuristics_level'],
                    'block_tcp'             => $_r->input('block_tcp') ?? $flags['block_tcp'],
                    'block_udp'             => $_r->input('block_udp') ?? $flags['block_udp'],
                    'block_fragments'       => $_r->input('block_fragments') ?? $flags['block_fragments'],
                    'syn_cookies'           => $_r->input('syn_cookies') ?? $flags['syn_cookies'],
                    'drop_tcp_out_of_state' => $_r->input('drop_tcp_out_of_state') ?? $flags['drop_tcp_out_of_state'],
                    'app_filter_strict'     => $_r->input('app_filter_strict') ?? $flags['app_filter_strict'],
                    'syn_cookie_data_promote'   => $flags['syn_cookie_data_promote'],
                    'syn_cookie_os_gate'        => $flags['syn_cookie_os_gate'],
                    'syn_challenge'             => $flags['syn_challenge'],
                    'syn_drop_first'            => $flags['syn_drop_first'],
                    'src_limit_pps'             => $flags['src_limit_pps'],
                    'src_limit_mbps'            => $flags['src_limit_mbps'],
                    'drop_unexpected_protocols' => $flags['drop_unexpected_protocols'],
                    'drop_non_core_protocols'   => $flags['drop_non_core_protocols'],
                    'drop_udp_to_tcp_ports'     => $flags['drop_udp_to_tcp_ports'],
                    'udp_established_lane'      => $flags['udp_established_lane']
                ]), $x)->status() !== 200;

                if($any_fail) {
                    break;
                }

            }

        }

        if($any_fail) {
            Db::rollBack();

            return response()->json(['detail' => 'BULK OPERATION FAILED'], 500);
        }
        else {
            Db::commit();

            return response()->json(['detail' => 'PREFIXES UPDATED'], 200);
        }


    }
}
