<?php

namespace App\Helpers;

use Carbon\Carbon;
use Illuminate\Http\Request;

use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

use App\Helpers\Utility\UtilPrefix;
use App\Models\ApiToken;
use App\Models\ApiTokenBearer;
use App\Models\ApiTokenPermission;
use App\Models\ApiTokenWhitelist;
use Illuminate\Support\Facades\Redis;

class Security
{
    public static function real_ip() {
        $potential_ip_headers = [
            'HTTP_CLIENT_IP',
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_FORWARDED',
            'HTTP_X_CLUSTER_CLIENT_IP',
            'HTTP_FORWARDED_FOR',
            'HTTP_FORWARDED',
            'REMOTE_ADDR'
        ];
    
        foreach ($potential_ip_headers as $key){
            if (array_key_exists($key, $_SERVER) === true) {
                foreach (explode(',', $_SERVER[$key]) as $ip){
                    $ip = trim($ip); // Clean it up - It will probably be clean but security shouldn't predicate on probably.
                    
                    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false){
                        return $ip;
                    }
                }
            }
        }
        
        // Should we be unable to find anything, default to what Laravel will give us.
        return request()->ip();
    }

    // This determines whether any configurated MFA should be bypassed - Only happens for the internal VPN
    public static function mfa_bypassed() {
        $blocks = config('security.mfa_bypass_blocks', []);

        if(empty($blocks)) {
            return false;
        }

        foreach(array_unique([request()->ip(), static::real_ip()]) as $ip) {
            foreach($blocks as $block) {

                if(Str::contains($ip, ':') !== Str::contains($block, ':')) {
                    continue;
                }

                if(UtilPrefix::ip_in_block($ip, $block, true)) {
                    return true;
                }
            }
        }

        return false;
    }

    public static function valid_token_information(Request $_request) {
        if($token_valid = ApiToken::where([
            ['identifier', '=', $_request->input('identifier')],
        ])->first()) {

            if(Hash::check($_request->input('token'), $token_valid->token)) {

                if(ApiTokenWhitelist::whereIn('address', [Security::real_ip(), '0.0.0.0'])->where('token_id', '=', $token_valid->id)->exists()) {
                    return $token_valid->id;
                }
            }
        }

        return false;
    }

    public static function bearer_token_organisation_id(Request $_request) {
        // Get the bearer from the header if possible...
        $unchecked_authorisation_header = $_request->header('Authorization', $_request->header('Authorisation', false));

        // Check if we got anything, and if so, IS it a bearer...
        if($unchecked_authorisation_header && str_contains($unchecked_authorisation_header, 'Bearer')) {
 
            $bearer = preg_split('/\s+/', $unchecked_authorisation_header)[1];
 
            // Now check whether we've got a token for that...
            if($bearer_valid = ApiTokenBearer::leftJoin('api_tokens', 'api_token_bearer.token_id', '=', 'api_tokens.id')->where('bearer', '=', $bearer)->whereRaw('expiration_at > now()')->first()) {
                return $bearer_valid->organisation_id;
            }
        }

        return false;
    }

    public static function valid_bearer_token(Request $_request, ...$_perms) {
         // Get the bearer from the header if possible...
         $unchecked_authorisation_header = $_request->header('Authorization', $_request->header('Authorisation', false));

         $_perms = array_merge([], ...$_perms);

         // Check if we got anything, and if so, IS it a bearer...
         if($unchecked_authorisation_header && str_contains($unchecked_authorisation_header, 'Bearer')) {

             $bearer = preg_split('/\s+/', $unchecked_authorisation_header)[1];

             $cache_lookup_key = "api-bearer-$bearer";

             // Check if we've recently resolved this bearer - Otherwise look it up and remember it until it expires.
             $token_id = (int) Redis::get($cache_lookup_key) ?: false;

             if($token_id === false && ($bearer_valid = ApiTokenBearer::where('bearer', '=', $bearer)->whereRaw('expiration_at > now()')->first())) {
                $token_id = $bearer_valid->token_id;

                // The cache must never outlive the bearer itself, so it is capped to the seconds the bearer has left.
                Redis::setex($cache_lookup_key, (int) max(1, min(300, Carbon::now()->diffInSeconds(Carbon::parse($bearer_valid->expiration_at), false))), $token_id);
             }

             // Now check whether we've got a token for that...
             if($token_id !== false) {

                $cache_bearer_key = "api-whitelist-$token_id:" . Security::real_ip();

                // Check if we've recently checked this bearer token against the whitelist - If we have, let it pass through.
                $is_whitelisted = Redis::get($cache_bearer_key) ?: ApiTokenWhitelist::whereIn('address', [Security::real_ip(), '0.0.0.0'])->where('token_id', '=', $token_id)->exists();

                 if($is_whitelisted) {

                    Redis::setex($cache_bearer_key, 300, 1);

                    $cache_token_key = "api-token-$token_id";

                     // Now check whether a token is currently active...
                     if(Redis::get($cache_token_key) ?: ApiToken::find($token_id)) {

                         Redis::setex($cache_token_key, 300, 1);

                         // Check whether all permissions required can be fetched
                         $proceed = false;

                         if(count($_perms) == 0) {
                            $proceed = true;
                         }
                         else {
                            $sorted_perms = $_perms;
                            sort($sorted_perms);

                            $cache_perms_key = "api-perms-$token_id-" . md5(implode(',', $sorted_perms));

                            if(Redis::get($cache_perms_key)) {
                                $proceed = true;
                            }
                            elseif(count($_perms) == (ApiTokenPermission::leftJoin('api_scopes', 'api_token_permissions.api_scopes_id', '=', 'api_scopes.id')->whereIn('name', $_perms)->where('api_token_id', '=', $token_id)->count())) {
                                $proceed = true;

                                Redis::setex($cache_perms_key, 300, 1);
                            }
                         }

                         if($proceed) {
                            return [$bearer, $token_id, true];
                         }
                     }
                     else {
                        \Log::debug("Inactive Token.");
                        \Log::debug(str_contains($unchecked_authorisation_header, 'Bearer'));
                        \Log::debug($_request->header());
                        \Log::debug(Security::real_ip());

                        return ['INACTIVE', $token_id, false];
                     }
                }
            }
            else if($bearer_valid = ApiTokenBearer::where('bearer', '=', $bearer)->whereRaw('expiration_at < now()')->first()) {
                \Log::debug("Expired Token.");
                \Log::debug(str_contains($unchecked_authorisation_header, 'Bearer'));
                \Log::debug($_request->header());
                \Log::debug(Security::real_ip());
                
                 return ['EXPIRED', $bearer_valid->token_id, false];
            }
        }

        \Log::debug("Invalid Token.");
        \Log::debug($_perms);
        \Log::debug(str_contains($unchecked_authorisation_header, 'Bearer'));
        \Log::debug($_request->header());
        \Log::debug(Security::real_ip());

        return ['INVALID', 0, false];
    }
}
?>