<?php

namespace App\Console\Commands\Network;

use App\Console\Commands\BaseCommand;
use Carbon\Carbon;
use App\Models\Prefixes;
use App\Models\Supernets;
use Illuminate\Support\Facades\Concurrency;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;

class PrefixSortV2 extends BaseCommand
{

    protected $signature = 'prefix:sort-v2 {--full-sort=}';

    protected $description = 'Sorts prefixes based on supernetting and subnetting.';

    public static function closure(bool $full_sort, int $modulo, int $max, string $supernet_lookup_key, string $still_exists_lookup_key) {

        // Use keys since we don't have "$this" here.
        $supernet_lookup = json_decode(Redis::get($supernet_lookup_key), true);
        $still_exists_lookup = json_decode(Redis::get($still_exists_lookup_key), true);
        
        $now = Carbon::now();

        // Select all prefixes relevant
        $v4_prefixes = DB::table('prefixes AS a')->select(
                'id',
                'address',
                'mask',
                'organisation_id',
                DB::raw("( SELECT array_agg(id) FROM prefixes b WHERE b.deleted_at IS NULL AND b.asn_id = a.asn_id AND b.mask > a.mask AND b.address << set_masklen(a.address, a.mask)) as subnet_ids")
            )
            ->where(DB::raw("id % $max"), '=', $modulo)
            ->where([ [DB::raw('family(address)'), '=', 4], ['mask', '!=', 32], ['mask', '>=', 16] ])
            ->whereNull('deleted_at')
            ->orderByDesc('mask');

        $v6_prefixes = DB::table('prefixes AS a')->select(
                'id',
                'address',
                'mask',
                'organisation_id',
                DB::raw("( SELECT array_agg(id) FROM prefixes b WHERE b.deleted_at IS NULL AND b.asn_id = a.asn_id AND b.mask > a.mask AND b.address << set_masklen(a.address, a.mask)) as subnet_ids")
            )
            ->where(DB::raw("id % $max"), '=', $modulo)
            ->where([ [DB::raw('family(address)'), '=', 6], ['mask', '!=', 128], ['mask', '>=', 16] ])
            ->whereNull('deleted_at')
            ->orderByDesc('mask');

        if(!$full_sort) {
            $v4_prefixes = $v4_prefixes->where('a.created_at', '>', Carbon::now()->subMinutes(5));
            $v6_prefixes = $v6_prefixes->where('a.created_at', '>', Carbon::now()->subMinutes(5));
        }

        $v4_prefixes = $v4_prefixes->get();
        $v6_prefixes = $v6_prefixes->get();

        // Safe, premature exit
        if(sizeof($v4_prefixes) === 0 && sizeof($v6_prefixes) === 0) {
            return 0;
        }

        $to_insert = [];

        foreach([$v4_prefixes, $v6_prefixes] as $prefix_set) {
            
            foreach($prefix_set as $prefix) {

                $subnet_ids = $prefix->subnet_ids === null ? [] : explode(',', trim($prefix->subnet_ids, '{}'));

                if(count($subnet_ids)) {
                    foreach($subnet_ids as $subnet_id) {
                        if(!isset($supernet_lookup["$prefix->id-$subnet_id"]) && isset($still_exists_lookup[$subnet_id])) {

                            // It doesn't exist yet so we can insert it - It must not have been deleted between now and then however.
                            $to_insert["$prefix->id-$subnet_id"] = [
                                'created_at' => $now,
                                'updated_at' => $now,
                                'tenant_id' => $prefix->organisation_id,
                                'supernet_id' => $prefix->id,
                                'subnet_id' => $subnet_id
                            ];

                        }
                    }
                }
            }
        }

        if(sizeof($to_insert)) {
            Log::info('Inserting ' . sizeof($to_insert) . ' supernet associations.');
        }

        foreach(array_chunk($to_insert, config('creatia.constant.chunk_medium')) as $insertion_chunk) {
            DB::table('supernets')->insert(array_values($insertion_chunk));
        }
    }

    public function handle()
    {
        $start = microtime(true);

        // We temporarily store these in Redis since they can exceed the arg-max
        $supernet_lookup = Supernets::select(DB::raw('concat(supernet_id, \'-\', subnet_id) as lookup'))
            ->whereNotNull('supernet_id')
            ->whereNotNull('subnet_id')
            ->get()
            ->keyBy('lookup')
            ->toArray();
            
        $still_exists_lookup = Prefixes::select('id')->get()->keyBy('id')->toArray();

        // These realistically shouldn't live for longer than an hour - Obviously keeping them is fine, but we also want Redis to clean them up if we ever move to a different name, etc.
        $supernet_lookup_key = "$this->signature:supernet-lookup";
        $still_exists_lookup_key = "$this->signature:still-exists-lookup";

        Redis::setex("$this->signature:supernet-lookup", 3600, json_encode($supernet_lookup));
        Redis::setex("$this->signature:still-exists-lookup", 3600, json_encode($still_exists_lookup));

        $full_sort = $this->option('full-sort') || Redis::get('prefix:sort-v2-thread:full') ? 0 : 1;

        $tasks = [];

        for ($i = 0; $i < 6; ++$i) {
            $tasks[] =  static fn () => self::closure(Redis::get('prefix:sort-v2-thread:full') ? 0 : 1, $i, 6, $supernet_lookup_key, $still_exists_lookup_key);
        }

        Concurrency::run($tasks);
        
        if($full_sort === 1) {
            Redis::setex('prefix:sort-v2-thread:full', 300, true);
        }

        if(($time_taken = microtime(true) - $start) && $time_taken > 10) {
            Log::debug("Prefix sorting execution has exceeded safe time: " . (microtime(true) - $start));
        }

        return 0;
    }
}