<?php

namespace App\Http\Controllers\Network;

// Laravel Specifics
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Carbon\Carbon;

// Creatia Core
use App\Models\Asns;
use App\Models\Suborganisation;
use App\Helpers\Utility\UtilPrefix;
use App\Http\Controllers\Network\NetworkController;
// Module Specifics
use App\Models\PrefixExclusions;
use App\Models\PrefixInclusions;
use App\Models\Prefixes;
use App\Models\Supernets;
use App\Models\PrefixSubtenants;
use Illuminate\Support\Facades\Auth;

class AdminNetworkController extends NetworkController
{
    public function get_inclusions(Request $_r, $_asn_id) {
        if(Asns::find($_asn_id)) {        
            return response()->json(PrefixInclusions::select(DB::raw('concat("address", \'/\', "mask") as cidr'))->where('asn_id', '=', $_asn_id)->pluck('cidr') ?? [], 200);
        }

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

    public function get_exclusions(Request $_r, $_asn_id) {
        if(Asns::find($_asn_id)) {        
            return response()->json(PrefixExclusions::select(DB::raw('concat("address", \'/\', "mask") as cidr'))->where('asn_id', '=', $_asn_id)->pluck('cidr') ?? [], 200);
        }

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

    public function put_inclusions(Request $_r, $_asn_id) {
        try {
            if(Asns::find($_asn_id)) {

                $to_insert = [];
    
                foreach($_r->input('addresses') as $addr) {
                    $to_insert[] = [
                        'asn_id' => $_asn_id,
                        'mask' => $addr['mask'],
                        'address' => $addr['address']
                    ];
                }
    
                DB::beginTransaction();
    
                PrefixInclusions::where('asn_id', '=', $_asn_id)->delete();
                PrefixInclusions::insert($to_insert);            
    
                DB::commit();
    
                return response()->json(['detail' => __('api.200_GENERIC')], 200);
            }
        }
        catch(\Exception $ex) {}

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

    public function put_exclusions(Request $_r, $_asn_id) {
        try {
            if(Asns::find($_asn_id)) {

                $to_insert = [];
    
                foreach($_r->input('addresses') as $addr) {
                    $to_insert[] = [
                        'asn_id' => $_asn_id,
                        'mask' => $addr['mask'],
                        'address' => $addr['address']
                    ];
                }
    
                DB::beginTransaction();
    
                PrefixExclusions::where('asn_id', '=', $_asn_id)->delete();
                PrefixExclusions::insert($to_insert);            
    
                DB::commit();
    
                return response()->json(['detail' => __('api.200_GENERIC')], 200);
            }
        }
        catch(\Exception $ex) {}

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

    public function get_prefix_metadata(Request $_r, $_prefix_id) {
        try {
            $network = Prefixes::find($_prefix_id);

            return response()->json([
                'name' => $network->name,
                'created_at' => $network->created_at,
                'address' => "$network->address/$network->mask"
            ], 200);
        }
        catch(\Exception $ex) {}

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

    public function patch_bulk_upsert(Request $_r) {
        /* Expected schema like:
            {
                organisation_id: number, (required)
                prefixes: [
                    {
                        prefix: string, (required)
                        subtenant_prefix_ownership: number[], (optional) (subtenant org id)
                        subtenant_subnet_ownership: number[], (optional) (subtenant org id)
                        subtenant_delete_previous_state: boolean, (optional, assumed no)
                        name: string,(optional)
                    }
                ]
            }
        */

        try {
            $outcome = DB::transaction(function () use( $_r) {
                $organisation_id = $_r->input('organisation_id');

                // Probable parents to reference later
                $potential_parents = Prefixes::select(DB::raw('concat(address, \'/\', mask) as xy'))->where('organisation_id', $organisation_id == 0 ? '!=' : '=', $organisation_id)->get()->pluck('xy');
    
                // Subtenants to insert
                $subtenants_to_insert = [];

                // Keep track of anything the end user should know
                $transaction_statement = [];

                // Go over each prefix...
                foreach($_r->input('prefixes', []) as $prefix) {
                    // First, attempt to locate the prefix - If it's not there, attempt to locate it's parent.
                    $address = $mask = null;

                    // We will need to remember the final ID
                    $final_id = null;

                    // We need to remember the organisation ID we find for the prefix in question for subtenanting.
                    $final_organisation_id = null;
    
                    if(str_contains($prefix['prefix'], '/')) {
                        [$address, $mask] = explode('/', $prefix['prefix']);
                    }
                    else {
                        $address = $prefix['prefix'];
                        $mask = 32;
                    }
    
                    $existing = Prefixes::where([
                        ['address', '=', $address],
                        ['mask', '=', $mask],
                        ['organisation_id', $organisation_id == 0 ? '!=' : '=', $organisation_id]
                    ])->first();

                    $final_organisation_id = $existing->organisation_id ?? null;
    
                    // If it already exists, update that sucker
                    if($existing) {
                        
                        if($prefix['subtenant_delete_previous_state']) {
                            PrefixSubtenants::where('prefix_id', '=', $existing->id)->delete();   
                        }
    
                        if(isset($prefix['name'])) {
                            $existing->name = $prefix['name'];
                        }
    
                        $final_id = $existing->id;
    
                        $existing->save();

                        $transaction_statement["$address/$mask"] = [
                            'prefix' => 'Updated',
                            'invalid_subtenants' => []
                        ];
                    }
                    else { // Otherwise, create it
                        $has_parent = false;

                        foreach($potential_parents as $supernet) {
                            if(UtilPrefix::ip_in_block("$address/$mask", $supernet)) {
                                $has_parent = explode('/', $supernet);
                            }
                        }

                        if($has_parent) {
                            // Fetch the actual parent
                            $parent = Prefixes::where([
                                ['address', '=', $has_parent[0]],
                                ['mask', '=', $has_parent[1]],
                                ['organisation_id', $organisation_id == 0 ? '!=' : '=', $organisation_id]
                            ])->first();

                            $final_organisation_id = $parent->organisation_id;

                            // Refuse to plant the range inside a carve-out somebody else holds beneath the parent unless told to.
                            $contesting = Prefixes::getContestingPrefix("$address/$mask", $parent->mask, $parent->organisation_id);

                            if($contesting !== false) {

                                if(!$_r->boolean('override')) {

                                    $transaction_statement["$address/$mask"] = [
                                        'prefix' => 'Contested',
                                        'invalid_subtenants' => []
                                    ];

                                    continue;
                                }

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

                            $new = Prefixes::create([
                                'name' => $prefix['name'],
                                'address' => $address,
                                'mask' => $mask,
                                'created_source' => 'api',
                                'organisation_id' => $parent->organisation_id,
                                'asn_id' => $parent->asn_id,
                            ]);
    
                            $final_id = $new->id;

                            $transaction_statement["$address/$mask"] = [
                                'prefix' => 'Inserted',
                                'invalid_subtenants' => []
                            ];
                        }
                        else {
                            $transaction_statement["$address/$mask"] = [
                                'prefix' => 'Invalid',
                            ];
                        }
                    }

                    if($final_id != null) {

                        // Get suborganisations and cache ahead
                        $valid_suborganisations = Suborganisation::where('parent_id', '=', $final_organisation_id)->get()->pluck('chiid_id');

                        if(isset($prefix['subtenant_prefix_ownership'])) {
                            foreach($_r->input('subtenant_prefix_ownership', []) as $sub) {
                                if(array_search($sub, $valid_suborganisations)) {
                                    $subtenants_to_insert[] = [
                                        'created_at' => Carbon::now(),
                                        'updated_at' => Carbon::now(),
                                        'prefix_id' => $final_id,
                                        'subtenant_id' => $sub,
                                        'also_subnets' => 0,
                                    ];
                                }
                                else {
                                    $transaction_statement["$address/$mask"]['invalid_subtenants'][] = $sub;
                                }
                            }
                        }
        
                        if(isset($prefix['subtenant_subnet_ownership'])) {
                            foreach($_r->input('subtenant_subnet_ownership', []) as $sub) {
                                if(array_search($sub, $valid_suborganisations)) {
                                    $subtenants_to_insert[] = [
                                        'created_at' => Carbon::now(),
                                        'updated_at' => Carbon::now(),
                                        'prefix_id' => $final_id,
                                        'subtenant_id' => $sub,
                                        'also_subnets' => 1,
                                    ];                                    
                                }
                                else {
                                    $transaction_statement["$address/$mask"]['invalid_subtenants'][] = $sub;
                                }
                            }
                        }
                    }
                }
    
                PrefixSubtenants::insert($subtenants_to_insert);
                return [
                    'message' => 'Success',
                    'statement' => $transaction_statement
                ];
            });
        }
        catch(\Exception $ex) {
            return response()->json(['message' => 'System error'], 500);
        }

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

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

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

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

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

        $subtenants = [];

        $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' => $prefix->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 ?? '-'
            ] : []),

            'subtenants' => $subtenants
            
        ], 200);
    }

    public function get_prefix_options(Request $_r) {
        $prefixes = Prefixes::select(DB::raw("CONCAT(address, '/', mask) as prefix"))->whereNull('deleted_at')->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::all()->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::where('id', '=', $_prefix_id)->get()->pluck('id'))
            ]
        ])->fails();

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

        // Delete prefix and supernet associations..
        Prefixes::where('id', '=', $_prefix_id)->delete();
        Supernets::where('supernet_id', '=', $_prefix_id)->orWhere('subnet_id', '=', $_prefix_id)->delete();

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