<?php

namespace App\Modules\Network\Controllers;

// 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\Models\Suborganisation;
use App\Http\Controllers\BaseController;
use App\Helpers\Utility\RouteList;

// Mitigation
use App\Modules\Network\Models\Prefixes;
use App\Modules\Network\Models\PrefixSubtenants;
use App\Modules\Network\Models\Mitigation\PrefixMitigation;
use App\Modules\Network\Models\GeoIp\PrefixGeoIp;
use App\Modules\Network\Models\Supernets;

use App\Modules\Network\Helpers\ControllerCommons;
use App\Helpers\ClickhouseHandler;

use App\Helpers\Utility\GoliathBgp;
use App\Modules\Network\Hooks\NetworkHooks;
use Log;

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

    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 = Organisation::select(DB::raw('organisations.id as value'), DB::raw('name as label'))->leftJoin('suborganisations', 'child_id', '=', 'organisations.id')->where('suborganisations.parent_id', '=', $USER_ORGANISATION_ID)->get();
                
                // 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(Exception $ex) {}

        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 = Suborganisation::where('parent_id', '=', $USER_ORGANISATION_ID)->get()->pluck('child_id')->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,
                        ];
                    }

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

                    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(Exception $ex) {
            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', 'regex:/((?:(?:\d{1,3}\.?)){4}\/\d{1,2})/']])->fails();
            
            if($valid_request) {
                if($valid_network = Prefixes::validOwner($_network_id, $USER_ORGANISATION_ID)) {
                    $network_cidr = $valid_network->getCidr();
        
                    $valid_subnet = RouteList::valid_subnet($network_cidr, $_r->input('cidr'));
                    $inside_network_subnet = RouteList::ip_in_block($_r->input('cidr'), $network_cidr);

                    if($valid_subnet && $inside_network_subnet && !RouteList::ip_in_blocks($_r->input('cidr'), RouteList::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(Exception $ex) {}

        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;
        
        try {
            $valid_request = !Validator::make(array_merge([
                'cidr' => $_r->input('cidr'),
                'name' => $_r->input('name'),
            ], $_validation_shim['input']), array_merge([
                'cidr' => ['required', 'regex:/((?:(?:\d{1,3}\.?)){4}\/\d{1,2})/'],
                '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);

                if($is_rightful_editor && $is_parent_announced) {
                    $network_cidr = $parent_network->getCidr();
        
                    $valid_subnet = RouteList::valid_subnet($network_cidr, $_r->input('cidr'));
                    $inside_network_subnet = RouteList::ip_in_block($_r->input('cidr'), $network_cidr);

                    // Create the network and attach it to the parent subnet.
                    if($valid_subnet && $inside_network_subnet && !RouteList::ip_in_blocks($_r->input('cidr'), RouteList::get_excluded_blocks())) {
                        [$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) {
                            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,
                            ]);
    
                            if($_creation_shim != null) {
                                $_creation_shim($new_network, $parent_network, $_r);
                            }
    
                            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
                                ]
                            ]);
    
                            DB::commit();
    
                            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(Exception $ex) {
            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(
                    env('CLICKHOUSE_DB'),
                    env('CLICKHOUSE_ENDPOINT'),
                    env('CLICKHOUSE_PORT'),
                    env('CLICKHOUSE_USER'),
                    env('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_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', Rule::in(Prefixes::getOrganisationPrefixes(Auth::user()->organisation_id)->pluck('id')) ],
            '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',
                Rule::in(Prefixes::getOrganisationPrefixes(Auth::user()->organisation_id)->pluck('id')),
                Rule::in(Prefixes::where('id', '=', $_prefix_id)->whereIn('created_source', ['goliath-attack', 'user'])->get()->pluck('id'))
            ]
        ])->fails();

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

        // Run hooks
        NetworkHooks::on_delete($_prefix_id);

        // Delete prefix
        Prefixes::where('id', '=', $_prefix_id)->delete();

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

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

        $validated = $_r->validate([
            'draw' => 'integer',
            'start' => 'required|min:0',
            'length' => 'required|min:0',
            'order_direction' => Rule::in(['asc', 'desc']),
            'order_column' => Rule::in(['community', 'description', 'type'])
        ]);

        try {
            if($validated && ($_category === 'DISABLE' || $_category === 'INFO' || $_category === 'ACTION')) {
                $draw = $_r->input('draw');
                $start = $_r->input('start');
                $length = $_r->input('length');
                $search_term = $_r->input('search_string');
                $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($search_term != null) {
                    $search_data = json_decode(urldecode($_r->input('search')), true);

                    $global = $search_data['global'] ?? false;

                    foreach(($search_data['filters'] ?? []) as $filter) {
                        $communities = $communities->filter(function($x) use($filter) {
                            if(str_contains($x[$filter['id']], $filter['value'])) {
                                return true;
                            }
                        });
                    }
                    
                    if($global) {
                        $communities = $communities->filter(function($x) use($global) {
                            return str_contains($x['community'], $global) || str_contains($x['description'], $global) || str_contains($x['type'], $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' => $draw ?? 0,
                    'recordsTotal' => $total_count,
                    'recordsFiltered' => $filter_count,
                    'data' => $data
                ], 200);
            }    
        } catch(Exception $ex) {}

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