<?php

namespace App\Modules\Network\Controllers;

// Laravel
use Carbon\Carbon;
use Illuminate\Support\Facades\Storage;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

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

// Creatia Default
use App\Models\Organisation;
use App\Models\Suborganisation;
use App\Models\SecurityEvent;
use App\Helpers\ClickhouseHandler;
use App\Helpers\Api\Netbox;
use App\Models\Asns;

// Mitigation
use App\Modules\Network\Models\Mercury\MercuryAppliance;
use App\Modules\Network\Models\Mercury\MercuryBgpSnapshot;
use App\Modules\Network\Models\Mercury\MercuryOrganisationAttributes;
use App\Modules\Network\Models\Mercury\MercuryTunnel;


use Log;

class MercuryController extends NetworkController
{
    public function fetch_appliance_options(Request $_r) {
        return response()->json([
            'appliances' => MercuryAppliance::select('id', 'name')->where(function($x) {
                $x->where('hidden', '=', 0)->orWhereNull('hidden');
            })->where(function($x) {
                $x->where('locked', '=', 0)->orWhereNull('locked');
            })->orderBy('name')->get()->toArray(),
            'asns' => Asns::select('id', 'asn')->where([['organisation_id', '=', Auth::user()->organisation_id], ['approved', '=', 1]])->get()->toArray()
        ], 200);
    }

    public function fetch_tunnel_list(Request $_r) {        
        $USER_ORGANISATION_ID = Auth::user()->organisation_id;
        
        $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');
        
        // Build our query function
        $query_builder = function() use($USER_ORGANISATION_ID) {

            $query = MercuryTunnel::select(
                'mercury_tunnels.id',
                'asns.asn as asn',
                'mercury_appliances.name as appliance_name',
                'mercury_tunnels.name as tunnel_name',
                'bgp_state',
                'tunnel_state',
                'local_address',
                'remote_address',
                'remote_port',
                'gsl_device_v4',
                'gsl_device_v6',
                'client_device_v4',
                'client_device_v6',
                'type',
                'allow_announcements',
                'allow_downstreaming',
                'no_bgp'
                )
                ->leftJoin('mercury_appliances', 'appliance_id', 'mercury_appliances.id')
                ->leftJoin('asns', 'asn_id', 'asns.id')
                ->where('mercury_tunnels.organisation_id', '=', $USER_ORGANISATION_ID);

            return $query;
        };

        // Build our search function
        $query_search = function($query, $global, $filters) {

            if($global) {
                // Look over every row, though ruleset implementation required...
                $query = $query->where(function($query) use($global) {
                    $query->orWhere('asn', 'LIKE', "%$global%")
                        ->orWhere('appliance_name', 'LIKE', "%$global%")
                        ->orWhere('tunnel_name', 'LIKE', "%$global%")
                        ->orWhere('local_address', 'LIKE', "%$global%")
                        ->orWhere('remote_address', 'LIKE', "%$global%")
                        ->orWhere('remote_port', 'LIKE', "%$global%")
                        ->orWhere(DB::raw("CONCAT(remote_address, ':', remote_port)"), 'LIKE', "%$global%")
                        ->orWhere('gsl_device_v4', 'LIKE', "%$global%")
                        ->orWhere('gsl_device_v6', 'LIKE', "%$global%")
                        ->orWhere('client_device_v4', 'LIKE', "%$global%")
                        ->orWhere('client_device_v6', 'LIKE', "%$global%")
                        ->orWhere('type', 'LIKE', "%$global%");
                    });
            }

            if($filters) {
                foreach($filters as $filter) {
                    $col = '';
                    switch($filter['id']) {
                        case 'asn': $col = 'asn'; break;
                        case 'appliance_name': $col = 'appliance_name'; break;
                        case 'tunnel_name': $col = 'tunnel_name'; break;
                        case 'local_address': $col = 'local_address'; break;
                        case 'remote_port':
                        case 'remote_address': $col = DB::raw("CONCAT(remote_address, ':', remote_port)"); break;
                        case 'gsl_device_v4': $col = 'gsl_device_v4'; break;
                        case 'gsl_device_v6': $col = 'gsl_device_v6'; break;
                        case 'client_device_v4': $col = 'client_device_v4'; break;
                        case 'client_device_v6': $col = 'client_device_v6'; break;
                        case 'type': $col = 'type'; break;
                    }

                    if($col != '') {
                        $query = $query->where($col, 'LIKE', '%' . $filter['value'] . '%');
                    }
                }
            }

            return $query;
        };

        // Build our sorting function
        $query_sort = function($query, $column, $direction) {
            $col = 'mercury_tunnels.created_at';

            switch($column) {
                case 'asn': $col = 'asn'; break;
                case 'appliance_name': $col = 'appliance_name'; break;
                case 'tunnel_name': $col = 'tunnel_name'; break;
                case 'local_address': $col = 'local_address'; break;
                case 'remote_address': $col = DB::raw("CONCAT(remote_address, ':', remote_port)"); break;
                case 'gsl_device_v4': $col = 'gsl_device_v4'; break;
                case 'gsl_device_v6': $col = 'gsl_device_v6'; break;
                case 'client_device_v4': $col = 'client_device_v4'; break;
                case 'client_device_v6': $col = 'client_device_v6'; break;
                case 'type': $col = 'type'; break;
            }

            return $query->orderBy($col, !$direction ? 'desc' : $direction);

        };

        // Create valid sorting keys
        $sorting_keys = ['tunnel_name', 'asn', 'appliance_name', 'local_address', 'remote_port', 'remote_address', 'gsl_device_v4', 'gsl_device_v6', 'client_device_v4', 'client_device_v6', 'type'];

        // Build our transformation function
        $query_transform = function($data) {
            $data->transform(function($row) {
                return $row;
            });

            return $data;
        };

        return NetworkController::pagination_table_request($_r, $query_builder, $query_search, $query_sort, $query_transform, $sorting_keys);
    }

    public function get_tunnel(Request $_r, $_tunnel_id) {

        if($tunnel = MercuryTunnel::where([['organisation_id', '=', Auth::user()->organisation_id], ['id', '=', $_tunnel_id]])->get()->first()) {

            return response()->json([
                'tunnel_id' => $tunnel->id,
                'asn' => Asns::find($tunnel->asn_id)->asn,
                'appliance' => MercuryAppliance::find($tunnel->appliance_id)->name,
                'name' => $tunnel->name,
                'bgp_state' => $tunnel->bgp_state,
                'tunnel_state' => $tunnel->tunnel_state,
                'local_address' => $tunnel->local_address,
                'remote_address' => $tunnel->remote_port !== null ? "$tunnel->remote_address:$tunnel->remote_port" : $tunnel->remote_address,
                'gsl_device_v4' => $tunnel->gsl_device_v4,
                'gsl_device_v6' => $tunnel->gsl_device_v6,
                'client_device_v4' => $tunnel->client_device_v4,
                'client_device_v6' => $tunnel->client_device_v6,
                'type' => $tunnel->type,
                'allow_announcements' => $tunnel->allow_announcements,
                'allow_downstreaming' => $tunnel->allow_downstreaming,
            ], 200);

        }

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

    public function helper_pull_addresses() {
        // Fetch the Mercury Ranges...
        $ranges = Netbox::ipam_get_prefix_all(['description' => 'Mercury Tunnel Range'])['results'];

        $v4_gsl = $v6_gsl = $v4_client = $v6_client = '';

        foreach($ranges as $range) {
            // Get the addresses for this range...
            $addresses = Netbox::ipam_get_prefix_ip_available($range['id']);
            
            foreach($addresses as $address) {
                [$prefix, $mask] = explode('/', $address['address']);

                if($address['family'] === 4 && ($v4_gsl === '' || $v4_client === '')) {
                    $last_octet = explode('.', $prefix)[3];

                    if(($last_octet + 1) % 4 === 2) {
                        $v4_gsl = $prefix;
                        continue;
                    }

                    if(($last_octet + 1) % 4 === 3) {
                        $v4_client = $prefix;
                        continue;
                    }
                }
                elseif($address['family'] === 6 && ($v6_gsl === '' || $v6_client === '')) {
                    $last_hextet = hexdec(array_slice(explode(':', $prefix), -1, 1)[0]);

                    if(($last_hextet + 1) % 4 === 2) {
                        $v6_gsl = $prefix;
                        continue;
                    }

                    if(($last_hextet + 1) % 4 === 3) {
                        $v6_client = $prefix;
                        continue;
                    }
                }
            }
        }

        return [
            'gsl_v4'    => $v4_gsl,
            'gsl_v6'    => $v6_gsl,
            'client_v4' => $v4_client,
            'client_v6' => $v6_client
        ];
    }

    public function create_tunnel(Request $_r) {

        $valid_request = !Validator::make(
            $_r->input(),
            [
                'address'           => [ 'required', 'regex:/^((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(\/((1([0-1][0-9]|2[0-8]))|([1-9][0-9])|([8-9])))?)|(((25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])(\.)){3}(25[0-5]|(2[0-4]|1[0-9]|[1-9]|)[0-9])(\/((3[0-2])|([1-2][0-9])|([8-9])))?)$/'],
                'appliance_id'      => [ 'required', Rule::in(MercuryAppliance::where(function($x) {$x->where('hidden', '=', 0)->orWhereNull('hidden');})->where(function($x) {$x->where('locked', '=', 0)->orWhereNull('locked');})->pluck('id'))],
                'type'              => [ 'required', Rule::in(['VXLAN', 'GRE']) ],
                'port'              => [ 'required_if:type,VXLAN' ],
                'asn_id'            => [ 'required', Rule::in(Asns::where('organisation_id', '=', Auth::user()->organisation_id)->pluck('id')) ],
                'name'              => [ 'required', 'string' ]
            ]
        )->fails();

        if($valid_request && boolval(filter_var($_r->input('address'), FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE))) {
            try {
                // Elicit tunnel addresses
                $tunnel_addresses = static::helper_pull_addresses();

                // Determine whether announcements and downstreaming is permitted
                $attributes = MercuryOrganisationAttributes::where('organisation_id', '=', Auth::user()->organisation_id)->get()->first();

                // Select appliance and get it's current local address
                $appliance = MercuryAppliance::find($_r->input('appliance_id'));

                $new_tunnel = MercuryTunnel::create([
                    'organisation_id' => Auth::user()->organisation_id,
                    'asn_id' => $_r->input('asn_id'),
                    'appliance_id' => $_r->input('appliance_id'),
                    'name' => $_r->input('name'),
                    'billing_state' => '',
                    'bgp_state' => 'NEW',
                    'tunnel_state' => 'NEW',
                    'local_address' => str_contains($_r->input('address'), ':') ? $appliance->server_v6 : $appliance->server_v4,
                    'remote_address' => $_r->input('address'),
                    'remote_port' => $_r->input('type') === 'GRE' ? null : $_r->input('port'),
                    'gsl_device_v4' => $tunnel_addresses['gsl_v4'],
                    'gsl_device_v6' => $tunnel_addresses['gsl_v6'],
                    'client_device_v4' => $tunnel_addresses['client_v4'],
                    'client_device_v6' => $tunnel_addresses['client_v6'],
                    'type' => $_r->input('type'),
                    'allow_announcements' => $attributes['allow_announcements'] ?? null,
                    'allow_downstreaming' => $attributes['allow_downstreaming'] ?? null,
                    'no_bgp' => 0
                ]);

                if($new_tunnel->save()) {
                    // The tunnel has been successfully saved, so we can now tell netbox the IP address is in use.
                    foreach([
                        [ 'description' => "MER-$new_tunnel->id-ORG-$new_tunnel->organisation_id-GSL", 'status' => 'active', 'address' => $tunnel_addresses['gsl_v4'] . '/30' ],
                        [ 'description' => "MER-$new_tunnel->id-ORG-$new_tunnel->organisation_id-GSL", 'status' => 'active', 'address' => $tunnel_addresses['gsl_v6'] . '/126' ],
                        [ 'description' => "MER-$new_tunnel->id-ORG-$new_tunnel->organisation_id-CLI", 'status' => 'active', 'address' => $tunnel_addresses['client_v4'] . '/30' ],
                        [ 'description' => "MER-$new_tunnel->id-ORG-$new_tunnel->organisation_id-CLI", 'status' => 'active', 'address' => $tunnel_addresses['client_v6'] . '/126' ]
                    ] as $x) {
                        Netbox::ipam_post_ip($x);
                    }

                    return response()->json([
                        'tunnel_id' => $new_tunnel->id
                    ], 200);
                }
            }
            catch(\Exception $ex) {}
        }

    }

    public function edit_tunnel(Request $_r, $_tunnel_id) {
        $valid_request = !Validator::make( array_merge([ 'tunnel_id' => $_tunnel_id ], $_r->input()),
            [
                'tunnel_id'         => [ 'required', Rule::in(MercuryTunnel::where('organisation_id', '=', Auth::user()->organisation_id)->pluck('id')) ],
                'name'              => [ 'string' ],
                'reprovision'       => [ 'boolean']
            ]
        )->fails();

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

        $edit_tunnel = MercuryTunnel::find($_tunnel_id);

        if($_r->input('name', false)) {
            $edit_tunnel->name = $_r->input('name');
        }

        if($_r->input('reprovision', false)) {
            $attributes = MercuryOrganisationAttributes::where('organisation_id', '=', Auth::user()->organisation_id)->get()->first();

            $edit_tunnel->bgp_state = 'UPDATE';
            $edit_tunnel->tunnel_state = 'UPDATE';
            $edit_tunnel->allow_announcements = $attributes['allow_announcements'];
            $edit_tunnel->allow_downstreaming = $attributes['allow_downstreaming'];
        }

        $edit_tunnel->save();

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

    public function delete_tunnel(Request $_r, $_tunnel_id) {
        if($delete_tunnel = MercuryTunnel::where([['organisation_id', '=', Auth::user()->organisation_id], ['id', '=', $_tunnel_id]])->get()->first()) {
            
            $delete_tunnel->update([
                'bgp_state'     => 'DELETE',
                'tunnel_state'  => 'DELETE'
            ]);
            
            $delete_tunnel->delete();

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

        return response()->json('FAILURE', 400);
    }

    public function get_announcements(Request $_r) {
        $defaults = MercuryOrganisationAttributes::where('organisation_id', '=', Auth::user()->organisation_id)->get()->first();
        
        return response()->json([
            'allow_announcements' => $defaults === null ? true : boolval($defaults->allow_announcements)
        ], 200);
    }

    public function put_announcements(Request $_r) {

        // Validate this request in the first place.
        $valid_request = !Validator::make( array_merge(['organisation_id' => Auth::user()->organisation_id], $_r->all()), [
            'allow_announcements'   => ['required', 'boolean']
        ])->fails();

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

        $defaults =  MercuryOrganisationAttributes::firstOrNew(['organisation_id' => Auth::user()->organisation_id]);

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

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

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

}
