<?php

namespace App\Models;

use App\Models\Goliath\PrefixGoliathFirewall;
use App\Models\Goliath\PrefixGoliathFlag;
use App\Models\Goliath\PrefixGoliathInline;
use App\Models\Goliath\PrefixGoliathLoose;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

use App\Models\PrefixSubtenants;
use App\Models\Supernets;
use App\Traits\Owned;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Facades\Log;

class Prefixes extends Model
{
    use SoftDeletes, HasFactory, Owned;

    protected $fillable = [
        'name',
        'address',
        'mask',
        'seen_at',
        'still_announced',
        'created_source',
        'organisation_id',
        'asn_id',
        'setup_required'
    ];

    protected $casts = [
        'created_at' => 'datetime',
        'seen_at' => 'datetime',
        'updated_at' => 'datetime',
    ];

    public static function validOwner($_x, $_organisation_id) {
        $x = Prefixes::where([['organisation_id', '=', $_organisation_id],['id', '=', $_x]])->first();
        
        return $x ? $x : false;
    }

    public function getCidr() {
        return $this->address . '/' . $this->mask; 
    }

    public static function isTrueSubnet($_prefix_id) {
        return Supernets::where('supernet_id', '=', $_prefix_id)->count() ? false : true;
    }

    public static function getSubtenancyPrefixes($_subtenant_id) {
        $prefix_subtenants = PrefixSubtenants::where([['subtenant_id', '=', $_subtenant_id]])->get();

        $indirect_sub_prefixes = Prefixes::select('prefixes.id', 'address', 'mask', 'still_announced', 'created_source')->leftJoin('supernets', 'subnet_id', '=', 'prefixes.id')->whereIn('supernet_id', $prefix_subtenants->where('also_subnets', '=', 1)->pluck('prefix_id'))->get();
        $direct_sub_prefixes = Prefixes::whereIn('id', $prefix_subtenants->pluck('prefix_id'))->get();

        return $direct_sub_prefixes->merge($indirect_sub_prefixes)->unique();
    }

    public static function getOrganisationPrefixes($_subtenant_id) {
        // Supernet related prefixes
        $owned_prefixes = Prefixes::where('organisation_id', '=', $_subtenant_id)->get();

        // Subtenanting related prefixes
        $prefix_subtenants = PrefixSubtenants::where([['subtenant_id', '=', $_subtenant_id]])->get();

        $indirect_sub_prefixes = Prefixes::select('prefixes.id', 'address', 'mask', 'still_announced', 'created_source')->leftJoin('supernets', 'subnet_id', '=', 'prefixes.id')->whereIn('supernet_id', $prefix_subtenants->where('also_subnets', '=', 1)->pluck('prefix_id'))->get();
        $direct_sub_prefixes = Prefixes::whereIn('id', $prefix_subtenants->pluck('prefix_id'))->get();

        return $direct_sub_prefixes->merge($indirect_sub_prefixes)->merge($owned_prefixes)->unique();
    }

    public static function getOrganisationHasPrefix($_subtenant_id, $_prefix_id) {
        // Supernet related prefixes
        $owned_prefixes = Prefixes::where('organisation_id', '=', $_subtenant_id)->where('id', '=', $_prefix_id)->get();

        if($owned_prefixes->count() != 0) {
            return $owned_prefixes->first();
        }

        // Subtenanting related prefixes
        $prefix_subtenants = PrefixSubtenants::where([['subtenant_id', '=', $_subtenant_id]])->get();

        $indirect_prefixes = Prefixes::select('prefixes.id', 'address', 'mask', 'still_announced')->leftJoin('supernets', 'subnet_id', '=', 'prefixes.id')->whereIn('supernet_id', $prefix_subtenants->where('also_subnets', '=', 1)->pluck('prefix_id'))->where('prefixes.id', '=', $_prefix_id)->get();

        if($indirect_prefixes->where('id', '=', $_prefix_id)->count() != 0) {
            return $indirect_prefixes->where('id', '=', $_prefix_id)->first();
        }
        
        $direct_prefixes = Prefixes::whereIn('id', $prefix_subtenants->pluck('prefix_id'))->get();

        if($direct_prefixes->where('id', '=', $_prefix_id)->count() != 0) {
            return $direct_prefixes->where('id', '=', $_prefix_id)->first();
        }

        return false;
    }

    /**
     * A live prefix held by another organisation that sits between a parent and a would-be subnet of it - more specific than the parent, yet still containing the new range.
     * Attribution follows the most specific holder, so creating beneath one of these would take that organisation's attacks away from them. Returns the contesting prefix or false.
     */
    public static function getContestingPrefix($_cidr, $_parent_mask, $_organisation_id) {
        return Prefixes::where([
                ['organisation_id', '!=', $_organisation_id],
                ['mask', '>', $_parent_mask]
            ])
            ->whereRaw('set_masklen(address, mask) >>= ?::inet', [$_cidr])
            ->orderBy('mask', 'desc')
            ->first() ?? false;
    }

    public static function getIsInAnnouncedSupernet($_subnet_id) {
        return Prefixes::leftJoin('supernets', 'supernet_id', '=', 'prefixes.id')->where('subnet_id', '=', $_subnet_id)->where('still_announced', '=', '1')->count() ? true : (Prefixes::find($_subnet_id)->still_announced ? true : false); 
    }

    private static function isIpInSubnet($ip, $subnet) {
        // Split IP and subnet into their components
        list($subnetAddress, $subnetMask) = explode('/', $subnet);
        $subnetMask = intval($subnetMask);

        // Validate subnet mask

        if ($subnetMask < 0 || $subnetMask > 32) {
            throw new \Exception("Invalid subnet mask. Must be between 0 and 32.");
        }

        // Convert IP addresses to long integers
        $ipLong = ip2long($ip);
        $subnetLong = ip2long($subnetAddress);

        // Calculate subnet mask as a long integer
        if ($subnetMask == 32) {
            $mask = -1;
        } else {
            $mask = ~((1 << (32 - $subnetMask)) - 1);
        }

        // Perform bitwise AND to check if IP is in subnet
        return ($ipLong & $mask) === ($subnetLong & $mask);
    }

    public static function getOrganisationOwnsPrefixes($_organisation_id, $_list) {
        // The list will be an array containing two members per item. The members will be the date sighted, and prefix.
        // We need to use the date so we can determine if it was owned by that organisation at that time.

        $not_owned = [];
        $is_owned = [];

        foreach(array_chunk($_list, 5000) as $members) {

            $query = Prefixes::withTrashed()
                ->select(
                    \DB::raw('CONCAT(address, \'/\', mask) as prefix')
                )
                ->where([
                    ['organisation_id', '=', $_organisation_id],
                    [\DB::raw('family(address)'), '=', 4],
                    ['mask', '>', 8],
                    ['mask', '<', 33],
                    ['address', '!=', '0.0.0.0']
                ])
                ->whereNotIn(\DB::raw('CONCAT(address, \'/\', mask)'), ['85.154.132.0/2', '223.29.239.0/2', '203.10.98.51/31', '39.38.208.0/2', '198.178.119.8/27', '189.1.164.0/2'])
                ->where(function($a) use($members) {

                    foreach($members as $member) {

                        $a->orWhere(function($b) use($member) {
                            $b->whereRaw('CONCAT(address, \'/\', mask)::cidr >> ?', [$member['address']])
                            ->where('created_at', '<=', $member['time'])
                            ->where(function($c) use($member) {
                                $c->whereNull('deleted_at')
                                    ->orWhere('deleted_at', '>=', $member['time']);
                            });
                        });
                        
                    }
                });

            $owned = $query->get();

            foreach($members as $destination) {

                $found = false;

                foreach($owned as $parent) {
                    if(static::isIpInSubnet($destination['address'], $parent['prefix'])) {
                        $found = true;
                        break;
                    }
                }

                if($found) {
                    $is_owned[] = $destination['address'];
                }
                else {
                    $not_owned[] = $destination['address'];
                }
            }
        }

        return [sizeof($not_owned) === 0, $not_owned, $is_owned];
    }

    /**
     * This selects subnets that are true children of a given prefix. That is to say, there are no other parents for those child prefixes and they have no BGP visibility at this time.
     */
    public function getTrueChildPrefixIdentities() {

        // Select all relevant subnets WHERE those subnets aren't already deleted AND they are not presently announced.
        $child_subnets = Supernets::join('prefixes', 'prefixes.id', '=', 'supernets.subnet_id')
            ->whereNull('prefixes.deleted_at')
            ->where([ ['supernet_id', '=', $this->id], ['subnet_id', '!=', $this->id] ])
            ->where(function($query) {
                $query->where('prefixes.still_announced', '!=', 1)
                    ->orWhereNull('prefixes.still_announced');
            })
            ->select('prefixes.id')
            ->get()
            ->pluck('id')
            ->unique()
            ->toArray();

        // Filter further to remove prefixes that have multiple parents.
        $has_no_other_parents = [];

        foreach($child_subnets as $x) {

            $other_parents = Supernets::join('prefixes', 'supernet_id', '=', 'prefixes.id')
                ->where([['prefixes.still_announced', '=', 1], ['subnet_id', '=', $x], ['supernet_id', '!=', $this->id]])
                ->count();

            if($other_parents === 0) {
                $has_no_other_parents[] = $x;
            }

        }
        
        // Return the difference
        return $has_no_other_parents;
    }

    public function delete() {

        $children = $this->getTrueChildPrefixIdentities();

        Log::warning("Deletion for $this->address/$this->mask (#$this->id) running. Children to remove: " . sizeof($children));

        foreach($children as $x) {

            if($next_prefix = Prefixes::find($x)){
                $next_prefix->delete();
            }
        }

        // Remove or downgrade associated constructs.
        
        PrefixGoliathFlag::where('prefix_id', '=', $this->id)->where(function($x) {
            $x->whereNotNull('region_id')->orWhereNotNull('subregion_id')->orWhereNotNull('metro_id');
        })->delete();

        PrefixGoliathFlag::where('prefix_id', '=', $this->id)->update([
            'flag_synproxy'         => 0,
            'flag_heuristics'       => 1,
            'flag_heuristics_level' => 3,
        ]);

        PrefixGoliathInline::where('prefix_id', '=', $this->id)->update([
            'forced' => 0
        ]);

        PrefixGoliathFirewall::where('prefix_id', '=', $this->id)->delete();
        PrefixGoliathLoose::where('prefix_id', '=', $this->id)->delete();

        Log::warning("Deleting " . Supernets::where('subnet_id', '=', $this->id)->count() . " supernet entries for $this->address/$this->mask (#$this->id).");

        Supernets::where('subnet_id', '=', $this->id)->delete();

        parent::delete();
    }

    public function getPrefixOrganisations() {

        // All supernet entries relevant.
        $supernets = Supernets::where('subnet_id', '=', $this->id)->get();

        // Look for an overlap between these supernets and a subtenancy entry.
        $indirect_entries = PrefixSubtenants::whereIn('prefix_id', $supernets->pluck('supernet_id'))->where('also_subnets', '=', 1)->get()->pluck('subtenant_id');
        $direct_entries = PrefixSubtenants::where('prefix_id', '=', $this->id)->get()->pluck('subtenant_id');

        return array_unique([ ...$indirect_entries, ...$direct_entries ]);
    }

}
