<?php

namespace App\Http\Controllers\Network;

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

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

use Carbon\Carbon;

// Creatia Default
use App\Models\Organisation;
use App\Http\Controllers\BaseController;
use App\Helpers\Utility\UtilPrefix;

// Mitigation
use App\Models\Prefixes;
use App\Models\PrefixSubtenants;
use App\Models\Supernets;

use App\Helpers\ClickhouseHandler;
use App\Helpers\Utility\Arista;
use App\Models\BgpReroute;
use App\Notifications\SlackNotification;
use App\Rules\AllowedPrefixIdentity;
use App\Rules\Cidr;
use App\Rules\Fragments\PagedTable;
use App\Rules\SafeCidr;
use App\Rules\SubtenantedAddressV4;
use App\Rules\SubtenantedAddressV6;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;

class NetworkController extends BaseController
{
    const MODULE_PATH = '../app/';

    public function get_subtenants($_network_id) {
        $USER_ORGANISATION_ID = Auth::user()->organisation_id;

        // Wrap the entire thing in a try-catch to gracefully die.
        try {
            if(Prefixes::validOwner($_network_id, $USER_ORGANISATION_ID)) {
                // Get organisational subtenants and cast to an array.
                $options = 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 id as value, name as label
                    FROM org_tree
                    ORDER BY level, name
                ", ['parent_id' => $USER_ORGANISATION_ID]);

                $options = collect($options);
                
                // Get every subtenant for the resource
                $prefixOnly = PrefixSubtenants::leftJoin('organisations', 'subtenant_id', '=', 'organisations.id')->select('organisations.name as label', 'organisations.id as value', 'also_subnets')->where([['prefix_id', '=', $_network_id], ['also_subnets', '=', 0]])->get();
                $childrenAllowed = PrefixSubtenants::leftJoin('organisations', 'subtenant_id', '=', 'organisations.id')->select('organisations.name as label', 'organisations.id as value', 'also_subnets')->where([['prefix_id', '=', $_network_id], ['also_subnets', '=', 1]])->get();
                
                // Return it.
                return response()->json([
                    'data' => $options,
                    'selectedPrefixOnly' => $prefixOnly->toArray(),
                    'selectedChildrenAllowed' => $childrenAllowed->toArray()
                ], 200);
            }

        } catch(\Throwable $e) {}

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

    public function put_subtenant(Request $_r, $_network_id) {
        $USER_ORGANISATION_ID = Auth::user()->organisation_id;

        // Wrap the entire thing in a try-catch to gracefully die.
        try {
            
            // Get child organisations as options then cast to an array.
            $options = 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 id as value, name as label
                FROM org_tree
                ORDER BY level, name
            ", ['parent_id' => $USER_ORGANISATION_ID]);

            $options = collect($options)->pluck('value')->toArray();

            // Validate inputs
            $valid_request = !Validator::make([
                    'selectedPrefixOnly' => $_r->input('selectedPrefixOnly'),
                    'selectedChildrenAllowed' => $_r->input('selectedChildrenAllowed')
                ], [
                    'selectedPrefixOnly' => ['present', 'array'],
                    'selectedPrefixOnly.*' => [Rule::in(array_merge($options, [0]))],
                    'selectedChildrenAllowed' => ['present', 'array'],
                    'selectedChildrenAllowed.*' => [Rule::in(array_merge($options, [0]))]
            ])->fails();

            // If the request has a tenant that is valid, or is setting the tenant to zero.
            if($valid_request) {
                // Check if the requested network exists with the owner in mind, and whether the network is still announced OR it's just an inner.
                $requested_network = Prefixes::validOwner($_network_id, $USER_ORGANISATION_ID);
                $is_announced = $requested_network->still_announced == 1;
                $is_inner = Supernets::where('subnet_id', '=', $_network_id)->whereNotNull('supernet_id')->count();

                if($requested_network && ($is_announced || $is_inner)) {
                    // Begin transaction so we can keep this atomic
                    DB::beginTransaction();

                    // Erase subtenants so we can add in more
                    PrefixSubtenants::where('prefix_id', '=', $_network_id)->delete();

                    $new_subtenants = [];

                    foreach($_r->input('selectedPrefixOnly') as $x) {
                        $new_subtenants[] = [
                            'prefix_id' => $_network_id,
                            'subtenant_id' => $x,
                            'also_subnets' => 0,
                            'created_at' => Carbon::now(),
                            'updated_at' => Carbon::now()
                        ];
                    }

                    foreach($_r->input('selectedChildrenAllowed') as $x) {
                        $new_subtenants[] = [
                            'prefix_id' => $_network_id,
                            'subtenant_id' => $x,
                            'also_subnets' => 1,
                            'created_at' => Carbon::now(),
                            'updated_at' => Carbon::now()
                        ];
                    }

                    if($new_subtenants != []) {
                        DB::table('prefix_subtenants')->insert($new_subtenants);
                    }

                    // Commit transaction now that we're done.
                    DB::commit();

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

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

            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        } catch(\Throwable $e) {
            DB::rollback();
        }

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

    public function get_inside_subnet(Request $_r, $_network_id) {
        $USER_ORGANISATION_ID = Auth::user()->organisation_id;

        try {

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

            if($valid_request) {
                $valid_network = Prefixes::getOrganisationHasPrefix($USER_ORGANISATION_ID, $_network_id);

                if(Auth::user()->organisation_id === 1 && $valid_network === false) {
                    $valid_network = Prefixes::find($_network_id);
                } 

                if($valid_network) {
                    $network_cidr = $valid_network->getCidr();

                    // We need a different hook if it's a V6
                    if(str_contains($network_cidr, ':')) {
                        if(Prefixes::where([[ 'id', '=', $_network_id ], [ DB::raw("concat(address,'/',mask)::cidr"), '>>', $_r->input('cidr') ]])->count()) {
                            return response()->json(['detail' => __('api.200_GENERIC')], 200);
                        }
                    }
                    else {
                        $valid_subnet = UtilPrefix::valid_subnet($network_cidr, $_r->input('cidr'));
                        $inside_network_subnet = UtilPrefix::ip_in_block($_r->input('cidr'), $network_cidr);

                        if($valid_subnet && $inside_network_subnet && !UtilPrefix::ip_in_blocks($_r->input('cidr'), UtilPrefix::get_excluded_blocks())) {
                            return response()->json(['detail' => __('api.200_GENERIC')], 200);
                        }
                    }

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

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

            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        } catch(\Throwable $e) {}

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

    public function post_prefix(Request $_r, $_network_id, $_validation_shim, $_creation_shim, $_is_admin = false) {
        $USER_ORGANISATION_ID = Auth::user()->organisation_id ?? 0;
        
        try {
            $valid_request = !Validator::make(array_merge([
                'cidr' => $_r->input('cidr'),
                'name' => $_r->input('name'),
            ], $_validation_shim['input']), array_merge([
                'cidr' => ['required', Cidr::make(0)],
                'name' => ['string', 'nullable']
            ], $_validation_shim['rule']))->fails();
            
            if($valid_request) {
                $parent_network = Prefixes::find($_network_id);

                $is_rightful_editor = Prefixes::getOrganisationHasPrefix($USER_ORGANISATION_ID, $_network_id) || $_is_admin;
                $is_parent_announced = Prefixes::getIsInAnnouncedSupernet($_network_id) || $_is_admin;

                if($is_rightful_editor && $is_parent_announced) {
                    $network_cidr = $parent_network->getCidr();

                    $should_create = false;

                    if(str_contains($network_cidr, ':')) {
                        if(Prefixes::where([[ 'id', '=', $_network_id ], [ DB::raw("concat(address,'/',mask)::cidr"), '>>', $_r->input('cidr') ]])->count()) {
                            $should_create = true;
                        }
                    }
                    else {
                        $valid_subnet = UtilPrefix::valid_subnet($network_cidr, $_r->input('cidr'));
                        $inside_network_subnet = UtilPrefix::ip_in_block($_r->input('cidr'), $network_cidr);
                        

                        if($valid_subnet && $inside_network_subnet && !UtilPrefix::ip_in_blocks($_r->input('cidr'), UtilPrefix::get_excluded_blocks())) {
                            \Log::debug("Branch A");
                            $should_create = true;
                        }
                        else if($valid_subnet && $inside_network_subnet && $USER_ORGANISATION_ID == 1) {
                            \Log::debug("Branch B");
                            $should_create = true;
                        }
                    }
        
                    // Create the network and attach it to the parent subnet.
                    if($should_create) {
                        [$prefix, $bits] = explode('/', $_r->input('cidr'));

                        // Make sure the prefix does not already exist...
                        $is_existing = Prefixes::where([['address', '=', $prefix], ['mask', '=', $bits]])->count();

                        if($is_existing == 0) {

                            // ...and that it does not land inside a carve-out somebody else holds beneath the parent. Only an admin may say so explicitly.
                            $contesting = Prefixes::getContestingPrefix("$prefix/$bits", $parent_network->mask, $parent_network->organisation_id);

                            if($contesting !== false) {

                                if(!$_is_admin || !$_r->boolean('override')) {
                                    return response()->json(['detail' => __('api.422_CONTESTED_RANGE')], 422);
                                }

                                \Log::warning("Prefix $prefix/$bits placed inside contested range $contesting->address/$contesting->mask (#$contesting->id, Organisation #$contesting->organisation_id) by override from Organisation #$USER_ORGANISATION_ID.");
                            }

                            DB::beginTransaction();

                            $new_network = Prefixes::create([
                                'name' => $_r->input('name'),
                                'address' => $prefix,
                                'mask' => $bits,
                                'created_source' => 'user',
                                'organisation_id' => $parent_network->organisation_id,
                                'asn_id' => $parent_network->asn_id,
                            ]);
        
                            DB::table('supernets')->insert([[
                                    'created_at' => Carbon::now(),
                                    'updated_at' => Carbon::now(),
                                    'tenant_id' => $parent_network->organisation_id,
                                    'supernet_id' => $_network_id,
                                    'subnet_id' => $new_network->id
                                ], [
                                    'created_at' => Carbon::now(),
                                    'updated_at' => Carbon::now(),
                                    'tenant_id' => $parent_network->organisation_id,
                                    'supernet_id' => null,
                                    'subnet_id' => $new_network->id
                                ]
                            ]);

                            if($_creation_shim != null) {
                                $_creation_shim($new_network, $parent_network, $_r);
                            }
    
                            DB::commit();
    
                            return response()->json(['detail' => __('api.200_GENERIC')], 200);
                        }
                    }

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

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

            return response()->json(['detail' => __('api.400_INVALID_PAYLOAD')], 400);
        } catch(\Throwable $e) {
            DB::rollback();
        }

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

    public function get_portnames(Request $_r) {
        if(sha1($_r->input('key')) === env('RANDOM_SHA')) {
            try {
                $chh = new ClickhouseHandler(
                    config('creatia.clickhouse.database'),
                    config('creatia.clickhouse.endpoint'),
                    config('creatia.clickhouse.port'),
                    config('creatia.clickhouse.user'),
                    config('creatia.clickhouse.password')
                );
    
                $data = $chh->selectUnpaged("SELECT
                        hostname,
                        interfaces.interface_name,
                        interface_description,
                        interface_operstatus,
                        interface_speed
                    FROM
                    (
                        SELECT
                            source,
                            interface_name,
                            argMax(value, timestamp) AS interface_description
                        FROM interface_other
                        WHERE (key = 'description') AND (interface_name != '')
                        GROUP BY
                            source,
                            interface_name
                    ) AS interfaces
                    LEFT JOIN
                    (
                        SELECT
                            source,
                            argMax(description, timestamp) AS hostname
                        FROM component_description
                        WHERE component_name = 'hostname'
                        GROUP BY source
                    ) AS hostnames ON interfaces.source = hostnames.source
                    LEFT JOIN
                    (
                        SELECT
                            source,
                            interface_name,
                            argMax(value, timestamp) AS interface_operstatus
                        FROM interface_other
                        WHERE (key = 'oper-status')
                        GROUP BY
                            source,
                            interface_name
                    ) AS operstatus ON (interfaces.source = operstatus.source) AND (interfaces.interface_name = operstatus.interface_name)
                    LEFT JOIN
                    (
                        SELECT
                            source,
                            interface_name,
                            argMax(value, timestamp) AS interface_speed
                        FROM interface_other
                        WHERE (key = 'speed')
                        GROUP BY
                            source,
                            interface_name
                    ) AS intspeed ON (interfaces.source = intspeed.source) AND (interfaces.interface_name = intspeed.interface_name)
                    WHERE (interface_description ILIKE 'Cust:%') AND interface_description NOT LIKE '%[SID:GSL-%' AND (interface_operstatus = 'UP' OR interface_operstatus = '')
                    ORDER BY hostname,interfaces.interface_name DESC")->get();
    
                return response()->json($data, 500);
            }
            catch(\Throwable $ex) {
                Log::debug($ex);
            }
        }

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

    public function get_prefix_details(Request $_r, $_prefix_id) {

        if(($prefix = Prefixes::getOrganisationHasPrefix(Auth::user()->organisation_id, $_prefix_id)) && $prefix !== false) {

            // Retrieve all prefix details - Communities, subtenants, ASNs
            $prefix_details = Prefixes::select(
                    'asns.asn as asn',
                    'peer',
                    'owner',
                    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 string_agg(community, \',\') from prefix_community where prefix_id = prefixes.id and deleted_at is null) as "community"'),
                    'created_source',
                    'still_announced',
                    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', 'prefixes.asn_id', '=', 'asns.id')
                ->leftJoin('prefix_asn_peers', 'prefix_asn_peers.prefix_id', '=', 'prefixes.id')
                ->whereNull('prefixes.deleted_at')
                ->where('prefixes.id', '=', $_prefix_id)
                ->first() ?? [];

            $supernets = Prefixes::join('supernets', 'supernet_id', '=', 'prefixes.id')->where('subnet_id', '=', $_prefix_id)->select(
                'prefixes.id as id',
                DB::raw('CONCAT(address, \'/\', mask) as prefix')
            )
            ->orderBy('prefix', 'desc')->get()->filter(function($supernet) {
                return Prefixes::getOrganisationHasPrefix(Auth::user()->organisation_id, $supernet->id);
            });

            $subtenants = [];

            if(Auth::user()->canDo(['network.subtenant'])) {
                $exploded_direct = $prefix_details->direct_subtenants == null ? [] : explode(',', $prefix_details->direct_subtenants);
                $exploded_indirect = $prefix_details->indirect_subtenants == null ? [] : explode(',', $prefix_details->indirect_subtenants);

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

                foreach($exploded_direct as $x) {
                    if(isset($valid_subtenants[$x])) {
                        $subtenants[] = $x;
                    }
                }

                foreach($exploded_indirect as $x) {
                    if(isset($valid_subtenants[$x])) {
                        if(array_search($x, $exploded_direct) === false) {
                            $subtenants[] = "$x (Indirect)";
                        }
                    }
                }
            }

            return response()->json([
                'prefix'        => $prefix->getCidr(),
                'announced'     => $prefix->still_announced == 1 ? true : Prefixes::getIsInAnnouncedSupernet($_prefix_id),

                'parents'       => $supernets,

                ...($prefix_details !== [] ? [
                    'communities'           => explode(',', $prefix_details->community),
                    
                    'asn_peer'              => $prefix_details->peer ?? $prefix_details->asn,
                    'asn_owner'             => $prefix_details->owner ?? '-'
                ] : []),

                ...(Auth::user()->canDo(['network.subtenant']) ? [
                    'subtenants' => $subtenants
                ] : [])
            ], 200);
        }

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

    public function get_prefix_options(Request $_r) {
        $prefixes = Prefixes::select(DB::raw("CONCAT(address, '/', mask) as prefix"))->whereNull('deleted_at')->where('organisation_id', '=', Auth::user()->organisation_id)->get()->pluck('prefix');

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

    public function rename_prefix(Request $_r, $_prefix_id) {

        $valid_request = !Validator::make( array_merge([
            'prefix_id' => $_prefix_id,
        ], $_r->all()), [
            'prefix_id'         => [ 'required', new AllowedPrefixIdentity ],
            'name'              => [ 'string' ],
        ])->fails();

        if(!$valid_request) {
            return response()->json([], 400);
        }

        $edit_prefix = Prefixes::find($_prefix_id);

        $edit_prefix->name = $_r->input('name', null);

        $edit_prefix->save();

        return response()->json(['SUCCESS'], 200);
    }

    public function delete_prefix(Request $_r, $_prefix_id) {

        $valid_request = !Validator::make( array_merge([
            'prefix_id' => $_prefix_id,
        ], $_r->all()), [
            'prefix_id' => [
                'required',
                new AllowedPrefixIdentity
            ]
        ])->fails();

        if(!$valid_request) {
            return response()->json([], 400);
        }

        // Safely delete the prefix and it's children if needed.
        Prefixes::find($_prefix_id)->delete();

        return response()->json(['SUCCESS'], 200);
    }

    public function list_communities(Request $_r, $_category = 'INFO') {

        $valid_request = !Validator::make($_r->all(), PagedTable::rules(['community', 'description', 'type']))->fails();

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

        try {
            if($_category === 'DISABLE' || $_category === 'INFO' || $_category === 'ACTION') {
                $draw = $_r->input('draw');
                $start = $_r->input('start');
                $length = $_r->input('length');
                $filters = $_r->input('filter');
                $order_direction = $_r->input('order_direction');
                $order_column = $_r->input('order_column');

                $communities = collect(json_decode(file_get_contents(self::MODULE_PATH . 'Models/CommunityList.json'), true)[$_category]);

                if(isset($filters['global'])) {                    
                    $communities = $communities->filter(function($x) use($filters) {
                        return str_contains(strtoupper($x['community']), strtoupper($filters['global'])) || str_contains(strtoupper($x['description']), strtoupper($filters['global'])) || str_contains(strtoupper($x['type']), strtoupper($filters['global']));
                    });
                }

                if($order_column != null) {
                    $communities = $order_direction === 'desc' ? $communities->sortByDesc($order_column) : $communities->sortBy($order_column);
                }

                $filter_count = $communities->count();
                $data = $communities->slice($start, $length)->values();
                $total_count = sizeof($data);
    
                return response()->json([
                    'draw' => (int)$draw ?? 0,
                    'recordsTotal' => $total_count,
                    'recordsFiltered' => $filter_count,
                    'data' => $data
                ], 200);
            }    
        } catch(\Throwable $e) {}

        return response()->json(['detail' => 'Error processing the request'], 500);
    }

    public function list_redirects(Request $_r, $_category = 'ACTIVE') {
        return $this->paged_table($_r, function() use($_category) {
            $q = BgpReroute::select(
                'id',
                'created_at',
                'address',
                'expiration_at',
                'approved_at',
                'city',
                'provider',
                'permanent_request'
            )->where('organisation_id', '=', Auth::user()->organisation_id);

            if($_category === 'EXPIRED') {
                $q = $q->where('deleted_at', '>=', 'expiration_at');

            }
            elseif($_category === 'REMOVED') {
                $q = $q->where('deleted_at', '<', 'expiration_at');
            }

            return $q;
        }, [ 
            'address'           => [ 'db' => 'address', 'searchable' => true ],
            'expiration_at'     => [ 'db' => 'expiration_at', 'searchable' => true ],
            'approved_at'       => [ 'db' => 'approved_at', 'searchable' => true ],
            'city'              => [ 'db' => 'city', 'searchable' => true ],
            'provider'          => [ 'db' => 'provider', 'searchable' => true ],
            'permanent'         => [ 'db' => 'permanent', 'searchable' => true ],
        ], 'created_at', null);
    }

    public function fetch_redirect_ping(Request $_r, $_internal = false) {

        $validated = Validator::make( $_r->all(), [
            'address'   => [ 'required', new Cidr, new SafeCidr ]
        ]);

        \Log::debug(Auth::user()->organisation_id);
        \Log::debug($_r);

        if(!$validated->fails()) {

            // Build up lookup table for routers too so we can store the locations
            $router_lookup = [];
            $cleaned_list = [];

            foreach(json_decode(file_get_contents(self::MODULE_PATH . 'Models/RouterList.json'), true) as $location => $providers) {
                $cleaned_list[$location] = [];

                foreach($providers as $provider => $routers) {

                    if(array_search($provider, ['NTT', 'OPTUS', 'COGENT', 'LUMEN', 'TELSTRA', 'ZAYO', 'VOYAGER', 'OMANTEL']) !== false && is_array($routers)) {
                        
                        foreach($routers as $router) {

                            if($_internal) {
                                $cleaned_list[$location][$provider][$router['router']] = null;
                            }
                            else {
                                $cleaned_list[$location][$provider] = null;
                            }
    
                            $router_lookup[$router['router'] . '-' . $provider] = $_internal ? [
                                'location'      => $location,
                                'provider'      => $provider,
                                'router'        => $router['router']
                            ] : [
                                'location'      => $location,
                                'provider'      => $provider
                            ];
                        }
                    }
                }
            }
            
            $ping_results = Arista::ping_full($_r->input('address'));

            // Determine closest and anycast, then build up our cleaned up list...
            $is_anycast = false;
            $under_50 = 0;

            $closest_time = 10000;
            $closest_router = '';

            foreach($ping_results as $router => $latency) {
                [$addr, $provider] = explode('-', $router);


                if($latency <= 50) {
                    ++$under_50;
                }

                if($latency < $closest_time) {
                    $closest_time = $latency;
                    $closest_router = "$addr-$provider";
                }

                if($_internal) {
                    $cleaned_list[$router_lookup["$addr-$provider"]['location']][$provider][$addr] = $latency;
                }
                else {    
                    $cleaned_list[$router_lookup["$addr-$provider"]['location']][$provider] = $latency;
                }
            }

            $no_ping = false;
            $closest = false;

            if($closest_time === 10000) {
                $no_ping = true;
            }
            else {
                if(($under_50 / sizeof($ping_results)) > 0.4) {
                    $is_anycast = true;
                }

                $closest = $router_lookup[$closest_router];
            }

            return $_internal ? [
                'unpingable' => $no_ping,
                'closest' => $closest,
                'anycast' => $is_anycast,
                'values' => $cleaned_list
            ] : response()->json([
                'unpingable' => $no_ping,
                'closest' => $closest,
                'anycast' => $is_anycast,
                'values' => $cleaned_list
            ], 200);
        }
        else {

            if(array_search('The address must be a safe CIDR.', $validated->errors()->get('address')) !== false) {
                $user = Auth::user()->name;
                $organisation = Organisation::find(Auth::user()->organisation_id)->name;

                $to_send = "Unsafe redirect ping attempt has been lodged with the following information: \n"
                    . "Address: " . $_r->input('address') . "\n" 
                    . "User: $user ($organisation)";

                Notification::route('slack', config('creatia.webhook.standard'))->notify(new SlackNotification($to_send));

                return $_internal ? false : response()->json(['UNSAFE'], 423);
            }
            else {
                return $_internal ? false : response()->json(['INVALID'], 400);
            }
        }

        return $_internal ? false : response()->json([], 500);
    }

    public function fetch_redirect_options(Request $_r) {

        $list = collect(json_decode(file_get_contents(self::MODULE_PATH . 'Models/RouterList.json'), true));

        $options = [];


        foreach($list as $location => $providers) {
            $options[$location] = [];
            
            foreach($providers as $provider => $routers) {

                if(is_array($routers) && array_search($provider, ['NTT', 'OPTUS', 'COGENT', 'LUMEN', 'TELSTRA', 'ZAYO', 'SOFTBANK', 'VOYAGER', 'OMANTEL']) !== false) {

                    $options[$location][] = $provider;
                }
            }
        }

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

    public function create_redirect(Request $_r) {

        // Should probably rate limit this in general to prevent people ATTEMPTING to race condition more inserts. In theory, someone could time things to insert more than ten, but that window is absurdly small.

        $validated = Validator::make( $_r->all(), [
            'address'   => [ 'required', new Cidr, new SafeCidr ],
            'closest'   => [ 'required_without_all:location,provider' ],
            'location'  => [ 'required_if:closest,0' ],
            'provider'  => [ 'required_if:closest,0' ]
        ]);

        $user = Auth::user()->name;
        $organisation = Organisation::find(Auth::user()->organisation_id)->name;

        if(!$validated->fails()) {
            
            // Check whether the active redirects that are not permanent exceed counter - Exit
            $active_counter = BgpReroute::where([['expiration_at', '<', Carbon::now()],['permanent_request', '=', 0],['organisation_id', '=', Auth::user()->organisation_id]])->whereNull('approved_at')->count();

            // Check whether there has been more than 10 redirects inserted in the past hour - Exit
            $last_hour = BgpReroute::withTrashed()->where([['created_at', '>', Carbon::now()->subHour()],['permanent_request', '=', 0],['organisation_id', '=', Auth::user()->organisation_id]])->whereNull('approved_at')->count();

            if($active_counter < 1 && $last_hour < 200) {
                // TO DO - Check whether the IP is in an exclusion list...

                // Perform our ping test, if Singapore, Frankfurt, Tokyo and LAX respond with <50ms deny as it is anycast - Log and Exit
                $ping = self::fetch_redirect_ping($_r, true);

                if($ping['anycast']) {
                    return response()->json(['ANYCAST'], 422);
                }
                else {

                    // Pull out appropriate router as well...
                    $appropriate_router = null;

                    if($_r->input('closest', null) == null) {
                        $appropriate_router = array_keys($ping['values'][$_r->input('location')][$_r->input('provider')])[0];
                    }
                    else {
                        $appropriate_router = $ping['closest']['router'];
                    }

                    // Insert
                    BgpReroute::create([
                        'organisation_id'       => Auth::user()->organisation_id,
                        'address'               => explode('/', $_r->input('address'))[0],
                        'mask'                  => 32,
                        'permanent_request'     => 0,
                        'city'                  => $_r->input('location') ?? $ping['closest']['location'],
                        'provider'              => $_r->input('provider') ?? $ping['closest']['provider'],
                        'router'                => $appropriate_router,
                        'expiration_at'         => Carbon::now()->addMinutes(30),
                        'approved_at'           => null
                    ]);

                    $route_inserted = Arista::route_insert(
                        explode('/', $_r->input('address'))[0],
                        $_r->input('location') ?? $ping['closest']['location'],
                        $_r->input('provider') ?? $ping['closest']['provider'],
                        $appropriate_router
                    );

                    $to_send = "Redirect inserted with the following information: \n"
                    . "Address: " . $_r->input('address') . "\n" 
                    . "Location: " . $_r->input('location') . "\n" 
                    . "Provider: " . $_r->input('provider') . "\n" 
                    . "User: $user ($organisation)";
    
                    Notification::route('slack', config('creatia.webhook.standard'))->notify(new SlackNotification($to_send));

                    return response()->json([], 200);
                }
            }
            else {
                // FAILURE - HOURLY LIMIT EXCEEDED
                return response()->json(['LIMIT EXCEEDED'], 429);
            }
        }

        if(array_search('The address must be a safe CIDR.', $validated->errors()->get('address')) !== false) {

            $to_send = "Unsafe redirect creation attempt with the following information: \n"
                . "Address: " . $_r->input('address') . "\n" 
                . "Location: " . $_r->input('location') . "\n" 
                . "Provider: " . $_r->input('provider') . "\n" 
                . "User: $user ($organisation)";

            Notification::route('slack', config('creatia.webhook.standard'))->notify(new SlackNotification($to_send));

            return response()->json(['UNSAFE'], 423);
        }

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

    public function request_permanent(Request $_r) {

    }

    public function remove_redirect(Request $_r, $_id) {

        $reroute = BgpReroute::where([['organisation_id', '=', Auth::user()->organisation_id], ['id', '=', $_id]])->first() ?? false;

        if($reroute && $reroute->delete()) {
            Arista::route_remove($reroute->address, $reroute->city, $reroute->provider, $reroute->router);

            $user = Auth::user()->name;
            $organisation = Organisation::find(Auth::user()->organisation_id)->name;

            $to_send = "Redirect removed with the following information: \n"
            . "Address: $reroute->address\n" 
            . "Location: $reroute->city\n" 
            . "Provider: $reroute->provider\n" 
            . "User: $user ($organisation)";

            Notification::route('slack', config('creatia.webhook.standard'))->notify(new SlackNotification($to_send));

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

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

    public function refresh_redirect(Request $_r, $_id) {

        // Only allow a refresh IF we're within acceptable limits...
        $active_counter = BgpReroute::where([['created_at', '>', Carbon::now()->subHour()],['permanent_request', '=', 0],['organisation_id', '=', Auth::user()->organisation_id], ['id', '!=', $_id]])->whereNull('approved_at')->count();

        $last_hour = BgpReroute::withTrashed()->where([['created_at', '>', Carbon::now()->subHour()],['permanent_request', '=', 0],['organisation_id', '=', Auth::user()->organisation_id], ['id', '!=', $_id]])->whereNull('approved_at')->count();

        if($active_counter < 1 && $last_hour < 200) {

            $reroute = BgpReroute::where([['organisation_id', '=', Auth::user()->organisation_id], ['id', '=', $_id]])->first() ?? false;

            if($reroute) {

                // Delete the old one and create a new one. Yipee
                BgpReroute::create([
                    'organisation_id'       => Auth::user()->organisation_id,
                    'address'               => $reroute->address,
                    'mask'                  => 32,
                    'permanent_request'     => 0,
                    'city'                  => $reroute->city,
                    'provider'              => $reroute->provider,
                    'router'                => $reroute->router,
                    'expiration_at'         => Carbon::now()->addMinutes(30),
                    'approved_at'           => null
                ]);

                $route_inserted = Arista::route_insert(
                    $reroute->address,
                    $reroute->city,
                    $reroute->provider,
                    $reroute->router
                );

                $user = Auth::user()->name;
                $organisation = Organisation::find(Auth::user()->organisation_id)->name;

                $to_send = "Redirect refreshed with the following information: \n"
                . "Address: $reroute->address\n" 
                . "Location: $reroute->city\n" 
                . "Provider: $reroute->provider\n" 
                . "User: $user ($organisation)";
    
                Notification::route('slack', config('creatia.webhook.standard'))->notify(new SlackNotification($to_send));

                $reroute->delete();

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

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

    public function build_from_list(Request $_r) {

        $valid_request = !Validator::make($_r->all(), [
            'list' => 'required|file|mimes:csv,txt|max:10240', // Max 10MB
        ])->fails();

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

        $file = $_r->file('list');

        if($file) {
            $path = $file->getRealPath();

            if(($handle = fopen($path, 'r')) !== false) {

                // Keep track of statuses
                $statuses = [
                    'successful' => [],
                    'failure' => [] 
                ];

                // Run this once - We ignore the header.
                fgetcsv($handle);

                // Retrieve rows, we have yet to sanitise them however.
                $dirty_rows = [];

                while(($row = fgetcsv($handle)) !== false) {
                    if(empty($row[0])) {
                        \Log::debug($row);
                        continue;
                    }

                    // Prefix, Only if existing (Exclude for default which is false), Name

                    $dirty_rows[] = [
                        'prefix'        => $row[0],
                        'IfExisting'    => $row[1],
                        'name'          => $row[2]
                    ];
                }

                // Close the handler
                fclose($handle);

                // Remove anything gross before we sanitise the rows
                $presanitised_rows = [];

                foreach($dirty_rows as $dirty) {

                    $tmp = explode('/', str_replace(['"', "'", '`', ' '], '', trim($dirty['prefix'])));

                    [$address, $mask] = sizeof($tmp) === 2 ? $tmp : [ $tmp[0], str_contains($tmp[0], '.') === true ? 32 : 128 ];


                    // Skip these
                    if(str_contains($address, '.') === true && $mask > 32) {
                        $statuses['failure']["$address/$mask"] = 'You have supplied an invalid IPv4 CIDR.';
                        continue;
                    }


                    $presanitised_rows[] = [
                        'address'       => $address,
                        'mask'          => $mask,
                        'IfExisting'    => empty($dirty['ifExisting']) || $dirty['ifExisting'] === null || !$dirty['ifExisting'] ? false : true,
                        'name'          => trim($dirty['name'],"\"'` ")
                    ];

                }

                // Check if the rows are valid

                $rules = [
                    'address'       => ['required', 'ipaddress'],
                    'mask'          => ['required', 'min:16', 'max:128'],
                    'ifExisting'    => ['required', 'boolean'],
                    'name'          => ['required', 'alpha_dash']
                ];

                $allocated_prefixes = Prefixes::getOrganisationPrefixes(Auth::user()->organisation_id);

                $to_rename = $to_make = [];

                foreach($presanitised_rows as $row) {

                    $fails = Validator::make($row, $_r->all(), $rules)->fails() || filter_var($row['address'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE | FILTER_FLAG_NO_PRIV_RANGE) === false;

                    if($fails) {
                        if(str_contains($row['address'], '-') !== false) {
                            $statuses['failure'][$row['address'] . '/' . $row['mask']] = 'This row is invalid. Dash based ranges are not accepted.';
                        }
                        else {
                            $statuses['failure'][$row['address'] . '/' . $row['mask']] = 'This row must contain a valid IP address, mask, existence flag or lack of and an alphanumeric name which may have dashes.';
                        }

                        continue;
                    }

                    // Either the prefix already exists, or the prefix is inside something that exists.
                    $existing_prefix = $allocated_prefixes->where('address', '=', $row['address'])->where('mask', '=', $row['mask'])->first()->id ?? false;

                    $inside_prefix = false;

                    foreach($allocated_prefixes as $allocated) {

                        $valid_subnet = DB::select(
                            "SELECT CASE 
                                        WHEN (? :: cidr) << (? :: cidr) THEN TRUE
                                        ELSE FALSE
                                    END AS is_subnet",
                            [$row['address'] . '/' . $row['mask'], "$allocated->address/$allocated->mask"]
                        );

                        if($valid_subnet[0]->is_subnet) {
                            $inside_prefix = $allocated->id;
                            break;
                        }

                    }

                    if(!$existing_prefix && !$inside_prefix) {
                        $statuses['failure'][$row['address'] . '/' . $row['mask']] = 'You can only modify existing prefixes or create a subnet of an existing prefix.';
                        continue;
                    }

                    if($row['IfExisting'] || $existing_prefix) { // If it must exist, or it does exist, then...
                        $to_rename[] = [ $existing_prefix, $row['name'], $row['address'] . '/' . $row['mask'] ]; 
                    }
                    else { // Then it will need to be made
                        $to_make[] = [ $inside_prefix, $row['name'], $row['address'] . '/' . $row['mask'] ];
                    }
                }

                // Rename existing prefixes
                foreach($to_rename as $x) {
                    $outcome = $this->rename_prefix(new Request(['name' => $x[1]]), $x[0])->status();

                    if($outcome === 200) {
                        $statuses['successful'][$x[2]] = 'Modified';
                    }
                    else {
                        $statuses['failure'][$x[2]] = 'There was an issue renaming this prefix.';
                    }
                }

                foreach($to_make as $x) {
                    $outcome = $this->post_prefix(new Request(['cidr' => $x[2], 'name' => $x[1]]), $x[0], [
                            'input' => [],
                            'rule' => []
                        ],
                        function($new, $parent, $request) {},
                        false
                    )->status();

                    if($outcome === 200) {
                        $statuses['successful'][$x[2]] = 'Created';
                    }
                    else {
                        $statuses['failure'][$x[2]] = match($outcome) {
                            409     => 'You have declared this prefix earlier in your list. The first one was used.',
                            422     => 'This range sits inside an allocation held by another organisation.',
                            default => 'There was an issue making this prefix.'
                        };
                    }
                }

                return response()->json([...$statuses, 'failure_count' => sizeof($statuses['failure']), 'successful_count' => sizeof($statuses['successful'])], 200);
            }

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

        return response()->json(['INVALID'], 401);

    }

    public function get_valid_owned_subnet(Request $_r) {
        $all_prefixes = Prefixes::getSubtenancyPrefixes(Auth::user()->organisation_id)->pluck('id');

        return response()->json([
            'valid' => Auth::user()->organisation_id == 1 || boolval(Prefixes::whereIn('id', $all_prefixes)->where(DB::raw("CONCAT(address, '/', mask)::cidr"), '>>=', ($_r->input('address') . (str_contains($_r->input('address'), ':') ? '/128' : '/32')))->count())
        ], 200);
    }

    public function check_prefix_conditions(Request $_r) {

        $valid_request = !Validator::make($_r->all(), [
            'address'           => [ 'required', new Cidr ],
            'identity'          => [ 'nullable', 'exists:prefixes,id' ],
            'condition'         => [ 'nullable', 'array', Rule::in(['IPV4', 'IPV6', 'SAFE', 'OWNED', 'SUBTENANTED', 'INSIDE', 'CONTAINS', 'MASK']) ]
        ])->fails();

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

        // Build up some more rules we need to test it by
        $rules = [];

        if($_r->input('condition') !== null) {

            $results = [];

            foreach($_r->input('condition') as $check) { // TO-DO - Implement as required, this is a long-term refactor for the many things around Creatia that check something specific about a prefix.
                switch ($check) {
                    // Standard non-database oriented checks
                    case 'IPV4':
                        $results[] = (new Cidr(FILTER_FLAG_IPV4))->passes('address', $_r->input('address'));
                        break;
                    case 'IPV6':
                        $results[] = (new Cidr(FILTER_FLAG_IPV6))->passes('address', $_r->input('address'));
                        break;
                    case 'SAFE':
                        $results[] = (new SafeCidr)->passes('address', $_r->input('address'));
                        break;
                    case 'MASK':
                        
                        break;

                    // Database checks for textual addresses
                    case 'OWNED-ADDRESS':

                        $results[] = Prefixes::where([
                            [ 'organisation_id', '=', Auth::user()->organisation_id ],
                            [ DB::raw('CONCAT(address, \'/\', mask)'), '=', $_r->input('address') ]
                        ])->exists();

                        break;
                    case 'SUBTENANTED-ADDRESS':
                        $rules[] =  (new SubtenantedAddressV4)->passes('address', $_r->input('address')) || (new SubtenantedAddressV6)->passes('address', $_r->input('address'));
                        break;
                    case 'INSIDE-ADDRESS':

                        if( (new Cidr)->passes('inside', $_r->input('inside')) ) {
                            $results[] = DB::selectOne(
                                "SELECT inet :cidr2 >> inet :cidr1 as result",
                                ['cidr2' => $_r->input('inside'), 'cidr1' => $_r->input('address')]
                            )->result;
                        }
                        else {
                            $rules[] = false;
                        }

                        break;
                    case 'CONTAINS-ADDRESS':
                        
                        if( (new Cidr)->passes('contains', $_r->input('contains')) ) {
                            $results[] = DB::selectOne(
                                "SELECT inet :cidr2 << inet :cidr1 as result",
                                ['cidr2' => $_r->input('contains'), 'cidr1' => $_r->input('address')]
                            )->result;
                        }
                        else {
                            $rules[] = false;
                        }

                        break;

                    // Database checks for address identifiers - TO DO

                }

                if(array_search(false, $results)) {
                    return response()->json(['detail' => 'The supplied inputs do not meet the specified criteria.'], 400);
                }
            } 

        }

        return response()->json(['detail' => 'Prefix has met all specified conditions.'], 200);
    }

}