<?php

namespace App\Console\Commands\Hook;

use App\Helpers\Utility\SymEncrypt;
use App\Models\Organisation;
use App\Models\Webhook;
use App\Models\WebhookHookType;
use App\Models\WebhookMessage;
use App\Models\WebhookMessageStatus;
use App\Models\WebhookUsageType;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;

class HookProcess extends \App\Console\Commands\BaseCommand
{

    protected $signature = 'hook:process';

    protected $description = 'Send queued mitigation notifications to their webhooks.';

    // Minutes before a message is aged out and not sent. This is to avoid message floods.
    private int $freshness = 60;

    // We gate the type of events we'll actually send to prevent anything errant sending through.
    private const KINDS = [ 'attack-start', 'attack-finish', 'attack-report' ];

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        $sym_encrypt = new SymEncrypt(config('creatia.reporting.encryption_key'));

        Organisation::select('id')->each(function($organisation) use($sym_encrypt) {

            $hooks = Webhook::where('organisation_id', '=', $organisation->id)->where('usage_type', '=', WebhookUsageType::Mitigation->value)->select('id', 'hook', 'hook_type', 'usage_type')->get()->toArray();

            if(sizeof($hooks) === 0) {
                return true;
            }

            foreach($hooks as $hook) {

                $messages = WebhookMessage::where([
                    [ 'webhook_id', '=', $hook['id'] ],
                    [ 'status', '=', WebhookMessageStatus::Pending->value ],
                    [ 'message_type', '=', $hook['usage_type'] ],
                    [ 'organisation_id', '=', $organisation->id ],
                    [ 'created_at', '>', Carbon::now()->subMinutes($this->freshness) ]
                ])
                ->select('id', 'payload')
                ->orderBy('id', 'asc')
                ->get()
                ->toArray();

                if(sizeof($messages) === 0) {
                    continue;
                }

                $decrypted_hook = $sym_encrypt->decrypt($hook['hook']);

                foreach($messages as $message) {

                    try {
                        $outcome = false;

                        // Error on an unknown notification type
                        if(!in_array($message['payload']['kind'] ?? '', self::KINDS, true)) {
                            throw new \UnexpectedValueException('Unknown notification kind: ' . ($message['payload']['kind'] ?? '(none)'));
                        }

                        // If an identical row is queued, resolve it
                        if(WebhookMessage::where([['webhook_id', '=', $hook['id']], [DB::raw('payload::text'), '=', json_encode($message['payload'])], ['id', '!=', $message['id']]])->count()) {
                            $outcome = true;
                        }
                        else {

                            $this->log_event("Mitigation webhook notification is being sent.", [
                                'organisation'  => $organisation->id,
                                'hook'          => $hook['id'],
                                'message'       => $message['id']
                            ]);

                            switch($hook['hook_type']) {
                                case WebhookHookType::Slack->value:
                                    $outcome = Http::withHeaders([ 'Content-Type' => 'application/json' ])->timeout(6)->post($decrypted_hook, [
                                        'text' => $this->render($message['payload']),
                                    ])->successful();

                                    break;
                                case WebhookHookType::Discord->value:
                                    $outcome = Http::withHeaders([ 'Content-Type' => 'application/json' ])->timeout(6)->post($decrypted_hook, [
                                        'content' => $this->render($message['payload']),
                                    ])->successful();
                                    break;
                                case WebhookHookType::Raw->value:
                                    $outcome = Http::withHeaders([ 'Content-Type' => 'application/json' ])->timeout(6)->post($decrypted_hook, $message['payload'])->successful();
                                    break;
                            }

                            sleep((int) config('creatia.hook.pace', 2)); // Paced so chat platform rate limits are never provoked; tests zero this via config.
                        }

                        WebhookMessage::where('id', '=', $message['id'])->update([
                            'sent_at'   => Carbon::now(),
                            'status'    => $outcome ? WebhookMessageStatus::Sent->value : WebhookMessageStatus::Error->value
                        ]);
                    }
                    catch(\Throwable $ex) {

                        $this->log_error("Mitigation webhook notification failed to send.", [
                            'organisation'  => $organisation->id,
                            'hook'          => $hook['id'],
                            'message'       => $message['id'],
                            'exception'     => $ex
                        ]);

                        WebhookMessage::where('id', '=', $message['id'])->update([
                            'sent_at'   => Carbon::now(),
                            'status'    => WebhookMessageStatus::Error->value
                        ]);
                    }
                }

                // Unset it as soon as we're done with. We should keep it out of memory once it's done.
                unset($decrypted_hook);
            }

            return true;
        });

        return 0;
    }

    /**
     * Assemble the human reading of a structured payload for the chat platforms. The descriptor is
     * safe to speak - the pipeline stored the nearest name the organisation is entitled to.
     */
    private function render(array $payload): string
    {
        $attack = $payload['attack'] ?? [];

        $target = ($attack['destination'] ?? 'unknown') . (($attack['descriptor'] ?? '') !== '' && $attack['descriptor'] !== null ? " ({$attack['descriptor']})" : '');
        $observed = ($attack['gbps'] ?? 0) . ' Gbps / ' . ($attack['mpps'] ?? 0) . ' Mpps';

        $started = isset($attack['started_at']) && $attack['started_at'] !== null ? Carbon::createFromTimestamp($attack['started_at'])->format('Y-m-d H:i:s') . ' UTC' : 'unknown';
        $finished = isset($attack['finished_at']) && $attack['finished_at'] !== null ? Carbon::createFromTimestamp($attack['finished_at'])->format('Y-m-d H:i:s') . ' UTC' : 'unknown';

        $lines = match($payload['kind'] ?? '') {
            // Mitigation Events
            'attack-start' => [
                'A DDoS attack against your network has crossed your alerting threshold and is being mitigated.',
                "Target: $target",
                "Detected: $started",
                "Observed so far: $observed",
            ],
            'attack-finish' => [
                'A mitigated DDoS attack against your network has concluded.',
                "Target: $target",
                "Started: $started",
                "Concluded: $finished",
                "Peak: $observed",
                'A report will follow once analysis completes, if the attack recorded enough traffic.',
            ],
            'attack-report' => [
                'The report for a mitigated DDoS attack against your network is ready to download.',
                "Target: $target",
                "Window: $started - $finished",
                "Peak: $observed",
                'Report: ' . ($payload['report']['url'] ?? ''),
                ...( isset($payload['report']['expires_at']) ? [ 'The link expires on ' . Carbon::createFromTimestamp($payload['report']['expires_at'])->format('jS F Y') . '.' ] : []),
            ],
            default => throw new \UnexpectedValueException('Unrenderable notification kind: ' . ($payload['kind'] ?? '(none)')),
        };

        $lines[] = 'Reference: ' . ($payload['reference'] ?? 'unknown');

        return implode("\n", $lines);
    }
}
