<?php

namespace App\Helpers\Utility;

use Carbon\Carbon;
use Illuminate\Support\Facades\DB;

/**
 * Resolves attack destinations into the organisations entitled to hear about them, and the label each
 * destination carries.
 *
 * This is the single rulebook for reporting visibility - OculiProcessEvents uses it to decide who an
 * event is shipped to, and the Mitigation notification pipeline uses it to decide who a notification
 * may be sent to. Keeping both on the same code path means the two can never drift apart: an
 * organisation is told about an attack if and only if Oculi holds a copy of that attack for them.
 *
 * Visibility mirrors Prefixes::getOrganisationPrefixes() - a prefix is visible to the organisation that
 * owns it, to any organisation subtenanted onto it directly, and to any organisation subtenanted onto
 * one of its supernets with `also_subnets` set. Subtenancies are honoured as at the time of the attack,
 * the same window ReportPermitSubtenant uses, so replays and backfills attribute against who held the
 * block then rather than who holds it now.
 *
 * Containment alone is not entitlement. Walking outward from the destination, the most specific live
 * prefix decides who controls it, and the walk ends at the first prefix held by anyone else - exactly
 * as a more specific announcement takes the traffic away from the covering route. Whoever holds the
 * covering block, or is subtenanted onto it, is told nothing about attacks inside a block that has
 * been carved out from under them, and a cascade issued above that boundary does not reach below it.
 * Only live prefixes take part, so a carve-out that is later removed stops shadowing its parent.
 *
 * Attacks land on single addresses, and the only label a destination carries is the name of a prefix
 * that IS that address - a /32 or /128 somebody has named. The covering block's name is never used as
 * a stand-in, so most destinations carry no label at all, and the one that exists is the same for
 * every organisation told about the attack.
 *
 * The naive shape of this is one containment query plus one subtenancy walk per destination, which is
 * thousands of round trips a minute. Instead each batch resolves in three queries regardless of size:
 * every containing prefix for every distinct destination in the batch (one index probe per
 * destination), the supernet closure across just those prefixes, and their subtenancies. Everything
 * after that is in-memory set work. The containment probe depends on the expression index created in
 * 2026_09_25_000001_oculi_attribution_indexing - without it this degrades to a sequential scan of
 * `prefixes` per destination.
 */
class PrefixAttribution
{

    /**
     * Resolve a batch of destinations into the prefixes that contain them, the organisations that can
     * see each of those prefixes, and the label of any that is the destination itself. Three queries,
     * independent of how many destinations are passed.
     *
     * Each chain holds only the prefixes a destination's controlling organisation holds, nearest first;
     * anything above the ownership boundary is left out. Every grant records the prefix it was issued
     * on (`via`), which for a cascade is the supernet rather than the prefix it landed on.
     *
     * @param  array<int, string>  $destinations
     * @return array{labels: array<string, string>, chains: array<string, array<int, int>>, grants: array<int, array<int, array{organisation: int, from: ?Carbon, until: ?Carbon, via: int}>>}
     */
    public function resolve(array $destinations): array
    {
        $empty = [ 'labels' => [], 'chains' => [], 'grants' => [] ];

        if(($destinations = array_values($destinations)) === []) {
            return $empty;
        }

        // Positional rather than value keyed, so nothing rides on how Postgres chooses to render an inet.
        $tuples = [];

        foreach(array_keys($destinations) as $index) {
            $tuples[] = "($index, ?::inet)";
        }

        $candidates = DB::select(
            'SELECT d.idx,
                    p.id,
                    p.organisation_id,
                    p.name,
                    (set_masklen(p.address, p.mask) = d.destination)::int AS exact
               FROM (VALUES ' . implode(', ', $tuples) . ') AS d(idx, destination)
               JOIN prefixes p ON set_masklen(p.address, p.mask) >>= d.destination
              WHERE p.deleted_at IS NULL
              ORDER BY d.idx ASC, p.mask DESC, p.id ASC',
            $destinations
        );

        if($candidates === []) {
            return $empty;
        }

        $labels = [];
        $chains = [];
        $grants = [];

        // Per destination: who holds the most specific prefix, and whether the walk outward has already left their holdings.
        $controlling = [];
        $sealed = [];

        foreach($candidates as $candidate) {

            $idx = (int) $candidate->idx;
            $owner = (int) $candidate->organisation_id;

            if(isset($sealed[$idx])) {
                continue;
            }

            /**
             * Nearest first, courtesy of the ORDER BY. The first prefix seen for a destination decides
             * who controls it; the first prefix held by anyone else ends the chain for good, and a
             * prefix that lands in nobody's chain is never loaded at all.
             */
            if(!isset($controlling[$idx])) {
                $controlling[$idx] = $owner;
            }
            elseif($controlling[$idx] !== $owner) {
                $sealed[$idx] = true;
                continue;
            }

            $id = (int) $candidate->id;

            $chains[$destinations[$idx]][] = $id;

            // A prefix that is the destination itself, and has been given a name, is the destination's label.
            if((int) $candidate->exact === 1 && $candidate->name !== null && $candidate->name !== '') {
                $labels[$destinations[$idx]] = $candidate->name;
            }

            if(isset($grants[$id])) {
                continue;
            }

            // Ownership is unconditional and has no window attached to it.
            $grants[$id] = [[ 'organisation' => $owner, 'from' => null, 'until' => null, 'via' => $id ]];
        }

        $identities = array_keys($grants);

        /**
         * `supernets` is a full closure rather than a parent pointer (see PrefixSortV2), so every
         * ancestor is its own row. Every ancestor of a candidate contains the same destination and is
         * therefore a candidate itself, which is why both sides can be constrained to the batch.
         */
        $ancestry = [];

        foreach(DB::table('supernets')->select('supernet_id', 'subnet_id')
            ->whereIn('subnet_id', $identities)
            ->whereIn('supernet_id', $identities)
            ->distinct()
            ->cursor() as $link) {
                $ancestry[(int) $link->subnet_id][] = (int) $link->supernet_id;
            }

        /**
         * Soft deleted subtenancies are deliberately included - a subtenancy that has since been torn
         * down was still in force for an attack that happened underneath it.
         */
        $cascading = [];

        foreach(DB::table('prefix_subtenants')->select('prefix_id', 'subtenant_id', 'also_subnets', 'created_at', 'deleted_at')
            ->whereIn('prefix_id', $identities)
            ->cursor() as $subtenancy) {

            $grant = [
                'organisation'  => (int) $subtenancy->subtenant_id,
                'from'          => $subtenancy->created_at === null ? null : Carbon::parse($subtenancy->created_at),
                'until'         => $subtenancy->deleted_at === null ? null : Carbon::parse($subtenancy->deleted_at),
                'via'           => (int) $subtenancy->prefix_id
            ];

            // A subtenancy always covers the prefix it was granted on...
            $grants[(int) $subtenancy->prefix_id][] = $grant;

            // ...and, when it cascades, everything sitting underneath it as well.
            if((int) $subtenancy->also_subnets === 1) {
                $cascading[(int) $subtenancy->prefix_id][] = $grant;
            }
        }

        foreach($ancestry as $subnet_id => $supernet_ids) {
            foreach($supernet_ids as $supernet_id) {
                foreach($cascading[$supernet_id] ?? [] as $grant) {
                    $grants[$subnet_id][] = $grant;
                }
            }
        }

        return [ 'labels' => $labels, 'chains' => $chains, 'grants' => $grants ];
    }

    /**
     * Walk a destination's prefix stack from the most specific outwards and decide which organisations
     * are entitled to it, each paired with the first prefix that entitled them. An organisation whose
     * grant on a tight prefix had not been issued yet at the time of the attack is still entitled by
     * whatever it did hold further up.
     *
     * @param  array<int, int>  $chain
     * @param  array<int, array<int, array{organisation: int, from: ?Carbon, until: ?Carbon, via: int}>>  $grants
     * @return array<int, int>  Organisation identifier to prefix identifier.
     */
    public function nearest(array $chain, array $grants, Carbon $start, Carbon $finish): array
    {
        $nearest = [];

        foreach($chain as $prefix_id) {

            foreach($grants[$prefix_id] ?? [] as $grant) {

                if(isset($nearest[$grant['organisation']])) {
                    continue;
                }

                /**
                 * Grants are shared across the batch, so a cascade can sit on a prefix that is in this
                 * chain while the supernet it was issued on is shadowed for this particular destination.
                 */
                if(!in_array($grant['via'], $chain, true)) {
                    continue;
                }

                if($grant['from'] !== null && $grant['from']->greaterThan($start)) {
                    continue;
                }

                if($grant['until'] !== null && $grant['until']->lessThan($finish)) {
                    continue;
                }

                $nearest[$grant['organisation']] = $prefix_id;
            }
        }

        return $nearest;
    }
}
