<?php

namespace App\Models\Goliath;

use App\Models\Goliath\GoliathAppliance;
use App\Models\Organisation;
use App\Models\Prefixes;
use App\Notifications\SlackNotification;
use Exception;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Notification;

class GoliathRuleset extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'id',
        'name',
        'type',
        'rules',
        'unencoded_rules',
        'organisation_id'
    ];

    private $rule_manifest = [];

    private $default_rule = [];

    private $changeLog = [];

    protected static function booted()
    {

        $change = function ($_m) {

            try {
                $new = $_m->getDirty();
                $old = $_m->getOriginal();
    
                $user = Auth::user();
    
                $organisation = Organisation::find($_m->organisation_id);
    
                $addresses = $_m->id ? Prefixes::leftJoin('prefix_goliath_firewall', 'prefix_goliath_firewall.prefix_id', '=', 'prefixes.id')
                    ->leftJoin('goliath_rulesets', 'prefix_goliath_firewall.ruleset_id', '=', 'goliath_rulesets.id')
                    ->leftJoin('asns', 'prefixes.asn_id', '=', 'asns.id')
                    ->where('ruleset_id', '=', $new['id'] ?? $old['id'])
                    ->whereNull(['prefixes.deleted_at', 'prefix_goliath_firewall.deleted_at'])
                    ->select(
                        'address',
                        'asns.asn',
                        DB::raw("CONCAT(address, '/', mask) as network"),
                    )
                    ->get() : [];
    
                $affectedReadable = $affected = [];
    
                foreach($addresses as $x) {
                    $affectedReadable[] = "<https://bgp.tools/prefix/$x->address|$x->network> (<https://bgp.tools/as/$x->asn|AS$x->asn>)";
                    $affected[] = $x->network;
                }
    
                $_m->changeLog = [
                    'Ruleset' => $old['name'] ?? $new['name'],
                    'User' => $user->name ?? 'NO USER SET',
                    'Organisation' => "$organisation->name",
                    'Prefixes' => [ 'Readable' => $affectedReadable, 'List' => $affected ],
                    'Rules' => [ 'Old' => isset($old['rules']) ? json_decode($old['rules'] ?? '{}', true) : null, 'New' => json_decode($new['rules'] ?? '{}', true) ]
                ];
            }
            catch(\Exception $ex) {
                \Log::error($_m);
                \Log::error($ex);
            } 

        };

        static::creating($change);
        static::updating($change);
        static::deleting($change);

        $done = function ($_m) {


            try {

                $notification = "Mitigation Ruleset Changed\n"
                    . 'Ruleset: ' . $_m->changeLog['Ruleset'] . "\n"
                    . 'User: ' . $_m->changeLog['User'] . "\n"
                    . 'Organisation: ' . $_m->changeLog['Organisation'] . "\n"
                    . "Affected: \n" . ($_m->changeLog['Prefixes']['Readable'] === [] ? '-' : implode("\n", $_m->changeLog['Prefixes']['Readable']));


                (new GoliathHistory([
                    'type'              => $_m->changeLog['Prefixes']['Readable'] === [] ? GoliathHistoryType::Ruleset : GoliathHistoryType::RulesetFull,
                    'history'           => json_encode($_m->changeLog),
                    'organisation_id'   => $_m->organisation_id,
                    'user_id'           => Auth::user()->id ?? null
                ]))->save();

                Notification::route('slack', config('creatia.webhook.goliath'))->notify(new SlackNotification($notification));
            }
            catch(\Exception $ex) {
                \Log::error($ex);

                try { \Log::info($notification); } catch (\Exception $i) {}
            }
        };

        static::created($done);
        static::updated($done);
        static::deleted($done);

    }

    public function formatRule(string $name, int $x, int $y, string $bpf, string $action, ?int $gotoId = null, ?int $bpsLimit = null, ?int $ppsLimit = null, ?int $srcMask = null, ?int $dstMask = null) {

        $rule = [
            'matchAction' => $action,
            'name'      => $name,
            'x'        => $x,
            'y'        => $y
        ];

        if($bpf !== '') {
            $rule['bpfRule'] = $bpf;
        }

        if($action === 'police') {
            $rule = [ 
                ...$rule,
                'bpsLimit' => $bpsLimit,
                'ppsLimit' => $ppsLimit,
                ...($srcMask !== null ? [ 'srcMask' => $srcMask ] : []),
                ...($dstMask !== null ? [ 'dstMask' => $dstMask ] : []) 
            ];
        }
        elseif($action === 'goto') {
            $rule['gotoNext'] = $gotoId;
        }

        return $rule;
    }

    public function addDefault(int $x, int $y, string $action, ?int $gotoId = null, ?int $bpsLimit = null, ?int $ppsLimit = null, ?int $srcMask = null, ?int $dstMask = null) {
        $this->rule_manifest[] = $this->formatRule('Default', $x, $y, '', $action, $gotoId, $bpsLimit, $ppsLimit, $srcMask, $dstMask);
    }

    public function addRule(string $name, int $x, int $y, string $bpf, string $action, ?int $gotoId = null, ?int $bpsLimit = null, ?int $ppsLimit = null, ?int $srcMask = null, ?int $dstMask = null) {        
        $this->rule_manifest[] = $this->formatRule($name, $x, $y, $bpf, $action, $gotoId, $bpsLimit, $ppsLimit, $srcMask, $dstMask);
    }

    public function save($options = []) {

        if($this->rule_manifest !== []) {
            $this->rules = json_encode($this->rule_manifest);
        }

        if(($this->unencoded_rules !== null && $this->unencoded_rules !== [])) {
            $this->unencoded_rules = json_encode($this->unencoded_rules);
        }
        else {
            // If there's no unencoded rules, we actually have to build it up...

            $unencoded_temporary = [];

            foreach($this->rule_manifest as $i => $rule) {

                if($i + 1 === sizeof($this->rule_manifest)) {
                    break;
                }

                $temp = [
                    'name' => $rule['name'] ?? "Rule $i",
                    'type' => $rule['matchAction'],
                    'x' => 0,
                    'y' => $i * 200,
                ];

                if($rule['matchAction'] === 'police') {
                    $temp = [
                        ...$temp,
                        'bps' => $rule['bpsLimit'],
                        'pps' => $rule['ppsLimit']
                    ];
                }
                elseif($rule['matchAction'] === 'goto') {
                    $temp['goto'] = $rule['gotoNext'];
                }

                if($i !== sizeof($this->rule_manifest) - 1) {
                    $temp = [
                        ...$temp,
                        'clauses' => [[
                            'operator' => 'start',
                            'type' => 'Freeform',
                            'negation' => false,
                            'value' => [ $rule['bpfRule'] ],
                            'bpf' => $rule['bpfRule']
                        ]]
                    ];
                }

                $unencoded_temporary[] = $temp;
            }

            // Set the last to the default
            $this->unencoded_rules = json_encode($unencoded_temporary);
        }

        return parent::save($options);
    }

    public function compile() {
        $rules = [];

        foreach($this->rule_manifest as $y) {
            switch($y['matchAction']) {
                case 'police':
                    $rules[] = [
                        'bpfRule'       => $y['bpfRule'] ?? null,
                        'matchAction'   => 'police',


                        // These should be present, it is bad to assume all data is clean even with adequate steps. Mistakes happen. But they should not detonate a ruleset.
                        'bpsLimit' => $y['bpsLimit'] ?? 0,
                        'ppsLimit' => $y['ppsLimit'] ?? 0,

                        // These may not be present. If they're not, don't include them.
                        ...( isset($y['srcMask']) ? [ 'srcMask' => $y['srcMask'] ] : [] ),
                        ...( isset($y['dstMask']) ? [ 'srcMask' => $y['dstMask'] ] : [] )
                    ];
                break;
                case 'goto':
                    $rules[] = [
                        'bpfRule'       => $y['bpfRule'] ?? null,
                        'matchAction'   => 'redirect',
                        'gotoID'      => $y['gotoNext']
                    ];
                break;
                case 'accept':
                case 'drop':
                    $rules[] = [
                        'bpfRule'       => $y['bpfRule'] ?? null,
                        'matchAction'   => $y['matchAction']
                    ];
                break;
            }
        }

        // Check if the last rule has no bpfRule, and if it does not, and it is an accept, remove it.
        $last_key = array_key_last($rules);
        if($rules[$last_key]['bpfRule'] === null && $rules[$last_key]['matchAction'] === 'accept') {
            unset($rules[$last_key]);
        }
        elseif($rules[$last_key]['bpfRule'] === null) {
            unset($rules[$last_key]['bpfRule']);
        }

        return json_decode(json_encode($rules));
    }

    // This will eventually need to live in a repository
    public function checkCompilationValidity() {

        $endpoints = GoliathAppliance::join('goliath_subregions', 'goliath_subregions.id', 'goliath_appliances.subregion_id')->whereIn('telemetry_identifier', config('creatia.endpoint.goliath'))->select('address')->orderBy('updated_at')->get()->pluck('address')->toArray();

        foreach($endpoints as $i => $x) {


            try {
                $response = Http::withHeaders([ 'Content-Type' => 'application/json' ])->retry(1)->timeout(3)->post("http://$x:5000/validate/cost", (object)[
                    "420" => $this->compile()
                ]);


                if($response->successful()) {
                    $percentage = (($response->json()['maxCost'] / $response->json()['budget']) * 100);

                    return [
                        'valid' => $response->json()['valid'],
                        'capacity' => $percentage < 100 && !$response->json('withinBudget') ? 200 : $percentage
                    ];
                }
            }
            catch(\Throwable $t) {
                \Log::warning($t);
            }

        }

        throw new Exception('Could not reach any appliance.');

    }
}
