<?php

namespace App\Console\Commands\Mitigation;

use App\Helpers\Utility\PrefixAttribution;
use App\Models\Mitigation\MitigationAttackState;
use App\Models\Mitigation\MitigationNotification;
use App\Models\Mitigation\MitigationNotificationStatus;
use App\Models\Mitigation\MitigationNotificationType;
use App\Models\Mitigation\MitigationReportLink;
use App\Models\Organisation;
use App\Models\Webhook;
use App\Models\WebhookMessage;
use App\Models\WebhookMessageStatus;
use App\Models\WebhookUsageType;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

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

    protected $signature = 'mitigation:queue-reports';

    protected $description = 'Queue ready attack report links for organisations entitled to them.';

    // Days that a link will stay valid for.
    private int $expiry = 7;

    private PrefixAttribution $attribution;

    public function __construct()
    {
        $this->attribution = new PrefixAttribution();

        parent::__construct();
    }

    public function handle()
    {
        $this->expiry = (int) config('creatia.oculi.link_days', 7);

        // Only select organisations that have opted into using webhooks for reporting.
        Organisation::join('report_organisation_attributes', 'organisation_id', '=', 'organisations.id')->where('send_webhook', '=', 1)->select(
            'organisations.id',
            'send_individual',
            'minimum_threshold_gbps',
            'minimum_threshold_mpps'
        )->each(function($organisation) {

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

            // Notifications that are currently pended. So we can keep track of that.
            $pending = MitigationNotification::where([
                    [ 'organisation_id', '=', $organisation->id ],
                    [ 'status', '=', MitigationNotificationStatus::Pending->value ],
                    [ 'type', '=', MitigationNotificationType::Report->value ]
                ])
                ->orderBy('id', 'asc')
                ->get();

            if($pending->isEmpty()) {
                return true;
            }

            // If there's no hooks or individual reports aren't being sent.
            if(sizeof($hooks) === 0 || !$organisation->send_individual) {
                MitigationNotification::whereIn('id', $pending->pluck('id'))->update([ 'status' => MitigationNotificationStatus::Suppressed->value ]);
                return true;
            }

            // Our actual understanding of the attack states at this time.
            $states = MitigationAttackState::where('organisation_id', '=', $organisation->id)
                ->whereIn('attack_id', $pending->pluck('attack_id')->unique()->values())
                ->get()
                ->keyBy('attack_id');

            // Whether someone is entitled to see a given destination based on the prefixes we track within Creatia for organisations.
            $resolution = $this->attribution->resolve($states->pluck('destination')->filter()->unique()->values()->all());

            foreach($pending as $notification) {

                $state = $states->get($notification->attack_id);

                // If the state has aged out, it's not ready, or Oculi never collected it in full, there is no report to send. Suppress.
                if($state === null || !$state->ready || !$state->report) {
                    $notification->update([ 'status' => MitigationNotificationStatus::Suppressed->value ]);
                    continue;
                }

                // Ready but it's too small, suppress it.
                if($state->gbps < $organisation->minimum_threshold_gbps || $state->mpps < $organisation->minimum_threshold_mpps) {
                    $notification->update([ 'status' => MitigationNotificationStatus::Suppressed->value ]);
                    continue;
                }

                // If the organisation is entitled to the destination.
                $visible = $this->attribution->nearest(
                    $resolution['chains'][$state->destination] ?? [],
                    $resolution['grants'],
                    $state->started_at ?? Carbon::now(),
                    $state->finished_at ?? Carbon::now()
                );

                if(!isset($visible[$organisation->id])) {

                    $this->log_warning('Bad queue attempt for destination report.', [
                        'organisation'  => $organisation->id,
                        'attack'        => $notification->attack_id,
                        'destination'   => $state->destination
                    ]);

                    $notification->update([ 'status' => MitigationNotificationStatus::Denied->value ]);
                    continue;
                }

                // Valid, generate link and message, update the status.
                DB::transaction(function() use($organisation, $notification, $state, $hooks) {

                    $token = '';

                    do {
                        $token = Str::random(128);
                    }
                    while(MitigationReportLink::where('token', '=', $token)->count());

                    $link = new MitigationReportLink([
                        'token'             => $token,
                        'attack_id'         => $notification->attack_id,
                        'organisation_id'   => $organisation->id,
                        'expires_at'        => Carbon::now()->addDays($this->expiry)
                    ]);

                    $link->save();

                    foreach($hooks as $hook) {

                        (new WebhookMessage([
                            'key'               => 'mr-' . $notification->id,
                            'message_type'      => WebhookUsageType::Mitigation->value,
                            'payload'           => $this->compose($state, $link),
                            'webhook_id'        => $hook['id'],
                            'status'            => WebhookMessageStatus::Pending->value,
                            'sent_at'           => null,
                            'organisation_id'   => $organisation->id
                        ]))->save();
                    }

                    $notification->update([ 'status' => MitigationNotificationStatus::Sent->value ]);

                    return true;
                });
            }

            return true;
        });

        return 0;
    }

    private function compose(MitigationAttackState $state, MitigationReportLink $link): array
    {
        $reference = strtoupper(substr(str_replace('-', '', $state->attack_id), 0, 12));

        return [
            'kind'      => 'attack-report',
            'version'   => 1,
            'reference' => "R-$reference",
            'attack'    => [
                'id'            => $state->attack_id,
                'destination'   => $state->destination,
                'descriptor'    => $state->descriptor,
                'started_at'    => $state->started_at?->getTimestamp(),
                'finished_at'   => $state->finished_at?->getTimestamp(),
                'gbps'          => round($state->gbps, 2),
                'mpps'          => round($state->mpps, 2)
            ],
            'report'    => [
                'url'           => url('api/guest/mitigation/report/download/' . $link->token),
                'expires_at'    => $link->expires_at?->getTimestamp()
            ]
        ];
    }
}
