<?php

namespace App\Console\Commands\Oculi;

use App\Models\Organisation;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Str;

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

    protected $signature = 'oculi:sync-information';

    protected $description = 'Synchronise organisation and prefix visibility information against Oculi so we have accurate information over there.';

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

    /**
     * Every (block, organisation) pair in force, derived the way Prefixes::getOrganisationPrefixes sees
     * things: a live prefix is viewed by its owner, by anyone subtenanted onto it, and by anyone
     * subtenanted with `also_subnets` onto one of its supernets. Oculi wants the largest block each
     * organisation holds rather than its pieces, so a block is dropped when the same organisation views
     * another block containing it, and anything tighter than a /24 or /48 is left out because no carpet
     * can ever fit inside it. The containment probe goes back to `prefixes` rather than the pairs so the
     * expression index from 2026_09_25_000001_oculi_attribution_indexing serves it.
     *
     * The list only decides whether Oculi CREATES a carpet. Its members are the organisation's own event
     * copies, which attribution already withholds inside somebody else's carve-out, so a block listed
     * for an organisation that holds no copies there can never yield a carpet. That is why this need not
     * repeat the ownership boundary PrefixAttribution draws.
     *
     * @return array<int, array{prefix: string, organisation_id: int}>
     */
    private function viewers(): array
    {
        $rows = DB::select("
            WITH live AS (
                SELECT id,
                       network(set_masklen(address, mask)) AS block,
                       organisation_id
                  FROM prefixes
                 WHERE deleted_at IS NULL
                   AND ((family(address) = 4 AND mask <= 24) OR (family(address) = 6 AND mask <= 48))
            ),
            viewers AS (
                SELECT id AS prefix_id, organisation_id
                  FROM live
                 UNION
                SELECT s.prefix_id, s.subtenant_id
                  FROM prefix_subtenants s
                  JOIN live ON live.id = s.prefix_id
                 WHERE s.deleted_at IS NULL
                 UNION
                SELECT n.subnet_id, s.subtenant_id
                  FROM prefix_subtenants s
                  JOIN supernets n ON n.supernet_id = s.prefix_id
                  JOIN live ON live.id = n.subnet_id
                 WHERE s.deleted_at IS NULL
                   AND s.also_subnets = 1
            ),
            pairs AS (
                SELECT DISTINCT live.block, v.organisation_id
                  FROM viewers v
                  JOIN live ON live.id = v.prefix_id
            )
            SELECT text(p.block) AS prefix, p.organisation_id
              FROM pairs p
             WHERE NOT EXISTS (
                    SELECT 1
                      FROM prefixes q
                      JOIN viewers v ON v.prefix_id = q.id
                     WHERE q.deleted_at IS NULL
                       AND v.organisation_id = p.organisation_id
                       AND set_masklen(q.address, q.mask) >> p.block
                   )
             ORDER BY 1, 2
        ");

        return array_map(fn ($row) => [ 'prefix' => $row->prefix, 'organisation_id' => (int) $row->organisation_id ], $rows);
    }

    /**
     * PUT one complete picture. The response on success; null on any failure, which has been logged.
     */
    private function put(PendingRequest $oculi, string $path, array $payload): ?Response
    {
        try {

            $response = $oculi->put($path, $payload);

            if($response->successful()) {
                return $response;
            }

            $this->log_error("Communications failure in sending information to Oculi.", [
                'path'      => $path,
                'status'    => $response->status(),
                'reason'    => $response->reason(),
                'response'  => Str::limit($response->body(), 2000)
            ]);

        }
        catch(\Throwable $ex) {
            $this->log_error("Creatia failure in sending information to Oculi.", [
                'path'      => $path,
                'exception' => $ex->getMessage()
            ]);
        }

        return null;
    }

    public function handle()
    {
        if(($endpoint = config('creatia.oculi.url')) === null || $endpoint === '') {
            $this->log_warning("Not running as no Oculi endpoint is configured.", []);
            return 0;
        }

        $verify = config('creatia.oculi.cert');
        $verify = ($verify === null || $verify === '') ? true : $verify;

        $oculi = Http::baseUrl(config('creatia.oculi.url'))
            ->withBasicAuth('creatia', config('creatia.oculi.secret'))
            ->withOptions(['verify' => $verify])
            ->acceptJson()
            ->timeout((int) config('creatia.oculi.timeout', 300));

        $synchronised = true;

        // Every organisation there has ever been, deleted ones included - their attacks still need a name against them.
        $organisations = Organisation::withTrashed()->select('id', 'name')->get();

        if(($response = $this->put($oculi, 'mitigation/organisations', [ 'organisations' => $organisations ])) !== null) {
            $this->log_event("Organisation information synchronised with Oculi.", [
                'organisations' => sizeof($organisations)
            ]);
        }
        else {
            $synchronised = false;
        }

        // The prefix picture is attempted whatever happened above - it is a complete picture in its own right.
        try {
            $prefixes = $this->viewers();
        }
        catch(\Throwable $ex) {
            $this->log_error("Creatia failure in deriving prefix visibility for Oculi.", [
                'exception' => $ex->getMessage()
            ]);

            return 1;
        }

        if(($response = $this->put($oculi, 'mitigation/prefixes', [ 'prefixes' => $prefixes ])) !== null) {

            // A large `removed` on a routine pass means the picture was truncated somewhere, so the counts are worth keeping.
            $this->log_event("Prefix visibility synchronised with Oculi.", [
                'pairs'     => sizeof($prefixes),
                'added'     => $response->json('added'),
                'removed'   => $response->json('removed')
            ]);
        }
        else {
            $synchronised = false;
        }

        return $synchronised ? 0 : 1;
    }
}
