<?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 Default
use App\Models\Asns;

use App\Http\Controllers\Network\NetworkController;

// Mitigation
use App\Models\Prefixes;
use App\Models\PrefixCommunity;

// Mitigation
use App\Models\Goliath\GoliathAppliance;
use App\Models\Goliath\GoliathAttack;
use App\Models\Goliath\GoliathRegion;
use App\Models\Goliath\GoliathSubregion;
use App\Models\Goliath\PrefixGoliathFirewall;
use App\Models\Goliath\PrefixGoliathFlag;
use App\Models\Goliath\PrefixGoliathInline;
use App\Models\Goliath\GoliathAsnAttributes;

use App\Rules\Bpf;

use App\Helpers\ChartHandler;
use App\Helpers\ClickhouseHandler;
use App\Helpers\Utility\GoliathAttacks;
use App\Models\Goliath\GoliathHistory;
use App\Models\Goliath\GoliathOrganisationSpecial;
use App\Models\Goliath\GoliathRuleset;
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\AllowedPrefixIdentity;
use App\Rules\ExclusiveWith;
use App\Rules\Fragments\Regionality;
use App\Rules\GameFilters;
use App\Rules\HeuristicsLevel;
use App\Rules\Interval;
use App\Rules\OwnedAsnIdentity;
use App\Rules\OwnedRulesetIdentity;
use App\Rules\RegionIdentity;
use App\Rules\RulesetActionType;
use App\Rules\RulesetClauseOperator;
use App\Rules\RulesetClauseType;
use App\Rules\RulesetIdentity;
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;

class GoliathController extends NetworkController
{
    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);
    }

    // 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) {

        if($_network_id == null || Prefixes::getOrganisationHasPrefix(Auth::user()->organisation_id, $_network_id)) {
            return $this->paged_table($_r, function() use($_network_id, $_chain_id) {
                
                $prefixes_relevant = Prefixes::getOrganisationPrefixes(Auth::user()->organisation_id)->pluck('id');

                $query = Prefixes::select(
                        'prefixes.organisation_id',
                        'prefixes.id',
                        'asn',
                        'name',
                        'peer',
                        'owner',
                        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',
                        'still_announced',

                        // Special little area for subtenancies
                        ...(
                            Auth::user()->canDo(['network.subtenant']) ? [
                                DB::raw('(SELECT string_agg(DISTINCT organisations.name, \',\') FROM supernets INNER JOIN prefix_subtenants ON supernet_id = prefix_subtenants.prefix_id INNER JOIN organisations ON organisations.id = prefix_subtenants.subtenant_id WHERE supernets.subnet_id = prefixes.id AND prefix_subtenants.deleted_at IS NULL) AS "indirect_subtenants"'),
                                DB::raw('(SELECT string_agg(DISTINCT organisations.name, \',\') FROM prefix_subtenants INNER JOIN organisations ON organisations.id = prefix_subtenants.subtenant_id WHERE prefix_subtenants.prefix_id = prefixes.id AND prefix_subtenants.deleted_at IS NULL) AS "direct_subtenants"')
                            ] : []
                        )
                    )
                    ->leftJoin('asns', 'asn_id', '=', 'asns.id')
                    ->leftJoin('prefix_asn_peers', 'prefix_asn_peers.prefix_id', '=', 'prefixes.id')
                    ->whereNull('prefixes.deleted_at')
                    ->whereRaw('family(address) = 4')
                    ->whereIn('prefixes.id', $prefixes_relevant);

                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);
                }

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

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

                // Subtenancy permission specific
                ...(
                    Auth::user()->canDo(['network.subtenant']) ? [
                        'subtenants'    => [ 'db' => DB::raw("CONCAT(
                            (SELECT string_agg(DISTINCT organisations.name, ',') FROM supernets INNER JOIN prefix_subtenants ON supernet_id = prefix_subtenants.prefix_id INNER JOIN organisations ON organisations.id = prefix_subtenants.subtenant_id WHERE supernets.subnet_id = prefixes.id AND prefix_subtenants.deleted_at IS NULL),
                            CONCAT(' ', (SELECT string_agg(DISTINCT organisations.name, ',') FROM prefix_subtenants INNER JOIN organisations ON organisations.id = prefix_subtenants.subtenant_id WHERE prefix_subtenants.prefix_id = prefixes.id AND prefix_subtenants.deleted_at IS NULL))
                        )"), '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('goliath_regions', 'goliath_regions.id', '=', 'prefix_goliath_flags.region_id')
                    ->leftJoin('goliath_subregions', 'goliath_subregions.id', '=', 'prefix_goliath_flags.subregion_id')
                    ->leftJoin('prefixes', 'prefixes.id', '=', 'prefix_goliath_flags.prefix_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();

                $parent_announced = Prefixes::leftJoin('supernets', 'supernet_id', '=', 'prefixes.id')
                    ->whereIn('subnet_id', $data->pluck('id'))
                    ->where('still_announced', '=', 1)
                    ->select('subnet_id')
                    ->distinct()
                    ->get()
                    ->keyBy('subnet_id');

                $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;
                    }
                }

                // Get a list of valid subtenants that the users can see - This is protective in the case that something is assigned when it should not be. 
                $valid_subtenants = collect(DB::select("
                    WITH RECURSIVE org_tree AS (
                        -- Base case: direct children
                        SELECT o.id, o.name, s.parent_id, 1 as level
                        FROM organisations o
                        JOIN suborganisations s ON s.child_id = o.id
                        WHERE s.parent_id = :parent_id
                        
                        UNION ALL
                        
                        -- Recursive case: children of children
                        SELECT o.id, o.name, s.parent_id, ot.level + 1
                        FROM organisations o
                        JOIN suborganisations s ON s.child_id = o.id
                        JOIN org_tree ot ON ot.id = s.parent_id
                    )
                    SELECT name
                    FROM org_tree
                    ORDER BY level, name
                ", ['parent_id' => Auth::user()->organisation_id]))->keyBy('name');

                $data->transform(function($row) use($flag_lookup, $inline_lookup, $parent_inline_lookup, $parent_announced, $valid_subtenants, $loose_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;

                    // Subtenancy display stuff
                    if(Auth::user()->canDo(['network.subtenant'])) {
                        $subtenant_indirect = [];
                        $subtenant_direct = [];

                        foreach($row['indirect_subtenants'] === null ? [] : array_reverse(explode(',', $row['indirect_subtenants'])) as $value) {
                            if(isset($valid_subtenants[$value])) {
                                $subtenant_indirect[] = $value;
                            }
                        }

                        foreach($row['direct_subtenants'] === null ? [] : array_reverse(explode(',', $row['direct_subtenants'])) as $value) {
                            if(isset($valid_subtenants[$value])) {
                                $subtenant_direct[] = $value;
                            }
                        }

                        $row['subtenants'] = $subtenant_direct === [] && $subtenant_indirect === [] ? null : [
                            'indirect' => $subtenant_indirect,
                            'direct' => $subtenant_direct
                        ];

                        unset($row['indirect_subtenants']);
                        unset($row['direct_subtenants']);
                    }

                    if($row['still_announced'] == 0 && isset($parent_announced[$row['id']])) {
                        $row['still_announced'] = 1;
                    }

                    switch($row['created_source']) {
                        case 'goliath-attack':
                            $row['source'] = 'attack';
                            break;
                        case 'user':
                            $row['source'] = 'user';
                            break;
                        default:
                            $row['source'] = 'import';
                    }

                    unset($row['created_source']);

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

                    return $row;
                });

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

    /**
     *  DESCRIPTION: Determines all the rulesets a user can be shown.
     * 
     *  PRIMARY EXECUTION PATHS:
     * 
     *      - Return a list of rulesets that are either (universal AND owned by GSL) OR they are just owned by the user IF a prefix ID is not provided.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRetrieveListOfRulesets()
     * 
     *      - Return a list of rulesets relevant to a given prefix IF a prefix ID is provided.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRetrieveListOfRulesets()
     * 
     *  CRITICAL SECURITY AND LOGICAL CONSTRAINTS:
     *      
     *      - Only rulesets that are owned by the user, or universal AND owned by GSL are visible - OR They are assigned to a prefix the user CAN look at, and you are looking at that prefix.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRetrieveListOfRulesets()
     * 
     *      - A list of prefixes with specific regionality will display
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRetrieveListOfRulesets()
     */

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

        if($_prefix_id === null || Prefixes::getOrganisationHasPrefix(Auth::user()->organisation_id, $_prefix_id)) {
            return $this->paged_table($_r, function() use($_prefix_id) {

                $query = null;
    
                $to_select = [
                    'goliath_rulesets.id',
                    'goliath_rulesets.name',
                    'type',
                    'rules',
                    'goliath_rulesets.organisation_id'
                ];
    
                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')
                        ->whereNull('prefix_goliath_firewall.deleted_at')
                        ->whereNull('prefixes.deleted_at')
                        ->where(function($x) {
                            $x->where('prefixes.organisation_id', '=', Auth::user()->organisation_id)->orWhereIn('prefixes.id', Prefixes::getSubtenancyPrefixes(Auth::user()->organisation_id)->pluck('id'));
                        })
                        ->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::where(function($q) {
                            $q->where(function($q2) {
                                $q2->where('type', '=', RulesetType::Universal)->where('goliath_rulesets.organisation_id', '=', 1);
                            })->orWhere('goliath_rulesets.organisation_id', '=', Auth::user()->organisation_id);
                        })->select($to_select)->addSelect([
                        'prefix_count' => PrefixGoliathFirewall::leftJoin('prefixes', 'prefixes.id', '=', 'prefix_id')
                            ->where('organisation_id', '=', Auth::user()->organisation_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 ],
                '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);
    }

    /**
     *  DESCRIPTION: Returns information relevant for a given ruleset.
     * 
     *  PRIMARY EXECUTION PATHS:
     * 
     *      - Return the specific ruleset associated with a chain ID.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRetrieveListOfRulesets()
     * 
     *      - Return the regionality associated with the chain when a prefix is provided, if there is any.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRetrieveListOfRulesets()
     * 
     *  CRITICAL SECURITY AND LOGICAL CONSTRAINTS:
     *      
     *      - A ruleset is only returned when it is owned by the user, OR it is owned by GSL AND Universal.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRetrieveListOfRulesets()
     * 
     *      - Can edit functionality is correct.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRetrieveListOfRulesets()
     * 
     *      - If a prefix is provided, you must make sure the correct regionality IS provided.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRetrieveListOfRulesets()
     * 
     *      - If a prefix is provided, the identity must be known to the user organisation. Otherwise it must fail.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRetrieveListOfRulesets()
     */

    public function get_ruleset(Request $_r, int $_chain_id) {
        
        $valid_request = !Validator::make([ 
            ...$_r->all(), ...[ 'chain_id' => $_chain_id ]
        ], [
            'chain_id'  => [ new RulesetIdentity ],
            'prefix_id' => [ new AllowedPrefixIdentity ]
        ])->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)
            ->where(function($q) {
                $q->where(function($q2) {
                    $q2->where('type', '=', RulesetType::Universal)->where('goliath_rulesets.organisation_id', '=', 1);
                })->orWhere('goliath_rulesets.organisation_id', '=', Auth::user()->organisation_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);
    }

    /**
     *  DESCRIPTION: Returns ruleset options.
     * 
     *  PRIMARY EXECUTION PATHS:
     * 
     *      - Return the rulesets available to a given user, alongside regions and subregions.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRetrieveListOfRulesetOptions()
     * 
     *      - When a prefix ID is provided, also provide information about whether a global rule is set and currently set regions.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRetrieveListOfRulesetOptions()
     * 
     *  CRITICAL SECURITY AND LOGICAL CONSTRAINTS:
     *      
     *      - If a prefix ID is provided, the user must have some ownership of said prefix.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRetrieveListOfRulesetOptions()
     * 
     */

    public function get_ruleset_options(Request $_r) {

        // Check whether or not the user owns the prefix.
        $valid_request = Validator::make( array_merge([
            'prefix_id'     => $_r->input('prefix_id')
        ], $_r->all()), [
            'prefix_id'     => [ 'nullable', new AllowedPrefixIdentity ],
        ]);

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

        // Load standard options for everything
        $chains = GoliathRuleset::where(function($q) { $q->where([['type', '=', RulesetType::Universal], ['organisation_id', '=', 1]]); })->orWhere('organisation_id', '=', Auth::user()->organisation_id)->select(
            'id',
            'name',
            'type'
        )->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') && ( 
            $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);
    }

    /**
     *  DESCRIPTION: Returns a list of prefixes associated with a given ruleset.
     * 
     *  PRIMARY EXECUTION PATHS:
     * 
     *      - Return a list of prefixes that should be visible to a given user, based upon a supplied ruleset ID.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRetrieveRulesetPrefixes()
     *
     *  CRITICAL SECURITY AND LOGICAL CONSTRAINTS:
     *      
     *      - The chain ID need be universal and owned by organisation one, OR owned by the current user.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRetrieveRulesetPrefixes()
     * 
     */

    public function get_ruleset_prefixes(Request $_r, int $_chain_id) {
        try {
            $valid_chain = GoliathRuleset::where('id', '=', $_chain_id)
                ->where(function($q) { 
                    $q->where([['type', '=', RulesetType::Universal], ['goliath_rulesets.organisation_id', '=', 1]])
                        ->orWhere('goliath_rulesets.organisation_id', '=', Auth::user()->organisation_id);
                })
                ->count();

            if($valid_chain) {
                $prefixes = PrefixGoliathFirewall::leftJoin('prefixes', 'prefix_goliath_firewall.prefix_id', '=', 'prefixes.id')
                    ->where(function($q) {
                        $q->where('prefixes.organisation_id', '=', Auth::user()->organisation_id)->orWhereIn('prefixes.id', Prefixes::getSubtenancyPrefixes(Auth::user()->organisation_id)->pluck('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);
            }

            return response()->json(['detail' => __('api.409_NOT_PERMITTED')], 409);
        }
        catch(\Exception $ex) {}

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

    /**
     *  DESCRIPTION: Controls whether or not a ruleset is being created or modified.
     * 
     *  PRIMARY EXECUTION PATHS:
     * 
     *      - Create a new ruleset: Occurs when no chain ID is provided.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testCreateRuleset()
     * 
     *      - Modify an existing ruleset: Occurs when you supply a chain ID for an object you own.
     *          - TEST COVERAGE: 
     *              + Feature/RulesetControllerTest -> testModifyRuleset()
     * 
     *      - Fork an existing ruleset: Occurs when you supply a chain ID for an object you do not own, and GSL owns it.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testModifyRuleset()
     * 
     *  SECONDARY EXECUTION PATHS:
     * 
     *      - Ruleset applied to a prefix: Occurs when a `prefix_id` parameter is provided.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testCreateAssignRuleset()
     *              + Feature/RulesetControllerTest -> testCreateAssignRegionalRuleset()
     * 
     *  CRITICAL SECURITY AND LOGICAL CONSTRAINTS:
     *      
     *      - The supplied chain ID need correlate with an owned chain ID, or a universal chain.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testModifyRuleset()
     *              + Feature/RulesetControllerTest -> testCreateAssignRuleset()
     *              + Feature/RulesetControllerTest -> testCreateAssignRegionalRuleset()
     * 
     *      - Created chains must have exclusive ownership to the end user
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testModifyRuleset()
     *              + Feature/RulesetControllerTest -> testCreateAssignRuleset()
     *              + Feature/RulesetControllerTest -> testCreateAssignRegionalRuleset()
     * 
     *      - The supplied prefix ID, if supplied, must correlate with an owned prefix.
     *          - TEST COVERAGE:
     *              + DELEGATED TO self->put_prefix_ruleset()
     * 
     */

    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 AllowedPrefixIdentity ],
            'chain_id'                  => [ 'nullable', new RulesetIdentity ],
            
            'name'                      => ['required', 'string', 'max:255', SafeName::make(SafeName::CHARSET_NO_HASH)],

            '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 RulesetIdentity],

            '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 RulesetIdentity],
            '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);

            if($existing->organisation_id === Auth::user()->organisation_id) { // We own it
                $ruleset_object = $existing;
                $ruleset_object->name = $_r->input('name');
                $ruleset_object->unencoded_rules = $_r->input('unencoded');
            }
            else { // We do not
                $ruleset_object = new GoliathRuleset([
                    'name'              => $_r->input('name'),
                    'type'              => RulesetType::Reusable->value,
                    'organisation_id'   => Auth::user()->organisation_id,
                    '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);
    }

    /**
     *  DESCRIPTION: Handles the deletion of a ruleset.
     * 
     *  PRIMARY EXECUTION PATHS:
     * 
     *      - Remove ruleset with no attached prefix.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRemoveRuleset()
     * 
     *      - Remove ruleset with attached prefixes and no reassignment.
     *          - TEST COVERAGE: 
     *              + Feature/RulesetControllerTest -> testRemoveRuleset()
     * 
     *      - Remove ruleset with attached prefixes and reassignment.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRemoveReassignRuleset()
     *              + Feature/RulesetControllerTest -> testRemoveRulesetReassignGlobal()
     *              + Feature/RulesetControllerTest -> testRemoveRulesetPrefixAssocation()
     * 
     *  CRITICAL SECURITY AND LOGICAL CONSTRAINTS:
     *      
     *      - The supplied reassignment chain ID need correlate with an owned chain ID, or a universal chain.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRemoveReassignRuleset()
     * 
     *      - The supplied chain ID must be owned by the user.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRemoveRuleset()
     * 
     *      - Firewall entries themselves should only have the ruleset set to null on ruleset deletion.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRemoveRuleset()
     * 
     *      - When a ruleset is removed, any prefix associated with that ruleset, if it has regional rulesets, must have those regional rulesets removed.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testRemoveRulesetGlobal()
     *              + Feature/RulesetControllerTest -> testRemoveRulesetPrefixAssocation()
     * 
     */
    
    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 OwnedRulesetIdentity ],
            'reassign_chain_id'     => [ new RulesetIdentity ]
        ])->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)->where('organisation_id', Auth::user()->organisation_id)->delete();

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

    /**
     *  DESCRIPTION: Handles the dettachment of a ruleset from a prefix.  @@@ NO COVERAGE
     * 
     *  PRIMARY EXECUTION PATHS:
     * 
     *      - Detach global ruleset.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testDeletePrefixRuleset()
     * 
     *      - Detach regional ruleset.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testDeletePrefixRuleset()
     * 
     *  CRITICAL SECURITY AND LOGICAL CONSTRAINTS:
     *      
     *      - The user owns the prefix they're trying to delete the ruleset on, or it is subtenanted to them.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testDeletePrefixRuleset()
     * 
     *      - Detaching a global ruleset eliminates regional rulesets.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testDeletePrefixRuleset()
     * 
     */

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

        $valid_request = !Validator::make( array_merge([
            'prefix_id'         => $_prefix_id
        ], $_r->all()), [
            'prefix_id'         => [ new AllowedPrefixIdentity ],
            ...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);
    }

    /**
     *  DESCRIPTION: Handles the attachment of a ruleset to a prefix.
     * 
     *  PRIMARY EXECUTION PATHS:
     * 
     *      - Set global chain.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testAttachChain()
     * 
     *      - Set regional chain.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testAttachRegionalChain()
     *              + Feature/RulesetControllerTest -> testAttachRegionalDuplicateChain()
     * 
     *      - Duplicate existing ruleset
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testAttachDuplicateChain()
     *              + Feature/RulesetControllerTest -> testAttachRegionalDuplicateChain()
     * 
     *  CRITICAL SECURITY AND LOGICAL CONSTRAINTS:
     *      
     *      - The user owns the prefix they're trying to add the ruleset to.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testAttachChain()
     *              + Feature/RulesetControllerTest -> testAttachDuplicateChain()
     *              + Feature/RulesetControllerTest -> testAttachRegionalChain()
     *              + Feature/RulesetControllerTest -> testAttachRegionalDuplicateChain()
     *              + Feature/RulesetControllerTest -> testAttachFlaglessChain()
     * 
     *      - The user owns the ruleset or it is universal.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testAttachChain()
     *              + Feature/RulesetControllerTest -> testAttachDuplicateChain()
     *              + Feature/RulesetControllerTest -> testAttachRegionalChain()
     *              + Feature/RulesetControllerTest -> testAttachRegionalDuplicateChain()
     *              + Feature/RulesetControllerTest -> testAttachFlaglessChain()
     * 
     *      - Regional rulesets cannot be set without a global.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> 
     * 
     *      - If a prefix lacks a flags entry, give it a H2 flags entry.
     *          - TEST COVERAGE:
     *              + Feature/RulesetControllerTest -> testAttachFlaglessChain()
     * 
     */

    public function put_prefix_ruleset(Request $_r, int $_prefix_id) {
        
        $valid_request = Validator::make( array_merge([
            'prefix_id' => $_prefix_id
        ], $_r->all()), [

            'prefix_id'                 => [ 'required', new AllowedPrefixIdentity ],
            'chain_id'                  => [ 'required', new RulesetIdentity ],
            
            '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);
    }

    /**
     *  DESCRIPTION: Returns the flag information for a given prefix.
     * 
     *  PRIMARY EXECUTION PATHS:
     * 
     *      - Return the flag information for a users prefix.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testGetFlags()
     * 
     *  CRITICAL SECURITY AND LOGICAL CONSTRAINTS:
     *      
     *      - The user can only access a prefix they own or have subtenanted to them.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testGetFlags()
     *
     */

    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 AllowedPrefixIdentity ],
            ...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,
            'filters' => $flags === null ? null : json_decode($flags->flag_filters),
            'block_tcp' => boolval($flags->flag_block_tcp ?? false),
            'block_udp' => boolval($flags->flag_block_udp ?? false),
            '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);
    }

    /**
     *  DESCRIPTION: Modify flag information for a given prefix.
     * 
     *  PRIMARY EXECUTION PATHS:
     * 
     *      - The user is able to modify their flags.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPutFlags()
     * 
     * 
     *  CRITICAL SECURITY AND LOGICAL CONSTRAINTS:
     *      
     *      - The user can only access a prefix they own or have subtenanted to them.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPutFlags()
     *
     *      - If FiveM is true, S-On H-X should be enforced.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPutFlags()
     * 
     *      - If the prefix is from Mercury, inline should be enforced.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPutFlags()
     *
     *      - If the prefix is from Mercury, inline should be enforced.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPutFlags()
     */

    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 AllowedPrefixIdentity],
            'inline'            => ['required', 'boolean'],
            'synproxy'          => ['required', 'boolean'],
            'loose'             => ['required', 'boolean'],
            'heuristics'        => ['required', new HeuristicsLevel],
            ...Regionality::full(),
            'filters'                   => ['nullable', new GameFilters],
            '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'),
            'subregion_id' => $_r->input('subregion_id'),
            'metro_id' => $_r->input('metro_id')
        ]);

        $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
        ]);

        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);
    }

    /**
     *  DESCRIPTION: Allow the creation of a new prefix with flag information.
     * 
     *  PRIMARY EXECUTION PATHS:
     * 
     *      - The user is able to create a new prefix with new flags.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPostSubnet()
     * 
     *  CRITICAL SECURITY AND LOGICAL CONSTRAINTS:
     *      
     *      - The user can only create a subnet under a prefix they own or have subtenanted to them.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPostSubnet()
     *
     *      - The function must backchain to put_flags() to decrease risk of errors or divergent behaviour.
     *          - NO COVERAGE REQUIRED - SELF EVIDENT.
     * 
     *      - Can only add prefixes for announced supernets.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPostSubnet()
     */

    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 GameFilters],
            'loose'                 => ['required', 'boolean'],
            'inline'                => ['required', 'boolean'],
            'heuristics'            => ['required', new HeuristicsLevel],
            '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', 3),
                '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");

        }, false);

    }

    // TO-DO: Feature Test Coverage, Re-implement
    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);
    }

    /**
     *  DESCRIPTION: Allow the retrieval of prefix default information for a given user.
     * 
     *  PRIMARY EXECUTION PATHS:
     * 
     *      - Defaults are currently set.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testGetDefaults()
     * 
     *      - Defaults are not currently set so default values are returned.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testGetDefaults()
     * 
     *  CRITICAL SECURITY AND LOGICAL CONSTRAINTS:
     *      
     *      - The user can only access an ASN they own.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testGetDefaults()
     */

    public function get_defaults(Request $_r, int $_asn_id) {
        $valid_request = !Validator::make( ['asn_id' => $_asn_id], ['asn_id' => ['required', new OwnedAsnIdentity(true)]] )->fails();

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

    /**
     *  DESCRIPTION: Allow the changing of defaults for a given ASN.
     * 
     *  PRIMARY EXECUTION PATHS:
     * 
     *      - Allow the setting of an existing default.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPutDefaults()
     * 
     *      - Allow the creation of a new default.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPutDefaults()
     * 
     *      - The override parameter induces an ASN wide change.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPutDefaults()
     * 
     *  CRITICAL SECURITY AND LOGICAL CONSTRAINTS:
     *      
     *      - The user can only access an ASN they own.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPutDefaults()
     * 
     *      - Overrides respect that level 2 and Mercury must be inline. We don't care about subnets here because they'll be forced to behave via their parent for Mercury.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPutDefaults()
     */

    // TO-DO: Feature Test Coverage
    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 OwnedAsnIdentity(true)],
            'synproxy' => ['required', 'boolean'],
            'heuristics' => ['required', new HeuristicsLevel],
            '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' => boolval($_r->input('heuristics')),
                    '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')
                ->leftJoin('users', 'users.id', '=', 'goliath_history.user_id')
                ->where('goliath_history.created_at', '>', Carbon::parse('2025-09-09 05:00:00'))
                ->where(function($q) {
                    $q->where('goliath_history.organisation_id', '=', Auth::user()->organisation_id)
                        ->orWhere('goliath_history_visibility.organisation_id', '=', Auth::user()->organisation_id);
                })
                ->select(
                    'goliath_history.id',
                    'goliath_history.created_at',
                    DB::raw('users.name as user'),
                    'type',
                    'history'
                );

        }, [ 
            'user'              => [ 'db' => 'users.name', 'searchable' => true ],
            'type'              => [ 'db' => 'type', 'searchable' => true ],
            'identifier'        => [ 'db' => DB::raw("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"), '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'];
                        break;
                    case 'Inline':
                        try {
                            $row->history = $history['Inline'];
                        }
                        catch(\Exception $ex) {
                            $row->history = ['error' => true];
                        }

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

                        $row->identifier = $history['Prefix'];
                        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'];
                        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'];
                        
                        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'];

                        break;
                }

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

            return $data;
        });

    }

    // TO-DO: Feature Test Coverage
    public function get_attack_summary(Request $_r) {
        // Get count of ongoing attacks
        $ongoing_count = GoliathAttack::where('goliath_attacks.updated_at', '>', Carbon::now()->subMinutes(2))
            ->where('organisation_id', '=', Auth::user()->organisation_id)
            ->leftJoin('goliath_attack_prefixes', 'attack_id', '=', 'goliath_attacks.id')
            ->leftJoin('prefixes', 'prefix_id', '=', 'prefixes.id')
            ->distinct('attack_id')
            ->count();

        // Get count blocked last 24 hours
        $last_day_blocked = GoliathAttack::where('goliath_attacks.updated_at', '>', Carbon::now()->subHours(24))
            ->where('organisation_id', '=', Auth::user()->organisation_id)
            ->leftJoin('goliath_attack_prefixes', 'attack_id', '=', 'goliath_attacks.id')
            ->leftJoin('prefixes', 'prefix_id', '=', 'prefixes.id')
            ->distinct('attack_id')
            ->count();

        // Get count blocked last week
        $last_week_blocked = GoliathAttack::where('goliath_attacks.updated_at', '>', Carbon::now()->subHours(168))
            ->where('organisation_id', '=', Auth::user()->organisation_id)
            ->leftJoin('goliath_attack_prefixes', 'attack_id', '=', 'goliath_attacks.id')
            ->leftJoin('prefixes', 'prefix_id', '=', 'prefixes.id')
            ->distinct('attack_id')
            ->count();

        // Get global attacks
        $global_count = GoliathAttack::where('goliath_attacks.updated_at', '>', Carbon::now()->subMinutes(2))->count();

        return response()->json([
            'global' => $global_count,
            'ongoing' => $ongoing_count,
            'blocked_daily' => $last_day_blocked,
            'blocked_weekly' => $last_week_blocked
        ], 200);
    }
    
    // TO-DO: Feature Test Coverage
    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($flag_status, $_prefix_id) {
            $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')
            ->leftJoin('goliath_attack_prefixes', 'attack_id', '=', 'goliath_attacks.id')
            ->leftJoin('prefixes', 'prefix_id', '=', 'prefixes.id')
            ->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'
            )
            ->where('goliath_attacks.updated_at', '>', Carbon::now()->subDays(30))
            ->whereIn('prefixes.id', Prefixes::getOrganisationPrefixes(Auth::user()->organisation_id)->pluck('id'))
            ->groupBy('goliath_attacks.id', 'goliath_metro.telemetry_identifier', 'goliath_subregions.telemetry_identifier', 'goliath_regions.telemetry_identifier');

            if($_prefix_id !== null) {
                $q = $q->where('goliath_attack_prefixes.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 ]
        ], 'goliath_attacks.created_at', null);
    }

    // TO-DO: Re-implement
    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::where('organisation_id', '=', Auth::user()->organisation_id)->get()->pluck('asn');

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

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

    // NOTE - DIFFICULT TO DO TEST COVERAGE, NO NATIVE BPF VALIDATION WITHOUT APPLIANCES.
    public function get_valid_bpf(Request $_r) {

        // Pass on the BPF rule to a forwarding agent for the appliances
        $validated = Validator::make( $_r->all(), [
            'rule'   => [ 'required', new Bpf ]
        ]);

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

    }

    /**
     *  DESCRIPTION: Allow the retrieval of default special flags.
     * 
     *  PRIMARY EXECUTION PATHS:
     * 
     *      - Allow the retrieval of default special flags.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testGetSpecial()
     * 
     *      - Allow the retrieval of default special flags.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testGetSpecial()
     * 
     */

    public function get_special(Request $_r) {
        
        $special = GoliathOrganisationSpecial::where('organisation_id', '=', Auth::user()->organisation_id)->first();
        
        return response()->json($special === null ? [
            'special' => false
        ] : [
            'special' => boolval($special->special)
        ], 200);
    }

    /**
     *  DESCRIPTION: Allow the changing of default special flags.
     * 
     *  PRIMARY EXECUTION PATHS:
     * 
     *      - Allow the setting of an existing default.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPutSpecial()
     * 
     *      - Allow the creation of a new default.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPutSpecial()
     * 
     *      - The override parameter induces organisation wide change.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPutSpecial()
     * 
     */

    public function put_special(Request $_r) {

        $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' => Auth::user()->organisation_id]);

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

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

            $organisation = Organisation::find(Auth::user()->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(Auth::user()->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);
    }

    /**
     *  DESCRIPTION: Allow the retrieval of prefix specific special flags.
     * 
     *  PRIMARY EXECUTION PATHS:
     * 
     *      - Allow the retrieval of an unset prefixes special status.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testGetSpecialPrefix()
     * 
     *      - Allow the retrieval of a set prefixes special status.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testGetSpecialPrefix()
     * 
     *      - Allow the retrieval of a flagless prefixes special status.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testGetSpecialPrefix()
     *
     *  CRITICAL SECURITY AND LOGICAL CONSTRAINTS:
     *      
     *      - The user can only access a prefix they own or have subtenanted to them.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testGetSpecialPrefix()
     *
     */

    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 AllowedPrefixIdentity ],
            ...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);
    }

    /**
     *  DESCRIPTION: Allow the changing of prefix specific special flags.
     * 
     *  PRIMARY EXECUTION PATHS:
     * 
     *      - Allow the modification of an unset prefixes special status.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPutSpecialPrefix()
     * 
     *      - Allow the modification of a set prefixes special status.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPutSpecialPrefix()
     * 
     *      - Allow the modification of a flagless prefixes special status.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPutSpecialPrefix()
     *
     *  CRITICAL SECURITY AND LOGICAL CONSTRAINTS:
     *      
     *      - The user can only access a prefix they own or have subtenanted to them.
     *          - TEST COVERAGE:
     *              + Feature/FlagControllerTest -> testPutSpecialPrefix()
     *
     */

    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 AllowedPrefixIdentity ],
            '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 bulk_update_flags(Request $_r, int $_prefix_id) {
        
        $valid_request = !Validator::make( array_merge([
            'prefix_id' => $_prefix_id,
        ], $_r->all()), [
            'prefix_id'             => [ 'required', new AllowedPrefixIdentity ],
            'inline'                => [ 'nullable', Rule::in([null, true, false])],
            'synproxy'              => [ 'nullable', Rule::in([null, true, false])],
            'filters'               => [ 'nullable', new GameFilters ],
            'loose'                 => [ 'nullable', Rule::in([null, true, false])],
            'heuristics_level'      => [ 'nullable', new HeuristicsLevel],
            '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);
        }

    }

    public function get_ruleset_validity(Request $_r) {
        
        // Validate this request in the first place.
        $valid_request = Validator::make( $_r->all(), [

            '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 RulesetIdentity],

            '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 RulesetIdentity],

        ]);

        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[] = [
                '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'         => 0,
            'y'         => 0,
            '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)
        ];

        $ruleset = new GoliathRuleset([
            'name'              => null,
            'type'              => RulesetType::Reusable->value,
            'organisation_id'   => Auth::user()->organisation_id,
            'unencoded_rules'   => null
        ]);
        
        foreach($rules as $rule) {
            $ruleset->addRule('', 0, 0, $rule['bpf'], $rule['action'], $rule['gotoId'], $rule['bpsLimit'], $rule['ppsLimit'], $rule['srcMask'], $rule['dstMask']);
        }

        $ruleset->addDefault(0, 0, $default_rule['action'], $default_rule['gotoId'], $default_rule['bpsLimit'], $default_rule['ppsLimit'], $default_rule['srcMask'], $default_rule['dstMask']);

        try {
            return response()->json( $ruleset->checkCompilationValidity(), 200);
        }
        catch(\Throwable $t) {
            return response()->json([ 'valid' => true, 'capacity' => 100 ], 500);
        }
    }
}
