<?php

namespace App\Console\Commands\Network;

use App\Console\Commands\BaseCommand;
use Illuminate\Support\Facades\DB;

use App\Models\Prefixes;

use App\Models\Asns;
use App\Models\Organisation;

use App\Helpers\Utility\UtilPrefix;
use Illuminate\Support\Facades\Log;
use Throwable;

class PrefixReassociate extends BaseCommand
{
    protected $signature = 'prefix:reassociate {--override=}';

    protected $description = 'Reassociate known prefixes against known and approved ASNs. This only happens intra-organisation, where prefixes may have changed ASN.';

    public function handle()
    {
        // Get a list of prefixes that belong to a given ASN - We're essentially creating an ASN to prefix dictionary...
        $originator_prefix_dictionary = [];

        foreach([...UtilPrefix::get_detailed(), ...UtilPrefix::get_detailed_v6()] as $announced_prefix) {

            if(isset($originator_prefix_dictionary[$announced_prefix->asn_originator])) {
                $originator_prefix_dictionary[$announced_prefix->asn_originator][] = $announced_prefix->prefix;
            }
            else {
                $originator_prefix_dictionary[$announced_prefix->asn_originator] = [$announced_prefix->prefix];
            }
        }


        if(sizeof($originator_prefix_dictionary) === 0) {
            return 0;
        }

        // Simple ASN lookup table: organisation_id => [asn => asn_id]
        $asn_lookup = [];

        foreach(Asns::where('approved', '=', 1)->get() as $asn) {
            $asn_lookup[$asn->organisation_id][$asn->asn] = $asn->id;
        }

        /**
         * Build comprehensive prefix lookup tables
         * 1. By prefix string: for finding mismatched ASNs
         * 2. By prefix ID: for efficient organisation_id lookups
         */
        $all_announced_prefixes = array_merge(...array_values($originator_prefix_dictionary));
        
        $prefix_lookup = [];
        $prefix_id_lookup = [];
        
        foreach(Prefixes::whereIn(DB::raw('concat("address", \'/\', "mask")'), $all_announced_prefixes)
            ->select('id', 'asn_id', 'organisation_id', DB::raw('concat("address", \'/\', "mask") as prefix'))
            ->get() as $prefix) {
            
            $prefix_data = [
                'id' => $prefix->id,
                'asn_id' => $prefix->asn_id,
                'organisation_id' => $prefix->organisation_id
            ];
            
            $prefix_lookup[$prefix->prefix][] = $prefix_data;
            $prefix_id_lookup[$prefix->id] = $prefix_data;
        }

        foreach(Organisation::all() as $organisation) {

            if(isset($asn_lookup[$organisation->id]) && sizeof($asn_lookup[$organisation->id]) > 0) {

                foreach($asn_lookup[$organisation->id] as $asn => $asn_id) {

                    // Get the announced prefixes for this ASN
                    $valid_dictionary_prefixes = isset($originator_prefix_dictionary[$asn]) ? $originator_prefix_dictionary[$asn] : [];
                    
                    if(empty($valid_dictionary_prefixes)) {
                        continue;
                    }

                    /**
                     * Stage 1: Find prefix IDs that:
                     * 1. Are announced by this ASN (in $valid_dictionary_prefixes)
                     * 2. Currently have a different asn_id (candidates for reassociation)
                     * Note: We include ALL mismatched ASNs here (both intra and inter-org)  but we'll only process inter-org conflicts in the transaction
                     */
                    
                    $relevant_prefix_ids = [];
                    
                    foreach($valid_dictionary_prefixes as $prefix) {
                        if(isset($prefix_lookup[$prefix])) {
                            foreach($prefix_lookup[$prefix] as $prefix_data) {
                                if($prefix_data['asn_id'] != $asn_id) {
                                    $relevant_prefix_ids[] = $prefix_data['id'];
                                }
                            }
                        }
                    }

                    if(empty($relevant_prefix_ids)) {
                        continue;
                    }

                    DB::transaction(function() use($organisation, $asn, $relevant_prefix_ids, $prefix_id_lookup) {
                        try {

                            // Stage 2: Filter to only inter-organisation conflicts using efficient ID lookup
                            $inter_org_prefix_ids = [];
                            
                            foreach($relevant_prefix_ids as $prefix_id) {
                                if(isset($prefix_id_lookup[$prefix_id]) && $prefix_id_lookup[$prefix_id]['organisation_id'] != $organisation->id) {
                                    $inter_org_prefix_ids[] = $prefix_id;
                                }
                            }
                            
                            // Only proceed if there are inter-organisation conflicts
                            if(empty($inter_org_prefix_ids)) {
                                return true;
                            }
                            
                            $inter_count = count($inter_org_prefix_ids);
                            $affected_ids = $inter_org_prefix_ids;

                            if($inter_count > 200) {
                                if($this->option('override') === "AS$asn") {
                                    Log::warning("A prefix deletion attempt as a result of ASN ownership change is being enacted. New ASN is AS$asn - Prefixes are: " . json_encode($affected_ids));

                                    DB::beginTransaction();

                                    Prefixes::whereIn('id', $affected_ids)->update(['still_announced' => 0]);

                                    foreach($affected_ids as $affected) {
                                        if(($affected_found = Prefixes::find($affected)) && $affected_found !== null) {
                                            $affected_found->delete();
                                        }
                                    }

                                    DB::commit();
                                }
                                else {
                                    Log::warning("A prefix deletion attempt as a result of ASN ownership changing will affect more than two-hundred prefixes. New ASN is AS$asn - Prefixes are: " . json_encode($affected_ids));
                                }
                            }
                            else {
                                Log::warning("A safe prefix deletion as a result of an ASN ownership change is occurring. New ASN will be AS$asn - Prefixes are: " . json_encode($affected_ids));
                                
                                DB::beginTransaction();

                                Prefixes::whereIn('id', $affected_ids)->update(['still_announced' => 0]);

                                foreach($affected_ids as $affected) {
                                    if(($affected_found = Prefixes::find($affected)) && $affected_found !== null) {
                                        $affected_found->delete();
                                    }
                                }

                                DB::commit();
                            }
                            
                            return true;
                        }
                        catch(Throwable $x) {
                            Log::error($x);
                        }
                    });
                }
            }
        }
        
        return 0;
    }
}