<?php

namespace App\Modules\Network\Controllers;

use App\Http\Controllers\BaseController;
use App\Modules\Network\Models\FirewallRules;
use App\Modules\Network\Models\Networks;
use App\Modules\Network\Models\Sensitivities;
use App\Modules\Network\Models\Rulesets;
use App\Models\Organisation;
use App\Modules\Network\Models\NetworkOrganisationAttributes;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use App\Helpers\Utility\RouteList;
use Illuminate\Support\Facades\Validator;
use Symfony\Component\HttpFoundation\IpUtils;

use App\Modules\Network\Helpers\ControllerCommons;

use Log;

class FirewallController extends BaseController
{
    public function get_networks(Request $_r, $_network_id = null) {
        $draw = $_r->input('draw');
        $start = $_r->input('start');
        $length = $_r->input('length');
        $search_term = $_r->input('search_string');

        // Handle the yuckiness that comes from the fact this is reusable code.
        $subnetHasValidSubtenant = Networks::where('subtenant_organisation_id', '=', Auth::user()->organisation_id);

        if($_network_id !== null) {
            $subnetHasValidSubtenant = $subnetHasValidSubtenant->where('id', '=', $_network_id);
        }

        $subnetHasValidSubtenant = $subnetHasValidSubtenant->count();
        
        if($_network_id == null || Networks::validOwner($_network_id, Auth::user()->organisation_id) || Networks::validSubtenant($_network_id, Auth::user()->organisation_id) || $subnetHasValidSubtenant) {
            return BaseController::pagination_api_request($_r, function($draw, $start, $length) use ($_network_id, $search_term) {
                $supernets_relevant = array_unique(Networks::where('subtenant_organisation_id', '=', Auth::user()->organisation_id)->pluck('member_of_id')->toArray());

                $data = Networks::select('networks.id', 'ip', 'width', 'networks.name', 'still_announced', 'in_sync', DB::raw('rulesets.name as ruleset'), DB::raw('sensitivities.name as sensitivity'), 'networks.organisation_id', 'asn')
                    ->leftJoin('asns', 'asn_id', '=', 'asns.id')
                    ->leftJoin('sensitivities', 'sensitivity_id', '=', 'sensitivities.id')
                    ->leftJoin('rulesets', 'ruleset_id', '=', 'rulesets.id')
                    ->where(function($query) use($supernets_relevant) {
                        $query->orWhere('networks.organisation_id', '=', Auth::user()->organisation_id)
                            ->orWhere('networks.subtenant_organisation_id', '=', Auth::user()->organisation_id)
                            ->orWhereIn('networks.id', $supernets_relevant ?? []);
                    })
                    ->where('member_of_id', '=', $_network_id)
                    ->orderBy('ip')
                    ->orderBy('width')
                    ->skip($start)
                    ->take($length);
                
                $count = 0;
                
                if($search_term) {
                    $data = $data->where([['networks.organisation_id', '=', Auth::user()->organisation_id], ['member_of_id', '=', $_network_id]])
                        ->where(function($query) use ($search_term){
                            $query->orWhere(DB::raw("CONCAT(ip, '/', width)"), 'LIKE', "%$search_term%")
                                ->orWhere('rulesets.name', 'like', "%$search_term%")
                                ->orWhere('sensitivities.name', 'like', "%$search_term%")
                                ->orWhere('asn', 'like', "%$search_term%")
                                ->orWhere('networks.name', 'like', "%$search_term%");
                        });
                    
                    $count = Networks::leftJoin('asns', 'asn_id', '=', 'asns.id')
                        ->leftJoin('sensitivities', 'sensitivity_id', '=', 'sensitivities.id')
                        ->leftJoin('rulesets', 'ruleset_id', '=', 'rulesets.id')
                        ->where(function($query) use($supernets_relevant) {
                            $query->orWhere('networks.organisation_id', '=', Auth::user()->organisation_id)
                                ->orWhere('networks.subtenant_organisation_id', '=', Auth::user()->organisation_id)
                                ->orWhereIn('networks.id', $supernets_relevant ?? []);
                        })
                        ->where('member_of_id', '=', $_network_id)
                        ->where(function($query) use ($search_term){
                            $query->orWhere(DB::raw("CONCAT(ip, '/', width)"), 'LIKE', "%$search_term%")
                                ->orWhere('rulesets.name', 'like', "%$search_term%")
                                ->orWhere('sensitivities.name', 'like', "%$search_term%")
                                ->orWhere('asn', 'like', "%$search_term%")
                                ->orWhere('networks.name', 'like', "%$search_term%");
                        })->count();
                }
                else {
                    $count = Networks::where(function($query) use($supernets_relevant) {
                        $query->orWhere('networks.organisation_id', '=', Auth::user()->organisation_id)
                            ->orWhere('networks.subtenant_organisation_id', '=', Auth::user()->organisation_id)
                            ->orWhereIn('networks.id', $supernets_relevant ?? []);
                    })->count();
                }
    
                $data = $data->get();
    
                return [
                    'draw' => $draw,
                    'total' => $data->count(),
                    'filtered' => $count,
                    'data' => $data,
                ];
            });
        }

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

    public function get_network_subtenant_options($_geo_ip_id) {
        try {
            if(Networks::validOwner($_geo_ip_id, Auth::user()->organisation_id)) {
                $options = Organisation::select(DB::raw('organisations.id as value'), DB::raw('name as label'))->leftJoin('suborganisations', 'child_id', '=', 'organisations.id')->where('suborganisations.parent_id', '=', Auth::user()->organisation_id)->get()->toArray();

                return response()->json([
                    'data' => $options,
                    'selectedValue' => Networks::find($_geo_ip_id)->subtenant_organisation_id ?? 0
                ], 200);
            }
        } catch(Exception $ex) {}

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

    public function put_network_subtenant(Request $_r, $_network_id) {
        try {
            $options = Suborganisation::select('child_id')->where('parent_id', '=', Auth::user()->organisation_id)->get()->pluck('child_id')->toArray();

            // Validate inputs
            $valid_request = !Validator::make([
                'subtenant' => $_r->input('subtenant_id'),
                ], [
                'subtenant' => ['required', Rule::in(array_merge($options, [0]))],
            ])->fails();

            // If valid, update
            if($valid_request) {
                if(($network_model = Networks::validOwner($_network_id, Auth::user()->organisation_id)) && ($network_model->still_announced == 1 || $network_model->member_of_id != null)) {

                    $network_model->subtenant_organisation_id = $_r->input('subtenant_id') == 0 ? null : $_r->input('subtenant_id');
                    
                    if($network_model->save()) {
                        return response()->json(['detail' => __('api.200_GENERIC')], 200);
                    }
                    
                    return response()->json(['detail' => __('api.500_FAILURE')], 500);
                }

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

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

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

    public function get_firewall_rules(Request $_r, $id) {
        $draw = $_r->input('draw');
        $start = $_r->input('start');
        $length = $_r->input('length');

        if(Networks::where([['organisation_id', '=', Auth::user()->organisation_id],['id','=',$id]])->count()) {
            return BaseController::pagination_api_request($_r, function($draw, $start, $length) use($id) {
                $data = FirewallRules::where('network_id', '=', $id)->skip($start)->take($length)->get();
                $count = FirewallRules::where('network_id', '=', $id)->count();
    
                return [
                    'draw' => $draw,
                    'total' => $data->count(),
                    'filtered' => $count,
                    'data' => $data,
                ];
            });
        }

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

    public function get_firewall_rule(Request $_r, $network_id, $rule_id) {
        try {
            if(Networks::where([['organisation_id', '=', Auth::user()->organisation_id],['id','=',$network_id]])->count()) {
                $rule = FirewallRules::where([['network_id','=',$network_id],['id','=',$rule_id]])->get();
            
                if($rule != null) {
                    return response()->json($rule[0], 200);
                }
            }

            return response()->json(['detail' => 'Failed to find firewall rule'], 404);
        }
        catch(Exception $ex) {
            return response()->json(['detail' => 'Server Error'], 500);
        }
    }

    public function post_firewall_rule(Request $_r, $network_id) {
        $params_to_validate = [];
        $query_params = [];
        
        try {
            $valid_params = false;
            switch ($_r->input('type')) {
                case 'custom':
                    $params_to_validate = [
                        'description' => '',
                        'bpf_rule' => 'required',
                    ];

                    $query_params = [
                        'type' => 'custom',
                        'description' => $_r->input('description', null),
                        'bpf_rule' => $_r->input('bpf_rule'),
                    ];
                    break;
                case 'preset':
                    $params_to_validate = [
                        'description' => '',
                        'preset_rule' => 'required',
                    ];

                    $query_params = [
                        'type' => 'preset',
                        'description' => $_r->input('description', null),
                        'preset_rule' => $_r->input('preset_rule'),
                    ];
                    break;
                case 'acl':
                    $params_to_validate = [
                        'description' => '',
                        'protocol' => 'required',
                        'action' => 'required',
                        'port' => 'required',
                    ];

                    $query_params = [
                        'type' => 'acl',
                        'description' => $_r->input('description', null),
                        'protocol' => $_r->input('protocol'),
                        'action' => $_r->input('action'),
                        'port' => $_r->input('port'),
                    ];
                    break;
            }

            $valid_params = $_r->validate($params_to_validate);

            if($valid_params && $params_to_validate != []) {
                // Check if the network that the user has requested actually exists AND belongs to them.
                if(Networks::where([['organisation_id', '=', Auth::user()->organisation_id],['id','=',$network_id]])->count()) {
                    $x = new FirewallRules;

                    $x->fill($query_params);
                    $x->active = 0;
                    $x->network_id = $network_id;
                    
                    if($x->save()) {
                        return response()->json(['detail' => 'Rule created'], 200);
                    }    
                }
            }

        } catch(Exception $ex) {}

        return response()->json(['detail' => 'Error making rule'], 500);
    }

    public function delete_firewall_rule(Request $_r, $network_id, $rule_id) {
        try {
            // Check if the network in question belongs to the user.
            $valid_network = Networks::where([['organisation_id', '=', Auth::user()->organisation_id],['id', '=', $network_id]])->count();

            if($valid_network) {
                if(FirewallRules::where([['network_id', '=', $network_id],['id', '=', $rule_id]])->delete()) {
                    return response()->json(['detail' => 'Rule deleted'], 200);
                }   
            }

        } catch(Exception $ex) {}

        return response()->json(['detail' => 'Error making rule'], 500);
    }

    public function toggle_firewall_rule(Request $_r, $network_id, $rule_id) {
        try {
            $updated = 0;

            // Check if the network in question belongs to the user.            
            $valid_network = Networks::validOwner($network_id, Auth::user()->organisation_id); 
            $valid_network = $valid_network ? $valid_network : Networks::validSubtenant($network_id, Auth::user()->organisation_id);

            if($valid_network) {
                if($_r->input('action') == 'disable') {
                    $updated = FirewallRules::where([['network_id', '=', $network_id],['id', '=', $rule_id]])->update(['active' => 0]);
                }
                elseif($_r->input('action') == 'enable') {
                    $updated = FirewallRules::where([['network_id', '=', $network_id],['id', '=', $rule_id]])->update(['active' => 1]);
                }
            }

            if($updated) {
                return response()->json(['detail' => 'Rule toggled'], 200);
            }

        } catch(Exception $ex) {}

        return response()->json(['detail' => 'Error toggling rule'], 500);
    }

    // C-PASS-1
    public function get_sensitivities($network_id) {
        try {
            if($network = Networks::validOwner($network_id, Auth::user()->organisation_id)) {
                $if_cannot_payload = false;

                if(ControllerCommons::isInnerNetwork($network) && !ControllerCommons::canSetInnerSense($network)) {
                    $if_cannot_payload = [[
                        'id' => 0,
                        'label' => 'Same as Outer'
                    ]];
                }
                else if(ControllerCommons::isOuterNetwork($network) && !ControllerCommons::canSetOuterSense($network)) {
                    $if_cannot_payload = [[
                        'id' => 0,
                        'label' => 'Unavailable While Inner Prefix Has Sensitivity'
                    ]];
                }

                return response()->json([
                    'data' => !$if_cannot_payload ? Sensitivities::select('id', DB::raw('"name" as label'))->get()->toArray() : $if_cannot_payload,
                    'selectedValue' => Networks::find($network_id)->sensitivity_id ?? 0
                ], 200);
            }
        } catch(Exception $ex) {}

        return response()->json(['detail' => 'Error updating ruleset'], 500);
    }

    public function get_rulesets($network_id) {
        try {
            if(Networks::validOwner($network_id, Auth::user()->organisation_id)) {
                return response()->json([
                    'data' => Rulesets::select('id', DB::raw('"name" as label'))->get()->toArray(),
                    'selectedValue' => Networks::find($network_id)->ruleset_id ?? 0
                ], 200);
            }
        } catch(Exception $ex) {}

        return response()->json(['detail' => 'Error updating ruleset'], 500);
    }

    // C-PASS-1
    public function put_firewall_profile(Request $_r, $network_id) {
        try {
            $sense = $_r->input('sensitivity');
            $rule = $_r->input('ruleset');

            $valid_settings = ($sense == '0' || Sensitivities::find($sense)) && ($rule == '0' || Rulesets::find($rule)); 

            $network = Networks::validOwner($network_id, Auth::user()->organisation_id); 
            $network = $network ? $network : Networks::validSubtenant($network_id, Auth::user()->organisation_id);

            // Check whether parent is still announced
            $network_parent = Networks::find($network->member_of_id);

            if($network && $valid_settings && ($network->still_announced == 1 || $network_parent->still_announced == 1)) {
                $attrs = NetworkOrganisationAttributes::where('organisation_id', '=', $network->organisation_id)->first() ?? new NetworkOrganisationAttributes([
                    'allowed_small' => 10,
                    'allowed_big' => 100
                ]);

                if(ControllerCommons::isInnerNetwork($network) && ($attrs->allowed_small <= ControllerCommons::countUsedInner($network->organisation_id)) && !ControllerCommons::canSetInnerSense($network) && $sense != 0) { // Small
                    return response()->json(['detail' => __('api.409_NOT_PERMITTED')], 409);      
                }
                else if(ControllerCommons::isOuterNetwork($network) && ($attrs->allowed_big <= ControllerCommons::countUsedOuter($network->organisation_id)) && !ControllerCommons::canSetOuterSense($network) && $sense != 0) { // Large
                    return response()->json(['detail' => __('api.409_NOT_PERMITTED')], 409);
                } 
                else if( ($attrs->allowed_small <= ControllerCommons::countUsedInner($network->organisation_id) || $attrs->allowed_big <= ControllerCommons::countUsedOuter($network->organisation_id)) &&
                    (!(($sense == '0' && $rule == '0') || ($network->sensitivity_id !== null || $network->ruleset_id !== null)))) {

                    // If the network they're trying to change already has a rule - Or they're changing it to nothing - If so, let them adjust it. Otherwise don't.
                    return response()->json(['detail' => __('api.409_NOT_PERMITTED')], 409);
                }
                else if($sense != 0) {
                    // Perform a search to see if this belongs to any supernet that wasn't covered by the above scenarios - This is in place should anything leak through, and because double ups aren't crushed right now.
                    $candidate_supernets = Networks::select(DB::raw("concat(ip, '/', width) AS prefix"))->where([['width', '<', $network->width],['organisation_id', '=', $network->organisation_id]])->whereNotNull('sensitivity_id')->get()->pluck('prefix');

                    if(ControllerCommons::isValidSubnetInSupernets("$network->ip/$network->width", $candidate_supernets)) {
                        return response()->json(['detail' => __('api.409_NOT_PERMITTED')], 409);
                    }

                    // Lastly, perform a search through potential subnets to avoid the same as above.
                    $candidate_subnets = Networks::select(DB::raw("concat(ip, '/', width) AS prefix"))->where([['width', '>', $network->width],['organisation_id', '=', $network->organisation_id]])->whereNotNull('sensitivity_id')->get()->pluck('prefix');

                    if(ControllerCommons::isValidSupernetContainingSubnets("$network->ip/$network->width", $candidate_subnets)) {
                        return response()->json(['detail' => __('api.409_NOT_PERMITTED')], 409);
                    }
                }

                Networks::where('id', '=', $network_id)->update([
                    'sensitivity_id' => $sense == '0' ? null : $sense,
                    'ruleset_id' => $rule == '0' ? null : $rule,
                    'last_source' => 'creatia',
                    'sent' => 0,
                    'in_sync' => 0
                ]);
    
                return response()->json(['detail' => 'Profile updated'], 200);
            }
        } catch(Exception $ex) {}

        return response()->json(['detail' => 'Error updating profile'], 500);
    }

    public function get_inside_network_subnet(Request $_r, $_network_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 = Networks::validOwner($_network_id, Auth::user()->organisation_id)) {
                    $network_cidr = $valid_network->getCidr();
        
                    $inside_network_subnet = RouteList::ip_in_block($_r->input('cidr'), $network_cidr );

                    if($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', ['child' => $_r->input('cidr'), 'parent' => $network_cidr])], 404);
                }

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

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

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

    public function post_smaller_network(Request $_r, $_network_id) {
        try {
            $valid_request = !Validator::make([
                'cidr' => $_r->input('cidr'),
                'name' => $_r->input('name')
            ], [
                'cidr' => ['required', 'regex:/((?:(?:\d{1,3}\.?)){4}\/\d{1,2})/'],
                'name' => ['string', 'nullable']
            ])->fails();
            
            if($valid_request) {

                $valid_network = Networks::validOwner($_network_id, Auth::user()->organisation_id); 
                $valid_network = $valid_network ? $valid_network : Networks::validSubtenant($_network_id, Auth::user()->organisation_id);
    
                // Check whether parent is still announced
                $network_parent = Networks::find($valid_network->member_of_id);

                if($valid_network && ($valid_network->still_announced == 1 || $network_parent->still_announced == 1)) {
                    $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);

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

                        Networks::create([
                            'name' => $_r->input('name'),
                            'asn_id' => $valid_network->asn_id,
                            'ip' => $prefix,
                            'width' => $bits,
                            'organisation_id' => $valid_network->organisation_id,
                            'in_sync' => 1,
                            'member_of_ip' => $valid_network->ip,
                            'member_of_id' => $valid_network->id
                        ]);

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

                    return response()->json(['detail' => __('api.404_INVALID_CHILD', ['child' => $_r->input('cidr'), 'parent' => $network_cidr])], 404);
                }

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

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

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

    public function get_specific_information(Request $_r, $_network_id) {
        try {
            $network = Networks::validOwner($_network_id, Auth::user()->organisation_id); 
            $network = $network ? $network : Networks::validSubtenant($_network_id, Auth::user()->organisation_id);

            if(($network = Networks::validOwner($_network_id, Auth::user()->organisation_id))) {
                $count_large = ControllerCommons::countUsedOuter(Auth::user()->organisation_id);
                $count_small = ControllerCommons::countUsedInner(Auth::user()->organisation_id);
    
                $attributes = NetworkOrganisationAttributes::where('organisation_id', '=', Auth::user()->organisation_id)->first();
    
                return response()->json([
                    'prefix_name' => $network->name,
                    'prefix_address' => "$network->ip/$network->width",
                    'bigger_used' => $count_large ?? 0,
                    'smaller_used' => $count_small ?? 0,
                    'bigger_pool' => $attributes->allowed_big ?? 100,
                    'smaller_pool' => $attributes->allowed_small ?? 10,
                ], 200);
            }

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

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

    public function get_organisation_attributes(Request $_r) {
        try {
            $count_large = Networks::where([['organisation_id', '=', Auth::user()->organisation_id]])->whereNull('member_of_id')->whereRaw('(sensitivity_id is not null OR ruleset_id is not null)')->count();
            $count_small = Networks::where([['organisation_id', '=', Auth::user()->organisation_id]])->whereNotNull('member_of_id')->whereRaw('(sensitivity_id is not null OR ruleset_id is not null)')->count();

            $attributes = NetworkOrganisationAttributes::where('organisation_id', '=', Auth::user()->organisation_id)->first() ?? [];

            return response()->json([
                'bigger_used' => $count_large ?? 0,
                'smaller_used' => $count_small ?? 0,
                'bigger_pool' => $attributes->allowed_big ?? 100,
                'smaller_pool' => $attributes->allowed_small ?? 10,
            ], 200);
        }
        catch(Exception $ex) {}

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